Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions calm-models/src/types/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { CALM_NARRATIVE_DOCUMENT_TYPES_LIST, isNarrativeDocumentType } from './index';

describe('isNarrativeDocumentType', () => {
it.each(CALM_NARRATIVE_DOCUMENT_TYPES_LIST)('accepts %s', (type) => {
expect(isNarrativeDocumentType(type)).toBe(true);
});

it('rejects unsupported values', () => {
expect(isNarrativeDocumentType('architecture')).toBe(false);
expect(isNarrativeDocumentType(1)).toBe(false);
});
});
8 changes: 8 additions & 0 deletions calm-models/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export const CALM_DOCUMENT_TYPES_LIST = [

export type CalmDocumentType = (typeof CALM_DOCUMENT_TYPES_LIST)[number];

export const CALM_NARRATIVE_DOCUMENT_TYPES_LIST = ['knowledge', 'sad'] as const;

export type NarrativeDocumentType = (typeof CALM_NARRATIVE_DOCUMENT_TYPES_LIST)[number];

export function isNarrativeDocumentType(input: unknown): input is NarrativeDocumentType {
return typeof input === 'string' && CALM_NARRATIVE_DOCUMENT_TYPES_LIST.includes(input as NarrativeDocumentType);
}

export function isValidCalmDocumentType(input: string): input is CalmDocumentType {
return CALM_DOCUMENT_TYPES_LIST.some((type) => type === input);
}
Expand Down
39 changes: 32 additions & 7 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,32 +803,44 @@ calm workspace init my-system

#### `calm workspace add <file>`

Register a CALM document with the active workspace. By default the file is referenced at its current location on disk (no copying). Prompts interactively for document type and (manifest) name if they cannot be determined automatically.
Register a CALM JSON document or narrative Markdown document with the active workspace. By default the file is referenced at its current location on disk (no copying). Prompts interactively for document type and (manifest) name if they cannot be determined automatically.

```
calm workspace add <file> [--id <id>] [--type <type>] [--namespace <namespace>] [--copy]
calm workspace add <file> [--id <id>] [--type <type>] [--namespace <namespace>] [--copy] [--calm-hub-document-id <id> --ver <version> [--calm-hub-url <url>]]
```

| Option | Description |
|--------|-------------|
| `--id <id>` | Explicit manifest registration id. Overrides automatic resolution. |
| `--type <type>` | Document type. If omitted, an interactive dropdown is shown. One of: `pattern`, `architecture`, `interface`, `flow`, `control`, `schema`, `timeline`, `adr`. |
| `--namespace <namespace>` | CalmHub namespace to record in the manifest. If omitted, it is derived from the document `$id`. |
| `--type <type>` | Document type. If omitted, an interactive dropdown is shown. One of: `pattern`, `architecture`, `interface`, `flow`, `control`, `schema`, `timeline`, `adr`, `knowledge`, `sad`. |
| `--namespace <namespace>` | CalmHub namespace to record in the manifest. It is required for narrative Markdown and otherwise derived from the document `$id` when omitted. |
| `--copy` | Copy the file into the bundle's `files/` directory instead of referencing it in place. |
| `--calm-hub-document-id <id>` and `--ver <version>` | Recover an existing narrative document. Both options are required together. |
| `--calm-hub-url <url>` | Optional CalmHub URL used only for narrative recovery. It otherwise uses the configured URL. |

**Document `$id` handling.** `add` inspects the file's CalmHub `$id`:
**Narrative Markdown documents.** Use `--type knowledge` or `sad`. `add` reads YAML frontmatter. A non-empty `title` becomes the manifest name unless you supply `--id`. `--namespace` is required. The initial manifest version is `1.0.0`. Markdown has no CALM `$id` and is never rewritten.

To restore a removed narrative document without creating a new CalmHub document, supply its verified Hub id and version. The local Markdown must exactly match the stored Hub version.

**JSON document `$id` handling.** For JSON mapping documents, `add` inspects the file's CalmHub `$id`:
- **No `$id`** β†’ you are prompted interactively to build one from its components (see below); the `$id` is written into the file and the document is added.
- **Conformant `$id`** β†’ left untouched; the manifest namespace is derived from it.
- **Non-conformant `$id`** β†’ left as-is; a warning is printed and the document is still tracked, but it cannot be pushed to CalmHub until the `$id` is fixed (silently rewriting it would lose data for types that don't use CalmHub URLs, e.g. `flow`, `adr`, `timeline`).

**Manifest name resolution** (when `--id` is not given): the `title` field from the JSON file, else an interactive prompt.
**Manifest name resolution** (when `--id` is not given): the `title` field from the JSON file or Markdown frontmatter, else an interactive prompt.

```shell
# Interactive β€” prompts for type, builds the $id if needed, then the manifest name
calm workspace add ./architectures/payment-service.json

# Reference an already-conformant document without copying
calm workspace add ./architectures/payment-service.json --type architecture

# Register a narrative Markdown document; the frontmatter title becomes its manifest name
calm workspace add ./docs/payments-sad.md --type sad --namespace finos

# Restore an existing narrative document
calm workspace add ./docs/payments-sad.md --type sad --namespace finos --calm-hub-document-id 42 --ver 1.2.0
```

#### `calm workspace new [type] [name] [template]`
Expand Down Expand Up @@ -863,7 +875,9 @@ where `$TYPE` is one of `patterns`, `architectures`, `standards`, `interfaces`.

#### `calm workspace push`

Push every document in the workspace manifest to a CalmHub instance. Each document's identity β€” namespace, type, mapping id and **version** β€” comes from its `$id` (of the form `$BASE_URL/calm/namespaces/$NAMESPACE/$TYPE/$MAPPING_ID/versions/$VERSION`). Push **does not auto-bump**: it creates exactly the version each document declares. Documents without a well-formed mapping `$id` (or whose type has no CalmHub resource type) are skipped with a warning.
Push every document in the workspace manifest to a CalmHub instance. JSON mapping documents derive their identity β€” namespace, type, mapping id and **version** β€” from `$id` (of the form `$BASE_URL/calm/namespaces/$NAMESPACE/$TYPE/$MAPPING_ID/versions/$VERSION`). Push **does not auto-bump**: it creates exactly the version each document declares. Documents without a well-formed mapping `$id` (or whose type has no CalmHub resource type) are skipped with a warning.

Narrative Markdown documents use `--type knowledge` or `--type sad`. They require YAML frontmatter with a `title` and `--namespace`. The first push stores the Hub numeric document id, location, and version (`1.0.0`) in `workspace-manifest.json`. Later changes require `workspace bump`; the command updates the manifest version without rewriting the Markdown.

```
calm workspace push [--calm-hub-url <url>] [--fail-if-modified]
Expand All @@ -887,6 +901,17 @@ calm workspace push --calm-hub-url https://calmhub.example.com
calm workspace push --fail-if-modified # strict merge-time mode
```

```shell
# First-class document POC: add, publish, inspect, edit, bump, and publish again
calm workspace add ./docs/payments-sad.md --type sad --namespace finos
calm workspace push --calm-hub-url http://localhost:8080
calm workspace show # shows the published Hub location
calm workspace check --calm-hub-url http://localhost:8080
# Edit ./docs/payments-sad.md, then bump and publish the new version
calm workspace bump --minor --calm-hub-url http://localhost:8080
calm workspace push --calm-hub-url http://localhost:8080
```

#### `calm workspace check`

Check whether any tracked document has changed on disk relative to CalmHub but has **not** been version-bumped. Intended as a CI/PR gate β€” it **exits non-zero** when a bump is required, so a PR cannot merge with unversioned changes.
Expand Down
5 changes: 5 additions & 0 deletions cli/smoke/harness/hub-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ export function hubApi(baseUrl: string = SMOKE_HUB_URL) {
`${baseUrl}/calm/namespaces/${namespace}/${type}/${mapping}/versions/${version}`
);
},
async getNarrativeDocument(namespace: string, type: string, id: number, version: string): Promise<string> {
const body = await getJson(`${baseUrl}/api/calm/namespaces/${namespace}/documents/${type}/${id}/versions/${version}`);
if (typeof body.documentMarkdown !== 'string') throw new Error('Narrative document response has no documentMarkdown');
return body.documentMarkdown;
},
};
}
62 changes: 62 additions & 0 deletions cli/smoke/workspace-documents.smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import path from 'path';
import * as fs from 'fs';
import { execSync } from 'child_process';
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import { installPackedCli, type CliInstall } from '../src/test_helpers/cli-runner';
import { SMOKE_HUB_URL } from './global-setup';
import { hubApi } from './harness/hub-api';
import { hubDocId } from './harness/fixtures';

