yarramate 0.5.0 → 0.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 (48) hide show
  1. package/README.md +39 -10
  2. package/catalogues/core-enrichment.yaml +700 -0
  3. package/dist/adapters/likec4-cli.js +179 -55
  4. package/dist/adapters/likec4-export.d.ts +1 -1
  5. package/dist/adapters/likec4-prepare.d.ts +6 -0
  6. package/dist/adapters/likec4-prepare.js +40 -2
  7. package/dist/adapters/mcp-cli.js +55 -49
  8. package/dist/apply-command.d.ts +2 -0
  9. package/dist/apply-command.js +199 -0
  10. package/dist/{new-command.d.ts → ask-command.d.ts} +1 -1
  11. package/dist/ask-command.js +729 -0
  12. package/dist/brief.d.ts +3 -0
  13. package/dist/brief.js +235 -0
  14. package/dist/check-command.js +70 -4
  15. package/dist/cli-support.d.ts +4 -1
  16. package/dist/cli-support.js +3 -2
  17. package/dist/cli.js +22 -586
  18. package/dist/compiler.js +45 -7
  19. package/dist/core-contract.d.ts +1 -1
  20. package/dist/{status-command.d.ts → design-command.d.ts} +1 -1
  21. package/dist/design-command.js +218 -0
  22. package/dist/export-command.d.ts +2 -0
  23. package/dist/export-command.js +228 -0
  24. package/dist/interrogate-command.d.ts +104 -0
  25. package/dist/interrogate-command.js +268 -0
  26. package/dist/next-command.d.ts +22 -0
  27. package/dist/next-command.js +167 -0
  28. package/dist/reconciliation.d.ts +2 -0
  29. package/dist/reconciliation.js +38 -0
  30. package/docs/CONSUMING-YARRAMATE.md +28 -21
  31. package/package.json +13 -10
  32. package/schema/yarramate-apply-result.schema.json +32 -0
  33. package/schema/yarramate-ask-result.schema.json +391 -0
  34. package/schema/yarramate-check-result.schema.json +12 -0
  35. package/schema/yarramate-core-contract.schema.json +5 -10
  36. package/schema/yarramate-design-step.schema.json +152 -0
  37. package/schema/yarramate-document.schema.json +76 -9
  38. package/schema/yarramate-interrogation-report.schema.json +89 -0
  39. package/schema/yarramate-likec4-generated-project-v2.schema.json +6 -0
  40. package/schema/yarramate-likec4-generated-project.schema.json +6 -0
  41. package/schema/yarramate-operations.schema.json +303 -0
  42. package/schema/yarramate-question-catalogue.schema.json +431 -0
  43. package/schema/yarramate-reconciliation-report.schema.json +8 -2
  44. package/skills/yarramate-architecture/SKILL.md +42 -17
  45. package/skills/yarramate-architecture/references/native-authoring.md +73 -17
  46. package/dist/new-command.js +0 -111
  47. package/dist/status-command.js +0 -171
  48. package/schema/yarramate-status-result.schema.json +0 -273
package/dist/compiler.js CHANGED
@@ -14,6 +14,23 @@ const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema
14
14
  const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
15
15
  const compareById = (left, right) => left.id.localeCompare(right.id);
