@adhd/backlog 1.0.4 → 1.0.6

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 (59) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +194 -39
  3. package/api.d.ts +87 -0
  4. package/api.ir.json +1 -1
  5. package/citation.d.ts +176 -0
  6. package/envelope.d.ts +36 -2
  7. package/index.d.ts +23 -3
  8. package/index.js +105 -57
  9. package/index.mjs +11450 -6758
  10. package/ir-artifact.d.ts +7 -3
  11. package/lifecycle.d.ts +49 -0
  12. package/package.json +5 -5
  13. package/query/canonical.d.ts +16 -0
  14. package/query/card.d.ts +116 -4
  15. package/query/get.d.ts +8 -1
  16. package/query/index.d.ts +1 -0
  17. package/query/query.d.ts +32 -22
  18. package/query/redirect.d.ts +30 -0
  19. package/query/resolve.d.ts +75 -0
  20. package/query/similar-clusters.d.ts +8 -0
  21. package/query/similarity-signals.d.ts +47 -0
  22. package/query/spec-staleness.d.ts +21 -0
  23. package/query/types.d.ts +249 -34
  24. package/query/verdict-core.d.ts +21 -0
  25. package/query/verdict.d.ts +22 -0
  26. package/query/views/catalog.d.ts +47 -0
  27. package/query/views/registry.d.ts +27 -7
  28. package/query/views/report.d.ts +49 -0
  29. package/query/views/semantic.d.ts +28 -5
  30. package/query/views/stats.d.ts +38 -2
  31. package/readiness.d.ts +29 -0
  32. package/retry-policy.d.ts +44 -0
  33. package/serve.d.ts +1 -1
  34. package/server.d.ts +71 -3
  35. package/service-config.d.ts +109 -0
  36. package/service-errors.d.ts +51 -0
  37. package/skill/SKILL.md +688 -81
  38. package/store/catalog-invariant-guard.d.ts +13 -7
  39. package/vocabulary.d.ts +48 -0
  40. package/write/anchor-check.d.ts +139 -0
  41. package/write/attestation.d.ts +64 -0
  42. package/write/catalog-merge.d.ts +15 -8
  43. package/write/catalog.d.ts +72 -2
  44. package/write/citation-path.d.ts +31 -0
  45. package/write/citation.d.ts +157 -0
  46. package/write/create-issue.d.ts +143 -29
  47. package/write/errors.d.ts +196 -1
  48. package/write/gate.d.ts +67 -0
  49. package/write/merge-project.d.ts +54 -0
  50. package/write/obligation.d.ts +133 -0
  51. package/write/relate.d.ts +1 -1
  52. package/write/revision.d.ts +27 -0
  53. package/write/similarity-scan.d.ts +84 -0
  54. package/write/spec-revision.d.ts +131 -0
  55. package/write/spec-revision.reconcile.d.ts +48 -0
  56. package/write/transition.d.ts +13 -2
  57. package/write/tx.d.ts +44 -2
  58. package/write/uid-prefix.d.ts +78 -0
  59. package/write/update.d.ts +23 -1
package/ir-artifact.d.ts CHANGED
@@ -61,9 +61,13 @@ export declare function bakedIrArtifactPath(distDir: string): string;
61
61
  export declare function readBakedIrArtifact(distDir: string): Operation[] | undefined;
