yarramate 1.18.0 → 1.19.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.
@@ -27,6 +27,10 @@ import { validateOperations } from './schema-validation.js';
27
27
  // asserted — it never silently shrinks anything.
28
28
  const SCALAR_CONCEPT_FIELDS = ['kind', 'name', 'description', 'status', 'owner'];
29
29
  const LIST_CONCEPT_FIELDS = ['aka', 'constraints', 'references', 'presentIn', 'attestations', 'distinctFrom', 'supersedes'];
30
+ // The third category (#448). `parts` is the first MAP-valued concept field:
31
+ // it neither replaces like a scalar nor appends like a list, it merges by
32
+ // slot. See `mergeMapField`.
33
+ const MAP_CONCEPT_FIELDS = ['parts'];
30
34
  const SCALAR_RELATIONSHIP_FIELDS = ['kind', 'from', 'to', 'name', 'description', 'status', 'mode', 'content'];
31
35
  const LIST_RELATIONSHIP_FIELDS = ['references', 'presentIn'];
32
36
  // An overlay entry's address is the pair (target, key); everything else it
@@ -233,6 +237,66 @@ const appendListField = (source, map, key, additions) => {
233
237
  }
234
238
  return spliceValue(source, start, valueEnd, `\n${sequenceEntries(merged, indent + 2)}`);
235
239
  };
240
+ // Merge a MAP-valued field by key (#448). A named slot rebinds, an unnamed one
241
+ // is untouched: ADR 0062's convention, where a write enriches what is there and
242
+ // never silently shrinks it. Replacing the whole map would unbind slots the
243
+ // operation never mentioned, which is exactly the silent shrinking that rule
244
+ // forbids.
245
+ //
246
+ // The per-slot work is `setScalarField` against the NESTED map, because
247
+ // inserting or replacing one key of a mapping is what that already does. The
248
+ // source is re-parsed between slots for the same reason the caller re-parses
249
+ // between fields: every splice moves the offsets after it.
250
+ const mergeMapField = (source, locateMap, key, additions) => {
251
+ const entries = Object.entries(additions);
252
+ if (entries.length === 0)
253
+ return source;
254
+ const map = locateMap(source);
255
+ if (map === undefined)
256
+ return source;
257
+ if (map.flow) {
258
+ return rewriteFlowItem(source, map, (fields) => ({
259
+ ...fields,
260
+ [key]: {
261
+ ...(fields[key] ?? {}),
262
+ ...additions,
263
+ },
264
+ }));
265
+ }
266
+ const existing = nestedMap(map, key);
267
+ if (existing === undefined) {
268
+ const pair = pairFor(map, key);
269
+ const indent = fieldIndentOf(source, map);
270
+ const rendered = entries
271
+ .map(([slot, value]) => `${' '.repeat(indent + 2)}${slot}: ${valueText(value)}`)
272
+ .join('\n');
273
+ // A `parts` that exists but is not a block mapping (flow, or empty) is
274
+ // replaced wholesale rather than merged into: there is nothing to preserve
275
+ // that the entries do not already carry.
276
+ if (pair !== undefined) {
277
+ const held = isMap(pair.value)
278
+ ? pair.value.toJSON()
279
+ : {};
280
+ const merged = { ...held, ...additions };
281
+ const [start, valueEnd] = nodeRange(pair.value);
282
+ return spliceValue(source, start, valueEnd, `\n${Object.entries(merged)
283
+ .map(([slot, value]) => `${' '.repeat(indent + 2)}${slot}: ${valueText(value)}`)
284
+ .join('\n')}`);
285
+ }
286
+ return insertBlock(source, itemFieldInsertAt(source, map), `${' '.repeat(indent)}${key}:\n${rendered}\n`);
287
+ }
288
+ let updated = source;
289
+ for (const [slot, value] of entries) {
290
+ const host = locateMap(updated);
291
+ if (host === undefined)
292
+ break;
293
+ const target = nestedMap(host, key);
294
+ if (target === undefined)
295
+ break;
296
+ updated = setScalarField(updated, target, slot, value);
297
+ }
298
+ return updated;
299
+ };
236
300
  // Retraction (#115): delete the field's whole entry, from the start of its
