@adhd/backlog 0.1.6 → 0.1.8
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/CHANGELOG.md +25 -0
- package/client.d.ts +25 -1
- package/env.d.ts +33 -0
- package/index.js +101 -98
- package/index.mjs +4759 -4462
- package/model.d.ts +68 -0
- package/package.json +3 -3
- package/store/lifecycle.d.ts +13 -0
- package/store/repo-migration.d.ts +51 -0
- package/store/serve-lock.d.ts +42 -0
- package/store/signal-cleanup.d.ts +38 -0
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.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"bin": {
|
|
5
5
|
"backlog": "index.js"
|
|
6
6
|
},
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"@adhd/apigen-plugin-ir-cache": "^0.1.0",
|
|
14
14
|
"@adhd/apigen-plugin-mcp": "^0.2.3",
|
|
15
15
|
"@adhd/apigen-plugin-openapi": "^0.2.2",
|
|
16
|
-
"@adhd/environment": "^0.1.
|
|
16
|
+
"@adhd/environment": "^0.1.5",
|
|
17
17
|
"@adhd/environment-base-spec": "^0.1.0",
|
|
18
18
|
"@adhd/sox-graph-store": "^0.8.4",
|
|
19
|
-
"@adhd/sox-store-adapter": "^0.
|
|
19
|
+
"@adhd/sox-store-adapter": "^0.7.0",
|
|
20
20
|
"@adhd/sox-telemetry": "^0.2.0",
|
|
21
21
|
"pino": "10.3.1",
|
|
22
22
|
"pino-pretty": "13.1.3",
|
package/store/lifecycle.d.ts
CHANGED
|
@@ -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,42 @@
|
|
|
1
|
+
/** Thrown when another live process already holds the serve lock for this
|
|
2
|
+
* store. Carries the holder's pid and the lock file path so callers can
|
|
3
|
+
* surface both — refusing SILENTLY (a bare non-zero exit with no reason)
|
|
4
|
+
* is exactly the failure mode that let this incident go undetected. */
|
|
5
|
+
export declare class ServeLockHeldError extends Error {
|
|
6
|
+
readonly holderPid: number;
|
|
7
|
+
readonly lockPath: string;
|
|
8
|
+
constructor(holderPid: number, lockPath: string);
|
|
9
|
+
}
|
|
10
|
+
export interface ServeLockHandle {
|
|
11
|
+
/** Idempotent. Removes the lock file ONLY if it still names this process
|
|
12
|
+
* as holder (never deletes a lock a later process legitimately took over
|
|
13
|
+
* after detecting this one as stale — that would delete a live peer's
|
|
14
|
+
* lock out from under it). */
|
|
15
|
+
release: () => void;
|
|
16
|
+
}
|
|
17
|
+
/** `:memory:` has no filesystem identity — no cross-process concern, no lock. */
|
|
18
|
+
export declare function isLockableDbPath(dbPath: string): boolean;
|
|
19
|
+
/** Canonicalizes to the realpath when the file (or its parent dir) exists,
|
|
20
|
+
* so two spellings of the same store collapse to one lock identity; falls
|
|
21
|
+
* back to a plain absolute resolve when nothing on disk exists yet (a
|
|
22
|
+
* not-yet-created db is still a valid lock anchor — mirrors sox-ecosystem's
|
|
23
|
+
* `singleton.ts` `canonicalizePath`, reimplemented here for the same reason
|
|
24
|
+
* noted in this file's header). */
|
|
25
|
+
export declare function canonicalDbPath(dbPath: string): string;
|
|
26
|
+
export declare function serveLockPath(dbPath: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Acquire the `backlog serve` writer lock for `dbPath`. Fails LOUD and
|
|
29
|
+
* IMMEDIATELY (no spin-wait/retry-until-timeout — a refused `serve` should
|
|
30
|
+
* say why right now, not silently poll and eventually give up) when a live
|
|
31
|
+
* holder is found: throws {@link ServeLockHeldError} naming the holder's
|
|
32
|
+
* pid. A lock file whose recorded pid is no longer alive (crash, SIGKILL,
|
|
33
|
+
* native panic — the same "can't run cleanup code" cases
|
|
34
|
+
* `signal-cleanup.ts`'s own doc comment is honest about) is detected via a
|
|
35
|
+
* liveness probe on READ, not relied on to have been cleaned up by the dead
|
|
36
|
+
* process, and is reclaimed automatically.
|
|
37
|
+
*
|
|
38
|
+
* Call this BEFORE opening the store; release the returned handle only
|
|
39
|
+
* AFTER the store is fully closed (see this file's header re: the
|
|
40
|
+
* shutdown-window).
|
|
41
|
+
*/
|
|
42
|
+
export declare function acquireServeLock(dbPath: string): ServeLockHandle;
|
|
@@ -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;
|