62
62
  /**
63
63
  * Writes the baked IR artifact to `outFile`, recording the provenance the
64
- * reader re-validates against: the source `.d.ts` path (audit), its sha256 and
65
- * byte length, plus a sha256 per EVERY `*.d.ts` under the artifact's `dist/`
66
- * (`artifactSource.deps`). Uses the plugin's shared `atomicWriteJson`, so the
64
+ * reader re-validates against: the source `.d.ts`'s DIST-RELATIVE path
65
+ * (`api.d.ts`; audit, machine-independent), its sha256 and byte length, plus a
66
+ * sha256 per EVERY `*.d.ts` under the artifact's `dist/`
67
+ * (`artifactSource.deps`). Emits NO timestamp and NO absolute path, so two
68
+ * builds of identical source produce byte-identical artifacts — reproducible
69
+ * output that `version`'s no-bump fast path depends on (`normalizedHash` over
70
+ * `dist/`). Uses the plugin's shared `atomicWriteJson`, so the
67
71
  * artifact is published atomically and durably exactly like a runtime-cache
68
72
  * entry — a build killed mid-write can never leave a half-written artifact
69
73
  * behind.
package/lifecycle.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * lifecycle.ts — the `starting / live / ready` trichotomy (D-A, Segment B).
3
+ *
4
+ * Exactly the three states DESIGN §2 D1 names. Failure/stop are transitions,
5
+ * never states:
6
+ *
7
+ * - `starting`: env resolved, store opening, the mount being composed (the
8
+ * ~14 s cold-start window), transports not yet accepting.
9
+ * - `live`: the transport accepts connections (stdio connected / socket bound).
10
+ * - `ready`: the serving-path probe (`readiness.ts`) has passed.
11
+ *
12
+ * A LIVENESS failure means RESTART; a READINESS failure is REPORT ONLY, state
13
+ * unchanged, NO restart. {@link IServiceLifecycle.fail} records a failure
14
+ * WITHOUT changing the state and returns the action the supervisor must take.
15
+ */
16
+ export type IServiceState = 'starting' | 'live' | 'ready';
17
+ export interface IServiceFailure {
18
+ kind: 'liveness' | 'readiness';
19
+ code: string;
20
+ message: string;
21
+ subject?: string;
22
+ }
23
+ export interface IServiceReport {
24
+ state: IServiceState;
25
+ /** ISO timestamp the current state was entered. */
26
+ since: string;
27
+ /** Present on a failure. A liveness failure means RESTART; a readiness
28
+ * failure means REPORT ONLY, no restart. */
29
+ failure?: IServiceFailure;
30
+ lastTickAt?: string;
31
+ degraded: boolean;
32
+ }
33
+ export interface IServiceLifecycle {
34
+ state(): IServiceState;
35
+ report(): IServiceReport;
36
+ /** starting → live (transport accepting). */
37
+ markLive(): void;
38
+ /** live → ready (the serving-path probe passed). Idempotent. */
39
+ markReady(): void;
40
+ /** Record a failure WITHOUT changing the state; returns the supervisor action. */
41
+ fail(f: IServiceFailure): {
42
+ action: 'restart' | 'report';
43
+ };
44
+ whenReady(): Promise<void>;
45
+ }
46
+ export interface ILifecycleDeps {
47
+ now?: () => number;
48
+ }
49
+ export declare function createLifecycle(deps?: ILifecycleDeps): IServiceLifecycle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhd/backlog",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "bin": {
5
5
  "adhd-backlog": "index.js"
6
6
  },
@@ -10,17 +10,17 @@
10
10
  "@adhd/apigen-engine-naming": "^0.2.5",
11
11
  "@adhd/apigen-plugin-api-fastify": "^0.2.6",
12
12
  "@adhd/apigen-plugin-batch": "^0.2.5",
13
- "@adhd/apigen-plugin-cli-output": "^0.2.6",
13
+ "@adhd/apigen-plugin-cli-output": "^0.2.7",
14
14
  "@adhd/apigen-plugin-ir-cache": "^0.1.3",
15
15
  "@adhd/apigen-plugin-mcp": "^0.3.1",
16
16
  "@adhd/apigen-plugin-openapi": "^0.2.5",
17
17
  "@adhd/environment": "^0.1.8",
18
18
  "@adhd/environment-base-spec": "^0.1.3",
19
19
  "@adhd/environment-builder": "^0.1.7",
20
- "@adhd/sox-graph-store": "^0.11.1",
21
- "@adhd/sox-hybrid-search": "^0.5.0",
20
+ "@adhd/sox-graph-store": "^0.12.0",
21
+ "@adhd/sox-hybrid-search": "^0.6.0",
22
22
  "@adhd/sox-semantic": "^0.1.8",
23
- "@adhd/sox-store-adapter": "^0.10.0",
23
+ "@adhd/sox-store-adapter": "^0.11.0",
24
24
  "@adhd/sox-telemetry": "^0.3.2",
25
25
  "@modelcontextprotocol/sdk": "^1.29.0",
26
26
  "ajv": "^8.20.0",
@@ -0,0 +1,16 @@
1
+ import { AdapterTransaction } from '@adhd/sox-store-adapter';
2
+ import { GraphBackend } from '@adhd/sox-graph-store';
3
+
4
+ /**
5
+ * Resolve `uid` to the canonical head of its reserved `duplicate_of` chain.
6
+ * Read-only; returns the input uid when it resolves to nothing or is unlinked.
7
+ * Never throws on a linked uid.
8
+ */
9
+ export declare function resolveCanonicalIssue(graph: GraphBackend, uid: string): Promise<string>;
10
+ /**
11
+ * Tx-scoped {@link resolveCanonicalIssue} for the reserved same-judgement
12
+ * write path: the same chain walk (duplicate_of → one-hop redirect →
13
+ * `SUPERSEDES` head), hand-composed against the transaction handle rather than
14
+ * a bare `GraphBackend` (ADR-0002 — one concept, one algorithm).
15
+ */
16
+ export declare function resolveIssueCanonicalTx(tx: AdapterTransaction, uid: string): Promise<string>;
package/query/card.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { IIssueAuditEntry, IIssueCard, IIssueField, IIssueRef } from './types.js';
1
+ import { IVerdictRung } from './verdict.js';
2
+ import { IIssueAuditEntry, IIssueCard, IIssueField, IIssueRef, IObligationView, ISimilarLinks, IScoreKind } from './types.js';
2
3
  import { EdgeRecord, GraphBackend, NodeRecord } from '@adhd/sox-graph-store';