237
301
  // key line through the end of its value's last line. A flow item is
238
302
  // rewritten instead — line-based deletion there would take the whole item
@@ -602,6 +666,14 @@ export const applyOperations = (input) => {
602
666
  continue;
603
667
  source = appendListField(source, itemMap(source, collection, id).map, key, additions);
604
668
  }
669
+ if (operation.op === 'update-concept') {
670
+ for (const key of MAP_CONCEPT_FIELDS) {
671
+ const additions = payload[key];
672
+ if (additions === undefined)
673
+ continue;
674
+ source = mergeMapField(source, (current) => itemMap(current, collection, id)?.map, key, additions);
675
+ }
676
+ }
605
677
  for (const key of removals) {
606
678
  const removed = removeField(source, itemMap(source, collection, id).map, key);
607
679
  if (removed === undefined) {
@@ -735,6 +735,7 @@ export function runAskCommand(options, cwd) {
735
735
  ? {}
736
736
  : { catalogues: report.catalogues }),
737
737
  semantics: report.semantics,
738
+ inputs: report.inputs,
738
739
  summary: report.summary,
739
740
  waves: report.waves,
740
741
  };
package/dist/compiler.js CHANGED
@@ -689,6 +689,7 @@ function compileWorkspaceResolved(parsed) {
689
689
  name: slot,
690
690
  kindIdentity: kind.identity,
691
691
  required: part.required === true,
692
+ kindMatching: part.kindMatching === 'descendants' ? 'descendants' : 'exact',
692
693
  });
693
694
  }
694
695
  if (!slotsOk)
@@ -2130,13 +2131,27 @@ function compileWorkspaceResolved(parsed) {
2130
2131
  }
2131
2132
  boundTo.set(target, slot);
2132
2133
  const actual = kindOfSubject.get(target);
