yarramate 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/adapters/visual/session-server.js +27 -1
  2. package/dist/adapters/visual/workspace-model.d.ts +16 -3
  3. package/dist/adapters/visual/workspace-model.js +12 -5
  4. package/dist/ask-command.js +25 -16
  5. package/dist/catalogue-sources.d.ts +20 -0
  6. package/dist/catalogue-sources.js +39 -0
  7. package/dist/check-command.js +38 -9
  8. package/dist/cli-support.d.ts +8 -1
  9. package/dist/cli-support.js +5 -1
  10. package/dist/cli.d.ts +10 -0
  11. package/dist/cli.js +17 -1
  12. package/dist/design-command.js +21 -9
  13. package/dist/export-command.js +41 -5
  14. package/dist/import-command.d.ts +16 -0
  15. package/dist/import-command.js +156 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/interrogate-command.d.ts +82 -1
  19. package/dist/interrogate-command.js +195 -29
  20. package/dist/interrogation-entry.d.ts +1 -1
  21. package/dist/interrogation-entry.js +1 -1
  22. package/dist/projection.d.ts +47 -0
  23. package/dist/projection.js +126 -0
  24. package/dist/visual-app-lib/editor.js +26771 -26713
  25. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +16 -3
  26. package/dist/visual-app-lib/types/interrogate-command.d.ts +82 -1
  27. package/dist/visual-app-lib/types/projection.d.ts +47 -0
  28. package/dist/visual-app-lib/types/workspace.d.ts +15 -0
  29. package/dist/workbook-entry.d.ts +16 -0
  30. package/dist/workbook-entry.js +16 -0
  31. package/dist/workbook-import-entry.d.ts +30 -0
  32. package/dist/workbook-import-entry.js +30 -0
  33. package/dist/workbook-merge.d.ts +77 -0
  34. package/dist/workbook-merge.js +108 -0
  35. package/dist/workbook-operations.d.ts +9 -0
  36. package/dist/workbook-operations.js +139 -0
  37. package/dist/workbook-read.d.ts +43 -0
  38. package/dist/workbook-read.js +314 -0
  39. package/dist/workbook-xlsx.d.ts +54 -0
  40. package/dist/workbook-xlsx.js +228 -0
  41. package/dist/workbook.d.ts +32 -0
  42. package/dist/workbook.js +308 -0
  43. package/dist/workspace.d.ts +15 -0
  44. package/dist/workspace.js +1 -0
  45. package/docs/CONSUMING-YARRAMATE.md +127 -0
  46. package/package.json +9 -1
  47. package/schema/yarramate-interrogation-report.schema.json +9 -0
  48. package/schema/yarramate-question-catalogue.schema.json +2 -3
  49. package/schema/yarramate-workspace.schema.json +4 -0
@@ -124,6 +124,28 @@ const shippedCatalogue = (() => {
124
124
  return undefined;
125
125
  }
126
126
  })();