16
16
  const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from(state, 'utf8').toString('hex')}`;
17
+ const describeAspect = (aspect) => aspect.replace('-', ' ');
18
+ // Candidate order is the policy-matrix declaration order: the resolved kind
19
+ // map inserts core policies first, then extension kinds as declared.
20
+ const candidateKindHint = (kinds, rejected, sourceAspect, targetAspect) => {
21
+ const candidates = [...kinds]
22
+ .filter(([id, kind]) => id !== rejected &&
23
+ (kind.sourceAspects?.includes(sourceAspect) ?? true) &&
24
+ (kind.targetAspects?.includes(targetAspect) ?? true))
25
+ .map(([id]) => id);
26
+ if (candidates.length === 0) {
27
+ return '';
28
+ }
29
+ const observed = sourceAspect === targetAspect
30
+ ? `both endpoints are ${describeAspect(sourceAspect)}`
31
+ : `source is ${describeAspect(sourceAspect)} and target is ${describeAspect(targetAspect)}`;
32
+ return `; ${observed}; valid candidates: ${candidates.join(', ')}`;
33
+ };
17
34
  const diagnosticFailure = (diagnostics) => ({
18
35
  ok: false,
19
36
  diagnostics: [...diagnostics].sort((left, right) => left.path.localeCompare(right.path) ||
@@ -779,27 +796,35 @@ function compileWorkspaceResolved(sources) {
779
796
  }
780
797
  const policy = selectedProfile.relationshipKinds.get(relationship.kind);
781
798
  if (policy !== undefined) {
782
- for (const endpoint of ['source', 'target']) {
783
- const reference = endpoint === 'source' ? relationship.from : relationship.to;
799
+ const aspectOf = (reference) => {
784
800
  const resolvedConcept = conceptByQualifiedId.get(qualifyReference(value.id, reference));
785
- const kind = resolvedConcept === undefined
801
+ return resolvedConcept === undefined
786
802
  ? undefined
787
803
  : profiles
788
804
  .get(resolvedConcept.profile)
789
- ?.conceptKinds.get(resolvedConcept.concept.kind);
805
+ ?.conceptKinds.get(resolvedConcept.concept.kind)?.aspect;
806
+ };
807
+ const sourceAspect = aspectOf(relationship.from);
808
+ const targetAspect = aspectOf(relationship.to);
809
+ const candidates = sourceAspect === undefined || targetAspect === undefined
810
+ ? ''
811
+ : candidateKindHint(selectedProfile.relationshipKinds, relationship.kind, sourceAspect, targetAspect);
812
+ for (const endpoint of ['source', 'target']) {
813
+ const reference = endpoint === 'source' ? relationship.from : relationship.to;
814
+ const aspect = endpoint === 'source' ? sourceAspect : targetAspect;
790
815
  const allowed = endpoint === 'source'
791
816
  ? policy.sourceAspects
792
817
  : policy.targetAspects;
793
- if (kind !== undefined &&
818
+ if (aspect !== undefined &&
794
819
  allowed !== undefined &&
795
- !allowed.includes(kind.aspect)) {
820
+ !allowed.includes(aspect)) {
796
821
  const field = endpoint === 'source' ? 'from' : 'to';
797
822
  const pointer = `/relationships/${index}/${field}`;
798
823
  const source = location(['relationships', index, field], pointer);
799
824
  diagnostics.push({
800
825
  severity: 'error',
801
826
  code: 'YM404',
802
- message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((aspect) => `"${aspect}"`).join(' or ')}; "${reference}" has aspect "${kind.aspect}"${policy.repair === undefined ? '' : `; ${policy.repair}`}`,
827
+ message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((entry) => `"${entry}"`).join(' or ')}; "${reference}" has aspect "${aspect}"${policy.repair === undefined ? '' : `; ${policy.repair}`}${candidates}`,
803
828
  path: input.path,
804
829
  pointer,
805
830
  line: source.line,
@@ -886,6 +911,19 @@ function compileWorkspaceResolved(sources) {
886
911
  source: location(['concepts', index, 'references', referenceIndex, 'ref'], `/concepts/${index}/references/${referenceIndex}/ref`),
887
912
  });
888
913
  }
914
+ // An attestation is a recorded judgment, not content the engine
915
+ // evaluates: the claim's existence is what triggers can see, and
916
+ // revocation is deletion, reviewed at the Git boundary.
917
+ for (const [attestationIndex, attestation] of (concept.attestations ?? []).entries()) {
918
+ claims.push({
919
+ id: `${subject}~attestation-${attestation.topic}`,
920
+ subject,
921
+ predicate: `yarramate/attestation/${attestation.topic}`,
922
+ object: { value: `${attestation.by} ${attestation.on}` },
923
+ origin: 'declared',
924
+ source: location(['concepts', index, 'attestations', attestationIndex, 'topic'], `/concepts/${index}/attestations/${attestationIndex}/topic`),
925
+ });
926
+ }
889
927
  for (const [stateIndex, state] of (concept.presentIn ?? []).entries()) {
890
928
  const stateIdentity = qualifyReference(value.id, state);
891
929
  claims.push({
@@ -5,7 +5,7 @@ export interface CoreContractFormat {
5
5
  readonly packageExport: string;
6
6
  }
7
7
  export interface CoreContractCommand {
8
- readonly name: 'init' | 'add' | 'connect' | 'check' | 'compile' | 'view' | 'context' | 'compare' | 'evidence';
8
+ readonly name: 'init' | 'design' | 'apply' | 'ask' | 'check' | 'reconcile' | 'export';
9
9
  readonly binary: 'yarramate';
10
10
  readonly machineFormat?: string;
11
11
  }
@@ -1,2 +1,2 @@
1
1
  import { type CliResult } from './cli-support.js';
2
- export declare function runStatusCommand(options: readonly string[], cwd: string): CliResult;
2
+ export declare function runDesignCommand(options: readonly string[], cwd: string): CliResult;
@@ -0,0 +1,218 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parseDocument } from 'yaml';
5
+ import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
6
+ import { compileWorkspaceWithProfileContext, } from './compiler.js';
7
+ import { evaluateCatalogue, loadQuestionCatalogue, } from './interrogate-command.js';
8
+ import { evaluateProjection } from './projection.js';
9
+ import { renderBrief } from './brief.js';
10
+ import { loadWorkspaceManifest } from './workspace.js';
11
+ // The catalogue is internal to design: it ships inside the package,
12
+ // versioned with it, and harnesses never pass catalogue paths. The
13
+ // relative hop works from both src/ (dev) and dist/ (shipped).
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const shippedCataloguePath = join(here, '..', 'catalogues', 'core-enrichment.yaml');
16
+ // The top step is the first open question in wave order, then catalogue
17
+ // order within the wave; a subject-scoped question serves its first open
18
+ // subject and reports how many more share it. One question at a time is
19
+ // the discipline; everything else is a read (ask --open).
20
+ const selectStep = (report, subjectFilter) => {
21
+ for (const wave of report.waves) {
22
+ for (const question of wave.questions) {
23
+ if (!question.open)
24
+ continue;
25
+ if (question.subjects === undefined) {
26
+ if (subjectFilter !== undefined)
27
+ continue;
28
+ return {
29
+ questionId: question.id,
30
+ wave: wave.id,
31
+ scope: 'workspace',
32
+ authority: question.authority,
33
+ question: question.question,
34
+ materiality: question.materiality,
35
+ resolution: question.resolution,
36
+ };
37
+ }
38
+ const subjects = subjectFilter === undefined
39
+ ? question.subjects
40
+ : question.subjects.filter(({ id }) => id === subjectFilter);
41
+ const first = subjects[0];
42
+ if (first === undefined)
43
+ continue;
44
+ return {
45
+ questionId: question.id,
46
+ wave: wave.id,
47
+ scope: 'subject',
48
+ authority: question.authority,
49
+ question: first.question,
50
+ materiality: question.materiality,
51
+ resolution: question.resolution,
52
+ subject: {
53
+ id: first.id,
54
+ ...(first.name === undefined ? {} : { name: first.name }),
55
+ },
56
+ ...(subjects.length > 1
57
+ ? { remainingSubjects: subjects.length - 1 }
58
+ : {}),
59
+ };
60
+ }
61
+ }
62
+ return null;
63
+ };
64
+ export function runDesignCommand(options, cwd) {
65
+ const json = options.includes('--json');
66
+ let subjectFilter;
67
+ let cataloguePath;
68
+ const rest = [];
69
+ const withoutJson = options.filter((option) => option !== '--json');
70
+ for (let index = 0; index < withoutJson.length; index += 1) {
71
+ const option = withoutJson[index];
72
+ if (option === '--subject' || option === '--catalogue') {
73
+ const value = withoutJson[index + 1];
74
+ if (value === undefined || value.startsWith('-')) {
75
+ return { exitCode: 2, stdout: '', stderr: usage };
76
+ }
77
+ if (option === '--subject') {
78
+ if (subjectFilter !== undefined) {
79
+ return { exitCode: 2, stdout: '', stderr: usage };
80
+ }
81
+ subjectFilter = value;
82
+ }
83
+ else {
84
+ if (cataloguePath !== undefined) {
85
+ return { exitCode: 2, stdout: '', stderr: usage };
86
+ }
87
+ cataloguePath = value;
88
+ }
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (option === undefined || option.startsWith('-')) {
93
+ return { exitCode: 2, stdout: '', stderr: usage };
94
+ }
95
+ rest.push(option);
96
+ }
97
+ const [workspacePath] = rest;
98
+ if (rest.length !== 1 || workspacePath === undefined) {
99
+ return { exitCode: 2, stdout: '', stderr: usage };
100
+ }
101
+ try {
102
+ const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
103
+ if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
104
+ return {
105
+ exitCode: 2,
106
+ stdout: '',
107
+ stderr: 'design requires an explicit workspace manifest (yarramate/workspace/v1)\n',
108
+ };
109
+ }
110
+ const failed = (diagnostics) => ({
111
+ exitCode: 1,
112
+ stdout: json
113
+ ? diagnosticJson(diagnostics)
114
+ : humanDiagnostics(diagnostics),
115
+ stderr: '',
116
+ });
117
+ const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
118
+ if (!loadedWorkspace.ok)
119
+ return failed(loadedWorkspace.diagnostics);
120
+ const workspace = loadedWorkspace.workspace;
121
+ const resolvedCataloguePath = cataloguePath === undefined
122
+ ? shippedCataloguePath
123
+ : resolve(cwd, cataloguePath);
124
+ const loadedCatalogue = loadQuestionCatalogue({
125
+ path: cataloguePath ?? resolvedCataloguePath,
126
+ source: readFileSync(resolvedCataloguePath, 'utf8'),
127
+ });
128
+ if (!loadedCatalogue.ok)
129
+ return failed(loadedCatalogue.diagnostics);
130
+ const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
131
+ path,
132
+ source: readFileSync(resolve(cwd, path), 'utf8'),
133
+ })));
134
+ if (!compilation.ok)
135
+ return failed(compilation.diagnostics);
136
+ if (subjectFilter !== undefined) {
137
+ const known = new Set(compilation.graph.subjects.map(({ id }) => id));
138
+ if (!known.has(subjectFilter)) {
139
+ return {
140
+ exitCode: 1,
141
+ stdout: '',
142
+ stderr: `Unknown subject identity: ${subjectFilter} (the compiled workspace declares ${known.size} subjects)\n`,
143
+ };
144
+ }
145
+ }
146
+ const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
147
+ const step = selectStep(report, subjectFilter);
148
+ let slice;
149
+ if (step?.subject !== undefined) {
150
+ const projection = evaluateProjection(compilation.graph, {
151
+ format: 'yarramate/projection/v1',
152
+ id: 'design-step',
153
+ version: '0.0',
154
+ query: {
155
+ subjects: [step.subject.id],
156
+ relationships: 'connected',
157
+ },
158
+ presentation: {
159
+ title: step.subject.name ?? step.subject.id,
160
+ description: `The neighbourhood of ${step.subject.id} as declared today.`,
161
+ },
162
+ }, compilation.profileContext);
163
+ slice = renderBrief(projection, compilation.profileContext);
164
+ }
165
+ const result = {
166
+ format: 'yarramate/design-step/v1',
167
+ workspace: workspace.id,
168
+ catalogue: report.catalogue,
169
+ progress: {
170
+ questions: report.summary.questions,
171
+ openQuestions: report.summary.openQuestions,
172
+ open: report.summary.open,
173
+ waves: report.waves.map((wave) => ({
174
+ id: wave.id,
175
+ open: wave.questions.reduce((total, question) => total +
176
+ (question.open ? (question.subjects?.length ?? 1) : 0), 0),
177
+ })),
178
+ },
179
+ step,
180
+ ...(slice === undefined ? {} : { slice }),
181
+ };
182
+ if (json) {
183
+ return {
184
+ exitCode: 0,
185
+ stdout: `${JSON.stringify(result, null, 2)}\n`,
186
+ stderr: '',
187
+ };
188
+ }
189
+ const waveSummary = result.progress.waves
190
+ .map(({ id, open }) => `${id} ${open} open`)
191
+ .join(' · ');
192
+ const lines = [
193
+ `Design interview — workspace ${workspace.id} · catalogue ${report.catalogue}`,
194
+ `Waves: ${waveSummary}`,
195
+ '',
196
+ ];
197
+ if (step === null) {
198
+ lines.push(subjectFilter === undefined
199
+ ? 'Interview complete: no open questions. The model answers everything the catalogue asks.'
200
+ : `Interview complete for ${subjectFilter}: no open questions touch it.`);
201
+ }
202
+ else {
203
+ lines.push(`Q [${step.wave} · ${step.questionId}] (authority: ${step.authority})`, ` ${step.question}`, ` Why it matters: ${step.materiality}`, ` How to answer: ${step.resolution}`);
204
+ if (step.remainingSubjects !== undefined) {
205
+ lines.push(` (${step.remainingSubjects} more subject${step.remainingSubjects === 1 ? '' : 's'} share${step.remainingSubjects === 1 ? 's' : ''} this question)`);
206
+ }
207
+ if (slice !== undefined) {
208
+ lines.push('', 'Subject slice:', '', slice.trimEnd());
209
+ }
210
+ lines.push('', 'Answer by updating the model (one atomic batch):', ` yarramate apply <operations.yaml> ${workspacePath}`, `Then re-run: yarramate design ${workspacePath}`);
211
+ }
212
+ return { exitCode: 0, stdout: `${lines.join('\n')}\n`, stderr: '' };
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ return { exitCode: 2, stdout: '', stderr: `${message}\n` };
217
+ }
218
+ }
@@ -0,0 +1,2 @@
1
+ import { type CliResult } from './cli-support.js';
2
+ export declare function runExportCommand(options: readonly string[], cwd: string): CliResult;
@@ -0,0 +1,228 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { parseDocument } from 'yaml';
6
+ import { renderBrief } from './brief.js';
7
+ import { humanDiagnostics, usage } from './cli-support.js';
8
+ import { compileWorkspaceWithProfileContext, } from './compiler.js';
9
+ import { serializeSemanticGraph } from './graph.js';
10
+ import { evaluateProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
11
+ import { loadWorkspaceManifest } from './workspace.js';
12
+ // The adapter stays a separate process behind the verb: the core never
13
+ // imports adapter code (the adapter-runtime-dependency exclusion), it
14
+ // hands the invocation to the sibling binary shipped in the same package.
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const likec4AdapterEntry = join(here, 'adapters', 'likec4-cli.js');
17
+ const claimValue = (claims, subject, predicate) => {
18
+ const object = claims.find((claim) => claim.subject === subject && claim.predicate === predicate)?.object;
19
+ return object !== undefined && 'value' in object ? object.value : undefined;
20
+ };
21
+ const briefFileName = (id) => `${id.replaceAll('#', '--')}.md`;
22
+ const parseExportOptions = (options) => {
23
+ const positionals = [];
24
+ let out;
25
+ let budget;
26
+ let json = false;
27
+ for (let index = 0; index < options.length; index += 1) {
28
+ const option = options[index];
29
+ if (option === '--json') {
30
+ json = true;
31
+ continue;
32
+ }
33
+ if (option === '--out' || option === '--budget') {
34
+ const value = options[index + 1];
35
+ if (value === undefined || value.startsWith('-'))
36
+ return undefined;
37
+ if (option === '--out') {
38
+ if (out !== undefined)
39
+ return undefined;
40
+ out = value;
41
+ }
42
+ else {
43
+ if (budget !== undefined || !/^[1-9][0-9]*$/.test(value)) {
44
+ return undefined;
45
+ }
46
+ budget = Number(value);
47
+ }
48
+ index += 1;
49
+ continue;
50
+ }
51
+ if (option === undefined || option.startsWith('-'))
52
+ return undefined;
53
+ positionals.push(option);
54
+ }
55
+ return {
56
+ positionals,
57
+ ...(out === undefined ? {} : { out }),
58
+ ...(budget === undefined ? {} : { budget }),
59
+ json,
60
+ };
61
+ };
62
+ export function runExportCommand(options, cwd) {
63
+ const [kind, ...rest] = options;
64
+ if (kind === undefined ||
65
+ !['graph', 'markdown', 'briefs', 'likec4'].includes(kind)) {
66
+ return { exitCode: 2, stdout: '', stderr: usage };
67
+ }
68
+ const parsed = parseExportOptions(rest);
69
+ if (parsed === undefined) {
70
+ return { exitCode: 2, stdout: '', stderr: usage };
71
+ }
72
+ // likec4 delegates whole: <likec4-project.yaml> <output-dir> <workspace>.
73
+ if (kind === 'likec4') {
74
+ const [projectDefinition, outputDirectory, workspacePath] = parsed.positionals;
75
+ if (parsed.positionals.length !== 3 ||
76
+ projectDefinition === undefined ||
77
+ outputDirectory === undefined ||
78
+ workspacePath === undefined ||
79
+ parsed.out !== undefined ||
80
+ parsed.budget !== undefined ||
81
+ parsed.json) {
82
+ return { exitCode: 2, stdout: '', stderr: usage };
83
+ }
84
+ if (!existsSync(likec4AdapterEntry)) {
85
+ return {
86
+ exitCode: 2,
87
+ stdout: '',
88
+ stderr: `LikeC4 adapter entry not found at ${likec4AdapterEntry}; ` +
89
+ 'run from the installed package or use the yarramate-likec4 binary directly\n',
90
+ };
91
+ }
92
+ const delegated = spawnSync(process.execPath, [
93
+ likec4AdapterEntry,
94
+ 'export-project',
95
+ projectDefinition,
96
+ outputDirectory,
97
+ workspacePath,
98
+ ], { cwd, encoding: 'utf8' });
99
+ const exitCode = delegated.status === 0 ? 0 : delegated.status === 1 ? 1 : 2;
100
+ return {
101
+ exitCode,
102
+ stdout: delegated.stdout ?? '',
103
+ stderr: delegated.stderr ?? '',
104
+ };
105
+ }
106
+ const expectedPositionals = kind === 'graph' ? 1 : 2;
107
+ const workspacePath = parsed.positionals[expectedPositionals - 1];
108
+ const projectionPath = kind === 'graph' ? undefined : parsed.positionals[0];
109
+ if (parsed.positionals.length !== expectedPositionals ||
110
+ workspacePath === undefined ||
111
+ parsed.json ||
112
+ (parsed.budget !== undefined && kind !== 'briefs') ||
113
+ (kind === 'briefs' && parsed.out === undefined)) {
114
+ return { exitCode: 2, stdout: '', stderr: usage };
115
+ }
116
+ try {
117
+ const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
118
+ if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
119
+ return {
120
+ exitCode: 2,
121
+ stdout: '',
122
+ stderr: 'export requires an explicit workspace manifest (yarramate/workspace/v1)\n',
123
+ };
124
+ }
125
+ const failed = (diagnostics) => ({
126
+ exitCode: 1,
127
+ stdout: humanDiagnostics(diagnostics),
128
+ stderr: '',
129
+ });
130
+ const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
131
+ if (!loadedWorkspace.ok)
132
+ return failed(loadedWorkspace.diagnostics);
133
+ const workspace = loadedWorkspace.workspace;
134
+ const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
135
+ path,
136
+ source: readFileSync(resolve(cwd, path), 'utf8'),
137
+ })));
138
+ if (!compilation.ok)
139
+ return failed(compilation.diagnostics);
140
+ if (kind === 'graph') {
141
+ const serialized = serializeSemanticGraph(compilation.graph);
142
+ if (parsed.out === undefined) {
143
+ return { exitCode: 0, stdout: serialized, stderr: '' };
144
+ }
145
+ const outPath = resolve(cwd, parsed.out);
146
+ mkdirSync(dirname(outPath), { recursive: true });
147
+ writeFileSync(outPath, serialized, 'utf8');
148
+ return {
149
+ exitCode: 0,
150
+ stdout: `Wrote graph to ${parsed.out}\n`,
151
+ stderr: '',
152
+ };
153
+ }
154
+ const loadedProjection = loadProjection({
155
+ path: projectionPath,
156
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
157
+ });
158
+ if (!loadedProjection.ok)
159
+ return failed(loadedProjection.diagnostics);
160
+ const result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
161
+ if (kind === 'markdown') {
162
+ const rendered = renderProjectionMarkdown(result);
163
+ if (parsed.out === undefined) {
164
+ return { exitCode: 0, stdout: rendered, stderr: '' };
165
+ }
166
+ const outPath = resolve(cwd, parsed.out);
167
+ mkdirSync(dirname(outPath), { recursive: true });
168
+ writeFileSync(outPath, rendered, 'utf8');
169
+ return {
170
+ exitCode: 0,
171
+ stdout: `Wrote markdown to ${parsed.out}\n`,
172
+ stderr: '',
173
+ };
174
+ }
175
+ // briefs: the handoff bundle — one brief per projected concept, each
176
+ // the concept's one-hop neighbourhood (ADR 0055), plus an index so N
177
+ // implementers can each pick up one slice.
178
+ const stateIds = new Set(result.claims
179
+ .filter(({ predicate }) => predicate === 'yarramate/state/type')
180
+ .map(({ subject }) => subject));
181
+ const concepts = result.subjects
182
+ .filter(({ id, type }) => type === 'concept' && !stateIds.has(id))
183
+ .map(({ id }) => id)
184
+ .sort((left, right) => left.localeCompare(right));
185
+ const outDirectory = resolve(cwd, parsed.out);
186
+ mkdirSync(outDirectory, { recursive: true });
187
+ const indexLines = [
188
+ `# Briefs — ${result.presentation?.title ?? result.projection}`,
189
+ '',
190
+ `Derived from projection ${result.projection}; one brief per concept,`,
191
+ 'each the concept\'s connected neighbourhood as declared today.',
192
+ '',
193
+ ];
194
+ for (const id of concepts) {
195
+ const slice = evaluateProjection(compilation.graph, {
196
+ format: 'yarramate/projection/v1',
197
+ id: 'export-brief',
198
+ version: '0.0',
199
+ query: { subjects: [id], relationships: 'connected' },
200
+ presentation: {
201
+ title: claimValue(result.claims, id, 'yarramate/concept/name') ?? id,
202
+ description: `The neighbourhood of ${id} as declared today.`,
203
+ },
204
+ }, compilation.profileContext);
205
+ const brief = renderBrief(slice, compilation.profileContext, parsed.budget);
206
+ writeFileSync(join(outDirectory, briefFileName(id)), brief, 'utf8');
207
+ const name = claimValue(result.claims, id, 'yarramate/concept/name');
208
+ const conceptKind = claimValue(result.claims, id, 'yarramate/concept/kind') ?? 'unknown';
209
+ const status = claimValue(result.claims, id, 'yarramate/lifecycle/status');
210
+ indexLines.push(`- [${name ?? id}](${briefFileName(id)}) — ` +
211
+ `${conceptKind.split('#')[1] ?? conceptKind}` +
212
+ `${status === undefined ? '' : ` (${status})`} — \`${id}\``);
213
+ }
214
+ if (concepts.length === 0) {
215
+ indexLines.push('No concepts selected by this projection.');
216
+ }
217
+ writeFileSync(join(outDirectory, 'INDEX.md'), `${indexLines.join('\n')}\n`, 'utf8');
218
+ return {
219
+ exitCode: 0,
220
+ stdout: `Wrote ${concepts.length} brief${concepts.length === 1 ? '' : 's'} and INDEX.md to ${parsed.out}\n`,
221
+ stderr: '',
222
+ };
223
+ }
224
+ catch (error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ return { exitCode: 2, stdout: '', stderr: `${message}\n` };
227
+ }
228
+ }
@@ -0,0 +1,104 @@
1
+ import { type Diagnostic, type ResolvedProfileContext, type SemanticGraph, type WorkspaceSource } from './compiler.js';
2
+ interface CatalogueSelector {
3
+ readonly kinds: readonly string[];
4
+ readonly kindMatching?: 'exact' | 'descendants';
5
+ readonly statuses?: readonly string[];
6
+ readonly documents?: readonly string[];
7
+ }
8
+ type CatalogueCondition = {
9
+ readonly condition: 'missing-claim';
10
+ readonly predicate: string;
11
+ } | {
12
+ readonly condition: 'missing-relationship';
13
+ readonly kinds: readonly string[];
14
+ readonly direction: 'incoming' | 'outgoing' | 'any';
15
+ readonly kindMatching?: 'exact' | 'descendants';
16
+ } | {
17
+ readonly condition: 'isolated';
18
+ } | {
19
+ readonly condition: 'no-subject-of-kind';
20
+ readonly kinds: readonly string[];
21
+ } | {
22
+ readonly condition: 'no-state-defined';
23
+ } | {
24
+ readonly condition: 'missing-linkage';
25
+ readonly kinds: readonly string[];
26
+ readonly direction: 'incoming' | 'outgoing';
27
+ readonly counterpartKinds: readonly string[];
28
+ readonly kindMatching?: 'exact' | 'descendants';
29
+ } | {
30
+ readonly condition: 'missing-reference';
31
+ readonly predicate: string;
32
+ readonly direction: 'incoming' | 'outgoing';
33
+ } | {
34
+ readonly condition: 'missing-attestation';
35
+ readonly topic: string;
36
+ };
37
+ export interface CatalogueQuestion {
38
+ readonly id: string;
39
+ readonly wave: string;
40
+ readonly scope: 'workspace' | 'subject';
41
+ readonly subjects?: CatalogueSelector;
42
+ readonly trigger: readonly CatalogueCondition[];
43
+ readonly question: string;
44
+ readonly materiality: string;
45
+ readonly resolution: string;
46
+ readonly authority: 'human' | 'agent' | 'either';
47
+ }
48
+ export interface QuestionCatalogue {
49
+ readonly format: 'yarramate/question-catalogue/v1';
50
+ readonly id: string;
51
+ readonly version: string;
52
+ readonly profile: string;
53
+ readonly presentation?: {
54
+ readonly title?: string;
55
+ readonly description?: string;
56
+ };
57
+ readonly waves: readonly {
58
+ readonly id: string;
59
+ readonly name: string;
60
+ readonly description?: string;
61
+ }[];
62
+ readonly questions: readonly CatalogueQuestion[];
63
+ }
64
+ interface OpenSubject {
65
+ readonly id: string;
66
+ readonly name?: string;
67
+ readonly question: string;
68
+ }
69
+ interface ReportQuestion {
70
+ readonly id: string;
71
+ readonly scope: 'workspace' | 'subject';
72
+ readonly authority: 'human' | 'agent' | 'either';
73
+ readonly open: boolean;
74
+ readonly question: string;
75
+ readonly materiality: string;
76
+ readonly resolution: string;
77
+ readonly subjects?: readonly OpenSubject[];
78
+ }
79
+ export interface InterrogationReport {
80
+ readonly format: 'yarramate/interrogation-report/v1';
81
+ readonly workspace: string;
82
+ readonly catalogue: string;
83
+ readonly summary: {
84
+ readonly questions: number;
85
+ readonly openQuestions: number;
86
+ readonly open: number;
87
+ };
88
+ readonly waves: readonly {
89
+ readonly id: string;
90
+ readonly name: string;
91
+ readonly questions: readonly ReportQuestion[];
92
+ }[];
93
+ }
94
+ export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext): Omit<InterrogationReport, 'workspace'>;
95
+ export type CatalogueLoadResult = {
96
+ readonly ok: true;
97
+ readonly catalogue: QuestionCatalogue;
98
+ } | {
99
+ readonly ok: false;
100
+ readonly diagnostics: readonly Diagnostic[];
101
+ };
102
+ export declare function loadQuestionCatalogue(catalogueSource: WorkspaceSource): CatalogueLoadResult;
103
+ export declare function renderInterrogationReport(report: InterrogationReport): string;
104
+ export {};