3
4
 
4
5
  /** SPEC.md §6.5's `assertKnownFields` — unknown name → `BacklogValidationError` naming it, never a silent drop. */
@@ -14,18 +15,129 @@ export declare function resolveAuditTrail(graph: GraphBackend, issueId: number,
14
15
  * actually blocking it right now," never the full historical blocker set.
15
16
  */
16
17
  export declare function resolveBlockers(graph: GraphBackend, issueId: number): Promise<IIssueRef[]>;
17
- /** `related` — both directions of `relates_to` (an `n:m` symmetric-in-practice rel, §3), deduplicated. */
18
+ /**
19
+ * `related` — every live relation type that touches this issue, deduplicated
20
+ * by node and tagged with the relation that produced it (C2 — structural
21
+ * legibility; SPEC.md §5/§6.5, DESIGN §5 AC2):
22
+ *
23
+ * - `relates_to` (both directions — an `n:m` symmetric-in-practice rel, §3),
24
+ * tagged `relates_to`;
25
+ * - `part_of` (both directions: the parent when this item is the child, the
26
+ * children when it is the parent), tagged `part_of`;
27
+ * - `blocks` outbound (this item blocks the target), tagged `blocks`; and
28
+ * `blocks` incoming (the source blocks this item), tagged `blocked_by`.
29
+ *
30
+ * `blockers` (the non-terminal INCOMING subset) is deliberately NOT removed —
31
+ * it stays as the "what's actually blocking me right now" view; `related` is
32
+ * the structural superset. Widening this result is additive ROWS, never a
33
+ * signature change: a consumer reading `uid`/`title`/`status` is unaffected.
34
+ * Dedup is by node id, first-seen wins, in the deterministic order above.
35
+ */
18
36
  export declare function resolveRelated(graph: GraphBackend, issueId: number, outgoing?: EdgeRecord[]): Promise<IIssueRef[]>;
37
+ /**
38
+ * C9 — the reviewed `similar_to` links touching one issue, BOTH directions
39
+ * (`n:m`). Outgoing edges (`similar_to` from this issue) become `similarTo`;
40
+ * incoming edges (into it) become `similarFrom`. Each neighbour is projected to
41
+ * the compact `ISimilarRef` shape (uid/title/status + owning project/repo).
42
+ *
43
+ * ADVISORY relation only — the reserved `duplicate_of` same-judgement relation
44
+ * is deliberately never read here.
45
+ */
46
+ export declare function resolveSimilar(graph: GraphBackend, issueId: number, outgoing?: EdgeRecord[]): Promise<ISimilarLinks>;
47
+ /**
48
+ * Outbound `blocks` (C2) — the issues THIS one blocks, live targets only.
49
+ * Mirrors {@link resolveBlockers}'s shape but for the other direction: that
50
+ * function returns the non-terminal INCOMING blockers ("what stops me"),
51
+ * this one returns everything this item unblocks ("what I am stopping").
52
+ */
53
+ export declare function resolveBlocksOut(graph: GraphBackend, issueId: number, outgoing?: EdgeRecord[]): Promise<IIssueRef[]>;
54
+ /**
55
+ * Transitive outbound dependent count (C2, AC3): how many nodes reach
56
+ * `issueId` via `blocks` (inclusive of direct dependents — the nodes this one
57
+ * blocks, directly or through a chain). A cycle-safe BFS over the outgoing
58
+ * `blocks` edges: a `visited` set counts each node once and terminates a cycle
59
+ * (the reachability set is finite even when the edge set is cyclic).
60
+ *
61
+ * `scope`, when given, restricts the walk to node ids in the set — a list page
62
+ * passes its own candidate id set so an N-item page costs N bounded walks, not
63
+ * N whole-graph walks. On a single `get`, `scope` is omitted and the walk is
64
+ * unbounded but still cycle-safe.
65
+ */
66
+ export declare function resolveDependents(graph: GraphBackend, issueId: number, scope?: ReadonlySet<number>): Promise<number>;
67
+ /**
68
+ * The single `part_of` parent of this item, or `null` (C2, AC3). `part_of` is
69
+ * declared `n:1` (one parent per item, DATA_MODEL.md §3 / `EDGE_KIND_TABLE`),
70
+ * so at most one live outgoing `part_of` edge can exist; the multiplicity gate
71
+ * (`checkMultiplicityTx`) enforces that at write time.
72
+ */
73
+ export declare function resolvePartOf(graph: GraphBackend, issueId: number, outgoing?: EdgeRecord[]): Promise<IIssueRef | null>;
74
+ /**
75
+ * `obligations` (C4) — the declared, typed requirements on this issue, read
76
+ * from its live `has_obligation` targets. PURE PROJECTION: each node's stored
77
+ * `metadata` maps straight onto an {@link IObligationView}; the predicate is
78
+ * NEVER evaluated here (evaluation belongs to the C5 gate at transition time
79
+ * and the C6 verdict on read). One batched `getNodesByIds`; the returned order
80
+ * follows the edge order, and an edge whose target no longer resolves (a
81
+ * concurrently-invalidated row) is simply skipped rather than yielding a
82
+ * partial view.
83
+ */
84
+ export declare function resolveObligations(graph: GraphBackend, issueId: number, outgoing?: EdgeRecord[]): Promise<IObligationView[]>;
19
85
  export interface IAssembleIssueCardOptions {
20
86
  /** Score from a `searchRanked`/`searchNodes` response — populates `_score` when requested (§5a). */
21
87
  score?: number;
88
+ /** Provenance of `score` — populates `_score_kind` alongside `_score` (DESIGN §2 Invariant 5). */
89
+ scoreKind?: IScoreKind;
90
+ /**
91
+ * Bounds the sub-collection pseudo fields (`auditTrail`/`citations`/
92
+ * `related`/`blockers`) — `lastN` takes the LAST N rows, `after` is an
93
+ * opaque cursor naming a uid to resume strictly after. Applied HERE (at the
94
+ * card-assembly call site), never inside `resolveAuditTrail` itself: the
95
+ * `openCurve` view (`views/stats.ts`) calls an independent batched audit
96
+ * reader that needs the FULL trail and must not be bounded.
97
+ */
98
+ bounds?: {
99
+ lastN?: number;
100
+ after?: string;
101
+ };
22
102
  /** Pre-fetched outgoing edges (`getEdges({src: issue.id})`) — pass when the caller already has them (e.g. a batch `query` page) to avoid a redundant round trip. */
23
103
  outgoingEdges?: EdgeRecord[];
104
+ /**
105
+ * The candidate id set a `dependents` walk is bounded to (C2). A list page
106
+ * passes its own page id set so an N-item page costs N bounded walks; a
107
+ * single `get` omits it, leaving the walk unbounded (but cycle-safe). Only
108
+ * consulted when `fields` requests `dependents`.
109
+ */
110
+ scope?: ReadonlySet<number>;
111
+ /**
112
+ * C6 — the highest verdict ladder rung this card may derive. Default 2 (the
113
+ * list bound); a single-item `get` raises it to 3+ via `deriveThrough`.
114
+ * Only consulted when `fields` requests `verdict`.
115
+ */
116
+ verdictRung?: IVerdictRung;
117
+ /** C6 test instrumentation — threaded to `deriveVerdict`'s `onRung`. */
118
+ onVerdictRung?: (rung: number) => void;
119
+ /**
120
+ * C10 — a token the caller holds; when supplied AND the `spec` field was
121
+ * requested, `card.spec.freshness` reports whether that token is still
122
+ * current for this item's spec revision.
123
+ */
124
+ specToken?: string;
24
125
  }
25
126
  /**
26
127
  * Project `issue` onto the requested `fields` (default: the five-field terse
27
128
  * card, SPEC.md §6.5). `uid` is always populated regardless of `fields`.
28
129
  */
29
130
  export declare function assembleIssueCard(graph: GraphBackend, issue: NodeRecord, fields: readonly IIssueField[], opts?: IAssembleIssueCardOptions): Promise<IIssueCard>;
30
- /** Batch-assemble cards for a page of issue nodes, sharing one `getNodesByIds`-batched catalog/placement resolution pass where possible. Falls back to per-issue assembly (still batches internally) — a cross-issue batch would require a different, `getEdges`-list API this file's dependency (`GraphBackend`) does not expose. */
31
- export declare function assembleIssueCards(graph: GraphBackend, issues: NodeRecord[], fields: readonly IIssueField[], scoreByUid?: ReadonlyMap<string, number>): Promise<IIssueCard[]>;
131
+ /** Batch-assemble cards for a page of issue nodes, sharing one `getNodesByIds`-batched catalog/placement resolution pass where possible. Falls back to per-issue assembly (still batches internally) — a cross-issue batch would require a different, `getEdges`-list API this file's dependency (`GraphBackend`) does not expose.
132
+ *
133
+ * The page's own node ids are handed to each card as the `dependents` walk's
134
+ * `scope` (C2): a list read bounds each transitive-dependent count to the page
135
+ * it is rendering, so an N-item page costs N bounded walks rather than N walks
136
+ * over the whole `blocks` graph. A single `get` (which calls
137
+ * {@link assembleIssueCard} directly, not this function) leaves the walk
138
+ * unbounded but cycle-safe. `scope` is inert unless `fields` requests
139
+ * `dependents`. */
140
+ export declare function assembleIssueCards(graph: GraphBackend, issues: NodeRecord[], fields: readonly IIssueField[], scoreByUid?: ReadonlyMap<string, number>, scoreKindByUid?: ReadonlyMap<string, IScoreKind>, verdictOpts?: {
141
+ rung?: IVerdictRung;
142
+ onRung?: (rung: number) => void;
143
+ }): Promise<IIssueCard[]>;
package/query/get.d.ts CHANGED
@@ -5,7 +5,14 @@ import { GraphBackend } from '@adhd/sox-graph-store';
5
5
  * Fetch one issue by `uid`, projected to the requested `fields` (default:
6
6
  * the same five-field card `query` defaults to, SPEC.md §6.3.1/§6.5/AC-13).
7
7
  *
8
+ * `lastN`/`after` bound the sub-collection pseudo fields (`auditTrail`/
9
+ * `citations`/`related`/`blockers`) — `lastN` keeps the LAST N rows in each
10
+ * resolver's own order (the audit trail's oldest-first order is sliced to its
11
+ * newest tail), `after` resumes strictly after a uid. Both are opt-in; absent
12
+ * means the unbounded behavior every existing caller already had.
13
+ *
8
14
  * Errors: `IssueNotFoundError(uid)` (no live node carries `uid`),
