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.
@@ -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
- attestations.push({ topic, by, on });
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)
@@ -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: `yarramate/attestation/${attestation.topic}`,
1135
- object: { value: `${attestation.by} ${attestation.on}` },
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
  });
@@ -55,7 +55,22 @@ export interface StaleAttestationFinding {
55
55
  readonly changedAt?: string;
56
56
  readonly evidence: EvidenceLocator;
57
57
  }
58
- export type ReconciliationFinding = EvidenceFinding | StaleAttestationFinding;
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
  };
@@ -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 = [...(staleness?.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
@@ -32,6 +32,7 @@ export interface RtmRealizer {
32
32
  export interface RtmAttestation {
33
33
  readonly topic: string;
34
34
  readonly by: string;
35
+ readonly recordedBy?: string;
35
36
  readonly on: string;
36
37
  readonly source: RtmSource;
37
38
  }
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('yarramate/attestation/')) {
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 match = /^(.*) ([0-9]{4}-[0-9]{2}-[0-9]{2})$/.exec(claim.object.value);
208
- if (match === null)
208
+ const parts = parseAttestationClaimValue(claim.object.value);
209
+ if (parts === undefined)
209
210
  return [];
210
211
  return [
211
212
  {
212
- topic: claim.predicate.slice('yarramate/attestation/'.length),
213
- by: match[1],
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}`) + ` (${cite(attestation.source)})`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yarramate",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Tool-neutral semantic architecture engine and guided methodology",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -932,6 +932,10 @@
932
932
  "subjectsWithoutEvidence": {
933
933
  "type": "integer"
934
934
  },
935
+ "unconfirmedAttestations": {
936
+ "type": "integer",
937
+ "minimum": 0
938
+ },
935
939
  "expectationsCompared": {
936
940
  "type": "integer"
937
941
  },
@@ -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: reviewer-name
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. This is a report, not
227
- a gate: `check --strict` is unaffected.
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