yarramate 0.12.0 → 0.14.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/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![CodeQL](https://github.com/yarrasys/yarramate/actions/workflows/codeql.yml/badge.svg)](https://github.com/yarrasys/yarramate/actions/workflows/codeql.yml)
6
6
  [![license: MIT](https://img.shields.io/github/license/yarrasys/yarramate)](LICENSE)
7
7
 
8
+ Product site: **[yarramate.dev](https://yarramate.dev)**
9
+
8
10
  YarraMate keeps your coding agents' architecture context correct. Declare
9
11
  the design once, as a checked model in git; agents receive prose rendered
10
12
  from it — bounded briefs and open design questions — and the CLI
@@ -17,6 +19,20 @@ whose gaps are found deterministically.
17
19
  > YarraMate is pre-release software. Interfaces may evolve before the first
18
20
  > stable release.
19
21
 
22
+ ## Proven across harnesses
23
+
24
+ The design bet is that the model — not the session — is the state, so any
25
+ agent in any harness can resume the work cold. We tested that adversarially
26
+ on a real product: a Claude Code session worked the design interview all
27
+ day, then an OpenAI Codex session — no shared context, the tool never
28
+ named — resumed it from a ten-line pointer file and the published CLI. It
29
+ answered 63 open design questions, filed two genuine defect reports, and in
30
+ a later session reported that the model "was not merely documentation" — it
31
+ caught an approval-path regression before the release shipped.
32
+
33
+ The full story, with every commit, PR, and release attached:
34
+ [The model is the handover](docs/CASE-STUDY-CROSS-HARNESS.md).
35
+
20
36
  ## Why YarraMate?
21
37
 
22
38
  Architecture documents often drift away from implementation or become tied to
@@ -134,6 +150,7 @@ node dist/cli.js ask .yarramate/workspace.yaml
134
150
  node dist/cli.js ask .yarramate/workspace.yaml "free text about the model"
135
151
  node dist/cli.js ask .yarramate/workspace.yaml --subjects
136
152
  node dist/cli.js ask .yarramate/workspace.yaml --advise "a design question"
153
+ node dist/cli.js ask .yarramate/workspace.yaml --where "compiler"
137
154
  node dist/cli.js check .yarramate/workspace.yaml --json
138
155
  node dist/cli.js reconcile .yarramate/workspace.yaml
139
156
  node dist/cli.js export graph .yarramate/workspace.yaml
@@ -1,7 +1,6 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from './source-document.js';
3
- import adapterMappingSchema from '../schema/yarramate-adapter-mapping.schema.json' with {
4
- type: 'json'
3
+ import adapterMappingSchema from '../schema/yarramate-adapter-mapping.schema.json' with { type: 'json'
5
4
  };
6
5
  const Ajv2020 = Ajv2020Module.default;
7
6
  const validateSchema = new Ajv2020({ allErrors: true }).compile(adapterMappingSchema);
@@ -12,11 +12,9 @@ import { adapterMappingLocation, loadAdapterMapping, validateAdapterMapping, } f
12
12
  import { locateSourcePath } from '../source-document.js';
13
13
  import { prepareLikeC4Export, } from './likec4-prepare.js';
14
14
  import { exportLikeC4Project, loadLikeC4ProjectDefinition, } from './likec4-project.js';
15
- import generatedProjectSchema from '../../schema/yarramate-likec4-generated-project.schema.json' with {
16
- type: 'json'
15
+ import generatedProjectSchema from '../../schema/yarramate-likec4-generated-project.schema.json' with { type: 'json'
17
16
  };
18
- import generatedProjectV2Schema from '../../schema/yarramate-likec4-generated-project-v2.schema.json' with {
19
- type: 'json'
17
+ import generatedProjectV2Schema from '../../schema/yarramate-likec4-generated-project-v2.schema.json' with { type: 'json'
20
18
  };
21
19
  const Ajv2020 = Ajv2020Module.default;
22
20
  const validateGeneratedProjectMarker = new Ajv2020({
@@ -1,7 +1,6 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from '../source-document.js';
3
- import kindMappingSchema from '../../schema/yarramate-likec4-kind-mapping.schema.json' with {
4
- type: 'json'
3
+ import kindMappingSchema from '../../schema/yarramate-likec4-kind-mapping.schema.json' with { type: 'json'
5
4
  };
6
5
  const Ajv2020 = Ajv2020Module.default;
7
6
  const validateSchema = new Ajv2020({ allErrors: true }).compile(kindMappingSchema);
@@ -1,8 +1,7 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { loadSourceDocument } from '../source-document.js';
3
3
  import { exportLikeC4, } from './likec4-export.js';
4
- import likeC4ProjectSchema from '../../schema/yarramate-likec4-project.schema.json' with {
5
- type: 'json'
4
+ import likeC4ProjectSchema from '../../schema/yarramate-likec4-project.schema.json' with { type: 'json'
6
5
  };
7
6
  const Ajv2020 = Ajv2020Module.default;
8
7
  const validateLikeC4Project = new Ajv2020({ allErrors: true }).compile(likeC4ProjectSchema);
@@ -6,8 +6,7 @@ import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
6
6
  import { compileWorkspace } from './compiler.js';
7
7
  import { loadSourceDocument, locateSourcePath, } from './source-document.js';
8
8
  import { loadWorkspaceManifest } from './workspace.js';
9
- import operationsSchema from '../schema/yarramate-operations.schema.json' with {
10
- type: 'json'
9
+ import operationsSchema from '../schema/yarramate-operations.schema.json' with { type: 'json'
11
10
  };
12
11
  const Ajv2020 = Ajv2020Module.default;
13
12
  const validateOperations = new Ajv2020({ allErrors: true }).compile(operationsSchema);
@@ -114,12 +113,15 @@ const itemMap = (source, collection, id) => {
114
113
  const pair = pairFor(root, collection);
115
114
  if (pair === undefined || !isSeq(pair.value))
116
115
  return undefined;
117
- const found = pair.value.items.find((candidate) => isMap(candidate) &&
116
+ const sequence = pair.value;
117
+ const found = sequence.items.find((candidate) => isMap(candidate) &&
118
118
  candidate.items.some((field) => isScalar(field.key) &&
119
119
  field.key.value === 'id' &&
120
120
  isScalar(field.value) &&
121
121
  field.value.value === id));
122
- return found === undefined ? undefined : { map: found };
122
+ return found === undefined
123
+ ? undefined
124
+ : { map: found, sequence };
123
125
  };
124
126
  // The indent item fields sit at, read off the item's own first field.
125
127
  const fieldIndentOf = (source, map) => indentAt(source, nodeRange(map.items[0].key)[0]);
@@ -208,6 +210,40 @@ const removeField = (source, map, key) => {
208
210
  : nodeRange(pair.value)[2];
209
211
  return splice(source, start, lineEndAfter(source, valueEnd), '');
210
212
  };
213
+ // Whole-subject deletion (#123): remove the exact authored item range,
214
+ // marker line included. Unlike field removal — where line deletion on a
215
+ // flow item would silently take the whole item (the 0.8.1 regression) —
216
+ // deleting the whole item is precisely the intent here, so flow-style
217
+ // items in a block sequence share the line-based path. An item inside a
218
+ // flow collection rewrites the collection value instead, and removing
219
+ // the last item leaves an explicit empty collection: the document
220
+ // schema requires the key.
221
+ const removeCollectionItem = (source, collection, id) => {
222
+ const { map, sequence } = itemMap(source, collection, id);
223
+ if (sequence.flow) {
224
+ const remaining = sequence.toJSON().filter((item) => item.id !== id);
225
+ const [start, valueEnd] = nodeRange(sequence);
226
+ return splice(source, start, valueEnd, remaining.length === 0
227
+ ? '[]'
228
+ : stringify(remaining, {
229
+ collectionStyle: 'flow',
230
+ lineWidth: 0,
231
+ }).trimEnd());
232
+ }
233
+ // Node ranges may extend past the trailing newline to the next line
234
+ // start; afterContentLine anchors the removal on the item's own last
235
+ // content line.
236
+ const end = afterContentLine(source, nodeRange(map)[2]);
237
+ if (sequence.items.length === 1) {
238
+ let valueStart = nodeRange(sequence)[0];
239
+ while (valueStart > 0 &&
240
+ (source[valueStart - 1] === ' ' || source[valueStart - 1] === '\n')) {
241
+ valueStart -= 1;
242
+ }
243
+ return splice(source, valueStart, end, ' []\n');
244
+ }
245
+ return splice(source, lineStartOf(source, nodeRange(map)[0]), end, '');
246
+ };
211
247
  // ---------------------------------------------------------------------------
212
248
  export function runApplyCommand(options, cwd) {
213
249
  const json = options.includes('--json');
@@ -253,16 +289,20 @@ export function runApplyCommand(options, cwd) {
253
289
  addedRelationships: 0,
254
290
  updatedConcepts: 0,
255
291
  updatedRelationships: 0,
292
+ deletedConcepts: 0,
293
+ deletedRelationships: 0,
256
294
  };
295
+ const deletions = [];
296
+ const locateOperation = (index, message) => ({
297
+ severity: 'error',
298
+ code: 'YM912',
299
+ message,
300
+ ...locateSourcePath(operationsPath, yaml, lineCounter, ['operations', index, 'document'], `/operations/${index}/document`),
301
+ });
257
302
  for (const [index, operation] of operations.entries()) {
258
303
  const absolute = resolve(cwd, operation.document);
259
304
  const manifestPath = workspaceDocuments.get(absolute);
260
- const locate = (message) => ({
261
- severity: 'error',
262
- code: 'YM912',
263
- message,
264
- ...locateSourcePath(operationsPath, yaml, lineCounter, ['operations', index, 'document'], `/operations/${index}/document`),
265
- });
305
+ const locate = (message) => locateOperation(index, message);
266
306
  if (manifestPath === undefined) {
267
307
  return failed([
268
308
  locate(`Operation ${index} targets "${operation.document}", which is not a document of workspace "${workspace.id}"`),
@@ -280,6 +320,26 @@ export function runApplyCommand(options, cwd) {
280
320
  source = appendCollectionItem(source, 'relationships', operation.relationship);
281
321
  counts.addedRelationships += 1;
282
322
  }
323
+ else if (operation.op === 'delete-concept' ||
324
+ operation.op === 'delete-relationship') {
325
+ const collection = operation.op === 'delete-concept' ? 'concepts' : 'relationships';
326
+ const id = operation.op === 'delete-concept'
327
+ ? operation.concept.id
328
+ : operation.relationship.id;
329
+ if (itemMap(source, collection, id) === undefined) {
330
+ return failed([
331
+ locate(`Operation ${index} deletes "${id}", which does not exist in ${operation.document}`),
332
+ ]);
333
+ }
334
+ source = removeCollectionItem(source, collection, id);
335
+ deletions.push({ index, absolute, id });
336
+ if (operation.op === 'delete-concept') {
337
+ counts.deletedConcepts += 1;
338
+ }
339
+ else {
340
+ counts.deletedRelationships += 1;
341
+ }
342
+ }
283
343
  else {
284
344
  const collection = operation.op === 'update-concept' ? 'concepts' : 'relationships';
285
345
  const payload = (operation.op === 'update-concept'
@@ -334,6 +394,92 @@ export function runApplyCommand(options, cwd) {
334
394
  }
335
395
  candidates.set(absolute, source);
336
396
  }
397
+ // Reference integrity for deletes (#123), evaluated against the
398
+ // post-batch state: stage everything first, then look, so a concept
399
+ // deleted together with its referring relationships in one batch
400
+ // succeeds while a target anything still points at rejects the
401
+ // whole batch. Referring sites are relationship endpoints, owner,
402
+ // constraint and identified references; projection selectors are
403
+ // deliberately unchecked — they tolerate no-match by design. The
404
+ // compile gate below stays the backstop.
405
+ if (deletions.length > 0) {
406
+ const staged = workspace.documents.map((path) => {
407
+ const absolute = resolve(cwd, path);
408
+ return {
409
+ absolute,
410
+ value: parseDocument(candidates.get(absolute) ?? readFileSync(absolute, 'utf8')).toJSON(),
411
+ };
412
+ });
413
+ const qualify = (documentId, reference) => reference.includes('#') ? reference : `${documentId}#${reference}`;
414
+ const referrers = staged.flatMap(({ value }) => {
415
+ const documentId = value?.id;
416
+ if (documentId === undefined)
417
+ return [];
418
+ const sites = [];
419
+ for (const concept of value?.concepts ?? []) {
420
+ const subject = `${documentId}#${concept.id}`;
421
+ if (concept.owner !== undefined) {
422
+ sites.push({
423
+ ref: qualify(documentId, concept.owner),
424
+ subject,
425
+ field: 'owner',
426
+ });
427
+ }
428
+ for (const constraint of concept.constraints ?? []) {
429
+ sites.push({
430
+ ref: qualify(documentId, constraint.ref),
431
+ subject,
432
+ field: 'constraints',
433
+ });
434
+ }
435
+ for (const reference of concept.references ?? []) {
436
+ sites.push({
437
+ ref: qualify(documentId, reference.ref),
438
+ subject,
439
+ field: 'references',
440
+ });
441
+ }
442
+ }
443
+ for (const relationship of value?.relationships ?? []) {
444
+ const subject = `${documentId}#${relationship.id}`;
445
+ for (const endpoint of ['from', 'to']) {
446
+ const reference = relationship[endpoint];
447
+ if (reference !== undefined) {
448
+ sites.push({
449
+ ref: qualify(documentId, reference),
450
+ subject,
451
+ field: endpoint,
452
+ });
453
+ }
454
+ }
455
+ for (const reference of relationship.references ?? []) {
456
+ sites.push({
457
+ ref: qualify(documentId, reference.ref),
458
+ subject,
459
+ field: 'references',
460
+ });
461
+ }
462
+ }
463
+ return sites;
464
+ });
465
+ const documentIds = new Map(staged.map(({ absolute, value }) => [absolute, value?.id]));
466
+ const violations = deletions.flatMap((deletion) => {
467
+ const documentId = documentIds.get(deletion.absolute);
468
+ if (documentId === undefined)
469
+ return [];
470
+ const target = `${documentId}#${deletion.id}`;
471
+ const referring = referrers.filter((site) => site.ref === target);
472
+ if (referring.length === 0)
473
+ return [];
474
+ return [
475
+ locateOperation(deletion.index, `Operation ${deletion.index} deletes "${deletion.id}", which is still referenced by ${referring
476
+ .map((site) => `"${site.subject}" (${site.field})`)
477
+ .join(', ')}`),
478
+ ];
479
+ });
480
+ if (violations.length > 0)
481
+ return failed(violations);
482
+ }
337
483
  // The atomic gate: the whole candidate workspace must compile before a
338
484
  // single byte is written; any diagnostic rejects the entire batch.
339
485
  const compilation = compileWorkspace([...workspace.profiles, ...workspace.documents].map((path) => {
@@ -366,7 +512,9 @@ export function runApplyCommand(options, cwd) {
366
512
  const applied = counts.addedConcepts +
367
513
  counts.addedRelationships +
368
514
  counts.updatedConcepts +
369
- counts.updatedRelationships;
515
+ counts.updatedRelationships +
516
+ counts.deletedConcepts +
517
+ counts.deletedRelationships;
370
518
  return {
371
519
  exitCode: 0,
372
520
  stdout: `Applied ${applied} operation${applied === 1 ? '' : 's'} to ${touched.join(', ')}\n`,
@@ -3,12 +3,12 @@ import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { parseDocument } from 'yaml';
5
5
  import { compareArchitectureStates, } from './architecture-state.js';
6
- import { renderBrief } from './brief.js';
6
+ import { coreLocalKind, renderBrief } from './brief.js';
7
7
  import { deriveChangedSubjects } from './changed.js';
8
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
- import { evaluateEvidenceWorkspace, loadEvidence } from './evidence.js';
11
+ import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
12
12
  import { evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, } from './interrogate-command.js';
13
13
  import { buildNextSubjects, coverageClause, } from './next-command.js';
14
14
  import { conceptKinds, relationshipPolicies, } from './profile.js';
@@ -105,18 +105,118 @@ const plannedLines = (subjects) => {
105
105
  return ` ${subject.id.padEnd(width)} ${clauses.join('; ')}`;
106
106
  });
107
107
  };
108
+ // On a dense graph, hub seeds make 1-hop connected expansion reach most
109
+ // of the model (a focused ask on a 248-concept model reached 243). The
110
+ // cap keeps each seed's most material neighbours and announces the rest
111
+ // (ADR 0070); --neighbours overrides it, 0 lifts it.
112
+ const defaultNeighbourCap = 12;
113
+ const motivationKindIds = new Set(conceptKinds
114
+ .filter(({ layer }) => layer === 'motivation')
115
+ .map(({ id }) => id));
116
+ // Neighbours rank by the same reading the brief ranks paragraphs with
117
+ // under a budget (ADR 0042/0055): motivation first, then planned,
118
+ // current, retired. Ties break on seed affinity (a neighbour touching
119
+ // more seeds is more material to the slice), then id — deterministic.
120
+ const materialityRank = (graph, profileContext, id) => {
121
+ const kind = claimValue(graph.claims, id, 'yarramate/concept/kind');
122
+ const core = kind === undefined
123
+ ? undefined
124
+ : coreLocalKind(kind, profileContext?.conceptKindLineages);
125
+ if (core !== undefined && motivationKindIds.has(core))
126
+ return 0;
127
+ const status = claimValue(graph.claims, id, 'yarramate/lifecycle/status');
128
+ return status === 'planned' ? 1 : status === 'retired' ? 3 : 2;
129
+ };
108
130
  // The one-hop connected neighbourhood every slice and advice mode uses:
109
131
  // the same machinery context --subject exposed, now seeded by matching.
110
- const sliceProjection = (graph, seeds, title, profileContext) => evaluateProjection(graph, {
111
- format: 'yarramate/projection/v1',
112
- id: 'ask-slice',
113
- version: '0.0',
114
- query: { subjects: [...seeds], relationships: 'connected' },
115
- presentation: {
116
- title,
117
- description: `Connected neighbourhood of ${seeds.join(', ')}`,
118
- },
119
- }, profileContext);
132
+ // The capped result is always a subset of the uncapped one — the cap
133
+ // drops neighbours and their edges, never seeds, and never adds edges
134
+ // the connected expansion would not have selected.
135
+ const sliceProjection = (graph, seeds, title, profileContext, neighbourCap) => {
136
+ const full = evaluateProjection(graph, {
137
+ format: 'yarramate/projection/v1',
138
+ id: 'ask-slice',
139
+ version: '0.0',
140
+ query: { subjects: [...seeds], relationships: 'connected' },
141
+ presentation: {
142
+ title,
143
+ description: `Connected neighbourhood of ${seeds.join(', ')}`,
144
+ },
145
+ }, profileContext);
146
+ if (neighbourCap === 0)
147
+ return { result: full };
148
+ const seedSet = new Set(seeds);
149
+ const relationshipIds = new Set(full.subjects
150
+ .filter(({ type }) => type === 'relationship')
151
+ .map(({ id }) => id));
152
+ const endpointsById = new Map();
153
+ for (const claim of full.claims) {
154
+ if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
155
+ endpointsById.set(claim.id, [claim.subject, claim.object.ref]);
156
+ }
157
+ }
158
+ const neighboursOf = new Map(seeds.map((seed) => [seed, []]));
159
+ const affinity = new Map();
160
+ for (const [from, to] of endpointsById.values()) {
161
+ for (const [seed, neighbour] of [
162
+ [from, to],
163
+ [to, from],
164
+ ]) {
165
+ if (!seedSet.has(seed) || seedSet.has(neighbour))
166
+ continue;
167
+ const list = neighboursOf.get(seed);
168
+ if (list !== undefined && !list.includes(neighbour)) {
169
+ list.push(neighbour);
170
+ affinity.set(neighbour, (affinity.get(neighbour) ?? 0) + 1);
171
+ }
172
+ }
173
+ }
174
+ const keptNeighbours = new Set();
175
+ for (const list of neighboursOf.values()) {
176
+ const ordered = [...list].sort((left, right) => materialityRank(graph, profileContext, left) -
177
+ materialityRank(graph, profileContext, right) ||
178
+ (affinity.get(right) ?? 0) - (affinity.get(left) ?? 0) ||
179
+ left.localeCompare(right));
180
+ for (const neighbour of ordered.slice(0, neighbourCap)) {
181
+ keptNeighbours.add(neighbour);
182
+ }
183
+ }
184
+ const allNeighbours = new Set([...neighboursOf.values()].flat());
185
+ const omitted = allNeighbours.size - keptNeighbours.size;
186
+ if (omitted === 0)
187
+ return { result: full };
188
+ const keptConcepts = new Set([...seedSet, ...keptNeighbours]);
189
+ const keptRelationships = new Set([...endpointsById]
190
+ .filter(([, [from, to]]) => keptConcepts.has(from) && keptConcepts.has(to))
191
+ .map(([id]) => id));
192
+ const keptSubjects = new Set([...keptConcepts, ...keptRelationships]);
193
+ const keptDocuments = new Set([...keptConcepts].map((id) => id.slice(0, id.indexOf('#'))));
194
+ const result = {
195
+ ...full,
196
+ documents: full.documents.filter(({ id }) => keptDocuments.has(id)),
197
+ subjects: full.subjects.filter(({ id }) => keptSubjects.has(id)),
198
+ claims: full.claims.filter((claim) => (keptConcepts.has(claim.subject) && !relationshipIds.has(claim.id)) ||
199
+ keptRelationships.has(claim.subject) ||
200
+ keptRelationships.has(claim.id)),
201
+ };
202
+ return {
203
+ result,
204
+ neighbourhood: {
205
+ cap: neighbourCap,
206
+ kept: keptNeighbours.size,
207
+ omitted,
208
+ omittedBySeed: seeds.flatMap((seed) => {
209
+ const dropped = (neighboursOf.get(seed) ?? []).filter((neighbour) => !keptNeighbours.has(neighbour)).length;
210
+ return dropped > 0 ? [{ seed, omitted: dropped }] : [];
211
+ }),
212
+ },
213
+ };
214
+ };
215
+ // The honesty line, in the budgeted-ladder voice (ADR 0042): what was
216
+ // dropped is named, and so is the way to widen.
217
+ const neighbourhoodLine = (neighbourhood) => `[neighbours ${neighbourhood.cap}: ${neighbourhood.omitted} of ` +
218
+ `${neighbourhood.kept + neighbourhood.omitted} neighbours omitted — ` +
219
+ `raise --neighbours or pass --neighbours 0 for the full neighbourhood]`;
120
220
  export function runAskCommand(options, cwd) {
121
221
  let json = false;
122
222
  let subjects = false;
@@ -124,9 +224,11 @@ export function runAskCommand(options, cwd) {
124
224
  let open = false;
125
225
  let kinds = false;
126
226
  let advise = false;
227
+ let where = false;
127
228
  let compare;
128
229
  let changed;
129
230
  let budget;
231
+ let neighbours;
130
232
  let kindFilter;
131
233
  let statusFilter;
132
234
  let cataloguePath;
@@ -157,6 +259,10 @@ export function runAskCommand(options, cwd) {
157
259
  advise = true;
158
260
  continue;
159
261
  }
262
+ if (option === '--where') {
263
+ where = true;
264
+ continue;
265
+ }
160
266
  if (option === '--compare') {
161
267
  const from = options[index + 1];
162
268
  const to = options[index + 2];
@@ -172,6 +278,7 @@ export function runAskCommand(options, cwd) {
172
278
  continue;
173
279
  }
174
280
  if (option === '--budget' ||
281
+ option === '--neighbours' ||
175
282
  option === '--kind' ||
176
283
  option === '--status' ||
177
284
  option === '--catalogue' ||
@@ -186,6 +293,12 @@ export function runAskCommand(options, cwd) {
186
293
  }
187
294
  budget = Number(value);
188
295
  }
296
+ else if (option === '--neighbours') {
297
+ if (neighbours !== undefined || !/^(0|[1-9][0-9]*)$/.test(value)) {
298
+ return { exitCode: 2, stdout: '', stderr: usage };
299
+ }
300
+ neighbours = Number(value);
301
+ }
189
302
  else if (option === '--changed') {
190
303
  if (changed !== undefined) {
191
304
  return { exitCode: 2, stdout: '', stderr: usage };
@@ -231,13 +344,24 @@ export function runAskCommand(options, cwd) {
231
344
  exclusiveModes > 1 ||
232
345
  (advise && exclusiveModes > 0) ||
233
346
  (advise && query.length === 0) ||
347
+ (where &&
348
+ (exclusiveModes > 0 ||
349
+ advise ||
350
+ changed !== undefined ||
351
+ budget !== undefined ||
352
+ neighbours !== undefined ||
353
+ query.length === 0)) ||
234
354
  (query.length > 0 && exclusiveModes > 0) ||
235
355
  (changed !== undefined &&
236
356
  (query.length > 0 || exclusiveModes > 0 || advise)) ||
237
357
  ((kindFilter !== undefined || statusFilter !== undefined) && !subjects) ||
238
358
  (cataloguePath !== undefined && !open && !advise) ||
239
359
  (budget !== undefined &&
240
- (json || (query.length === 0 && !advise && changed === undefined)))) {
360
+ (json || (query.length === 0 && !advise && changed === undefined))) ||
361
+ (neighbours !== undefined &&
362
+ query.length === 0 &&
363
+ !advise &&
364
+ changed === undefined)) {
241
365
  return { exitCode: 2, stdout: '', stderr: usage };
242
366
  }
243
367
  try {
@@ -604,7 +728,7 @@ export function runAskCommand(options, cwd) {
604
728
  projections: workspace.projections.length,
605
729
  uncovered,
606
730
  };
607
- const evaluated = sliceProjection(graph, seeds, `Review slice ${changed}`, compilation.profileContext);
731
+ const { result: evaluated, neighbourhood } = sliceProjection(graph, seeds, `Review slice ${changed}`, compilation.profileContext, neighbours ?? defaultNeighbourCap);
608
732
  const result = {
609
733
  format: 'yarramate/ask-result/v1',
610
734
  workspace: workspace.id,
@@ -613,6 +737,7 @@ export function runAskCommand(options, cwd) {
613
737
  seeds,
614
738
  changed: derived.changed,
615
739
  coverage,
740
+ ...(neighbourhood === undefined ? {} : { neighbourhood }),
616
741
  result: evaluated,
617
742
  };
618
743
  if (changedIds.length === 0) {
@@ -626,6 +751,9 @@ export function runAskCommand(options, cwd) {
626
751
  `${plural(derived.changed.relationships.length, 'relationship')} changed (workspace ${workspace.id})`,
627
752
  '',
628
753
  rendered.trimEnd(),
754
+ ...(neighbourhood === undefined
755
+ ? []
756
+ : ['', neighbourhoodLine(neighbourhood)]),
629
757
  '',
630
758
  uncovered.length === 0
631
759
  ? `Review coverage: every changed subject appears in at least one of the ${coverage.projections} authored projections.`
@@ -638,11 +766,25 @@ export function runAskCommand(options, cwd) {
638
766
  // names a projection file is precise addressing; anything else runs
639
767
  // through free-text seeding, where exact subject ids win.
640
768
  const soleTerm = query.length === 1 ? query[0] : undefined;
641
- const projectionCandidate = soleTerm !== undefined && !advise && existsSync(resolve(cwd, soleTerm))
769
+ const projectionCandidate = soleTerm !== undefined &&
770
+ !advise &&
771
+ !where &&
772
+ existsSync(resolve(cwd, soleTerm))
642
773
  ? resolve(cwd, soleTerm)
643
774
  : undefined;
644
775
  if (projectionCandidate !== undefined &&
645
776
  parseDocument(readFileSync(projectionCandidate, 'utf8')).get('format') === 'yarramate/projection/v1') {
777
+ // A projection file defines its own query; there is no seeded
778
+ // expansion to cap, so an explicit --neighbours is a contradiction
779
+ // rather than something to ignore silently.
780
+ if (neighbours !== undefined) {
781
+ return {
782
+ exitCode: 2,
783
+ stdout: '',
784
+ stderr: `--neighbours applies to seeded slices; ${soleTerm} is a ` +
785
+ 'projection that defines its own query\n',
786
+ };
787
+ }
646
788
  const loaded = loadProjection({
647
789
  path: soleTerm,
648
790
  source: readFileSync(projectionCandidate, 'utf8'),
@@ -672,7 +814,93 @@ export function runAskCommand(options, cwd) {
672
814
  `List the roster: yarramate ask ${workspacePath} --subjects\n`,
673
815
  };
674
816
  }
675
- const evaluated = sliceProjection(graph, resolution.seeds, topic, compilation.profileContext);
817
+ // --where: evidence-backed pointing (ADR 0068). Verified locations for
818
+ // the matched subjects, an explicit list of matched-but-unobserved
819
+ // subjects, and a hand-off note for everything outside the model —
820
+ // authority follows epistemic status, so the routing is stated in the
821
+ // output rather than assumed by the reader.
822
+ if (where) {
823
+ const evidenceDocuments = [];
824
+ for (const path of workspace.evidence) {
825
+ const loaded = loadEvidence({
826
+ path,
827
+ source: readFileSync(resolve(cwd, path), 'utf8'),
828
+ });
829
+ if (!loaded.ok)
830
+ return failed(loaded.diagnostics);
831
+ evidenceDocuments.push(loaded.evidence);
832
+ }
833
+ const subjectOf = (observation) => 'subject' in observation
834
+ ? observation.subject
835
+ : (observation.claim.split('~')[0] ?? observation.claim);
836
+ // Subject- and claim-level observations often share a locator; the
837
+ // pointer is the same either way, so identical entries collapse.
838
+ const entriesBySeed = resolution.seeds.map((seed) => ({
839
+ subject: seed,
840
+ observations: [
841
+ ...new Map(evidenceDocuments
842
+ .flatMap((document) => document.observations
843
+ .filter((observation) => subjectOf(observation) === seed)
844
+ .map((observation) => ({
845
+ uri: observation.evidence.uri,
846
+ result: observation.result,
847
+ provider: document.provider,
848
+ ...(observation.evidence.message === undefined
849
+ ? {}
850
+ : { message: observation.evidence.message }),
851
+ })))
852
+ .map((entry) => [
853
+ `${entry.uri}${entry.result}${entry.provider}${entry.message ?? ''}`,
854
+ entry,
855
+ ])).values(),
856
+ ].sort((left, right) => left.uri.localeCompare(right.uri) ||
857
+ left.result.localeCompare(right.result)),
858
+ }));
859
+ const located = entriesBySeed.filter(({ observations }) => observations.length > 0);
860
+ const unobserved = entriesBySeed
861
+ .filter(({ observations }) => observations.length === 0)
862
+ .map(({ subject }) => subject);
863
+ const note = workspace.evidence.length === 0
864
+ ? 'This workspace declares no evidence overlay, so no location is verified. For code locations, use your search tools; author evidence observations to make locations verifiable.'
865
+ : 'Locations above are verified by evidence overlays. Subjects listed as unobserved are modeled but unlocated. For code outside the model, use your search tools or a code index.';
866
+ const result = {
867
+ format: 'yarramate/ask-result/v1',
868
+ workspace: workspace.id,
869
+ mode: 'where',
870
+ addressing: resolution.addressing,
871
+ topic,
872
+ seeds: resolution.seeds,
873
+ matched: resolution.matched,
874
+ located,
875
+ coverage: { unobserved, note },
876
+ };
877
+ const lines = [
878
+ `Where: "${topic}" — ${plural(resolution.matched, 'concept')} matched` +
879
+ (resolution.matched > resolution.seeds.length
880
+ ? `, seeded from the top ${resolution.seeds.length}`
881
+ : '') +
882
+ `: ${resolution.seeds.join(', ')}`,
883
+ '',
884
+ ];
885
+ for (const entry of located) {
886
+ lines.push(` ${entry.subject}`);
887
+ for (const observation of entry.observations) {
888
+ lines.push(` ${observation.result} ${observation.uri} (${observation.provider})`);
889
+ if (observation.message !== undefined) {
890
+ lines.push(` ${observation.message}`);
891
+ }
892
+ }
893
+ }
894
+ if (located.length === 0) {
895
+ lines.push(' no verified locations');
896
+ }
897
+ if (unobserved.length > 0) {
898
+ lines.push('', ` unobserved — modeled, no evidence: ${unobserved.join(', ')}`);
899
+ }
900
+ lines.push('', note);
901
+ return emit(result, `${lines.join('\n')}\n`);
902
+ }
903
+ const { result: evaluated, neighbourhood } = sliceProjection(graph, resolution.seeds, topic, compilation.profileContext, neighbours ?? defaultNeighbourCap);
676
904
  if (!advise) {
677
905
  const result = {
678
906
  format: 'yarramate/ask-result/v1',
@@ -682,6 +910,7 @@ export function runAskCommand(options, cwd) {
682
910
  topic,
683
911
  seeds: resolution.seeds,
684
912
  matched: resolution.matched,
913
+ ...(neighbourhood === undefined ? {} : { neighbourhood }),
685
914
  result: evaluated,
686
915
  };
687
916
  const rendered = budget === undefined
@@ -694,7 +923,9 @@ export function runAskCommand(options, cwd) {
694
923
  : '') +
695
924
  `: ${resolution.seeds.join(', ')}\n\n`
696
925
  : '';
697
- return emit(result, `${header}${rendered}`);
926
+ return emit(result, neighbourhood === undefined
927
+ ? `${header}${rendered}`
928
+ : `${header}${rendered.trimEnd()}\n\n${neighbourhoodLine(neighbourhood)}\n`);
698
929
  }
699
930
  // --advise: the expert composition. The engine assembles ground
700
931
  // truth — slice, open questions, drift — and stops; the reading and
@@ -774,6 +1005,7 @@ export function runAskCommand(options, cwd) {
774
1005
  seeds: resolution.seeds,
775
1006
  matched: resolution.matched,
776
1007
  slice: brief,
1008
+ ...(neighbourhood === undefined ? {} : { neighbourhood }),
777
1009
  openQuestions,
778
1010
  ...(reconciliation === undefined ? {} : { reconciliation }),
779
1011
  };
@@ -784,6 +1016,9 @@ export function runAskCommand(options, cwd) {
784
1016
  '== Model slice ==',
785
1017
  '',
786
1018
  brief.trimEnd(),
1019
+ ...(neighbourhood === undefined
1020
+ ? []
1021
+ : ['', neighbourhoodLine(neighbourhood)]),
787
1022
  '',
788
1023
  '== Open questions touching this slice ==',
789
1024
  ];
package/dist/brief.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import type { ResolvedProfileContext } from './compiler.js';
2
2
  import type { ProjectionResult } from './projection.js';
3
+ export declare const coreLocalKind: (kind: string, lineages: ReadonlyMap<string, readonly string[]> | undefined) => string | undefined;
3
4
  export declare function renderBrief(result: ProjectionResult, profileContext?: ResolvedProfileContext, budgetTokens?: number): string;
package/dist/brief.js CHANGED
@@ -14,7 +14,9 @@ const claimReferences = (claims, subject, predicate) => claims.flatMap((claim) =
14
14
  : []);
15
15
  // Resolve a qualified kind to its nearest core-profile local id through
16
16
  // declared lineage, mirroring how `next` orients relationships (ADR 0048).
17
- const coreLocalKind = (kind, lineages) => {
17
+ // Exported so ask's neighbour cap ranks neighbours by the same reading
18
+ // the brief ranks paragraphs with (ADR 0070).
19
+ export const coreLocalKind = (kind, lineages) => {
18
20
  for (const candidate of [kind, ...(lineages?.get(kind) ?? [])]) {
19
21
  const separator = candidate.indexOf('#');
20
22
  if (separator !== -1 && candidate.startsWith('yarramate/core@')) {
@@ -7,7 +7,7 @@ export interface CliResult {
7
7
  export declare const isMainModule: (moduleUrl: string, entrypoint: string | undefined) => boolean;
8
8
  export declare const packageVersion: string;
9
9
  export declare const versionResult: (binary: string) => CliResult;
10
- export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
10
+ export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <document-id>#<local-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
11
11
  export declare const diagnosticJson: (diagnostics: unknown) => string;
12
12
  export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counted?: {
13
13
  readonly documents: number;
@@ -18,7 +18,7 @@ export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counte
18
18
  readonly observations: number;
19
19
  readonly contradicted: number;
20
20
  }) => string;
21
- export declare const humanDiagnostics: (diagnostics: readonly Pick<Diagnostic, "path" | "line" | "column" | "code" | "message">[]) => string;
21
+ export declare const humanDiagnostics: (diagnostics: readonly Pick<Diagnostic, 'path' | 'line' | 'column' | 'code' | 'message'>[]) => string;
22
22
  export declare const sortDiagnostics: <T extends Diagnostic>(diagnostics: readonly T[]) => T[];
23
23
  export declare const resolveCliWorkspaceSources: (paths: readonly string[], cwd: string, options?: {
24
24
  readonly includeAdapterMappings?: boolean;
@@ -3,8 +3,7 @@ import { resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { parseDocument } from 'yaml';
5
5
  import { loadWorkspaceManifest } from './workspace.js';
6
- import packageManifest from '../package.json' with {
7
- type: 'json'
6
+ import packageManifest from '../package.json' with { type: 'json'
8
7
  };
9
8
  export const isMainModule = (moduleUrl, entrypoint) => {
10
9
  if (entrypoint === undefined)
@@ -23,7 +22,7 @@ export const versionResult = (binary) => ({
23
22
  stdout: `${binary} ${packageVersion}\n`,
24
23
  stderr: '',
25
24
  });
26
- export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
25
+ export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <document-id>#<local-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
27
26
  export const diagnosticJson = (diagnostics) => `${JSON.stringify({
28
27
  format: 'yarramate/diagnostic-result/v1',
29
28
  diagnostics,
package/dist/compiler.js CHANGED
@@ -2,11 +2,9 @@ import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { LineCounter, parseDocument } from 'yaml';
3
3
  import { conceptKinds, relationshipPolicies, } from './profile.js';
4
4
  import { closestCandidate, describeSchemaViolation, } from './source-document.js';
5
- import documentSchema from '../schema/yarramate-document.schema.json' with {
6
- type: 'json'
5
+ import documentSchema from '../schema/yarramate-document.schema.json' with { type: 'json'
7
6
  };
8
- import profileSchema from '../schema/yarramate-profile.schema.json' with {
9
- type: 'json'
7
+ import profileSchema from '../schema/yarramate-profile.schema.json' with { type: 'json'
10
8
  };
11
9
  const coreProfile = 'yarramate/core@0.1';
12
10
  const Ajv2020 = Ajv2020Module.default;
@@ -1,8 +1,7 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { LineCounter, parseDocument } from 'yaml';
3
3
  import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from './source-document.js';
4
- import coreContractSchema from '../schema/yarramate-core-contract.schema.json' with {
5
- type: 'json'
4
+ import coreContractSchema from '../schema/yarramate-core-contract.schema.json' with { type: 'json'
6
5
  };
7
6
  const Ajv2020 = Ajv2020Module.default;
8
7
  const validateCoreContract = new Ajv2020({ allErrors: true }).compile(coreContractSchema);
package/dist/evidence.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from './source-document.js';
3
- import evidenceSchema from '../schema/yarramate-evidence.schema.json' with {
4
- type: 'json'
3
+ import evidenceSchema from '../schema/yarramate-evidence.schema.json' with { type: 'json'
5
4
  };
6
5
  const Ajv2020 = Ajv2020Module.default;
7
6
  const validateEvidenceSchema = new Ajv2020({ allErrors: true }).compile(evidenceSchema);
@@ -1,7 +1,6 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { loadSourceDocument, locateSourcePath, } from './source-document.js';
3
- import catalogueSchema from '../schema/yarramate-question-catalogue.schema.json' with {
4
- type: 'json'
3
+ import catalogueSchema from '../schema/yarramate-question-catalogue.schema.json' with { type: 'json'
5
4
  };
6
5
  const Ajv2020 = Ajv2020Module.default;
7
6
  const validateCatalogue = new Ajv2020({ allErrors: true }).compile(catalogueSchema);
package/dist/profile.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export declare const layers: readonly ["motivation", "strategy", "business", "application", "technology", "physical", "implementation", "composite"];
2
- export declare const aspects: readonly ["motivation", "active-structure", "behavior", "passive-structure", "composite"];
1
+ export declare const layers: readonly ['motivation', 'strategy', 'business', 'application', 'technology', 'physical', 'implementation', 'composite'];
2
+ export declare const aspects: readonly ['motivation', 'active-structure', 'behavior', 'passive-structure', 'composite'];
3
3
  export type Layer = (typeof layers)[number];
4
4
  export type Aspect = (typeof aspects)[number];
5
5
  export interface ConceptKind {
@@ -14,7 +14,7 @@ export interface ConceptKind {
14
14
  readonly inspiredBy: string;
15
15
  }
16
16
  export declare const conceptKinds: readonly ConceptKind[];
17
- export declare const relationshipKinds: readonly ["composition", "aggregation", "assignment", "realization", "serving", "access", "influence", "association", "triggering", "flow", "specialization"];
17
+ export declare const relationshipKinds: readonly ['composition', 'aggregation', 'assignment', 'realization', 'serving', 'access', 'influence', 'association', 'triggering', 'flow', 'specialization'];
18
18
  export type RelationshipKind = (typeof relationshipKinds)[number];
19
19
  export interface RelationshipPolicy {
20
20
  readonly id: RelationshipKind;
@@ -1,7 +1,6 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { loadSourceDocument } from './source-document.js';
3
- import projectionSchema from '../schema/yarramate-projection.schema.json' with {
4
- type: 'json'
3
+ import projectionSchema from '../schema/yarramate-projection.schema.json' with { type: 'json'
5
4
  };
6
5
  const Ajv2020 = Ajv2020Module.default;
7
6
  const validateProjection = new Ajv2020({ allErrors: true }).compile(projectionSchema);
@@ -20,7 +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;
23
+ export declare const describeSchemaViolation: (error: Pick<ErrorObject, 'keyword' | 'message' | 'params'>) => string;
24
24
  export declare const closestCandidate: (value: string, candidates: Iterable<string>) => string | undefined;
25
25
  export declare function locateSourcePath(sourcePath: string, yaml: ReturnType<typeof parseDocument>, lineCounter: LineCounter, yamlPath: readonly (string | number)[], pointer: string): SourceLocation;
26
26
  export declare function loadSourceDocument<T>(source: WorkspaceSource, validate: ValidateFunction, schemaLabel: string): SourceDocumentResult<T>;
package/dist/workspace.js CHANGED
@@ -2,8 +2,7 @@ import { globSync, realpathSync, statSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, relative, resolve, sep, } from 'node:path';
3
3
  import Ajv2020Module from 'ajv/dist/2020.js';
4
4
  import { diagnosticOrder, loadSourceDocument, } from './source-document.js';
5
- import workspaceSchema from '../schema/yarramate-workspace.schema.json' with {
6
- type: 'json'
5
+ import workspaceSchema from '../schema/yarramate-workspace.schema.json' with { type: 'json'
7
6
  };
8
7
  const Ajv2020 = Ajv2020Module.default;
9
8
  const validateWorkspace = new Ajv2020({ allErrors: true }).compile(workspaceSchema);
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "yarramate",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Tool-neutral semantic architecture engine and guided methodology",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/yarrasys/yarramate.git"
9
9
  },
10
- "homepage": "https://github.com/yarrasys/yarramate#readme",
10
+ "homepage": "https://yarramate.dev",
11
11
  "bugs": {
12
12
  "url": "https://github.com/yarrasys/yarramate/issues"
13
13
  },
@@ -108,9 +108,9 @@
108
108
  "yaml": "^2.8.1"
109
109
  },
110
110
  "devDependencies": {
111
- "@types/node": "^24.0.0",
111
+ "@types/node": "^26.1.2",
112
112
  "likec4": "^1.59.2",
113
- "typescript": "^5.9.0",
113
+ "typescript": "^7.0.2",
114
114
  "vitest": "^4.1.10"
115
115
  }
116
116
  }
@@ -15,13 +15,17 @@
15
15
  "addedConcepts",
16
16
  "addedRelationships",
17
17
  "updatedConcepts",
18
- "updatedRelationships"
18
+ "updatedRelationships",
19
+ "deletedConcepts",
20
+ "deletedRelationships"
19
21
  ],
20
22
  "properties": {
21
23
  "addedConcepts": { "type": "integer", "minimum": 0 },
22
24
  "addedRelationships": { "type": "integer", "minimum": 0 },
23
25
  "updatedConcepts": { "type": "integer", "minimum": 0 },
24
- "updatedRelationships": { "type": "integer", "minimum": 0 }
26
+ "updatedRelationships": { "type": "integer", "minimum": 0 },
27
+ "deletedConcepts": { "type": "integer", "minimum": 0 },
28
+ "deletedRelationships": { "type": "integer", "minimum": 0 }
25
29
  }
26
30
  },
27
31
  "documents": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://yarrasys.dev/schema/yarramate-ask-result.schema.json",
4
4
  "title": "YarraMate ask result",
5
- "description": "One envelope for every consumed-now read: orientation, roster, slice, advice, next, open questions, and state comparison, discriminated by mode.",
5
+ "description": "One envelope for every consumed-now read: orientation, roster, slice, advice, verified locations, next, open questions, and state comparison, discriminated by mode.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "format",
@@ -23,6 +23,7 @@
23
23
  "roster",
24
24
  "slice",
25
25
  "advice",
26
+ "where",
26
27
  "next",
27
28
  "open",
28
29
  "compare",
@@ -262,6 +263,9 @@
262
263
  }
263
264
  },
264
265
  "additionalProperties": false
266
+ },
267
+ "neighbourhood": {
268
+ "$ref": "#/$defs/neighbourhoodOmission"
265
269
  }
266
270
  },
267
271
  "additionalProperties": false
@@ -301,6 +305,9 @@
301
305
  "type": "string",
302
306
  "minLength": 1
303
307
  },
308
+ "neighbourhood": {
309
+ "$ref": "#/$defs/neighbourhoodOmission"
310
+ },
304
311
  "openQuestions": {
305
312
  "type": "array",
306
313
  "items": {
@@ -363,6 +370,119 @@
363
370
  },
364
371
  "additionalProperties": false
365
372
  },
373
+ {
374
+ "type": "object",
375
+ "required": [
376
+ "mode",
377
+ "addressing",
378
+ "topic",
379
+ "seeds",
380
+ "matched",
381
+ "located",
382
+ "coverage"
383
+ ],
384
+ "properties": {
385
+ "format": true,
386
+ "workspace": true,
387
+ "mode": {
388
+ "const": "where"
389
+ },
390
+ "addressing": {
391
+ "enum": [
392
+ "free-text",
393
+ "subjects"
394
+ ]
395
+ },
396
+ "topic": {
397
+ "type": "string",
398
+ "minLength": 1
399
+ },
400
+ "seeds": {
401
+ "type": "array",
402
+ "items": {
403
+ "type": "string",
404
+ "minLength": 1
405
+ }
406
+ },
407
+ "matched": {
408
+ "type": "integer",
409
+ "minimum": 0
410
+ },
411
+ "located": {
412
+ "type": "array",
413
+ "items": {
414
+ "type": "object",
415
+ "required": [
416
+ "subject",
417
+ "observations"
418
+ ],
419
+ "additionalProperties": false,
420
+ "properties": {
421
+ "subject": {
422
+ "type": "string",
423
+ "minLength": 1
424
+ },
425
+ "observations": {
426
+ "type": "array",
427
+ "minItems": 1,
428
+ "items": {
429
+ "type": "object",
430
+ "required": [
431
+ "uri",
432
+ "result",
433
+ "provider"
434
+ ],
435
+ "additionalProperties": false,
436
+ "properties": {
437
+ "uri": {
438
+ "type": "string",
439
+ "minLength": 1
440
+ },
441
+ "result": {
442
+ "enum": [
443
+ "confirmed",
444
+ "contradicted",
445
+ "unknown",
446
+ "not-observed"
447
+ ]
448
+ },
449
+ "provider": {
450
+ "type": "string",
451
+ "minLength": 1
452
+ },
453
+ "message": {
454
+ "type": "string",
455
+ "minLength": 1
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ },
463
+ "coverage": {
464
+ "type": "object",
465
+ "required": [
466
+ "unobserved",
467
+ "note"
468
+ ],
469
+ "additionalProperties": false,
470
+ "properties": {
471
+ "unobserved": {
472
+ "type": "array",
473
+ "items": {
474
+ "type": "string",
475
+ "minLength": 1
476
+ }
477
+ },
478
+ "note": {
479
+ "type": "string",
480
+ "minLength": 1
481
+ }
482
+ }
483
+ }
484
+ }
485
+ },
366
486
  {
367
487
  "type": "object",
368
488
  "required": [
@@ -593,6 +713,52 @@
593
713
  }
594
714
  ],
595
715
  "$defs": {
716
+ "neighbourhoodOmission": {
717
+ "description": "Present only when the per-seed neighbour cap dropped part of the 1-hop expansion (ADR 0070): the cap in force, neighbours kept and omitted in total, and the per-seed omission counts. Absent means the slice is the complete connected neighbourhood.",
718
+ "type": "object",
719
+ "required": [
720
+ "cap",
721
+ "kept",
722
+ "omitted",
723
+ "omittedBySeed"
724
+ ],
725
+ "properties": {
726
+ "cap": {
727
+ "type": "integer",
728
+ "minimum": 1
729
+ },
730
+ "kept": {
731
+ "type": "integer",
732
+ "minimum": 0
733
+ },
734
+ "omitted": {
735
+ "type": "integer",
736
+ "minimum": 1
737
+ },
738
+ "omittedBySeed": {
739
+ "type": "array",
740
+ "items": {
741
+ "type": "object",
742
+ "required": [
743
+ "seed",
744
+ "omitted"
745
+ ],
746
+ "properties": {
747
+ "seed": {
748
+ "type": "string",
749
+ "minLength": 1
750
+ },
751
+ "omitted": {
752
+ "type": "integer",
753
+ "minimum": 1
754
+ }
755
+ },
756
+ "additionalProperties": false
757
+ }
758
+ }
759
+ },
760
+ "additionalProperties": false
761
+ },
596
762
  "conceptEntry": {
597
763
  "type": "object",
598
764
  "required": [
@@ -170,6 +170,18 @@
170
170
  }
171
171
  }
172
172
  },
173
+ "deleteTarget": {
174
+ "type": "object",
175
+ "additionalProperties": false,
176
+ "required": [
177
+ "id"
178
+ ],
179
+ "properties": {
180
+ "id": {
181
+ "$ref": "#/$defs/nonEmptyText"
182
+ }
183
+ }
184
+ },
173
185
  "operation": {
174
186
  "oneOf": [
175
187
  {
@@ -327,6 +339,46 @@
327
339
  }
328
340
  }
329
341
  }
342
+ },
343
+ {
344
+ "type": "object",
345
+ "additionalProperties": false,
346
+ "required": [
347
+ "op",
348
+ "document",
349
+ "concept"
350
+ ],
351
+ "properties": {
352
+ "op": {
353
+ "const": "delete-concept"
354
+ },
355
+ "document": {
356
+ "$ref": "#/$defs/nonEmptyText"
357
+ },
358
+ "concept": {
359
+ "$ref": "#/$defs/deleteTarget"
360
+ }
361
+ }
362
+ },
363
+ {
364
+ "type": "object",
365
+ "additionalProperties": false,
366
+ "required": [
367
+ "op",
368
+ "document",
369
+ "relationship"
370
+ ],
371
+ "properties": {
372
+ "op": {
373
+ "const": "delete-relationship"
374
+ },
375
+ "document": {
376
+ "$ref": "#/$defs/nonEmptyText"
377
+ },
378
+ "relationship": {
379
+ "$ref": "#/$defs/deleteTarget"
380
+ }
381
+ }
330
382
  }
331
383
  ]
332
384
  }
@@ -33,7 +33,10 @@ design questions, and the backlog — planned subjects in dependency
33
33
  order. `ask <workspace.yaml> --subjects` lists every concept;
34
34
  `ask <workspace.yaml> --kinds` lists the declarable vocabulary;
35
35
  `ask <workspace.yaml> "<free text>"` returns the model slice matching
36
- your words.
36
+ your words; `ask <workspace.yaml> --where "<free text>"` returns the
37
+ evidence-verified code locations of matching subjects — prefer those
38
+ over searching when the subject is modeled, and use your own search
39
+ tools beyond the coverage boundary the output states.
37
40
 
38
41
  - Existing implementation is the starting point: follow **Discover an
39
42
  existing project**.
@@ -168,8 +168,12 @@ yarramate apply operations.yaml workspace.yaml
168
168
  ```
169
169
 
170
170
  The whole candidate workspace must compile or nothing is written.
171
- Update operations enrich only — scalars replace, lists append; removals
172
- stay Git edits.
171
+ Update operations enrich by default — scalars replace, lists append — and
172
+ retract explicitly with `remove: [<field> ...]`. Whole subjects leave
173
+ through `delete-concept` / `delete-relationship` (payload: the `id` only),
174
+ rejected while anything still references the target; delete the referring
175
+ relationships in the same batch. To descope, retire (`status: retired`)
176
+ instead — delete only when the history itself is noise.
173
177
 
174
178
  ## Ownership and constraints
175
179