9
- * `BacklogValidationError('fields', ...)` (an unknown field name).
15
+ * `BacklogValidationError('fields'|'lastN', ...)` (an unknown field name, or
16
+ * a non-positive/non-integer `lastN`).
10
17
  */
11
18
  export declare function getIssue(graph: GraphBackend, input: IIssueGetByUidInput): Promise<IIssueCard>;
package/query/index.d.ts CHANGED
@@ -63,5 +63,6 @@ export * from './get.js';
63
63
  export * from './query.js';
64
64
  export * from './views/registry.js';
65
65
  export * from './views/stats.js';
66
+ export * from './views/report.js';
66
67
  export * from './views/semantic.js';
67
68
  export * from './markdown.js';
package/query/query.d.ts CHANGED
@@ -48,17 +48,27 @@ export interface IQueryStoreHandle {
48
48
  */
49
49
  readonly assertVocabulary?: () => Promise<void>;
50
50
  /**
51
- * Fail-loud status/priority catalog invariant guard
52
- * (`store/catalog-invariant-guard.ts`). When present, `queryIssuesWithMeta`
53
- * awaits it alongside {@link IQueryStoreHandle.assertVocabulary} before
54
- * dispatching any view: a catalog carrying an unflagged reserved terminal
55
- * status, or two same-kind rows sharing a case fold, must fail loudly rather
56
- * than serve a mis-classified read. OPTIONAL so a hand-built handle (tests,
57
- * the ETL) is unaffected — `api.ts`'s `queryHandle` wires it for every real
58
- * host, and it is deliberately re-run per query (not latched at open), like
59
- * the vocabulary guard.
51
+ * Status/priority catalog invariant check (`store/catalog-invariant-guard.ts`),
52
+ * exposed OPT-IN — deliberately NOT consulted by `queryIssuesWithMeta`, and
53
+ * therefore NEVER on the ordinary read path. A drifted catalog (an unflagged
54
+ * reserved terminal status, or two same-kind rows sharing a case fold) is a
55
+ * bounded data problem: the read path serves the store regardless, and the
56
+ * drift is surfaced as a NAMED, non-zero check by the `store-check` CLI verb.
57
+ * `api.ts`'s `queryHandle` still wires it for every real host, so a caller
58
+ * that WANTS to assert explicitly can call `handle.assertCatalogInvariants?.()`
59
+ * — but no read ever does so implicitly. (Earlier this was awaited inside
60
+ * `queryIssuesWithMeta`; that abort turned one drifted row into a total read
61
+ * outage and was removed. The write path's per-verb abort was removed the
62
+ * same way, so neither ordinary path is gated.)
60
63
  */
61
64
  readonly assertCatalogInvariants?: () => Promise<void>;
65
+ /**
66
+ * C6 test instrumentation — invoked with each verdict ladder rung the list
67
+ * path actually evaluates. Never consulted by production callers; wired only
68
+ * so the N-item list bound (AC6) can be MEASURED, not asserted. The list path
69
+ * must never evaluate beyond rungs 1–2.
70
+ */
71
+ readonly onVerdictRung?: (rung: number) => void;
62
72
  }