127
+ /**
128
+ * The shipped catalogue plus the ones the workspace carries (#345, ADR 0129).
129
+ *
130
+ * A catalogue that cannot be read is SKIPPED rather than failing the session,
131
+ * which is what the shipped one already did: an interrogation overlay is a
132
+ * garnish on a model, and losing a session over it would be worse than losing
133
+ * the questions. `check` is where a broken catalogue is refused.
134
+ */
135
+ const catalogueSetFor = (workspace) => {
136
+ if (shippedCatalogue === undefined)
137
+ return [];
138
+ const carried = [];
139
+ for (const path of workspace.questions ?? []) {
140
+ try {
141
+ carried.push({ path, source: readFileSync(path, "utf8") });
142
+ }
143
+ catch {
144
+ // Skipped, as above.
145
+ }
146
+ }
147
+ return [shippedCatalogue, ...carried];
148
+ };
127
149
  /**
128
150
  * Takes one session terminal, whatever caused it: a reviewer's End, a child
129
151
  * that failed, a browser that never came back, a cancelling main agent, or a
@@ -612,7 +634,11 @@ export const startVisualServer = async (options) => {
612
634
  // that. A staged view operation pins against these (ADR 0103), and a
613
635
  // projection missing from the map is one the commit will create.
614
636
  projectionDigests: projectionDigestsNow(),
615
- }, shippedCatalogue);
637
+ // The shipped catalogue plus whatever this workspace carries (#345),
638
+ // so the pane asks the interview `design` asks over the same files.
639
+ // Read on each recompile rather than once per process, because a
640
+ // consultant authoring a question mid-session is the whole point.
641
+ }, catalogueSetFor(resolvedWorkspace));
616
642
  // Closures below retain this array, so refresh its contents without
617
643
  // replacing the identity the session started with.
618
644
  views.splice(0, views.length, ...workspaceModel.views);
@@ -57,10 +57,20 @@ export interface DismissedQuestion {
57
57
  export declare const interrogationOverlayOf: (compiled: {
58
58
  readonly graph: SemanticGraph;
59
59
  readonly profileContext: ResolvedProfileContext;
60
- }, catalogue: {
60
+ },
61
+ /**
62
+ * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
63
+ * A set rather than one document so the pane asks the same interview the
64
+ * CLI does over the same workspace: an editor showing fewer questions than
65
+ * `design` does over the same files is a disagreement with no symptom.
66
+ */
67
+ catalogue: {
61
68
  readonly path: string;
62
69
  readonly source: string;
63
- },
70
+ } | readonly {
71
+ readonly path: string;
72
+ readonly source: string;
73
+ }[],
64
74
  /**
65
75
  * What the host has already dealt with (#328). Evaluation is unchanged and
66
76
  * the model is untouched: this decides only what the pane draws, because a
@@ -83,7 +93,10 @@ export declare const renderedWorkspaceOf: (compiled: {
83
93
  }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary" | "interrogation">, catalogue?: {
84
94
  readonly path: string;
85
95
  readonly source: string;
86
- }, dismissed?: readonly DismissedQuestion[]) => {
96
+ } | readonly {
97
+ readonly path: string;
98
+ readonly source: string;
99
+ }[], dismissed?: readonly DismissedQuestion[]) => {
87
100
  readonly model: VisualRenderedModel;
88
101
  readonly views: readonly VisualViewSummary[];
89
102
  };
@@ -6,7 +6,7 @@ import { projectGraphForCanvas } from "../../graph-projection.js";
6
6
  import { DEFAULT_PROJECTION_DIRECTORY } from "./view-identity.js";
7
7
  import { evaluateProjection, explainProjection } from "../../projection.js";
8
8
  import { kindLabelOf } from "../../kind-label.js";
9
- import { evaluateCatalogue, loadQuestionCatalogue, } from "../../interrogate-command.js";
9
+ import { evaluateCatalogue, composeCatalogues, } from "../../interrogate-command.js";
10
10
  /**
11
11
  * What a workspace looks like to the editor, however the editor is being run
12
12
  * (#252).
@@ -46,7 +46,14 @@ export const kindOptionsOf = (lineages) => [...lineages.keys()].map((id) => ({
46
46
  * them would read as five, and the reviewer counting boxes would find three.
47
47
  */
48
48
  export const conceptCountOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.filter(({ type }) => type === "concept").length;
