@cleocode/contracts 2026.9.24 → 2026.9.26

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.
@@ -193,6 +193,14 @@ export interface AcRow {
193
193
  updatedAt: string | null;
194
194
  /** Optional sha256(text) snapshot; writers MAY populate, readers MUST treat null as "unknown". */
195
195
  contentHash: string | null;
196
+ /**
197
+ * Row uid (T12341): the criterion's identity across devices and across edits
198
+ * (`id` changes with the text; `uid` does not). `null` until the store fills
199
+ * it; absent from accessors that predate it.
200
+ */
201
+ uid?: string | null;
202
+ /** Birth fingerprint (T12341): write-once, carried with the uid across edits. */
203
+ birthFp?: string | null;
196
204
  }
197
205
 
198
206
  /** Machine-readable AC child-projection drift codes for doctor/audit output. */
@@ -264,6 +272,12 @@ export interface AcBindingRow {
264
272
  bindingType: 'direct' | 'satisfies' | 'coverage';
265
273
  /** ISO-8601 timestamp of binding creation. */
266
274
  createdAt: string;
275
+ /**
276
+ * The evidence was recorded against a different text of this criterion
277
+ * (T12341): the criterion kept its identity through an edit, but no gate may
278
+ * count this binding until the evidence is re-verified. Absent means valid.
279
+ */
280
+ stale?: boolean;
267
281
  }
268
282
 
269
283
  /** Query options for bounded reads from the append-only task audit log. @task T10594 */
