@adhd/backlog 0.1.9 → 1.0.1

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 (65) hide show
  1. package/CHANGELOG.md +128 -47
  2. package/README.md +369 -81
  3. package/api.d.ts +196 -0
  4. package/api.ir.json +1 -0
  5. package/cli.d.ts +45 -18
  6. package/env.d.ts +23 -3
  7. package/envelope.d.ts +163 -0
  8. package/extract-live.d.ts +56 -0
  9. package/index.d.ts +11 -10
  10. package/index.js +255 -197
  11. package/index.mjs +11348 -19043
  12. package/install-skill.d.ts +23 -0
  13. package/ir-artifact.d.ts +86 -0
  14. package/package.json +51 -15
  15. package/query/card.d.ts +31 -0
  16. package/query/get.d.ts +11 -0
  17. package/query/index.d.ts +67 -0
  18. package/query/markdown.d.ts +11 -0
  19. package/query/query.d.ts +131 -0
  20. package/query/resolve.d.ts +123 -0
  21. package/query/types.d.ts +450 -0
  22. package/query/views/registry.d.ts +43 -0
  23. package/query/views/semantic.d.ts +101 -0
  24. package/query/views/stats.d.ts +109 -0
  25. package/search-shortcut.d.ts +79 -0
  26. package/serve.d.ts +18 -0
  27. package/server.d.ts +139 -4
  28. package/skill/SKILL.md +632 -138
  29. package/store/graph-backlog-store.d.ts +80 -17
  30. package/store/immediate-retry.d.ts +24 -13
  31. package/store/type-policy.d.ts +4 -0
  32. package/store/vocabulary-guard.d.ts +52 -0
  33. package/write/audit.d.ts +36 -0
  34. package/write/bootstrap.d.ts +161 -0
  35. package/write/catalog.d.ts +369 -0
  36. package/write/citation-path.d.ts +133 -0
  37. package/write/claim-lease.d.ts +21 -0
  38. package/write/claim.d.ts +80 -0
  39. package/write/create-issue.d.ts +253 -0
  40. package/write/delete.d.ts +39 -0
  41. package/write/embed-drain.d.ts +68 -0
  42. package/write/embedding-config.d.ts +81 -0
  43. package/write/embedding-observer.d.ts +80 -0
  44. package/write/errors.d.ts +365 -0
  45. package/write/issue-status.d.ts +10 -0
  46. package/write/move.d.ts +70 -0
  47. package/write/relate.d.ts +52 -0
  48. package/write/transition.d.ts +64 -0
  49. package/write/tx.d.ts +344 -0
  50. package/write/update.d.ts +81 -0
  51. package/client.d.ts +0 -174
  52. package/markdown.d.ts +0 -75
  53. package/migration-admin.d.ts +0 -26
  54. package/model.d.ts +0 -437
  55. package/store/audit-log.d.ts +0 -16
  56. package/store/claim.d.ts +0 -24
  57. package/store/crud.d.ts +0 -62
  58. package/store/ids.d.ts +0 -24
  59. package/store/lifecycle.d.ts +0 -36
  60. package/store/mapping.d.ts +0 -101
  61. package/store/mutate-metadata.d.ts +0 -8
  62. package/store/query.d.ts +0 -68
  63. package/store/repo-migration.d.ts +0 -51
  64. package/store/serve-lock.d.ts +0 -42
  65. package/store/structure.d.ts +0 -66