63
73
  /**
64
74
  * Whether a `query` verb's own input touches the semantic channel at all —
@@ -106,19 +116,19 @@ export declare function resolveTextInput(handle: IQueryStoreHandle, input: IIssu
106
116
  export interface IQueryIssuesOutcome {
107
117
  result: IIssueQueryResult;
108
118
  /**
109
- * Present only for `view:'list'` — the only view whose result is a
110
- * filtered/paginated row set with an honestly countable pre-limit total
111
- * (`envelope.ts`'s {@link IQueryEnvelopeMeta}). `view:'ready'` computes
112
- * readiness in-memory and stops enumerating once `limit` candidates are
113
- * found (`queryReady`'s own doc comment: removing that early exit to count
114
- * a true pre-limit total would reintroduce the O(candidates) cost its
115
- * grouped-relation-fetch design exists to avoid), `view:'stale'` applies no
116
- * limit at all so every row it returns already IS the total, and
117
- * `view:'similar'`/`'graph'`/`'order'`/`'overlap'` are a ranking, a graph
118
- * projection, a topological order, and an axis grouping respectively — none
119
- * of them a filtered row set with a "how many matched" count. Adding a
120
- * `meta` to any of those would mean inventing a number this module cannot
121
- * stand behind.
119
+ * Present for the four ITEM-LIST views (`list`/`ready`/`stale`/`similar`) —
120
+ * the only views whose result is a filtered row set with an honestly
121
+ * reportable completeness flag (`envelope.ts`'s {@link IQueryEnvelopeMeta}).
122
+ *
123
+ * `list` reports an exact pre-limit `total`; the other three derive `has_more`
124
+ * by fetching one row beyond the page and, when more exist, report `total`
125
+ * with `total_relation:'gte'` — an honest lower bound, never a fabricated
126
+ * exact number (DESIGN §2 Invariant 5, §7 condition 2). `graph`/`order`/
127
+ * `overlap` deliberately carry NO `meta`: they are a graph, a topological
128
+ * order, and an axis grouping — none a filtered row set with a "how many
129
+ * matched" count. Adding a `meta` to any of those would mean inventing a
130
+ * number this module cannot stand behind (see `meta-wire.e2e.ts`, whose
131
+ * `view:'graph'`-has-no-`meta` assertion is load-bearing).
122
132
  */