@@ -459,13 +473,50 @@ export interface TaskConflictDetails {
459
473
  * Subset of DataAccessor methods available inside a transaction callback.
460
474
  * Write-only — reads use the outer accessor (snapshot isolation).
461
475
  */
476
+ /**
477
+ * How {@link TransactionAccessor.insertNewTask} gives a task its row identity
478
+ * (T12341 spec §5.1, T12806).
479
+ *
480
+ * - `new` (the default): the task is created now; it gets a random UUIDv7.
481
+ * - `imported`: the task existed before this write (import, snapshot
482
+ * restore). It carries `uid` / `birthFp` when the source has them and no
483
+ * other row holds that uid; otherwise its uid is left NULL, so the
484
+ * deterministic recipe derives it from the row's own key and birth. An
485
+ * import never stamps an import-time uid.
486
+ */
487
+ export interface TaskInsertIdentity {
488
+ readonly origin: 'new' | 'imported';
489
+ /** The uid the source row carried, when it carried one. */
490
+ readonly uid?: string | null;
491
+ /** The birth fingerprint that goes with `uid`. */
492
+ readonly birthFp?: string | null;
493
+ }
494
+
495
+ /** A task's row identity (T12341): `uid` and `birthFp`, NULL until filled. */
496
+ export interface TaskRowIdentity {
497
+ readonly id: string;
498
+ readonly uid: string | null;
499
+ readonly birthFp: string | null;
500
+ }
501
+
462
502
  export interface TransactionAccessor {
463
503
  upsertSingleTask(task: Task): Promise<void>;
464
504
  /**
465
505
  * Insert a NEW task; never overwrites. Throws `ID_COLLISION` when the id is
466
- * already stored (T12724).
506
+ * already stored (T12724). `identity` says whether the task is new or
507
+ * imported ({@link TaskInsertIdentity}, T12806).
467
508
  */
468
- insertNewTask(task: Task): Promise<void>;
509
+ insertNewTask(task: Task, identity?: TaskInsertIdentity): Promise<void>;
510
+ /**
511
+ * Settle a task's row identity after an overwrite import replaced it (call
512
+ * it after `upsertSingleTask` in the same transaction). The same task (same
513
+ * birth as before the overwrite) keeps its identity; a different one is
514
+ * re-derived, and the history and bindings of its criteria are re-keyed so
515
+ * their evidence keeps resolving. Refuses (`E_VALIDATION`) once identity
516
+ * values are shared with other devices: there the overwrite would silently
517
+ * re-point a shared uid at different work (T12806).
518
+ */
519
+ clearTaskIdentity?(taskId: string): Promise<void>;
469
520
  archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
470
521
  removeSingleTask(taskId: string): Promise<void>;
471
522
  setMetaValue(key: string, value: unknown): Promise<void>;
@@ -501,6 +552,10 @@ export interface TransactionAccessor {
501
552
  targetTaskId?: string | null;
502
553
  projection?: string;
503
554
  contentHash?: string | null;
555
+ /** Row uid to keep (T12341); omitted → a new uid is minted. */
556
+ uid?: string | null;
557
+ /** Birth fingerprint to keep with the uid (T12341). */
558
+ birthFp?: string | null;
504
559
  }>,
505
560
  ): Promise<void>;
506
561
  /**
@@ -516,13 +571,48 @@ export interface TransactionAccessor {
516
571
  * @task T10508
517
572
  */
518
573
  deleteAcRowsForTask(taskId: string): Promise<void>;
574
+ /**
575
+ * Delete only the named AC rows of `taskId` (rows of other tasks are never
576
+ * touched). Used by the diff apply path so ACs that survive an edit keep
577
+ * their row — and therefore their evidence bindings.
578
+ *
579
+ * `keepBindingsForUids`: uids the same write carries onto new rows (an
580
+ * edited criterion keeps its uid, T12341); their bindings are kept, as
581
+ * stale evidence, instead of pruned.
582
+ * @task T12789
583
+ */
584
+ deleteAcRowsByIds(
585
+ taskId: string,
586
+ ids: readonly string[],
587
+ keepBindingsForUids?: readonly string[],
588
+ ): Promise<void>;
589
+ /**
590
+ * Update existing AC rows of their own task in place, keyed by `(id, taskId)`.
591
+ * Every mutable column is rewritten from the supplied row; `id` and
592
+ * `taskId` are never changed. A row that does not exist for that task is
593
+ * an error, never an insert.
594
+ * @task T12789
595
+ */
596
+ updateAcRows(
597
+ rows: Array<{
598
+ id: string;
599
+ taskId: string;
600
+ ordinal: number;
601
+ text: string;
602
+ kind?: 'text' | 'child_task' | 'evidence_bound';
603
+ sourceKey?: string;
604
+ targetTaskId?: string | null;
605
+ projection?: string;
606
+ contentHash?: string | null;
607
+ }>,
608
+ ): Promise<void>;
519
609
  /**
520
610
  * Append a history row to `task_acceptance_criteria_history` capturing the
521
611
  * AC text that is about to be superseded.
522
612
  * @task T10508
523
613
  */
524
614
  appendAcHistory(
525
- rows: Array<{ acId: string; previousText: string; reason: string }>,
615
+ rows: Array<{ acId: string; previousText: string; reason: string; acUid?: string | null }>,
526
616
  ): Promise<void>;
527
617
  /**
528
618
  * Read all `evidence_ac_bindings` rows whose `ac_id` ∈ the given set.
@@ -554,6 +644,16 @@ export interface TransactionAccessor {
554
644
  bindingType: 'direct' | 'satisfies' | 'coverage';
555
645
  }>,
556
646
  ): Promise<void>;
647
+ /**
648
+ * Delete every `evidence_ac_bindings` row whose `ac_id` names no AC row
649
+ * (bindings left behind before AC removal pruned them), recording each
650
+ * removed row in the task audit log (`ac.bindings.pruned`) first. Backs
651
+ * `cleo doctor ac-bindings --fix`.
652
+ *
653
+ * @returns the removed bindings
654
+ * @task T12790
655
+ */
656
+ pruneOrphanAcBindings(): Promise<AcBindingRow[]>;
557
657
  }
558
658
 
559
659
  // Re-export AcRow at the module level for both transaction + outer accessor use.