const CLI_ROOT = path.resolve(__dirname, '..');
const NS = 'smoke-workspace-documents';
const api = hubApi();

describe('workspace narrative-document POC', () => {
let cli: CliInstall;
let wsDir: string;
let documentPath: string;
let architecturePath: string;
let documentId: number;
const initial = '---\ntitle: Payments SAD\ndescription: Smoke document\n---\n# Payments\n';

async function run(args: string[]) {
return cli.run(args, { cwd: wsDir });
}

beforeAll(async () => {
cli = installPackedCli(CLI_ROOT, 'calm-smoke-workspace-documents');
wsDir = path.join(cli.tempDir, 'repo');
fs.mkdirSync(wsDir, { recursive: true });
execSync('git init', { cwd: wsDir, stdio: 'inherit' });
documentPath = path.join(wsDir, 'payments-sad.md');
fs.writeFileSync(documentPath, initial);
architecturePath = path.join(wsDir, 'payments.architecture.json');
fs.writeFileSync(architecturePath, JSON.stringify({
$schema: 'https://calm.finos.org/release/1.0/meta/calm.json',
$id: hubDocId(NS, 'architectures', 'payments', '1.0.0'),
title: 'Payments', nodes: [], relationships: [],
}, null, 2));
await cli.run(['hub', 'create', 'namespace', '--name', NS, '--description', 'workspace documents smoke', '-c', SMOKE_HUB_URL]);
}, 120_000);

afterAll(() => cli?.cleanup());

test('publishes, retrieves, bumps, and republishes a Markdown document', async () => {
await run(['workspace', 'init', 'documents']);
await run(['workspace', 'add', architecturePath, '--type', 'architecture', '--namespace', NS]);
await run(['workspace', 'add', documentPath, '--type', 'sad', '--namespace', NS]);
await run(['workspace', 'push', '--calm-hub-url', SMOKE_HUB_URL]);
expect(await api.listVersions(NS, 'architectures', 'payments')).toContain('1.0.0');

const manifestPath = path.join(wsDir, '.calm-workspace', 'bundles', 'documents', 'workspace-manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, { calmHubDocumentId: number }>;
documentId = manifest['Payments SAD'].calmHubDocumentId;
expect(await api.getNarrativeDocument(NS, 'sad', documentId, '1.0.0')).toBe(initial);

fs.writeFileSync(documentPath, initial.replace('# Payments', '# Updated payments'));
await expect(run(['workspace', 'check', '--calm-hub-url', SMOKE_HUB_URL])).rejects.toHaveProperty('exitCode', 1);
await run(['workspace', 'bump', '--minor', '--calm-hub-url', SMOKE_HUB_URL]);
await run(['workspace', 'push', '--calm-hub-url', SMOKE_HUB_URL]);
expect(await api.getNarrativeDocument(NS, 'sad', documentId, '1.1.0')).toContain('# Updated payments');
});
});
Loading
Loading