@@ -0,0 +1,369 @@
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
+ /**
125
+ * External filesystem roots (absolute paths) this project may cite from, in
126
+ * ADDITION to its own `metadata.path` root (BUG c6d35272). A citation target
127
+ * is accepted iff its CANONICAL (symlink-resolved) path lies within the
128
+ * project root OR one of these roots (`citation-path.ts`'s
129
+ * `resolveCitationTarget`); a `..` traversal or a symlink that escapes stays
130
+ * rejected, and an arbitrary absolute path outside every root is never
131
+ * readable. Only the resulting `sha` is persisted — never file content.
132
+ *
133
+ * This layer defaults to the EMPTY array, and {@link resolveProjectPolicy}'s
134
+ * injected runtime default (`defaultCitationAllowedExternalRoots()`) is ALSO
135
+ * empty — there is NO machine-global default root (BUG 62059b57 follow-up:
136
+ * the store's `~/.adhd/backlog` data home is not citable). A project opts
137
+ * into the external carve-out by naming roots here; an empty array (the
138
+ * default, or an explicit `[]`) leaves it disabled. This is TYPED,
139
+ * per-project config — deliberately never an environment toggle.
140
+ */
141
+ readonly citationAllowedExternalRoots: readonly string[];
142
+ readonly defaultStatus?: string;
143
+ readonly defaultKind?: string;
144
+ readonly dedupeScanEnabled: boolean;
145
+ readonly dedupeThreshold: number;
146
+ readonly claimStaleAfterMin: number;
147
+ /** `project_status` — empty means "no restriction" (§2). */
148
+ readonly allowedStatuses: readonly string[];
149
+ /** `project_kind` — empty means "no restriction" (§2). */
150
+ readonly allowedKinds: readonly string[];
151
+ /** `project_field_requirement` — field names required on every mutating write for this project (§2). */
152
+ readonly requiredFields: readonly string[];
153
+ }
154
+ export declare function resolveProjectPolicy(project: IResolvedProjectRow): IProjectPolicy;
155
+ /**
156
+ * Whether a resolved project has a known filesystem `path` (§8.4/§8.5) — the
157
+ * precondition for citation content-addressing to be POSSIBLE at all. When it
158
+ * does not, `computeCitationSha` can only ever return the `"unverified"`
159
+ * sentinel (`create-issue.ts`/`transition.ts` both short-circuit on a
160
+ * non-string/empty `metadata.path`), so `project_policy.citationRequiresSha`
161
+ * has nothing to gate: the gate applies only where verification is possible.
162
+ * A path-less project therefore records `sha:"unverified"` verbatim, matching
163
+ * the ETL's own precedent (`tools/etl/citation.ts`). Kept beside
164
+ * {@link resolveProjectPolicy} — it reads the same resolved project row. The
165
+ * PREDICATE itself is shared by BOTH write paths so the `create`/`transition`
166
+ * gates never drift on WHICH projects are verifiable; the surrounding
167
+ * `computeCitationSha` hashing body is NOT shared — it stays a per-file copy
168
+ * in each verb (this package's established per-file-duplication convention —
169
+ * see `transition.ts`'s own doc comment on its copy).
170
+ *
171
+ * A TYPE PREDICATE, not a bare `boolean`: both `computeCitationSha` call sites
172
+ * (and the waiver log beside each gate) need the narrowed `metadata.path` as a
173
+ * `string` immediately after the guard, and a predicate is the one form that
174
+ * gives them that without a non-null assertion or a re-read of the same field.
175
+ */
176
+ export declare function projectHasKnownPath(project: IResolvedProjectRow): project is IResolvedProjectRow & {
177
+ metadata: Record<string, unknown> & {
178
+ path: string;
179
+ };
180
+ };
181
+ export interface IUpsertProjectInput {
182
+ /** The business key `project` is upserted by (§3a, §4). */
183
+ name: string;
184
+ path?: string;
185
+ repoUrl?: string;
186
+ monorepo?: boolean;
187
+ description?: string;
188
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
189
+ by: string;
190
+ }
191
+ export interface IUpsertProjectOutcome {
192
+ uid: string;
193
+ /** `false` when an existing LIVE `project` row with this `name` was found and updated in place; `true` only on a genuine first mint. */
194
+ created: boolean;
195
+ project: IProjectSummary;
196
+ }
197
+ /**
198
+ * Create-or-update a `project` by `name` (§3a, §4). One `immediate`
199
+ * transaction: find the LIVE `project` row by `name` — on a HIT, merge the
200
+ * supplied fields into the EXISTING `meta` blob (never a wholesale replace;
201
+ * an existing `meta.policy` — §2's per-project policy, read back by
202
+ * {@link resolveProjectPolicy} — survives every subsequent `upsertProject`
203
+ * untouched) and audit `'updated'`; on a MISS, mint the `project` row AND,
204
+ * "on first creation ONLY," the reserved default `component` row named
205
+ * `(root)` plus its `owns_project` edge, in the SAME transaction (§4) — audit
206
+ * `'created'`. A repeat `upsertProject` against an existing project finds
207
+ * `(root)` already live (via the existing-project branch, which never touches
208
+ * it) and writes nothing further for it: idempotent, never a second row.
209
+ *
210
+ * Race-safety (§4c): the find-then-create SELECT and the INSERT/UPDATE both
211
+ * run inside the SAME `BEGIN IMMEDIATE` transaction two concurrent processes
212
+ * cannot both hold at once — one commits, the other's driver-level conflict
213
+ * surfaces as `E_CONTENTION` and is retried by {@link executeWriteTransaction}
214
+ * (250ms/500ms backoff, 3 attempts, §4c), never a racing check-then-insert.
215
+ *
216
+ * Errors: `InvalidArgumentError` (`name`/`by` missing or blank),
217
+ * `WriteContentionError`/`WriteIOError` (§4c, an exhausted or unclassified
218
+ * driver-level failure on the underlying `immediate` transaction).
219
+ */
220
+ export declare function upsertProject(handle: IWriteStoreHandle, input: IUpsertProjectInput): Promise<IUpsertProjectOutcome>;
221
+ /**
222
+ * Transaction-PARTICIPANT core of {@link upsertProject} — the exact body
223
+ * that used to run inside its `executeWriteTransaction` callback, extracted
224
+ * so a caller that already owns an open `immediate` transaction (the ETL's
225
+ * `tools/etl/import-item.ts` bundles an entire source item's writes into ONE
226
+ * transaction, SPEC.md §7a) can participate in it instead of nesting a
227
+ * second `executeWriteTransaction` call — nesting would either deadlock or
228
+ * silently open two separate transactions where one atomic one was intended.
229
+ * `upsertProject` itself is now a thin wrapper: open the transaction, hand it
230
+ * to this function. Same validation contract as `upsertProject` — callers of
231
+ * THIS function are responsible for having already run `assertNonBlank`/
232
+ * `assertNotBareRoleLiteral` on `input.name`/`input.by` (this function does
233
+ * not re-validate, matching every other `*Tx` helper in this module).
234
+ */
235
+ export declare function upsertProjectTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertProjectInput): Promise<IUpsertProjectOutcome>;
236
+ export interface IUpsertComponentInput {
237
+ /** `project` reference — `uid` or `name` (§6.1's disambiguation-by-shape rule), resolved-ONLY, never minted. */
238
+ project: string;
239
+ /** The business key `component` is upserted by, scoped to `project` (§3a, §4: "upsert by `(project, name)`"). */
240
+ name: string;
241
+ path?: string;
242
+ description?: string;
243
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
244
+ by: string;
245
+ }
246
+ export interface IUpsertComponentOutcome {
247
+ uid: string;
248
+ /** `false` when an existing LIVE `component` row with this `(project, name)` was found and updated in place. */
249
+ created: boolean;
250
+ component: IComponentSummary;
251
+ }
252
+ /**
253
+ * Create-or-update a `component` by `(project, name)` (§3a, §4). `project`
254
+ * is resolved-ONLY ({@link resolveProjectTx}) — an unresolved `project`
255
+ * throws `CatalogNotFoundError` before any component-side work runs. One
256
+ * `immediate` transaction thereafter: find the LIVE `component` row scoped to
257
+ * the resolved project's `uid` — on a HIT, merge the supplied fields into the
258
+ * EXISTING `meta` (never a wholesale replace, same rule as
259
+ * {@link upsertProject}) and audit `'updated'`; on a MISS, mint the row.
260
+ * Either way, the `owns_project` edge (project → component, §3a's registry
261
+ * edge set) is written with the SAME `ON CONFLICT` upsert `writeEdgeTx`
262
+ * itself already performs — a no-op re-livening when the edge already exists,
263
+ * so a component whose edge somehow drifted from its `meta.projectUid` (it
264
+ * never should, both are written in the SAME transaction on creation) is
265
+ * self-healed rather than left stale.
266
+ *
267
+ * Errors: `InvalidArgumentError` (`project`/`name`/`by` missing or blank),
268
+ * `CatalogNotFoundError` (`project` does not resolve),
269
+ * `WriteContentionError`/`WriteIOError` (§4c).
270
+ */
271
+ export declare function upsertComponent(handle: IWriteStoreHandle, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
272
+ /**
273
+ * Transaction-PARTICIPANT core of {@link upsertComponent} — same extraction
274
+ * rationale as {@link upsertProjectTx}: lets a caller that already owns an
275
+ * open `immediate` transaction (the ETL) participate instead of nesting a
276
+ * second `executeWriteTransaction`. `upsertComponent` itself is now a thin
277
+ * wrapper: validate, open the transaction, hand it to this function. Callers
278
+ * of THIS function are responsible for having already run the same
279
+ * `assertNonBlank`/`assertNotBareRoleLiteral` validation `upsertComponent`
280
+ * runs before opening its transaction.
281
+ */
282
+ export declare function upsertComponentTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
283
+ export interface IUpsertLocationInput {
284
+ /**
285
+ * `component` reference — `uid` or `name` (§6.1's disambiguation-by-shape
286
+ * rule). A bare NAME is ambiguous on its own (component names are unique
287
+ * only WITHIN a project, §6.3.2), so a name reference REQUIRES `project`
288
+ * to scope it; a `uid` reference never needs `project` (a documented
289
+ * judgment call — SPEC.md §3a states the call convention
290
+ * `upsertLocation({component, locType, value})` without a `project` field,
291
+ * but never states how a bare component name resolves; this reading stays
292
+ * consistent with every OTHER verb's own `project`-scoped component
293
+ * resolution — §6.1, §6.3.2 — rather than inventing a global name-uniqueness
294
+ * scan `resolveComponentTx` itself does not perform).
295
+ */
296
+ component: string;
297
+ project?: string;
298
+ locType: ILocationType;
299
+ value: string;
300
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
301
+ by: string;
302
+ }
303
+ export interface IUpsertLocationOutcome {
304
+ uid: string;
305
+ /** `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). */
306
+ created: boolean;
307
+ location: ILocationSummary;
308
+ }
309
+ /**
310
+ * Create-or-find a `location` by `(component, locType, value)` (§3a, §4).
311
+ * `component` is resolved-ONLY — by `uid` directly, or by `name` scoped to
312
+ * `project` (see {@link IUpsertLocationInput.component}'s doc comment for the
313
+ * resolved ambiguity). One `immediate` transaction: find a LIVE `location`
314
+ * row with the exact triple — a HIT is a genuine no-op (the triple IS the
315
+ * row's whole identity; there is no fourth field left to merge, unlike
316
+ * `project`/`component`), so NOTHING is written or audited and `created:
317
+ * false` is returned; a MISS mints the row plus its `has_location` edge
318
+ * (component → location, §3a's registry edge set) and audits `'created'`.
319
+ *
320
+ * An invalidated (`rmLocation`-ed) row is never resurrected or matched here —
321
+ * the SELECT is `t_invalid IS NULL`-scoped like every other resolution in
322
+ * this module, so re-`upsertLocation`-ing the identical triple after an
323
+ * `rmLocation` mints a genuinely NEW row with a NEW `uid`, leaving the
324
+ * invalidated row's own audit trail untouched (the same bi-temporal
325
+ * non-resurrection rule `delete.ts` documents for `issue`).
326
+ *
327
+ * Errors: `InvalidArgumentError` (`component`/`value`/`by` missing or blank,
328
+ * `locType` not one of `path`/`url`/`tool`, or a bare `component` NAME given
329
+ * without `project`), `CatalogNotFoundError` (`project`/`component` does not
330
+ * resolve), `WriteContentionError`/`WriteIOError` (§4c).
331
+ */
332
+ export declare function upsertLocation(handle: IWriteStoreHandle, input: IUpsertLocationInput): Promise<IUpsertLocationOutcome>;
333
+ export interface IRmLocationInput {
334
+ uid: string;
335
+ /** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
336
+ by: string;
337
+ /** 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`). */
338
+ reason?: string;
339
+ }
340
+ export interface IRmLocationOutcome {
341
+ uid: string;
342
+ invalidated: true;
343
+ }
344
+ /**
345
+ * Soft-invalidate a LIVE `location` by `uid` (§3a, §4: "invalidate"; §4c's
346
+ * table: "invalidate-by-uid after confirming the row is live, for uniformity
347
+ * with every other verb above"). One `immediate` transaction: resolve `uid`
348
+ * → LIVE `location` node (`resolveByUidTx`, never a bare `getNodeByUid`, §4c)
349
+ * → hand-composed guarded `UPDATE node SET t_invalid = ?, meta = ? WHERE
350
+ * rowid = ?` (merging `invalidatedReason`/`invalidatedAt` into the EXISTING
351
+ * `meta`, never a wholesale replace — the SAME shape `delete.ts` uses for
352
+ * `issue`) → invalidate the owning `has_location` edge
353
+ * ({@link invalidateEdgeTx}, resolved via the location's own
354
+ * `meta.componentUid`) → `writeAudit` (action `'deleted'`, matching
355
+ * `delete.ts`'s own vocabulary), all against the SAME `tx`.
356
+ *
357
+ * No `issue` node ever references a `location` directly — per §3a's fixed
358
+ * edge table ({@link EDGE_KIND_TABLE}), `has_location` (component → location)
359
+ * is the ONLY rel touching `location` at all — so the sole referencing edge
360
+ * this invalidates is the owning component's own `has_location` edge; no
361
+ * issue-side fallout exists to reconcile.
362
+ *
363
+ * Errors: `InvalidArgumentError` (`uid`/`by` missing or blank),
364
+ * `CatalogNotFoundError` (no LIVE `location` node carries `uid` — including
365
+ * an ALREADY-`rmLocation`-ed `uid`, the same non-resurrection rule
366
+ * {@link IUpsertLocationOutcome}'s doc comment describes),
367
+ * `WriteContentionError`/`WriteIOError` (§4c).
368
+ */
369
+ export declare function rmLocation(handle: IWriteStoreHandle, input: IRmLocationInput): Promise<IRmLocationOutcome>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * citation-path.ts — the citation-path containment contract (BUG c6d35272).
3
+ *
4
+ * A citation's `sha` hashes EXTERNAL file content (§6.3.2/§8.5). By default a
5
+ * citation target must resolve INSIDE the owning project's own root
6
+ * (`project.metadata.path`): that keeps the read surface confined to the tree
7
+ * the project already owns, so a citation can never be used as a
8
+ * file-exists/readable oracle for an arbitrary absolute path elsewhere on the
9
+ * host. But some genuine evidence lives OUTSIDE that root — a machine-level
10
+ * tool (`~/.local/bin/gx`), a globally-installed package's `dist/**`, a
11
+ * `/tmp` scratch artifact, or any root the project explicitly allowlists via
12
+ * `citationAllowedExternalRoots`. Before this module the ONLY way to cite such
13
+ * evidence on a path-PRESENT project was to omit the citation entirely —
14
+ * which `create`
15
+ * still reported as SUCCESS, silently downgrading a labelled-unverified
16
+ * citation to an unlabelled absence (exactly what "No citation, no claim"
17
+ * forbids).
18
+ *
19
+ * The carve-out is a TYPED, per-project ALLOWLIST — `project_policy.
20
+ * citationAllowedExternalRoots` (see `catalog.ts`'s `IProjectPolicy`) — never
21
+ * an env toggle and never a blanket "any absolute path" escape. A citation is
22
+ * accepted iff its CANONICAL (symlink-resolved) target lies within the project
23
+ * root OR within one of the project's allowed external roots. Every allowed
24
+ * root is itself canonicalized before comparison, so:
25
+ *
26
+ * - a relative `../…` traversal cannot widen the surface (it resolves to an
27
+ * absolute candidate that is then containment-checked);
28
+ * - a symlink INSIDE the root that points OUTSIDE it is rejected (the
29
+ * canonical target is outside every root) — this is the same defect the
30
+ * sibling item c6d90ddf names, closed here for free by canonicalizing;
31
+ * - a root that is itself reached through a symlink still matches, because
32
+ * both the candidate and the root are canonicalized through the SAME
33
+ * `realpath` pass.
34
+ *
35
+ * Nothing here reads file CONTENT — only paths are resolved and compared.
36
+ * Only the resulting sha is ever persisted (§8.5); the citation node stores
37
+ * `target`/`sha`, never the bytes.
38
+ */
39
+ /**
40
+ * The default external roots a project may cite from, used when the project's
41
+ * own policy supplies no `citationAllowedExternalRoots`. This is the EMPTY
42
+ * array: the runtime grants NO machine-global default root.
43
+ *
44
+ * Why: the runtime's own data home, `~/.adhd/backlog`, is the STORE's home,
45
+ * not an evidence tree. It holds only the machine-global backlog database
46
+ * (`production/data/backlog-v2.db`) and its snapshots (`production/backup-*`,
47
+ * top-level `backup-*`/`backups/`), plus the `test/` store. Granting it as a
48
+ * default root let a citation's `sha` read/hash surface resolve straight INTO
49
+ * the shared backlog graph — the very graph the citation containment exists to
50
+ * keep out of citation reads. The earlier narrowing from `~/.adhd` down to
51
+ * `~/.adhd/backlog` (BUG 62059b57) stopped one directory too high (BUG
52
+ * 62059b57 follow-up).
53
+ *
54
+ * This does NOT gut the carve-out MECHANISM: a project that genuinely needs a
55
+ * specific external root (a machine tool, a globally-installed package's
56
+ * `dist/**`) names it explicitly in `project_policy.citationAllowedExternalRoots`,
57
+ * and that typed, per-project allowlist is unchanged. Only the unearned
58
+ * machine-global default is gone.
59
+ *
60
+ * Deliberately a FUNCTION, called LAZILY by {@link resolveProjectPolicy} on
61
+ * every policy resolve, never a module-level constant: the call surface stays
62
+ * stable if a legitimate machine-global root is ever re-introduced, and every
63
+ * caller keeps the lazy, per-resolve contract.
64
+ */
65
+ export declare function defaultCitationAllowedExternalRoots(): string[];
66
+ /**
67
+ * Render `root` for a human-facing error message: `~`-anchored when it lies
68
+ * under the current home directory, the bare absolute path otherwise. Keeps
69
+ * the `CitationUnverifiableError` message stable across machines (a literal
70
+ * `/Users/<name>/.adhd` would leak the operator's identity and differ per
71
+ * host, making the message hard to assert on).
72
+ */
73
+ export declare function displayExternalRoot(root: string): string;
74
+ /**
75
+ * Lexical containment: does `candidate` sit at or under `root`?
76
+ *
77
+ * Uses `path.relative` — NEVER a bare `startsWith(root)`, which a sibling
78
+ * directory sharing a name prefix would defeat (`/repo` vs `/repo-evil`:
79
+ * `'/repo-evil/x'.startsWith('/repo')` is `true`). Both arguments are expected
80
+ * to be ABSOLUTE and (ideally) canonical already; this function is purely
81
+ * lexical and performs no I/O.
82
+ */
83
+ export declare function isPathWithin(root: string, candidate: string): boolean;
84
+ /**
85
+ * The §4c "the file genuinely is not there" error taxonomy, in ONE place.
86
+ *
87
+ * `ENOENT` (a path segment does not exist) and `ENOTDIR` (a path segment that
88
+ * should be a directory is in fact a file, so the target cannot exist) both
89
+ * mean exactly "not there" — the ONLY case any caller may degrade to the
90
+ * `'unverified'` sentinel. Every other errno (`EACCES`, `EPERM`, `EMFILE`,
91
+ * `EISDIR`, `ELOOP`, …) is a REAL I/O failure and must never be masked as an
92
+ * absent file. Shared by {@link canonicalizePath} and both write verbs'
93
+ * `computeCitationSha` (BUG c6d35272 follow-up) so that taxonomy is
94
+ * single-source and cannot drift between its three call sites.
95
+ *
96
+ * NOT used by `tools/etl/citation.ts`: that frozen tool deliberately ALSO
97
+ * exempts `EISDIR` (its own documented, corpus-driven divergence), so folding
98
+ * it into this narrower ENOENT/ENOTDIR predicate would regress it.
99
+ */
100
+ export declare function isMissingPathError(err: unknown): boolean;
101
+ /**
102
+ * Resolve `p` to its real, symlink-free absolute path.
103
+ *
104
+ * `p` need not exist. On `ENOENT`/`ENOTDIR` the nearest EXISTING ancestor is
105
+ * canonicalized and the non-existent tail is re-joined onto it, so a citation
106
+ * naming a file that does not exist still yields a stable absolute candidate
107
+ * for the containment check — the "does it actually exist / is it readable?"
108
+ * verdict is deliberately left to the caller's `readFile` (which the write
109
+ * layer's error taxonomy degrades to the `'unverified'` sentinel on
110
+ * ENOENT/ENOTDIR, `WriteIOError` otherwise). Any OTHER error (EACCES, ELOOP,
111
+ * …) propagates untouched — a real I/O fault must never be masked as a
112
+ * missing file.
113
+ */
114
+ export declare function canonicalizePath(p: string): Promise<string>;
115
+ /** The outcome of {@link resolveCitationTarget}. */
116
+ export interface IResolvedCitationTarget {
117
+ /** `true` iff the canonical candidate lies within the project root or one of `allowedRoots`. */
118
+ accepted: boolean;
119
+ /** The CANONICAL (symlink-resolved) absolute path — the exact path the caller should read. Returned even when `accepted` is `false`, for a caller that wants to report it. */
120
+ candidate: string;
121
+ }
122
+ /**
123
+ * Decide whether `file` (absolute, or relative to `projectRoot`) is a
124
+ * citable target: canonical containment against `projectRoot` ∪
125
+ * `allowedRoots`. See this module's header for the security model.
126
+ *
127
+ * `projectRoot` and every entry of `allowedRoots` are canonicalized through
128
+ * the SAME pass as the candidate, so a root reached via a symlink still
129
+ * matches its own contents (and a root that does not exist is canonicalized
130
+ * to its nearest existing ancestor + the literal tail, exactly like the
131
+ * candidate).
132
+ */
133
+ export declare function resolveCitationTarget(projectRoot: string, file: string, allowedRoots: readonly string[]): Promise<IResolvedCitationTarget>;
@@ -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>;