@@ -606,9 +706,15 @@ export interface DataAccessor {
606
706
  /**
607
707
  * Insert a NEW task under a freshly allocated or computed id. Never
608
708
  * overwrites: throws `ID_COLLISION` when the id is already stored, and
609
- * writes nothing (T12724).
709
+ * writes nothing (T12724). `identity`: {@link TaskInsertIdentity} (T12806).
710
+ */
711
+ insertNewTask(task: Task, identity?: TaskInsertIdentity): Promise<void>;
712
+
713
+ /**
714
+ * Row identities of the named tasks (archived included); a task without a
715
+ * row is omitted (T12806: a snapshot carries them).
610
716
  */
611
- insertNewTask(task: Task): Promise<void>;
717
+ getTaskIdentities?(taskIds: readonly string[]): Promise<TaskRowIdentity[]>;
612
718
 
613
719
  /** Archive a single task by ID (sets status='archived' + archive metadata). */
614
720
  archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
@@ -645,6 +751,14 @@ export interface DataAccessor {
645
751
  */
646
752
  getAcBindings(acIds: readonly string[]): Promise<AcBindingRow[]>;
647
753
 
754
+ /**
755
+ * Read every `evidence_ac_bindings` row whose `ac_id` names no row of
756
+ * `task_acceptance_criteria` — a binding for an AC that no longer exists.
757
+ * Read-only; backs `cleo doctor ac-bindings`.
758
+ * @task T12790
759
+ */
760
+ findOrphanAcBindings(): Promise<AcBindingRow[]>;
761
+
648
762
  // ---- Metadata (schema_meta KV store) ----
649
763
 
650
764
  /** Read a typed value from the metadata store. Returns null if not found. */
package/src/exit-codes.ts CHANGED
@@ -68,6 +68,15 @@ export enum ExitCode {
68
68
  * holder. @task T12506
69
69
  */
70
70
  WORKTREE_LOCKED = 25,
71
+ /**
72
+ * A guarded write (`--if-match`, or a claim holder's write) named a display
73
+ * id the task no longer carries: sync re-numbered it after a display-id
74
+ * collision (T12341), and the id now names another task or none. Writing
75
+ * would hit the wrong task, so the write refuses. LAFS code
76
+ * `E_TASK_RENAMED`; the details carry the task's current id
77
+ * (`details.expected`). @task T12800
78
+ */
79
+ TASK_RENAMED = 26,
71
80
 
72
81
  // === SESSION ERRORS (30-39) ===
73
82
  SESSION_EXISTS = 30,
package/src/index.ts CHANGED
@@ -369,8 +369,10 @@ export type {
369
369
  TaskConflictChange,
370
370
  TaskConflictDetails,
371
371
  TaskFieldUpdates,
372
+ TaskInsertIdentity,
372
373
  TaskPopulation,
373
374
  TaskQueryFilters,
375
+ TaskRowIdentity,
374
376
  TaskWriteGuard,
375
377
  TransactionAccessor,
376
378
  } from './data-accessor.js';
@@ -2286,6 +2288,14 @@ export type {
2286
2288
  TaskRefPriority,
2287
2289
  TaskSummary,
2288
2290
  } from './results.js';
2291
+ // === Row Identity Types (T12341 — uid merge keys) ===
2292
+ export type {
2293
+ RowIdentityKind,
2294
+ RowIdentityRef,
2295
+ RowIdentitySpec,
2296
+ StoredRefUid,
2297
+ SymmetricEdge,
2298
+ } from './row-identity.js';
2289
2299
  // === Scaffold + Diagnostic Result Types (SG-ARCH-SOLID T9831 / E-CONTRACTS-FOUNDATION T9832) ===
2290
2300
  export type {
2291
2301
  CheckResult,
@@ -2502,6 +2512,7 @@ export type {
2502
2512
  CriterionEvidenceLink,
2503
2513
  EpicLifecycle,
2504
2514
  EvidenceAtom,
2515
+ EvidenceMergeAnchor,
2505
2516
  EvidenceValidationContext,
2506
2517
  FileMeta,
2507
2518
  GateEvidence,
@@ -2982,6 +2993,7 @@ export type {
2982
2993
  NexusAccountOrganization,
2983
2994
  NexusAccountStatus,
2984
2995
  NexusAccountUser,
2996
+ NexusLoginDevice,
2985
2997
  NexusLoginResult,
2986
2998
  NexusLogoutResult,
2987
2999
  NexusProjectLink,
@@ -46,6 +46,27 @@ export const NEXUS_ACCOUNT_ERROR_CODES = [
46
46
  'E_NEXUS_REQUEST_FAILED',
47
47
  'E_NEXUS_NOT_A_PROJECT',
48
48
  'E_NEXUS_INVALID_LABEL',
49
+ // Device credentials (cleo-nexus device contract §3.3, §4.0.4; T12868).
50
+ /** No answer from the API (network error or timeout). */
51
+ 'E_NEXUS_UNREACHABLE',
52
+ /** `cleo login nexus` while a revoke of this device is unconfirmed (§3.3, v2.9). */
53
+ 'E_NEXUS_REVOKE_PENDING',
54
+ /** The device was signed out (E9, E11): 401 `device-signed-out`. */
55
+ 'E_NEXUS_DEVICE_SIGNED_OUT',
56
+ /** The device was revoked (E10, E12): 401 `device-revoked`. */
57
+ 'E_NEXUS_DEVICE_REVOKED',
58
+ /** A credential replaced or revoked elsewhere, or a `rotation-conflict` (R2). */
59
+ 'E_NEXUS_CREDENTIAL_COMPROMISED',
60
+ /** The device credential lacks the scope (a read-only device): 403 `insufficient-scope`. */
61
+ 'E_NEXUS_INSUFFICIENT_SCOPE',
62
+ /** A replica attach from a second device: 409 `replica-copied`. */
63
+ 'E_NEXUS_REPLICA_COPIED',
64
+ /** Another cleo process holds the device file or an upgrade in flight; retry. */
65
+ 'E_NEXUS_BUSY',
66
+ /** Several accounts hold a device credential on one origin and none was named. */
67
+ 'E_NEXUS_ACCOUNT_AMBIGUOUS',
68
+ /** An option that needs device credentials (`--read-only`) without `CLEO_NEXUS_DEVICE=1`. */
69
+ 'E_NEXUS_DEVICE_REQUIRED',
49
70
  ] as const;
50
71
 
51
72
  /** One of {@link NEXUS_ACCOUNT_ERROR_CODES}. */
@@ -112,6 +133,20 @@ export interface NexusAccountStatus {
112
133
  summary: string;
113
134
  }
114
135
 
136
+ /** The enrolled device, as `cleo login nexus` reports it (device contract §3.3 step 10). */
137
+ export interface NexusLoginDevice {
138
+ /** The Nexus device id (UUIDv7, scoped to this CLEO home, origin and user). */
139
+ deviceId: string;
140
+ /** Display name the server holds. */
141
+ name: string;
142
+ /** Server-side state (`active` right after a login). */
143
+ state: string;
144
+ /** Credential profile: `device` or `read-only`. */
145
+ profile: string | null;
146
+ /** `true` when E1 created the device row, `false` when it re-enrolled it. */
147
+ created: boolean | null;
148
+ }
149
+
115
150
  /** Result of `cleo login nexus` (never carries the token). */
116
151
  export interface NexusLoginResult {
117
152
  /** API origin the session belongs to. */
@@ -126,6 +161,10 @@ export interface NexusLoginResult {
126
161
  credentialsPath: string;
127
162
  /** Non-fatal problems, e.g. the account lookup failed after a good login. */
128
163
  warnings: string[];
164
+ /** The enrolled device; present only with `CLEO_NEXUS_DEVICE=1` (device credentials). */
165
+ device?: NexusLoginDevice;
166
+ /** Scopes of the stored device credential; present only with `CLEO_NEXUS_DEVICE=1`. */
167
+ scopes?: string[];
129
168
  }
130
169
 
131
170
  /** Result of `cleo logout nexus`. */
@@ -118,7 +118,7 @@ const tasksShowOutputContract: OperationOutputContract = {
118
118
  updatedAt: {
119
119
  type: ['string', 'null'],
120
120
  description:
121
- 'Task version for optimistic concurrency: pass it to `cleo update|complete --if-match`.',
121
+ 'Task version for optimistic concurrency: pass it to `cleo update|complete --if-match`. A stale version fails with E_CONFLICT (23); E_TASK_RENAMED (26) when sync re-numbered the task you read (details.expected is its new id).',
122
122
  },
123
123
  // T12502: the human assignee and the agent claim lease are separate.
124
124
  assignee: {
@@ -1616,7 +1616,13 @@ export interface TasksUpdateQueryParams {
1616
1616
  /**
1617
1617
  * Optimistic-concurrency guard: the task `updatedAt` the caller read. When
1618
1618
  * the stored version differs, the update fails with `E_CONFLICT` and the
1619
- * current version, instead of overwriting a newer write. @task T12503
1619
+ * current version, instead of overwriting a newer write. When sync
1620
+ * re-numbered the task the caller read (a display-id collision) and the
1621
+ * version is exactly the one it had under this id, the update fails with
1622
+ * `E_TASK_RENAMED` (exit 26) instead, `details.expected` naming the task's
1623
+ * new id; nothing is written to the task that holds the id now.
1624
+ * @task T12503
1625
+ * @task T12800
1620
1626
  */
1621
1627
  expectedUpdatedAt?: string;
1622
1628
  }
@@ -1702,8 +1708,11 @@ export interface TasksCompleteQueryParams {
1702
1708
  /**
1703
1709
  * Optimistic-concurrency guard (`--if-match`): the task `updatedAt` the
1704
1710
  * caller read. When the stored version differs, completion fails with
1705
- * `E_CONFLICT` carrying the current version and the changed fields.
1711
+ * `E_CONFLICT` carrying the current version and the changed fields, or
1712
+ * with `E_TASK_RENAMED` (exit 26, `details.expected` = the new id) when
1713
+ * sync re-numbered the task the caller read.
1706
1714
  * @task T12503
1715
+ * @task T12800
1707
1716
  */
1708
1717
  expectedUpdatedAt?: string;
1709
1718
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Row identity contracts — the uid every syncing row carries as its merge key
3
+ * (T12341).
4
+ *
5
+ * A table whose Gate A class syncs (`portable-*`) gets a `uid` column. The
6
+ * local primary key stays the local key and local foreign keys keep pointing
7
+ * at it; the uid is what a merge compares. The registry that declares, per
8
+ * table, how its uid is made lives in core (`store/row-identity.ts`); this
9
+ * module holds only the shapes.
10
+ *
11
+ * Spec: `cleo docs fetch t12341-uid-scheme`.
12
+ *
13
+ * Types only (arch gate 10): no runtime values are exported from here.
14
+ *
15
+ * @task T12341
16
+ * @epic T12323
17
+ * @module row-identity
18
+ */
19
+
20
+ /**
21
+ * How a table's uid is made.
22
+ *
23
+ * - `minted` — the local key was allocated on one device (`T####`, an
24
+ * autoincrement integer, a random id), so equal keys on two devices may be
25
+ * different rows. A new row gets a random UUIDv7; a row without a uid (an
26
+ * existing row, or one an older build wrote) gets a deterministic v7-layout
27
+ * uid over (table, key, birth, owner uids, content).
28
+ * - `natural` — the key is content or a relationship (an edge, a label), so
29
+ * equal keys mean the same row everywhere. The uid is a UUIDv8 over the key
30
+ * with every reference replaced by the referenced row's uid.
31
+ *
32
+ * @task T12341
33
+ */
34
+ export type RowIdentityKind = 'minted' | 'natural';
35
+
36
+ /**
37
+ * A column that holds another row's local key.
38
+ *
39
+ * @task T12341
40
+ */
41
+ export interface RowIdentityRef {
42
+ /** Physical column in this table. */
43
+ readonly column: string;
44
+ /** Physical table the column points into (its single-column local key). */
45
+ readonly table: string;
46
+ }
47
+
48
+ /**
49
+ * A stored copy of a fact about a referenced row, taken when the row is
50
+ * written: its uid (`ac_uid`, kept because an AC id is derived from the AC
51
+ * text and changes on edit), or the hash of its text (`ac_text_hash`, the
52
+ * text the evidence was recorded against).
53
+ *
54
+ * @task T12341
55
+ */
56
+ export interface StoredRefUid {
57
+ /** Physical column holding the copied fact (e.g. `ac_uid`). */
58
+ readonly column: string;
59
+ /** Column holding the referenced local key (e.g. `ac_id`). */
60
+ readonly from: string;
61
+ /** Referenced table. */
62
+ readonly table: string;
63
+ /** What is copied: the referenced row's uid (default) or the hash of its `text`. */
64
+ readonly source?: 'uid' | 'text_hash';
65
+ }
66
+
67
+ /**
68
+ * A relation table whose rows are symmetric for some values of a type column:
69
+ * the endpoint uids are hashed in sorted order, so `A related B` and
70
+ * `B related A` are one edge.
71
+ *
72
+ * @task T12341
73
+ */
74
+ export interface SymmetricEdge {
75
+ /** Column holding the relation type. */
76
+ readonly column: string;
77
+ /** Type values that are symmetric; every other value keeps direction. */
78
+ readonly values: readonly string[];
79
+ }
80
+
81
+ /**
82
+ * How one syncing table carries its uid.
83
+ *
84
+ * @task T12341
85
+ */
86
+ export interface RowIdentitySpec {
87
+ /** Physical table name. */
88
+ readonly table: string;
89
+ /** How the uid is made. */
90
+ readonly kind: RowIdentityKind;
91
+ /** Local key columns, in hash order. */
92
+ readonly key: readonly string[];
93
+ /** `minted`: the column recording when the row was created. */
94
+ readonly birth?: string;
95
+ /** `minted`: references whose target uid is part of the row's identity. */
96
+ readonly owners?: readonly RowIdentityRef[];
97
+ /**
98
+ * `minted`, append-only tables only: columns hashed into the uid. Their
99
+ * rows never change, and their keys collide across devices by construction.
100
+ */
101
+ readonly content?: readonly string[];
102
+ /** `natural`: key columns that reference rows; their target uid is hashed. */
103
+ readonly keyRefs?: readonly RowIdentityRef[];
104
+ /**
105
+ * `minted`: facts of the row's creation hashed into its birth fingerprint
106
+ * (`birth_fp`), besides the canonical birth. Each entry is a column name,
107
+ * `@ownerFp:<column>` (the birth fingerprint of the owner that column
108
+ * references, so the children of two colliding owners fingerprint apart),
109
+ * or `@refFp:<column>` (the fingerprint of the row a stored reference uid
110
+ * column names, e.g. the AC behind `ac_uid`).
111
+ * Read from the row as it is when its identity is first assigned; frozen
112
+ * per recipe version.
113
+ */
114
+ readonly birthFacts?: readonly string[];
115
+ /** `natural` relation tables: the symmetric relation types. */
116
+ readonly symmetric?: SymmetricEdge;
117
+ /**
118
+ * `minted`: every uid is random, never derived from content, including for
119
+ * existing rows. Required for `portable-secret` tables.
120
+ */
121
+ readonly randomOnly?: boolean;
122
+ /** Other references, translated to uids on the wire (not hashed). */
123
+ readonly refs?: readonly RowIdentityRef[];
124
+ /** JSON-array columns of referenced keys, translated element by element. */
125
+ readonly jsonArrayRefs?: readonly RowIdentityRef[];
126
+ /** Stored copies of referenced uids. */
127
+ readonly storedRefUids?: readonly StoredRefUid[];
128
+ /** The key is a user-facing display id that a collision re-mints. */
129
+ readonly displayId?: boolean;
130
+ /** Task that declared the table. */
131
+ readonly task: string;
132
+ }
package/src/task.ts CHANGED
@@ -160,6 +160,19 @@ export type VerificationGate =
160
160
  */
161
161
  | 'nexusImpact';
162
162
 
163
+ /**
164
+ * The part of a verified `pr:` atom that anchors `files:` bytes: its merge
165
+ * commit, when it merged, and what it changed (T12848).
166
+ */
167
+ export interface EvidenceMergeAnchor {
168
+ /** Merge commit SHA whose tree holds the PR's artifact bytes. */
169
+ mergeCommitSha: string;
170
+ /** ISO 8601 merge timestamp; the latest merge wins when several PRs changed a path. */
171
+ mergedAt: string;
172
+ /** Paths the PR changed. */
173
+ changedPaths?: readonly string[];
174
+ }
175
+
163
176
  /** Task and criterion scope used while validating evidence, independent of PR metadata. */
164
177
  export interface EvidenceValidationContext {
165
178
  /** Canonical current task intent; prose is never interpreted as a file declaration. */
@@ -168,8 +181,22 @@ export interface EvidenceValidationContext {
168
181
  gates: readonly VerificationGate[];
169
182
  /** Canonical criteria read for this verification operation. */
170
183
  criteria: ReadonlyArray<Pick<AcRow, 'id' | 'text' | 'updatedAt'>>;
171
- /** Verified PR merge whose immutable artifact bytes must be inspected. */
184
+ /**
185
+ * Verified PR merge whose immutable artifact bytes must be inspected.
186
+ *
187
+ * @deprecated Since T12848 no caller sets it: a single SHA cannot anchor a
188
+ * write with several `pr:` atoms. Use {@link EvidenceValidationContext.artifactPrs},
189
+ * which anchors each `files:` path at the merge of the PR that changed it.
190
+ * Still honoured when `artifactPrs` is absent.
191
+ */
172
192
  artifactCommitSha?: string;
193
+ /**
194
+ * Every verified PR in the evidence write (T12848). When non-empty, each
195
+ * `files:` path is inspected at the merge commit of the PR that changed it,
196
+ * so several `pr:` atoms in one write each anchor their own artifacts.
197
+ * Takes precedence over {@link EvidenceValidationContext.artifactCommitSha}.
198
+ */
199
+ artifactPrs?: ReadonlyArray<EvidenceMergeAnchor>;
173
200
  /**
174
201
  * Validate without persisting anything: `pr:`/`ci:` lookups write neither
175
202
  * the PR-result cache nor the branch-protection cache (T12671 review — the
@@ -435,8 +462,28 @@ export type EvidenceAtom =
435
462
  /** Event of the judged run (`push`, `pull_request`, …), when known. */
436
463
  event?: string;
437
464
  }>;
438
- /** Tree of the merge commit — the content every recorded check tested. */
465
+ /** Tree of the merge commit — the content every check judged on it (or on a tree-equal PR head) tested. */
439
466
  testedTree?: string;
467
+ /**
468
+ * Later default-branch commit whose green `push` runs stood in for a
469
+ * required check the merge commit's own run only had cancelled or
470
+ * skipped (T12742). Its CI tested the DESCENDANT's tree, not the merge
471
+ * tree: it descends from the merge commit, and no commit in
472
+ * `descendantRange` — merge or not — touched the PR's changed files or a
473
+ * pinned CI workflow. Checks judged there record it as their `sha`.
474
+ */
475
+ descendantSha?: string;
476
+ /**
477
+ * `<mergeCommitSha>..<descendantSha>`: the first-parent range proven to
478
+ * leave the PR's files and the pinned workflows untouched (T12742).
479
+ */
480
+ descendantRange?: string;
481
+ /**
482
+ * Final PR head whose latest `pull_request` runs were green for every
483
+ * check the descendant stood in for (T12742); a descendant never
484
+ * vouches for a PR that was not itself green.
485
+ */
486
+ descendantPrHeadSha?: string;
440
487
  /** Task the PR was verified to be linked to (T12634). */
441
488
  taskId?: string;
442
489
  /** Component PR the task is linked through; its CI ran as this integration PR's (T12671). */