yarramate 0.4.0 → 0.6.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.
@@ -0,0 +1,272 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parseDocument } from 'yaml';
4
+ import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
5
+ import { compileWorkspaceWithProfileContext, } from './compiler.js';
6
+ import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
7
+ import { evaluateProjection, loadProjection } from './projection.js';
8
+ import { loadWorkspaceManifest } from './workspace.js';
9
+ // Which endpoint of a declared relationship must exist before the other,
10
+ // read off each core kind's declared intent (ADR 0048). Kinds without a
11
+ // build-order reading (association, assignment, influence) contribute no
12
+ // ordering edge rather than a guessed one.
13
+ const corePrerequisiteEndpoints = new Map([
14
+ ['realization', 'source'],
15
+ ['serving', 'source'],
16
+ ['triggering', 'source'],
17
+ ['flow', 'source'],
18
+ ['composition', 'target'],
19
+ ['aggregation', 'target'],
20
+ ['access', 'target'],
21
+ ['specialization', 'target'],
22
+ ]);
23
+ const prerequisiteEndpoint = (kind, profileContext) => {
24
+ const candidates = [
25
+ kind,
26
+ ...(profileContext?.relationshipKindLineages.get(kind) ?? []),
27
+ ];
28
+ for (const candidate of candidates) {
29
+ const separator = candidate.indexOf('#');
30
+ if (separator === -1 || !candidate.startsWith('yarramate/core@')) {
31
+ continue;
32
+ }
33
+ const orientation = corePrerequisiteEndpoints.get(candidate.slice(separator + 1));
34
+ if (orientation !== undefined)
35
+ return orientation;
36
+ }
37
+ return undefined;
38
+ };
39
+ const claimValue = (claims, subject, predicate) => {
40
+ const object = claims.find((claim) => claim.subject === subject && claim.predicate === predicate)?.object;
41
+ return object !== undefined && 'value' in object ? object.value : undefined;
42
+ };
43
+ const plural = (count, singular) => `${count} ${count === 1 ? singular : `${singular}s`}`;
44
+ const coverageClause = (coverage) => {
45
+ if (coverage.observations === 0)
46
+ return 'no evidence';
47
+ const parts = [
48
+ ...(coverage.confirmed > 0 ? [`${coverage.confirmed} confirmed`] : []),
49
+ ...(coverage.contradicted > 0
50
+ ? [`${coverage.contradicted} contradicted`]
51
+ : []),
52
+ ...(coverage.unknown > 0 ? [`${coverage.unknown} unknown`] : []),
53
+ ...(coverage.notObserved > 0
54
+ ? [`${coverage.notObserved} not observed`]
55
+ : []),
56
+ ];
57
+ return `${plural(coverage.observations, 'observation')} (${parts.join(', ')})`;
58
+ };
59
+ export function runNextCommand(options, cwd) {
60
+ const json = options.includes('--json');
61
+ const rest = options.filter((option) => option !== '--json');
62
+ const [projectionPath, workspacePath] = rest;
63
+ if (rest.length !== 2 ||
64
+ projectionPath === undefined ||
65
+ workspacePath === undefined ||
66
+ rest.some((option) => option.startsWith('-'))) {
67
+ return { exitCode: 2, stdout: '', stderr: usage };
68
+ }
69
+ try {
70
+ const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
71
+ if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
72
+ return {
73
+ exitCode: 2,
74
+ stdout: '',
75
+ stderr: 'next requires an explicit workspace manifest (yarramate/workspace/v1)\n',
76
+ };
77
+ }
78
+ const failed = (diagnostics) => ({
79
+ exitCode: 1,
80
+ stdout: json ? diagnosticJson(diagnostics) : humanDiagnostics(diagnostics),
81
+ stderr: '',
82
+ });
83
+ const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
84
+ if (!loadedWorkspace.ok)
85
+ return failed(loadedWorkspace.diagnostics);
86
+ const workspace = loadedWorkspace.workspace;
87
+ const loadedProjection = loadProjection({
88
+ path: projectionPath,
89
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
90
+ });
91
+ if (!loadedProjection.ok)
92
+ return failed(loadedProjection.diagnostics);
93
+ const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
94
+ path,
95
+ source: readFileSync(resolve(cwd, path), 'utf8'),
96
+ })));
97
+ if (!compilation.ok)
98
+ return failed(compilation.diagnostics);
99
+ const graph = compilation.graph;
100
+ const evidenceDocuments = [];
101
+ for (const path of workspace.evidence) {
102
+ const loaded = loadEvidence({
103
+ path,
104
+ source: readFileSync(resolve(cwd, path), 'utf8'),
105
+ });
106
+ if (!loaded.ok)
107
+ return failed(loaded.diagnostics);
108
+ evidenceDocuments.push(loaded.evidence);
109
+ }
110
+ const evaluation = evaluateEvidenceWorkspace(graph, evidenceDocuments);
111
+ if (!evaluation.ok)
112
+ return failed(evaluation.diagnostics);
113
+ const result = evaluateProjection(graph, loadedProjection.projection, compilation.profileContext);
114
+ const planned = result.subjects
115
+ .filter(({ id, type }) => type === 'concept' &&
116
+ claimValue(result.claims, id, 'yarramate/lifecycle/status') ===
117
+ 'planned')
118
+ .map(({ id }) => id);
119
+ const plannedIds = new Set(planned);
120
+ const dependsOn = new Map();
121
+ const requiredBy = new Map();
122
+ for (const subject of result.subjects) {
123
+ if (subject.type !== 'relationship')
124
+ continue;
125
+ const claim = result.claims.find(({ id, object }) => id === subject.id && 'ref' in object);
126
+ if (claim === undefined || !('ref' in claim.object))
127
+ continue;
128
+ const orientation = prerequisiteEndpoint(claim.predicate, compilation.profileContext);
129
+ if (orientation === undefined)
130
+ continue;
131
+ const prerequisite = orientation === 'source' ? claim.subject : claim.object.ref;
132
+ const dependent = orientation === 'source' ? claim.object.ref : claim.subject;
133
+ if (prerequisite === dependent ||
134
+ !plannedIds.has(prerequisite) ||
135
+ !plannedIds.has(dependent)) {
136
+ continue;
137
+ }
138
+ dependsOn.set(dependent, (dependsOn.get(dependent) ?? new Set()).add(prerequisite));
139
+ requiredBy.set(prerequisite, (requiredBy.get(prerequisite) ?? new Set()).add(dependent));
140
+ }
141
+ // Deterministic Kahn ordering: everything whose prerequisites are
142
+ // already emitted goes next, lexicographic within a round; a cycle
143
+ // cannot be ordered, so its members are appended sorted and marked.
144
+ const ordered = [];
145
+ const cycles = new Set();
146
+ const emitted = new Set();
147
+ const remaining = new Set([...planned].sort());
148
+ while (remaining.size > 0) {
149
+ const ready = [...remaining]
150
+ .filter((id) => [...(dependsOn.get(id) ?? [])].every((dependency) => emitted.has(dependency)))
151
+ .sort();
152
+ if (ready.length === 0) {
153
+ for (const id of [...remaining].sort()) {
154
+ ordered.push(id);
155
+ cycles.add(id);
156
+ }
157
+ break;
158
+ }
159
+ for (const id of ready) {
160
+ ordered.push(id);
161
+ emitted.add(id);
162
+ remaining.delete(id);
163
+ }
164
+ }
165
+ const relationshipIds = new Set(graph.subjects
166
+ .filter(({ type }) => type === 'relationship')
167
+ .map(({ id }) => id));
168
+ const relationshipEndpoints = new Map();
169
+ const claimOwners = new Map();
170
+ for (const claim of graph.claims) {
171
+ claimOwners.set(claim.id, claim.subject);
172
+ if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
173
+ relationshipEndpoints.set(claim.id, [claim.subject, claim.object.ref]);
174
+ }
175
+ }
176
+ const coverageTargets = (observation) => {
177
+ const target = 'subject' in observation ? observation.subject : observation.claim;
178
+ const endpoints = relationshipEndpoints.get(target);
179
+ if (endpoints !== undefined)
180
+ return endpoints;
181
+ if ('claim' in observation) {
182
+ const owner = claimOwners.get(target);
183
+ if (owner === undefined)
184
+ return [];
185
+ return relationshipEndpoints.get(owner) ?? [owner];
186
+ }
187
+ return [target];
188
+ };
189
+ const coverage = new Map();
190
+ for (const id of planned) {
191
+ coverage.set(id, {
192
+ observations: 0,
193
+ confirmed: 0,
194
+ contradicted: 0,
195
+ unknown: 0,
196
+ notObserved: 0,
197
+ });
198
+ }
199
+ for (const report of evaluation.reports) {
200
+ for (const observation of report.observations) {
201
+ for (const target of coverageTargets(observation)) {
202
+ const tally = coverage.get(target);
203
+ if (tally === undefined)
204
+ continue;
205
+ tally.observations += 1;
206
+ if (observation.result === 'not-observed') {
207
+ tally.notObserved += 1;
208
+ }
209
+ else {
210
+ tally[observation.result] += 1;
211
+ }
212
+ }
213
+ }
214
+ }
215
+ const subjects = ordered.map((id) => {
216
+ const name = claimValue(result.claims, id, 'yarramate/concept/name');
217
+ return {
218
+ id,
219
+ kind: claimValue(result.claims, id, 'yarramate/concept/kind') ?? 'unknown',
220
+ ...(name === undefined ? {} : { name }),
221
+ dependsOn: [...(dependsOn.get(id) ?? [])].sort(),
222
+ requiredBy: [...(requiredBy.get(id) ?? [])].sort(),
223
+ evidence: coverage.get(id),
224
+ ...(cycles.has(id) ? { cycle: true } : {}),
225
+ };
226
+ });
227
+ const projectionLabel = `${loadedProjection.projection.id}@${loadedProjection.projection.version}`;
228
+ if (json) {
229
+ const payload = {
230
+ format: 'yarramate/next-result/v1',
231
+ workspace: workspace.id,
232
+ projection: projectionLabel,
233
+ subjects,
234
+ };
235
+ return {
236
+ exitCode: 0,
237
+ stdout: `${JSON.stringify(payload, null, 2)}\n`,
238
+ stderr: '',
239
+ };
240
+ }
241
+ if (subjects.length === 0) {
242
+ return {
243
+ exitCode: 0,
244
+ stdout: `No planned subjects in projection ${projectionLabel}.\n`,
245
+ stderr: '',
246
+ };
247
+ }
248
+ const width = Math.max(...subjects.map(({ id }) => id.length));
249
+ const lines = [
250
+ `Planned subjects in projection ${projectionLabel} (dependency order):`,
251
+ ...subjects.map((subject) => {
252
+ const clauses = [
253
+ ...(subject.requiredBy.length > 0
254
+ ? [`<- required by ${subject.requiredBy.join(', ')}`]
255
+ : []),
256
+ coverageClause(subject.evidence),
257
+ ...(subject.cycle === true ? ['dependency cycle'] : []),
258
+ ];
259
+ return ` ${subject.id.padEnd(width)} ${clauses.join('; ')}`;
260
+ }),
261
+ ];
262
+ return {
263
+ exitCode: 0,
264
+ stdout: `${lines.join('\n')}\n`,
265
+ stderr: '',
266
+ };
267
+ }
268
+ catch (error) {
269
+ const message = error instanceof Error ? error.message : String(error);
270
+ return { exitCode: 2, stdout: '', stderr: `${message}\n` };
271
+ }
272
+ }
package/dist/profile.d.ts CHANGED
@@ -21,6 +21,11 @@ export interface RelationshipPolicy {
21
21
  readonly intent: string;
22
22
  readonly sourceAspects?: readonly Aspect[];
23
23
  readonly targetAspects?: readonly Aspect[];
24
+ /**
25
+ * Repair hint appended to endpoint-aspect diagnostics (YM404). A remedy,
26
+ * never a correction: the compiler still rejects the input.
27
+ */
28
+ readonly repair?: string;
24
29
  }
