yarramate 0.3.2 → 0.4.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.
@@ -181,3 +181,109 @@ export function renderProjectionMarkdown(result) {
181
181
  }
182
182
  return `${lines.join('\n')}\n`;
183
183
  }
184
+ const estimateTokens = (text) => Math.ceil(text.length / 4);
185
+ export function renderBudgetedContext(result, budgetTokens) {
186
+ const concepts = result.subjects.filter(({ type }) => type === 'concept');
187
+ const relationships = result.subjects.filter(({ type }) => type === 'relationship');
188
+ const title = result.presentation?.title ?? result.projection;
189
+ const header = [
190
+ `context ${result.projection} — ${title}`,
191
+ `subjects: ${concepts.length} concepts, ${relationships.length} relationships`,
192
+ ];
193
+ const subjectLines = [];
194
+ for (const concept of concepts) {
195
+ const name = claimValue(result.claims, concept.id, 'yarramate/concept/name');
196
+ const kind = claimValue(result.claims, concept.id, 'yarramate/concept/kind') ??
197
+ 'unknown';
198
+ const status = claimValue(result.claims, concept.id, 'yarramate/lifecycle/status');
199
+ subjectLines.push(`- ${concept.id} [${kind.split('#')[1] ?? kind}]` +
200
+ `${name === undefined ? '' : ` ${name}`}` +
201
+ `${status === undefined ? '' : ` (${status})`}`);
202
+ }
203
+ const relationshipLines = [];
204
+ for (const relationship of relationships) {
205
+ const claim = result.claims.find(({ id, object }) => id === relationship.id && 'ref' in object);
206
+ if (claim !== undefined && 'ref' in claim.object) {
207
+ const kind = claim.predicate.split('#')[1] ?? claim.predicate;
208
+ relationshipLines.push(`- ${claim.subject} -${kind}-> ${claim.object.ref}`);
209
+ }
210
+ }
211
+ const descriptionLines = [];
212
+ for (const subject of result.subjects) {
213
+ const description = claimValue(result.claims, subject.id, 'yarramate/concept/description') ??
214
+ claimValue(result.claims, subject.id, 'yarramate/relationship/description');
215
+ if (description !== undefined) {
216
+ descriptionLines.push(`- ${subject.id}: ${description}`);
217
+ }
218
+ }
219
+ const detailPredicates = new Set([
220
+ 'yarramate/ownership/owner',
221
+ 'yarramate/constraint/requires',
222
+ 'yarramate/reference/refers-to',
223
+ 'yarramate/access/mode',
224
+ 'yarramate/state/present-in',
225
+ ]);
226
+ const detailLines = [];
227
+ for (const claim of result.claims) {
228
+ if (!detailPredicates.has(claim.predicate))
229
+ continue;
230
+ const value = 'ref' in claim.object ? claim.object.ref : claim.object.value;
231
+ const predicate = claim.predicate.split('/').pop() ?? claim.predicate;
232
+ detailLines.push(`- ${claim.subject} ${predicate}: ${value}`);
233
+ }
234
+ // Ranked ladder: the two header lines always render; every other line —
235
+ // including the subject skeleton — competes for the remaining budget in
236
+ // priority order, and anything dropped is always announced rather than
237
+ // silently omitted.
238
+ const sections = [
239
+ { heading: 'relationships:', lines: relationshipLines },
240
+ { heading: 'descriptions:', lines: descriptionLines },
241
+ { heading: 'details:', lines: detailLines },
242
+ ];
243
+ const rendered = [...header];
244
+ const omitted = [];
245
+ let spent = estimateTokens(rendered.join('\n'));
246
+ let droppedSubjects = 0;
247
+ for (const line of subjectLines) {
248
+ const cost = estimateTokens(line);
249
+ if (spent + cost > budgetTokens) {
250
+ droppedSubjects += 1;
251
+ continue;
252
+ }
253
+ rendered.push(line);
254
+ spent += cost;
255
+ }
256
+ if (droppedSubjects > 0) {
257
+ omitted.push(`subjects ${droppedSubjects} omitted`);
258
+ }
259
+ for (const section of sections) {
260
+ if (section.lines.length === 0)
261
+ continue;
262
+ const kept = [section.heading];
263
+ let sectionSpent = estimateTokens(section.heading);
264
+ let dropped = 0;
265
+ for (const line of section.lines) {
266
+ const cost = estimateTokens(line);
267
+ if (spent + sectionSpent + cost > budgetTokens) {
268
+ dropped += 1;
269
+ continue;
270
+ }
271
+ kept.push(line);
272
+ sectionSpent += cost;
273
+ }
274
+ if (kept.length > 1) {
275
+ rendered.push('', ...kept);
276
+ spent += sectionSpent;
277
+ }
278
+ else {
279
+ dropped = section.lines.length;
280
+ }
281
+ if (dropped > 0) {
282
+ omitted.push(`${section.heading.replace(':', '')} ${dropped} omitted`);
283
+ }
284
+ }
285
+ if (omitted.length > 0) {
286
+ rendered.push('', `[budget ${budgetTokens}: ${omitted.join('; ')} — raise --budget or use JSON mode for the complete slice]`);
287
+ }
288
+ return `${rendered.join('\n')}\n`;
289
+ }
@@ -1,4 +1,4 @@
1
- import type { ValidateFunction } from 'ajv';
1
+ import type { ErrorObject, ValidateFunction } from 'ajv';
2
2
  import { LineCounter, parseDocument } from 'yaml';
