yarramate 1.18.0 → 1.19.1
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/dist/apply-command.js +72 -0
- package/dist/ask-command.js +1 -0
- package/dist/compiler.js +97 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/interrogate-command.d.ts +60 -0
- package/dist/interrogate-command.js +77 -17
- package/dist/interrogation-entry.d.ts +1 -1
- package/dist/interrogation-entry.js +1 -1
- package/dist/operations.d.ts +14 -0
- package/dist/schema-validators-operations.generated.js +95 -31
- package/dist/schema-validators.generated.js +20 -7
- package/dist/visual-app-lib/editor.js +25406 -25257
- package/dist/visual-app-lib/types/interrogate-command.d.ts +60 -0
- package/dist/visual-app-lib/types/operations.d.ts +14 -0
- package/docs/NATIVE-DOCUMENT.md +19 -5
- package/package.json +1 -1
- package/schema/yarramate-ask-result.schema.json +8 -0
- package/schema/yarramate-interrogation-report.schema.json +30 -0
- package/schema/yarramate-operations.schema.json +12 -0
- package/schema/yarramate-pattern.schema.json +4 -0
package/dist/apply-command.js
CHANGED
|
@@ -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) {
|
package/dist/ask-command.js
CHANGED
package/dist/compiler.js
CHANGED
|
@@ -428,63 +428,95 @@ function compileWorkspaceResolved(parsed) {
|
|
|
428
428
|
continue;
|
|
429
429
|
}
|
|
430
430
|
const resolvedConceptKinds = new Map(parentProfile.conceptKinds);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const position = positionFor(['conceptKinds', index, 'parent']);
|
|
448
|
-
profileDiagnostics.push({
|
|
449
|
-
severity: 'error',
|
|
450
|
-
code: 'YM407',
|
|
451
|
-
message: `Concept parent "${kind.parent}" is not available`,
|
|
452
|
-
path: input.path,
|
|
453
|
-
pointer: `/conceptKinds/${index}/parent`,
|
|
454
|
-
line: position.line,
|
|
455
|
-
column: position.col,
|
|
456
|
-
});
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
// OntoClean: an anti-rigid kind cannot subsume a rigid one. The
|
|
460
|
-
// parent's lineage is the full ancestor chain, and subsumption is
|
|
461
|
-
// transitive, so an unannotated kind sitting in between does not
|
|
462
|
-
// launder the violation (ADR 0078).
|
|
463
|
-
if (kind.rigidity === 'rigid') {
|
|
464
|
-
const antiRigidAncestor = parent.lineage.find((ancestor) => conceptKindByIdentity.get(ancestor)?.rigidity === 'anti-rigid');
|
|
465
|
-
if (antiRigidAncestor !== undefined) {
|
|
466
|
-
const position = positionFor(['conceptKinds', index, 'rigidity']);
|
|
431
|
+
// A profile is a SET of kind declarations, so a kind whose parent is
|
|
432
|
+
// declared below it resolves like any other (#470). This used to be one
|
|
433
|
+
// pass in declaration order, which made a forward reference
|
|
434
|
+
// indistinguishable from a parent that does not exist - and `YM407` says
|
|
435
|
+
// "not available", so an author went looking for a missing kind while the
|
|
436
|
+
// file in front of them already declared it three lines down.
|
|
437
|
+
//
|
|
438
|
+
// Rounds until one adds nothing. Everything that compiled before compiles
|
|
439
|
+
// now and resolves on the first round, because parent-first was the only
|
|
440
|
+
// order that worked.
|
|
441
|
+
let pendingKinds = [...value.conceptKinds.entries()];
|
|
442
|
+
for (;;) {
|
|
443
|
+
const deferred = [];
|
|
444
|
+
for (const [index, kind] of pendingKinds) {
|
|
445
|
+
if (resolvedConceptKinds.has(kind.id)) {
|
|
446
|
+
const position = positionFor(['conceptKinds', index, 'id']);
|
|
467
447
|
profileDiagnostics.push({
|
|
468
448
|
severity: 'error',
|
|
469
|
-
code: '
|
|
470
|
-
message: `
|
|
449
|
+
code: 'YM409',
|
|
450
|
+
message: `Concept kind "${kind.id}" conflicts with an inherited kind`,
|
|
471
451
|
path: input.path,
|
|
472
|
-
pointer: `/conceptKinds/${index}/
|
|
452
|
+
pointer: `/conceptKinds/${index}/id`,
|
|
473
453
|
line: position.line,
|
|
474
454
|
column: position.col,
|
|
475
455
|
});
|
|
476
456
|
continue;
|
|
477
457
|
}
|
|
458
|
+
const parent = conceptKindByIdentity.get(kind.parent);
|
|
459
|
+
if (parent === undefined) {
|
|
460
|
+
// Might resolve in a later round. Whether it never resolves at all
|
|
461
|
+
// is decided once the rounds stop, not here.
|
|
462
|
+
deferred.push([index, kind]);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
// OntoClean: an anti-rigid kind cannot subsume a rigid one. The
|
|
466
|
+
// parent's lineage is the full ancestor chain, and subsumption is
|
|
467
|
+
// transitive, so an unannotated kind sitting in between does not
|
|
468
|
+
// launder the violation (ADR 0078).
|
|
469
|
+
if (kind.rigidity === 'rigid') {
|
|
470
|
+
const antiRigidAncestor = parent.lineage.find((ancestor) => conceptKindByIdentity.get(ancestor)?.rigidity === 'anti-rigid');
|
|
471
|
+
if (antiRigidAncestor !== undefined) {
|
|
472
|
+
const position = positionFor(['conceptKinds', index, 'rigidity']);
|
|
473
|
+
profileDiagnostics.push({
|
|
474
|
+
severity: 'error',
|
|
475
|
+
code: 'YM413',
|
|
476
|
+
message: `Rigid concept kind "${kind.id}" specializes anti-rigid kind "${antiRigidAncestor}"; nothing is essentially of an anti-rigid kind, so parent "${kind.id}" under an entity kind, or drop its "rigid" annotation`,
|
|
477
|
+
path: input.path,
|
|
478
|
+
pointer: `/conceptKinds/${index}/rigidity`,
|
|
479
|
+
line: position.line,
|
|
480
|
+
column: position.col,
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const resolved = {
|
|
486
|
+
identity: `${identity}#${kind.id}`,
|
|
487
|
+
aspect: parent.aspect,
|
|
488
|
+
layer: kind.layer ?? parent.layer,
|
|
489
|
+
lineage: [...parent.lineage, `${identity}#${kind.id}`],
|
|
490
|
+
...(kind.rigidity === undefined ? {} : { rigidity: kind.rigidity }),
|
|
491
|
+
};
|
|
492
|
+
resolvedConceptKinds.set(kind.id, resolved);
|
|
493
|
+
conceptKindByIdentity.set(resolved.identity, resolved);
|
|
478
494
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
495
|
+
// No round can help what the last one could not.
|
|
496
|
+
if (deferred.length === 0 || deferred.length === pendingKinds.length) {
|
|
497
|
+
pendingKinds = deferred;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
pendingKinds = deferred;
|
|
501
|
+
}
|
|
502
|
+
// What survived every round names a parent that never arrives. Two
|
|
503
|
+
// causes, and they are different problems for the author: the parent is
|
|
504
|
+
// declared nowhere, or it is in a cycle with this kind and neither can
|
|
505
|
+
// ever have a lineage.
|
|
506
|
+
const unresolvableIds = new Set(pendingKinds.map(([, kind]) => `${identity}#${kind.id}`));
|
|
507
|
+
for (const [index, kind] of pendingKinds) {
|
|
508
|
+
const position = positionFor(['conceptKinds', index, 'parent']);
|
|
509
|
+
profileDiagnostics.push({
|
|
510
|
+
severity: 'error',
|
|
511
|
+
code: 'YM407',
|
|
512
|
+
message: unresolvableIds.has(kind.parent)
|
|
513
|
+
? `Concept parent "${kind.parent}" cannot resolve: it is in a parent cycle with "${kind.id}", so neither kind has a lineage`
|
|
514
|
+
: `Concept parent "${kind.parent}" is not available`,
|
|
515
|
+
path: input.path,
|
|
516
|
+
pointer: `/conceptKinds/${index}/parent`,
|
|
517
|
+
line: position.line,
|
|
518
|
+
column: position.col,
|
|
519
|
+
});
|
|
488
520
|
}
|
|
489
521
|
const resolvedRelationshipKinds = new Map(parentProfile.relationshipKinds);
|
|
490
522
|
for (const [index, kind] of value.relationshipKinds.entries()) {
|
|
@@ -689,6 +721,7 @@ function compileWorkspaceResolved(parsed) {
|
|
|
689
721
|
name: slot,
|
|
690
722
|
kindIdentity: kind.identity,
|
|
691
723
|
required: part.required === true,
|
|
724
|
+
kindMatching: part.kindMatching === 'descendants' ? 'descendants' : 'exact',
|
|
692
725
|
});
|
|
693
726
|
}
|
|
694
727
|
if (!slotsOk)
|
|
@@ -2130,13 +2163,27 @@ function compileWorkspaceResolved(parsed) {
|
|
|
2130
2163
|
}
|
|
2131
2164
|
boundTo.set(target, slot);
|
|
2132
2165
|
const actual = kindOfSubject.get(target);
|
|
2133
|
-
|
|
2166
|
+
// `descendants` admits any kind whose lineage includes the slot kind
|
|
2167
|
+
// (#449), which is what the word already means on catalogue selectors
|
|
2168
|
+
// and on `missing-relationship`. It fails safe: minted wiring is
|
|
2169
|
+
// checked against the relationship table using the ACTUAL bound
|
|
2170
|
+
// subjects' kinds, so a descendant that is not a legal endpoint is
|
|
2171
|
+
// still refused by the ordinary relationship check rather than
|
|
2172
|
+
// slipping through on the pattern's authority.
|
|
2173
|
+
const admitted = actual === slotShape.kindIdentity ||
|
|
2174
|
+
(slotShape.kindMatching === 'descendants' &&
|
|
2175
|
+
actual !== undefined &&
|
|
2176
|
+
(conceptKindByIdentity.get(actual)?.lineage ?? []).includes(slotShape.kindIdentity));
|
|
2177
|
+
if (!admitted) {
|
|
2134
2178
|
diagnostics.push({
|
|
2135
2179
|
severity: 'error',
|
|
2136
2180
|
code: 'YM417',
|
|
2137
2181
|
message: `Part "${slot}" of "${instance}" binds "${target}", which is ` +
|
|
2138
2182
|
`"${actual ?? 'not a concept'}"; the pattern declares this part ` +
|
|
2139
|
-
`"${slotShape.kindIdentity}"
|
|
2183
|
+
`"${slotShape.kindIdentity}"` +
|
|
2184
|
+
(slotShape.kindMatching === 'descendants'
|
|
2185
|
+
? ' or a kind descending from it'
|
|
2186
|
+
: ''),
|
|
2140
2187
|
path: where.path,
|
|
2141
2188
|
pointer: where.pointer,
|
|
2142
2189
|
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
|
-
*
|
|
236
|
-
*
|
|
235
|
+
* Which optional input each condition goes QUIET without, or `undefined` for
|
|
236
|
+
* one that needs none (#450).
|
|
237
237
|
*
|
|
238
|
-
* A
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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';
|
package/dist/operations.d.ts
CHANGED
|
@@ -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;
|