@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
@@ -0,0 +1,351 @@
1
+ import { IComponentSummary, ILocationSummary, ILocationType, IProjectSummary } from '../query/types.js';
2
+ import { IEdgeKindRule, IWriteStoreHandle } from './tx.js';
3
+ import { AdapterTransaction } from '@adhd/sox-store-adapter';
4
+
5
+ /** Whether `ref` should be treated as a `uid` (exact resolve-or-throw) rather than a business `name` (find, and for the flat catalogs, find-then-mint). */
6
+ export declare function isUidShaped(ref: string): boolean;
7
+ export interface IResolvedCatalogRow {
8
+ rowid: number;
9
+ uid: string;
10
+ name: string;
11
+ }
12
+ export interface IResolvedProjectRow extends IResolvedCatalogRow {
13
+ metadata: Record<string, unknown> | undefined;
14
+ }
15
+ /**
16
+ * `project` (§1, §6.1): resolved-ONLY, `uid` or `name`, NEVER minted by an
17
+ * issue verb — minting a project is the explicit `upsertProject` registry
18
+ * verb's job alone (§3a), out of scope for this slice.
19
+ */
20
+ export declare function resolveProjectTx(tx: AdapterTransaction, ref: string): Promise<IResolvedProjectRow>;
21
+ /**
22
+ * `component`, scoped within `project` (§6.1, §6.3.2): resolved-ONLY, `uid`
23
+ * or `name` — an unresolved name throws `CatalogNotFoundError('component', name)`
24
+ * rather than silently forking a new component under the given project.
25
+ */
26
+ export declare function resolveComponentTx(tx: AdapterTransaction, input: {
27
+ projectUid: string;
28
+ ref: string;
29
+ }): Promise<IResolvedCatalogRow>;
30
+ /**
31
+ * `project`'s reserved default component, `(root)` (§3, §6.1, §6.3.2,
32
+ * §8 AC-23): what `createIssue` resolves to when `component` is omitted.
33
+ * A row `upsertProject` already guarantees is live for every project — NEVER
34
+ * minted here, in either case (§1). Resolved via `owns_project` (the SAME
35
+ * traversal §3a's registry uses), not a bare name lookup, so a `(root)` row
36
+ * belonging to a DIFFERENT project can never be mismatched onto this one.
37
+ */
38
+ export declare function resolveDefaultComponentTx(tx: AdapterTransaction, input: {
39
+ projectRowid: number;
40
+ }): Promise<IResolvedCatalogRow>;
41
+ export type FlatCatalogKind = 'kind' | 'status' | 'priority' | 'agent';
42
+ export interface IMintOrResolveInput {
43
+ catalogKind: FlatCatalogKind;
44
+ ref: string;
45
+ /** Metadata to seed onto a MINTED row only — never read or applied when `ref` resolves to an existing row. Evaluated lazily (only on an actual mint) since e.g. `priority`'s rank needs a fresh in-tx MAX query (§6.3.2). */
46
+ mintMetadata?: (tx: AdapterTransaction) => Promise<Record<string, unknown>>;
47
+ /**
48
+ * The caller's single logical-write timestamp, forwarded to `writeNodeTx` so a
49
+ * minted catalog row carries the SAME `t_created`/`t_valid` as the issue that
50
+ * caused the mint. Omitted, `writeNodeTx` stamps its own `nowISO()`, which is
51
+ * what produced per-row drift within one `createIssue`.
52
+ *
53
+ * Deliberately NOT threaded into `resolveEdgeKindTx`'s `edge_kind` rows: those
54
+ * are schema-infrastructure rows describing the relation table itself, not part
55
+ * of any one issue's logical write, so they keep their own clock.
56
+ */
57
+ at?: string;
58
+ }
59
+ /**
60
+ * The flat-catalog find-then-create (§1, §4c, §6.1): `kind`/`status`/
61
+ * `priority`/`agent` are mintable on an unresolved NAME; a uid-shaped `ref`
62
+ * that does not resolve instead throws — minting NEVER applies to a uid.
63
+ */
64
+ export declare function mintOrResolveCatalogTx(tx: AdapterTransaction, input: IMintOrResolveInput): Promise<IResolvedCatalogRow>;
65
+ /** `priority`'s mint rule (§6.3.2): "rank set to one past the current max rank (i.e. lowest urgency) — a novel priority can never silently outrank an existing one." */
66
+ export declare function nextPriorityRankTx(tx: AdapterTransaction): Promise<number>;
67
+ /**
68
+ * §3's fixed edge table — the ONE declared shape for every `rel` (§2's
69
+ * closing statement: "the SAME check for every row, never two different
70
+ * checks"). `audits`' `source_kind: '*'` is the one declared sentinel (§2).
71
+ */
72
+ export declare const EDGE_KIND_TABLE: readonly IEdgeKindRule[];
73
+ /**
74
+ * Resolve the `edge_kind` catalog row for `rel`, inside `tx` — the SAME
75
+ * `tx.executeGet` lookup §1 describes for the VALIDATION half of every edge
76
+ * write. `edge_kind` is NEVER caller-mintable (§1, §6.1): every row is
77
+ * "seeded once from §3's fixed edge table," normally by a dedicated
78
+ * bootstrap/seed step (§8.6 step 2 is the ETL's own version of this same
79
+ * seeding).
80
+ *
81
+ * **Documented deviation for this slice.** This task ships the write-layer
82
+ * core only (`errors.ts`/`tx.ts`/`catalog.ts`/`audit.ts`/`create-issue.ts`) —
83
+ * no dedicated seed/bootstrap script exists yet to have run before
84
+ * `createIssue` is first called. Rather than make `createIssue` depend on an
85
+ * out-of-scope script, this function self-heals: on a miss, it seeds the
86
+ * row from {@link EDGE_KIND_TABLE} — the IDENTICAL fixed §3 shape a real seed
87
+ * script would write, never a caller-supplied one — inside the SAME `tx` the
88
+ * calling verb already opened. This satisfies "seeded once" (idempotent:
89
+ * the `tx.executeGet` above finds it on every subsequent call) without
90
+ * requiring a separate bootstrap step to exist first. A future seed script
91
+ * making this self-heal path dead code is expected and fine — the row shape
92
+ * it would find already-present is identical either way.
93
+ *
94
+ * **A malformed row (should never happen — only this module ever writes
95
+ * one) is guarded, never crashed on, and never left to accumulate.** `meta`
96
+ * is read via {@link parseMetaObject} — a non-JSON `meta` value degrades to
97
+ * `{}` instead of throwing a raw `SyntaxError` out of the transaction
98
+ * callback (Finding 1: unguarded, that would misclassify as a retryable
99
+ * `E_IO` at `executeWriteTransaction`'s boundary and retry forever against a
100
+ * deterministic parse failure). Either that degraded `{}`, or a
101
+ * successfully-parsed object missing one of the three expected keys, takes
102
+ * the SAME "malformed" branch below: the row is invalidated and a correct
103
+ * replacement is minted from {@link EDGE_KIND_TABLE} in the SAME `tx`, so the
104
+ * next call's SELECT (now a deterministic `ORDER BY rowid` — the prior
105
+ * ordering-free `LIMIT 1` made which duplicate got read unspecified, Finding
106
+ * 2) can never re-find the malformed row and re-mint yet another duplicate.
107
+ * The store converges to exactly one live `edge_kind` row per `rel` after
108
+ * this call, rather than growing an unbounded set of malformed duplicates.
109
+ */
110
+ export declare function resolveEdgeKindTx(tx: AdapterTransaction, rel: string): Promise<IEdgeKindRule>;
111
+ /**
112
+ * Per-project policy (§2): DATA, realized as `project.meta.metadata.policy`
113
+ * — §2's table has no dedicated node/edge of its own (it is 1:1 with
114
+ * `project`, never many-to-many), so it lives inside the SAME metadata blob
115
+ * `project`'s other declared fields (`repoUrl`, `monorepo`, …, §3a) already
116
+ * occupy. Every field defaults exactly as §2 states when the project carries
117
+ * no `policy` object at all (a project seeded before policy existed, or
118
+ * minimally).
119
+ */
120
+ export interface IProjectPolicy {
121
+ readonly transitionRequiresNote: boolean;
122
+ readonly citationRequired: boolean;
123
+ readonly citationRequiresSha: boolean;
124
+ readonly defaultStatus?: string;
125
+ readonly defaultKind?: string;
126
+ readonly dedupeScanEnabled: boolean;
127
+ readonly dedupeThreshold: number;
128
+ readonly claimStaleAfterMin: number;
129
+ /** `project_status` — empty means "no restriction" (§2). */
130
+ readonly allowedStatuses: readonly string[];
131
+ /** `project_kind` — empty means "no restriction" (§2). */
132
+ readonly allowedKinds: readonly string[];
133
+ /** `project_field_requirement` — field names required on every mutating write for this project (§2). */
134
+ readonly requiredFields: readonly string[];
135
+ }
136
+ export declare function resolveProjectPolicy(project: IResolvedProjectRow): IProjectPolicy;
137
+ /**
138
+ * Whether a resolved project has a known filesystem `path` (§8.4/§8.5) — the
139
+ * precondition for citation content-addressing to be POSSIBLE at all. When it
140
+ * does not, `computeCitationSha` can only ever return the `"unverified"`
141
+ * sentinel (`create-issue.ts`/`transition.ts` both short-circuit on a
142
+ * non-string/empty `metadata.path`), so `project_policy.citationRequiresSha`
143
+ * has nothing to gate: the gate applies only where verification is possible.
144
+ * A path-less project therefore records `sha:"unverified"` verbatim, matching
145
+ * the ETL's own precedent (`tools/etl/citation.ts`). Kept beside
146
+ * {@link resolveProjectPolicy} — it reads the same resolved project row. The
147
+ * PREDICATE itself is shared by BOTH write paths so the `create`/`transition`
148
+ * gates never drift on WHICH projects are verifiable; the surrounding
149
+ * `computeCitationSha` hashing body is NOT shared — it stays a per-file copy
150
+ * in each verb (this package's established per-file-duplication convention —
151
+ * see `transition.ts`'s own doc comment on its copy).
152
+ *
153
+ * A TYPE PREDICATE, not a bare `boolean`: both `computeCitationSha` call sites
154
+ * (and the waiver log beside each gate) need the narrowed `metadata.path` as a
155
+ * `string` immediately after the guard, and a predicate is the one form that
156
+ * gives them that without a non-null assertion or a re-read of the same field.
157
+ */
158
+ export declare function projectHasKnownPath(project: IResolvedProjectRow): project is IResolvedProjectRow & {
159
+ metadata: Record<string, unknown> & {
160
+ path: string;
161
+ };
162
+ };
163
+ export interface IUpsertProjectInput {
164
+ /** The business key `project` is upserted by (§3a, §4). */
165
+ name: string;
166
+ path?: string;
167
+ repoUrl?: string;
168
+ monorepo?: boolean;
169
+ description?: string;
170
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
171
+ by: string;
172
+ }
173
+ export interface IUpsertProjectOutcome {
174
+ uid: string;
175
+ /** `false` when an existing LIVE `project` row with this `name` was found and updated in place; `true` only on a genuine first mint. */
176
+ created: boolean;
177
+ project: IProjectSummary;
178
+ }
179
+ /**
180
+ * Create-or-update a `project` by `name` (§3a, §4). One `immediate`
181
+ * transaction: find the LIVE `project` row by `name` — on a HIT, merge the
182
+ * supplied fields into the EXISTING `meta` blob (never a wholesale replace;
183
+ * an existing `meta.policy` — §2's per-project policy, read back by
184
+ * {@link resolveProjectPolicy} — survives every subsequent `upsertProject`
185
+ * untouched) and audit `'updated'`; on a MISS, mint the `project` row AND,
186
+ * "on first creation ONLY," the reserved default `component` row named
187
+ * `(root)` plus its `owns_project` edge, in the SAME transaction (§4) — audit
188
+ * `'created'`. A repeat `upsertProject` against an existing project finds
189
+ * `(root)` already live (via the existing-project branch, which never touches
190
+ * it) and writes nothing further for it: idempotent, never a second row.
191
+ *
192
+ * Race-safety (§4c): the find-then-create SELECT and the INSERT/UPDATE both
193
+ * run inside the SAME `BEGIN IMMEDIATE` transaction two concurrent processes
194
+ * cannot both hold at once — one commits, the other's driver-level conflict
195
+ * surfaces as `E_CONTENTION` and is retried by {@link executeWriteTransaction}
196
+ * (250ms/500ms backoff, 3 attempts, §4c), never a racing check-then-insert.
197
+ *
198
+ * Errors: `InvalidArgumentError` (`name`/`by` missing or blank),
199
+ * `WriteContentionError`/`WriteIOError` (§4c, an exhausted or unclassified
200
+ * driver-level failure on the underlying `immediate` transaction).
201
+ */
202
+ export declare function upsertProject(handle: IWriteStoreHandle, input: IUpsertProjectInput): Promise<IUpsertProjectOutcome>;
203
+ /**
204
+ * Transaction-PARTICIPANT core of {@link upsertProject} — the exact body
205
+ * that used to run inside its `executeWriteTransaction` callback, extracted
206
+ * so a caller that already owns an open `immediate` transaction (the ETL's
207
+ * `tools/etl/import-item.ts` bundles an entire source item's writes into ONE
208
+ * transaction, SPEC.md §7a) can participate in it instead of nesting a
209
+ * second `executeWriteTransaction` call — nesting would either deadlock or
210
+ * silently open two separate transactions where one atomic one was intended.
211
+ * `upsertProject` itself is now a thin wrapper: open the transaction, hand it
212
+ * to this function. Same validation contract as `upsertProject` — callers of
213
+ * THIS function are responsible for having already run `assertNonBlank`/
214
+ * `assertNotBareRoleLiteral` on `input.name`/`input.by` (this function does
215
+ * not re-validate, matching every other `*Tx` helper in this module).
216
+ */
217
+ export declare function upsertProjectTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertProjectInput): Promise<IUpsertProjectOutcome>;
218
+ export interface IUpsertComponentInput {
219
+ /** `project` reference — `uid` or `name` (§6.1's disambiguation-by-shape rule), resolved-ONLY, never minted. */
220
+ project: string;
221
+ /** The business key `component` is upserted by, scoped to `project` (§3a, §4: "upsert by `(project, name)`"). */
222
+ name: string;
223
+ path?: string;
224
+ description?: string;
225
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
226
+ by: string;
227
+ }
228
+ export interface IUpsertComponentOutcome {
229
+ uid: string;
230
+ /** `false` when an existing LIVE `component` row with this `(project, name)` was found and updated in place. */
231
+ created: boolean;
232
+ component: IComponentSummary;
233
+ }
234
+ /**
235
+ * Create-or-update a `component` by `(project, name)` (§3a, §4). `project`
236
+ * is resolved-ONLY ({@link resolveProjectTx}) — an unresolved `project`
237
+ * throws `CatalogNotFoundError` before any component-side work runs. One
238
+ * `immediate` transaction thereafter: find the LIVE `component` row scoped to
239
+ * the resolved project's `uid` — on a HIT, merge the supplied fields into the
240
+ * EXISTING `meta` (never a wholesale replace, same rule as
241
+ * {@link upsertProject}) and audit `'updated'`; on a MISS, mint the row.
242
+ * Either way, the `owns_project` edge (project → component, §3a's registry
243
+ * edge set) is written with the SAME `ON CONFLICT` upsert `writeEdgeTx`
244
+ * itself already performs — a no-op re-livening when the edge already exists,
245
+ * so a component whose edge somehow drifted from its `meta.projectUid` (it
246
+ * never should, both are written in the SAME transaction on creation) is
247
+ * self-healed rather than left stale.
248
+ *
249
+ * Errors: `InvalidArgumentError` (`project`/`name`/`by` missing or blank),
250
+ * `CatalogNotFoundError` (`project` does not resolve),
251
+ * `WriteContentionError`/`WriteIOError` (§4c).
252
+ */
253
+ export declare function upsertComponent(handle: IWriteStoreHandle, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
254
+ /**
255
+ * Transaction-PARTICIPANT core of {@link upsertComponent} — same extraction
256
+ * rationale as {@link upsertProjectTx}: lets a caller that already owns an
257
+ * open `immediate` transaction (the ETL) participate instead of nesting a
258
+ * second `executeWriteTransaction`. `upsertComponent` itself is now a thin
259
+ * wrapper: validate, open the transaction, hand it to this function. Callers
260
+ * of THIS function are responsible for having already run the same
261
+ * `assertNonBlank`/`assertNotBareRoleLiteral` validation `upsertComponent`
262
+ * runs before opening its transaction.
263
+ */
264
+ export declare function upsertComponentTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
265
+ export interface IUpsertLocationInput {
266
+ /**
267
+ * `component` reference — `uid` or `name` (§6.1's disambiguation-by-shape
268
+ * rule). A bare NAME is ambiguous on its own (component names are unique
269
+ * only WITHIN a project, §6.3.2), so a name reference REQUIRES `project`
270
+ * to scope it; a `uid` reference never needs `project` (a documented
271
+ * judgment call — SPEC.md §3a states the call convention
272
+ * `upsertLocation({component, locType, value})` without a `project` field,
273
+ * but never states how a bare component name resolves; this reading stays
274
+ * consistent with every OTHER verb's own `project`-scoped component
275
+ * resolution — §6.1, §6.3.2 — rather than inventing a global name-uniqueness
276
+ * scan `resolveComponentTx` itself does not perform).
277
+ */
278
+ component: string;
279
+ project?: string;
280
+ locType: ILocationType;
281
+ value: string;
282
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
283
+ by: string;
284
+ }
285
+ export interface IUpsertLocationOutcome {
286
+ uid: string;
287
+ /** `false` when a LIVE `location` row with this exact `(component, locType, value)` already existed — a genuine no-op: identity IS the row's entire content, so nothing is written or audited (§4a's "never disguised as a real write" rule, `move.ts`'s stated-no-op pattern). */
288
+ created: boolean;
289
+ location: ILocationSummary;
290
+ }
291
+ /**
292
+ * Create-or-find a `location` by `(component, locType, value)` (§3a, §4).
293
+ * `component` is resolved-ONLY — by `uid` directly, or by `name` scoped to
294
+ * `project` (see {@link IUpsertLocationInput.component}'s doc comment for the
295
+ * resolved ambiguity). One `immediate` transaction: find a LIVE `location`
296
+ * row with the exact triple — a HIT is a genuine no-op (the triple IS the
297
+ * row's whole identity; there is no fourth field left to merge, unlike
298
+ * `project`/`component`), so NOTHING is written or audited and `created:
299
+ * false` is returned; a MISS mints the row plus its `has_location` edge
300
+ * (component → location, §3a's registry edge set) and audits `'created'`.
301
+ *
302
+ * An invalidated (`rmLocation`-ed) row is never resurrected or matched here —
303
+ * the SELECT is `t_invalid IS NULL`-scoped like every other resolution in
304
+ * this module, so re-`upsertLocation`-ing the identical triple after an
305
+ * `rmLocation` mints a genuinely NEW row with a NEW `uid`, leaving the
306
+ * invalidated row's own audit trail untouched (the same bi-temporal
307
+ * non-resurrection rule `delete.ts` documents for `issue`).
308
+ *
309
+ * Errors: `InvalidArgumentError` (`component`/`value`/`by` missing or blank,
310
+ * `locType` not one of `path`/`url`/`tool`, or a bare `component` NAME given
311
+ * without `project`), `CatalogNotFoundError` (`project`/`component` does not
312
+ * resolve), `WriteContentionError`/`WriteIOError` (§4c).
313
+ */
314
+ export declare function upsertLocation(handle: IWriteStoreHandle, input: IUpsertLocationInput): Promise<IUpsertLocationOutcome>;
315
+ export interface IRmLocationInput {
316
+ uid: string;
317
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
318
+ by: string;
319
+ /** Optional explanation recorded on the invalidation's audit row and merged into the location's own `meta` (mirrors `delete.ts`'s `reason`, which SPEC.md §6.3.7 requires for `issue` — kept optional here since §3a/§4 state no such requirement for `location`). */
320
+ reason?: string;
321
+ }
322
+ export interface IRmLocationOutcome {
323
+ uid: string;
324
+ invalidated: true;
325
+ }
326
+ /**
327
+ * Soft-invalidate a LIVE `location` by `uid` (§3a, §4: "invalidate"; §4c's
328
+ * table: "invalidate-by-uid after confirming the row is live, for uniformity
329
+ * with every other verb above"). One `immediate` transaction: resolve `uid`
330
+ * → LIVE `location` node (`resolveByUidTx`, never a bare `getNodeByUid`, §4c)
331
+ * → hand-composed guarded `UPDATE node SET t_invalid = ?, meta = ? WHERE
332
+ * rowid = ?` (merging `invalidatedReason`/`invalidatedAt` into the EXISTING
333
+ * `meta`, never a wholesale replace — the SAME shape `delete.ts` uses for
334
+ * `issue`) → invalidate the owning `has_location` edge
335
+ * ({@link invalidateEdgeTx}, resolved via the location's own
336
+ * `meta.componentUid`) → `writeAudit` (action `'deleted'`, matching
337
+ * `delete.ts`'s own vocabulary), all against the SAME `tx`.
338
+ *
339
+ * No `issue` node ever references a `location` directly — per §3a's fixed
340
+ * edge table ({@link EDGE_KIND_TABLE}), `has_location` (component → location)
341
+ * is the ONLY rel touching `location` at all — so the sole referencing edge
342
+ * this invalidates is the owning component's own `has_location` edge; no
343
+ * issue-side fallout exists to reconcile.
344
+ *
345
+ * Errors: `InvalidArgumentError` (`uid`/`by` missing or blank),
346
+ * `CatalogNotFoundError` (no LIVE `location` node carries `uid` — including
347
+ * an ALREADY-`rmLocation`-ed `uid`, the same non-resurrection rule
348
+ * {@link IUpsertLocationOutcome}'s doc comment describes),
349
+ * `WriteContentionError`/`WriteIOError` (§4c).
350
+ */
351
+ export declare function rmLocation(handle: IWriteStoreHandle, input: IRmLocationInput): Promise<IRmLocationOutcome>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * claim-lease.ts — the ephemeral claim lease's read-side logic, shared by
3
+ * every write verb that needs to know whether a live claim currently blocks
4
+ * it. `claim.ts` (§6.3.5) is the only verb that MUTATES `claimedBy`/
5
+ * `claimedAt`; this module only ever reads and interprets that pair, so
6
+ * `transition.ts` (§6.3.5's own "a live claim by someone else blocks a
7
+ * status change" rule) can enforce the SAME staleness semantics without
8
+ * duplicating the age-math or silently drifting from it over time.
9
+ */
10
+ /** Pulls the raw `claimedBy`/`claimedAt` pair off an issue's metadata, typed and undefined-safe. */
11
+ export declare function extractClaimMeta(metadata: Record<string, unknown> | undefined): {
12
+ claimedBy: string | undefined;
13
+ claimedAt: string | undefined;
14
+ };
15
+ /**
16
+ * A missing `claimedAt` (should not happen alongside a real `claimedBy`, but
17
+ * never trusted) is treated as infinitely old — the same fail-open choice
18
+ * `claim.ts`'s own reclaim branch already makes.
19
+ */
20
+ export declare function claimAgeMinutes(claimedAt: string | undefined, now: string): number;
21
+ export declare function isClaimStale(claimedAt: string | undefined, now: string, staleAfterMin: number): boolean;
@@ -0,0 +1,80 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+
3
+ export interface IClaimInput {
4
+ /** The `issue` uid to claim/release/renew (§6.3, an "Issue verb"). */
5
+ uid: string;
6
+ /** The claimant — the acting agent or person. REQUIRED on every action (§6.3's opening rule). */
7
+ by: string;
8
+ action: 'claim' | 'release' | 'renew';
9
+ /** Human-confirmed override of a NON-stale claim (§6.2). Only consulted on `action:'claim'` when the current claimant differs and the lease is not yet stale. */
10
+ force?: boolean;
11
+ }
12
+ export interface IClaimOutcome {
13
+ uid: string;
14
+ status: 'claimed' | 'held' | 'reclaimed-stale' | 'renewed' | 'released' | 'release-noop';
15
+ claimedBy?: string;
16
+ claimedAt?: string;
17
+ heldBy?: string;
18
+ heldSince?: string;
19
+ previousClaimant?: string;
20
+ wasClaimedBy?: string;
21
+ }
22
+ /**
23
+ * `claim` / `release` / `renew` an issue's lease (§6.3.5). One `immediate`
24
+ * transaction; the CAS decision and the metadata touch both run against the
25
+ * SAME `tx` the transaction opened, so two concurrent callers against the
26
+ * SAME `uid` serialize through the transaction's RESERVED lock — the loser's
27
+ * own re-fetch (inside its own attempt, after the winner commits) sees the
28
+ * winner's already-written `claimedBy`/`claimedAt`, never a stale
29
+ * pre-transaction read.
30
+ *
31
+ * Rule table (SPEC.md §6.3.5, restated here for the metadata-only shape the
32
+ * lease now lives in):
33
+ *
34
+ * | action | current `claimedBy` | outcome |
35
+ * |-----------|-------------------------------------------------------|---------|
36
+ * | `claim` | unset | write `{claimedBy, claimedAt}`; `status:'claimed'` |
37
+ * | `claim` | `== by` | no-op write of `claimedAt`; `status:'held'` |
38
+ * | `claim` | `!= by`, age < `claim_stale_after_min`, `!force` | throws `ClaimHeldError` |
39
+ * | `claim` | `!= by`, age ≥ threshold, OR `force:true` | write `{claimedBy, claimedAt}`; `status:'reclaimed-stale'`, `previousClaimant` |
40
+ * | `release` | `== by` | clear both fields; `status:'released'` |
41
+ * | `release` | `!= by` or unset | no write; `status:'release-noop'`, `wasClaimedBy` |
42
+ * | `renew` | `== by` | bump `claimedAt`; `status:'renewed'` |
43
+ * | `renew` | `!= by` or unset | throws `ClaimHeldError` |
44
+ *
45
+ * **Audit.** Every branch that performs a real write emits exactly one
46
+ * `audit` node via {@link writeAudit}, inside the SAME transaction (§4a "no
47
+ * bare mutation"). `writeAudit`'s own doc comment enumerates the audit
48
+ * `action` vocabulary as `'claimed'`/`'released'`/`'renewed'`/
49
+ * `'reclaimed-stale'` — four values, not five — so the `claim`-when-`== by`
50
+ * ("held", an idempotent re-affirmation of an already-owned claim) branch
51
+ * reuses `action:'claimed'`; its `IClaimOutcome.status` is still the distinct
52
+ * `'held'` value the rule table names, carried in the OUTCOME, not the audit
53
+ * action string. The two branches the rule table marks "no write"
54
+ * (`release-noop`) perform no touch and emit no audit — nothing changed, so
55
+ * §4a's "every STATE CHANGE writes an audit node" does not apply. This
56
+ * reading resolves a genuine tension in SPEC.md §6.3.5 (see this package's
57
+ * write-verb report for the full citation) between "every branch writes an
58
+ * audit node" and the rule table's own "no write" / four-action vocabulary;
59
+ * it is the only reading consistent with `writeAudit`'s own doc comment.
60
+ *
61
+ * **`renew` against an unclaimed issue.** The rule table assigns this to the
62
+ * SAME `ClaimHeldError` outcome as "held by someone else" — but there is no
63
+ * real holder to report. SPEC.md does not resolve this case explicitly; this
64
+ * implementation reports `ClaimHeldError('', '')` (empty-string sentinels,
65
+ * never a fabricated agent name or timestamp) so the thrown error's shape is
66
+ * still exactly `ClaimHeldError` per the rule table, without inventing data
67
+ * the graph does not have.
68
+ *
69
+ * Errors: `InvalidArgumentError` (`uid`/`by` missing/blank, or `action`
70
+ * outside the closed `'claim'|'release'|'renew'` set — the TS type restricts
71
+ * this at compile time but a transport boundary, e.g. CLI/MCP JSON input, can
72
+ * still send an arbitrary string), `IssueNotFoundError` (no live `issue` node
73
+ * carries `uid`), `ClaimHeldError` (per the rule table above),
74
+ * `IssueTerminalError` (`action:'claim'` only — the issue's current status
75
+ * is `terminal`; `release`/`renew` are exempt, since cleanup on an issue
76
+ * that got closed out from under the claimant must still succeed),
77
+ * `WriteContentionError`/`WriteIOError` (§4c — an exhausted driver-level
78
+ * retry on the underlying `immediate` transaction).
79
+ */
80
+ export declare function claim(handle: IWriteStoreHandle, input: IClaimInput): Promise<IClaimOutcome>;