3
3
  import type { Diagnostic, WorkspaceSource } from './compiler.js';
4
4
  export interface SourceLocation {
@@ -20,5 +20,7 @@ export type SourceDocumentResult<T> = {
20
20
  readonly diagnostics: readonly Diagnostic[];
21
21
  };
22
22
  export declare const diagnosticOrder: (left: Diagnostic, right: Diagnostic) => number;
23
+ export declare const describeSchemaViolation: (error: Pick<ErrorObject, "keyword" | "message" | "params">) => string;
24
+ export declare const closestCandidate: (value: string, candidates: Iterable<string>) => string | undefined;
23
25
  export declare function locateSourcePath(sourcePath: string, yaml: ReturnType<typeof parseDocument>, lineCounter: LineCounter, yamlPath: readonly (string | number)[], pointer: string): SourceLocation;
24
26
  export declare function loadSourceDocument<T>(source: WorkspaceSource, validate: ValidateFunction, schemaLabel: string): SourceDocumentResult<T>;
@@ -4,6 +4,48 @@ export const diagnosticOrder = (left, right) => left.path.localeCompare(right.pa
4
4
  left.column - right.column ||
5
5
  left.code.localeCompare(right.code) ||
6
6
  left.message.localeCompare(right.message);
7
+ export const describeSchemaViolation = (error) => {
8
+ const base = error.message ?? error.keyword;
9
+ if (error.keyword === 'const') {
10
+ return `${base}: expected ${JSON.stringify(error.params.allowedValue)}`;
11
+ }
12
+ if (error.keyword === 'enum' &&
13
+ Array.isArray(error.params.allowedValues)) {
14
+ const allowed = error.params.allowedValues;
15
+ const shown = allowed
16
+ .slice(0, 8)
17
+ .map((value) => JSON.stringify(value))
18
+ .join(', ');
19
+ return `${base}: ${shown}${allowed.length > 8 ? ', …' : ''}`;
20
+ }
21
+ return base;
22
+ };
23
+ const editDistance = (left, right) => {
24
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
25
+ for (let i = 1; i <= left.length; i += 1) {
26
+ let diagonal = previous[0];
27
+ previous[0] = i;
28
+ for (let j = 1; j <= right.length; j += 1) {
29
+ const above = previous[j];
30
+ previous[j] = Math.min(above + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
31
+ diagonal = above;
32
+ }
33
+ }
34
+ return previous[right.length];
35
+ };
36
+ export const closestCandidate = (value, candidates) => {
37
+ const threshold = Math.max(2, Math.floor(value.length / 4));
38
+ let best;
39
+ let bestDistance = threshold + 1;
40
+ for (const candidate of [...candidates].sort()) {
41
+ const distance = editDistance(value.toLowerCase(), candidate.toLowerCase());
42
+ if (distance < bestDistance) {
43
+ best = candidate;
44
+ bestDistance = distance;
45
+ }
46
+ }
47
+ return best;
48
+ };
7
49
  export function locateSourcePath(sourcePath, yaml, lineCounter, yamlPath, pointer) {
8
50
  const node = yaml.getIn(yamlPath, true);
9
51
  const offset = typeof node === 'object' &&
@@ -62,7 +104,7 @@ export function loadSourceDocument(source, validate, schemaLabel) {
62
104
  code: 'YM201',
63
105
  message: property
64
106
  ? `Property "${property}" is not allowed`
65
- : `${schemaLabel} schema violation: ${error.message ?? error.keyword}`,
107
+ : `${schemaLabel} schema violation: ${describeSchemaViolation(error)}`,
66
108
  ...location,
67
109
  };
68
110
  })
@@ -0,0 +1,2 @@
1
+ import { type CliResult } from './cli-support.js';
2
+ export declare function runStatusCommand(options: readonly string[], cwd: string): CliResult;
@@ -0,0 +1,171 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parseDocument } from 'yaml';
4
+ import { runCheckCommand } from './check-command.js';
5
+ import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
6
+ import { compileWorkspace } from './compiler.js';
7
+ import { evaluateEvidenceWorkspace, loadEvidence } from './evidence.js';
8
+ import { loadProjection } from './projection.js';
9
+ import { reconcileEvidenceReports, } from './reconciliation.js';
10
+ import { loadWorkspaceManifest } from './workspace.js';
11
+ const plural = (count, singular, pluralForm) => `${count} ${count === 1 ? singular : (pluralForm ?? `${singular}s`)}`;
12
+ export function runStatusCommand(options, cwd) {
13
+ const json = options.includes('--json');
14
+ const paths = options.filter((option) => option !== '--json');
15
+ const [workspacePath] = paths;
16
+ if (paths.length !== 1 ||
17
+ workspacePath === undefined ||
18
+ workspacePath.startsWith('-')) {
19
+ return { exitCode: 2, stdout: '', stderr: usage };
20
+ }
21
+ try {
22
+ const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
23
+ if (parseDocument(manifestSource).get('format') !==
24
+ 'yarramate/workspace/v1') {
25
+ return {
26
+ exitCode: 2,
27
+ stdout: '',
28
+ stderr: 'status requires an explicit workspace manifest (yarramate/workspace/v1)\n',
29
+ };
30
+ }
31
+ const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
32
+ if (!loadedWorkspace.ok) {
33
+ return {
34
+ exitCode: 1,
35
+ stdout: json
36
+ ? diagnosticJson(loadedWorkspace.diagnostics)
37
+ : humanDiagnostics(loadedWorkspace.diagnostics),
38
+ stderr: '',
39
+ };
40
+ }
41
+ const workspace = loadedWorkspace.workspace;
42
+ const checked = runCheckCommand([workspacePath, '--json'], cwd);
43
+ const checkPayload = JSON.parse(checked.stdout);
44
+ const projections = workspace.projections.map((path) => {
45
+ const loaded = loadProjection({
46
+ path,
47
+ source: readFileSync(resolve(cwd, path), 'utf8'),
48
+ });
49
+ return loaded.ok
50
+ ? {
51
+ id: loaded.projection.id,
52
+ path,
53
+ ...(loaded.projection.presentation?.title === undefined
54
+ ? {}
55
+ : { title: loaded.projection.presentation.title }),
56
+ }
57
+ : { id: path, path };
58
+ });
59
+ let documents = workspace.documents.map((path) => ({ id: path, path }));
60
+ let states = [];
61
+ let reconciliation;
62
+ if (checkPayload.ok) {
63
+ const compilation = compileWorkspace([...workspace.profiles, ...workspace.documents].map((path) => ({
64
+ path,
65
+ source: readFileSync(resolve(cwd, path), 'utf8'),
66
+ })));
67
+ if (compilation.ok) {
68
+ const sourceByDocument = new Map(compilation.graph.documents.map((document) => [
69
+ document.id,
70
+ document.source,
71
+ ]));
72
+ documents = workspace.documents.map((path) => {
73
+ const id = [...sourceByDocument.entries()].find(([, source]) => source === path)?.[0];
74
+ return { id: id ?? path, path };
75
+ });
76
+ states = compilation.graph.claims
77
+ .filter(({ predicate }) => predicate === 'yarramate/state/type')
78
+ .map(({ subject, object }) => ({
79
+ id: subject,
80
+ type: 'value' in object && typeof object.value === 'string'
81
+ ? object.value
82
+ : 'baseline',
83
+ }));
84
+ if (workspace.evidence.length > 0) {
85
+ const evidenceDocuments = workspace.evidence.flatMap((path) => {
86
+ const loaded = loadEvidence({
87
+ path,
88
+ source: readFileSync(resolve(cwd, path), 'utf8'),
89
+ });
90
+ return loaded.ok ? [loaded.evidence] : [];
91
+ });
92
+ const evaluation = evaluateEvidenceWorkspace(compilation.graph, evidenceDocuments);
93
+ if (evaluation.ok) {
94
+ reconciliation = reconcileEvidenceReports(workspace.id, evaluation.reports).summary;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ const result = {
100
+ format: 'yarramate/status-result/v1',
101
+ workspace: workspace.id,
102
+ ok: checkPayload.ok,
103
+ check: {
104
+ ok: checkPayload.ok,
105
+ diagnostics: checkPayload.ok ? [] : checkPayload.diagnostics,
106
+ ...(checkPayload.counted === undefined
107
+ ? {}
108
+ : { counted: checkPayload.counted }),
109
+ },
110
+ ...(reconciliation === undefined ? {} : { reconciliation }),
111
+ inventory: {
112
+ documents,
113
+ profiles: workspace.profiles,
114
+ states,
115
+ projections,
116
+ evidence: workspace.evidence,
117
+ adapterMappings: workspace.adapterMappings,
118
+ contracts: workspace.contracts,
119
+ },
120
+ };
121
+ if (json) {
122
+ return {
123
+ exitCode: result.ok ? 0 : 1,
124
+ stdout: `${JSON.stringify(result, null, 2)}\n`,
125
+ stderr: '',
126
+ };
127
+ }
128
+ const lines = [];
129
+ const counted = result.check.counted;
130
+ lines.push(`Workspace ${result.workspace}: check ${result.ok ? 'ok' : 'failing'}` +
131
+ (counted === undefined
132
+ ? ''
133
+ : ` (${plural(counted.concepts, 'concept')}, ` +
134
+ `${plural(counted.relationships, 'relationship')}, ` +
135
+ `${plural(counted.states, 'state')}, ` +
136
+ `${plural(counted.documents, 'document')})`));
137
+ if (!result.ok) {
138
+ lines.push(`Diagnostics: ${plural(result.check.diagnostics.length, 'error')}; run \`yarramate check ${workspacePath}\` for details`);
139
+ }
140
+ if (result.reconciliation !== undefined) {
141
+ lines.push(`Reconciliation: ${plural(result.reconciliation.observations, 'observation')}, ` +
142
+ `${result.reconciliation.confirmed} confirmed, ` +
143
+ `${plural(result.reconciliation.findings, 'finding')}` +
144
+ (result.reconciliation.findings > 0
145
+ ? ` (${result.reconciliation.contradicted} contradicted, ` +
146
+ `${result.reconciliation.unknown} unknown, ` +
147
+ `${result.reconciliation.notObserved} not observed)`
148
+ : ''));
149
+ }
150
+ lines.push(`Documents: ${documents.map(({ id }) => id).join(', ') || 'none'}`);
151
+ if (states.length > 0) {
152
+ lines.push(`States: ${states.map(({ id, type }) => `${id} (${type})`).join(', ')}`);
153
+ }
154
+ lines.push(`Projections: ${projections
155
+ .map(({ id, title }) => title === undefined ? id : `${id} — ${title}`)
156
+ .join('; ') || 'none'}`);
157
+ lines.push(`Profiles: ${workspace.profiles.length} · ` +
158
+ `Evidence: ${workspace.evidence.length} · ` +
159
+ `Adapter mappings: ${workspace.adapterMappings.length} · ` +
160
+ `Contracts: ${workspace.contracts.length}`);
161
+ return {
162
+ exitCode: result.ok ? 0 : 1,
163
+ stdout: `${lines.join('\n')}\n`,
164
+ stderr: '',
165
+ };
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ return { exitCode: 2, stdout: '', stderr: `${message}\n` };
170
+ }
171
+ }
@@ -143,3 +143,51 @@ yarramate-graphify observe \
143
143
  Graphify extraction remains a separate installation and operation. The
144
144
  adapter observes only explicitly mapped nodes and never promotes them into
145
145
  canonical architecture.
146
+
147
+ ## MCP server for agent harnesses
148
+
149
+ Harnesses that load MCP servers can connect the bundled read-only adapter:
150
+
151
+ ```json
152
+ {
153
+ "mcpServers": {
154
+ "yarramate": {
155
+ "command": "yarramate-mcp"
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ It exposes `yarramate_status`, `yarramate_check`, `yarramate_reconcile`,
162
+ and `yarramate_context` (projection path or ad-hoc subjects, with an
163
+ optional token budget). Every tool call executes the same stable CLI in
164
+ the server's working directory; nothing mutates native documents, and
165
+ authoring stays with the CLI and Git review.
166
+
167
+ ## Continuous drift signal in CI
168
+
169
+ The repository root ships a composite GitHub Action that checks the
170
+ workspace and reports intent-vs-evidence drift on every pull request:
171
+
172
+ ```yaml
173
+ name: architecture
174
+ on: pull_request
175
+ jobs:
176
+ drift:
177
+ runs-on: ubuntu-latest
178
+ steps:
179
+ - uses: actions/checkout@v4
180
+ - uses: actions/setup-node@v4
181
+ with:
182
+ node-version: 22
183
+ - uses: yarrasys/yarramate@main
184
+ with:
185
+ workspace: .yarramate/workspace.yaml
186
+ ```
187
+
188
+ The job fails on deterministic correctness errors and, by default, when
189
+ reconciliation reports contradicted claims; unknown and not-observed
190
+ findings are reported in the job summary without failing. Set
191
+ `fail-on-contradiction: 'false'` to make the whole signal advisory. The
192
+ action never mutates sources — it runs only the read-only `check` and
193
+ `reconcile` commands, so it is safe as a required check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yarramate",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Tool-neutral semantic architecture engine and guided methodology",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -58,6 +58,7 @@
58
58
  "./schema/likec4-kind-mapping": "./schema/yarramate-likec4-kind-mapping.schema.json",
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
+ "./schema/status-result": "./schema/yarramate-status-result.schema.json",
61
62
  "./schema/graph-v2": "./schema/yarramate-graph-v2.schema.json",
62
63
  "./schema/workspace": "./schema/yarramate-workspace.schema.json",
63
64
  "./schema/evidence": "./schema/yarramate-evidence.schema.json",
@@ -69,7 +70,8 @@
69
70
  "bin": {
70
71
  "yarramate": "dist/cli.js",
71
72
  "yarramate-likec4": "dist/adapters/likec4-cli.js",
72
- "yarramate-graphify": "dist/adapters/graphify-cli.js"
73
+ "yarramate-graphify": "dist/adapters/graphify-cli.js",
74
+ "yarramate-mcp": "dist/adapters/mcp-cli.js"
73
75
  },
74
76
  "packageManager": "pnpm@11.7.0",
75
77
  "engines": {
@@ -34,7 +34,11 @@
34
34
  "items": {
35
35
  "type": "object",
36
36
  "additionalProperties": false,
37
- "required": ["id", "schema", "packageExport"],
37
+ "required": [
38
+ "id",
39
+ "schema",
40
+ "packageExport"
41
+ ],
38
42
  "properties": {
39
43
  "id": {
40
44
  "$ref": "#/$defs/formatIdentity"
@@ -55,14 +59,19 @@
55
59
  "items": {
56
60
  "type": "object",
57
61
  "additionalProperties": false,
58
- "required": ["name", "binary"],
62
+ "required": [
63
+ "name",
64
+ "binary"
65
+ ],
59
66
  "properties": {
60
67
  "name": {
61
68
  "enum": [
62
69
  "init",
63
70
  "add",
64
71
  "connect",
72
+ "new",
65
73
  "check",
74
+ "status",
66
75
  "compile",
67
76
  "view",
68
77
  "context",