@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
@@ -0,0 +1,133 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+
3
+ /** `block` refuses the transition; `warn` records the shortfall but allows it (the C5 gate reads this). */
4
+ export type IObligationSeverity = 'block' | 'warn';
5
+ /** Which side of an edge the subject sits on: `in` = subject is `dst`, `out` = subject is `src`. */
6
+ export type IRelationDirection = 'in' | 'out';
7
+ /**
8
+ * The CLOSED predicate core (DESIGN §2 Primitive 3). No CEL, no dynamic leaf,
9
+ * no timeout — the grammar is exactly these six productions. Validated
10
+ * recursively by {@link assertValidPredicate}.
11
+ *
12
+ * - `evidence{kind,min?}` → distinct verified attestations of `kind` ≥ `min ?? 1`.
13
+ * - `blockers_terminal()` → every incoming live `blocks` source is terminal.
14
+ * - `relation{type,direction}` → a live edge of `type` touches the subject.
15
+ * - `all_of` / `any_of` / `not` → boolean composition. Empty `all_of` ⇒ `true`,
16
+ * empty `any_of` ⇒ `false` (documented, asserted).
17
+ */
18
+ export type IPredicate = {
19
+ op: 'evidence';
20
+ kind: string;
21
+ min?: number;
22
+ } | {
23
+ op: 'blockers_terminal';
24
+ } | {
25
+ op: 'relation';
26
+ type: string;
27
+ direction: IRelationDirection;
28
+ } | {
29
+ op: 'all_of';
30
+ of: IPredicate[];
31
+ } | {
32
+ op: 'any_of';
33
+ of: IPredicate[];
34
+ } | {
35
+ op: 'not';
36
+ of: IPredicate;
37
+ };
38
+ export interface IObligationAppliesTo {
39
+ /** Absent ⇒ applies to any from-status. */
40
+ from?: string;
41
+ /**
42
+ * REQUIRED. Scopes a TRANSITION into that status: a concrete status name, or
43
+ * `'*'` for "any terminal transition". Evaluated by the C5 gate at the
44
+ * transition, and surfaced by the C6 verdict as a *prediction* of that gate.
45
+ */
46
+ to: string;
47
+ }
48
+ export interface IObligationOverride {
49
+ /** Non-empty actor identities permitted to override this obligation — an override always requires a recorded reason at the C5 layer. */
50
+ actors: string[];
51
+ }
52
+ export interface IObligateInput {
53
+ uid: string;
54
+ applies_to: IObligationAppliesTo;
55
+ requirement: IPredicate;
56
+ on_fail: IObligationSeverity;
57
+ override?: IObligationOverride;
58
+ by: string;
59
+ }
60
+ export interface IObligateOutcome {
61
+ uid: string;
62
+ obligationUid: string;
63
+ }
64
+ export interface IUnobligateInput {
65
+ obligationUid: string;
66
+ by: string;
67
+ }
68
+ export interface IUnobligateOutcome {
69
+ obligationUid: string;
70
+ invalidated: true;
71
+ }
72
+ /**
73
+ * The three leaf questions the grammar can ask. Implemented by each caller's
74
+ * own resolver — C5's tx-backed `evaluateTransitionGateTx` and C6's verdict
75
+ * resolver — because the leaf lookups are the ONLY part that touches storage;
76
+ * the composition is {@link evaluatePredicate}'s job alone.
77
+ */
78
+ export interface IPredicateResolver {
79
+ /** Distinct attestations of `claim.kind === kind` whose `check.state === 'verified'`. */
80
+ countVerifiedAttestations(kind: string): Promise<number>;
81
+ /** `true` iff every incoming live `blocks` edge's source issue is terminal; a missing status is non-terminal (fail-closed). */
82
+ blockersAllTerminal(): Promise<boolean>;
83
+ /** `true` iff a live edge of `type` exists with the subject as `dst` (`in`) or `src` (`out`). */
84
+ relationExists(type: string, direction: IRelationDirection): Promise<boolean>;
85
+ }
86
+ /**
87
+ * Recursive validator over the closed core — throws {@link InvalidPredicateError}
88
+ * on an unknown `op`, a missing/ill-typed leaf, an unexpected extra key, a
89
+ * non-array `all_of`/`any_of` `of`, or `evidence.min < 1`. Runs BEFORE any
90
+ * transaction opens.
91
+ */
92
+ export declare function assertValidPredicate(pred: unknown): asserts pred is IPredicate;
93
+ /**
94
+ * Pure evaluator — the ONE implementation of the grammar's semantics, shared
95
+ * by the C5 write gate (tx-backed resolver) and the C6 verdict (graph-backed
96
+ * resolver). NOT re-implemented per caller (adhd ADR-0002).
97
+ *
98
+ * **Documented short-circuits:** an empty `all_of` is `true`, an empty `any_of`
99
+ * is `false`, and `not` of an empty `any_of` is therefore `true`.
100
+ *
101
+ * **Fail-closed:** a resolver rejection propagates — it is never coerced to
102
+ * `true`. A gate that cannot determine a leaf must refuse, not pass.
103
+ */
104
+ export declare function evaluatePredicate(pred: IPredicate, resolver: IPredicateResolver): Promise<boolean>;
105
+ /**
106
+ * Declare an obligation on a live issue. One `immediate` transaction: resolve
107
+ * the issue (`resolveLiveIssueTx`), write the `obligation` node, resolve
108
+ * `has_obligation`, write the edge, and audit `'obligated'`. The subject issue
109
+ * is NEVER mutated — no column, no `meta` touch, no stored status.
110
+ *
111
+ * Validation (`assertValidPredicate`, `applies_to.to` required, `on_fail` ∈
112
+ * `{block,warn}`, override shape) runs BEFORE `executeWriteTransaction`, so a
113
+ * malformed predicate never holds the write lock.
114
+ *
115
+ * Errors: `InvalidArgumentError` (blank `uid`/`by`, omitted/blank
116
+ * `applies_to.to`, bad `on_fail`, malformed override),
117
+ * `InvalidPredicateError` (the closed-core grammar), `IssueNotFoundError`,
118
+ * `StaleSupersedeError`, `WriteContentionError`/`WriteIOError` (§4c).
119
+ */
120
+ export declare function obligate(handle: IWriteStoreHandle, input: IObligateInput): Promise<IObligateOutcome>;
121
+ /**
122
+ * Retire an obligation. One `immediate` transaction: resolve `obligationUid` to
123
+ * a live `obligation` node (else {@link ObligationNotFoundError}),
124
+ * soft-invalidate it (`UPDATE node SET t_invalid = ?, meta = <merged
125
+ * invalidatedAt/invalidatedReason> WHERE rowid = ?` — the `delete.ts` /
126
+ * `rmLocation` bi-temporal shape), invalidate the owning `has_obligation` edge,
127
+ * and audit `'unobligated'`. Never a hard delete — the row and its trail stay
128
+ * readable; it simply stops appearing on the card.
129
+ *
130
+ * Errors: `InvalidArgumentError` (blank `obligationUid`/`by`),
131
+ * `ObligationNotFoundError`, `WriteContentionError`/`WriteIOError` (§4c).
132
+ */
133
+ export declare function unobligate(handle: IWriteStoreHandle, input: IUnobligateInput): Promise<IUnobligateOutcome>;
package/write/relate.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
2
 
