@adhd/backlog 0.1.5 → 0.1.7

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.
package/model.d.ts CHANGED
@@ -145,6 +145,16 @@ export interface CreateItemInput {
145
145
  dedupeScan?: DedupeScanInput;
146
146
  /** Skip the dedupe gate and file anyway (planner override after reviewing candidates). */
147
147
  force?: boolean;
148
+ /**
149
+ * Citations to attach at creation time (BUG-BACKLOG-CREATE-ITEM-DROPS-CITATIONS-001).
150
+ * Previously absent from this interface entirely — a caller passing
151
+ * `citations` on create got a success response with the item created and
152
+ * the citations silently discarded (no such field existed to carry them
153
+ * through). Validated the same way as every other citation write path
154
+ * (`Citation.file` non-empty — see `lifecycle.ts`'s `assertValidCitation`)
155
+ * and rejected as a whole (no partial write) before allocation runs.
156
+ */
157
+ citations?: Citation[];
148
158
  }
149
159
  export interface CreateItemResult {
150
160
  item: BacklogItem;
@@ -367,3 +377,61 @@ export interface MigrationStatusResult {
367
377
  export interface SetMigrationPhaseResult extends MigrationStatusResult {
368
378
  configPath: string;
369
379
  }
380
+ /** One item's move within a `RepoMigrationPlan` — always dry-runnable, never mutates on its own. */
381
+ export interface RepoMigrationPlanItem {
382
+ nodeId: number;
383
+ /** The humanId this item currently carries in `fromRepo`. */
384
+ humanId: string;
385
+ /**
386
+ * The humanId this item will carry in `toRepo` — identical to `humanId`
387
+ * unless `renamed` is true. Deterministic: preserves the item's `family`
388
+ * prefix and picks the next free number in that family within `toRepo`
389
+ * (mirrors `computeNextHumanId`'s own `max + 1` allocation rule), scanning
390
+ * BOTH `toRepo`'s pre-existing items AND every earlier item in this same
391
+ * plan already assigned a number in that family — so two colliding items
392
+ * sharing a family (e.g. two different `BUG-001`s) never collide with each
393
+ * other's rename target either.
394
+ */
395
+ targetHumanId: string;
396
+ /** True iff `humanId` already exists as a LIVE item in `toRepo` and had to be renamed to avoid an id collision. */
397
+ renamed: boolean;
398
+ title: string;
399
+ status: BacklogStatus;
400
+ }
401
+ /**
402
+ * The full, deterministic plan for moving every live item out of `fromRepo`
403
+ * into `toRepo` — computed by a pure read-only scan (`planRepoMigration`),
404
+ * safe to call repeatedly and to inspect before ever mutating anything.
405
+ */
406
+ export interface RepoMigrationPlan {
407
+ fromRepo: string;
408
+ toRepo: string;
409
+ items: RepoMigrationPlanItem[];
410
+ /** Count of `items` where `renamed === true` — the collision count. */
411
+ collisionCount: number;
412
+ }
413
+ /** Per-item outcome of actually executing a `RepoMigrationPlan`. Every planned item gets exactly one of these — nothing is ever silently dropped. */
414
+ export interface RepoMigrationItemResult {
415
+ nodeId: number;
416
+ fromHumanId: string;
417
+ toHumanId: string;
418
+ renamed: boolean;
419
+ ok: boolean;
420
+ /** Present iff `ok === false` — the item was left untouched in `fromRepo`, never partially moved. */
421
+ error?: string;
422
+ }
423
+ /**
424
+ * `migrateRepo`'s result. When `dryRun` is true (the default — a caller must
425
+ * pass `dryRun:false` explicitly to mutate anything), `results` is absent and
426
+ * NOTHING was written; `plan` alone previews exactly what would happen.
427
+ */
428
+ export interface RepoMigrationResult {
429
+ fromRepo: string;
430
+ toRepo: string;
431
+ dryRun: boolean;
432
+ plan: RepoMigrationPlan;
433
+ /** Present only when `dryRun === false`. One entry per `plan.items` entry, same order. */
434
+ results?: RepoMigrationItemResult[];
435
+ succeeded?: number;
436
+ failed?: number;
437
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhd/backlog",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "bin": {
5
5
  "backlog": "index.js"
6
6
  },
@@ -15,8 +15,8 @@
15
15
  "@adhd/apigen-plugin-openapi": "^0.2.2",
16
16
  "@adhd/environment": "^0.1.5",
17
17
  "@adhd/environment-base-spec": "^0.1.0",
18
- "@adhd/sox-graph-store": "^0.8.3",
19
- "@adhd/sox-store-adapter": "^0.5.7",
18
+ "@adhd/sox-graph-store": "^0.8.4",
19
+ "@adhd/sox-store-adapter": "^0.5.8",
20
20
  "@adhd/sox-telemetry": "^0.2.0",
21
21
  "pino": "10.3.1",
22
22
  "pino-pretty": "13.1.3",
@@ -1,6 +1,19 @@
1
1
  import { GraphBacklogStore } from './graph-backlog-store.js';
2
2
  import { ArchiveOpts, BacklogItem, BacklogStatus, Citation, StatsScope, TransitionOpts } from '../model.js';
3
3
 
4
+ /**
5
+ * `Citation.file` is required at the TypeScript/JSON-Schema level
6
+ * (BUG-APIGEN-CORE-CLIENT-001 makes that presence check reach the extracted
7
+ * schema), but presence alone still accepts `""`/whitespace — a citation
8
+ * with no actual file is not evidence. Every write path that accepts a
9
+ * caller-supplied `Citation` (inline on `transitionStatus`/`resolveItem`, and
10
+ * standalone `addCitation`) runs every entry through this before it is
11
+ * persisted. Also reused by `crud.ts`'s `createItemNode` and
12
+ * `structure.ts`'s `supersedeItemNode` for citations supplied inline on
13
+ * `CreateItemInput` (BUG-BACKLOG-CREATE-ITEM-DROPS-CITATIONS-001) — one
14
+ * validation rule, every write path.
15
+ */
16
+ export declare function assertValidCitation(citation: Citation): void;
4
17
  export declare function transitionStatusNode(store: GraphBacklogStore, repo: string, humanId: string, status: BacklogStatus, opts: TransitionOpts): Promise<BacklogItem>;
5
18
  /** Sugar for transitionStatus into any terminal status (SPEC.md §5.4). */
6
19
  export declare function resolveItemNode(store: GraphBacklogStore, repo: string, humanId: string, status: BacklogStatus, opts: TransitionOpts): Promise<BacklogItem>;
@@ -0,0 +1,51 @@
1
+ import { GraphBacklogStore } from './graph-backlog-store.js';
2
+ import { BacklogItem, RepoMigrationItemResult, RepoMigrationPlan, RepoMigrationPlanItem, RepoMigrationResult } from '../model.js';
3
+
4
+ /**
5
+ * Pure, read-only, deterministic migration plan for every LIVE item whose
6
+ * `namespace` is `fromRepo` — deliberately namespace-scoped (not
7
+ * `metadata.repo`-scoped) so an item whose two repo fields have already
8
+ * diverged (found empirically: `FEAT-001`/`FEAT-002`, `namespace:"adhd"` but
9
+ * `metadata.repo:"PseudoSky/adhd"`) is still picked up and fully repaired,
10
+ * not silently skipped because its rendered `repo` already "looks" correct.
11
+ *
12
+ * Collision resolution walks `sourceItems` in humanId order (stable,
13
+ * reproducible across repeated calls) and, for a colliding humanId, assigns
14
+ * the next free number in that family — tracked in an in-memory
15
+ * `familyMaxInTarget` map seeded from `toRepo`'s REAL current max per family
16
+ * and incremented for every rename already planned earlier in this same
17
+ * pass, so two same-family collisions in one batch never target each other.
18
+ */
19
+ export declare function planRepoMigration(store: GraphBacklogStore, fromRepo: string, toRepo: string): Promise<RepoMigrationPlan>;
20
+ /**
21
+ * Executes exactly ONE planned move, re-verifying the live state at the
22
+ * exact node still matches what the plan assumed (a stale plan — the source
23
+ * item moved/mutated, or the target humanId got claimed — since planning
24
+ * throws `InvalidArgumentError` rather than silently proceeding on bad
25
+ * assumptions; `executeRepoMigration` catches this per-item so one stale
26
+ * entry never aborts the whole batch or gets silently skipped).
27
+ *
28
+ * Mirrors `structure.ts`'s `renameHumanIdNode` exactly, generalized to also
29
+ * rewrite `namespace` (which `renameHumanIdNode` deliberately refuses to
30
+ * touch — same-repo only) — see this module's top-of-file doc comment for
31
+ * why `touch()` cannot do that column and a raw `UPDATE` is required.
32
+ */
33
+ export declare function migrateRepoItemNode(store: GraphBacklogStore, item: RepoMigrationPlanItem, fromRepo: string, toRepo: string, actor: string): Promise<BacklogItem>;
34
+ /**
35
+ * Executes every item in `plan`, sequentially (each rename must observe the
36
+ * previous item's real committed state before computing/verifying the next
37
+ * — see `planRepoMigration`'s doc comment on why this must not run in
38
+ * parallel). Every planned item gets exactly one result — `ok:true` or
39
+ * `ok:false` with `error` — so a failure is always visible and never
40
+ * silently drops an item from the report; a per-item failure does not abort
41
+ * the remaining items (an aborted batch would leave an unpredictable subset
42
+ * moved with no way to tell which from the caller's plan alone).
43
+ */
44
+ export declare function executeRepoMigration(store: GraphBacklogStore, plan: RepoMigrationPlan, actor: string): Promise<RepoMigrationItemResult[]>;
45
+ /**
46
+ * The single entry point client.ts/CLI/MCP expose. `dryRun` defaults to
47
+ * `true` — a caller MUST pass `dryRun:false` explicitly to mutate anything,
48
+ * so a bare "preview this migration" call (e.g. an agent double-checking
49
+ * before committing) can never accidentally execute.
50
+ */
51
+ export declare function migrateRepo(store: GraphBacklogStore, fromRepo: string, toRepo: string, actor: string, dryRun?: boolean): Promise<RepoMigrationResult>;
@@ -0,0 +1,38 @@
1
+ export interface SignalCleanupHandle {
2
+ /** Removes the listeners this call installed (idempotent). Call from the
3
+ * owning function's own `finally` once its normal-path cleanup has run,
4
+ * so a signal arriving AFTER that point is not double-handled. */
5
+ dispose: () => void;
6
+ }
7
+ /**
8
+ * Installs SIGINT/SIGTERM handlers that run `cleanup()` at most once, then
9
+ * exit with the conventional 128+signum code. `cleanup` is expected to be
10
+ * `closeGraphBacklogStoreSafe`-shaped (never throws) but is wrapped so a
11
+ * defect there can never suppress the exit or hang the process.
12
+ *
13
+ * @param cleanup Releases this connection's own store lease. Called with no
14
+ * arguments; capture whatever store handle it needs via closure — this
15
+ * lets callers install the handler BEFORE the store finishes opening
16
+ * (`cleanup` reads a `let store` that may still be `undefined` at signal
17
+ * time, in which case the underlying `closeGraphBacklogStoreSafe(undefined)`
18
+ * is a documented no-op).
19
+ * @param onExit Test-only override for `process.exit` (never overridden in
20
+ * production) so a red→green test can observe "would have exited with N"
21
+ * without actually terminating the test runner's process.
22
+ */
23
+ export declare function installSignalCleanup(cleanup: () => Promise<void>, onExit?: (code: number) => void): SignalCleanupHandle;
24
+ /**
25
+ * True iff nothing on this process already listens for SIGINT/SIGTERM.
26
+ * `startBacklogServer` uses this to avoid installing a SECOND, competing
27
+ * handler when its caller (`serve.ts`'s `runServeCommand`) has already
28
+ * registered its own SIGINT/SIGTERM → `AbortController.abort()` handling
29
+ * BEFORE ever calling into this function — that existing handler already
30
+ * overrides Node's default terminate-without-cleanup behaviour for the
31
+ * entire async store-open window, and it drives a *graceful* drain (fastify/
32
+ * MCP transport teardown via the aborted `signal`) that a second,
33
+ * unconditional close+`process.exit` handler would race and potentially cut
34
+ * short. Direct/embedded callers of `startBacklogServer` that do NOT go
35
+ * through `serve.ts` have no such external coverage, so they get this
36
+ * module's own handler instead.
37
+ */
38
+ export declare function hasExternalSignalHandling(): boolean;