@adhd/backlog 0.1.8 → 1.0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +93 -44
  2. package/README.md +332 -81
  3. package/api.d.ts +146 -0
  4. package/cli.d.ts +45 -18
  5. package/env.d.ts +23 -3
  6. package/envelope.d.ts +163 -0
  7. package/index.d.ts +11 -10
  8. package/index.js +531 -173
  9. package/index.mjs +29814 -15647
  10. package/install-skill.d.ts +23 -0
  11. package/package.json +50 -15
  12. package/query/card.d.ts +31 -0
  13. package/query/get.d.ts +11 -0
  14. package/query/index.d.ts +67 -0
  15. package/query/markdown.d.ts +11 -0
  16. package/query/query.d.ts +131 -0
  17. package/query/resolve.d.ts +123 -0
  18. package/query/types.d.ts +450 -0
  19. package/query/views/registry.d.ts +43 -0
  20. package/query/views/semantic.d.ts +101 -0
  21. package/query/views/stats.d.ts +109 -0
  22. package/search-shortcut.d.ts +79 -0
  23. package/serve.d.ts +18 -0
  24. package/server.d.ts +139 -4
  25. package/skill/SKILL.md +619 -138
  26. package/store/graph-backlog-store.d.ts +80 -17
  27. package/store/immediate-retry.d.ts +24 -13
  28. package/store/type-policy.d.ts +4 -0
  29. package/store/vocabulary-guard.d.ts +52 -0
  30. package/version-info.d.ts +15 -0
  31. package/write/audit.d.ts +36 -0
  32. package/write/bootstrap.d.ts +123 -0
  33. package/write/catalog.d.ts +351 -0
  34. package/write/claim-lease.d.ts +21 -0
  35. package/write/claim.d.ts +80 -0
  36. package/write/create-issue.d.ts +250 -0
  37. package/write/delete.d.ts +39 -0
  38. package/write/embed-drain.d.ts +68 -0
  39. package/write/embedding-observer.d.ts +80 -0
  40. package/write/errors.d.ts +303 -0
  41. package/write/issue-status.d.ts +10 -0
  42. package/write/move.d.ts +70 -0
  43. package/write/relate.d.ts +52 -0
  44. package/write/transition.d.ts +60 -0
  45. package/write/tx.d.ts +344 -0
  46. package/write/update.d.ts +81 -0
  47. package/client.d.ts +0 -169
  48. package/markdown.d.ts +0 -75
  49. package/migration-admin.d.ts +0 -26
  50. package/model.d.ts +0 -437
  51. package/store/audit-log.d.ts +0 -16
  52. package/store/claim.d.ts +0 -24
  53. package/store/crud.d.ts +0 -62
  54. package/store/ids.d.ts +0 -24
  55. package/store/lifecycle.d.ts +0 -36
  56. package/store/mapping.d.ts +0 -101
  57. package/store/mutate-metadata.d.ts +0 -8
  58. package/store/query.d.ts +0 -68
  59. package/store/repo-migration.d.ts +0 -51
  60. package/store/serve-lock.d.ts +0 -42
  61. package/store/structure.d.ts +0 -66