25
30
  /**
26
31
  * Safe, intentionally broad semantic constraints. A future licensed
package/dist/profile.js CHANGED
@@ -137,6 +137,7 @@ export const relationshipPolicies = [
137
137
  id: 'assignment',
138
138
  intent: 'Allocate an active structure to behavior or responsibility',
139
139
  sourceAspects: ['active-structure'],
140
+ repair: 'assign from an active-structure element (an actor, component, or node), or use "association"',
140
141
  },
141
142
  { id: 'realization', intent: 'Fulfil a more abstract concept' },
142
143
  { id: 'serving', intent: 'Make behavior or an interface available' },
@@ -144,11 +145,13 @@ export const relationshipPolicies = [
144
145
  id: 'access',
145
146
  intent: 'Read, write, create, or use passive structure',
146
147
  targetAspects: ['passive-structure'],
148
+ repair: 'point "access" at passive structure (a business object, data object, or artifact), or use "association"',
147
149
  },
148
150
  {
149
151
  id: 'influence',
150
152
  intent: 'Affect a motivation concept',
151
153
  targetAspects: ['motivation'],
154
+ repair: 'point "influence" at a motivation concept (a goal, requirement, or principle), or use "association"',
152
155
  },
153
156
  { id: 'association', intent: 'Relevant connection with no stronger meaning' },
154
157
  {
@@ -156,6 +159,7 @@ export const relationshipPolicies = [
156
159
  intent: 'Express temporal or causal precedence',
157
160
  sourceAspects: ['behavior'],
158
161
  targetAspects: ['behavior'],
162
+ repair: 'use "flow" between active-structure elements, or introduce a behavior concept and "assignment"',
159
163
  },
160
164
  { id: 'flow', intent: 'Transfer information, value, goods, or material' },
161
165
  { id: 'specialization', intent: 'Express a more specific form' },
@@ -1,9 +1,17 @@
1
+ import type { SemanticGraph } from './compiler.js';
1
2
  import type { EvidenceLocator, EvidenceReport, EvidenceResult } from './evidence.js';
3
+ export interface AssertedRelationship {
4
+ readonly from: string;
5
+ readonly to: string;
6
+ readonly kind: string;
7
+ readonly name?: string;
8
+ }
2
9
  export interface ReconciliationFinding {
3
10
  readonly target: {
4
11
  readonly type: 'subject' | 'claim';
5
12
  readonly id: string;
6
13
  };
14
+ readonly asserted?: AssertedRelationship;
7
15
  readonly result: Exclude<EvidenceResult, 'confirmed'>;
8
16
  readonly provider: string;
9
17
  readonly evidenceDocument: string;
@@ -20,7 +28,9 @@ export interface ReconciliationReport {
20
28
  readonly contradicted: number;
21
29
  readonly unknown: number;
22
30
  readonly notObserved: number;
31
+ readonly subjectsWithoutEvidence: number;
23
32
  };
24
33
  readonly findings: readonly ReconciliationFinding[];
34
+ readonly unobservedSubjects?: readonly string[];
25
35
  }
26
- export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[]): ReconciliationReport;
36
+ export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[], graph?: SemanticGraph): ReconciliationReport;
@@ -1,4 +1,68 @@
1
- export function reconcileEvidenceReports(workspace, reports) {
1
+ const assertedRelationshipsByClaim = (graph) => {
2
+ const asserted = new Map();
3
+ if (graph === undefined)
4
+ return asserted;
5
+ const relationshipIds = new Set(graph.subjects
6
+ .filter(({ type }) => type === 'relationship')
7
+ .map(({ id }) => id));
8
+ const names = new Map();
9
+ for (const claim of graph.claims) {
10
+ if (claim.predicate === 'yarramate/relationship/name' &&
11
+ 'value' in claim.object) {
12
+ names.set(claim.subject, claim.object.value);
13
+ }
14
+ }
15
+ for (const claim of graph.claims) {
16
+ if (!relationshipIds.has(claim.id) || !('ref' in claim.object))
17
+ continue;
18
+ const name = names.get(claim.id);
19
+ asserted.set(claim.id, {
20
+ from: claim.subject,
21
+ to: claim.object.ref,
22
+ kind: claim.predicate,
23
+ ...(name === undefined ? {} : { name }),
24
+ });
25
+ }
26
+ return asserted;
27
+ };
28
+ const unobservedCurrentConcepts = (graph, reports) => {
29
+ if (graph === undefined)
30
+ return [];
31
+ const relationshipIds = new Set(graph.subjects
32
+ .filter(({ type }) => type === 'relationship')
33
+ .map(({ id }) => id));
34
+ const claimsById = new Map(graph.claims.map((claim) => [claim.id, claim]));
35
+ const observed = new Set();
36
+ for (const report of reports) {
37
+ for (const observation of report.observations) {
38
+ if ('subject' in observation) {
39
+ observed.add(observation.subject);
40
+ continue;
41
+ }
42
+ const claim = claimsById.get(observation.claim);
43
+ if (claim === undefined)
44
+ continue;
45
+ observed.add(claim.subject);
46
+ if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
47
+ observed.add(claim.object.ref);
48
+ }
49
+ }
50
+ }
51
+ const conceptIds = new Set(graph.subjects
52
+ .filter(({ type }) => type === 'concept')
53
+ .map(({ id }) => id));
54
+ return graph.claims
55
+ .filter((claim) => claim.predicate === 'yarramate/lifecycle/status' &&
56
+ 'value' in claim.object &&
57
+ claim.object.value === 'current' &&
58
+ conceptIds.has(claim.subject) &&
59
+ !observed.has(claim.subject))
60
+ .map(({ subject }) => subject)
61
+ .sort((left, right) => left.localeCompare(right));
62
+ };
63
+ export function reconcileEvidenceReports(workspace, reports, graph) {
64
+ const assertedByClaim = assertedRelationshipsByClaim(graph);
65
+ const unobservedSubjects = unobservedCurrentConcepts(graph, reports);
2
66
  const summary = {
3
67
  evidenceDocuments: reports.length,
4
68
  observations: 0,
@@ -7,6 +71,7 @@ export function reconcileEvidenceReports(workspace, reports) {
7
71
  contradicted: 0,
8
72
  unknown: 0,
9
73
  notObserved: 0,
74
+ subjectsWithoutEvidence: unobservedSubjects.length,
10
75
  };
11
76
  const findings = [];
12
77
  for (const report of reports) {
@@ -22,10 +87,13 @@ export function reconcileEvidenceReports(workspace, reports) {
22
87
  else {
23
88
  summary[observation.result] += 1;
24
89
  }
90
+ const target = 'subject' in observation
91
+ ? { type: 'subject', id: observation.subject }
92
+ : { type: 'claim', id: observation.claim };
93
+ const asserted = target.type === 'claim' ? assertedByClaim.get(target.id) : undefined;
25
94
  findings.push({
26
- target: 'subject' in observation
27
- ? { type: 'subject', id: observation.subject }
28
- : { type: 'claim', id: observation.claim },
95
+ target,
96
+ ...(asserted === undefined ? {} : { asserted }),
29
97
  result: observation.result,
30
98
  provider: report.provider,
31
99
  evidenceDocument: report.evidence,
@@ -43,5 +111,6 @@ export function reconcileEvidenceReports(workspace, reports) {
43
111
  workspace,
44
112
  summary,
45
113
  findings,
114
+ ...(unobservedSubjects.length === 0 ? {} : { unobservedSubjects }),
46
115
  };
47
116
  }
@@ -91,7 +91,7 @@ export function runStatusCommand(options, cwd) {
91
91
  });
92
92
  const evaluation = evaluateEvidenceWorkspace(compilation.graph, evidenceDocuments);
93
93
  if (evaluation.ok) {
94
- reconciliation = reconcileEvidenceReports(workspace.id, evaluation.reports).summary;
94
+ reconciliation = reconcileEvidenceReports(workspace.id, evaluation.reports, compilation.graph).summary;
95
95
  }
96
96
  }
97
97
  }
@@ -145,6 +145,9 @@ export function runStatusCommand(options, cwd) {
145
145
  ? ` (${result.reconciliation.contradicted} contradicted, ` +
146
146
  `${result.reconciliation.unknown} unknown, ` +
147
147
  `${result.reconciliation.notObserved} not observed)`
148
+ : '') +
149
+ (result.reconciliation.subjectsWithoutEvidence > 0
150
+ ? `, ${plural(result.reconciliation.subjectsWithoutEvidence, 'current subject')} without evidence`
148
151
  : ''));
149
152
  }
150
153
  lines.push(`Documents: ${documents.map(({ id }) => id).join(', ') || 'none'}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yarramate",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Tool-neutral semantic architecture engine and guided methodology",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,6 +59,7 @@
59
59
  "./schema/likec4-check-result": "./schema/yarramate-likec4-check-result.schema.json",
60
60
  "./schema/state-comparison": "./schema/yarramate-state-comparison.schema.json",
61
61
  "./schema/status-result": "./schema/yarramate-status-result.schema.json",
62
+ "./schema/next-result": "./schema/yarramate-next-result.schema.json",
62
63
  "./schema/graph-v2": "./schema/yarramate-graph-v2.schema.json",
63
64
  "./schema/workspace": "./schema/yarramate-workspace.schema.json",
64
65
  "./schema/evidence": "./schema/yarramate-evidence.schema.json",
@@ -92,11 +93,11 @@
92
93
  "self:contract": "pnpm build && node dist/cli.js context .yarramate/projections/core-contract-foundation.yaml .yarramate/workspace.yaml",
93
94
  "self:evidence": "pnpm build && node dist/cli.js evidence .yarramate/evidence/repository.yaml .yarramate/workspace.yaml",
94
95
  "self:reconcile": "pnpm build && node dist/cli.js reconcile .yarramate/workspace.yaml",
95
- "self:check:likec4": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/integrations/likec4/project.yaml .yarramate/workspace.yaml",
96
- "self:check:likec4:json": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/integrations/likec4/project.yaml --json .yarramate/workspace.yaml",
97
- "self:export:likec4": "pnpm build && node dist/adapters/likec4-cli.js export-project .yarramate/integrations/likec4/project.yaml .yarramate-out/likec4 .yarramate/workspace.yaml",
96
+ "self:check:likec4": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml .yarramate/workspace.yaml",
97
+ "self:check:likec4:json": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml --json .yarramate/workspace.yaml",
98
+ "self:export:likec4": "pnpm build && node dist/adapters/likec4-cli.js export-project .yarramate/likec4-project.yaml .yarramate-out/likec4 .yarramate/workspace.yaml",
98
99
  "validate": "pnpm self:export:likec4 && likec4 validate --no-layout .yarramate-out/likec4",
99
- "verify": "pnpm typecheck && pnpm test && pnpm self:check && pnpm validate",
100
+ "verify": "pnpm typecheck && pnpm build && pnpm test && pnpm self:check && pnpm validate",
100
101
  "test": "vitest run",
101
102
  "typecheck": "tsc --noEmit"
102
103
  },
@@ -20,6 +20,9 @@
20
20
  },
21
21
  "counted": {
22
22
  "$ref": "#/$defs/counted"
23
+ },
24
+ "strict": {
25
+ "$ref": "#/$defs/strict"
23
26
  }
24
27
  },
25
28
  "allOf": [
@@ -50,6 +53,15 @@
50
53
  }
51
54
  ],
52
55
  "$defs": {
56
+ "strict": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["observations", "contradicted"],
60
+ "properties": {
61
+ "observations": { "type": "integer", "minimum": 0 },
62
+ "contradicted": { "type": "integer", "minimum": 0 }
63
+ }
64
+ },
53
65
  "counted": {
54
66
  "type": "object",
55
67
  "additionalProperties": false,
@@ -70,6 +70,7 @@
70
70
  "add",
71
71
  "connect",
72
72
  "new",
73
+ "next",
73
74
  "check",
74
75
  "status",
75
76
  "compile",
@@ -45,6 +45,12 @@
45
45
  }
46
46
  }
47
47
  },
48
+ "inputDigests": {
49
+ "type": "object",
50
+ "additionalProperties": {
51
+ "$ref": "#/$defs/sha256"
52
+ }
53
+ },
48
54
  "files": {
49
55
  "const": [
50
56
  "likec4.config.json",
@@ -51,6 +51,12 @@
51
51
  }
52
52
  }
53
53
  },
54
+ "inputDigests": {
55
+ "type": "object",
56
+ "additionalProperties": {
57
+ "$ref": "#/$defs/sha256"
58
+ }
59
+ },
54
60
  "files": {
55
61
  "const": [
56
62
  "likec4.config.json",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "path": {
43
43
  "type": "string",
44
- "description": "Repository-relative path resolved from the CLI working directory. Parent traversal, absolute paths, and backslashes are rejected.",
44
+ "description": "Path resolved from the project-definition document's directory, matching workspace-manifest semantics. Parent traversal, absolute paths, and backslashes are rejected, so references stay beneath the project document.",
45
45
  "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
46
46
  },
47
47
  "subjectIdentity": {
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://yarramate.org/schema/next-result/v1",
4
+ "title": "YarraMate next result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["format", "workspace", "projection", "subjects"],
8
+ "properties": {
9
+ "format": {
10
+ "const": "yarramate/next-result/v1"
11
+ },
12
+ "workspace": {
13
+ "type": "string",
14
+ "minLength": 1
15
+ },
16
+ "projection": {
17
+ "type": "string",
18
+ "minLength": 1
19
+ },
20
+ "subjects": {
21
+ "type": "array",
22
+ "items": {
23
+ "$ref": "#/$defs/subject"
24
+ }
25
+ }
26
+ },
27
+ "$defs": {
28
+ "subject": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["id", "kind", "dependsOn", "requiredBy", "evidence"],
32
+ "properties": {
33
+ "id": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "kind": {
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "name": {
42
+ "type": "string"
43
+ },
44
+ "dependsOn": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "string",
48
+ "minLength": 1
49
+ }
50
+ },
51
+ "requiredBy": {
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string",
55
+ "minLength": 1
56
+ }
57
+ },
58
+ "evidence": {
59
+ "$ref": "#/$defs/coverage"
60
+ },
61
+ "cycle": {
62
+ "const": true
63
+ }
64
+ }
65
+ },
66
+ "coverage": {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "required": [
70
+ "observations",
71
+ "confirmed",
72
+ "contradicted",
73
+ "unknown",
74
+ "notObserved"
75
+ ],
76
+ "properties": {
77
+ "observations": { "type": "integer", "minimum": 0 },
78
+ "confirmed": { "type": "integer", "minimum": 0 },
79
+ "contradicted": { "type": "integer", "minimum": 0 },
80
+ "unknown": { "type": "integer", "minimum": 0 },
81
+ "notObserved": { "type": "integer", "minimum": 0 }
82
+ }
83
+ }
84
+ }
85
+ }