yarramate 0.17.0 → 0.18.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/dist/attestation-staleness.js +8 -1
- package/dist/compiler.d.ts +12 -0
- package/dist/compiler.js +46 -2
- package/dist/reconciliation.d.ts +17 -1
- package/dist/reconciliation.js +48 -1
- package/dist/rtm.d.ts +1 -0
- package/dist/rtm.js +9 -7
- package/package.json +1 -1
- package/schema/yarramate-ask-result.schema.json +4 -0
- package/schema/yarramate-document.schema.json +5 -0
- package/schema/yarramate-operations.schema.json +7 -1
- package/schema/yarramate-reconciliation-report.schema.json +36 -1
- package/skills/yarramate-architecture/references/native-authoring.md +15 -3
|
@@ -49,7 +49,14 @@ const attestedConcepts = (source, documentId) => {
|
|
|
49
49
|
}
|
|
50
50
|
const { topic, by, on } = record;
|
|
51
51
|
if (topic !== undefined && by !== undefined && on !== undefined) {
|
|
52
|
-
|
|
52
|
+
// The authority is a reference; report it in the same qualified form
|
|
53
|
+
// the compiler resolves, so one sign-off reads identically in a
|
|
54
|
+
// staleness finding, a reconcile finding, and the RTM.
|
|
55
|
+
attestations.push({
|
|
56
|
+
topic,
|
|
57
|
+
by: by.includes('#') ? by : `${documentId}#${by}`,
|
|
58
|
+
on,
|
|
59
|
+
});
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
if (attestations.length === 0)
|
package/dist/compiler.d.ts
CHANGED
|
@@ -63,5 +63,17 @@ export type ContextualCompilationResult = {
|
|
|
63
63
|
readonly ok: false;
|
|
64
64
|
readonly diagnostics: readonly Diagnostic[];
|
|
65
65
|
};
|
|
66
|
+
export declare const ATTESTATION_PREDICATE_PREFIX = "yarramate/attestation/";
|
|
67
|
+
export declare const attestationClaimValue: (attestation: {
|
|
68
|
+
readonly by: string;
|
|
69
|
+
readonly on: string;
|
|
70
|
+
readonly recordedBy?: string;
|
|
71
|
+
}) => string;
|
|
72
|
+
export interface AttestationClaimParts {
|
|
73
|
+
readonly by: string;
|
|
74
|
+
readonly on: string;
|
|
75
|
+
readonly recordedBy?: string;
|
|
76
|
+
}
|
|
77
|
+
export declare const parseAttestationClaimValue: (value: string) => AttestationClaimParts | undefined;
|
|
66
78
|
export declare function compileWorkspace(sources: readonly WorkspaceSource[]): CompilationResult;
|
|
67
79
|
export declare const compileWorkspaceWithProfileContext: (sources: readonly WorkspaceSource[]) => ContextualCompilationResult;
|
package/dist/compiler.js
CHANGED
|
@@ -37,6 +37,25 @@ const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from
|
|
|
37
37
|
const aliasClaimId = (subject, alias) => `${subject}~alias-${Buffer.from(alias, 'utf8').toString('hex')}`;
|
|
38
38
|
const distinctFromClaimId = (subject, other) => `${subject}~distinct-from-${Buffer.from(other, 'utf8').toString('hex')}`;
|
|
39
39
|
const supersedesClaimId = (subject, predecessor) => `${subject}~supersedes-${Buffer.from(predecessor, 'utf8').toString('hex')}`;
|
|
40
|
+
export const ATTESTATION_PREDICATE_PREFIX = 'yarramate/attestation/';
|
|
41
|
+
// An attestation claim packs the authority, the date it was given, and
|
|
42
|
+
// the recorder when a machine held the pen. A reference carries no
|
|
43
|
+
// spaces and the date is fixed width, so the three parse back out of one
|
|
44
|
+
// value unambiguously wherever a reader needs them.
|
|
45
|
+
export const attestationClaimValue = (attestation) => attestation.recordedBy === undefined
|
|
46
|
+
? `${attestation.by} ${attestation.on}`
|
|
47
|
+
: `${attestation.by} ${attestation.on} ${attestation.recordedBy}`;
|
|
48
|
+
export const parseAttestationClaimValue = (value) => {
|
|
49
|
+
const match = /^(\S+) ([0-9]{4}-[0-9]{2}-[0-9]{2})(?: (.+))?$/.exec(value);
|
|
50
|
+
if (match === null)
|
|
51
|
+
return undefined;
|
|
52
|
+
const recordedBy = match[3];
|
|
53
|
+
return {
|
|
54
|
+
by: match[1],
|
|
55
|
+
on: match[2],
|
|
56
|
+
...(recordedBy === undefined ? {} : { recordedBy }),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
40
59
|
const describeAspect = (aspect) => aspect.replace('-', ' ');
|
|
41
60
|
// Candidate order is the policy-matrix declaration order: the resolved kind
|
|
42
61
|
// map inserts core policies first, then extension kinds as declared.
|
|
@@ -1128,11 +1147,36 @@ function compileWorkspaceResolved(sources) {
|
|
|
1128
1147
|
// evaluates: the claim's existence is what triggers can see, and
|
|
1129
1148
|
// revocation is deletion, reviewed at the Git boundary.
|
|
1130
1149
|
for (const [attestationIndex, attestation] of (concept.attestations ?? []).entries()) {
|
|
1150
|
+
// The authority is held to the same rule as ownership: a judgment
|
|
1151
|
+
// is worthless if nobody in the model made it, and a name only the
|
|
1152
|
+
// signer knows cannot be checked by the reviewer reading the diff.
|
|
1153
|
+
const authority = qualifyReference(value.id, attestation.by);
|
|
1154
|
+
if (!conceptByQualifiedId.has(authority)) {
|
|
1155
|
+
const pointer = `/concepts/${index}/attestations/${attestationIndex}/by`;
|
|
1156
|
+
const source = location(['concepts', index, 'attestations', attestationIndex, 'by'], pointer);
|
|
1157
|
+
diagnostics.push({
|
|
1158
|
+
severity: 'error',
|
|
1159
|
+
code: 'YM304',
|
|
1160
|
+
message: `Unresolved attestation authority reference "${attestation.by}"`,
|
|
1161
|
+
path: input.path,
|
|
1162
|
+
pointer,
|
|
1163
|
+
line: source.line,
|
|
1164
|
+
column: source.column,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1131
1167
|
claims.push({
|
|
1132
1168
|
id: `${subject}~attestation-${attestation.topic}`,
|
|
1133
1169
|
subject,
|
|
1134
|
-
predicate:
|
|
1135
|
-
object: {
|
|
1170
|
+
predicate: `${ATTESTATION_PREDICATE_PREFIX}${attestation.topic}`,
|
|
1171
|
+
object: {
|
|
1172
|
+
value: attestationClaimValue({
|
|
1173
|
+
by: authority,
|
|
1174
|
+
on: attestation.on,
|
|
1175
|
+
...(attestation.recordedBy === undefined
|
|
1176
|
+
? {}
|
|
1177
|
+
: { recordedBy: attestation.recordedBy }),
|
|
1178
|
+
}),
|
|
1179
|
+
},
|
|
1136
1180
|
origin: 'declared',
|
|
1137
1181
|
source: location(['concepts', index, 'attestations', attestationIndex, 'topic'], `/concepts/${index}/attestations/${attestationIndex}/topic`),
|
|
1138
1182
|
});
|
package/dist/reconciliation.d.ts
CHANGED
|
@@ -55,7 +55,22 @@ export interface StaleAttestationFinding {
|
|
|
55
55
|
readonly changedAt?: string;
|
|
56
56
|
readonly evidence: EvidenceLocator;
|
|
57
57
|
}
|
|
58
|
-
export
|
|
58
|
+
export interface UnconfirmedAttestationFinding {
|
|
59
|
+
readonly target: {
|
|
60
|
+
readonly type: 'subject';
|
|
61
|
+
readonly id: string;
|
|
62
|
+
};
|
|
63
|
+
readonly result: 'unconfirmed-attestation';
|
|
64
|
+
readonly attestation: {
|
|
65
|
+
readonly topic: string;
|
|
66
|
+
readonly by: string;
|
|
67
|
+
readonly recordedBy: string;
|
|
68
|
+
readonly on: string;
|
|
69
|
+
};
|
|
70
|
+
readonly provider: 'model';
|
|
71
|
+
readonly declared: DeclaredSource;
|
|
72
|
+
}
|
|
73
|
+
export type ReconciliationFinding = EvidenceFinding | StaleAttestationFinding | UnconfirmedAttestationFinding;
|
|
59
74
|
export interface AttestationStaleness {
|
|
60
75
|
readonly findings: readonly StaleAttestationFinding[];
|
|
61
76
|
readonly notes: readonly string[];
|
|
@@ -73,6 +88,7 @@ export interface ReconciliationReport {
|
|
|
73
88
|
readonly notObserved: number;
|
|
74
89
|
readonly subjectsWithoutEvidence: number;
|
|
75
90
|
readonly staleAttestations?: number;
|
|
91
|
+
readonly unconfirmedAttestations?: number;
|
|
76
92
|
readonly expectationsCompared: number;
|
|
77
93
|
readonly expectationsWithoutObservation: number;
|
|
78
94
|
};
|
package/dist/reconciliation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ATTESTATION_PREDICATE_PREFIX, parseAttestationClaimValue, } from './compiler.js';
|
|
1
2
|
const assertedRelationshipsByClaim = (graph) => {
|
|
2
3
|
const asserted = new Map();
|
|
3
4
|
if (graph === undefined)
|
|
@@ -147,10 +148,48 @@ const unobservedCurrentConcepts = (graph, reports) => {
|
|
|
147
148
|
.map(({ subject }) => subject)
|
|
148
149
|
.sort((left, right) => left.localeCompare(right));
|
|
149
150
|
};
|
|
151
|
+
// A judgment a machine transcribed is not the act the authority
|
|
152
|
+
// performed. The recorder is in the model, so the difference is
|
|
153
|
+
// derivable here: an authority who wrote the record in their own hand
|
|
154
|
+
// names nobody else, and anything else is a claim awaiting confirmation.
|
|
155
|
+
const unconfirmedAttestations = (graph) => {
|
|
156
|
+
if (graph === undefined)
|
|
157
|
+
return [];
|
|
158
|
+
return graph.claims.flatMap((claim) => {
|
|
159
|
+
if (!claim.predicate.startsWith(ATTESTATION_PREDICATE_PREFIX))
|
|
160
|
+
return [];
|
|
161
|
+
if (!('value' in claim.object))
|
|
162
|
+
return [];
|
|
163
|
+
const parts = parseAttestationClaimValue(claim.object.value);
|
|
164
|
+
const recordedBy = parts?.recordedBy;
|
|
165
|
+
if (parts === undefined || recordedBy === undefined)
|
|
166
|
+
return [];
|
|
167
|
+
// The authority is qualified; a recorder naming the same subject,
|
|
168
|
+
// long form or short, is that authority signing for themselves.
|
|
169
|
+
const local = parts.by.slice(parts.by.indexOf('#') + 1);
|
|
170
|
+
if (recordedBy === parts.by || recordedBy === local)
|
|
171
|
+
return [];
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
target: { type: 'subject', id: claim.subject },
|
|
175
|
+
result: 'unconfirmed-attestation',
|
|
176
|
+
attestation: {
|
|
177
|
+
topic: claim.predicate.slice(ATTESTATION_PREDICATE_PREFIX.length),
|
|
178
|
+
by: parts.by,
|
|
179
|
+
recordedBy,
|
|
180
|
+
on: parts.on,
|
|
181
|
+
},
|
|
182
|
+
provider: 'model',
|
|
183
|
+
declared: claim.source,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
});
|
|
187
|
+
};
|
|
150
188
|
export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
151
189
|
const assertedByClaim = assertedRelationshipsByClaim(graph);
|
|
152
190
|
const unobservedSubjects = unobservedCurrentConcepts(graph, reports);
|
|
153
191
|
const expectations = compareExpectations(graph, reports);
|
|
192
|
+
const unconfirmed = unconfirmedAttestations(graph);
|
|
154
193
|
const summary = {
|
|
155
194
|
evidenceDocuments: reports.length,
|
|
156
195
|
observations: 0,
|
|
@@ -167,10 +206,18 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
|
167
206
|
...(staleness === undefined
|
|
168
207
|
? {}
|
|
169
208
|
: { staleAttestations: staleness.findings.length }),
|
|
209
|
+
// Recorder disagreement is derived from the model alone, so the
|
|
210
|
+
// counter appears whenever there was a graph to read.
|
|
211
|
+
...(graph === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { unconfirmedAttestations: unconfirmed.length }),
|
|
170
214
|
expectationsCompared: expectations.compared,
|
|
171
215
|
expectationsWithoutObservation: expectations.unobserved.length,
|
|
172
216
|
};
|
|
173
|
-
const findings = [
|
|
217
|
+
const findings = [
|
|
218
|
+
...(staleness?.findings ?? []),
|
|
219
|
+
...unconfirmed,
|
|
220
|
+
];
|
|
174
221
|
for (const report of reports) {
|
|
175
222
|
summary.observations += report.observations.length;
|
|
176
223
|
for (const observation of report.observations) {
|
package/dist/rtm.d.ts
CHANGED
package/dist/rtm.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ATTESTATION_PREDICATE_PREFIX, parseAttestationClaimValue, } from './compiler.js';
|
|
1
2
|
import { coreLocalKind, isDeclaredNonGoal } from './brief.js';
|
|
2
3
|
import { conceptKinds } from './profile.js';
|
|
3
4
|
// The RTM is one derived reading of the graph: the chain
|
|
@@ -28,7 +29,7 @@ export function buildRtm(workspace, graph, profileContext, evidenceReports) {
|
|
|
28
29
|
for (const claim of graph.claims) {
|
|
29
30
|
if (!('value' in claim.object))
|
|
30
31
|
continue;
|
|
31
|
-
if (claim.predicate.startsWith(
|
|
32
|
+
if (claim.predicate.startsWith(ATTESTATION_PREDICATE_PREFIX)) {
|
|
32
33
|
attestationClaims.set(claim.subject, [
|
|
33
34
|
...(attestationClaims.get(claim.subject) ?? []),
|
|
34
35
|
claim,
|
|
@@ -204,14 +205,13 @@ export function buildRtm(workspace, graph, profileContext, evidenceReports) {
|
|
|
204
205
|
.flatMap((claim) => {
|
|
205
206
|
if (!('value' in claim.object))
|
|
206
207
|
return [];
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
208
|
+
const parts = parseAttestationClaimValue(claim.object.value);
|
|
209
|
+
if (parts === undefined)
|
|
209
210
|
return [];
|
|
210
211
|
return [
|
|
211
212
|
{
|
|
212
|
-
topic: claim.predicate.slice(
|
|
213
|
-
|
|
214
|
-
on: match[2],
|
|
213
|
+
topic: claim.predicate.slice(ATTESTATION_PREDICATE_PREFIX.length),
|
|
214
|
+
...parts,
|
|
215
215
|
source: citation(claim),
|
|
216
216
|
},
|
|
217
217
|
];
|
|
@@ -330,7 +330,9 @@ export function renderRtmMarkdown(rtm) {
|
|
|
330
330
|
const attestations = row.attestations.length === 0
|
|
331
331
|
? 'none'
|
|
332
332
|
: row.attestations
|
|
333
|
-
.map((attestation) => escapeCell(`${attestation.topic}: ${attestation.by} on ${attestation.on}
|
|
333
|
+
.map((attestation) => escapeCell(`${attestation.topic}: ${attestation.by} on ${attestation.on}${attestation.recordedBy === undefined
|
|
334
|
+
? ''
|
|
335
|
+
: `, recorded by ${attestation.recordedBy}`}`) + ` (${cite(attestation.source)})`)
|
|
334
336
|
.join('<br>');
|
|
335
337
|
lines.push(`| ${requirement} | ${row.status ?? 'undeclared'} | ${lineage} | ${realizers} | ${evidence} | ${attestations} | ${cite(row.source)} |`);
|
|
336
338
|
}
|
package/package.json
CHANGED
|
@@ -314,6 +314,11 @@
|
|
|
314
314
|
"pattern": "^[a-z][a-z0-9-]*$"
|
|
315
315
|
},
|
|
316
316
|
"by": {
|
|
317
|
+
"description": "The authority whose judgment this records, as a subject reference. A sign-off names someone the model already knows, on the same rule as ownership: an authority absent from the model cannot be held to the judgment.",
|
|
318
|
+
"$ref": "#/$defs/reference"
|
|
319
|
+
},
|
|
320
|
+
"recordedBy": {
|
|
321
|
+
"description": "Who wrote this record, when that is not the attesting authority's own hand. An agent recording a judgment on someone's behalf names itself here, so a sign-off and a transcription of one are never the same bytes.",
|
|
317
322
|
"$ref": "#/$defs/nonEmptyText"
|
|
318
323
|
},
|
|
319
324
|
"on": {
|
|
@@ -86,7 +86,8 @@
|
|
|
86
86
|
"required": [
|
|
87
87
|
"topic",
|
|
88
88
|
"by",
|
|
89
|
-
"on"
|
|
89
|
+
"on",
|
|
90
|
+
"recordedBy"
|
|
90
91
|
],
|
|
91
92
|
"properties": {
|
|
92
93
|
"topic": {
|
|
@@ -94,6 +95,11 @@
|
|
|
94
95
|
"pattern": "^[a-z][a-z0-9-]*$"
|
|
95
96
|
},
|
|
96
97
|
"by": {
|
|
98
|
+
"description": "The authority whose judgment this records, resolved against the model as a subject reference once the write lands.",
|
|
99
|
+
"$ref": "#/$defs/nonEmptyText"
|
|
100
|
+
},
|
|
101
|
+
"recordedBy": {
|
|
102
|
+
"description": "Who is writing this record. Required here and optional in a document: a hand-written sign-off already has a committer, whereas a batch names the agent holding the pen, so an authority's judgment and a machine's transcription of one are never the same bytes.",
|
|
97
103
|
"$ref": "#/$defs/nonEmptyText"
|
|
98
104
|
},
|
|
99
105
|
"on": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"notObserved": { "type": "integer", "minimum": 0 },
|
|
32
32
|
"subjectsWithoutEvidence": { "type": "integer", "minimum": 0 },
|
|
33
33
|
"staleAttestations": { "type": "integer", "minimum": 0 },
|
|
34
|
+
"unconfirmedAttestations": { "type": "integer", "minimum": 0 },
|
|
34
35
|
"expectationsCompared": { "type": "integer", "minimum": 0 },
|
|
35
36
|
"expectationsWithoutObservation": { "type": "integer", "minimum": 0 }
|
|
36
37
|
}
|
|
@@ -142,9 +143,43 @@
|
|
|
142
143
|
"finding": {
|
|
143
144
|
"oneOf": [
|
|
144
145
|
{ "$ref": "#/$defs/evidenceFinding" },
|
|
145
|
-
{ "$ref": "#/$defs/staleAttestationFinding" }
|
|
146
|
+
{ "$ref": "#/$defs/staleAttestationFinding" },
|
|
147
|
+
{ "$ref": "#/$defs/unconfirmedAttestationFinding" }
|
|
146
148
|
]
|
|
147
149
|
},
|
|
150
|
+
"unconfirmedAttestationFinding": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"additionalProperties": false,
|
|
153
|
+
"required": ["target", "result", "attestation", "provider", "declared"],
|
|
154
|
+
"properties": {
|
|
155
|
+
"target": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"required": ["type", "id"],
|
|
159
|
+
"properties": {
|
|
160
|
+
"type": { "const": "subject" },
|
|
161
|
+
"id": { "$ref": "#/$defs/subjectIdentity" }
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"result": { "const": "unconfirmed-attestation" },
|
|
165
|
+
"attestation": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"additionalProperties": false,
|
|
168
|
+
"required": ["topic", "by", "recordedBy", "on"],
|
|
169
|
+
"properties": {
|
|
170
|
+
"topic": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
171
|
+
"by": { "type": "string", "minLength": 1 },
|
|
172
|
+
"recordedBy": { "type": "string", "minLength": 1 },
|
|
173
|
+
"on": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"provider": { "const": "model" },
|
|
180
|
+
"declared": { "$ref": "#/$defs/declaredSource" }
|
|
181
|
+
}
|
|
182
|
+
},
|
|
148
183
|
"evidenceFinding": {
|
|
149
184
|
"type": "object",
|
|
150
185
|
"additionalProperties": false,
|
|
@@ -208,8 +208,9 @@ identified references, not a policy engine or free-form metadata bag.
|
|
|
208
208
|
name: Shared architecture context
|
|
209
209
|
attestations:
|
|
210
210
|
- topic: adequacy
|
|
211
|
-
by:
|
|
211
|
+
by: review-board
|
|
212
212
|
on: "2026-08-01"
|
|
213
|
+
recordedBy: claude-fable-5
|
|
213
214
|
```
|
|
214
215
|
|
|
215
216
|
An attestation records that an authority accepted the subject as adequate
|
|
@@ -219,12 +220,23 @@ outside the engine — only the claim's existence is checked. Revoke by
|
|
|
219
220
|
deleting the entry; both signing and revoking are reviewed at the Git
|
|
220
221
|
boundary (ADR 0056).
|
|
221
222
|
|
|
223
|
+
`by` is a subject reference, resolved like `owner`: it names a concept the
|
|
224
|
+
model already holds, and an unresolved one is `YM304`, a hard error. A name
|
|
225
|
+
nobody modelled is not an authority. When you write the entry on someone
|
|
226
|
+
else's judgment, name yourself in `recordedBy` rather than putting your own
|
|
227
|
+
handle in `by` — `apply` requires `recordedBy` on every attestation an
|
|
228
|
+
operations batch writes, because a batch is a machine transcribing a
|
|
229
|
+
judgment it did not make (ADR 0082). `reconcile` then reports an
|
|
230
|
+
`unconfirmed-attestation` finding for each record whose recorder is not its
|
|
231
|
+
authority: the sign-off stands, and the report says out loud that a machine
|
|
232
|
+
wrote it down.
|
|
233
|
+
|
|
222
234
|
A sign-off covers the wording it read. If the subject's `name` or
|
|
223
235
|
`description` changes in a commit after the attestation's `on` date,
|
|
224
236
|
`yarramate reconcile` reports a `stale-attestation` finding naming the
|
|
225
237
|
attestation and the commit that reworded the subject (ADR 0074). Renew
|
|
226
|
-
the sign-off by updating `on` after the rewording.
|
|
227
|
-
|
|
238
|
+
the sign-off by updating `on` after the rewording. Neither finding is a
|
|
239
|
+
gate: `check --strict` is unaffected.
|
|
228
240
|
|
|
229
241
|
## Rationale and citations
|
|
230
242
|
|