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.
- package/dist/adapters/visual/session-server.js +30 -1
- package/dist/adapters/visual/workspace-model.d.ts +20 -3
- package/dist/adapters/visual/workspace-model.js +12 -5
- package/dist/artifact-coverage.d.ts +13 -0
- package/dist/artifact-coverage.js +58 -0
- package/dist/ask-command.js +25 -16
- package/dist/catalogue-sources.d.ts +20 -0
- package/dist/catalogue-sources.js +39 -0
- package/dist/check-command.js +38 -9
- package/dist/cli-support.d.ts +7 -0
- package/dist/cli-support.js +4 -0
- package/dist/cli.js +6 -1
- package/dist/compiler.d.ts +23 -0
- package/dist/compiler.js +23 -1
- package/dist/design-command.js +21 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/interrogate-command.d.ts +117 -1
- package/dist/interrogate-command.js +220 -34
- package/dist/interrogation-entry.d.ts +1 -1
- package/dist/interrogation-entry.js +1 -1
- package/dist/projection.d.ts +47 -0
- package/dist/projection.js +126 -0
- package/dist/reconciliation.d.ts +21 -1
- package/dist/reconciliation.js +93 -2
- package/dist/visual-app-lib/editor.js +26788 -27093
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +20 -3
- package/dist/visual-app-lib/types/compiler.d.ts +23 -0
- package/dist/visual-app-lib/types/interrogate-command.d.ts +117 -1
- package/dist/visual-app-lib/types/projection.d.ts +47 -0
- package/dist/visual-app-lib/types/workspace.d.ts +23 -0
- package/dist/workbook-import-entry.d.ts +30 -0
- package/dist/workbook-import-entry.js +30 -0
- package/dist/workbook.js +15 -0
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.js +36 -13
- package/docs/CONSUMING-YARRAMATE.md +63 -0
- package/package.json +5 -1
- package/schema/yarramate-interrogation-report.schema.json +9 -0
- package/schema/yarramate-question-catalogue.schema.json +32 -3
- package/schema/yarramate-reconciliation-report.schema.json +21 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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,
|
|
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,
|
|
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
|
|
58
|
-
if (!
|
|
64
|
+
const composed = composeCatalogues(Array.isArray(catalogue) ? catalogue : [catalogue]);
|
|
65
|
+
if (!composed.ok)
|
|
59
66
|
return undefined;
|
|
60
|
-
const report = evaluateCatalogue(
|
|
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
|
+
}
|
package/dist/ask-command.js
CHANGED
|
@@ -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 {
|
|
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
|
|
472
|
+
const composed = composeCatalogues(catalogueSources({
|
|
472
473
|
path: shippedCataloguePath,
|
|
473
474
|
source: readFileSync(shippedCataloguePath, 'utf8'),
|
|
474
|
-
}, compilation.profileContext);
|
|
475
|
-
if (!
|
|
476
|
-
return failed(
|
|
477
|
-
const report = evaluateCatalogue(
|
|
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
|
|
702
|
+
const composed = composeCatalogues(catalogueSources({
|
|
702
703
|
path: cataloguePath ?? resolvedCataloguePath,
|
|
703
704
|
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
704
|
-
}, compilation.profileContext);
|
|
705
|
-
if (!
|
|
706
|
-
return failed(
|
|
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(
|
|
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
|
|
1005
|
+
const composed = composeCatalogues(catalogueSources({
|
|
997
1006
|
path: cataloguePath ?? resolvedCataloguePath,
|
|
998
1007
|
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
999
|
-
}, compilation.profileContext);
|
|
1000
|
-
if (!
|
|
1001
|
-
return failed(
|
|
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(
|
|
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
|
+
];
|
package/dist/check-command.js
CHANGED
|
@@ -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 {
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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 =
|
|
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
|
package/dist/cli-support.d.ts
CHANGED
|
@@ -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[];
|
package/dist/cli-support.js
CHANGED
|
@@ -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
|
}
|
package/dist/compiler.d.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|