yarramate 1.6.0 → 1.8.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 (42) hide show
  1. package/dist/adapters/visual/session-server.js +30 -1
  2. package/dist/adapters/visual/workspace-model.d.ts +20 -3
  3. package/dist/adapters/visual/workspace-model.js +12 -5
  4. package/dist/artifact-coverage.d.ts +13 -0
  5. package/dist/artifact-coverage.js +58 -0
  6. package/dist/ask-command.js +25 -16
  7. package/dist/catalogue-sources.d.ts +20 -0
  8. package/dist/catalogue-sources.js +39 -0
  9. package/dist/check-command.js +38 -9
  10. package/dist/cli-support.d.ts +7 -0
  11. package/dist/cli-support.js +4 -0
  12. package/dist/cli.js +6 -1
  13. package/dist/compiler.d.ts +23 -0
  14. package/dist/compiler.js +23 -1
  15. package/dist/design-command.js +21 -9
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +2 -1
  18. package/dist/interrogate-command.d.ts +117 -1
  19. package/dist/interrogate-command.js +220 -34
  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/reconciliation.d.ts +21 -1
  25. package/dist/reconciliation.js +93 -2
  26. package/dist/visual-app-lib/editor.js +26788 -27093
  27. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +20 -3
  28. package/dist/visual-app-lib/types/compiler.d.ts +23 -0
  29. package/dist/visual-app-lib/types/interrogate-command.d.ts +117 -1
  30. package/dist/visual-app-lib/types/projection.d.ts +47 -0
  31. package/dist/visual-app-lib/types/workspace.d.ts +23 -0
  32. package/dist/workbook-import-entry.d.ts +30 -0
  33. package/dist/workbook-import-entry.js +30 -0
  34. package/dist/workbook.js +15 -0
  35. package/dist/workspace.d.ts +23 -0
  36. package/dist/workspace.js +36 -13
  37. package/docs/CONSUMING-YARRAMATE.md +63 -0
  38. package/package.json +5 -1
  39. package/schema/yarramate-interrogation-report.schema.json +9 -0
  40. package/schema/yarramate-question-catalogue.schema.json +32 -3
  41. package/schema/yarramate-reconciliation-report.schema.json +21 -1
  42. package/schema/yarramate-workspace.schema.json +8 -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
@@ -598,6 +620,9 @@ export const startVisualServer = async (options) => {
598
620
  compiledWorkspace = {
599
621
  graph: compiled.graph,
600
622
  profileContext: compiled.profileContext,
623
+ // Threaded whole (ADR 0131): a narrow copy here is how a slot
624
+ // question would silently never fire in the embedded pane.
625
+ patternMemberships: compiled.patternMemberships,
601
626
  };
602
627
  const workspaceModel = renderedWorkspaceOf(compiledWorkspace, views, {
603
628
  authority: rendered.authority,
@@ -612,7 +637,11 @@ export const startVisualServer = async (options) => {
612
637
  // that. A staged view operation pins against these (ADR 0103), and a
613
638
  // projection missing from the map is one the commit will create.
614
639
  projectionDigests: projectionDigestsNow(),
615
- }, shippedCatalogue);
640
+ // The shipped catalogue plus whatever this workspace carries (#345),
641
+ // so the pane asks the interview `design` asks over the same files.
642
+ // Read on each recompile rather than once per process, because a
643
+ // consultant authoring a question mid-session is the whole point.
644
+ }, catalogueSetFor(resolvedWorkspace));
616
645
  // Closures below retain this array, so refresh its contents without
617
646
  // replacing the identity the session started with.
618
647
  views.splice(0, views.length, ...workspaceModel.views);
@@ -4,6 +4,7 @@ import type { ResolvedWorkspace } from "../../workspace.js";
4
4
  import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.js";
5
5
  import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
6
6
  import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
7
+ import { type CataloguePatternMembership } from "../../interrogate-command.js";
7
8
  import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
8
9
  import type { VisualInterrogationOverlay, VisualRenderedModel } from "./wire.js";
