@adhd/backlog 1.0.5 → 1.0.6

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +194 -39
  3. package/api.d.ts +87 -0
  4. package/api.ir.json +1 -1
  5. package/citation.d.ts +176 -0
  6. package/envelope.d.ts +35 -1
  7. package/index.d.ts +23 -3
  8. package/index.js +106 -58
  9. package/index.mjs +11627 -7068
  10. package/ir-artifact.d.ts +7 -3
  11. package/lifecycle.d.ts +49 -0
  12. package/package.json +5 -5
  13. package/query/canonical.d.ts +16 -0
  14. package/query/card.d.ts +116 -4
  15. package/query/get.d.ts +8 -1
  16. package/query/index.d.ts +1 -0
  17. package/query/query.d.ts +20 -13
  18. package/query/redirect.d.ts +30 -0
  19. package/query/resolve.d.ts +48 -0
  20. package/query/similar-clusters.d.ts +8 -0
  21. package/query/similarity-signals.d.ts +47 -0
  22. package/query/spec-staleness.d.ts +21 -0
  23. package/query/types.d.ts +249 -34
  24. package/query/verdict-core.d.ts +21 -0
  25. package/query/verdict.d.ts +22 -0
  26. package/query/views/catalog.d.ts +47 -0
  27. package/query/views/registry.d.ts +27 -7
  28. package/query/views/report.d.ts +49 -0
  29. package/query/views/semantic.d.ts +28 -5
  30. package/query/views/stats.d.ts +38 -2
  31. package/readiness.d.ts +29 -0
  32. package/retry-policy.d.ts +44 -0
  33. package/serve.d.ts +1 -1
  34. package/server.d.ts +71 -3
  35. package/service-config.d.ts +109 -0
  36. package/service-errors.d.ts +51 -0
  37. package/skill/SKILL.md +688 -81
  38. package/store/catalog-invariant-guard.d.ts +4 -4
  39. package/vocabulary.d.ts +48 -0
  40. package/write/anchor-check.d.ts +139 -0
  41. package/write/attestation.d.ts +64 -0
  42. package/write/catalog-merge.d.ts +15 -8
  43. package/write/catalog.d.ts +63 -0
  44. package/write/citation-path.d.ts +31 -0
  45. package/write/citation.d.ts +157 -0
  46. package/write/create-issue.d.ts +143 -29
  47. package/write/errors.d.ts +159 -0
  48. package/write/gate.d.ts +67 -0
  49. package/write/merge-project.d.ts +54 -0
  50. package/write/obligation.d.ts +133 -0
  51. package/write/relate.d.ts +1 -1
  52. package/write/revision.d.ts +27 -0
  53. package/write/similarity-scan.d.ts +84 -0
  54. package/write/spec-revision.d.ts +131 -0
  55. package/write/spec-revision.reconcile.d.ts +48 -0
  56. package/write/transition.d.ts +13 -2
  57. package/write/tx.d.ts +21 -1
  58. package/write/update.d.ts +23 -1
package/write/relate.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
2
 
3
3
  /** The closed `rel` union `relate` accepts (§3/§6.3.6) — issue → issue in every case. */