package/write/tx.d.ts ADDED
@@ -0,0 +1,344 @@
1
+ import { BacklogWriteError } from './errors.js';
2
+ import { TypePolicy } from '@adhd/sox-graph-store';
3
+ import { AdapterTransaction, StoreAdapter } from '@adhd/sox-store-adapter';
4
+
5
+ /**
6
+ * The on-write embedding substrate (§4b, FEAT-021) — the narrow slice of
7
+ * `semanticBackend`/`SemanticBackend` (`store/semantic-search.ts`,
8
+ * reference-only) this write layer actually needs. Deliberately NOT the
9
+ * spec's own `createEmbeddingObserver(semanticBackend)` shape: that observer
10
+ * is constructed from a `GraphWriteObserver` and registered with
11
+ * `createGraphBackend`'s `observers` option, so it only ever fires from
12
+ * INSIDE the library's own `writeNode`/`writeNodeInTx` (`GraphWriteObserver.
13
+ * onNodeWritten`, verified against the published `@adhd/sox-graph-store`
14
+ * dist) — a hook this file's entire premise is that the write layer
15
+ * structurally never reaches, because every write here goes through the
16
+ * hand-composed, tx-scoped primitives below instead of the library's own
17
+ * autocommitting methods (this file's own opening doc comment). So §4b's
18
+ * "AFTER the write layer's `immediate` transaction has committed" contract is
19
+ * satisfied here by explicit, write-verb-owned invocation (see
20
+ * `embedding-observer.ts`'s `scheduleIssueEmbedding`) rather than an
21
+ * observer callback — the write verb itself IS the thing that knows the
22
+ * subject transaction just committed, so it is the thing that calls this
23
+ * interface's methods, once, right after `executeWriteTransaction` resolves.
24
+ *
25
+ * `embedDocument`/`upsertVector`/`deleteVector` only — no query-side method
26
+ * (`knn`/`embedQuery`/`iterVectors`) belongs here: those are `query/**`'s
27
+ * concern (`IQueryStoreHandle.search`, `query/views/semantic.ts`), out of
28
+ * this slice's scope to touch.
29
+ */
30
+ export interface IEmbeddingBackend {
31
+ /** The embedding model identifier stamped alongside every vector this backend writes/deletes — passed straight through to `upsertVector`/`deleteVector`'s own `modelId` so a caller wiring multiple models never mixes spaces. */
32
+ readonly modelId: string;
33
+ /** Embeds `content` (the issue's `${title}\n${body}` text — see `embedding-observer.ts`'s own doc comment on why this MUST be composed identically to `create-issue.ts`'s `scanForDuplicates`). Rejects on a provider failure — never swallowed here; the caller (`scheduleIssueEmbedding`) is what degrades a rejection to a logged `embedding_failed` audit row (§4b). */
34
+ embedDocument(content: string): Promise<Float32Array>;
35
+ /** Upserts `vector` for graph node `nodeRowid` (the SAME numeric value `writeNodeTx`/`writeAudit` already track as `.rowid` — never `uid`, per `@adhd/sox-vector-store`'s own `id`-is-rowid convention, confirmed against `query/views/semantic.spec.ts`'s `indexIssue` helper). */
36
+ upsertVector(nodeRowid: number, vector: Float32Array): Promise<void>;
37
+ /** Deletes any vector stored for `nodeRowid` under `this.modelId`. A no-op (never throws) when no vector was ever stored for that rowid — mirrors `AsyncVectorBackend.delete`'s own idempotent contract (`@adhd/sox-vector-store`). */
38
+ deleteVector(nodeRowid: number): Promise<void>;
39
+ }
40
+ /**
41
+ * The dependencies a write verb needs to open its own `immediate` transaction
42
+ * and validate the edges it writes. Constructed once at store-open time (out
43
+ * of scope for this slice — see the store-bootstrap file that wires
44
+ * `createGraphBackend`'s own `typePolicy` option, DATA_MODEL.md §0 point 5)
45
+ * and threaded through every write verb.
46
+ */
47
+ export interface IWriteStoreHandle {
48
+ /** The `StoreAdapter` the backlog store exposes — never `GraphBackend.transaction`, which cannot request `immediate` mode (§4c). */
49
+ readonly adapter: StoreAdapter;
50
+ /**
51
+ * The SAME injected `TypePolicy` instance the store's `GraphBackend` was
52
+ * constructed with (or `DEFAULT_TYPE_POLICY` — though the spec requires an
53
+ * open-schema policy be injected, DATA_MODEL.md §0 point 5). `writeEdgeTx`
54
+ * calls this directly, in-process, never through `writeEdge` (§2).
55
+ */
56
+ readonly typePolicy: TypePolicy;
57
+ /**
58
+ * §4b's on-write embedding substrate — OPTIONAL, mirroring
59
+ * `IDuplicateScanHandle.search`'s own "never silently go dark, but also
60
+ * never hard-fail when unwired" posture (`create-issue.ts`). Absent →
61
+ * `scheduleIssueEmbedding` (`embedding-observer.ts`) is a true no-op: no
62
+ * embed attempt, no `embedding_*` audit row, nothing logged — filing/
63
+ * updating an issue must never depend on RAG being configured in a given
64
+ * environment. Present → every genuine create and every body-changing
65
+ * update schedules a post-commit embed/audit round-trip (see
66
+ * `embedding-observer.ts`'s own doc comment for the exact verb-by-verb
67
+ * wiring and the touch-vs-supersede scope decision).
68
+ */
69
+ readonly embedding?: IEmbeddingBackend;
70
+ }
71
+ /** A node row as read back inside a transaction, mapped onto the fields the write layer needs. */
72
+ export interface ITxNodeRow {
73
+ rowid: number;
74
+ uid: string;
75
+ kind: string;
76
+ name: string | null;
77
+ content: string;
78
+ metadata: Record<string, unknown> | undefined;
79
+ tInvalid: string | null;
80
+ isSuperseded: boolean;
81
+ }
82
+ /** Current UTC timestamp in the same ISO-8601 shape `@adhd/sox-graph-store` stamps every row with. */
83
+ export declare function nowISO(): string;
84
+ /** `sha256` hex digest of arbitrary string/Buffer content — used for citation and audit content-addressing (§4a/§8.5), never for the library's own dedupe hash (that stays trim+lowercase, see {@link writeNodeTx}). */
85
+ export declare function sha256Hex(input: string | Buffer): string;
86
+ /**
87
+ * Deterministic JSON serialization with keys sorted at every level — the
88
+ * "canonical JSON serialization... with stable (sorted) key order" DATA_MODEL.md
89
+ * §10 point 2 defines for `transition.sha` and states is "the same convention
90
+ * SPEC.md §4a already states for the audit `sha`, applied identically."
91
+ * `undefined` values are omitted (never serialized as `null` silently — a
92
+ * caller that means "explicitly null" must pass `null`, not omit the key).
93
+ */
94
+ export declare function canonicalJSONStringify(value: Record<string, unknown>): string;
95
+ /**
96
+ * uid → node, inside a tx. Mirrors `getNodeByUid`'s own SELECT
97
+ * (`SELECT * FROM node WHERE uid = ?`, `@adhd/sox-graph-store` dist/index.js:1573-1576)
98
+ * issued against `tx` instead of the bare adapter — never a `getNodeByUid`
99
+ * call itself, which always runs against `this.adapter` (§4c).
100
+ */
101
+ export declare function getNodeByUidTx(tx: AdapterTransaction, uid: string): Promise<ITxNodeRow | null>;
102
+ /**
103
+ * Resolves `uid` to the CURRENT, live `issue` row, or throws.
104
+ *
105
+ * Every issue verb needs exactly this, and the check is in two parts that are
106
+ * easy to half-implement:
107
+ *
108
+ * - `tInvalid !== null` — the issue was soft-deleted (`delete.ts`).
109
+ * - `isSuperseded` — the issue's body was edited, so `update.ts` minted a
110
+ * NEW node with a new uid and flipped this row's `is_superseded` flag.
111
+ * **The supersede CAS sets `is_superseded` ONLY; it never touches
112
+ * `t_invalid`** (update.ts's `UPDATE node SET is_superseded = 1 WHERE
113
+ * rowid = ? AND is_superseded = 0`). So a superseded row is still
114
+ * `t_invalid IS NULL`, and a `tInvalid`-only guard lets a stale uid
115
+ * straight through.
116
+ *
117
+ * That second half was missing from `claim`, `relate`, `move` and `delete`
118
+ * while `update` and `transition` each had their own hand-written copy of it.
119
+ * The consequences were silent, not loud — this repo is parallel-process
120
+ * enabled, so a caller holding a uid from before a concurrent body edit is a
121
+ * real scenario, and it could `claim` a superseded node (two agents each
122
+ * believing they hold the lease on the same issue), `relate` an edge onto it
123
+ * (permanently invisible against the issue's real uid, since `card.ts` never
124
+ * walks SUPERSEDES chains), or `delete` it and be told `invalidated: true`
125
+ * while the live issue was untouched.
126
+ *
127
+ * It exists as ONE function, rather than as a documented convention, because
128
+ * the convention is exactly what failed: six verbs hand-duplicated the fetch
129
+ * and four dropped half the guard. A new verb that calls this cannot
130
+ * reproduce the gap; a new verb that hand-rolls `getNodeByUidTx` can, so
131
+ * prefer this everywhere an issue uid arrives from a caller.
132
+ *
133
+ * @throws IssueNotFoundError when no row carries `uid`, it is not an `issue`,
134
+ * or it has been soft-deleted.
135
+ * @throws StaleSupersedeError when `uid` names a superseded node — the uid was
136
+ * valid once and names a real row, so this is deliberately NOT reported as
137
+ * "not found": the caller's reference is stale, not wrong.
138
+ */
139
+ export declare function resolveLiveIssueTx(tx: AdapterTransaction, uid: string): Promise<ITxNodeRow>;
140
+ /** 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). */
141
+ export declare function getNodeByRowidTx(tx: AdapterTransaction, rowid: number): Promise<ITxNodeRow | null>;
142
+ export interface IWriteNodeTxInput {
143
+ /** 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. */
144
+ kind: string;
145
+ /** 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). */
146
+ name?: string;
147
+ /** The content column. Defaults to `name` when omitted (mirrors `findOrCreateNode`'s own `opts?.content ?? name` convention, `@adhd/sox-graph-store` dist/index.js:1446). */
148
+ content?: string;
149
+ metadata?: Record<string, unknown>;
150
+ /**
151
+ * The ISO-8601 timestamp stamped onto this row's `t_occurred`/`t_created`/
152
+ * `t_valid` columns. Optional; defaults to a fresh {@link nowISO}() call
153
+ * when omitted, so an existing single-row call site compiles and behaves
154
+ * unchanged.
155
+ *
156
+ * A caller composing ONE logical write out of several rows (e.g.
157
+ * `createIssue`'s issue node + N citation nodes + the `writeAudit` audit
158
+ * node, all inside the SAME `executeWriteTransaction` callback) MUST
159
+ * capture a single `const now = nowISO()` up front and pass it as `at` to
160
+ * EVERY {@link writeNodeTx} (and {@link writeEdgeTx}) call in that closure.
161
+ * Without this, each call computes its own fresh, microseconds-later
162
+ * `nowISO()`, so the issue node's persisted `t_created`, the citations'
163
+ * `t_created`, and the audit row's recorded `at` all drift apart within
164
+ * what is supposed to be one atomic, single-instant write — breaking the
165
+ * guarantee (§4a) that the audit's `at` describes exactly when the subject
166
+ * it records was created.
167
+ */
168
+ at?: string;
169
+ /**
170
+ * NOT a real input — {@link writeNodeTx} unconditionally never dedupes (see
171
+ * its own doc comment below) and never reads a dedupe flag off `input` at
172
+ * all. Declared here ONLY as `never` so that a future call site which
173
+ * writes `{ ..., skipDedupe: true }` (or `false`) — believing, from having
174
+ * read the library's own `writeNodeInTx` signature, that this is a real
175
+ * per-call toggle — fails to COMPILE (excess-property check on an object
176
+ * literal) instead of silently having the property ignored. SPEC.md §1/§4
177
+ * state "All entity writes pass `skipDedupe: true`" with NO kind-scoped
178
+ * exception anywhere in the spec — every kind this write layer ever
179
+ * composes (`project`/`component`/`location`/`issue`/`kind`/`edge_kind`/
180
+ * `status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit`,
181
+ * SPEC.md §3) skips dedupe unconditionally, so there is no legitimate
182
+ * call-site variance to gate: the safest surface for "always true" is a
183
+ * surface that cannot express anything else.
184
+ */
185
+ skipDedupe?: never;
186
+ }
187
+ /**
188
+ * Hand-composed node INSERT, issued against `tx`. Mirrors `writeNodeInTx`'s
189
+ * own INSERT column list and defaults EXACTLY (`@adhd/sox-graph-store`
190
+ * dist/index.js:1410-1421) with `skipDedupe: true` UNCONDITIONALLY in every
191
+ * normal run — the spec §1 requires this on every entity write ("two
192
+ * identical-body issues are two rows, never one collapsed row") — so this
193
+ * function skips the content-hash SELECT `writeNodeInTx` runs when
194
+ * `skipDedupe` is falsy, and always inserts a fresh row. `uid` is
195
+ * `crypto.randomUUID()` — the SAME generator the library uses
196
+ * (`generateUid()`, dist/index.js:646-648) — so a write-layer-written row is
197
+ * byte-for-byte indistinguishable in shape from one the library itself would
198
+ * have written, just composed by hand to stay inside the caller's own
199
+ * transaction.
200
+ *
201
+ * **`skipDedupe` is uncontrollable from any call site, by construction, not
202
+ * by per-call convention.** {@link IWriteNodeTxInput} has no way to ask this
203
+ * function to dedupe (its `skipDedupe` field is typed `never` specifically
204
+ * so no call site can even compile one in). This is deliberate: seven more
205
+ * write verbs (`update`/`transition`/`claim`/`relate`/`move`/`delete`/
206
+ * `supersede`) are built on top of this file by separate agents, and
207
+ * SPEC.md §1/§4 ("All entity writes pass `skipDedupe: true`") name NO kind
208
+ * that is exempt — the catalog kinds (`kind`/`status`/`priority`/`agent`/
209
+ * `edge_kind`), the resolved-only kinds (`project`/`component`), and every
210
+ * subject kind (`issue`/`note`/`citation`/`transition`/`audit`/`location`)
211
+ * all go through this SAME unconditional INSERT. A future call site cannot
212
+ * "forget" to pass `skipDedupe: true` because there is no longer any code
213
+ * path — in this function OR in its input type — where forgetting it would
214
+ * change behavior. The ONLY door back to the library's own dedupe behavior
215
+ * is {@link resolveDedupeMode}'s env switch, checked below — negative-control
216
+ * test use only, never a per-call toggle.
217
+ */
218
+ export declare function writeNodeTx(tx: AdapterTransaction, input: IWriteNodeTxInput): Promise<{
219
+ rowid: number;
220
+ uid: string;
221
+ }>;
222
+ /**
223
+ * The declared `(rel, source_kind, target_kind, multiplicity)` table the spec
224
+ * §3 fixes once — the same "typed rels, each name unique with ONE
225
+ * (source_kind → target_kind)" table, `audits` carrying the one declared
226
+ * `source_kind: '*'` sentinel (§2). This is DATA the write layer already
227
+ * knows at compile time; catalog.ts's `resolveEdgeKindTx` reconciles it with
228
+ * the `edge_kind` catalog ROW (seeded once, never caller-mintable, §1) rather
229
+ * than re-deriving it from this table on every call.
230
+ */
231
+ export type EdgeMultiplicity = 'n:1' | '1:n' | 'n:m';
232
+ export interface IEdgeKindRule {
233
+ rel: string;
234
+ /** `'*'` is the one declared sentinel (`audits`, §2) — skip the source-kind match for this rule only. */
235
+ sourceKind: string;
236
+ targetKind: string;
237
+ multiplicity: EdgeMultiplicity;
238
+ }
239
+ export interface IWriteEdgeTxInput {
240
+ srcRowid: number;
241
+ srcUid: string;
242
+ srcKind: string;
243
+ dstRowid: number;
244
+ dstUid: string;
245
+ dstKind: string;
246
+ rel: string;
247
+ metadata?: Record<string, unknown>;
248
+ weight?: number;
249
+ /** The resolved `edge_kind` rule this rel must satisfy (catalog.ts's `resolveEdgeKindTx`). */
250
+ rule: IEdgeKindRule;
251
+ typePolicy: TypePolicy;
252
+ /**
253
+ * The ISO-8601 timestamp stamped onto this edge's `t_created`/`t_valid`
254
+ * columns. Optional; defaults to a fresh {@link nowISO}() call when
255
+ * omitted. Same one-logical-write-one-timestamp rule as
256
+ * {@link IWriteNodeTxInput.at} — a caller writing several edges (or an
257
+ * edge alongside node writes) inside one `executeWriteTransaction`
258
+ * callback should pass the SAME captured `now` to every one of them.
259
+ */
260
+ at?: string;
261
+ }
262
+ /**
263
+ * Edge write, inside a tx (upsert, re-livening included). Mirrors
264
+ * `writeEdgeInternal`'s own INSERT EXACTLY (`@adhd/sox-graph-store`
265
+ * dist/index.js:1894-1898) — same `ON CONFLICT(src, dst, rel) DO UPDATE`,
266
+ * same `t_invalid = NULL` re-livening — issued against `tx` instead of the
267
+ * bare adapter, never a `writeEdge` call itself (§4c).
268
+ *
269
+ * Endpoint-kind validation (§2: resolve `edge_kind`, check `source_kind`/
270
+ * `target_kind`, THEN call the injected `TypePolicy` directly in-process,
271
+ * never through `writeEdge`) and multiplicity enforcement both run BEFORE the
272
+ * INSERT, against the SAME `tx` handle.
273
+ */
274
+ export declare function writeEdgeTx(tx: AdapterTransaction, input: IWriteEdgeTxInput): Promise<void>;
275
+ /**
276
+ * Internal — a resolved `edge_kind` rule's declared endpoint kind does not
277
+ * match the actual endpoint being written. This is a write-layer composition
278
+ * defect (the caller passed the wrong node as an endpoint), never a
279
+ * caller-facing input-validation case with its own named class in the spec's
280
+ * taxonomy — every write-layer call site passes endpoint kinds it just resolved or
281
+ * wrote itself in the SAME transaction, so this should be unreachable in
282
+ * practice; it exists to fail loudly rather than silently write a
283
+ * mismatched edge if that invariant is ever broken by a future verb.
284
+ */
285
+ export declare class BacklogEdgeKindMismatchError extends BacklogWriteError {
286
+ readonly code: "E_VALIDATION";
287
+ readonly retryable = false;
288
+ constructor(rel: string, side: 'source' | 'target', expectedKind: string, actualKind: string);
289
+ }
290
+ export interface IInvalidateEdgeTxInput {
291
+ srcRowid: number;
292
+ dstRowid: number;
293
+ rel: string;
294
+ reason?: string;
295
+ /**
296
+ * The ISO-8601 timestamp stamped onto `t_invalid`/`meta.invalidatedAt`.
297
+ * Optional; defaults to a fresh {@link nowISO}() call when omitted. Same
298
+ * one-logical-write-one-timestamp rule as {@link IWriteNodeTxInput.at}.
299
+ */
300
+ at?: string;
301
+ }
302
+ /**
303
+ * Edge invalidate, inside a tx. Mirrors `invalidateEdge` EXACTLY
304
+ * (`@adhd/sox-graph-store` dist/index.js:1846-1855) — same idempotent
305
+ * "already invalidated or absent → no-op" behavior — issued against `tx`
306
+ * instead of the bare adapter.
307
+ */
308
+ export declare function invalidateEdgeTx(tx: AdapterTransaction, input: IInvalidateEdgeTxInput): Promise<void>;
309
+ /**
310
+ * The one transaction wrapper every write verb calls — `store.adapter.transaction(fn,
311
+ * {mode: resolveTransactionMode()})`, which is `'immediate'` (§4c: `BEGIN
312
+ * IMMEDIATE`, the RESERVED-lock-at-BEGIN "compare-and-swap primitive" every
313
+ * check-then-act verb in §4's table depends on) in every normal run and only
314
+ * ever `'deferred'` when a test has explicitly set
315
+ * `ADHD_BACKLOG_UNSAFE_TX_MODE=deferred` to prove a negative control (see
316
+ * {@link resolveTransactionMode}'s own doc comment) — with the §4c retry
317
+ * contract layered on top:
318
+ *
319
+ * - A {@link BacklogWriteError} thrown from `fn` (an app-level validation
320
+ * failure, or the `supersede` CAS's {@link StaleSupersedeError}) is
321
+ * ALREADY a decided, terminal, transport-facing error — rethrown
322
+ * immediately, untouched, never reclassified or retried.
323
+ * - A raw driver-level error is classified via {@link classifyDriverError}.
324
+ * `E_CONTENTION` retries up to 3 total attempts (linear backoff 250ms then
325
+ * 500ms) before surfacing {@link WriteContentionError}. `E_IO`
326
+ * (`isDatabaseError(err)` true — a recognized-but-otherwise-unclassified
327
+ * database/driver error) surfaces {@link WriteIOError} on the FIRST
328
+ * occurrence, never retried — §4c's uniform reason across every write
329
+ * class: `writeAudit` (§4a) rides inside literally every write-layer
330
+ * transaction as an unguarded, keyless-content INSERT, so a retry whose
331
+ * earlier attempt actually committed would silently double the audit
332
+ * trail even where the SUBJECT write is safe to retry on its own merits.
333
+ * An `E_IO` classification whose `err` is NOT database-shaped at all
334
+ * (`isDatabaseError(err)` false — a genuine app-level bug, never a driver
335
+ * fault) is rethrown UNTOUCHED instead — never wrapped in
336
+ * {@link WriteIOError}, which always asserts `retryable: true`.
337
+ * - An `E_CONSTRAINT` this function did not already expect as
338
+ * {@link StaleSupersedeError} (i.e. a raw, unclassified constraint
339
+ * violation — structurally unreachable under the hand-composed
340
+ * find-then-create + `immediate`-mode discipline every verb follows, §1)
341
+ * is rethrown UNTOUCHED — never swallowed, never coerced into a
342
+ * not-actually-specified wrapper class.
343
+ */
344
+ export declare function executeWriteTransaction<T>(handle: IWriteStoreHandle, fn: (tx: AdapterTransaction) => Promise<T>): Promise<T>;
@@ -0,0 +1,81 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+
3
+ export interface IUpdateIssueInput {
4
+ /** The `issue` uid to update (§6.3, an "Issue verb"). */
5
+ uid: string;
6
+ /** The acting identity — an agent or a person (§6.3's opening rule). REQUIRED. */
7
+ by: string;
8
+ /** → `touch` (metadata/name only). */
9
+ title?: string;
10
+ /** → `supersede` (§3/§4: "body change → supersede... never touch a body"). Mints a fresh `uid` — see {@link IUpdateIssueOutcome.uid}. */
11
+ body?: string;
12
+ /** catalog name or uid; → `touch` + `has_kind` edge rewrite (hand-composed invalidate-old + upsert-new, same tx, §4c). An unresolved NAME mints; a uid-shaped ref that does not resolve throws `CatalogNotFoundError('kind', ref)` (§6.1 — minting never applies to a uid). */
13
+ kind?: string;
14
+ /** catalog name or uid; → `touch` + `has_priority` edge rewrite. An unresolved NAME mints (rank = one past the current max); a uid-shaped ref that does not resolve throws `CatalogNotFoundError('priority', ref)`. */
15
+ priority?: string;
16
+ /** Plain metadata scalar (§6.2) — no edge. */
17
+ assignee?: string;
18
+ /** 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
+ author?: string;
20
+ /**
21
+ * §4b/§6.2 — waits for the fire-and-forget on-write embedding round-trip
22
+ * before `update` returns, when `true` and `handle.embedding` is
23
+ * configured. Only meaningful on a BODY-changing call (the `supersede`
24
+ * path) — a pure touch (title/kind/priority/assignee/author only) never
25
+ * schedules a re-embed at all, so `awaitEmbed:true` on a touch-only patch
26
+ * is a harmless no-op (see `embedding-observer.ts`'s own doc comment for
27
+ * why touch is deliberately excluded). On a body change, TWO round-trips
28
+ * are scheduled (delete the old node's vector, upsert the new node's) —
29
+ * `awaitEmbed:true` awaits BOTH before returning.
30
+ */
31
+ awaitEmbed?: boolean;
32
+ }
33
+ export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author';
34
+ export interface IUpdateIssueOutcome {
35
+ /**
36
+ * The resulting CURRENT issue's uid — the SAME `input.uid` when `body` was
37
+ * not given (a pure touch/edge-rewrite on the existing node), or the FRESH
38
+ * uid the supersede minted when it was (§6.3.2's `create+supersedes`
39
+ * composition reads exactly this field as "the NEW node the supersede
40
+ * primitive minted" — `IUpdateIssueOutcome` carries no separate
41
+ * `supersededUid`, so `input.uid` is that composition's own record of what
42
+ * was superseded).
43
+ */
44
+ uid: string;
45
+ /**
46
+ * Every key present in the input with a defined value, and ONLY those keys
47
+ * (`assertNoSilentlyDiscardedPatchKeys`, carried forward per §6.3.3 as a
48
+ * general correctness rule, not machinery scoped to any particular patch
49
+ * shape). Empty is unreachable here — a zero-field patch throws
50
+ * `InvalidArgumentError` before this outcome is ever constructed.
51
+ */
52
+ changed: IUpdateIssueChangedField[];
53
+ }
54
+ /**
55
+ * Update an issue (§4, §6.3.3). One `immediate` transaction.
56
+ *
57
+ * Errors: `InvalidArgumentError` (`uid`/`by` missing/blank; blank `title`/
58
+ * `body` when given; no fields at all in the patch — a zero-field call is a
59
+ * client error, not a silent no-op success, since the caller almost
60
+ * certainly meant a different verb), `BacklogValidationError('status', ...)`
61
+ * (an untyped caller sent a `status` field — §8 AC-14, this is the ONE
62
+ * runtime rejection this whole file exists to guarantee, and it names
63
+ * `transition` in its message), `IssueNotFoundError` (no live `issue` node
64
+ * carries `uid`, including an already-superseded one — this file's own doc
65
+ * comment on why that is a deliberate divergence, reusing
66
+ * `StaleSupersedeError` rather than `IssueNotFoundError` for that specific
67
+ * case since the identity itself still exists, just not at THIS uid
68
+ * anymore), `CatalogNotFoundError('kind'|'priority'|'agent', ref)`
69
+ * (uid-shaped ref only — an unresolved NAME instead auto-mints, §6.1),
70
+ * `InvalidArgumentError('kind', ...)` (§2 `project_kind` — a resolved
71
+ * `kind` name outside this project's non-empty `allowedKinds` set) / a
72
+ * project-declared `requiredFields` entry left blank by this call (§2 —
73
+ * see this file's own doc comment on the composition gap this resolves),
74
+ * `StaleSupersedeError(uid)` (body-change path only — the supersede CAS
75
+ * lost a race to a concurrent edit of the same `uid`, OR `uid` was already
76
+ * superseded before this call ever started; the patch was not applied,
77
+ * re-`get` and retry, never a silent partial apply),
78
+ * `WriteContentionError`/`WriteIOError` (§4c — an exhausted driver-level
79
+ * retry on the underlying `immediate` transaction).
80
+ */
81
+ export declare function update(handle: IWriteStoreHandle, input: IUpdateIssueInput): Promise<IUpdateIssueOutcome>;
package/client.d.ts DELETED
@@ -1,169 +0,0 @@
1
- import { GraphBacklogStore } from './store/graph-backlog-store.js';
2
- import { BacklogConfig } from './env.js';
3
- import { ArchiveOpts, ArchiveResult, AuditTrailResult, BacklogFilter, BacklogItem, BacklogStats, BacklogStatus, ClaimOpts, ClaimResult, Citation, CreateItemInput, CreateItemResult, DependencyGraph, ImportMarkdownInput, ImportResult, MigrationPhase, MigrationStatusResult, Priority, ReleaseResult, RepoMigrationResult, SetMigrationPhaseResult, StatsScope, TopoOrderResult, TransitionOpts, UpdateItemInput } from './model.js';
4
- import { Environment } from '@adhd/environment';
5
-
6
- /** The one type apigen special-cases via the `ctx-name-only` invariant. */
7
- export interface BacklogCtx {
8
- store: GraphBacklogStore;
9
- env: Environment<BacklogConfig>;
10
- /**
11
- * Test-isolation escape hatch ONLY — mirrors `BuildBacklogEnvOptions.adhdRoot`
12
- * (the same value passed to `buildBacklogEnv({ adhdRoot })` when constructing
13
- * `env`). NEVER set this in production code (`server.ts`/`cli.ts` never do).
14
- * `setMigrationPhase` threads it through to `writeMigrationPhase` so a
15
- * temp-rooted test `ctx` can never write to the real machine-global
16
- * `~/.adhd` — omitting this on a real ctx write is exactly the bug
17
- * `migration-admin.spec.ts`'s negative control caught (a test run wrote
18
- * `phase-4` to the real `~/.adhd/backlog/production/config.yaml` before
19
- * this field existed; reverted, see CHANGELOG).
20
- */
21
- adhdRoot?: string;
22
- }
23
- /**
24
- * Dedupe-scans (FTS + symbol/path/errorText metadata match) before writing.
25
- * Allocates humanId as family + next number within (repo, family) unless
26
- * idOverride is given.
27
- */
28
- export declare function createItem(ctx: BacklogCtx, input: CreateItemInput): Promise<CreateItemResult>;
29
- /**
30
- * repo is required — humanId alone is not globally unique. A genuine miss
31
- * (humanId doesn't exist under ANY repo) still returns `null` — unchanged,
32
- * every existing caller relying on nullable-not-throwing keeps working.
33
- *
34
- * BUG-BACKLOG-REPO-LOOKUP-UX-001: previously a repo/humanId MISMATCH (the
35
- * item is live, just filed under a different `repo` string) was
36
- * indistinguishable from a genuine miss — both silently returned `null`,
37
- * which is worse than `appendNote`'s bare-but-at-least-thrown
38
- * `BacklogItemNotFoundError` (and, before this fix, could surface through
39
- * apigen's MCP layer as the unrelated broken int64/null encoding,
40
- * BUG-APIGEN-LOGICAL-NULL-OBJECT-RESULT-INT64-001 — out of scope here, but
41
- * this fix removes the only path that made this lookup look like that bug).
42
- * Now: a real cross-repo match THROWS the same informative
43
- * `BacklogItemNotFoundError` (with `foundInRepos` naming the actual repo) the
44
- * mutating lookups already throw, instead of masquerading as "not found".
45
- */
46
- export declare function getItem(ctx: BacklogCtx, repo: string, humanId: string): Promise<BacklogItem | null>;
47
- export declare function updateItem(ctx: BacklogCtx, repo: string, humanId: string, patch: UpdateItemInput): Promise<BacklogItem>;
48
- export declare function listItems(ctx: BacklogCtx, filter?: BacklogFilter): Promise<BacklogItem[]>;
49
- /** Invalidates the node (bi-temporal — never a hard delete). */
50
- export declare function softDeleteItem(ctx: BacklogCtx, repo: string, humanId: string, reason: string): Promise<void>;
51
- export declare function stats(ctx: BacklogCtx, scope?: StatsScope): Promise<BacklogStats>;
52
- /** Open + prioritized, most-severe first. */
53
- export declare function spotlight(ctx: BacklogCtx, scope?: StatsScope, limit?: number): Promise<BacklogItem[]>;
54
- /** Open items whose every DEPENDS_ON target is a terminal status AND which are not currently claimed. */
55
- export declare function readyItems(ctx: BacklogCtx, scope?: StatsScope): Promise<BacklogItem[]>;
56
- /** The DEPENDS_ON set of `humanId` that is NOT yet terminal. */
57
- export declare function blockers(ctx: BacklogCtx, repo: string, humanId: string): Promise<BacklogItem[]>;
58
- export declare function dependencyGraph(ctx: BacklogCtx, scope?: StatsScope): Promise<DependencyGraph>;
59
- export declare function topoOrder(ctx: BacklogCtx, scope?: StatsScope): Promise<TopoOrderResult>;
60
- /** Items whose claim lease is older than maxAgeMin with no renewal — candidates for --force reclaim. */
61
- export declare function staleClaims(ctx: BacklogCtx, maxAgeMin: number, scope?: StatsScope): Promise<BacklogItem[]>;
62
- export declare function claimItem(ctx: BacklogCtx, repo: string, humanId: string, by: string, opts?: ClaimOpts): Promise<ClaimResult>;
63
- /** Same-claimant renewal — always succeeds (bumps claimedAt), no contention check. */
64
- export declare function renewClaim(ctx: BacklogCtx, repo: string, humanId: string, by: string): Promise<ClaimResult>;
65
- export declare function releaseClaim(ctx: BacklogCtx, repo: string, humanId: string, by: string, opts?: {
66
- force?: boolean;
67
- }): Promise<ReleaseResult>;
68
- /** Durable ownership (planner decision) — distinct from the ephemeral claim lease. */
69
- export declare function assignItem(ctx: BacklogCtx, repo: string, humanId: string, to: string, by: string): Promise<BacklogItem>;
70
- /** transitionStatus(id, 'IN_PROGRESS', ...) + an implicit claimItem(id, by) — a no-op claim-wise if already held by `by`. */
71
- export declare function startWork(ctx: BacklogCtx, repo: string, humanId: string, by: string): Promise<BacklogItem>;
72
- export declare function transitionStatus(ctx: BacklogCtx, repo: string, humanId: string, status: BacklogStatus, opts: TransitionOpts): Promise<BacklogItem>;
73
- export declare function addCitation(ctx: BacklogCtx, repo: string, humanId: string, citation: Citation): Promise<BacklogItem>;
74
- export declare function appendNote(ctx: BacklogCtx, repo: string, humanId: string, by: string, text: string): Promise<BacklogItem>;
75
- /** Sugar for transitionStatus into any terminal status. */
76
- export declare function resolveItem(ctx: BacklogCtx, repo: string, humanId: string, status: BacklogStatus, opts: TransitionOpts): Promise<BacklogItem>;
77
- /**
78
- * Renders terminal items to CHANGELOG.md-formatted markdown and marks them
79
- * archived (metadata.archivedAt set) so renderToMarkdown's default view
80
- * excludes them — the graph node itself is NEVER deleted.
81
- */
82
- export declare function archiveResolved(ctx: BacklogCtx, scope: StatsScope, opts?: ArchiveOpts): Promise<ArchiveResult>;
83
- export declare function addDependency(ctx: BacklogCtx, repo: string, humanId: string, dependsOnHumanId: string): Promise<void>;
84
- export declare function removeDependency(ctx: BacklogCtx, repo: string, humanId: string, dependsOnHumanId: string): Promise<void>;
85
- export declare function linkRelated(ctx: BacklogCtx, repo: string, humanIdA: string, humanIdB: string): Promise<void>;
86
- /** Mints a new item, links new SUPERSEDES old, invalidates old with reason. */
87
- export declare function supersedeItem(ctx: BacklogCtx, repo: string, oldHumanId: string, newInput: CreateItemInput, reason: string): Promise<BacklogItem>;
88
- /** Creates N children linked child PART_OF parent. Parent is left open. */
89
- export declare function splitItem(ctx: BacklogCtx, repo: string, parentHumanId: string, children: CreateItemInput[]): Promise<BacklogItem[]>;
90
- /** SAME_AS(drop -> keep), invalidates drop with an auto-generated reason. */
91
- export declare function mergeItems(ctx: BacklogCtx, repo: string, keepHumanId: string, dropHumanId: string, reason: string): Promise<BacklogItem>;
92
- export declare function setPriority(ctx: BacklogCtx, repo: string, humanId: string, priority: Priority): Promise<BacklogItem>;
93
- /** MEMBER_OF edge to a plan node (auto-created if the plan slug hasn't been seen before). */
94
- export declare function attachToPlan(ctx: BacklogCtx, repo: string, humanId: string, planSlug: string): Promise<void>;
95
- /**
96
- * BUG-BACKLOG-REPO-SPLIT-001 / DEBT-BACKLOG-REPO-MOVE-001 — the dedicated
97
- * primitive to move every live item out of `fromRepo` into `toRepo` (a
98
- * repo-key correction, e.g. a legacy `"adhd"` string onto the canonical
99
- * `"PseudoSky/adhd"`). `dryRun` defaults to `true`: a caller must pass
100
- * `dryRun:false` explicitly to write anything — the default call is always
101
- * safe to make speculatively and returns the full plan (including which
102
- * items would be renamed, and to what) with zero mutation.
103
- *
104
- * Collisions (a humanId already live in `toRepo`) are never silently
105
- * dropped, overwritten, or left ambiguous — each is deterministically
106
- * renamed to the next free number in its own family within `toRepo`
107
- * (`store/repo-migration.ts`'s `planRepoMigration`), and an audit note
108
- * recording the original `(fromRepo, humanId)` is attached to the moved
109
- * item so the rename is traceable. Cross-item links (DEPENDS_ON,
110
- * RELATES_TO, PART_OF, SUPERSEDES, SAME_AS, MEMBER_OF, ASSIGNED_TO) are
111
- * preserved automatically — a move never changes a node's id, only its
112
- * `namespace`/`repo`/`humanId`/`name`/`content`.
113
- *
114
- * Every planned item gets exactly one reported outcome
115
- * (`results[i].ok`/`error`) when `dryRun:false` — a per-item failure never
116
- * aborts the rest of the batch and never goes unreported.
117
- */
118
- export declare function migrateRepo(ctx: BacklogCtx, fromRepo: string, toRepo: string, by: string, dryRun?: boolean): Promise<RepoMigrationResult>;
119
- export declare function importFromMarkdown(ctx: BacklogCtx, input: ImportMarkdownInput): Promise<ImportResult>;
120
- /**
121
- * Excludes archived items (SPEC.md §5.4 archiveResolved) — see markdown.ts's
122
- * renderItemsToMarkdown doc comment. Archival exclusion goes through
123
- * `BacklogFilter.excludeArchived` (query.ts's `applyExcludeArchivedFilter`)
124
- * rather than a private scan here, so a caller comparing this output
125
- * against `listItems`/`queryItemNodes` for the SAME filter (e.g.
126
- * `render-projections.mjs`'s round-trip verify) can reproduce this exact
127
- * item set by passing `{ ...filter, excludeArchived: true }` themselves —
128
- * see BUG-BACKLOG-RENDER-VERIFY-ARCHIVED-MISMATCH-001.
129
- */
130
- export declare function renderToMarkdown(ctx: BacklogCtx, filter?: BacklogFilter): Promise<string>;
131
- export declare function exportJson(ctx: BacklogCtx, filter?: BacklogFilter): Promise<BacklogItem[]>;
132
- /** Bi-temporal history + supersession chain. */
133
- export declare function auditTrail(ctx: BacklogCtx, repo: string, humanId: string): Promise<AuditTrailResult>;
134
- /**
135
- * MIGRATION.md §4.4 — a QUERIED signal, never hardcoded prose: reports the
136
- * live `migration.phase` config value (`env.ts`, env-overridable via
137
- * `ADHD_BACKLOG_MIGRATION_PHASE`) plus a human-readable meaning, so an agent
138
- * (or the `backlog-usage` skill) always asks the tool which of BACKLOG.md or
139
- * the tool is authoritative right now, instead of trusting a stale doc
140
- * sentence. NOT yet per-repo-keyed (MIGRATION.md §9 open decision 6) — one
141
- * global value for the whole machine.
142
- */
143
- export declare function migrationStatus(ctx: BacklogCtx): Promise<MigrationStatusResult>;
144
- /**
145
- * MIGRATION.md §4.4's "admin CLI call" half: writes `migration.phase`
146
- * THROUGH to the GLOBAL layer's `config.yaml` (`migration-admin.ts`) so the
147
- * new value is a durable, cross-process, cross-repo signal — not merely an
148
- * env var scoped to whoever's shell happened to export it. Whoever executes
149
- * a phase's Definition of Done calls this exactly once, after verifying the
150
- * DoD, never speculatively.
151
- */
152
- export declare function setMigrationPhase(ctx: BacklogCtx, phase: MigrationPhase): Promise<SetMigrationPhaseResult>;
153
- export interface BacklogVersionInfo {
154
- /** This package's real `package.json` name, e.g. `"@adhd/backlog"`. */
155
- name: string;
156
- /** This package's real `package.json` version — never hardcoded. */
157
- version: string;
158
- }
159
- /**
160
- * Reports this running package's own real `name`/`version`, read fresh from
161
- * `package.json` on every call (never a compiled-in constant, so a
162
- * republished build can never drift from what this reports). `ctx` is
163
- * unused — kept for signature consistency with every other `client.ts`
164
- * export (the `ctx-name-only` invariant every extraction/mount/CLI-dispatch
165
- * path in this package assumes, per this file's own top-of-file doc
166
- * comment) rather than special-casing a bare, ctx-less export whose
167
- * extraction/dispatch behavior has not been verified.
168
- */
169
- export declare function version(ctx: BacklogCtx): Promise<BacklogVersionInfo>;