2133
- if (actual !== slotShape.kindIdentity) {
2134
+ // `descendants` admits any kind whose lineage includes the slot kind
2135
+ // (#449), which is what the word already means on catalogue selectors
2136
+ // and on `missing-relationship`. It fails safe: minted wiring is
2137
+ // checked against the relationship table using the ACTUAL bound
2138
+ // subjects' kinds, so a descendant that is not a legal endpoint is
2139
+ // still refused by the ordinary relationship check rather than
2140
+ // slipping through on the pattern's authority.
2141
+ const admitted = actual === slotShape.kindIdentity ||
2142
+ (slotShape.kindMatching === 'descendants' &&
2143
+ actual !== undefined &&
2144
+ (conceptKindByIdentity.get(actual)?.lineage ?? []).includes(slotShape.kindIdentity));
2145
+ if (!admitted) {
2134
2146
  diagnostics.push({
2135
2147
  severity: 'error',
2136
2148
  code: 'YM417',
2137
2149
  message: `Part "${slot}" of "${instance}" binds "${target}", which is ` +
2138
2150
  `"${actual ?? 'not a concept'}"; the pattern declares this part ` +
2139
- `"${slotShape.kindIdentity}"`,
2151
+ `"${slotShape.kindIdentity}"` +
2152
+ (slotShape.kindMatching === 'descendants'
2153
+ ? ' or a kind descending from it'
2154
+ : ''),
2140
2155
  path: where.path,
2141
2156
  pointer: where.pointer,
2142
2157
  line: where.line,
package/dist/index.d.ts CHANGED
@@ -19,4 +19,4 @@ export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, typ
19
19
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
20
20
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
21
21
  export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
22
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CataloguePatternVacancy, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
22
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CataloguePatternVacancy, type CatalogueInput, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
package/dist/index.js CHANGED
@@ -17,4 +17,4 @@ export { applyOperations, landOperations, posixDirectoryOf, } from './apply-comm
17
17
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
18
18
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
19
19
  export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
20
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
20
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
@@ -347,9 +347,69 @@ export interface InterrogationReport {
347
347
  readonly catalogues?: readonly string[];
348
348
  /** {@link INTERROGATION_SEMANTICS_VERSION} at the time of evaluation. */
349
349
  readonly semantics: string;
350
+ /**
351
+ * Which optional inputs the evaluation was GIVEN (#450). A condition that
352
+ * reads one it was not given stays quiet, which is right - the caller did
353
+ * not look - but a quiet condition and a satisfied one are both
354
+ * `open: false`, so without this a host summing closed questions reads
355
+ * "nothing was supplied" as "nothing is missing". For an absence question
356
+ * like `missing-part` the silent direction is "the interview is satisfied",
357
+ * which stops an agent working rather than making it do redundant work.
358
+ *
359
+ * `asked: false` (#375, ADR 0132) says the same thing one level up, for a
360
+ * selector that matched no subject. This is the level below it, for inputs.
361
+ * It is a separate field rather than a third `asked` value because `asked`
362
+ * is published and because "no subject" and "no data" are different facts a
363
+ * host acts on differently.
364
+ *
365
+ * REQUIRED, on ADR 0110's reasoning for `trigger`: the fact exists for every
366
+ * report, so an optional field would force every consumer to write an
367
+ * absent-case branch for a case that cannot occur. Every key is present on
368
+ * every report, so a host reads a boolean rather than testing for presence.
369
+ *
370
+ * To find which QUESTIONS could not be evaluated, join a question's echoed
371
+ * `trigger` to this map through {@link conditionInput}.
372
+ */
373
+ readonly inputs: Readonly<Record<CatalogueInput, boolean>>;
350
374
  readonly summary: InterrogationSummary;
351
375
  readonly waves: readonly ReportWave[];
352
376
  }
377
+ /**
378
+ * What a condition needs in order to mean anything: a subject, or only the
379
+ * workspace (#400).
380
+ *
381
+ * A wave gate evaluates with NO subject, and the catalogue schema offered the
382
+ * whole vocabulary in that position, so a subject-scope condition in
383
+ * `opensWhen` produced a wave that silently never opened (`has-linkage`,
384
+ * `near-duplicate`, `fills-pattern-slot`) or a gate that was silently inert
385
+ * (`missing-linkage`, `isolated`, `missing-claim`, `missing-constraint`) —
386
+ * measured, both halves. Neither was refused. That is the same failure
387
+ * `YM914` already refuses from a different cause: a gate nothing can satisfy
388
+ * is indistinguishable from a gate that is merely unmet.
389
+ *
390
+ * This is a `Record` over the union's discriminant rather than a list of the
391
+ * workspace-scope names, and that is the point. An allowlist cannot fail for
392
+ * the author who wrote it (CONTRIBUTING.md's ninth rule), so a new condition
393
+ * must not be able to arrive and be quietly absent from a gate check. Here it
394
+ * cannot: adding a member to `CatalogueCondition` is a TYPECHECK ERROR until
395
+ * its scope is declared, so the compiler asks the question rather than this
396
+ * table remembering the answer.
397
+ */
398
+ /**
399
+ * An optional input to {@link evaluateCatalogue} that some condition needs in
400
+ * order to mean anything (#450).
401
+ *
402
+ * `catalogues` is not one: it names contributing catalogues in the report and
403
+ * no condition reads it, so withholding it cannot silence anything.
404
+ */
405
+ export type CatalogueInput = 'profileContext' | 'evidence' | 'patternMemberships' | 'patternVacancies';
406
+ /**
407
+ * The input this condition goes quiet without, if any (#450). Published beside
408
+ * {@link conditionScope} so a host can join a report's echoed trigger to its
409
+ * `inputs` and answer the question it actually has: which of the questions in
410
+ * front of me could not be evaluated?
411
+ */
412
+ export declare const conditionInput: (condition: CatalogueCondition) => CatalogueInput | undefined;
353
413
  export declare const conditionScope: (condition: CatalogueCondition) => 'workspace' | 'subject';
354
414
  export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string;
355
415
  export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext, evidence?: readonly CatalogueEvidenceObservation[],
@@ -232,26 +232,59 @@ const linkageHits = (index, condition, subjectId, profileContext) => {
232
232
  });
233
233
  };
234
234
  /**
235
- * What a condition needs in order to mean anything: a subject, or only the
236
- * workspace (#400).
235
+ * Which optional input each condition goes QUIET without, or `undefined` for
236
+ * one that needs none (#450).
237
237
  *
238
- * A wave gate evaluates with NO subject, and the catalogue schema offered the
239
- * whole vocabulary in that position, so a subject-scope condition in
240
- * `opensWhen` produced a wave that silently never opened (`has-linkage`,
241
- * `near-duplicate`, `fills-pattern-slot`) or a gate that was silently inert
242
- * (`missing-linkage`, `isolated`, `missing-claim`, `missing-constraint`) —
243
- * measured, both halves. Neither was refused. That is the same failure
244
- * `YM914` already refuses from a different cause: a gate nothing can satisfy
245
- * is indistinguishable from a gate that is merely unmet.
238
+ * A condition that reads an input it was not given returns `false`, which is
239
+ * correct - the caller did not look, so the answer is unknown rather than
240
+ * negative - but byte-identical to a satisfied condition. This table is what
241
+ * lets a report say which of the two happened.
246
242
  *
247
- * This is a `Record` over the union's discriminant rather than a list of the
248
- * workspace-scope names, and that is the point. An allowlist cannot fail for
249
- * the author who wrote it (CONTRIBUTING.md's ninth rule), so a new condition
250
- * must not be able to arrive and be quietly absent from a gate check. Here it
251
- * cannot: adding a member to `CatalogueCondition` is a TYPECHECK ERROR until
252
- * its scope is declared, so the compiler asks the question rather than this
253
- * table remembering the answer.
243
+ * A `Record` over the union's discriminant for the same reason
244
+ * {@link CONDITION_SCOPE} is one, and the reason is CONTRIBUTING's ninth rule:
245
+ * a hand-written list of "which conditions need data" is a closed enumeration
246
+ * authored by whoever knew today's conditions, and a new condition would join
247
+ * the engine and be quietly absent from it. Here it cannot - adding a member to
248
+ * `CatalogueCondition` is a TYPECHECK ERROR until its dependency is declared.
249
+ *
250
+ * The other direction is closed too: a new member of {@link CatalogueInput}
251
+ * fails to compile until {@link InterrogationReport}'s `inputs` reports it.
252
+ *
253
+ * `unconstrained-kind` is the only condition that goes quiet without
254
+ * `profileContext`. Others pass it to helpers for lineage widening, where its
255
+ * absence narrows what matches rather than silencing the condition.
254
256
  */
257
+ const CONDITION_INPUTS = {
258
+ 'has-any-subject': undefined,
259
+ 'no-subject-of-kind': undefined,
260
+ 'has-subject-of-kind': undefined,
261
+ 'below-subject-count': undefined,
262
+ 'no-state-defined': undefined,
263
+ 'exists-linkage': undefined,
264
+ 'no-linkage-exists': undefined,
265
+ 'missing-claim': undefined,
266
+ 'missing-relationship': undefined,
267
+ isolated: undefined,
268
+ 'missing-linkage': undefined,
269
+ 'has-linkage': undefined,
270
+ 'missing-constraint': undefined,
271
+ 'missing-flow-content': undefined,
272
+ 'missing-reference': undefined,
273
+ 'missing-attestation': undefined,
274
+ 'near-duplicate': undefined,
275
+ 'unconstrained-kind': 'profileContext',
276
+ 'unscoped-succession': undefined,
277
+ 'unchallenged-evidence': 'evidence',
278
+ 'fills-pattern-slot': 'patternMemberships',
279
+ 'missing-part': 'patternVacancies',
280
+ };
281
+ /**
282
+ * The input this condition goes quiet without, if any (#450). Published beside
283
+ * {@link conditionScope} so a host can join a report's echoed trigger to its
284
+ * `inputs` and answer the question it actually has: which of the questions in
285
+ * front of me could not be evaluated?
286
+ */
287
+ export const conditionInput = (condition) => CONDITION_INPUTS[condition.condition];
255
288
  const CONDITION_SCOPE = {
256
289
  'has-any-subject': 'workspace',
257
290
  'no-subject-of-kind': 'workspace',
@@ -648,6 +681,15 @@ patternVacancies) {
648
681
  ? {}
649
682
  : { catalogues }),
650
683
  semantics: INTERROGATION_SEMANTICS_VERSION,
684
+ // What the caller actually handed over (#450). Every key present on every
685
+ // report, so a host reads a boolean rather than testing for presence. A
686
+ // new member of `CatalogueInput` will not compile until it appears here.
687
+ inputs: {
688
+ profileContext: profileContext !== undefined,
689
+ evidence: evidence !== undefined,
690
+ patternMemberships: patternMemberships !== undefined,
691
+ patternVacancies: patternVacancies !== undefined,
692
+ },
651
693
  summary: {
652
694
  // Questions in OPENED waves only (#334, ADR 0125). A closed wave's
653
695
  // questions have not been asked, so counting them in the denominator
@@ -1195,6 +1237,24 @@ export function renderInterrogationReport(report) {
1195
1237
  `${report.summary.open} open ` +
1196
1238
  `(${report.summary.openQuestions} of ${report.summary.questions} questions)`,
1197
1239
  ];
1240
+ // Named only when it MATTERS (#450): an input this catalogue's own triggers
1241
+ // read, that the caller did not supply. Listing every input on every report
1242
+ // would be noise a reader learns to skip, and the one line that mattered
1243
+ // would be skipped with it. Derived from (inputs withheld) x (conditions
1244
+ // actually used), which is the question a reader has rather than the one the
1245
+ // field literally answers.
1246
+ const withheld = [
1247
+ ...new Set(report.waves.flatMap((wave) => wave.questions.flatMap((question) => question.trigger
1248
+ .map((condition) => conditionInput(condition))
1249
+ .filter((input) => input !== undefined && !report.inputs[input])))),
1250
+ ].sort();
1251
+ if (withheld.length > 0) {
1252
+ // Not a warning about the model: a warning about this evaluation. The
1253
+ // questions reading those conditions answered "no" because nothing was
1254
+ // handed over, which is indistinguishable from "satisfied" in the lines
1255
+ // below.
1256
+ lines.push(` note: ${withheld.join(', ')} not supplied — questions reading ${withheld.length === 1 ? 'it' : 'them'} could not be evaluated`);
1257
+ }
1198
1258
  for (const wave of report.waves) {
1199
1259
  lines.push('', `== ${wave.name} ==`);
1200
1260
  // A wave that has not opened must not read like one whose questions are
@@ -1 +1 @@
1
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCompositionResult, type ComposedCatalogue, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
1
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCompositionResult, type ComposedCatalogue, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CataloguePatternVacancy, type CatalogueInput, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
@@ -7,4 +7,4 @@
7
7
  // alone: catalogue loading takes a WorkspaceSource, evaluation takes an
8
8
  // in-memory graph, and a test pins the import graph free of Node builtins.
9
9
  // The same shape the visual-graph projector uses (`./adapter/visual-graph`).
10
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
10
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
@@ -32,6 +32,20 @@ export interface ConceptFields {
32
32
  readonly constraints?: readonly ConstraintReference[];
33
33
  readonly references?: readonly IdentifiedReference[];
34
34
  readonly presentIn?: readonly string[];
35
+ /**
36
+ * The subjects this instance binds into its pattern's slots (ADR 0123),
37
+ * keyed by part name (#448). The first MAP-valued concept field, so it is a
38
+ * third category beside the scalars and the lists rather than a schema line.
39
+ *
40
+ * `update-concept` MERGES by slot: a named slot rebinds, an unnamed one is
41
+ * untouched. That is ADR 0062's convention rather than a new decision — a
42
+ * write enriches what is there and never silently shrinks it — and
43
+ * replace-whole-map would quietly unbind slots the operation never
44
+ * mentioned. Retraction is coarse, `remove: ['parts']`, because a second
45
+ * retraction idiom for one field reads fine to whoever wrote it and traps
46
+ * everyone else.
47
+ */
48
+ readonly parts?: Readonly<Record<string, string>>;
35
49
  readonly attestations?: ReadonlyArray<{
36
50
  readonly topic: string;
37
51
  readonly by: string;