4
- export type RelateRel = 'relates_to' | 'supersedes' | 'blocks' | 'duplicate_of' | 'part_of';
4
+ export type RelateRel = 'relates_to' | 'supersedes' | 'blocks' | 'duplicate_of' | 'part_of' | 'similar_to';
5
5
  export interface IRelateInput {
6
6
  /** The relation's SOURCE `issue` uid (§1: uid is the only identifier — never a name, never repo-scoped). */
7
7
  sourceUid: string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * revision.ts — the issue content-revision counter (DESIGN §2 Primitive 1/4).
3
+ *
4
+ * One revision counter, one name: `subject.revision` (Primitive 2) and
5
+ * `source_revision` (Primitive 4) are the SAME quantity — a monotonic counter
6
+ * on the issue's own `meta.revision`, bumped on every mutating write. A check
7
+ * or a verdict is stale relative to this one counter, so it must be read
8
+ * through one function rather than each consumer re-deriving it.
9
+ *
10
+ * This module is pure (no store, no I/O): it reads only the `meta` blob a
11
+ * caller already has in hand. `C6`'s verdict layer owns the bump sites
12
+ * (`create-issue` seeds `revision: 0`; `update`/`transition`/`claim`/`relate`/
13
+ * `move` set `nextRevision(priorMeta)`); `attest`/`recheck` deliberately do
14
+ * NOT bump, because they never touch the subject.
15
+ *
16
+ * A never-mutated issue (no `meta.revision` at all, e.g. every issue created
17
+ * before the counter existed) reads as `0`, so the pre-counter corpus is
18
+ * consistent without a backfill.
19
+ */
20
+ /**
21
+ * The monotonic revision of an issue's content — `0` for a never-mutated
22
+ * issue. A non-finite or non-numeric stored value (a malformed `meta` blob)
23
+ * degrades to `0` rather than propagating `NaN` into a comparison.
24
+ */
25
+ export declare function readRevision(meta: Record<string, unknown> | undefined): number;
26
+ /** `readRevision(meta) + 1` — the revision a mutating write stamps onto its node. */
27
+ export declare function nextRevision(meta: Record<string, unknown> | undefined): number;
@@ -0,0 +1,84 @@
1
+ import { StoreSearchBackend } from '@adhd/sox-hybrid-search';
2
+ import { GraphBackend } from '@adhd/sox-graph-store';
3
+
4
+ /** The search/graph substrate the scan needs — structurally compatible with the create gate's `IDuplicateScanHandle`. */
5
+ export interface ISimilarityScanHandle {
6
+ readonly graph?: GraphBackend;
7
+ readonly search?: {
8
+ readonly backend: StoreSearchBackend;
9
+ embedQuery?(text: string): Promise<Float32Array>;
10
+ };
11
+ }
12
+ /** Why the scan could not produce a calibrated comparison — never a generic "unavailable". */
13
+ export type SimilarityScanDegradedReason = 'no-search-backend' | 'no-embed-query' | 'no-vector-scores';
14
+ /** One advisory similarity candidate, with the provenance a reviewer needs to judge it across projects. */
15
+ export interface ISimilarCandidate {
16
+ uid: string;
17
+ title: string;
18
+ /** Cosine similarity in `[0,1]`, straight off the vector channel. */
19
+ score: number;
20
+ scope: 'same-project' | 'cross-project';
21
+ provenance?: {
22
+ projectUid: string;
23
+ projectName: string;
24
+ componentUid?: string;
25
+ repoUrl?: string;
26
+ };
27
+ /** Which independent signals fired, e.g. `['cosine','title-tokens']`. */
28
+ signals?: string[];
29
+ }
30
+ export interface ISimilarityScanInput {
31
+ title: string;
32
+ body: string;
33
+ scope: 'same-project' | 'multi-project' | 'store-wide';
34
+ /** Required for `same-project`/`multi-project`: the filing project. */
35
+ projectUid?: string;
36
+ sameProjectThreshold: number;
37
+ crossProjectThreshold: number;
38
+ margin: number;
39
+ tokenOverlapMin: number;
40
+ limit?: number;
41
+ /**
42
+ * The filing item's own citation tokens / component path — the "A" side of
43
+ * the structural signal. Optional: absent means the structural signal can
44
+ * only ever fire on the candidate's side, which is the correct default for a
45
+ * caller with no new-item context (the view cluster scan).
46
+ */
47
+ citationTokens?: readonly string[];
48
+ componentPath?: string;
49
+ /** Ids the scan must EXCLUDE (the declared `dedupeExcludeUid` parent + its `part_of` ancestor chain). */
50
+ excludeIds?: ReadonlySet<number>;
51
+ }
52
+ /** The richer outcome {@link scanSimilarCandidatesWithMeta} returns; {@link scanSimilarCandidates} is its candidates-only projection. */
53
+ export interface ISimilarityScanOutcome {
54
+ candidates: ISimilarCandidate[];
55
+ degraded: boolean;
56
+ degradedReason?: SimilarityScanDegradedReason;
57
+ }
58
+ /**
59
+ * Citation-derived structural tokens + owning component path for one issue.
60
+ *
61
+ * Exported so BOTH scan callers can supply the A-side of the AC7 structural
62
+ * signal from a real stored item: the `view:'similar'` cluster block derives
63
+ * its seed's context with this directly (the seed is already written), while
64
+ * the create-time gate derives the not-yet-written filing item's context from
65
+ * its input in `write/create-issue.ts`.
66
+ */
67
+ export declare function structuralContextFor(graph: GraphBackend, issueId: number): Promise<{
68
+ citationTokens: string[];
69
+ componentPath?: string;
70
+ }>;
71
+ /**
72
+ * The scan, with its degraded-mode metadata. Returns `{candidates:[], degraded:false}`
73
+ * when the scope is a COMPLETE scan of nothing (the scope resolves to an empty
74
+ * candidate set, or every candidate was excluded); returns a degraded outcome
75
+ * when there ARE rows to compare against but the calibrated (vector) channel
76
+ * could not answer — the distinction the create gate's `abort` fails closed on.
77
+ */
78
+ export declare function scanSimilarCandidatesWithMeta(handle: ISimilarityScanHandle, input: ISimilarityScanInput): Promise<ISimilarityScanOutcome>;
79
+ /**
80
+ * The candidates-only projection of {@link scanSimilarCandidatesWithMeta}
81
+ * (the C9 spec's stated signature): best-first, advisory, never writes, and
82
+ * `[]` when the embedding substrate is absent.
83
+ */
84
+ export declare function scanSimilarCandidates(handle: ISimilarityScanHandle, input: ISimilarityScanInput): Promise<ISimilarCandidate[]>;
@@ -0,0 +1,131 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { GraphBackend, NodeRecord } from '@adhd/sox-graph-store';
3
+
4
+ /** The long-form file export an anchor points at: `locator + digest` (Primitive 2) — referenced, never embedded. */
5
+ export interface ISpecAnchor {
6
+ locator: string;
7
+ digest: string;
8
+ }
9
+ /**
10
+ * Immutable revision object metadata (node `kind: 'SPEC'`). The node's
11
+ * `content` column holds the FRAGMENT appended at this revision (the delta; the
12
+ * base revision's fragment IS the base document). No revision stores a full
13
+ * snapshot — the document is the fold over the chain's `content` values and
14
+ * `revision_token` is the hash of that fold.
15
+ */
16
+ export interface ISpecRevisionMeta {
17
+ /** The ticket's logical id (the head resolved at append time) — see the module header. */
18
+ spec_of: string;
19
+ /** 1,2,3… monotonic within the spec. */
20
+ revision_seq: number;
21
+ /** `'sha256:<hex>'` of the FOLDED document at this revision. */
22
+ revision_token: string;
23
+ /** Predecessor revision uid (`null` for the base revision). */
24
+ prev_revision: string | null;
25
+ /** The long-form file export (Primitive 2). */
26
+ anchor: ISpecAnchor;
27
+ appended_by: string;
28
+ appended_at: string;
29
+ }
30
+ export interface ISpecAppendInput {
31
+ /** The ticket's logical id — any uid on its `SUPERSEDES` chain is resolved forward. */
32
+ uid: string;
33
+ /** The delta appended at this revision (becomes the node's `content`). */
34
+ fragment: string;
35
+ /** The file export for this revision. */
36
+ anchor?: ISpecAnchor;
37
+ /** REQUIRED CAS token — the revision uid the caller read (`''` when the ticket had no spec yet). */
38
+ base_revision: string;
39
+ by: string;
40
+ }
41
+ export interface ISpecAppendOutcome {
42
+ /** UNCHANGED — cross-checked by AC1. */
43
+ uid: string;
44
+ /** The NEW revision uid. */
45
+ spec_revision: string;
46
+ /** `'sha256:<hex>'`. */
47
+ spec_revision_token: string;
48
+ revision_seq: number;
49
+ }
50
+ /** Current revision + its token for a work item. */
51
+ export interface ISpecPointer {
52
+ revision_uid: string;
53
+ revision_token: string;
54
+ revision_seq: number;
55
+ }
56
+ /**
57
+ * The store surface {@link appendSpecRevision} needs. Beyond the ordinary write
58
+ * handle it requires the store's `GraphBackend`, because revision DISCOVERY must
59
+ * run the SAME live-kind traversal the read path (`deriveSpecHead`) and the
60
+ * reconciliation (`spec-revision.reconcile.ts`) use — and that traversal
61
+ * (`query/resolve.ts`'s `resolveEdgeScopedCandidates`) is a `GraphBackend` read
62
+ * that must run BEFORE the `BEGIN IMMEDIATE` transaction opens (a `GraphBackend`
63
+ * call never runs inside it — see `resolveLogicalHeadTx`). `GraphBacklogStore`
64
+ * and the test `TestIssueStore` both satisfy this shape.
65
+ */
66
+ export type ISpecAppendStore = IWriteStoreHandle & {
67
+ readonly graph: GraphBackend;
68
+ };
69
+ /**
70
+ * Rowids of live items whose DECLARED catalog kind is `SPEC` — an
71
+ * `issue`-shaped node carrying a live `has_kind` edge to the `kind:'SPEC'`
72
+ * catalog row. This is the SHIPPED production shape: the reconciled corpus is
73
+ * `issue` nodes whose DECLARED kind is `SPEC`, and reconciliation stamps them
74
+ * `spec_of`/`revision_seq` WITHOUT changing the raw `node.kind` column. Any
75
+ * discovery keyed on the raw column alone is therefore blind to them.
76
+ *
77
+ * Uses the SAME live-kind traversal the read path's `filter.kind`
78
+ * (`query/views/semantic.ts`) and `spec-revision.reconcile.ts` use —
79
+ * `query/resolve.ts`'s {@link resolveEdgeScopedCandidates} on
80
+ * `has_kind`→`SPEC` — so read, write, and reconcile can never disagree on what
81
+ * "a live `kind:'SPEC'` item" is. Returns `undefined` when `SPEC` resolves to
82
+ * no live catalog row (nothing is declared SPEC).
83
+ */
84
+ export declare function declaredSpecRowids(graph: GraphBackend): Promise<ReadonlySet<number> | undefined>;
85
+ /**
86
+ * Every live SPEC document — the UNION of two representations the store has
87
+ * held, both of which are specs:
88
+ *
89
+ * (a) a node whose RAW `kind` column is `SPEC` — the immutable revision object
90
+ * {@link appendSpecRevision} mints (and the shape the unit fixtures
91
+ * build); and
92
+ * (b) an `issue` node whose DECLARED catalog kind is `SPEC` — the reconciled
93
+ * production corpus (see {@link declaredSpecRowids}).
94
+ *
95
+ * This is the ONE discovery the read path ({@link deriveSpecHead}), the write
96
+ * path (`findChainHeadTx` via {@link declaredSpecRowids}), and the
97
+ * reconciliation all share, so they cannot drift on which items are specs. The
98
+ * union is load-bearing: a raw-kind-only reader missed shape (b) entirely.
99
+ */
100
+ export declare function discoverLiveSpecNodes(graph: GraphBackend): Promise<NodeRecord[]>;
101
+ /**
102
+ * Append a revision. One `BEGIN IMMEDIATE` transaction (ADR-0001 + ADR-0012 —
103
+ * no temp-file/rename/flock; atomicity is the store's job):
104
+ *
105
+ * 1. resolve `uid` → logical head (input may be any `SUPERSEDES`-chain uid — AC9);
106
+ * 2. read the current revision; if it is not `base_revision` →
107
+ * {@link SpecRevisionConflictError} (stale base — no write; AC6);
108
+ * 3. write a NEW `kind:'SPEC'` node holding the fragment (AC1/AC8);
109
+ * 4. advance the ticket's `meta.spec_revision` IN PLACE via the CAS helper,
110
+ * bumping `meta.revision` — the uid is preserved (AC1);
111
+ * 5. `writeAudit(action:'spec-appended')`.
112
+ *
113
+ * NEVER update or delete a revision node (AC1/AC8).
114
+ */
115
+ export declare function appendSpecRevision(handle: ISpecAppendStore, input: ISpecAppendInput): Promise<ISpecAppendOutcome>;
116
+ /**
117
+ * The derived chain head for a ticket — a live `SPEC` node whose
118
+ * `meta.spec_of` is in the ticket's `SUPERSEDES`-chain uid set with the max
119
+ * `meta.revision_seq` (read-path analogue of {@link findChainHeadTx}, backed by
120
+ * the same reconciliation-created functional index). `undefined` when the ticket has
121
+ * no live revision.
122
+ */
123
+ export declare function deriveSpecHead(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
124
+ /** The pointer ON THE RECORD only (`meta.spec_revision`) — never the derived head. Used by the staleness cross-check. */
125
+ export declare function readPointerRecord(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
126
+ /**
127
+ * Current revision + its token for a work item (`undefined` if it has no spec).
128
+ * Prefers the on-record pointer; falls back to the derived chain head so a
129
+ * ticket whose pointer was not (yet) written — pre-reconciliation data — still reads.
130
+ */
131
+ export declare function readSpecPointer(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
@@ -0,0 +1,48 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { GraphBackend } from '@adhd/sox-graph-store';
3
+
4
+ /** The store surface the reconciliation needs — satisfied by `GraphBacklogStore` and the test store alike. */
5
+ export type ISpecReconcileStore = IWriteStoreHandle & {
6
+ readonly graph: GraphBackend;
7
+ };
8
+ export interface ISpecReconcileStamp {
9
+ revisionUid: string;
10
+ /** The logical id stamped onto `meta.spec_of` (the ticket's head). */
11
+ specOf: string;
12
+ revisionSeq: number;
13
+ prevRevision: string | null;
14
+ revisionToken: string;
15
+ /** `false` when the node already carries exactly this stamp (skip the write). */
16
+ needsStamp: boolean;
17
+ }
18
+ export interface ISpecReconcileHead {
19
+ ticketUid: string;
20
+ revisionUid: string;
21
+ revisionToken: string;
22
+ revisionSeq: number;
23
+ /** `false` when the ticket's pointer already names this head with this token. */
24
+ needsPointer: boolean;
25
+ }
26
+ export interface ISpecReconcileReport {
27
+ /** How many live `SPEC` nodes were discovered. */
28
+ scanned: number;
29
+ stamps: ISpecReconcileStamp[];
30
+ heads: ISpecReconcileHead[];
31
+ /** `SPEC` nodes already stamped (idempotent no-op). */
32
+ alreadyStamped: number;
33
+ /** `SPEC` nodes with no live `part_of` parent — left untouched, reported. */
34
+ orphaned: string[];
35
+ /** Whether the lookup index exists after this call. */
36
+ indexCreated: boolean;
37
+ }
38
+ /**
39
+ * Compute the reconciliation plan WITHOUT writing (the dry-run). Discovery is
40
+ * `query`-driven; nothing is mutated.
41
+ */
42
+ export declare function planSpecRevisionReconcile(store: ISpecReconcileStore): Promise<ISpecReconcileReport>;
43
+ /**
44
+ * Apply the reconciliation. Idempotent: an already-stamped `SPEC` and an
45
+ * already-pointed ticket are both skipped, so a second run is a no-op. One
46
+ * `BEGIN IMMEDIATE` transaction (ADR-0001) — never a hard delete.
47
+ */
48
+ export declare function applySpecRevisionReconcile(store: ISpecReconcileStore): Promise<ISpecReconcileReport>;
@@ -1,5 +1,5 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
- import { ICitationInput } from './create-issue.js';
2
+ import { ICitation } from '../citation.js';
3
3
 
4
4
  export interface ITransitionInput {
5
5
  /** The `issue` uid to transition (§6.3, an "Issue verb"). */
@@ -11,7 +11,7 @@ export interface ITransitionInput {
11
11
  /** REQUIRED unless `project_policy.transition_requires_note` is `false` (default `true`) — optional in the type; enforced at runtime (`NoteRequiredError`), never at the TS level. */
12
12
  note?: string;
13
13
  /** REQUIRED (≥1) when `project_policy.citation_required` is `true` AND `toStatus` resolves to a terminal status. Written as `citation` nodes + `has_citation` edges exactly like `create`'s own citations. */
14
- citations?: ICitationInput[];
14
+ citations?: ICitation[];
15
15
  /**
16
16
  * The item-level disclosure-contract git context — the SAME plain metadata
17
17
  * scalar `create`'s own `gitContext` field writes (repo `AGENTS.md`'s "Cite
@@ -22,6 +22,17 @@ export interface ITransitionInput {
22
22
  * per-citation `ref` — it rides the issue, never a `citation` node.
23
23
  */
24
24
  gitContext?: string;
25
+ /**
26
+ * A recorded override of a refusing obligation (C5, DESIGN §2 Primitive 3).
27
+ * Honoured ONLY when the caller is in the refusing obligation's
28
+ * `override.actors`; a reason is ALWAYS required (never a configurable
29
+ * boolean) and is recorded on the audit row. A claimed override by a
30
+ * non-listed actor, or one with a blank reason, throws
31
+ * `OverrideNotPermittedError`.
32
+ */
33
+ override?: {
34
+ reason: string;
35
+ };
25
36
  }
26
37
  export interface ITransitionOutcome {
27
38
  uid: string;
package/write/tx.d.ts CHANGED
@@ -162,7 +162,7 @@ export declare function resolveLiveIssueTx(tx: AdapterTransaction, uid: string):
162
162
  /** rowid → node, inside a tx (used to resolve an edge endpoint's kind without a redundant round trip when the caller doesn't already know it). */
163
163
  export declare function getNodeByRowidTx(tx: AdapterTransaction, rowid: number): Promise<ITxNodeRow | null>;
164
164
  export interface IWriteNodeTxInput {
165
- /** The entity-type discriminator — `project`/`component`/`location`/`issue`/`kind`/`edge_kind`/`status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit` (§3). NEVER validated against a closed vocabulary here — the schema is open by design (§0 anti-antipattern 3); the write layer is the only composer of these literals. */
165
+ /** The entity-type discriminator — `project`/`component`/`location`/`issue`/`kind`/`edge_kind`/`status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit`/`attestation`/`obligation`/`SPEC` (§3; the SPEC revision kind is added by C10, DESIGN §12). NEVER validated against a closed vocabulary here — the schema is open by design (§0 anti-antipattern 3); the write layer is the only composer of these literals. */
166
166
  kind: string;
167
167
  /** The business name (`issue.title`, a catalog row's name, …). Omit for a node with no name (none currently exist in §3's table, but the column is nullable). */
168
168
  name?: string;
@@ -328,6 +328,26 @@ export interface IInvalidateEdgeTxInput {
328
328
  * instead of the bare adapter.
329
329
  */
330
330
  export declare function invalidateEdgeTx(tx: AdapterTransaction, input: IInvalidateEdgeTxInput): Promise<void>;
331
+ /**
332
+ * Update a LIVE node's `meta` IN PLACE inside `tx`, CAS-guarded on the node's
333
+ * own `meta.revision` counter (SR-2 monotonic revision + SR-6 per-node CAS,
334
+ * per DESIGN §2 Primitive 1's "node's monotonic `revision`, bumped on every
335
+ * mutating write"). Preserves `uid` — this is the ONLY write path that mutates
336
+ * a node without superseding it, and it exists solely for the C10 spec pointer
337
+ * (`meta.spec_revision`) and the SR-2 counter it bumps. No other verb may adopt
338
+ * it without a new decision.
339
+ *
340
+ * `patch`'s keys are shallow-merged over the node's existing meta; the merged
341
+ * blob's `revision` is set to `expectedRevision + 1`. Returns the new revision
342
+ * number on success, or `null` when the node is missing OR the stored revision
343
+ * no longer equals `expectedRevision` (a concurrent writer won the CAS — the
344
+ * caller must surface a stale-base error, and because this runs inside the
345
+ * caller's own `BEGIN IMMEDIATE` transaction, a thrown error rolls the whole
346
+ * transaction back). A node with no `meta.revision` at all reads as revision
347
+ * `0` (matching `revision.ts`'s `readRevision`), so a pre-counter row is
348
+ * CAS-addressable without a backfill.
349
+ */
350
+ export declare function updateNodeMetaTx(tx: AdapterTransaction, uid: string, patch: Record<string, unknown>, expectedRevision: number): Promise<number | null>;
331
351
  /**
332
352
  * The one transaction wrapper every write verb calls — `store.adapter.transaction(fn,
333
353
  * {mode: resolveTransactionMode()})`, which is `'immediate'` (§4c: `BEGIN
package/write/update.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
+ import { ICitation } from '../citation.js';
2
3
 
3
4
  export interface IUpdateIssueInput {
4
5
  /** The `issue` uid to update (§6.3, an "Issue verb"). */
@@ -17,6 +18,27 @@ export interface IUpdateIssueInput {
17
18
  assignee?: string;
18
19
  /** catalog agent name/uid; → `touch` + `authored_by` edge rewrite. An unresolved NAME mints; a uid-shaped ref that does not resolve throws `CatalogNotFoundError('agent', ref)`. */
19
20
  author?: string;
21
+ /**
22
+ * The DESIRED set of citations this issue should carry after this call — a
23
+ * DIFF-EMITTER, never a body rewrite (the architecture verdict): the live
24
+ * citation set is read inside the SAME `immediate` transaction, citations
25
+ * present live but absent from `citations` are removed (bi-temporally), and
26
+ * citations in `citations` but not live are added. Identity for the diff is
27
+ * `(file, lines, revision)` (§ `citationKey`), never `sha`/`context`.
28
+ *
29
+ * Routing a citation change through `body`/`supersede` is FORBIDDEN: that
30
+ * would mint a new issue uid and violate §1's "uid is the only identity".
31
+ * When `body` IS also given, the supersede mints the new node first, its
32
+ * live citations are carried forward, and THIS diff is applied against the
33
+ * carried-forward set on the new node — still inside the one transaction.
34
+ *
35
+ * Each citation's `sha` is computed (and policy-gated, exactly as `create`/
36
+ * `transition` do) before the transaction opens, so the write lock is never
37
+ * held across a filesystem/git read. `citations: []` clears every live
38
+ * citation. Omitted entirely ⇒ citations are untouched (and carried forward
39
+ * verbatim on a body edit).
40
+ */
41
+ citations?: ICitation[];
20
42
  /**
21
43
  * §4b/§6.2 — waits for the fire-and-forget on-write embedding round-trip
22
44
  * before `update` returns, when `true` and `handle.embedding` is
@@ -30,7 +52,7 @@ export interface IUpdateIssueInput {
30
52
  */
31
53
  awaitEmbed?: boolean;
32
54
  }
33
- export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author';
55
+ export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author' | 'citations';
34
56
  export interface IUpdateIssueOutcome {
35
57
  /**
36
58
  * The resulting CURRENT issue's uid — the SAME `input.uid` when `body` was