123
133
  meta?: IQueryEnvelopeMeta;
124
134
  }
@@ -0,0 +1,30 @@
1
+ import { GraphBackend, NodeRecord } from '@adhd/sox-graph-store';
2
+
3
+ /**
4
+ * The stored shape of a soft-retired row's redirect. `toUid` is the canonical
5
+ * live node the retired row points at; `reason` is the free-text explanation
6
+ * recorded on the retired node's audit trail (and, when set, its
7
+ * `meta.retiredReason`). Kept as a named contract so the merge writer and any
8
+ * future reader share one shape rather than re-deriving it from `meta`.
9
+ */
10
+ export interface IRedirect {
11
+ /** The canonical uid this retired row points at. */
12
+ toUid: string;
13
+ /** Why the row was retired — recorded on the retired node's audit + meta. */
14
+ reason: string;
15
+ }
16
+ /**
17
+ * If `record` is a soft-retired row carrying `meta.redirectTo`, return the
18
+ * canonical live node it points at (one hop only; a redirect→redirect chain
19
+ * longer than one hop throws). Otherwise `null`.
20
+ *
21
+ * `record` may itself be invalidated (`t_invalid` set) — that is the normal
22
+ * case, since retiring a row is what gives it a redirect. The TARGET, by
23
+ * contrast, must be a live node: a redirect into a tombstone is a broken
24
+ * invariant, not an unresolved name, so it throws rather than degrading to
25
+ * `null` (which a caller would misread as "this token leads nowhere").
26
+ *
27
+ * @throws InvalidArgumentError when the redirect target is missing/retired, or
28
+ * when the target is itself a redirect (a chain longer than one hop).
29
+ */
30
+ export declare function followRedirect(graph: GraphBackend, record: NodeRecord): Promise<NodeRecord | null>;
@@ -9,14 +9,89 @@ export interface IResolvedRef {
9
9
  name: string;
10
10
  record: NodeRecord;
11
11
  }