9
10
  /**
@@ -57,10 +58,22 @@ export interface DismissedQuestion {
57
58
  export declare const interrogationOverlayOf: (compiled: {
58
59
  readonly graph: SemanticGraph;
59
60
  readonly profileContext: ResolvedProfileContext;
60
- }, catalogue: {
61
+ /** From the compilation (ADR 0131); absent, slot questions stay quiet. */
62
+ readonly patternMemberships?: readonly CataloguePatternMembership[];
63
+ },
64
+ /**
65
+ * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
66
+ * A set rather than one document so the pane asks the same interview the
67
+ * CLI does over the same workspace: an editor showing fewer questions than
68
+ * `design` does over the same files is a disagreement with no symptom.
69
+ */
70
+ catalogue: {
61
71
  readonly path: string;
62
72
  readonly source: string;
63
- },
73
+ } | readonly {
74
+ readonly path: string;
75
+ readonly source: string;
76
+ }[],
64
77
  /**
65
78
  * What the host has already dealt with (#328). Evaluation is unchanged and
66
79
  * the model is untouched: this decides only what the pane draws, because a
@@ -80,10 +93,14 @@ dismissed?: readonly DismissedQuestion[]) => VisualInterrogationOverlay | undefi
80
93
  export declare const renderedWorkspaceOf: (compiled: {
81
94
  readonly graph: SemanticGraph;
82
95
  readonly profileContext: ResolvedProfileContext;
96
+ readonly patternMemberships?: readonly CataloguePatternMembership[];
83
97
  }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary" | "interrogation">, catalogue?: {
84
98
  readonly path: string;
85
99
  readonly source: string;
86
- }, dismissed?: readonly DismissedQuestion[]) => {
100
+ } | readonly {
101
+ readonly path: string;
102
+ readonly source: string;
103
+ }[], dismissed?: readonly DismissedQuestion[]) => {
87
104
  readonly model: VisualRenderedModel;
88
105
  readonly views: readonly VisualViewSummary[];
89
106
  };
@@ -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, compiled.patternMemberships);
61
68
  const dismissedEverywhere = new Set(dismissed
62
69
  .filter(({ subject }) => subject === undefined)
63
70
  .map(({ questionId }) => questionId));
@@ -0,0 +1,13 @@
1
+ export interface CoverageScopePattern {
2
+ readonly pattern: string;
3
+ /** Repository-relative files the pattern selected, sorted. */
4
+ readonly artifacts: readonly string[];
5
+ }
6
+ export type ArtifactCoverage = {
7
+ readonly assessed: false;
8
+ readonly reason: string;
9
+ } | {
10
+ readonly assessed: true;
11
+ readonly scope: readonly CoverageScopePattern[];
12
+ };
13
+ export declare function deriveArtifactCoverage(manifestDirectory: string, patterns: readonly string[] | undefined): ArtifactCoverage;
@@ -0,0 +1,58 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { globSync } from 'node:fs';
3
+ import { sep } from 'node:path';
4
+ const runGit = (cwd, args) => spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
5
+ export function deriveArtifactCoverage(manifestDirectory, patterns) {
6
+ if (patterns === undefined) {
7
+ return {
8
+ assessed: false,
9
+ reason: 'the workspace manifest declares no coverage scope',
10
+ };
11
+ }
12
+ // The root is the git toplevel of the manifest's directory, never the
13
+ // process cwd: the same command must report the same coverage wherever it
14
+ // was invoked (the #216 bug shape), and the repository boundary is git's
15
+ // to draw, not a directory-layout guess.
16
+ const toplevel = runGit(manifestDirectory, [
17
+ 'rev-parse',
18
+ '--show-toplevel',
19
+ ]);
20
+ if (toplevel.status !== 0) {
21
+ return {
22
+ assessed: false,
23
+ reason: 'the workspace does not live in a git repository',
24
+ };
25
+ }
26
+ const root = toplevel.stdout.trim();
27
+ // An artifact is any file git can see: tracked, or untracked and not
28
+ // ignored. Tracked-only would blind the report to exactly the newest
29
+ // files — the recurrence this feature exists to catch (ADR 0130).
30
+ const listed = runGit(root, [
31
+ 'ls-files',
32
+ '-z',
33
+ '--cached',
34
+ '--others',
35
+ '--exclude-standard',
36
+ ]);
37
+ if (listed.status !== 0) {
38
+ return {
39
+ assessed: false,
40
+ reason: `git ls-files failed: ${(listed.stderr ?? '').trim()}`,
41
+ };
42
+ }
43
+ const visible = new Set((listed.stdout ?? '').split('\0').filter((path) => path.length > 0));
44
+ // Glob matches are intersected with git's view, so a symlink escaping the
45
+ // repository or a build tree git ignores cannot enter the artifact set
46
+ // however broad the glob. The intersection also drops directories: git
47
+ // lists files only.
48
+ return {
49
+ assessed: true,
50
+ scope: patterns.map((pattern) => ({
51
+ pattern,
52
+ artifacts: globSync(pattern, { cwd: root })
53
+ .map((path) => path.split(sep).join('/'))
54
+ .filter((path) => visible.has(path))
55
+ .sort((left, right) => left.localeCompare(right)),
56
+ })),
57
+ };
58
+ }
@@ -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, compilation.patternMemberships);
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, compilation.patternMemberships),
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, compilation.patternMemberships);
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
@@ -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[];
@@ -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.js CHANGED
@@ -10,6 +10,7 @@ import { runExportCommand } from './export-command.js';
10
10
  import { runApplyCommand } from './apply-cli.js';
11
11
  import { runDesignCommand } from './design-command.js';
12
12
  import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
13
+ import { deriveArtifactCoverage } from './artifact-coverage.js';
13
14
  import { deriveAttestationStaleness } from './attestation-staleness.js';
14
15
  import { reconcileEvidenceReports } from './reconciliation.js';
15
16
  import { loadWorkspaceManifest } from './workspace.js';
@@ -84,9 +85,13 @@ const runReconciliation = (options, cwd) => {
84
85
  source: readFileSync(resolve(cwd, path), 'utf8'),
85
86
  documentId: documentIdByPath.get(path) ?? path,
86
87
  })));
