yarramate 1.0.0 → 1.2.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/catalogues/core-enrichment.yaml +32 -1
- package/dist/ask-command.js +1 -0
- package/dist/compiler.d.ts +17 -0
- package/dist/compiler.js +64 -5
- package/dist/design-command.js +67 -1
- package/dist/evidence.d.ts +27 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interrogate-command.d.ts +57 -17
- package/dist/interrogate-command.js +48 -1
- package/dist/interrogation-entry.d.ts +1 -0
- package/dist/interrogation-entry.js +10 -0
- package/dist/reconciliation.d.ts +7 -0
- package/dist/reconciliation.js +15 -1
- package/dist/subject-references.js +8 -0
- package/dist/visual-app/assets/{index-BLcpI8WN.js → index-CcmfL3oY.js} +1 -1
- package/dist/visual-app/index.html +1 -1
- package/dist/visual-app-lib/editor.js +27031 -26884
- package/dist/visual-app-lib/types/compiler.d.ts +17 -0
- package/dist/visual-app-lib/types/evidence.d.ts +27 -2
- package/docs/CONSUMING-YARRAMATE.md +47 -0
- package/package.json +5 -1
- package/schema/yarramate-ask-result.schema.json +12 -0
- package/schema/yarramate-design-step.schema.json +382 -1
- package/schema/yarramate-document.schema.json +30 -1
- package/schema/yarramate-evidence.schema.json +50 -0
- package/schema/yarramate-interrogation-report.schema.json +480 -23
- package/schema/yarramate-question-catalogue.schema.json +14 -3
- package/schema/yarramate-reconciliation-report.schema.json +6 -1
- package/skills/yarramate-architecture/SKILL.md +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
format: yarramate/question-catalogue/v1
|
|
2
2
|
id: core-enrichment
|
|
3
|
-
version: "1.
|
|
3
|
+
version: "1.1"
|
|
4
4
|
profile: yarramate/core@0.1
|
|
5
5
|
presentation:
|
|
6
6
|
title: Core enrichment interview
|
|
@@ -1549,3 +1549,34 @@ questions:
|
|
|
1549
1549
|
falsifiable. If nothing in scope is assigned to it, reclassify it
|
|
1550
1550
|
to a kind that does carry a claim, or retire it with a description
|
|
1551
1551
|
saying why it sits outside this architecture.
|
|
1552
|
+
|
|
1553
|
+
- id: succession-unscoped
|
|
1554
|
+
wave: hygiene
|
|
1555
|
+
since: "1.1"
|
|
1556
|
+
scope: subject
|
|
1557
|
+
subjects: {}
|
|
1558
|
+
trigger:
|
|
1559
|
+
- condition: unscoped-succession
|
|
1560
|
+
question: >-
|
|
1561
|
+
{subject.name} supersedes a predecessor that is still current. In what
|
|
1562
|
+
respect does it supersede it, and what remains of the predecessor?
|
|
1563
|
+
askPlain: >-
|
|
1564
|
+
{subject.name} takes over from something that is still here. What part
|
|
1565
|
+
does it take over, and what is the old one still needed for?
|
|
1566
|
+
materiality: >-
|
|
1567
|
+
A succession that replaced its predecessor outright says so by the
|
|
1568
|
+
predecessor being gone. One where both are still current is usually
|
|
1569
|
+
partial, and the part it does not cover is exactly what a reader
|
|
1570
|
+
planning against the model needs to know. An unqualified claim reads
|
|
1571
|
+
as a total replacement to every surface that consumes the field, and
|
|
1572
|
+
`ask --compare` reads the field rather than the prose beside it, so a
|
|
1573
|
+
partial succession recorded without its scope turns into a declared
|
|
1574
|
+
removal of something nobody is removing.
|
|
1575
|
+
authority: either
|
|
1576
|
+
resolution: >-
|
|
1577
|
+
Record the respect on the succession entry:
|
|
1578
|
+
`supersedes: [{ subject: <predecessor>, inRespectOf: <the part taken
|
|
1579
|
+
over> }]`. If the succession really is total, retire the predecessor,
|
|
1580
|
+
which says the same thing without a qualifier. If the two subjects
|
|
1581
|
+
simply coexist and neither takes over from the other, the succession
|
|
1582
|
+
is the wrong claim; remove it.
|
package/dist/ask-command.js
CHANGED
package/dist/compiler.d.ts
CHANGED
|
@@ -135,6 +135,23 @@ export type IncrementalCompilationResult = ({
|
|
|
135
135
|
readonly incremental: boolean;
|
|
136
136
|
readonly cache: CompilationCache;
|
|
137
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* A succession entry: a bare predecessor id, or one with the respect in which
|
|
140
|
+
* it was superseded.
|
|
141
|
+
*
|
|
142
|
+
* The scope is load-bearing rather than decorative. A model claimed that Zoekt
|
|
143
|
+
* superseded the Elasticsearch indexer, unqualified, while the source it was
|
|
144
|
+
* built from says Zoekt "handles only code search and does not replace
|
|
145
|
+
* Elasticsearch". The prose carried the qualifier and the field could not, and
|
|
146
|
+
* `ask --compare` reads the field, so the declared target architecture became
|
|
147
|
+
* the deletion of a component that is not being deleted (ADR 0109).
|
|
148
|
+
*/
|
|
149
|
+
export type NativeSuccession = string | {
|
|
150
|
+
readonly subject: string;
|
|
151
|
+
readonly inRespectOf: string;
|
|
152
|
+
};
|
|
153
|
+
export declare const successionSubject: (entry: NativeSuccession) => string;
|
|
154
|
+
export declare const successionScope: (entry: NativeSuccession) => string | undefined;
|
|
138
155
|
export { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue, parseAttestationClaimValue, parseConstraintExpectsValue, type AttestationClaimParts, type ConstraintExpectsParts, } from './graph-claims.js';
|
|
139
156
|
interface ResolvedPosition {
|
|
140
157
|
readonly line: number;
|
package/dist/compiler.js
CHANGED
|
@@ -48,6 +48,8 @@ const presenceClaimId = (subject, state) => `${subject}~present-in-${utf8Hex(sta
|
|
|
48
48
|
// position, so reordering the YAML leaves the graph byte-identical.
|
|
49
49
|
const aliasClaimId = (subject, alias) => `${subject}~alias-${utf8Hex(alias)}`;
|
|
50
50
|
const distinctFromClaimId = (subject, other) => `${subject}~distinct-from-${utf8Hex(other)}`;
|
|
51
|
+
export const successionSubject = (entry) => typeof entry === 'string' ? entry : entry.subject;
|
|
52
|
+
export const successionScope = (entry) => typeof entry === 'string' ? undefined : entry.inRespectOf;
|
|
51
53
|
const supersedesClaimId = (subject, predecessor) => `${subject}~supersedes-${utf8Hex(predecessor)}`;
|
|
52
54
|
export { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue, parseAttestationClaimValue, parseConstraintExpectsValue, } from './graph-claims.js';
|
|
53
55
|
const localKindId = (identity) => identity.slice(identity.indexOf('#') + 1);
|
|
@@ -605,6 +607,7 @@ function compileWorkspaceResolved(parsed) {
|
|
|
605
607
|
}
|
|
606
608
|
seenDocumentIds.add(value.id);
|
|
607
609
|
}
|
|
610
|
+
const forbidRules = [];
|
|
608
611
|
const conceptByQualifiedId = new Map(documents.flatMap(({ value }) => value.concepts.map((concept) => [
|
|
609
612
|
concept.id,
|
|
610
613
|
{
|
|
@@ -613,6 +616,20 @@ function compileWorkspaceResolved(parsed) {
|
|
|
613
616
|
document: value.id,
|
|
614
617
|
},
|
|
615
618
|
])));
|
|
619
|
+
for (const { value } of documents) {
|
|
620
|
+
for (const concept of value.concepts) {
|
|
621
|
+
for (const rule of concept.forbids ?? []) {
|
|
622
|
+
forbidRules.push({
|
|
623
|
+
declaredBy: concept.id,
|
|
624
|
+
relationship: rule.relationship,
|
|
625
|
+
...(rule.from === undefined ? {} : { from: rule.from }),
|
|
626
|
+
...(rule.to === undefined ? {} : { to: rule.to }),
|
|
627
|
+
exceptFrom: new Set(rule.exceptFrom ?? []),
|
|
628
|
+
exceptTo: new Set(rule.exceptTo ?? []),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
616
633
|
// Subject identity is the authored id, unique across the workspace, so a
|
|
617
634
|
// reference resolves as written. Kept as a named step rather than inlined
|
|
618
635
|
// because every reference in the model passes through here, and that is
|
|
@@ -653,7 +670,7 @@ function compileWorkspaceResolved(parsed) {
|
|
|
653
670
|
: [
|
|
654
671
|
[
|
|
655
672
|
concept.id,
|
|
656
|
-
concept.supersedes.map((predecessor) => qualifyReference(value.id, predecessor)),
|
|
673
|
+
concept.supersedes.map((predecessor) => qualifyReference(value.id, successionSubject(predecessor))),
|
|
657
674
|
],
|
|
658
675
|
])));
|
|
659
676
|
// Self-succession is a cycle of length one, but it has its own diagnostic
|
|
@@ -885,14 +902,14 @@ function compileWorkspaceResolved(parsed) {
|
|
|
885
902
|
}
|
|
886
903
|
for (const [supersedesIndex, predecessor] of (concept.supersedes ?? []).entries()) {
|
|
887
904
|
const pointer = `/concepts/${index}/supersedes/${supersedesIndex}`;
|
|
888
|
-
const predecessorIdentity = qualifyReference(value.id, predecessor);
|
|
905
|
+
const predecessorIdentity = qualifyReference(value.id, successionSubject(predecessor));
|
|
889
906
|
const subjectIdentity = concept.id;
|
|
890
907
|
if (!conceptByQualifiedId.has(predecessorIdentity)) {
|
|
891
908
|
const source = location(['concepts', index, 'supersedes', supersedesIndex], pointer);
|
|
892
909
|
diagnostics.push({
|
|
893
910
|
severity: 'error',
|
|
894
911
|
code: 'YM312',
|
|
895
|
-
message: `Unresolved succession reference "${predecessor}"`,
|
|
912
|
+
message: `Unresolved succession reference "${successionSubject(predecessor)}"`,
|
|
896
913
|
path: input.path,
|
|
897
914
|
pointer,
|
|
898
915
|
line: source.line,
|
|
@@ -1186,6 +1203,32 @@ function compileWorkspaceResolved(parsed) {
|
|
|
1186
1203
|
const tail = candidates.length === 0
|
|
1187
1204
|
? ''
|
|
1188
1205
|
: `; ArchiMate 3.2 permits: ${candidates.join(', ')}`;
|
|
1206
|
+
// A declared rule about the graph, checked against the graph. The
|
|
1207
|
+
// field is new, so no existing model can violate one: this can only
|
|
1208
|
+
// fire on a rule someone deliberately wrote (ADR 0108).
|
|
1209
|
+
for (const rule of forbidRules) {
|
|
1210
|
+
const kindMatches = rule.relationship === relationship.kind ||
|
|
1211
|
+
rule.relationship === policy.coreKind;
|
|
1212
|
+
if (!kindMatches)
|
|
1213
|
+
continue;
|
|
1214
|
+
if (rule.from !== undefined && rule.from !== relationship.from) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (rule.to !== undefined && rule.to !== relationship.to)
|
|
1218
|
+
continue;
|
|
1219
|
+
if (rule.exceptFrom.has(relationship.from))
|
|
1220
|
+
continue;
|
|
1221
|
+
if (rule.exceptTo.has(relationship.to))
|
|
1222
|
+
continue;
|
|
1223
|
+
const pointer = `/relationships/${index}/kind`;
|
|
1224
|
+
diagnostics.push({
|
|
1225
|
+
severity: 'error',
|
|
1226
|
+
code: 'YM415',
|
|
1227
|
+
message: `Relationship "${relationship.kind}" from "${relationship.from}" ` +
|
|
1228
|
+
`to "${relationship.to}" is forbidden by "${rule.declaredBy}"`,
|
|
1229
|
+
...location(['relationships', index, 'kind'], pointer),
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1189
1232
|
if (!permitted.has(policy.coreKind)) {
|
|
1190
1233
|
const pointer = `/relationships/${index}/kind`;
|
|
1191
1234
|
const source = location(['relationships', index, 'kind'], pointer);
|
|
@@ -1304,15 +1347,31 @@ function compileWorkspaceResolved(parsed) {
|
|
|
1304
1347
|
// predecessor is not required to be retired: the transition period
|
|
1305
1348
|
// during which both are current is real.
|
|
1306
1349
|
for (const [supersedesIndex, predecessor] of (concept.supersedes ?? []).entries()) {
|
|
1307
|
-
const predecessorIdentity = qualifyReference(value.id, predecessor);
|
|
1350
|
+
const predecessorIdentity = qualifyReference(value.id, successionSubject(predecessor));
|
|
1351
|
+
const scope = successionScope(predecessor);
|
|
1352
|
+
const successionSource = location(['concepts', index, 'supersedes', supersedesIndex], `/concepts/${index}/supersedes/${supersedesIndex}`);
|
|
1308
1353
|
claims.push({
|
|
1309
1354
|
id: supersedesClaimId(subject, predecessorIdentity),
|
|
1310
1355
|
subject,
|
|
1311
1356
|
predicate: 'yarramate/lineage/supersedes',
|
|
1312
1357
|
object: { ref: predecessorIdentity },
|
|
1313
1358
|
origin: 'declared',
|
|
1314
|
-
source:
|
|
1359
|
+
source: successionSource,
|
|
1315
1360
|
});
|
|
1361
|
+
// The respect is a claim of its own rather than a field on the
|
|
1362
|
+
// succession claim: `GraphClaim` is a triple, and widening it would
|
|
1363
|
+
// widen the published graph schema for one optional string. Its id is
|
|
1364
|
+
// the succession claim's, suffixed, so the two correlate.
|
|
1365
|
+
if (scope !== undefined) {
|
|
1366
|
+
claims.push({
|
|
1367
|
+
id: `${supersedesClaimId(subject, predecessorIdentity)}~respect`,
|
|
1368
|
+
subject,
|
|
1369
|
+
predicate: 'yarramate/lineage/supersedes-respect',
|
|
1370
|
+
object: { value: scope },
|
|
1371
|
+
origin: 'declared',
|
|
1372
|
+
source: successionSource,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1316
1375
|
}
|
|
1317
1376
|
if (concept.owner !== undefined) {
|
|
1318
1377
|
claims.push({
|
package/dist/design-command.js
CHANGED
|
@@ -37,6 +37,7 @@ const selectStep = (report, subjectFilter, askPlainById) => {
|
|
|
37
37
|
: { askPlain: askPlainTemplate.trim() }),
|
|
38
38
|
materiality: question.materiality,
|
|
39
39
|
resolution: question.resolution,
|
|
40
|
+
trigger: question.trigger,
|
|
40
41
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
41
42
|
};
|
|
42
43
|
}
|
|
@@ -57,6 +58,7 @@ const selectStep = (report, subjectFilter, askPlainById) => {
|
|
|
57
58
|
: { askPlain: renderQuestion(askPlainTemplate, first.id, first.name) }),
|
|
58
59
|
materiality: question.materiality,
|
|
59
60
|
resolution: question.resolution,
|
|
61
|
+
trigger: question.trigger,
|
|
60
62
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
61
63
|
subject: {
|
|
62
64
|
id: first.id,
|
|
@@ -74,6 +76,56 @@ const selectStep = (report, subjectFilter, askPlainById) => {
|
|
|
74
76
|
}
|
|
75
77
|
return null;
|
|
76
78
|
};
|
|
79
|
+
const localKind = (qualified) => {
|
|
80
|
+
const hash = qualified.lastIndexOf('#');
|
|
81
|
+
return hash === -1 ? qualified : qualified.slice(hash + 1);
|
|
82
|
+
};
|
|
83
|
+
const skeletonHeader = (documentAddress, op) => [
|
|
84
|
+
'',
|
|
85
|
+
'Prefilled skeleton (edit the <placeholders>, save as operations.yaml):',
|
|
86
|
+
' format: yarramate/operations/v1',
|
|
87
|
+
' operations:',
|
|
88
|
+
` - op: ${op}`,
|
|
89
|
+
` document: ${documentAddress}`,
|
|
90
|
+
];
|
|
91
|
+
// The skeleton is a rendering of the step's trigger (#289), printed only
|
|
92
|
+
// when a single condition maps unambiguously onto one operation, so a
|
|
93
|
+
// wrong skeleton is never offered; every other trigger leaves the output
|
|
94
|
+
// exactly as before. Kinds print as local names: that is the form a
|
|
95
|
+
// native document declares.
|
|
96
|
+
const renderSkeleton = (step, documentAddress) => {
|
|
97
|
+
if (documentAddress === undefined || step.trigger.length !== 1)
|
|
98
|
+
return [];
|
|
99
|
+
const condition = step.trigger[0];
|
|
100
|
+
if (condition.condition === 'no-subject-of-kind') {
|
|
101
|
+
const kinds = condition.kinds.map(localKind);
|
|
102
|
+
const alternatives = kinds.length > 1 ? ` # or: ${kinds.slice(1).join(', ')}` : '';
|
|
103
|
+
return [
|
|
104
|
+
...skeletonHeader(documentAddress, 'add-concept'),
|
|
105
|
+
' concept:',
|
|
106
|
+
' id: <kebab-case-id>',
|
|
107
|
+
` kind: ${kinds[0]}${alternatives}`,
|
|
108
|
+
' name: <one line>',
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
if (condition.condition === 'missing-relationship' &&
|
|
112
|
+
step.subject !== undefined) {
|
|
113
|
+
const kinds = condition.kinds.map(localKind);
|
|
114
|
+
const alternatives = kinds.length > 1 ? ` # or: ${kinds.slice(1).join(', ')}` : '';
|
|
115
|
+
const swap = condition.direction === 'any' ? ' # or swap the endpoints' : '';
|
|
116
|
+
const from = condition.direction === 'incoming' ? '<counterpart-id>' : step.subject.id;
|
|
117
|
+
const to = condition.direction === 'incoming' ? step.subject.id : '<counterpart-id>';
|
|
118
|
+
return [
|
|
119
|
+
...skeletonHeader(documentAddress, 'add-relationship'),
|
|
120
|
+
' relationship:',
|
|
121
|
+
' id: <kebab-case-id>',
|
|
122
|
+
` kind: ${kinds[0]}${alternatives}`,
|
|
123
|
+
` from: ${from}${swap}`,
|
|
124
|
+
` to: ${to}`,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
};
|
|
77
129
|
export function runDesignCommand(options, cwd) {
|
|
78
130
|
const json = options.includes('--json');
|
|
79
131
|
// Facilitation is a rendering preference, not an interview mode: the
|
|
@@ -232,7 +284,21 @@ export function runDesignCommand(options, cwd) {
|
|
|
232
284
|
if (slice !== undefined) {
|
|
233
285
|
lines.push('', 'Subject slice:', '', slice.trimEnd());
|
|
234
286
|
}
|
|
235
|
-
|
|
287
|
+
// The skeleton's document address is the manifest-relative form
|
|
288
|
+
// when the first document sits under the manifest directory - the
|
|
289
|
+
// address an author naturally writes and apply accepts (#216) -
|
|
290
|
+
// falling back to the workspace path, which apply also accepts.
|
|
291
|
+
const manifestDirectory = workspacePath.includes('/')
|
|
292
|
+
? workspacePath.slice(0, workspacePath.lastIndexOf('/'))
|
|
293
|
+
: '';
|
|
294
|
+
const firstDocument = workspace.documents[0];
|
|
295
|
+
const documentAddress = firstDocument === undefined
|
|
296
|
+
? undefined
|
|
297
|
+
: manifestDirectory !== '' &&
|
|
298
|
+
firstDocument.startsWith(`${manifestDirectory}/`)
|
|
299
|
+
? firstDocument.slice(manifestDirectory.length + 1)
|
|
300
|
+
: firstDocument;
|
|
301
|
+
lines.push('', 'Answer by updating the model (one atomic batch):', ` yarramate apply <operations.yaml> ${workspacePath}`, ...renderSkeleton(step, documentAddress), `Then re-run: yarramate design ${workspacePath}`);
|
|
236
302
|
}
|
|
237
303
|
return { exitCode: 0, stdout: `${lines.join('\n')}\n`, stderr: '' };
|
|
238
304
|
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -8,15 +8,39 @@ export interface EvidenceObservedValue {
|
|
|
8
8
|
readonly key: string;
|
|
9
9
|
readonly value: string;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* One search a provider ran and found nothing at, recorded so a reader can
|
|
13
|
+
* re-run it. yarramate never executes it: the engine has no access to the
|
|
14
|
+
* subject tree, and gaining one would be a different decision (ADR 0107).
|
|
15
|
+
*/
|
|
16
|
+
export type SearchProbe = {
|
|
17
|
+
readonly glob: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly grep: string;
|
|
20
|
+
readonly paths?: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A figure quoted in an evidence message, with how it was produced, so a
|
|
24
|
+
* reader can tell a measured number from a remembered one and re-derive it at
|
|
25
|
+
* a later commit.
|
|
26
|
+
*/
|
|
27
|
+
export interface Measurement {
|
|
28
|
+
readonly value: string;
|
|
29
|
+
readonly method: string;
|
|
30
|
+
}
|
|
31
|
+
interface ObservationProvenance {
|
|
32
|
+
readonly searched?: readonly SearchProbe[];
|
|
33
|
+
readonly measured?: readonly Measurement[];
|
|
34
|
+
}
|
|
11
35
|
export type EvidenceObservation = ({
|
|
12
36
|
readonly subject: string;
|
|
13
37
|
readonly result: EvidenceResult;
|
|
14
38
|
readonly evidence: EvidenceLocator;
|
|
15
|
-
} & Partial<EvidenceObservedValue>) | ({
|
|
39
|
+
} & ObservationProvenance & Partial<EvidenceObservedValue>) | ({
|
|
16
40
|
readonly claim: string;
|
|
17
41
|
readonly result: EvidenceResult;
|
|
18
42
|
readonly evidence: EvidenceLocator;
|
|
19
|
-
} & Partial<EvidenceObservedValue>);
|
|
43
|
+
} & ObservationProvenance & Partial<EvidenceObservedValue>);
|
|
20
44
|
export interface EvidenceDocument {
|
|
21
45
|
readonly format: 'yarramate/evidence/v1';
|
|
22
46
|
readonly id: string;
|
|
@@ -60,3 +84,4 @@ export type EvidenceWorkspaceEvaluationResult = {
|
|
|
60
84
|
export declare function loadEvidence(source: WorkspaceSource): EvidenceLoadResult;
|
|
61
85
|
export declare function evaluateEvidence(graph: SemanticGraph, evidence: EvidenceDocument): EvidenceEvaluationResult;
|
|
62
86
|
export declare function evaluateEvidenceWorkspace(graph: SemanticGraph, evidenceDocuments: readonly EvidenceDocument[]): EvidenceWorkspaceEvaluationResult;
|
|
87
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -18,3 +18,4 @@ export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, typ
|
|
|
18
18
|
export { connectableKinds, draftRelationship, proposeRelationshipId, } from './relationship-drafting.js';
|
|
19
19
|
export { draftConcept, proposeConceptId } from './concept-drafting.js';
|
|
20
20
|
export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
|
|
21
|
+
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, 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
|
@@ -16,3 +16,4 @@ export { applyOperations, landOperations, posixDirectoryOf, } from './apply-comm
|
|
|
16
16
|
export { connectableKinds, draftRelationship, proposeRelationshipId, } from './relationship-drafting.js';
|
|
17
17
|
export { draftConcept, proposeConceptId } from './concept-drafting.js';
|
|
18
18
|
export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
|
|
19
|
+
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
|
|
@@ -1,11 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { Diagnostic, ResolvedProfileContext, SemanticGraph, WorkspaceSource } from './compiler.js';
|
|
2
|
+
/**
|
|
3
|
+
* The version of condition evaluation itself, not of the package.
|
|
4
|
+
*
|
|
5
|
+
* A report says which catalogue asked its questions. It could not say which
|
|
6
|
+
* engine answered them, so a consumer holding stored answers could tell a
|
|
7
|
+
* model change from a catalogue deepening (via `since`) but not from a change
|
|
8
|
+
* in what a condition means. ADR 0097 replaced four aspect rules with the
|
|
9
|
+
* ArchiMate 3.2 table and flipped `missing-relationship` answers for unchanged
|
|
10
|
+
* models and unchanged questions; ADR 0083's `unconstrained-kind` goes
|
|
11
|
+
* near-empty under that same table. Neither was visible in any report.
|
|
12
|
+
*
|
|
13
|
+
* **Bump this when an existing question's answer can change for an unchanged
|
|
14
|
+
* model.** Do not bump it for anything else: not a release, not a new
|
|
15
|
+
* condition, not a catalogue edit, not a rendering change. A version that
|
|
16
|
+
* moves when answers did not is a version consumers learn to ignore.
|
|
17
|
+
*
|
|
18
|
+
* `test/interrogation-semantics.test.ts` fingerprints every condition against
|
|
19
|
+
* a fixture and fails if evaluation moves without this bumping, so the rule is
|
|
20
|
+
* enforced rather than remembered.
|
|
21
|
+
*/
|
|
22
|
+
export declare const INTERROGATION_SEMANTICS_VERSION = "1";
|
|
23
|
+
export interface CatalogueSelector {
|
|
24
|
+
/**
|
|
25
|
+
* Kinds to select. Absent selects every concept, which is what a
|
|
26
|
+
* kind-agnostic condition wants: succession can be declared on any subject,
|
|
27
|
+
* so enumerating the kinds that may carry it would be a list nobody can keep
|
|
28
|
+
* right rather than a constraint (ADR 0109).
|
|
29
|
+
*/
|
|
30
|
+
readonly kinds?: readonly string[];
|
|
4
31
|
readonly kindMatching?: 'exact' | 'descendants';
|
|
5
32
|
readonly statuses?: readonly string[];
|
|
6
33
|
readonly documents?: readonly string[];
|
|
7
34
|
}
|
|
8
|
-
type CatalogueCondition = {
|
|
35
|
+
export type CatalogueCondition = {
|
|
9
36
|
readonly condition: 'missing-claim';
|
|
10
37
|
readonly predicate: string;
|
|
11
38
|
} | {
|
|
@@ -56,6 +83,8 @@ type CatalogueCondition = {
|
|
|
56
83
|
readonly condition: 'near-duplicate';
|
|
57
84
|
} | {
|
|
58
85
|
readonly condition: 'unconstrained-kind';
|
|
86
|
+
} | {
|
|
87
|
+
readonly condition: 'unscoped-succession';
|
|
59
88
|
};
|
|
60
89
|
export interface CatalogueQuestion {
|
|
61
90
|
readonly id: string;
|
|
@@ -86,12 +115,12 @@ export interface QuestionCatalogue {
|
|
|
86
115
|
}[];
|
|
87
116
|
readonly questions: readonly CatalogueQuestion[];
|
|
88
117
|
}
|
|
89
|
-
interface OpenSubject {
|
|
118
|
+
export interface OpenSubject {
|
|
90
119
|
readonly id: string;
|
|
91
120
|
readonly name?: string;
|
|
92
121
|
readonly question: string;
|
|
93
122
|
}
|
|
94
|
-
interface ReportQuestion {
|
|
123
|
+
export interface ReportQuestion {
|
|
95
124
|
readonly id: string;
|
|
96
125
|
readonly scope: 'workspace' | 'subject';
|
|
97
126
|
readonly authority: 'human' | 'agent' | 'either';
|
|
@@ -99,23 +128,35 @@ interface ReportQuestion {
|
|
|
99
128
|
readonly question: string;
|
|
100
129
|
readonly materiality: string;
|
|
101
130
|
readonly resolution: string;
|
|
131
|
+
/**
|
|
132
|
+
* The catalogue trigger, verbatim (#289). The conditions that opened a
|
|
133
|
+
* question are its machine-readable answer shape: a host builds the
|
|
134
|
+
* matching affordance (a prefilled form, an operations skeleton) from
|
|
135
|
+
* them instead of re-deriving the shape from its own catalogue copy and
|
|
136
|
+
* drifting from engine semantics.
|
|
137
|
+
*/
|
|
138
|
+
readonly trigger: readonly CatalogueCondition[];
|
|
102
139
|
readonly since?: string;
|
|
103
140
|
readonly subjects?: readonly OpenSubject[];
|
|
104
141
|
}
|
|
142
|
+
export interface ReportWave {
|
|
143
|
+
readonly id: string;
|
|
144
|
+
readonly name: string;
|
|
145
|
+
readonly questions: readonly ReportQuestion[];
|
|
146
|
+
}
|
|
147
|
+
export interface InterrogationSummary {
|
|
148
|
+
readonly questions: number;
|
|
149
|
+
readonly openQuestions: number;
|
|
150
|
+
readonly open: number;
|
|
151
|
+
}
|
|
105
152
|
export interface InterrogationReport {
|
|
106
153
|
readonly format: 'yarramate/interrogation-report/v1';
|
|
107
154
|
readonly workspace: string;
|
|
108
155
|
readonly catalogue: string;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
};
|
|
114
|
-
readonly waves: readonly {
|
|
115
|
-
readonly id: string;
|
|
116
|
-
readonly name: string;
|
|
117
|
-
readonly questions: readonly ReportQuestion[];
|
|
118
|
-
}[];
|
|
156
|
+
/** {@link INTERROGATION_SEMANTICS_VERSION} at the time of evaluation. */
|
|
157
|
+
readonly semantics: string;
|
|
158
|
+
readonly summary: InterrogationSummary;
|
|
159
|
+
readonly waves: readonly ReportWave[];
|
|
119
160
|
}
|
|
120
161
|
export declare const renderQuestion: (template: string, subjectId: string, subjectName: string | undefined, counterparts?: readonly string[]) => string;
|
|
121
162
|
export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext): Omit<InterrogationReport, 'workspace'>;
|
|
@@ -128,4 +169,3 @@ export type CatalogueLoadResult = {
|
|
|
128
169
|
};
|
|
129
170
|
export declare function loadQuestionCatalogue(catalogueSource: WorkspaceSource): CatalogueLoadResult;
|
|
130
171
|
export declare function renderInterrogationReport(report: InterrogationReport): string;
|
|
131
|
-
export {};
|
|
@@ -10,6 +10,27 @@ import catalogueSchema from '../schema/yarramate-question-catalogue.schema.json'
|
|
|
10
10
|
const ajv2020Module = Ajv2020Module;
|
|
11
11
|
const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
|
|
12
12
|
const validateCatalogue = new Ajv2020({ allErrors: true }).compile(catalogueSchema);
|
|
13
|
+
/**
|
|
14
|
+
* The version of condition evaluation itself, not of the package.
|
|
15
|
+
*
|
|
16
|
+
* A report says which catalogue asked its questions. It could not say which
|
|
17
|
+
* engine answered them, so a consumer holding stored answers could tell a
|
|
18
|
+
* model change from a catalogue deepening (via `since`) but not from a change
|
|
19
|
+
* in what a condition means. ADR 0097 replaced four aspect rules with the
|
|
20
|
+
* ArchiMate 3.2 table and flipped `missing-relationship` answers for unchanged
|
|
21
|
+
* models and unchanged questions; ADR 0083's `unconstrained-kind` goes
|
|
22
|
+
* near-empty under that same table. Neither was visible in any report.
|
|
23
|
+
*
|
|
24
|
+
* **Bump this when an existing question's answer can change for an unchanged
|
|
25
|
+
* model.** Do not bump it for anything else: not a release, not a new
|
|
26
|
+
* condition, not a catalogue edit, not a rendering change. A version that
|
|
27
|
+
* moves when answers did not is a version consumers learn to ignore.
|
|
28
|
+
*
|
|
29
|
+
* `test/interrogation-semantics.test.ts` fingerprints every condition against
|
|
30
|
+
* a fixture and fails if evaluation moves without this bumping, so the rule is
|
|
31
|
+
* enforced rather than remembered.
|
|
32
|
+
*/
|
|
33
|
+
export const INTERROGATION_SEMANTICS_VERSION = '1';
|
|
13
34
|
const indexGraph = (graph) => {
|
|
14
35
|
const relationshipIds = new Set(graph.subjects
|
|
15
36
|
.filter(({ type }) => type === 'relationship')
|
|
@@ -146,7 +167,9 @@ const selectSubjects = (index, selector, profileContext) => {
|
|
|
146
167
|
// The schema's declared default for kindMatching is descendants, so a
|
|
147
168
|
// profile-derived kind satisfies a catalogue written against its parent.
|
|
148
169
|
const matching = selector.kindMatching ?? 'descendants';
|
|
149
|
-
let ids =
|
|
170
|
+
let ids = selector.kinds === undefined
|
|
171
|
+
? [...index.concepts]
|
|
172
|
+
: [...index.concepts].filter((id) => kindMatches(index.kindOf.get(id), selector.kinds, matching, profileContext));
|
|
150
173
|
if (selector.statuses !== undefined) {
|
|
151
174
|
const statuses = new Set(selector.statuses);
|
|
152
175
|
ids = ids.filter((id) => {
|
|
@@ -217,6 +240,28 @@ const conditionHolds = (index, condition, subjectId, profileContext) => {
|
|
|
217
240
|
switch (condition.condition) {
|
|
218
241
|
case 'missing-claim':
|
|
219
242
|
return !(index.claimsBySubject.get(subjectId) ?? []).some(({ predicate }) => predicate === condition.predicate);
|
|
243
|
+
case 'unscoped-succession': {
|
|
244
|
+
// A succession that replaced its predecessor outright says so by the
|
|
245
|
+
// predecessor being gone. One where both subjects are still current is
|
|
246
|
+
// usually partial, and the respect is the part a reader needs: a model
|
|
247
|
+
// claimed Zoekt superseded the Elasticsearch indexer while the source
|
|
248
|
+
// said Zoekt "does not replace" it for any scope but code search
|
|
249
|
+
// (ADR 0109). Fires only where the qualifier is missing AND the
|
|
250
|
+
// predecessor is still current, so a completed replacement stays quiet.
|
|
251
|
+
const claims = index.claimsBySubject.get(subjectId) ?? [];
|
|
252
|
+
return claims.some((claim) => {
|
|
253
|
+
if (claim.predicate !== 'yarramate/lineage/supersedes')
|
|
254
|
+
return false;
|
|
255
|
+
if (!('ref' in claim.object))
|
|
256
|
+
return false;
|
|
257
|
+
const scoped = claims.some((other) => other.predicate === 'yarramate/lineage/supersedes-respect' &&
|
|
258
|
+
other.id === `${claim.id}~respect`);
|
|
259
|
+
if (scoped)
|
|
260
|
+
return false;
|
|
261
|
+
const predecessorStatus = index.statusOf.get(claim.object.ref);
|
|
262
|
+
return predecessorStatus !== 'retired';
|
|
263
|
+
});
|
|
264
|
+
}
|
|
220
265
|
case 'missing-relationship': {
|
|
221
266
|
// Relationship kinds resolve through profile lineage by default, the
|
|
222
267
|
// same rule as selectors: a catalogue written against core kinds must
|
|
@@ -398,6 +443,7 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
|
398
443
|
question: question.question.trim(),
|
|
399
444
|
materiality: question.materiality.trim(),
|
|
400
445
|
resolution: question.resolution.trim(),
|
|
446
|
+
trigger: question.trigger,
|
|
401
447
|
...(question.since === undefined ? {} : { since: question.since }),
|
|
402
448
|
};
|
|
403
449
|
if (question.scope === 'workspace') {
|
|
@@ -431,6 +477,7 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
|
431
477
|
return {
|
|
432
478
|
format: 'yarramate/interrogation-report/v1',
|
|
433
479
|
catalogue: `${catalogue.id}@${catalogue.version}`,
|
|
480
|
+
semantics: INTERROGATION_SEMANTICS_VERSION,
|
|
434
481
|
summary: {
|
|
435
482
|
questions: applicableQuestions.length,
|
|
436
483
|
openQuestions,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The interrogation engine as a runtime-neutral entry point.
|
|
2
|
+
//
|
|
3
|
+
// The `.` barrel reaches node:fs, node:path and node:child_process through
|
|
4
|
+
// workspace, source-store and attestation-staleness, so a consumer running
|
|
5
|
+
// inside a Worker or a Durable Object cannot take the engine from there
|
|
6
|
+
// without dragging Node in behind it. This subpath carries the pure engine
|
|
7
|
+
// alone: catalogue loading takes a WorkspaceSource, evaluation takes an
|
|
8
|
+
// in-memory graph, and a test pins the import graph free of Node builtins.
|
|
9
|
+
// The same shape the visual-graph projector uses (`./adapter/visual-graph`).
|
|
10
|
+
export { INTERROGATION_SEMANTICS_VERSION, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
|
package/dist/reconciliation.d.ts
CHANGED
|
@@ -86,6 +86,13 @@ export interface ReconciliationReport {
|
|
|
86
86
|
readonly contradicted: number;
|
|
87
87
|
readonly unknown: number;
|
|
88
88
|
readonly notObserved: number;
|
|
89
|
+
/**
|
|
90
|
+
* `not-observed` observations naming no search. A negative claim about a
|
|
91
|
+
* tree nobody read exhaustively is the one result whose message nothing
|
|
92
|
+
* can check, so the ones offering a reader nothing to re-run are counted
|
|
93
|
+
* rather than left to read like any other finding (ADR 0107).
|
|
94
|
+
*/
|
|
95
|
+
readonly unsupportedAbsences?: number;
|
|
89
96
|
readonly subjectsWithoutEvidence: number;
|
|
90
97
|
readonly staleAttestations?: number;
|
|
91
98
|
readonly unconfirmedAttestations?: number;
|
package/dist/reconciliation.js
CHANGED
|
@@ -199,6 +199,7 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
|
199
199
|
contradicted: 0,
|
|
200
200
|
unknown: 0,
|
|
201
201
|
notObserved: 0,
|
|
202
|
+
unsupportedAbsences: 0,
|
|
202
203
|
subjectsWithoutEvidence: unobservedSubjects.length,
|
|
203
204
|
// Attestation staleness is assessed only when the caller derived it
|
|
204
205
|
// (the reconcile command); the counter appears exactly then, so a
|
|
@@ -219,6 +220,7 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
|
219
220
|
...(staleness?.findings ?? []),
|
|
220
221
|
...unconfirmed,
|
|
221
222
|
];
|
|
223
|
+
const absenceNotes = [];
|
|
222
224
|
for (const report of reports) {
|
|
223
225
|
summary.observations += report.observations.length;
|
|
224
226
|
for (const observation of report.observations) {
|
|
@@ -228,6 +230,18 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
|
228
230
|
}
|
|
229
231
|
if (observation.result === 'not-observed') {
|
|
230
232
|
summary.notObserved += 1;
|
|
233
|
+
// A not-observed asserts a negative, and is the only result whose
|
|
234
|
+
// message nothing else in the pipeline can check: the locator it
|
|
235
|
+
// carries points at what the author looked at, not at the absence
|
|
236
|
+
// they claim. One naming no search offers a reader nothing to
|
|
237
|
+
// re-run, so it is counted and named rather than left to read like
|
|
238
|
+
// a checked finding (ADR 0107).
|
|
239
|
+
if ((observation.searched ?? []).length === 0) {
|
|
240
|
+
summary.unsupportedAbsences += 1;
|
|
241
|
+
const named = 'subject' in observation ? observation.subject : observation.claim;
|
|
242
|
+
absenceNotes.push(`The not-observed observation for ${named} names no search, ` +
|
|
243
|
+
`so nothing here can be re-run to test the absence it asserts.`);
|
|
244
|
+
}
|
|
231
245
|
}
|
|
232
246
|
else {
|
|
233
247
|
summary[observation.result] += 1;
|
|
@@ -260,7 +274,7 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
|
260
274
|
(expectationOf(left)?.key ?? '').localeCompare(expectationOf(right)?.key ?? '') ||
|
|
261
275
|
(expectationOf(left)?.observed ?? '').localeCompare(expectationOf(right)?.observed ?? ''));
|
|
262
276
|
summary.findings = findings.length;
|
|
263
|
-
const notes = staleness?.notes ?? [];
|
|
277
|
+
const notes = [...(staleness?.notes ?? []), ...absenceNotes];
|
|
264
278
|
return {
|
|
265
279
|
format: 'yarramate/reconciliation-report/v1',
|
|
266
280
|
workspace,
|
|
@@ -12,6 +12,14 @@ export const SUBJECT_REFERENCE_POSITIONS = [
|
|
|
12
12
|
path: ['concepts', '*', 'supersedes', '*'],
|
|
13
13
|
form: 'reference',
|
|
14
14
|
},
|
|
15
|
+
// The scoped succession form, `{ subject, inRespectOf }` (ADR 0109). A
|
|
16
|
+
// rename has to move this one too, or a scoped succession would silently
|
|
17
|
+
// keep pointing at the old address while the bare form beside it moved.
|
|
18
|
+
{
|
|
19
|
+
group: 'document',
|
|
20
|
+
path: ['concepts', '*', 'supersedes', '*', 'subject'],
|
|
21
|
+
form: 'reference',
|
|
22
|
+
},
|
|
15
23
|
{
|
|
16
24
|
group: 'document',
|
|
17
25
|
path: ['concepts', '*', 'constraints', '*', 'ref'],
|