@adhd/backlog 0.0.1 → 0.0.2

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.
@@ -0,0 +1,28 @@
1
+ export type SkillHost = 'claude' | 'codex' | 'opencode';
2
+ export type SkillScope = 'user' | 'project';
3
+ export interface InstallSkillResult {
4
+ host: SkillHost;
5
+ scope: SkillScope;
6
+ path: string;
7
+ }
8
+ /**
9
+ * Copies the packaged `SKILL.md` into every requested host's skill
10
+ * directory, under a `backlog/` subdirectory (matching the `memory-usage`
11
+ * precedent's per-skill-named-directory shape). Also drops a thin
12
+ * `extension.json` (`{ name, version, type: "skill", entrypoint: "SKILL.md"
13
+ * }`) alongside it, matching that same precedent's shape for hosts that
14
+ * consume it — additive, never required for Claude Code's own
15
+ * description-based auto-surfacing to work.
16
+ *
17
+ * `homeOverride` is a TEST-ISOLATION ESCAPE HATCH ONLY (mirrors
18
+ * `BuildBacklogEnvOptions.adhdRoot`/`BacklogCtx.adhdRoot` elsewhere in this
19
+ * package) — never passed by `runInstallSkillCommand`/the real CLI, which
20
+ * always resolves the genuine machine home directory for `--scope user`.
21
+ */
22
+ export declare function installSkill(argv: string[], cwd?: string, homeOverride?: string): InstallSkillResult[];
23
+ /** CLI entry — parses argv, runs the install, prints the same
24
+ * `console.log(JSON.stringify(...))` shape every other CLI command uses
25
+ * (BUG-APIGEN-015 parity), so scripting `backlog install-skill` composes
26
+ * with the rest of the CLI's output convention despite bypassing apigen
27
+ * dispatch entirely. */
28
+ export declare function runInstallSkillCommand(argv: string[]): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { BacklogFilter, BacklogItem, BacklogStatus, Priority } from './model.js';
1
+ import { BacklogFilter, BacklogItem, BacklogStatus, MalformedHeaderInfo, Priority } from './model.js';
2
2
 
3
3
  declare const LEGACY_TERMINAL_DONE: string[];
4
4
  declare const LEGACY_TERMINAL_WORKAROUND: string[];
@@ -36,8 +36,19 @@ export interface ParsedMarkdownItem {
36
36
  priority: string;
37
37
  body: string;
38
38
  }
39
+ export interface ParseWithDiagnosticsResult {
40
+ items: ParsedMarkdownItem[];
41
+ malformedHeaders: MalformedHeaderInfo[];
42
+ }
39
43
  /** Ported (structure preserved) from tools/util/backlog.mjs:106-155 (`parse`). */
40
44
  export declare function parseBacklogMarkdown(text: string): ParsedMarkdownItem[];
