@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.
- package/dist/branch-lock.d.ts +42 -0
- package/dist/branch-lock.d.ts.map +1 -1
- package/dist/branch-lock.js.map +1 -1
- package/dist/data-accessor.d.ts +109 -4
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/exit-codes.d.ts +9 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +9 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nexus-account.d.ts +18 -1
- package/dist/nexus-account.d.ts.map +1 -1
- package/dist/nexus-account.js +21 -0
- package/dist/nexus-account.js.map +1 -1
- package/dist/operations/output-contracts-data.js +1 -1
- package/dist/operations/output-contracts-data.js.map +1 -1
- package/dist/operations/tasks.d.ts +11 -2
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js.map +1 -1
- package/dist/row-identity.d.ts +128 -0
- package/dist/row-identity.d.ts.map +1 -0
- package/dist/row-identity.js +20 -0
- package/dist/row-identity.js.map +1 -0
- package/dist/task.d.ts +48 -2
- package/dist/task.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/branch-lock.ts +42 -0
- package/src/data-accessor.ts +119 -5
- package/src/exit-codes.ts +9 -0
- package/src/index.ts +12 -0
- package/src/nexus-account.ts +39 -0
- package/src/operations/output-contracts-data.ts +1 -1
- package/src/operations/tasks.ts +11 -2
- package/src/row-identity.ts +132 -0
- package/src/task.ts +49 -2
package/src/data-accessor.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/nexus-account.ts
CHANGED
|
@@ -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: {
|
package/src/operations/tasks.ts
CHANGED
|
@@ -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.
|
|
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
|
-
/**
|
|
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
|
|
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). */
|