12
+ /**
13
+ * Resolve `ref` to exactly one LIVE node, by exact uid or by a UNIQUE uid
14
+ * prefix. Exact match is the fast path and always wins.
15
+ *
16
+ * Errors: `AmbiguousReferenceError` when a prefix matches ≥2 live nodes (the
17
+ * candidates are carried on the error and named in its message);
18
+ * `IssueNotFoundError` (kind `issue`) / `CatalogNotFoundError` (any other
19
+ * kind) when an exact uid or prefix matches nothing; `InvalidArgumentError`
20
+ * when `ref` is a uid attempt shorter than the minimum prefix length.
21
+ */
22
+ export declare function resolveUidPrefix(graph: GraphBackend, ref: string, opts?: {
23
+ expectedKind?: string;
24
+ }): Promise<NodeRecord>;
25
+ /**
26
+ * Like {@link resolveUidPrefix}, but `null` instead of a not-found error when
27
+ * nothing matches — for callers that treat a uid miss as "fall through to a
28
+ * name lookup." An AMBIGUOUS prefix still throws (never silently picks one),
29
+ * and a too-short uid attempt returns `null` so a genuine short business name
30
+ * is not swallowed.
31
+ */
32
+ export declare function tryResolveUidPrefix(graph: GraphBackend, ref: string, opts?: {
33
+ expectedKind?: string;
34
+ }): Promise<NodeRecord | null>;
12
35
  /**
13
36
  * Resolve `uid` → the live `issue` node, or throw {@link IssueNotFoundError}
14
37
  * (SPEC.md §6.1: "a `uid` with no matching live node throws
15
38
  * `IssueNotFoundError(uid)`"). This is the READ-PATH counterpart of
16
39
  * `write/tx.ts`'s `getNodeByUidTx` — safe to call standalone because it is
17
40
  * not composing a check-then-act write around the result.
41
+ *
42
+ * Accepts an exact uid or a UNIQUE uid prefix (see {@link resolveUidPrefix});
43
+ * a superseded node — whether reached by exact uid or prefix — throws the same
44
+ * {@link StaleSupersedeError} pointing at the chain head.
18
45
  */
19
46
  export declare function resolveIssueByUid(graph: GraphBackend, uid: string): Promise<NodeRecord>;