49
- export const interrogationOverlayOf = (compiled, catalogue,
49
+ export const interrogationOverlayOf = (compiled,
50
+ /**
51
+ * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
52
+ * A set rather than one document so the pane asks the same interview the
53
+ * CLI does over the same workspace: an editor showing fewer questions than
54
+ * `design` does over the same files is a disagreement with no symptom.
55
+ */
56
+ catalogue,
50
57
  /**
51
58
  * What the host has already dealt with (#328). Evaluation is unchanged and
52
59
  * the model is untouched: this decides only what the pane draws, because a
@@ -54,10 +61,10 @@ export const interrogationOverlayOf = (compiled, catalogue,
54
61
  * a pane embedded in it.
55
62
  */
56
63
  dismissed = []) => {
57
- const loaded = loadQuestionCatalogue(catalogue);
58
- if (!loaded.ok)
64
+ const composed = composeCatalogues(Array.isArray(catalogue) ? catalogue : [catalogue]);
65
+ if (!composed.ok)
59
66
  return undefined;
60
- const report = evaluateCatalogue(loaded.catalogue, compiled.graph, compiled.profileContext);
67
+ const report = evaluateCatalogue(composed.composed.catalogue, compiled.graph, compiled.profileContext, undefined, composed.composed.catalogues);
61
68
  const dismissedEverywhere = new Set(dismissed
62
69
  .filter(({ subject }) => subject === undefined)
63
70
  .map(({ questionId }) => questionId));
@@ -9,7 +9,8 @@ import { runCheckCommand } from './check-command.js';
9
9
  import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
10
10
  import { compileWorkspaceWithProfileContext, } from './compiler.js';
11
11
  import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
12
- import { evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, } from './interrogate-command.js';
12
+ import { catalogueSources } from './catalogue-sources.js';
13
+ import { evaluateCatalogue, composeCatalogues, renderInterrogationReport, } from './interrogate-command.js';
13
14
  import { buildNextSubjects, coverageClause, } from './next-command.js';
14
15
  import { conceptKinds, relationshipPolicies, } from './profile.js';
15
16
  import { ARCHIMATE_RELATIONSHIPS_VERSION, CORE_CONCEPT_KIND_ORDER, PERMITTED_RELATIONSHIP_LETTERS, RELATIONSHIP_LETTERS, } from './archimate-relationships.generated.js';
@@ -468,13 +469,13 @@ export function runAskCommand(options, cwd) {
468
469
  const planned = buildNextSubjects(wholeWorkspace, compilation.graph, compilation.profileContext, evaluation.reports);
469
470
  const current = entries.filter(({ status }) => status === 'current');
470
471
  const retired = entries.filter(({ status }) => status === 'retired');
471
- const loadedCatalogue = loadQuestionCatalogue({
472
+ const composed = composeCatalogues(catalogueSources({
472
473
  path: shippedCataloguePath,
473
474
  source: readFileSync(shippedCataloguePath, 'utf8'),
474
- }, compilation.profileContext);
475
- if (!loadedCatalogue.ok)
476
- return failed(loadedCatalogue.diagnostics);
477
- const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations));
475
+ }, workspace, cwd), compilation.profileContext);
476
+ if (!composed.ok)
477
+ return failed(composed.diagnostics);
478
+ const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues);
478
479
  const result = {
479
480
  format: 'yarramate/ask-result/v1',
480
481
  workspace: workspace.id,
@@ -698,12 +699,12 @@ export function runAskCommand(options, cwd) {
698
699
  const resolvedCataloguePath = cataloguePath === undefined
699
700
  ? shippedCataloguePath
700
701
  : resolve(cwd, cataloguePath);
701
- const loadedCatalogue = loadQuestionCatalogue({
702
+ const composed = composeCatalogues(catalogueSources({
702
703
  path: cataloguePath ?? resolvedCataloguePath,
703
704
  source: readFileSync(resolvedCataloguePath, 'utf8'),
704
- }, compilation.profileContext);
705
- if (!loadedCatalogue.ok)
706
- return failed(loadedCatalogue.diagnostics);
705
+ }, workspace, cwd), compilation.profileContext);
706
+ if (!composed.ok)
707
+ return failed(composed.diagnostics);
707
708
  // The evidence overlay rides along for the one condition that
708
709
  // reads it (unchallenged-evidence); a workspace declaring no
709
710
  // evidence passes an overlay known to be empty.
@@ -718,13 +719,21 @@ export function runAskCommand(options, cwd) {
718
719
  evidenceObservations.push(...loaded.evidence.observations);
719
720
  }
720
721
  const report = {
721
- ...evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext, evidenceObservations),
722
+ ...evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues),
722
723
  workspace: workspace.id,
723
724
  };
725
+ // Field by field, to fix key ORDER in the emitted JSON. Every optional
726
+ // field has to be threaded through explicitly, which is why `catalogues`
727
+ // is here: a copier like this drops a new field silently and the only
728
+ // symptom is an absent one, which reads as "did not apply" rather than
729
+ // as "was lost".
724
730
  const ordered = {
725
731
  format: report.format,
726
732
  workspace: report.workspace,
727
733
  catalogue: report.catalogue,
734
+ ...(report.catalogues === undefined
735
+ ? {}
736
+ : { catalogues: report.catalogues }),
728
737
  semantics: report.semantics,
729
738
  summary: report.summary,
730
739
  waves: report.waves,
@@ -993,12 +1002,12 @@ export function runAskCommand(options, cwd) {
993
1002
  const resolvedCataloguePath = cataloguePath === undefined
994
1003
  ? shippedCataloguePath
995
1004
  : resolve(cwd, cataloguePath);
996
- const loadedCatalogue = loadQuestionCatalogue({
1005
+ const composed = composeCatalogues(catalogueSources({
997
1006
  path: cataloguePath ?? resolvedCataloguePath,
998
1007
  source: readFileSync(resolvedCataloguePath, 'utf8'),
999
- }, compilation.profileContext);
1000
- if (!loadedCatalogue.ok)
1001
- return failed(loadedCatalogue.diagnostics);
1008
+ }, workspace, cwd), compilation.profileContext);
1009
+ if (!composed.ok)
1010
+ return failed(composed.diagnostics);
1002
1011
  // Loaded ahead of evaluation so the overlay feeds the one condition
1003
1012
  // that reads it (unchallenged-evidence), then reused for the
1004
1013
  // reconciliation summary below.
@@ -1012,7 +1021,7 @@ export function runAskCommand(options, cwd) {
1012
1021
  return failed(loaded.diagnostics);
1013
1022
  evidenceDocuments.push(loaded.evidence);
1014
1023
  }
1015
- const report = evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations));
1024
+ const report = evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues);
1016
1025
  const openQuestions = [];