3
3
  /** The closed `rel` union `relate` accepts (§3/§6.3.6) — issue → issue in every case. */
4
- export type RelateRel = 'relates_to' | 'supersedes' | 'blocks' | 'duplicate_of' | 'part_of';
4
+ export type RelateRel = 'relates_to' | 'supersedes' | 'blocks' | 'duplicate_of' | 'part_of' | 'similar_to';
5
5
  export interface IRelateInput {
6
6
  /** The relation's SOURCE `issue` uid (§1: uid is the only identifier — never a name, never repo-scoped). */
7
7
  sourceUid: string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * revision.ts — the issue content-revision counter (DESIGN §2 Primitive 1/4).
3
+ *
4
+ * One revision counter, one name: `subject.revision` (Primitive 2) and
5
+ * `source_revision` (Primitive 4) are the SAME quantity — a monotonic counter
6
+ * on the issue's own `meta.revision`, bumped on every mutating write. A check
7
+ * or a verdict is stale relative to this one counter, so it must be read
8
+ * through one function rather than each consumer re-deriving it.
9
+ *
10
+ * This module is pure (no store, no I/O): it reads only the `meta` blob a
11
+ * caller already has in hand. `C6`'s verdict layer owns the bump sites
12
+ * (`create-issue` seeds `revision: 0`; `update`/`transition`/`claim`/`relate`/
13
+ * `move` set `nextRevision(priorMeta)`); `attest`/`recheck` deliberately do
14
+ * NOT bump, because they never touch the subject.
15
+ *
16
+ * A never-mutated issue (no `meta.revision` at all, e.g. every issue created
17
+ * before the counter existed) reads as `0`, so the pre-counter corpus is
18
+ * consistent without a backfill.
19
+ */
20
+ /**
21
+ * The monotonic revision of an issue's content — `0` for a never-mutated
22
+ * issue. A non-finite or non-numeric stored value (a malformed `meta` blob)
23
+ * degrades to `0` rather than propagating `NaN` into a comparison.
24
+ */
25
+ export declare function readRevision(meta: Record<string, unknown> | undefined): number;
26
+ /** `readRevision(meta) + 1` — the revision a mutating write stamps onto its node. */
27
+ export declare function nextRevision(meta: Record<string, unknown> | undefined): number;
@@ -0,0 +1,84 @@
1
+ import { StoreSearchBackend } from '@adhd/sox-hybrid-search';
2
+ import { GraphBackend } from '@adhd/sox-graph-store';
3
+
4
+ /** The search/graph substrate the scan needs — structurally compatible with the create gate's `IDuplicateScanHandle`. */
5
+ export interface ISimilarityScanHandle {
6
+ readonly graph?: GraphBackend;
7
+ readonly search?: {
8
+ readonly backend: StoreSearchBackend;
9
+ embedQuery?(text: string): Promise<Float32Array>;
10
+ };
11
+ }
12
+ /** Why the scan could not produce a calibrated comparison — never a generic "unavailable". */
13
+ export type SimilarityScanDegradedReason = 'no-search-backend' | 'no-embed-query' | 'no-vector-scores';
14
+ /** One advisory similarity candidate, with the provenance a reviewer needs to judge it across projects. */
15
+ export interface ISimilarCandidate {
16
+ uid: string;
17
+ title: string;
18
+ /** Cosine similarity in `[0,1]`, straight off the vector channel. */
19
+ score: number;
20
+ scope: 'same-project' | 'cross-project';
21
+ provenance?: {
22
+ projectUid: string;
23
+ projectName: string;
24
+ componentUid?: string;
25
+ repoUrl?: string;
26
+ };
27
+ /** Which independent signals fired, e.g. `['cosine','title-tokens']`. */
28
+ signals?: string[];
29
+ }
30
+ export interface ISimilarityScanInput {
31
+ title: string;
32
+ body: string;
33
+ scope: 'same-project' | 'multi-project' | 'store-wide';
34
+ /** Required for `same-project`/`multi-project`: the filing project. */
35
+ projectUid?: string;
36
+ sameProjectThreshold: number;
37
+ crossProjectThreshold: number;
38
+ margin: number;
39
+ tokenOverlapMin: number;
40
+ limit?: number;
41
+ /**
42
+ * The filing item's own citation tokens / component path — the "A" side of
43
+ * the structural signal. Optional: absent means the structural signal can
44
+ * only ever fire on the candidate's side, which is the correct default for a
45
+ * caller with no new-item context (the view cluster scan).
46
+ */
47
+ citationTokens?: readonly string[];
48
+ componentPath?: string;
49
+ /** Ids the scan must EXCLUDE (the declared `dedupeExcludeUid` parent + its `part_of` ancestor chain). */
50
+ excludeIds?: ReadonlySet<number>;
51
+ }
52
+ /** The richer outcome {@link scanSimilarCandidatesWithMeta} returns; {@link scanSimilarCandidates} is its candidates-only projection. */
53
+ export interface ISimilarityScanOutcome {
54
+ candidates: ISimilarCandidate[];
55
+ degraded: boolean;
56
+ degradedReason?: SimilarityScanDegradedReason;
57
+ }
58
+ /**
59
+ * Citation-derived structural tokens + owning component path for one issue.
60
+ *
61
+ * Exported so BOTH scan callers can supply the A-side of the AC7 structural
62
+ * signal from a real stored item: the `view:'similar'` cluster block derives
63
+ * its seed's context with this directly (the seed is already written), while
64
+ * the create-time gate derives the not-yet-written filing item's context from
65
+ * its input in `write/create-issue.ts`.
66
+ */
67
+ export declare function structuralContextFor(graph: GraphBackend, issueId: number): Promise<{
68
+ citationTokens: string[];
69
+ componentPath?: string;
70
+ }>;
71
+ /**
72
+ * The scan, with its degraded-mode metadata. Returns `{candidates:[], degraded:false}`
73
+ * when the scope is a COMPLETE scan of nothing (the scope resolves to an empty
74
+ * candidate set, or every candidate was excluded); returns a degraded outcome
75
+ * when there ARE rows to compare against but the calibrated (vector) channel
76
+ * could not answer — the distinction the create gate's `abort` fails closed on.
77
+ */
78
+ export declare function scanSimilarCandidatesWithMeta(handle: ISimilarityScanHandle, input: ISimilarityScanInput): Promise<ISimilarityScanOutcome>;
79
+ /**
80
+ * The candidates-only projection of {@link scanSimilarCandidatesWithMeta}
81
+ * (the C9 spec's stated signature): best-first, advisory, never writes, and
82
+ * `[]` when the embedding substrate is absent.
83
+ */
84
+ export declare function scanSimilarCandidates(handle: ISimilarityScanHandle, input: ISimilarityScanInput): Promise<ISimilarCandidate[]>;
@@ -0,0 +1,131 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { GraphBackend, NodeRecord } from '@adhd/sox-graph-store';
3
+
4
+ /** The long-form file export an anchor points at: `locator + digest` (Primitive 2) — referenced, never embedded. */
5
+ export interface ISpecAnchor {
6
+ locator: string;
7
+ digest: string;
8
+ }
9
+ /**
10
+ * Immutable revision object metadata (node `kind: 'SPEC'`). The node's
11
+ * `content` column holds the FRAGMENT appended at this revision (the delta; the
12
+ * base revision's fragment IS the base document). No revision stores a full
13
+ * snapshot — the document is the fold over the chain's `content` values and
14
+ * `revision_token` is the hash of that fold.
15
+ */
16
+ export interface ISpecRevisionMeta {
17
+ /** The ticket's logical id (the head resolved at append time) — see the module header. */
18
+ spec_of: string;
19
+ /** 1,2,3… monotonic within the spec. */
20
+ revision_seq: number;
21
+ /** `'sha256:<hex>'` of the FOLDED document at this revision. */
22
+ revision_token: string;
23
+ /** Predecessor revision uid (`null` for the base revision). */
24
+ prev_revision: string | null;
25
+ /** The long-form file export (Primitive 2). */
26
+ anchor: ISpecAnchor;
27
+ appended_by: string;
28
+ appended_at: string;
29
+ }
30
+ export interface ISpecAppendInput {
31
+ /** The ticket's logical id — any uid on its `SUPERSEDES` chain is resolved forward. */
32
+ uid: string;
33
+ /** The delta appended at this revision (becomes the node's `content`). */
34
+ fragment: string;
35
+ /** The file export for this revision. */
36
+ anchor?: ISpecAnchor;
37
+ /** REQUIRED CAS token — the revision uid the caller read (`''` when the ticket had no spec yet). */
38
+ base_revision: string;
39
+ by: string;
40
+ }
41
+ export interface ISpecAppendOutcome {
42
+ /** UNCHANGED — cross-checked by AC1. */
43
+ uid: string;
44
+ /** The NEW revision uid. */
45
+ spec_revision: string;
46
+ /** `'sha256:<hex>'`. */
47
+ spec_revision_token: string;
48
+ revision_seq: number;
49
+ }
50
+ /** Current revision + its token for a work item. */
51
+ export interface ISpecPointer {
52
+ revision_uid: string;
53
+ revision_token: string;
54
+ revision_seq: number;
55
+ }
56
+ /**
57
+ * The store surface {@link appendSpecRevision} needs. Beyond the ordinary write
58
+ * handle it requires the store's `GraphBackend`, because revision DISCOVERY must
59
+ * run the SAME live-kind traversal the read path (`deriveSpecHead`) and the
60
+ * reconciliation (`spec-revision.reconcile.ts`) use — and that traversal
61
+ * (`query/resolve.ts`'s `resolveEdgeScopedCandidates`) is a `GraphBackend` read
62
+ * that must run BEFORE the `BEGIN IMMEDIATE` transaction opens (a `GraphBackend`
63
+ * call never runs inside it — see `resolveLogicalHeadTx`). `GraphBacklogStore`
64
+ * and the test `TestIssueStore` both satisfy this shape.
65
+ */
66
+ export type ISpecAppendStore = IWriteStoreHandle & {
67
+ readonly graph: GraphBackend;
68
+ };
69
+ /**
70
+ * Rowids of live items whose DECLARED catalog kind is `SPEC` — an
71
+ * `issue`-shaped node carrying a live `has_kind` edge to the `kind:'SPEC'`
72
+ * catalog row. This is the SHIPPED production shape: the reconciled corpus is
73
+ * `issue` nodes whose DECLARED kind is `SPEC`, and reconciliation stamps them
74
+ * `spec_of`/`revision_seq` WITHOUT changing the raw `node.kind` column. Any
75
+ * discovery keyed on the raw column alone is therefore blind to them.
76
+ *
77
+ * Uses the SAME live-kind traversal the read path's `filter.kind`
78
+ * (`query/views/semantic.ts`) and `spec-revision.reconcile.ts` use —
79
+ * `query/resolve.ts`'s {@link resolveEdgeScopedCandidates} on
80
+ * `has_kind`→`SPEC` — so read, write, and reconcile can never disagree on what
81
+ * "a live `kind:'SPEC'` item" is. Returns `undefined` when `SPEC` resolves to
82
+ * no live catalog row (nothing is declared SPEC).
83
+ */
84
+ export declare function declaredSpecRowids(graph: GraphBackend): Promise<ReadonlySet<number> | undefined>;
85
+ /**
86
+ * Every live SPEC document — the UNION of two representations the store has
87
+ * held, both of which are specs:
88
+ *
89
+ * (a) a node whose RAW `kind` column is `SPEC` — the immutable revision object
90
+ * {@link appendSpecRevision} mints (and the shape the unit fixtures
91
+ * build); and
92
+ * (b) an `issue` node whose DECLARED catalog kind is `SPEC` — the reconciled
93
+ * production corpus (see {@link declaredSpecRowids}).
94
+ *
95
+ * This is the ONE discovery the read path ({@link deriveSpecHead}), the write
96
+ * path (`findChainHeadTx` via {@link declaredSpecRowids}), and the
97
+ * reconciliation all share, so they cannot drift on which items are specs. The
98
+ * union is load-bearing: a raw-kind-only reader missed shape (b) entirely.
99
+ */
100
+ export declare function discoverLiveSpecNodes(graph: GraphBackend): Promise<NodeRecord[]>;
101
+ /**
102
+ * Append a revision. One `BEGIN IMMEDIATE` transaction (ADR-0001 + ADR-0012 —
103
+ * no temp-file/rename/flock; atomicity is the store's job):
104
+ *
105
+ * 1. resolve `uid` → logical head (input may be any `SUPERSEDES`-chain uid — AC9);
106
+ * 2. read the current revision; if it is not `base_revision` →
107
+ * {@link SpecRevisionConflictError} (stale base — no write; AC6);
108
+ * 3. write a NEW `kind:'SPEC'` node holding the fragment (AC1/AC8);
109
+ * 4. advance the ticket's `meta.spec_revision` IN PLACE via the CAS helper,
110
+ * bumping `meta.revision` — the uid is preserved (AC1);
111
+ * 5. `writeAudit(action:'spec-appended')`.
112
+ *
113
+ * NEVER update or delete a revision node (AC1/AC8).
114
+ */
115
+ export declare function appendSpecRevision(handle: ISpecAppendStore, input: ISpecAppendInput): Promise<ISpecAppendOutcome>;
116
+ /**
117
+ * The derived chain head for a ticket — a live `SPEC` node whose
118
+ * `meta.spec_of` is in the ticket's `SUPERSEDES`-chain uid set with the max
119
+ * `meta.revision_seq` (read-path analogue of {@link findChainHeadTx}, backed by
120
+ * the same reconciliation-created functional index). `undefined` when the ticket has
121
+ * no live revision.
122
+ */
123
+ export declare function deriveSpecHead(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
124
+ /** The pointer ON THE RECORD only (`meta.spec_revision`) — never the derived head. Used by the staleness cross-check. */
125
+ export declare function readPointerRecord(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
126
+ /**
127
+ * Current revision + its token for a work item (`undefined` if it has no spec).
128
+ * Prefers the on-record pointer; falls back to the derived chain head so a
129
+ * ticket whose pointer was not (yet) written — pre-reconciliation data — still reads.
130
+ */
131
+ export declare function readSpecPointer(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
@@ -0,0 +1,48 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { GraphBackend } from '@adhd/sox-graph-store';
3
+
4
+ /** The store surface the reconciliation needs — satisfied by `GraphBacklogStore` and the test store alike. */
5
+ export type ISpecReconcileStore = IWriteStoreHandle & {
6
+ readonly graph: GraphBackend;
7
+ };
8
+ export interface ISpecReconcileStamp {
9
+ revisionUid: string;
10
+ /** The logical id stamped onto `meta.spec_of` (the ticket's head). */
11
+ specOf: string;
12
+ revisionSeq: number;
13
+ prevRevision: string | null;
14
+ revisionToken: string;
15
+ /** `false` when the node already carries exactly this stamp (skip the write). */
16
+ needsStamp: boolean;
17
+ }
18
+ export interface ISpecReconcileHead {
19
+ ticketUid: string;
20
+ revisionUid: string;
21
+ revisionToken: string;
22
+ revisionSeq: number;
23
+ /** `false` when the ticket's pointer already names this head with this token. */
24
+ needsPointer: boolean;
25
+ }
26
+ export interface ISpecReconcileReport {
27
+ /** How many live `SPEC` nodes were discovered. */
28
+ scanned: number;
29
+ stamps: ISpecReconcileStamp[];
30
+ heads: ISpecReconcileHead[];
31
+ /** `SPEC` nodes already stamped (idempotent no-op). */
32
+ alreadyStamped: number;
33
+ /** `SPEC` nodes with no live `part_of` parent — left untouched, reported. */
34
+ orphaned: string[];
35
+ /** Whether the lookup index exists after this call. */
36
+ indexCreated: boolean;
37
+ }
38
+ /**
39
+ * Compute the reconciliation plan WITHOUT writing (the dry-run). Discovery is
40
+ * `query`-driven; nothing is mutated.
41
+ */
42
+ export declare function planSpecRevisionReconcile(store: ISpecReconcileStore): Promise<ISpecReconcileReport>;
43
+ /**
44
+ * Apply the reconciliation. Idempotent: an already-stamped `SPEC` and an
45
+ * already-pointed ticket are both skipped, so a second run is a no-op. One
46
+ * `BEGIN IMMEDIATE` transaction (ADR-0001) — never a hard delete.
47
+ */
48
+ export declare function applySpecRevisionReconcile(store: ISpecReconcileStore): Promise<ISpecReconcileReport>;
@@ -1,5 +1,5 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
- import { ICitationInput } from './create-issue.js';
2
+ import { ICitation } from '../citation.js';
3
3
 
4
4
  export interface ITransitionInput {
5
5
  /** The `issue` uid to transition (§6.3, an "Issue verb"). */
@@ -11,7 +11,7 @@ export interface ITransitionInput {
11
11
  /** REQUIRED unless `project_policy.transition_requires_note` is `false` (default `true`) — optional in the type; enforced at runtime (`NoteRequiredError`), never at the TS level. */
12
12
  note?: string;
13
13
  /** REQUIRED (≥1) when `project_policy.citation_required` is `true` AND `toStatus` resolves to a terminal status. Written as `citation` nodes + `has_citation` edges exactly like `create`'s own citations. */
14
- citations?: ICitationInput[];
14
+ citations?: ICitation[];
15
15
  /**
16
16
  * The item-level disclosure-contract git context — the SAME plain metadata
17
17
  * scalar `create`'s own `gitContext` field writes (repo `AGENTS.md`'s "Cite
@@ -22,6 +22,17 @@ export interface ITransitionInput {
22
22
  * per-citation `ref` — it rides the issue, never a `citation` node.
23
23
  */
24
24
  gitContext?: string;
25
+ /**
26
+ * A recorded override of a refusing obligation (C5, DESIGN §2 Primitive 3).
27
+ * Honoured ONLY when the caller is in the refusing obligation's
28
+ * `override.actors`; a reason is ALWAYS required (never a configurable
29
+ * boolean) and is recorded on the audit row. A claimed override by a
30
+ * non-listed actor, or one with a blank reason, throws
31
+ * `OverrideNotPermittedError`.
32
+ */
33
+ override?: {
34
+ reason: string;
35
+ };
25
36
  }
26
37
  export interface ITransitionOutcome {
27
38
  uid: string;
package/write/tx.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { INodeReadExecutor } from './uid-prefix.js';
1
2
  import { BacklogWriteError } from './errors.js';
2
3
  import { TypePolicy } from '@adhd/sox-graph-store';
3
4
  import { AdapterTransaction, StoreAdapter } from '@adhd/sox-store-adapter';
@@ -98,7 +99,28 @@ export declare function canonicalJSONStringify(value: Record<string, unknown>):
98
99
  * issued against `tx` instead of the bare adapter — never a `getNodeByUid`
99
100
  * call itself, which always runs against `this.adapter` (§4c).
100
101
  */
101
- export declare function getNodeByUidTx(tx: AdapterTransaction, uid: string): Promise<ITxNodeRow | null>;
102
+ export declare function getNodeByUidTx(tx: INodeReadExecutor, uid: string): Promise<ITxNodeRow | null>;
103
+ /**
104
+ * uid reference → LIVE `issue`/catalog node row, inside a tx, accepting a
105
+ * UNIQUE uid PREFIX as well as an exact uid. This is the single write-side
106
+ * funnel every issue verb ({@link resolveLiveIssueTx}) and catalog
107
+ * uid-resolution (`catalog.ts`'s `resolveByUidTx`, hence `rm-location`) goes
108
+ * through, so the prefix contract is defined once rather than per verb.
109
+ *
110
+ * Exact match is the fast path and keeps winning: a full uid is looked up
111
+ * directly, and only when that misses is a prefix scan attempted. A prefix
112
+ * below `MIN_UID_PREFIX_LENGTH` throws an "too short" argument error; a prefix
113
+ * that matches zero rows throws the kind-appropriate not-found; a prefix that
114
+ * matches two or more throws {@link AmbiguousReferenceError} naming every
115
+ * candidate — never an arbitrary pick.
116
+ *
117
+ * @throws IssueNotFoundError (kind `issue`) / CatalogNotFoundError (any other
118
+ * kind) when nothing live matches.
119
+ * @throws AmbiguousReferenceError when a prefix matches more than one live row.
120
+ */
121
+ export declare function resolveUidPrefixTx(exec: INodeReadExecutor, ref: string, opts?: {
122
+ expectedKind?: string;
123
+ }): Promise<ITxNodeRow>;
102
124
  /**
103
125
  * Resolves `uid` to the CURRENT, live `issue` row, or throws.
104
126
  *
@@ -140,7 +162,7 @@ export declare function resolveLiveIssueTx(tx: AdapterTransaction, uid: string):
140
162
  /** rowid → node, inside a tx (used to resolve an edge endpoint's kind without a redundant round trip when the caller doesn't already know it). */
141
163
  export declare function getNodeByRowidTx(tx: AdapterTransaction, rowid: number): Promise<ITxNodeRow | null>;
142
164
  export interface IWriteNodeTxInput {
143
- /** The entity-type discriminator — `project`/`component`/`location`/`issue`/`kind`/`edge_kind`/`status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit` (§3). NEVER validated against a closed vocabulary here — the schema is open by design (§0 anti-antipattern 3); the write layer is the only composer of these literals. */
165
+ /** The entity-type discriminator — `project`/`component`/`location`/`issue`/`kind`/`edge_kind`/`status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit`/`attestation`/`obligation`/`SPEC` (§3; the SPEC revision kind is added by C10, DESIGN §12). NEVER validated against a closed vocabulary here — the schema is open by design (§0 anti-antipattern 3); the write layer is the only composer of these literals. */
144
166
  kind: string;
145
167
  /** The business name (`issue.title`, a catalog row's name, …). Omit for a node with no name (none currently exist in §3's table, but the column is nullable). */
146
168
  name?: string;
@@ -306,6 +328,26 @@ export interface IInvalidateEdgeTxInput {
306
328
  * instead of the bare adapter.
307
329
  */
308
330
  export declare function invalidateEdgeTx(tx: AdapterTransaction, input: IInvalidateEdgeTxInput): Promise<void>;
331
+ /**
332
+ * Update a LIVE node's `meta` IN PLACE inside `tx`, CAS-guarded on the node's
333
+ * own `meta.revision` counter (SR-2 monotonic revision + SR-6 per-node CAS,
334
+ * per DESIGN §2 Primitive 1's "node's monotonic `revision`, bumped on every
335
+ * mutating write"). Preserves `uid` — this is the ONLY write path that mutates
336
+ * a node without superseding it, and it exists solely for the C10 spec pointer
337
+ * (`meta.spec_revision`) and the SR-2 counter it bumps. No other verb may adopt
338
+ * it without a new decision.
339
+ *
340
+ * `patch`'s keys are shallow-merged over the node's existing meta; the merged
341
+ * blob's `revision` is set to `expectedRevision + 1`. Returns the new revision
342
+ * number on success, or `null` when the node is missing OR the stored revision
343
+ * no longer equals `expectedRevision` (a concurrent writer won the CAS — the
344
+ * caller must surface a stale-base error, and because this runs inside the
345
+ * caller's own `BEGIN IMMEDIATE` transaction, a thrown error rolls the whole
346
+ * transaction back). A node with no `meta.revision` at all reads as revision
347
+ * `0` (matching `revision.ts`'s `readRevision`), so a pre-counter row is
348
+ * CAS-addressable without a backfill.
349
+ */
350
+ export declare function updateNodeMetaTx(tx: AdapterTransaction, uid: string, patch: Record<string, unknown>, expectedRevision: number): Promise<number | null>;
309
351
  /**
310
352
  * The one transaction wrapper every write verb calls — `store.adapter.transaction(fn,
311
353
  * {mode: resolveTransactionMode()})`, which is `'immediate'` (§4c: `BEGIN
@@ -0,0 +1,78 @@
1
+ import { InvalidArgumentError } from './errors.js';
2
+
3
+ /** Whether `ref` is an exact, full uid (resolve-or-throw), never a business `name` and never a prefix. */
4
+ export declare function isUidShaped(ref: string): boolean;
5
+ /**
6
+ * The shortest uid prefix this resolver will accept. Eight hex characters —
7
+ * the first UUID block, the human-copyable unit, and 32 bits of address space;
8
+ * see this file's header for the full justification.
9
+ */
10
+ export declare const MIN_UID_PREFIX_LENGTH = 8;
11
+ /**
12
+ * Whether `ref` is a proper prefix (length `>=` {@link MIN_UID_PREFIX_LENGTH},
13
+ * `< 36`) of some canonical uid string — hex characters with hyphens only at
14
+ * the canonical boundary positions.
15
+ */
16
+ export declare function isUidPrefixShaped(ref: string): boolean;
17
+ /** How a caller-supplied reference reads: an exact uid, a uid prefix, a too-short uid attempt, or not uid-shaped at all. */
18
+ export type UidRefKind = 'exact' | 'prefix' | 'too-short' | 'not-uid';
19
+ /**
20
+ * Classify `ref` for uid resolution. `too-short` is a uid ATTEMPT (hex/hyphen
21
+ * only) below {@link MIN_UID_PREFIX_LENGTH} — a strict uid context rejects it
22
+ * as too short, while a uid-or-name context falls back to a name lookup, so a
23
+ * genuine 6-character value that is itself a valid name is never swallowed.
24
+ */
25
+ export declare function classifyUidRef(ref: string): UidRefKind;
26
+ /** One live node matching a uid PREFIX, projected to the fields the ambiguity decision and its error need. */
27
+ export interface IUidCandidate {
28
+ uid: string;
29
+ kind: string;
30
+ name: string | null;
31
+ isSuperseded: boolean;
32
+ }
33
+ /**
34
+ * The minimal executor a prefix candidate read needs. Structural, so it is
35
+ * satisfied by `AdapterTransaction`, graph-store's `GraphTransaction`, and a
36
+ * bare `StoreAdapter` alike — nothing here depends on which one it is.
37
+ */
38
+ export interface IUidPrefixQueryExecutor {
39
+ executeAll<T = Record<string, unknown>>(sql: string, args?: unknown[]): Promise<{
40
+ rows: T[];
41
+ }>;
42
+ }
43
+ /**
44
+ * The executor a uid resolution needs: the prefix candidate read plus the
45
+ * exact single-row read. Structural, so `AdapterTransaction`,
46
+ * graph-store's `GraphTransaction`, and a bare `StoreAdapter` all satisfy it.
47
+ */
48
+ export interface INodeReadExecutor extends IUidPrefixQueryExecutor {
49
+ executeGet<T = Record<string, unknown>>(sql: string, args?: unknown[]): Promise<T | null>;
50
+ }
51
+ /**
52
+ * Every LIVE node whose uid starts with `prefix`.
53
+ *
54
+ * The range predicate `uid >= ? AND uid < ?` is an indexed prefix scan (the
55
+ * store keeps a unique index on `uid`) and, unlike a `LIKE` pattern, is
56
+ * case-exact against the lowercase uids the store mints — so the input is
57
+ * lower-cased once here and the caller never has to reason about collation.
58
+ * Soft-deleted rows are excluded in SQL (`t_invalid IS NULL`); a superseded
59
+ * row is returned (its `is_superseded` flag carried) so a caller that resolves
60
+ * to it can raise the same stale-reference error the exact path does.
61
+ */
62
+ export declare function queryLiveUidPrefixCandidates(exec: IUidPrefixQueryExecutor, prefix: string): Promise<IUidCandidate[]>;
63
+ /**
64
+ * The ambiguity decision, shared by both paths. Returns the sole candidate, or
65
+ * `null` when there are none (the caller owns the not-found error, which
66
+ * differs by kind). Two or more candidates throw {@link AmbiguousReferenceError}
67
+ * — the resolver never auto-selects.
68
+ */
69
+ export declare function selectUniqueUidCandidate(ref: string, candidates: readonly IUidCandidate[]): IUidCandidate | null;
70
+ /**
71
+ * The not-found error for a uid reference, keyed by the kind the caller asked
72
+ * for. `asPrefix` selects the prefix-specific wording ("no item matches")
73
+ * versus the exact-uid wording, so a caller can tell "this short reference
74
+ * matched nothing" from "this full uid is not here."
75
+ */
76
+ export declare function missingUidError(expectedKind: string | undefined, ref: string, asPrefix: boolean): Error;
77
+ /** The too-short refusal — a uid attempt below {@link MIN_UID_PREFIX_LENGTH}. */
78
+ export declare function tooShortUidError(ref: string): InvalidArgumentError;
package/write/update.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
+ import { ICitation } from '../citation.js';
2
3
 
3
4
  export interface IUpdateIssueInput {
4
5
  /** The `issue` uid to update (§6.3, an "Issue verb"). */
@@ -17,6 +18,27 @@ export interface IUpdateIssueInput {
17
18
  assignee?: string;
18
19
  /** catalog agent name/uid; → `touch` + `authored_by` edge rewrite. An unresolved NAME mints; a uid-shaped ref that does not resolve throws `CatalogNotFoundError('agent', ref)`. */
19
20
  author?: string;
21
+ /**
22
+ * The DESIRED set of citations this issue should carry after this call — a
23
+ * DIFF-EMITTER, never a body rewrite (the architecture verdict): the live
24
+ * citation set is read inside the SAME `immediate` transaction, citations
25
+ * present live but absent from `citations` are removed (bi-temporally), and
26
+ * citations in `citations` but not live are added. Identity for the diff is
27
+ * `(file, lines, revision)` (§ `citationKey`), never `sha`/`context`.
28
+ *
29
+ * Routing a citation change through `body`/`supersede` is FORBIDDEN: that
30
+ * would mint a new issue uid and violate §1's "uid is the only identity".
31
+ * When `body` IS also given, the supersede mints the new node first, its
32
+ * live citations are carried forward, and THIS diff is applied against the
33
+ * carried-forward set on the new node — still inside the one transaction.
34
+ *
35
+ * Each citation's `sha` is computed (and policy-gated, exactly as `create`/
36
+ * `transition` do) before the transaction opens, so the write lock is never
37
+ * held across a filesystem/git read. `citations: []` clears every live
38
+ * citation. Omitted entirely ⇒ citations are untouched (and carried forward
39
+ * verbatim on a body edit).
40
+ */
41
+ citations?: ICitation[];
20
42
  /**
21
43
  * §4b/§6.2 — waits for the fire-and-forget on-write embedding round-trip
22
44
  * before `update` returns, when `true` and `handle.embedding` is
@@ -30,7 +52,7 @@ export interface IUpdateIssueInput {
30
52
  */
31
53
  awaitEmbed?: boolean;
32
54
  }
33
- export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author';
55
+ export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author' | 'citations';
34
56
  export interface IUpdateIssueOutcome {
35
57
  /**
36
58
  * The resulting CURRENT issue's uid — the SAME `input.uid` when `body` was