47
+ /**
48
+ * Resolve a token to the LOGICAL node it names TODAY: follow a one-hop merge
49
+ * redirect (C1's soft-retired project/component rows), then walk the
50
+ * `SUPERSEDES` chain to its head. NEVER throws on a superseded uid — that is
51
+ * the whole point of the split from {@link resolveIssueByUid}, which throws
52
+ * `StaleSupersedeError` because its callers (`get`, `card` blockers, `stats`)
53
+ * want to be told a reference is stale, not silently forwarded.
54
+ *
55
+ * This is the non-throwing resolver C3's attestation `subject.id` uses: an
56
+ * attestation written against a uid that a later body-edit superseded must
57
+ * still name the SAME logical item, and a caller holding a stale citation
58
+ * wants today's issue, not the next-oldest corpse.
59
+ *
60
+ * Delegates the chain walk to {@link currentUidOf} rather than re-implementing
61
+ * it (ADR-0002 — one concept, one implementation). Returns the head
62
+ * `NodeRecord`; when the chain dead-ends with no live successor it returns the
63
+ * node it started from, which is the most useful answer available.
64
+ */
65
+ export declare function resolveLogicalIssue(graph: GraphBackend, ref: string): Promise<NodeRecord>;
66
+ /**
67
+ * Normalise any issue uid (possibly superseded) to its SUPERSEDES-chain HEAD
68
+ * uid. Returns the input unchanged for an already-live uid.
69
+ *
70
+ * ONE concept, ONE implementation: this delegates to {@link resolveLogicalIssue}
71
+ * (which returns the head `NodeRecord`) — it is NOT a second chain walk
72
+ * (ADR-0002). Used by C3's attestation read path so an attestation filed
73
+ * against a uid a later body edit superseded still names the SAME logical item.
74
+ */
75
+ export declare function resolveLogicalIssueId(graph: GraphBackend, uid: string): Promise<string>;
76
+ /**
77
+ * Walk the `SUPERSEDES` chain forward from a superseded node to the uid the
78
+ * issue lives under NOW.
79
+ *
80
+ * `update`'s body path mints a new node and writes `SUPERSEDES` new→old, so
81
+ * the successor of a node is the `src` of the edge whose `dst` is that node —
82
+ * the reverse of the direction the edge reads. Repeated edits build a chain,
83
+ * and a uid cited before several edits is several hops back, so this follows
84
+ * the chain to its head rather than stopping at the first hop: a caller
85
+ * holding a stale citation wants today's issue, not the next-oldest corpse.
86
+ *
87
+ * Defensive, because this runs on an error path that must not itself throw:
88
+ * a cycle (impossible by construction, since each edit mints a fresh node,
89
+ * but not enforced by a constraint) is bounded by `seen`; a missing or
90
+ * invalidated successor ends the walk and yields the last good uid, and a
91
+ * chain that dead-ends immediately yields `undefined` — which
92
+ * {@link StaleSupersedeError} documents as "not known here".
93
+ */
94
+ export declare function currentUidOf(graph: GraphBackend, superseded: NodeRecord): Promise<string | undefined>;
20
95
  /**
21
96
  * Resolve `ref` (uid or name, disambiguated by shape — SPEC.md §6.1) against
22
97
  * `expectedKind`, on the READ path: an unresolved NAME is never an error here
@@ -0,0 +1,8 @@
1
+ import { IIssueQueryInput, ISimilarViewClusterBlock } from './types.js';
2
+ import { ISimilarityScanHandle } from '../write/similarity-scan.js';
3
+
4
+ /**
5
+ * Build the additive `view:'similar'` cluster block, or `undefined` when the
6
+ * query is not scope-filtered. Never writes.
7
+ */
8
+ export declare function buildSimilarClusterBlock(handle: ISimilarityScanHandle, input: IIssueQueryInput): Promise<ISimilarViewClusterBlock | undefined>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * similarity-signals.ts — the PURE multi-signal comparator the C9
3
+ * cross-project similarity guard is built on (C9 spec, `similarity-signals.ts`
4
+ * row). No store access, no I/O: two independent signals over plain values, so
5
+ * the guard is unit-testable in isolation and the scan (`write/similarity-scan.ts`)
6
+ * stays a thin composition of this plus the raw vector channel.
7
+ *
8
+ * Why a SECOND signal at all (AC7): cosine alone is never sufficient
9
+ * cross-project. Two boilerplate filings from unrelated repos can sit at high
10
+ * cosine while sharing no real intent — the title-token Jaccard and the
11
+ * structural (citation / component-path) overlap are the independent evidence
12
+ * that keeps such a pair advisory-invisible.
13
+ */
14
+ /** The plain, store-free view of one item a signal comparison needs. */
15
+ export interface ISignalItem {
16
+ title: string;
17
+ body?: string;
18
+ /**
19
+ * Tokens drawn from the item's citations — `file`/`symbol`/`errorText`/
20
+ * `blastRadius`. Callers pass whatever citation strings they have; this
21
+ * module tokenizes them.
22
+ */
23
+ citationTokens?: readonly string[];
24
+ /** The owning component's `meta.path` (repo-relative), when known. */
25
+ componentPath?: string;
26
+ }
27
+ /**
28
+ * Lowercase, split on every non-alphanumeric run, and drop stop words — the
29
+ * token stream {@link titleTokenOverlap} and {@link sharedStructuralSignal}
30
+ * both compare over, so the two signals can never disagree on what a "token"
31
+ * is.
32
+ */
33
+ export declare function normalizeTokens(text: string): string[];
34
+ /**
35
+ * Jaccard overlap of two title token SETS: `|A∩B| / |A∪B|`, in `[0,1]`.
36
+ * Two empty (or all-stop-word) titles have no overlap to speak of and score
37
+ * `0`, never `1` — "nothing in common" must not read as "identical".
38
+ */
39
+ export declare function titleTokenOverlap(a: string, b: string): number;
40
+ /**
41
+ * The second independent signal for a cross-project candidate: does the pair
42
+ * share a citation token (`file`/`symbol`/`errorText`/`blastRadius`) OR do
43
+ * their component paths nest (one is a path-prefix of the other — e.g.
44
+ * `packages/agent` and `packages/agent/agent-engine-compiler`)? Either is
45
+ * structural evidence of a real shared locus, independent of the cosine.
46
+ */
47
+ export declare function sharedStructuralSignal(a: ISignalItem, b: ISignalItem): boolean;
@@ -0,0 +1,21 @@
1
+ import { GraphBackend } from '@adhd/sox-graph-store';
2
+
3
+ export type SpecFreshness = 'fresh' | 'stale' | 'unknown';
4
+ export interface ISpecCheckInput {
5
+ uid: string;
6
+ token?: string;
7
+ }
8
+ export interface ISpecCheckOutcome {
9
+ current_revision: string;
10
+ /** `'sha256:<hex>'` — always this encoding. */
11
+ current_token: string;
12
+ state: SpecFreshness;
13
+ method: 'token' | 'content_hash' | 'ancestry' | 'none';
14
+ reason?: string;
15
+ }
16
+ /**
17
+ * Compare the caller's `token` against the ticket's current revision token and
18
+ * report the freshness ladder's verdict. See the module header for the rules.
19
+ * `uid` is the ticket (any uid on its `SUPERSEDES` chain is resolved forward).
20
+ */
21
+ export declare function checkSpecStaleness(graph: GraphBackend, input: ISpecCheckInput): Promise<ISpecCheckOutcome>;