1017
1026
  for (const wave of report.waves) {
1018
1027
  for (const question of wave.questions) {
@@ -0,0 +1,20 @@
1
+ import type { WorkspaceSource } from './compiler.js';
2
+ import type { ResolvedWorkspace } from './workspace.js';
3
+ export declare const shippedCataloguePath: string;
4
+ /** The base catalogue as a source, read from disk. */
5
+ export declare const shippedCatalogueSource: () => WorkspaceSource;
6
+ /**
7
+ * The catalogues a verb should compose: the base, then whatever the workspace
8
+ * carries (#345, ADR 0129).
9
+ *
10
+ * Built in ONE place because every verb that interviews has to make the same
11
+ * choice, and a verb that forgot the workspace half would silently ask fewer
12
+ * questions than the workspace declares - a failure with no symptom, which is
13
+ * the shape this repository keeps being bitten by.
14
+ *
15
+ * The base is REPLACED by `--catalogue` and ADDED TO by `questions:`. Those
16
+ * are different powers on purpose: a host controls the catalogue that is not
17
+ * in the workspace (#328), and a consultant adds to it mid-engagement without
18
+ * a product release.
19
+ */
20
+ export declare const catalogueSources: (base: WorkspaceSource, workspace: Pick<ResolvedWorkspace, 'questions'>, cwd: string) => readonly WorkspaceSource[];
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ // The catalogue ships inside the package, versioned with it, and harnesses
5
+ // never pass catalogue paths. The relative hop works from both src/ (dev) and
6
+ // dist/ (shipped). Defined ONCE: `design`, `ask` and `check` all need it, and
7
+ // three copies of a path constant is three chances for them to disagree about
8
+ // which catalogue is the base.
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ export const shippedCataloguePath = join(here, '..', 'catalogues', 'core-enrichment.yaml');
11
+ /** The base catalogue as a source, read from disk. */
12
+ export const shippedCatalogueSource = () => ({
13
+ path: shippedCataloguePath,
14
+ source: readFileSync(shippedCataloguePath, 'utf8'),
15
+ });
16
+ /**
17
+ * The catalogues a verb should compose: the base, then whatever the workspace
18
+ * carries (#345, ADR 0129).
19
+ *
20
+ * Built in ONE place because every verb that interviews has to make the same
21
+ * choice, and a verb that forgot the workspace half would silently ask fewer
22
+ * questions than the workspace declares - a failure with no symptom, which is
23
+ * the shape this repository keeps being bitten by.
24
+ *
25
+ * The base is REPLACED by `--catalogue` and ADDED TO by `questions:`. Those
26
+ * are different powers on purpose: a host controls the catalogue that is not
27
+ * in the workspace (#328), and a consultant adds to it mid-engagement without
28
+ * a product release.
29
+ */
30
+ export const catalogueSources = (base, workspace, cwd) => [
31
+ base,
32
+ // `?? []` because `questions` is optional on the published type: adding a
33
+ // required field to `ResolvedWorkspace` broke a consumer's production module
34
+ // once already.
35
+ ...(workspace.questions ?? []).map((path) => ({
36
+ path,
37
+ source: readFileSync(resolve(cwd, path), 'utf8'),
38
+ })),
39
+ ];
@@ -1,13 +1,15 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { catalogueSources, shippedCatalogueSource, } from './catalogue-sources.js';
3
+ import { composeCatalogues } from './interrogate-command.js';
2
4
  import { resolve } from 'node:path';
3
5
  import Ajv2020Module from 'ajv/dist/2020.js';
4
6
  import { parseDocument } from 'yaml';
5
7
  import { loadAdapterMapping, validateAdapterMappings, } from './adapter-mapping.js';
6
8
  import { checkResultJson, humanDiagnostics, resolveCliWorkspaceSources, sortDiagnostics, usage, } from './cli-support.js';
7
- import { compileWorkspace, withDiagnosticSubjects } from './compiler.js';
9
+ import { compileWorkspaceWithProfileContext, withDiagnosticSubjects, } from './compiler.js';
8
10
  import { checkCoreContract, loadCoreContract, } from './core-contract.js';
9
11
  import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
10
- import { loadProjection } from './projection.js';
12
+ import { loadProjection, projectionReferenceDiagnostics, } from './projection.js';
11
13
  import { reconcileEvidenceReports, } from './reconciliation.js';
12
14
  // `.default ?? module`, not a bare `.default`: NodeNext sees the raw CJS
13
15
  // `module.exports` and a bundler the unwrapped class. One shape for all of
@@ -138,13 +140,15 @@ export function runCheckCommand(options, cwd) {
138
140
  : humanDiagnostics(contractDiagnostics);
139
141
  return { exitCode: 1, stdout: output, stderr: '' };
140
142
  }
141
- const projectionDiagnostics = sortDiagnostics(resolved.projections.flatMap((path) => {
142
- const loaded = loadProjection({
143
- path,
144
- source: readFileSync(resolve(cwd, path), 'utf8'),
145
- });
146
- return loaded.ok ? [] : loaded.diagnostics;
143
+ const projectionSources = resolved.projections.map((path) => ({
144
+ path,
145
+ source: readFileSync(resolve(cwd, path), 'utf8'),
146
+ }));
147
+ const loadedProjections = projectionSources.map((source) => ({
148
+ source,
149
+ loaded: loadProjection(source),
147
150
  }));
151
+ const projectionDiagnostics = sortDiagnostics(loadedProjections.flatMap(({ loaded }) => loaded.ok ? [] : loaded.diagnostics));
148
152
  if (projectionDiagnostics.length > 0) {
149
153
  const output = json
150
154
  ? checkResultJson(false, projectionDiagnostics)
@@ -177,7 +181,7 @@ export function runCheckCommand(options, cwd) {
177
181
  : humanDiagnostics(mappingLoadDiagnostics);
178
182
  return { exitCode: 1, stdout: output, stderr: '' };
179
183
  }
180
- const result = compileWorkspace(coreSources);
184
+ const result = compileWorkspaceWithProfileContext(coreSources);
181
185
  const mappingValidation = result.ok
182
186
  ? validateAdapterMappings(result.graph, loadedMappings.flatMap((loaded) => loaded.ok ? [loaded.mapping] : []))
183
187
  : undefined;
@@ -190,9 +194,34 @@ export function runCheckCommand(options, cwd) {
190
194
  const evidenceDiagnostics = evidenceEvaluation === undefined || evidenceEvaluation.ok
191
195
  ? []
192
196
  : evidenceEvaluation.diagnostics;
197
+ // A catalogue the manifest declares is workspace content, so `check`
198
+ // refuses a broken one (#345, ADR 0129). Composed rather than checked one
199
+ // by one, because the refusals that matter are CROSS-catalogue: a wave
200
+ // declared twice, and a question naming a wave nothing in the set
201
+ // declares. Checking each file alone would miss both and would refuse the
202
+ // one thing the feature exists to allow, a question joining a wave another
203
+ // catalogue declared.
204
+ const catalogueDiagnostics = result.ok && resolved.questions.length > 0
205
+ ? (() => {
206
+ const composed = composeCatalogues(catalogueSources(shippedCatalogueSource(), resolved, cwd), result.profileContext);
207
+ return composed.ok ? [] : composed.diagnostics;
208
+ })()
209
+ : [];
210
+ // A projection is a document, and a query holds references the same way a
211
+ // relationship does. Checked HERE rather than with the projection's own
212
+ // schema load above, because a reference can only be resolved against a
213
+ // model that compiled: reporting dangling names out of a workspace that
214
+ // does not build would bury the real failure under its consequences.
215
+ const referenceDiagnostics = result.ok
216
+ ? loadedProjections.flatMap(({ source, loaded }) => loaded.ok
217
+ ? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext)
218
+ : [])
219
+ : [];
193
220
  const optionalDiagnostics = sortDiagnostics([
194
221
  ...mappingDiagnostics,
195
222
  ...evidenceDiagnostics,
223
+ ...referenceDiagnostics,
224
+ ...catalogueDiagnostics,
196
225
  ]);
197
226
  const ok = result.ok && optionalDiagnostics.length === 0;
198
227
  // Published results name the subject a diagnostic is about wherever its
@@ -7,7 +7,7 @@ export interface CliResult {
7
7
  export declare const isMainModule: (moduleUrl: string, entrypoint: string | undefined) => boolean;
8
8
  export declare const packageVersion: string;
9
9
  export declare const versionResult: (binary: string) => CliResult;
10
- export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
10
+ export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
11
11
  export declare const diagnosticJson: (diagnostics: unknown) => string;
12
12
  export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counted?: {
13
13
  readonly documents: number;
@@ -30,6 +30,13 @@ export declare const resolveCliWorkspaceSources: (paths: readonly string[], cwd:
30
30
  readonly contracts: readonly string[];
31
31
  /** Pattern documents, which ride in `paths` and are not documents. */
32
32
  readonly patterns: readonly string[];
33
+ /**
34
+ * Question catalogues the workspace carries (#345). Handed over rather
35
+ * than resolved-and-dropped, which is the #268 failure recorded below:
36
+ * a category a manifest declares and no verb receives is ignored with
37
+ * no symptom.
38
+ */
39
+ readonly questions: readonly string[];
33
40
  } | {
34
41
  readonly ok: false;
35
42
  readonly diagnostics: readonly Diagnostic[];
@@ -22,7 +22,7 @@ export const versionResult = (binary) => ({
22
22
  stdout: `${binary} ${packageVersion}\n`,
23
23
  stderr: '',
24
24
  });
25
- export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
25
+ export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
26
26
  export const diagnosticJson = (diagnostics) => `${JSON.stringify({
27
27
  format: 'yarramate/diagnostic-result/v1',
28
28
  diagnostics,
@@ -51,6 +51,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
51
51
  evidence: [],
52
52
  contracts: [],
53
53
  patterns: [],
54
+ questions: [],
54
55
  };
55
56
  }
56
57
  const manifestPath = paths[0];
@@ -62,6 +63,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
62
63
  evidence: [],
63
64
  contracts: [],
64
65
  patterns: [],
66
+ questions: [],
65
67
  };
66
68
  }
67
69
  const source = readFileSync(resolve(cwd, manifestPath), 'utf8');
@@ -73,6 +75,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
73
75
  evidence: [],
74
76
  contracts: [],
75
77
  patterns: [],
78
+ questions: [],
76
79
  };
77
80
  }
78
81
  const loaded = loadWorkspaceManifest({ path: manifestPath, source }, cwd);
@@ -99,6 +102,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
99
102
  evidence: loaded.workspace.evidence,
100
103
  contracts: loaded.workspace.contracts,
101
104
  patterns: loaded.workspace.patterns,
105
+ questions: loaded.workspace.questions ?? [],
102
106
  }
103
107
  : { ok: false, diagnostics: loaded.diagnostics };
104
108
  };
package/dist/cli.d.ts CHANGED
@@ -3,3 +3,13 @@ import { type CliResult } from './cli-support.js';
3
3
  export type { CliResult } from './cli-support.js';
4
4
  export declare const deriveInitId: (directory: string) => string;
5
5
  export declare function runCli(args: readonly string[], cwd?: string): CliResult;
6
+ /**
7
+ * Every verb, including the one that cannot be synchronous.
8
+ *
9
+ * `import xlsx` has to inflate a workbook, and the only inflater available
10
+ * everywhere this runs is `DecompressionStream`, which is async. Widening
11
+ * `runCli` to return a promise would change the type every one of its callers
12
+ * reads - the readers half of the rule in CONTRIBUTING.md - so the async verb
13
+ * gets its own entry and `runCli` keeps its signature.
14
+ */
15
+ export declare function runCliAsync(args: readonly string[], cwd?: string): Promise<CliResult>;
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { compileWorkspace } from './compiler.js';
5
5
  import { diagnosticJson, isMainModule, usage, versionResult, } from './cli-support.js';
6
6
  import { runAskCommand } from './ask-command.js';
7
7
  import { runCheckCommand } from './check-command.js';
8
+ import { runImportCommand } from './import-command.js';
8
9
  import { runExportCommand } from './export-command.js';
9
10
  import { runApplyCommand } from './apply-cli.js';
10
11
  import { runDesignCommand } from './design-command.js';
@@ -227,8 +228,23 @@ export function runCli(args, cwd = process.cwd()) {
227
228
  }
228
229
  return { exitCode: 2, stdout: '', stderr: usage };
229
230
  }
231
+ /**
232
+ * Every verb, including the one that cannot be synchronous.
233
+ *
234
+ * `import xlsx` has to inflate a workbook, and the only inflater available
235
+ * everywhere this runs is `DecompressionStream`, which is async. Widening
236
+ * `runCli` to return a promise would change the type every one of its callers
237
+ * reads - the readers half of the rule in CONTRIBUTING.md - so the async verb
238
+ * gets its own entry and `runCli` keeps its signature.
239
+ */
240
+ export async function runCliAsync(args, cwd = process.cwd()) {
241
+ const [command, ...options] = args;
242
+ if (command === 'import')
243
+ return runImportCommand(options, cwd);
244
+ return runCli(args, cwd);
245
+ }
230
246
  if (isMainModule(import.meta.url, process.argv[1])) {
231
- const result = runCli(process.argv.slice(2));
247
+ const result = await runCliAsync(process.argv.slice(2));
232
248
  process.stdout.write(result.stdout);
233
249
  process.stderr.write(result.stderr);
234
250
  process.exitCode = result.exitCode;
@@ -5,7 +5,7 @@ import { parseDocument } from 'yaml';
5
5
  import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
6
6
  import { compileWorkspaceWithProfileContext, } from './compiler.js';
7
7
  import { loadEvidence } from './evidence.js';
8
- import { evaluateCatalogue, loadQuestionCatalogue, renderQuestion, } from './interrogate-command.js';
8
+ import { composeCatalogues, evaluateCatalogue, renderQuestion, } from './interrogate-command.js';
9
9
  import { evaluateProjection } from './projection.js';
10
10
  import { renderBrief } from './brief.js';
11
11
  import { loadWorkspaceManifest } from './workspace.js';
@@ -205,12 +205,23 @@ export function runDesignCommand(options, cwd) {
205
205
  })));
206
206
  if (!compilation.ok)
207
207
  return failed(compilation.diagnostics);
208
- const loadedCatalogue = loadQuestionCatalogue({
209
- path: cataloguePath ?? resolvedCataloguePath,
210
- source: readFileSync(resolvedCataloguePath, 'utf8'),
211
- }, compilation.profileContext);
212
- if (!loadedCatalogue.ok)
213
- return failed(loadedCatalogue.diagnostics);
208
+ // The base, then whatever the workspace carries (#345, ADR 0129). The
209
+ // base is REPLACED by `--catalogue` and ADDED TO by `questions:`, which is
210
+ // what lets a consultant author a question mid-engagement with no product
211
+ // release while a host still controls the catalogue that is not in the
212
+ // workspace.
213
+ const composed = composeCatalogues([
214
+ {
215
+ path: cataloguePath ?? resolvedCataloguePath,
216
+ source: readFileSync(resolvedCataloguePath, 'utf8'),
217
+ },
218
+ ...(workspace.questions ?? []).map((path) => ({
219
+ path,
220
+ source: readFileSync(resolve(cwd, path), 'utf8'),
221
+ })),
222
+ ], compilation.profileContext);
223
+ if (!composed.ok)
224
+ return failed(composed.diagnostics);
214
225
  // The evidence overlay rides along for the one condition that reads
215
226
  // it (unchallenged-evidence). A workspace declaring no evidence
216
227
  // passes an empty overlay — known to be empty, which keeps that
@@ -235,8 +246,9 @@ export function runDesignCommand(options, cwd) {
235
246
  };
236
247
  }
237
248
  }
238
- const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceObservations);
239
- const askPlainById = new Map(loadedCatalogue.catalogue.questions.flatMap((question) => question.askPlain === undefined
249
+ const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues);
250
+ // Keyed by the QUALIFIED id, matching what the report now carries.
251
+ const askPlainById = new Map(composed.composed.catalogue.questions.flatMap((question) => question.askPlain === undefined
240
252
  ? []
241
253
  : [[question.id, question.askPlain]]));
242
254
  const step = selectStep(report, subjectFilter, askPlainById);
@@ -1,16 +1,18 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
2
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { parseDocument } from 'yaml';
6
7
  import { renderBrief } from './brief.js';
7
8
  import { deriveChangedSubjects } from './changed.js';
8
- import { humanDiagnostics, usage } from './cli-support.js';
9
+ import { humanDiagnostics, packageVersion, usage, } from './cli-support.js';
9
10
  import { compileWorkspaceWithProfileContext, } from './compiler.js';
10
11
  import { serializeSemanticGraph } from './graph.js';
11
12
  import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
12
13
  import { evaluateProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
13
14
  import { buildRtm, renderRtmMarkdown } from './rtm.js';
15
+ import { workbookFrom } from './workbook.js';
14
16
  import { loadWorkspaceManifest } from './workspace.js';
15
17
  // The adapter stays a separate process behind the verb: the core never
16
18
  // imports adapter code (the adapter-runtime-dependency exclusion), it
@@ -74,7 +76,7 @@ const parseExportOptions = (options) => {
74
76
  export function runExportCommand(options, cwd) {
75
77
  const [kind, ...rest] = options;
76
78
  if (kind === undefined ||
77
- !['graph', 'markdown', 'briefs', 'rtm', 'likec4'].includes(kind)) {
79
+ !['graph', 'markdown', 'briefs', 'rtm', 'likec4', 'xlsx'].includes(kind)) {
78
80
  return { exitCode: 2, stdout: '', stderr: usage };
79
81
  }
80
82
  const parsed = parseExportOptions(rest);
@@ -128,7 +130,8 @@ export function runExportCommand(options, cwd) {
128
130
  parsed.json ||
129
131
  (usesChanged && (kind === 'graph' || kind === 'rtm')) ||
130
132
  (parsed.budget !== undefined && kind !== 'briefs') ||
131
- ((kind === 'briefs' || kind === 'rtm') && parsed.out === undefined)) {
133
+ ((kind === 'briefs' || kind === 'rtm' || kind === 'xlsx') &&
134
+ parsed.out === undefined)) {
132
135
  return { exitCode: 2, stdout: '', stderr: usage };
133
136
  }
134
137
  try {
@@ -149,14 +152,17 @@ export function runExportCommand(options, cwd) {
149
152
  if (!loadedWorkspace.ok)
150
153
  return failed(loadedWorkspace.diagnostics);
151
154
  const workspace = loadedWorkspace.workspace;
152
- const compilation = compileWorkspaceWithProfileContext([
155
+ // Named rather than inlined so the workbook can pin its digests against
156
+ // exactly the bytes that compiled, the way a visual commit does (#355).
157
+ const sources = [
153
158
  ...workspace.profiles,
154
159
  ...workspace.patterns,
155
160
  ...workspace.documents,
156
161
  ].map((path) => ({
157
162
  path,
158
163
  source: readFileSync(resolve(cwd, path), 'utf8'),
159
- })));
164
+ }));
165
+ const compilation = compileWorkspaceWithProfileContext(sources);
160
166
  if (!compilation.ok)
161
167
  return failed(compilation.diagnostics);
162
168
  if (kind === 'rtm') {
@@ -246,6 +252,36 @@ export function runExportCommand(options, cwd) {
246
252
  return failed(loadedProjection.diagnostics);
247
253
  result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
248
254
  }
255
+ if (kind === 'xlsx') {
256
+ // A workbook an architect can work in (#355). It takes a PROJECTION,
257
+ // like markdown and briefs do, which is what gives it version selection
258
+ // for free: a projection query already has a `states` facet, so
259
+ // "export the target state" is an existing capability rather than a
260
+ // flag competing with it.
261
+ const bytes = workbookFrom(result, {
262
+ workspace: workspace.id,
263
+ yarramateVersion: packageVersion,
264
+ sourceDigests: Object.fromEntries(sources.map(({ path, source }) => [
265
+ path,
266
+ createHash('sha256').update(source, 'utf8').digest('hex'),
267
+ ])),
268
+ conceptKinds: [
269
+ ...compilation.profileContext.conceptKindLineages.keys(),
270
+ ].sort(),
271
+ relationshipKinds: [
272
+ ...compilation.profileContext.relationshipKindLineages.keys(),
273
+ ].sort(),
274
+ statuses: ['planned', 'current', 'retired'],
275
+ });
276
+ const outPath = resolve(cwd, parsed.out);
277
+ mkdirSync(dirname(outPath), { recursive: true });
278
+ writeFileSync(outPath, bytes);
279
+ return {
280
+ exitCode: 0,
281
+ stdout: `Wrote workbook to ${parsed.out}\n`,
282
+ stderr: '',
283
+ };
284
+ }
249
285
  if (kind === 'markdown') {
250
286
  const rendered = renderProjectionMarkdown(result, compilation.profileContext);
251
287
  if (parsed.out === undefined) {