88
+ // Coverage anchors on the manifest's own directory, not the process
89
+ // cwd, so the same command reports the same coverage wherever it was
90
+ // invoked (ADR 0130).
91
+ const coverage = deriveArtifactCoverage(dirname(resolve(cwd, workspacePath)), loadedWorkspace.manifest.coverage);
87
92
  return {
88
93
  exitCode: 0,
89
- stdout: `${JSON.stringify(reconcileEvidenceReports(loadedWorkspace.workspace.id, evaluation.reports, compilation.graph, staleness), null, 2)}\n`,
94
+ stdout: `${JSON.stringify(reconcileEvidenceReports(loadedWorkspace.workspace.id, evaluation.reports, compilation.graph, staleness, coverage), null, 2)}\n`,
90
95
  stderr: '',
91
96
  };
92
97
  }
@@ -94,9 +94,31 @@ export interface ResolvedProfileContext {
94
94
  */
95
95
  readonly permittedRelationshipKinds: (fromKindIdentity: string, toKindIdentity: string) => ReadonlySet<RelationshipKind> | undefined;
96
96
  }
97
+ /**
98
+ * One slot of one pattern instance, and the subject bound into it
99
+ * (ADR 0131). Compile CONTEXT, not graph content: `parts` binds existing
100
+ * subjects (#268), the binding is consumed during expansion, and the graph
101
+ * stays indistinguishable from a hand-authored one — so the compile result
102
+ * is the only place this fact survives. `pattern` is the kind identity
103
+ * (`yarrasys/api-led@1.0#api`), the naming ADR 0129 chose: identity that
104
+ * travels, never a document path.
105
+ */
106
+ export interface PatternMembership {
107
+ readonly member: string;
108
+ readonly slot: string;
109
+ readonly instance: string;
110
+ readonly pattern: string;
111
+ }
97
112
  export type CompilationResult = {
98
113
  readonly ok: true;
99
114
  readonly graph: SemanticGraph;
115
+ /**
116
+ * Optional in the type although the compiler always emits it: this
117
+ * shape is published, and a required addition is free for readers
118
+ * and a break for constructors. Read it as `?? []` — and thread it
119
+ * to `evaluateCatalogue`, or `fills-pattern-slot` never fires.
120
+ */
121
+ readonly patternMemberships?: readonly PatternMembership[];
100
122
  } | {
101
123
  readonly ok: false;
102
124
  readonly diagnostics: readonly Diagnostic[];
@@ -105,6 +127,7 @@ export type ContextualCompilationResult = {
105
127
  readonly ok: true;
106
128
  readonly graph: SemanticGraph;
107
129
  readonly profileContext: ResolvedProfileContext;
130
+ readonly patternMemberships?: readonly PatternMembership[];
108
131
  } | {
109
132
  readonly ok: false;
110
133
  readonly diagnostics: readonly Diagnostic[];
package/dist/compiler.js CHANGED
@@ -2332,8 +2332,24 @@ function compileWorkspaceResolved(parsed) {
2332
2332
  if (diagnostics.length > 0) {
2333
2333
  return diagnosticFailure(diagnostics);
2334
2334
  }
2335
+ // Membership survives the compile as context (ADR 0131): one entry per
2336
+ // bound slot, sorted so the emission is deterministic. Always emitted,
2337
+ // possibly empty — an empty array is a workspace with no bindings, while
2338
+ // an evaluation missing the array is a caller that never looked.
2339
+ const patternMemberships = patternInstances
2340
+ .flatMap(({ instance, pattern, bindings }) => [...bindings].map(([slot, member]) => ({
2341
+ member,
2342
+ slot,
2343
+ instance,
2344
+ pattern: pattern.kindIdentity,
2345
+ })))
2346
+ .sort((left, right) => left.member.localeCompare(right.member) ||
2347
+ left.pattern.localeCompare(right.pattern) ||
2348
+ left.instance.localeCompare(right.instance) ||
2349
+ left.slot.localeCompare(right.slot));
2335
2350
  return {
2336
2351
  ok: true,
2352
+ patternMemberships,
2337
2353
  profileContext: {
2338
2354
  conceptKindLineages: immutableMap([...conceptKindByIdentity]
2339
2355
  .sort(([left], [right]) => left.localeCompare(right))
@@ -2418,7 +2434,13 @@ function compileWorkspaceResolved(parsed) {
2418
2434
  }
2419
2435
  export function compileWorkspace(sources) {
2420
2436
  const result = compileWorkspaceResolved(parseSources(sources).parsed);
2421
- return result.ok ? { ok: true, graph: result.graph } : result;
2437
+ return result.ok
2438
+ ? {
2439
+ ok: true,
2440
+ graph: result.graph,
2441
+ patternMemberships: result.patternMemberships,
2442
+ }
2443
+ : result;
2422
2444
  }
2423
2445
  export const compileWorkspaceWithProfileContext = (sources) => compileWorkspaceResolved(parseSources(sources).parsed);
2424
2446
  /**