45
+ /**
46
+ * Same parse as {@link parseBacklogMarkdown}, plus `malformedHeaders` — every
47
+ * `##`/`###` line that looks like a corrupted/typo'd id header and was
48
+ * dropped instead of parsed into an item (DEBT-BACKLOG-IMPORT-SILENT-DROP-001).
49
+ * `importFromMarkdown` (client.ts) uses this to populate `ImportResult.malformedHeaders`.
50
+ */
51
+ export declare function parseBacklogMarkdownWithDiagnostics(text: string): ParseWithDiagnosticsResult;
41
52
  /**
42
53
  * SPEC.md §5.6 `renderToMarkdown` — one `###` block per item. Archived-item
43
54
  * exclusion (SPEC.md §5.4 `archiveResolved`'s "renderToMarkdown's default
@@ -0,0 +1,26 @@
1
+ import { MigrationPhase } from './model.js';
2
+ import { BacklogConfig } from './env.js';
3
+ import { Environment } from '@adhd/environment';
4
+
5
+ /**
6
+ * Mirrors `@adhd/environment-builder`'s own internal `roots.ts` global-root
7
+ * formula (`~/.<orgNamespace>/<project>/<namespace>/config.yaml`) using ONLY
8
+ * the public fields the `@adhd/environment` `Environment` instance exposes
9
+ * (`orgNamespace`/`project`/`namespace`) — `@adhd/backlog` does not depend on
10
+ * the internal `environment-builder` package directly, so this is a
11
+ * deliberate, narrow re-derivation, not an import of a private module.
12
+ * `adhdRootOverride` mirrors `EnvironmentOptions.adhdRoot`'s own test-isolation
13
+ * escape hatch (see `BuildBacklogEnvOptions`) for tests that must never touch
14
+ * the real machine-global `~/.adhd`.
15
+ */
16
+ export declare function globalConfigPath(env: Environment<BacklogConfig>, adhdRootOverride?: string): string;
17
+ /**
18
+ * Reads the existing global `config.yaml` (if any), deep-merges in
19
+ * `migration.phase`, and writes it back — preserving every OTHER key already
20
+ * in the file (e.g. a previously-set `db.busyTimeoutMs` override) rather than
21
+ * clobbering the whole file. A missing or malformed existing file is treated
22
+ * as empty (never fatal for a write), mirroring
23
+ * `@adhd/environment-builder`'s own `readLayerFile` tolerance for a corrupt
24
+ * layer. Returns the absolute path written, for caller confirmation.
25
+ */
26
+ export declare function writeMigrationPhase(env: Environment<BacklogConfig>, phase: MigrationPhase, adhdRootOverride?: string): string;
@@ -48,6 +48,8 @@ export interface BacklogItem {
48
48
  projectPath?: string;
49
49
  /** Plan slug this item is attached to, if any — e.g. "agent-registry-schema". */
50
50
  plan?: string;
51
+ /** Source markdown path this item was imported from, if any (DEBT-BACKLOG-IMPORT-PLAN-PROVENANCE-001). */
52
+ importedFrom?: string;
51
53
  /** Durable ownership — who this item is assigned to (may differ from the active claimant). */
52
54
  assignee?: string;
53
55
  /** Ephemeral claim lease — see SPEC.md §5. */
@@ -92,6 +94,8 @@ export interface CreateItemInput {
92
94
  priority?: Priority;
93
95
  tags?: string[];
94
96
  plan?: string;
97
+ /** Source markdown path this item is being imported from, if any (DEBT-BACKLOG-IMPORT-PLAN-PROVENANCE-001). */
98
+ importedFrom?: string;
95
99
  dedupeScan?: DedupeScanInput;
96
100
  /** Skip the dedupe gate and file anyway (planner override after reviewing candidates). */
97
101
  force?: boolean;
@@ -106,6 +110,14 @@ export interface UpdateItemInput {
106
110
  body?: string;
107
111
  tags?: string[];
108
112
  projectPath?: string;
113
+ /**
114
+ * Provenance owner of this item (the source-file path that authored it).
115
+ * Only ever set to BACKFILL a legacy row whose `importedFrom` was never
116
+ * stamped (created before the provenance field existed) — see
117
+ * `importFromMarkdown`'s owning-import branch. An already-stamped owner is
118
+ * immutable and must never be reassigned via this patch.
119
+ */
120
+ importedFrom?: string;
109
121
  }
110
122
  export interface BacklogFilter {
111
123
  repo?: string;
@@ -119,6 +131,30 @@ export interface BacklogFilter {
119
131
  claimedBy?: string;
120
132
  tags?: string[];
121
133
  grep?: string;
134
+ /**
135
+ * Exact-match on `BacklogItem.importedFrom` — the sourcePath that OWNS an
136
+ * item's canonical content (DEBT-BACKLOG-IMPORT-SCOPE-CROSSFILE-001).
137
+ * Needed for a root-level `BACKLOG.md` projection: filtering by bare
138
+ * `{repo}` alone would also surface every item cross-referenced FROM root
139
+ * by a plan/package file (which correctly carries a `plan`/`projectPath`
140
+ * of its own once ownership-gating lands) — `importedFrom` is the only
141
+ * field that reliably answers "does THIS file own this item's content",
142
+ * independent of which OTHER files also cite the same id.
143
+ */
144
+ importedFrom?: string;
145
+ /**
146
+ * Repo-level projection selector (MIGRATION.md §2.2 "root BACKLOG.md =
147
+ * repo-only, no projectPath/plan"). When true, returns only items that carry
148
+ * NEITHER a `projectPath` NOR a `plan` — i.e. items owned by the repo root
149
+ * rather than a package or plan projection. Unlike the `importedFrom`
150
+ * workaround it does not depend on provenance, so a freshly tool-created
151
+ * repo-level item (which has no `importedFrom`) still appears in the root
152
+ * projection — the Phase-3 DoD ("a fresh create-item appears in the
153
+ * regenerated BACKLOG.md") requires this. Cross-referenced items that carry a
154
+ * `plan`/`projectPath` render in that plan/package projection instead, never
155
+ * duplicated into root.
156
+ */
157
+ rootLevel?: boolean;
122
158
  limit?: number;
123
159
  offset?: number;
124
160
  }
@@ -194,16 +230,45 @@ export interface ImportMarkdownInput {
194
230
  path: string;
195
231
  repo: string;
196
232
  projectPath?: string;
233
+ /** Plan slug to attach every imported item to (DEBT-BACKLOG-IMPORT-PLAN-PROVENANCE-001) — replaces the post-import attachToPlan-per-id workaround. */
234
+ plan?: string;
235
+ /**
236
+ * Provenance path recorded on each imported node's `importedFrom` field
237
+ * (DEBT-BACKLOG-IMPORT-PLAN-PROVENANCE-001). Defaults to `path` when
238
+ * omitted — the file actually read IS the source, so a caller only needs
239
+ * to set this explicitly when it differs (e.g. importing from a scratch
240
+ * copy but wanting the ORIGINAL path recorded).
241
+ */
242
+ sourcePath?: string;
197
243
  dryRun?: boolean;
198
244
  }
245
+ /** A `##`/`###` header line that failed the strict `HEADER_RE` id pattern but looks like an attempted id (DEBT-BACKLOG-IMPORT-SILENT-DROP-001). */
246
+ export interface MalformedHeaderInfo {
247
+ /** 1-based line number in the source file. */
248
+ line: number;
249
+ headerLine: string;
250
+ }
199
251
  export interface ImportResult {
200
252
  parsed: number;
201
253
  created: number;
202
254
  skippedDuplicates: number;
255
+ /**
256
+ * Of `skippedDuplicates` (an already-existing humanId), how many had a
257
+ * title/body/priority/status that DIFFERED from the graph's current copy
258
+ * and were refreshed to match the re-imported source
259
+ * (BUG-BACKLOG-IMPORT-INSERT-ONLY-NO-UPDATE-001 — re-importing used to be
260
+ * pure insert-only: a status/content change made directly in a
261
+ * `BACKLOG.md` file after the first import was silently never reflected
262
+ * in the graph on a later re-import). An unchanged existing item is a
263
+ * true no-op — never counted here.
264
+ */
265
+ updated: number;
203
266
  errors: Array<{
204
267
  humanId: string;
205
268
  message: string;
206
269
  }>;
270
+ /** Headers that look like a corrupted/typo'd id and were dropped instead of parsed — never silent (DEBT-BACKLOG-IMPORT-SILENT-DROP-001). */
271
+ malformedHeaders: MalformedHeaderInfo[];
207
272
  }
208
273
  export interface AuditTrailEntry {
209
274
  at: string;
@@ -218,3 +283,18 @@ export interface AuditTrailResult {
218
283
  supersededBy?: string;
219
284
  };
220
285
  }
286
+ export type MigrationPhase = 'not-started' | 'phase-1' | 'phase-2' | 'phase-3' | 'phase-4' | 'phase-5' | 'complete';
287
+ export interface MigrationStatusResult {
288
+ phase: MigrationPhase;
289
+ /** One-line human-readable meaning of `phase`, e.g. "phase-2: BACKLOG.md is still authoritative; the tool is shadow-running in parity-check mode." */
290
+ description: string;
291
+ /** True once the graph (not hand-edited markdown) is authoritative — phase-3 and later. */
292
+ toolIsAuthoritative: boolean;
293
+ }
294
+ /** `setMigrationPhase`'s result — `MigrationStatusResult` plus the absolute
295
+ * path of the GLOBAL `config.yaml` the new phase was persisted to (so a
296
+ * caller can confirm this was a durable, cross-process write, not merely an
297
+ * in-memory value). */
298
+ export interface SetMigrationPhaseResult extends MigrationStatusResult {
299
+ configPath: string;
300
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@adhd/backlog",
3
+ "version": "0.0.2",
4
+ "bin": {
5
+ "backlog": "./dist/index.js"
6
+ },
7
+ "dependencies": {
8
+ "@adhd/sox-graph-store": "^0.3.0",
9
+ "better-sqlite3": "^12.10.0",
10
+ "@adhd/environment": "0.0.2",
11
+ "@adhd/environment-base-spec": "0.0.3",
12
+ "@adhd/apigen-core-client": "^0.1.1",
13
+ "@adhd/apigen-plugin-api-fastify": "^0.1.2",
14
+ "@adhd/apigen-plugin-openapi": "^0.1.3",
15
+ "@adhd/apigen-plugin-mcp": "^0.1.2",
16
+ "@adhd/apigen-plugin-cli-output": "^0.1.3",
17
+ "@adhd/apigen-engine-naming": "^0.1.3",
18
+ "yaml": "1.10.3"
19
+ },
20
+ "devDependencies": {
21
+ "@types/better-sqlite3": "^7.6.13"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.mjs",
25
+ "typings": "./dist/index.d.ts",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "CHANGELOG.md",
32
+ "skill"
33
+ ]
34
+ }
@@ -0,0 +1,12 @@
1
+ import { Scope } from '@adhd/environment-base-spec';
2
+
3
+ export interface RunServeCommandOpts {
4
+ scope?: Scope;
5
+ /** Test-only override — see `buildBacklogEnv`'s `BuildBacklogEnvOptions`. */
6
+ adhdRoot?: string;
7
+ cwd?: string;
8
+ }
9
+ /** Runs until the process receives SIGTERM/SIGINT (the normal way a host
10
+ * process manager — or `.mcp.json`'s own stdio transport lifecycle — stops
11
+ * a long-lived MCP/HTTP server), then resolves cleanly. */
12
+ export declare function runServeCommand(argv: string[], opts?: RunServeCommandOpts): Promise<void>;
@@ -0,0 +1,54 @@
1
+ import { BacklogCtx } from './client.js';
2
+ import { composeSchemas, Operation, OutputPlugin, RunInput } from '@adhd/apigen-core-client';
3
+ import { Scope } from '@adhd/environment-base-spec';
4
+
5
+ /**
6
+ * Guards a live-mount `plugin.run()` call. Exported (not local to this file)
7
+ * because `cli.ts`'s `runBacklogCli` — the third transport, mounting
8
+ * `@adhd/apigen-plugin-cli-output` the exact same way this file mounts
9
+ * fastify/mcp — needs the identical guard; duplicating a 3-line assertion
10
+ * across two files in the SAME package isn't worth a new `packages/`
11
+ * extraction (CLAUDE.md's "Two-Use Refactor Rule" targets logic reusable
12
+ * ACROSS packages, not an in-package private helper), so it's shared via a
13
+ * plain re-export instead.
14
+ */
15
+ export declare function requireRun(plugin: OutputPlugin): (input: RunInput) => Promise<void>;
16
+ export interface StartOpts {
17
+ transport: 'http' | 'mcp' | 'both';
18
+ port?: number;
19
+ host?: string;
20
+ scope?: Scope;
21
+ /** Test-only override — see `buildBacklogEnv`'s `BuildBacklogEnvOptions`. */
22
+ adhdRoot?: string;
23
+ cwd?: string;
24
+ signal: AbortSignal;
25
+ }
26
+ /**
27
+ * Builds the composed, apigen-ready package descriptor for `client.ts`'s
28
+ * exports. `ctx` may be a plain `BacklogCtx` (the store is already open —
29
+ * `startBacklogServer`'s case, where a long-lived server needs its store
30
+ * immediately regardless of what request comes first) OR a LAZY `() =>
31
+ * BacklogCtx` thunk (`runBacklogCli`'s case) — `createClient` only calls it
32
+ * when a dispatched command actually reaches the real function, which never
33
+ * happens for `--help`/no-args/an unknown command. This is what closes
34
+ * DEBT-BACKLOG-CLI-EAGER-STORE-OPEN-001: `operations`/`schemas` below are
35
+ * computed purely from the built `client.d.ts` (via `extractClientOperations`)
36
+ * and never touch `ctx` at all, so a lazy caller can defer opening the real
37
+ * backing store until a command that actually needs it is dispatched.
38
+ */
39
+ export declare function buildBacklogApigenPackage(ctx: BacklogCtx | (() => BacklogCtx)): Promise<{
40
+ pkg: {
41
+ id: string;
42
+ schemas: ReturnType<typeof composeSchemas>;
43
+ importPath: string;
44
+ fns: Record<string, (...args: unknown[]) => unknown>;
45
+ createClient: () => Promise<BacklogCtx>;
46
+ };
47
+ operations: Operation[];
48
+ }>;
49
+ /**
50
+ * Opens (or reuses) the backlog store + env, mounts every `client.ts` export
51
+ * live via `@adhd/apigen-plugin-api-fastify` and/or `@adhd/apigen-plugin-mcp`
52
+ * — no code generation.
53
+ */
54
+ export declare function startBacklogServer(opts: StartOpts): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { GraphBacklogStore } from './graph-backlog-store.js';
2
+ import { AuditTrailEntry } from '../model.js';
3
+
4
+ export declare const BACKLOG_AUDIT_EVENT_TAG = "backlog-audit-event";
5
+ /**
6
+ * Records one `transition`/`claim` event for the item at `itemNodeId`.
7
+ * `content` is a uniqueness-marker string (mirrors `buildNodeContent()`'s
8
+ * own reasoning, `mapping.ts`) — `@adhd/sox-graph-store`'s global
9
+ * content-hash dedup would otherwise be free to collapse two
10
+ * byte-identical events (e.g. two items independently transitioning
11
+ * `OPEN`→`IN_PROGRESS` with no `by`/`reason` at all) into ONE node.
12
+ */
13
+ export declare function writeAuditEvent(store: GraphBacklogStore, itemNodeId: number, repo: string, humanId: string, kind: AuditTrailEntry['kind'], detail: Record<string, unknown>): void;
14
+ /** Every persisted event for `itemNodeId`, oldest first — ready to merge
15
+ * straight into `auditTrail()`'s `history` array. */
16
+ export declare function queryAuditEvents(store: GraphBacklogStore, itemNodeId: number): AuditTrailEntry[];
@@ -5,15 +5,22 @@ declare function dedupeScan(store: GraphBacklogStore, repo: string, input: Creat
5
5
  export declare function createItemNode(store: GraphBacklogStore, input: CreateItemInput): CreateItemResult;
6
6
  export declare function getItemNode(store: GraphBacklogStore, repo: string, humanId: string): BacklogItem | null;
7
7
  /**
8
- * DEVIATION: `@adhd/sox-graph-store` exposes no primitive to update a node's
9
- * `content` column after creation (`touch()`'s `Partial<NodeMeta>` covers
8
+ * DEVIATION (mitigated — DEBT-BACKLOG-CONTENT-IMMUTABLE-001): `@adhd/sox-graph-store`
9
+ * exposes no PUBLIC primitive to update a node's `content` column after
10
+ * creation (`touch()`'s `Partial<NodeMeta>` covers
10
11
  * name/summary/topic/tags/importance/confidence/tExpires/metadata — never
11
12
  * `content`; verified against the real source). `title` updates the `summary`
12
13
  * column (source of truth for the API) AND `metadata.title`; `body` updates
13
- * ONLY `metadata.body` (source of truth for the API). The underlying FTS
14
- * `content` (set once at `createItem` time) therefore does not reflect a
15
- * later body edit — `listItems({ grep })` may miss a post-edit body term
16
- * until the item is superseded. Filed as DEBT-BACKLOG-CONTENT-IMMUTABLE-001.
14
+ * `metadata.body` (source of truth for the API). Below, a title/body change
15
+ * ALSO re-synchronizes the FTS-indexed `content`/`content_hash` columns
16
+ * directly via raw SQL on the store-owned `db` handle — the same DESIGN.md
17
+ * §14-sanctioned escape hatch `structure.ts`'s `removeDependencyNode` already
18
+ * uses for the one other gap (`edge` deletion) the `GraphBackend` API lacks.
19
+ * This is safe specifically because `fts_node_au` (the real schema's `AFTER
20
+ * UPDATE ON node` trigger — `~/dev/ai/sox-ecosystem/libs/data/graph/graph-store/
21
+ * src/index.ts`'s `FTS_TRIGGERS`) re-indexes `fts_node` automatically on
22
+ * ANY write to `node.content`/`name`/`summary`, so no separate FTS statement
23
+ * is needed here.
17
24
  */
18
25
  export declare function updateItemNode(store: GraphBacklogStore, repo: string, humanId: string, patch: UpdateItemInput): BacklogItem;
19
26
  export declare function softDeleteItemNode(store: GraphBacklogStore, repo: string, humanId: string, reason: string): void;
@@ -0,0 +1,20 @@
1
+ import { GraphBackend } from '@adhd/sox-graph-store';
2
+ import { default as Database } from 'better-sqlite3';
3
+
4
+ export interface GraphBacklogStore {
5
+ /** Raw handle — ONLY for the CAS transaction wrapper (mutate-metadata.ts / ids.ts). */
6
+ readonly db: Database.Database;
7
+ /** All non-CAS reads/writes go through this. */
8
+ readonly graph: GraphBackend;
9
+ }
10
+ /**
11
+ * @param busyTimeoutMs SQLite `busy_timeout` (ms) — how long a blocked
12
+ * `.immediate()` waits for a contended lock before throwing `SQLITE_BUSY`
13
+ * (DEBT-BACKLOG-CONCURRENCY-BUSY-RETRY-001). Callers reading from
14
+ * `BacklogConfig` should pass `env.config.db.busyTimeoutMs`; the default
15
+ * here (5000) matches that config field's own default, for callers (tests,
16
+ * ad-hoc scripts) that open a store directly without going through
17
+ * `buildBacklogEnv`.
18
+ */
19
+ export declare function openGraphBacklogStore(dbPath: string, busyTimeoutMs?: number): GraphBacklogStore;
20
+ export declare function closeGraphBacklogStore(store: GraphBacklogStore): void;
@@ -0,0 +1,24 @@
1
+ import { GraphBacklogStore } from './graph-backlog-store.js';
2
+ import { NodeRecord } from '@adhd/sox-graph-store';
3
+
4
+ /**
5
+ * Resolves the humanId to insert under (either `idOverride`, re-verified for
6
+ * an already-live node, or the next auto-allocated `family-NNN`) and invokes
7
+ * `insert(humanId, existing)` — ALL inside one retried `.immediate()`
8
+ * transaction, so no other concurrent `.immediate()`-wrapped write can
9
+ * interleave between "the id was resolved" and "a node claiming it landed".
10
+ * `existing` is the already-live node under `idOverride` (re-checked HERE,
11
+ * not just by an earlier, racy caller-side check) — `insert` is expected to
12
+ * short-circuit on a non-null `existing` exactly like `createItemNode`'s
13
+ * documented idempotent-reimport behavior, but now race-free.
14
+ */
15
+ export declare function allocateHumanIdAndInsert<T>(store: GraphBacklogStore, repo: string, family: string, idOverride: string | undefined, insert: (humanId: string, existing: NodeRecord | null) => T): T;
16
+ /**
17
+ * @deprecated kept ONLY as a standalone id-generator for any caller that does
18
+ * not need an atomic insert alongside it. `createItemNode`/
19
+ * `supersedeItemNode` no longer use this (see `allocateHumanIdAndInsert`'s
20
+ * doc comment for why splitting allocate-then-insert-later is unsafe under
21
+ * concurrency). Still correct in isolation — just NOT TOCTOU-safe when the
22
+ * caller's own insert happens in a separate, later transaction.
23
+ */
24
+ export declare function allocateHumanId(store: GraphBacklogStore, repo: string, family: string): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * immediate-retry.ts — bounded, jittered exponential-backoff retry wrapper
3
+ * around a `db.transaction(fn).immediate()` call (DEBT-BACKLOG-CONCURRENCY-
4
+ * BUSY-RETRY-001). `mutate-metadata.ts` / `ids.ts` are the ONLY two write
5
+ * paths that call `.immediate()` directly (DESIGN.md §3/§4.3) and both funnel
6
+ * through this wrapper — retrying ONLY the specific `SQLITE_BUSY`/
7
+ * `SQLITE_BUSY_TIMEOUT`/`SQLITE_BUSY_SNAPSHOT` error `better-sqlite3` throws
8
+ * when a `BEGIN IMMEDIATE`'s wait exceeds `busy_timeout`. Any other thrown
9
+ * error (including `NotFoundError`, `ClaimContentionError`) propagates
10
+ * immediately, unretried — and the semantic `'held'` claim-contention RESULT
11
+ * (claim.ts) is a normal RETURN VALUE, never an exception, so it is never
12
+ * touched by this wrapper either.
13
+ */
14
+ export interface ImmediateRetryOpts {
15
+ /** Total attempts (first try + retries). Default 5. */
16
+ maxAttempts?: number;
17
+ }
18
+ /**
19
+ * Runs `attempt()` (expected to be `() => db.transaction(fn).immediate()`),
20
+ * retrying up to `maxAttempts` times ONLY when it throws a SQLITE_BUSY-shaped
21
+ * error, with jittered exponential backoff between attempts (20ms, 40ms,
22
+ * 80ms, 160ms, capped at 500ms). Any other error propagates immediately.
23
+ * After the final attempt still fails, the last SQLITE_BUSY error is
24
+ * re-thrown (a genuine, sustained pileup is still a real failure — this
25
+ * bounds the wait, it doesn't hide contention forever).
26
+ */
27
+ export declare function withImmediateRetry<T>(attempt: () => T, opts?: ImmediateRetryOpts): T;
@@ -0,0 +1,101 @@
1
+ import { BacklogItem, BacklogStatus, Citation, Note, Priority } from '../model.js';
2
+ import { NodeRecord } from '@adhd/sox-graph-store';
3
+
4
+ export declare const BACKLOG_ITEM_TAG = "backlog-item";
5
+ export declare const BACKLOG_PLAN_TAG = "backlog-plan";
6
+ export declare const BACKLOG_ASSIGNEE_TAG = "backlog-assignee";
7
+ /**
8
+ * Mirrors `@adhd/sox-graph-store`'s PRIVATE (unexported) `hashContent()`
9
+ * (`sha256(content.trim().toLowerCase())`) exactly. Needed only by
10
+ * `updateItemNode` (crud.ts), which writes `node.content`/`node.content_hash`
11
+ * directly via raw SQL (DESIGN.md §14's sanctioned escape hatch for the one
12
+ * gap `touch()` doesn't cover — DEBT-BACKLOG-CONTENT-IMMUTABLE-001) and must
13
+ * keep `content_hash` consistent with the `content` it just wrote, exactly as
14
+ * `writeNode()` would have. If upstream ever changes its normalization, this
15
+ * copy must change with it — there is no shared primitive to import instead.
16
+ */
17
+ export declare function computeContentHash(content: string): string;
18
+ /**
19
+ * `@adhd/sox-graph-store`'s `searchNodes(query)` binds `query` DIRECTLY as an
20
+ * FTS5 `MATCH` argument, parsed by FTS5's own boolean/column-filter query
21
+ * grammar — NOT a plain-text search. `searchNodes`'s own `query.replace(/"/g,
22
+ * '""')` cannot make this safe for arbitrary text: doubling an EXISTING `"`
23
+ * always yields an EVEN number of quote characters in the output, so a
24
+ * caller can never end up with a validly single-quoted phrase this way
25
+ * (verified empirically — wrapping a title in quotes before calling
26
+ * `searchNodes` still throws). A title containing a bare `-`, `:`, `(`, `)`,
27
+ * or `"` (all real English titles — "off-by-one", "fix: the thing" — not
28
+ * edge cases) crashes `createItemNode`'s dedupe scan outright with a raw
29
+ * `SqliteError`, since `force:true` (the only path that SKIPS the dedupe
30
+ * scan) is not the default. `dedupeScan` (crud.ts) uses this to strip every
31
+ * FTS5-syntax-significant character to a space before searching — this is
32
+ * BEHAVIOR-PRESERVING for the already-working case (a title with no special
33
+ * characters passes through unchanged, still an implicit-AND bareword
34
+ * query), and merely prevents the CRASH for the common case that hits one.
35
+ */
36
+ /**
37
+ * Strips every FTS5-syntax-significant character to a space before a title/
38
+ * grep term ever reaches `searchNodes`'s raw MATCH bind (dedupeScan and
39
+ * `queryItemNodes`'s grep path, BUG-BACKLOG-DEDUPE-FTS-SYNTAX-CRASH-001).
40
+ *
41
+ * The original fix (2026-07) only stripped the small set of characters found
42
+ * in the reported crash: `"():^*-`. `#` (an entirely ordinary character in a
43
+ * real title, e.g. "Fixes #123" or this file's own regression test) was
44
+ * discovered independently to ALSO crash `searchNodes` with `fts5: syntax
45
+ * error near "#"` — and probing FTS5's actual grammar directly (not
46
+ * guessing) turned up a much longer list that crashes the same way: `. { }
47
+ * ~ [ ] @ ! $ % & = < > ? / \ ; ,` — `{` is the most dangerous of these
48
+ * (`no such column: create` — it gets parsed as column-filter syntax rather
49
+ * than merely erroring, a correctness risk beyond a crash). A second
50
+ * whack-a-mole char-by-char addition would leave the same class of gap open
51
+ * for whatever punctuation mark comes up next, so this strips every
52
+ * character that is NOT a Unicode letter, number, underscore, or whitespace
53
+ * — the complete, non-enumerable-by-hand safe set for an FTS5 bareword
54
+ * query — rather than continuing to enumerate a denylist.
55
+ */
56
+ export declare function sanitizeFtsQuery(text: string): string;
57
+ /**
58
+ * The JSON shape persisted in `node.meta` (DESIGN.md §2.2/§4.1). Every
59
+ * mutating store operation reads the CURRENT full object, computes a new
60
+ * COMPLETE object, and writes it back via `mutateMetadata` — `touch()`
61
+ * replaces `meta` wholesale (verified, DESIGN.md §14 point 4), so a partial
62
+ * write here would silently drop every other field.
63
+ */
64
+ export interface BacklogNodeMeta {
65
+ humanId: string;
66
+ kind: string;
67
+ family: string;
68
+ title: string;
69
+ body: string;
70
+ status: BacklogStatus;
71
+ priority?: Priority;
72
+ repo: string;
73
+ projectPath?: string;
74
+ plan?: string;
75
+ /** Source markdown path this item was imported from, if any (DEBT-BACKLOG-IMPORT-PLAN-PROVENANCE-001). */
76
+ importedFrom?: string;
77
+ assignee?: string;
78
+ claimedBy?: string;
79
+ claimedAt?: string;
80
+ citations: Citation[];
81
+ notes: Note[];
82
+ createdAt: string;
83
+ updatedAt: string;
84
+ /** Set by archiveResolved — excludes the item from renderToMarkdown's default view. */
85
+ archivedAt?: string;
86
+ /** Dedupe-scan exact-match fields (DESIGN.md §2.4). */
87
+ dedupeSymbol?: string;
88
+ dedupePath?: string;
89
+ dedupeErrorText?: string;
90
+ }
91
+ export declare function humanIdKind(humanId: string): string;
92
+ export declare function humanIdFamily(humanId: string): string;
93
+ /** See the file-level DEVIATION doc comment for why the marker is appended. */
94
+ export declare function buildNodeContent(repo: string, humanId: string, title: string, body: string): string;
95
+ export declare function buildNodeName(repo: string, humanId: string): string;
96
+ /** DESIGN.md §2.2 — importance derived deterministically from priority. */
97
+ export declare function importanceForPriority(priority: Priority | undefined): number;
98
+ export declare function buildTags(kind: string, family: string, userTags?: readonly string[]): string[];
99
+ /** A node counts as a live backlog item iff it carries the tag AND is not superseded (DESIGN.md §14). */
100
+ export declare function isLiveBacklogItemNode(node: NodeRecord): boolean;
101
+ export declare function toBacklogItem(node: NodeRecord): BacklogItem;
package/package.json CHANGED
@@ -1,20 +1,34 @@
1
1
  {
2
2
  "name": "@adhd/backlog",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
+ "bin": {
5
+ "backlog": "./dist/index.js"
6
+ },
4
7
  "dependencies": {
5
8
  "@adhd/sox-graph-store": "^0.3.0",
6
9
  "better-sqlite3": "^12.10.0",
7
- "@adhd/environment": "^0.0.2",
8
- "@adhd/environment-base-spec": "^0.0.3",
9
- "@adhd/apigen-core-client": "^0.1.2",
10
- "@adhd/apigen-plugin-api-fastify": "^0.1.3",
11
- "@adhd/apigen-plugin-openapi": "^0.1.4",
12
- "@adhd/apigen-plugin-mcp": "^0.1.3"
10
+ "@adhd/environment": "0.0.2",
11
+ "@adhd/environment-base-spec": "0.0.3",
12
+ "@adhd/apigen-core-client": "^0.1.1",
13
+ "@adhd/apigen-plugin-api-fastify": "^0.1.2",
14
+ "@adhd/apigen-plugin-openapi": "^0.1.3",
15
+ "@adhd/apigen-plugin-mcp": "^0.1.2",
16
+ "@adhd/apigen-plugin-cli-output": "^0.1.3",
17
+ "@adhd/apigen-engine-naming": "^0.1.3",
18
+ "yaml": "1.10.3"
19
+ },
20
+ "devDependencies": {
21
+ "@types/better-sqlite3": "^7.6.13"
13
22
  },
14
- "main": "./index.js",
15
- "module": "./index.mjs",
16
- "typings": "./index.d.ts",
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.mjs",
25
+ "typings": "./dist/index.d.ts",
17
26
  "publishConfig": {
18
27
  "access": "public"
19
- }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "CHANGELOG.md",
32
+ "skill"
33
+ ]
20
34
  }