@adhd/backlog 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhd/backlog",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "bin": {
5
5
  "adhd-backlog": "index.js"
6
6
  },
package/query/query.d.ts CHANGED
@@ -47,6 +47,18 @@ export interface IQueryStoreHandle {
47
47
  * open) so a long-lived process notices a store rewritten under it.
48
48
  */
49
49
  readonly assertVocabulary?: () => Promise<void>;
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.
60
+ */
61
+ readonly assertCatalogInvariants?: () => Promise<void>;
50
62
  }
51
63
  /**
52
64
  * Whether a `query` verb's own input touches the semantic channel at all —
@@ -0,0 +1,62 @@
1
+ import { StoreAdapter } from '@adhd/sox-store-adapter';
2
+
3
+ /**
4
+ * One way the catalog can violate an invariant. Carried as structured data on
5
+ * {@link CatalogInvariantError} so a caller (a diagnostic, a test) can act on
6
+ * the offending rows without parsing the rendered message.
7
+ */
8
+ export type CatalogInvariantViolation = {
9
+ kind: 'unflagged-terminal';
10
+ /** The row's own name, verbatim. */
11
+ name: string;
12
+ /** The row's uid — the handle a reader uses to inspect or repair it. */
13
+ uid: string;
14
+ } | {
15
+ kind: 'case-fragment-duplicate';
16
+ /** The case-folded name the rows collide on. */
17
+ fold: string;
18
+ /** Every colliding spelling, in rowid order (≥ 2). */
19
+ names: readonly string[];
20
+ /** The colliding rows' uids, aligned index-for-index with {@link names}. */
21
+ uids: readonly string[];
22
+ };
23
+ /**
24
+ * Render a violation list into the self-explaining message the guard throws.
25
+ * Exported so a diagnostic can reuse the exact wording the error carries.
26
+ */
27
+ export declare function renderCatalogInvariantViolations(violations: ReadonlyArray<CatalogInvariantViolation>): string;
28
+ /**
29
+ * The status/priority catalog violates an invariant this build's read layer
30
+ * depends on. Carries the offending rows as structured fields so a caller can
31
+ * render them; the message itself names every row AND the repair.
32
+ */
33
+ export declare class CatalogInvariantError extends Error {
34
+ readonly violations: ReadonlyArray<CatalogInvariantViolation>;
35
+ constructor(violations: ReadonlyArray<CatalogInvariantViolation>);
36
+ }
37
+ /**
38
+ * Read every live status/priority row and return the invariant violations.
39
+ * A pure read — it never writes, so it is safe against any store, including
40
+ * the live one.
41
+ *
42
+ * BOUNDED by the catalog: exactly one statement against
43
+ * `node WHERE kind IN ('status','priority') AND t_invalid IS NULL`, folding
44
+ * each name in JS. The row set is the catalog (a handful of rows), never the
45
+ * issue graph.
46
+ */
47
+ export declare function inspectCatalogInvariants(adapter: StoreAdapter): Promise<CatalogInvariantViolation[]>;
48
+ /**
49
+ * Assert the store's status/priority catalog satisfies both invariants.
50
+ *
51
+ * Passes (no throw) on an empty store, and on a catalog whose reserved
52
+ * terminal statuses are flagged and whose same-kind names are fold-unique.
53
+ * Throws {@link CatalogInvariantError}, naming every offending row and the
54
+ * repair that resolves it, otherwise.
55
+ *
56
+ * BOUNDED BY CONSTRUCTION: this runs on every write verb (`api.ts`'s
57
+ * `writeHandle`), every query (`api.ts`'s `queryHandle` → `query.ts`), and
58
+ * store open (`openGraphBacklogStore`), so it must not scan the store per
59
+ * call. It reads only the (small) status/priority catalog — see
60
+ * {@link inspectCatalogInvariants}.
61
+ */
62
+ export declare function assertCatalogInvariants(adapter: StoreAdapter): Promise<void>;
@@ -0,0 +1,123 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { NodeRecord } from '@adhd/sox-graph-store';
3
+
4
+ /** The two catalog kinds this repair collapses (D3's whole scope). */
5
+ export type CatalogKind = 'status' | 'priority';
6
+ /**
7
+ * A status fold group that CANNOT be safely collapsed: two or more LIVE rows
8
+ * share a fold, yet none carries the write path's lowercase spelling. Merging
9
+ * would have to pick a non-lowercase canonical (or rename one to a spelling no
10
+ * write emits), and the next ordinary `create` would exact-match miss and
11
+ * re-mint the fragment. Surfaced, never merged, never renamed.
12
+ */
13
+ export interface IUnmergeableGroup {
14
+ kind: CatalogKind;
15
+ names: string[];
16
+ }
17
+ /**
18
+ * A planned merge: the fold groups to collapse, by uid. `groups` is EMPTY when
19
+ * the store has no live case-variant group — the idempotency signal.
20
+ *
21
+ * `unmergeable` carries the status groups deliberately LEFT ALONE (no
22
+ * write-spelling member). It is a diagnostic, never an action: the plan is
23
+ * incomplete-by-refusal for those groups, so a caller that treats a zero-group
24
+ * plan as "the store is clean" must also check this list.
25
+ */
26
+ export interface IMergePlan {
27
+ groups: Array<{
28
+ canonicalUid: string;
29
+ fragmentUids: string[];
30
+ }>;
31
+ unmergeable: IUnmergeableGroup[];
32
+ at: string;
33
+ }
34
+ /**
35
+ * One merged-away fragment, with everything needed to restore it. The
36
+ * `fragmentMeta`/`canonicalMetaBefore` fields carry the full prior `meta`
37
+ * objects (not just the changed keys), so reverse is byte-exact on the keys
38
+ * this repair touches and never guesses.
39
+ *
40
+ * {@link canonicalMetaBefore} is the one field beyond the required shape —
41
+ * without it a reverse cannot distinguish "canonical had no rank, adopted the
42
+ * fragment's" from "canonical's own rank was this", and cannot remove exactly
43
+ * the aliases this repair appended. It is OPTIONAL so a hand-authored minimal
44
+ * journal still type-checks; a journal written by this module always includes
45
+ * it.
46
+ */
47
+ export interface IMergeJournalEntry {
48
+ fragmentUid: string;
49
+ canonicalUid: string;
50
+ fragmentMeta: unknown;
51
+ repointedEdges: Array<{
52
+ srcRowid: number;
53
+ rel: string;
54
+ fragmentDstRowid: number;
55
+ }>;
56
+ /** The fragment's numeric `rank` at apply time, when it had one — the value the survivor did NOT adopt unless it had none of its own. */
57
+ retainedRank?: number;
58
+ /** Present only when a PRIORITY group had no upper-spelled member: the canonical row was renamed to uppercase, and this is how to undo it. A status canonical is never renamed. */
59
+ renamed?: {
60
+ uid: string;
61
+ from: string;
62
+ to: string;
63
+ };
64
+ /** The canonical row's full prior `meta`, for exact reversal of the appended aliases / adopted rank. */
65
+ canonicalMetaBefore?: unknown;
66
+ }
67
+ export interface IMergeJournal {
68
+ entries: IMergeJournalEntry[];
69
+ at: string;
70
+ }
71
+ /**
72
+ * Build the collapse plan from the LIVE catalog rows. Pure over node records —
73
+ * it never reads edges, so a fragment with more incoming edges than the
74
+ * canonical loses anyway (owner directive: spelling decides, not edge count).
75
+ *
76
+ * Grouping is by {@link catalogNameFold} alone. Within a fold group the
77
+ * canonical is the member already carrying the write path's spelling for that
78
+ * kind — lowercase for `status`, uppercase for `priority`.
79
+ *
80
+ * A `priority` group with no upper-spelled member falls back to its
81
+ * lowest-rowid member (deterministically renamed to uppercase at apply, as
82
+ * before). A `status` group with no lower-spelled member is NOT planned at all:
83
+ * any canonical would carry a spelling the write path never emits, so merging
84
+ * would regenerate the fragment on the next ordinary `create`. It is surfaced
85
+ * in `unmergeable` instead.
86
+ */
87
+ export declare function planCaseFragmentMerge(liveStatuses: readonly NodeRecord[], livePriorities: readonly NodeRecord[]): IMergePlan;
88
+ /**
89
+ * Apply the plan in ONE `executeWriteTransaction` (`BEGIN IMMEDIATE` + bounded
90
+ * busy-only retry, §4c) — every group in the plan commits together or not at
91
+ * all, so a refusal in a later group can never leave an earlier group half
92
+ * merged.
93
+ *
94
+ * For each group: re-point every incoming catalog edge onto the canonical,
95
+ * append the merged-away (and renamed-from) spellings to
96
+ * `canonical.meta.aliases`, adopt the canonical's rank (a fragment's only if
97
+ * the canonical lacks one), and invalidate each fragment with `t_invalid`.
98
+ *
99
+ * REFUSES (throws, rolling the whole apply back) any group whose fragment does
100
+ * not fold to the canonical's live name, or whose kind differs — the
101
+ * `closed`/`DONE` guard: two distinct tokens are never case-variants and can
102
+ * never be collapsed here. It also REFUSES a `status` group whose canonical is
103
+ * not already lowercase-spelled — merging INTO a spelling the write path never
104
+ * emits would let the next ordinary `create` re-mint the fragment.
105
+ *
106
+ * Only a `priority` group is ever renamed (to uppercase, when it had no
107
+ * uppercase member). A `status` `name` is NEVER rewritten.
108
+ *
109
+ * A planned fragment that is no longer LIVE (a concurrent writer invalidated
110
+ * it between plan and apply) is tolerated — it is skipped, exactly as D1's
111
+ * apply re-guards each row.
112
+ */
113
+ export declare function applyCaseFragmentMerge(handle: IWriteStoreHandle, plan: IMergePlan): Promise<IMergeJournal>;
114
+ /**
115
+ * Reverse an applied journal in ONE `executeWriteTransaction`: re-liven each
116
+ * fragment, restore its exact prior `meta`, re-point its original edges back
117
+ * off the canonical, restore the canonical's prior `meta` (dropping the
118
+ * appended aliases / adopted rank), and undo any rename.
119
+ *
120
+ * A fragment or canonical uid that no longer exists is skipped — the same
121
+ * tolerant posture D1's reverse takes for a concurrently-deleted row.
122
+ */
123
+ export declare function reverseCaseFragmentMerge(handle: IWriteStoreHandle, journal: IMergeJournal): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import { RESERVED_TERMINAL_STATUS_NAMES } from './catalog.js';
2
+ import { IWriteStoreHandle } from './tx.js';
3
+
4
+ /**
5
+ * The reserved terminal vocabulary — SINGLE SOURCE, owned by
6
+ * `write/catalog.ts` (the mint path that seeds from it) and re-exported here
7
+ * so this module's consumers keep one import site. A drift test
8
+ * (`catalog-mint-terminal.spec.ts`) asserts this is the SAME object the mint
9
+ * path uses, i.e. that exactly ONE definition exists in the codebase.
10
+ */
11
+ export { RESERVED_TERMINAL_STATUS_NAMES };
12
+ /**
13
+ * Unicode fold used by BOTH duplicate detection and JS-side grouping — never
14
+ * `SQL lower()`/`NOCASE` (ASCII-only, so it folds `A`–`Z` and nothing else).
15
+ * `NFKC` first so compatibility forms collapse, then `toLowerCase()` (the JS
16
+ * Unicode-aware case fold, mirroring PostgreSQL `citext`'s documented Unicode
17
+ * behavior) so `RESOLVED`, `resolved` and `Resolved` group together.
18
+ */
19
+ export declare function catalogNameFold(name: string): string;
20
+ /**
21
+ * The set of LIVE `status` rowids the backfill should set `terminal:true` on,
22
+ * plus the plan timestamp. Rows already `terminal:true` are EXCLUDED here (the
23
+ * idempotency guarantee: a fully-repaired store plans nothing).
24
+ */
25
+ export interface ITerminalBackfillPlan {
26
+ setTerminalRowids: number[];
27
+ at: string;
28
+ }
29
+ /**
30
+ * The apply journal — enough to restore EXACTLY the prior state. `hadTerminal`
31
+ * is the row's `meta.terminal === true` at apply time; because a row already
32
+ * `true` is never changed (and never journaled), every real entry's
33
+ * `hadTerminal` is `false`.
34
+ */
35
+ export interface ITerminalBackfillJournal {
36
+ entries: Array<{
37
+ rowid: number;
38
+ uid: string;
39
+ hadTerminal: boolean;
40
+ }>;
41
+ at: string;
42
+ }
43
+ /**
44
+ * Build the backfill plan: every LIVE `status` row whose FOLDED name is a
45
+ * reserved terminal name and whose `terminal` flag is not already `true`.
46
+ *
47
+ * Reads are plain SELECTs against the bare adapter — this is a repair planner,
48
+ * not a write verb, and it holds no lock across the (separately transactional)
49
+ * apply. A row created or renamed between plan and apply is tolerantly skipped
50
+ * by {@link applyTerminalBackfill}, which re-guards each row inside its own
51
+ * transaction.
52
+ */
53
+ export declare function planTerminalBackfill(handle: IWriteStoreHandle, at?: string): Promise<ITerminalBackfillPlan>;
54
+ /**
55
+ * Apply the plan in ONE `executeWriteTransaction` (`BEGIN IMMEDIATE` +
56
+ * busy-only bounded retry, §4c): read each target status row, guarded-parse
57
+ * its `meta`, set `terminal:true`, guarded
58
+ * `UPDATE node SET meta = ? WHERE rowid = ? AND t_invalid IS NULL`. Only the
59
+ * `terminal` flag is touched — every other `meta` key is carried through
60
+ * verbatim.
61
+ *
62
+ * A rowid that is no longer a LIVE `status` row between plan and apply is
63
+ * skipped (nothing to repair, nothing journaled); a planned rowid that IS a
64
+ * live non-`status` row is a plan/DB mismatch and throws loudly (ADR-0002 D5
65
+ * step 4's error-loudly posture) rather than silently rewriting an unrelated
66
+ * node.
67
+ */
68
+ export declare function applyTerminalBackfill(handle: IWriteStoreHandle, plan: ITerminalBackfillPlan): Promise<ITerminalBackfillJournal>;
69
+ /**
70
+ * Reverse an applied journal in ONE `executeWriteTransaction`: restore
71
+ * `meta.terminal` to its recorded `hadTerminal` for each entry — and NOTHING
72
+ * else. A rowid that is no longer a LIVE `status` row is skipped.
73
+ */
74
+ export declare function reverseTerminalBackfill(handle: IWriteStoreHandle, journal: ITerminalBackfillJournal): Promise<void>;
@@ -56,12 +56,81 @@ export interface IMintOrResolveInput {
56
56
  */
57
57
  at?: string;
58
58
  }
59
+ /**
60
+ * The reserved TERMINAL status vocabulary — the ONE in-code definition
61
+ * (SPEC.md §2: `status.terminal` drives closedness). Modelled directly on
62
+ * {@link EDGE_KIND_TABLE} further down this file: a frozen in-code table the
63
+ * mint layer seeds from, exactly as {@link resolveEdgeKindTx} seeds
64
+ * `edge_kind` from §3's fixed edge table.
65
+ *
66
+ * The flag, never the name, is the read-time source of truth —
67
+ * `query/card.ts`'s `isStatusTerminal` stays name-blind (ADR-0002 D1: an
68
+ * unfixed-source gap is repaired at the SOURCE, never papered over with a
69
+ * read-time name fallback, which would be a second, drifting source of
70
+ * truth). This table is that source fix: a name listed here that has no live
71
+ * `status` row is SEEDED `terminal:true` by {@link mintOrResolveStatusTx}, so
72
+ * minting `closed` (or any other reserved name) can no longer produce a row
73
+ * that reads as non-terminal — the drift `catalog-repair.ts` exists to clean
74
+ * up is not regenerated on the next mint.
75
+ *
76
+ * Membership is decided on the FOLDED name ({@link catalogNameFold}) — the
77
+ * same Unicode fold the duplicate guard and the case-fragment repair use, never
78
+ * SQL `lower()`/`NOCASE`, which fold only ASCII `A`–`Z`. The spellings below
79
+ * are the canonical catalog rows, but the PREDICATE folds, so `Closed`,
80
+ * `fixed`, `resolved` and `done` all count as reserved. This matters because a
81
+ * first-ever lowercase `fixed`/`resolved`/`done` has no uppercase row to
82
+ * collide against; under an exact-case predicate it would seed
83
+ * `terminal:false` — a closed status the read layer returns as open (backlog
84
+ * b4525bc3 / d7ec2c50). There is exactly ONE such table in the codebase
85
+ * (`catalog-repair.ts` re-exports this set rather than declaring its own).
86
+ */
87
+ export declare const RESERVED_TERMINAL_STATUS_NAMES: ReadonlySet<string>;
88
+ /** Whether `name` is one of {@link RESERVED_TERMINAL_STATUS_NAMES} under the case fold — so any spelling of the reserved terminal vocabulary (`closed`/`Closed`/`CLOSED`, `fixed`/`FIXED`) counts. */
89
+ export declare function isReservedTerminalStatusName(name: string): boolean;
59
90
  /**
60
91
  * The flat-catalog find-then-create (§1, §4c, §6.1): `kind`/`status`/
61
92
  * `priority`/`agent` are mintable on an unresolved NAME; a uid-shaped `ref`
62
93
  * that does not resolve instead throws — minting NEVER applies to a uid.
94
+ *
95
+ * A NAME that case-folds to an EXISTING LIVE row of the same kind but is
96
+ * spelled differently (`IN_PROGRESS` when `in_progress` is live) is REFUSED
97
+ * with {@link CaseVariantNameError} — never fold-resolved onto the existing
98
+ * row, never minted as a twin. Identity is exact-case (`WHERE name = ?`), so a
99
+ * variant is neither the same row nor a genuinely new name; it is ambiguous,
100
+ * and the ambiguity is stopped here rather than allowed to grow a duplicate
101
+ * the catalog-invariant guard would then abort every read over.
63
102
  */
64
103
  export declare function mintOrResolveCatalogTx(tx: AdapterTransaction, input: IMintOrResolveInput): Promise<IResolvedCatalogRow>;
104
+ /**
105
+ * Resolve-or-seed a `status` catalog row (§2, §6.1, §6.3.2, §6.3.4) — the
106
+ * ONE sanctioned status mint path, and the write-side counterpart of the
107
+ * read-side `query/card.ts`'s `isStatusTerminal`.
108
+ *
109
+ * Unlike the generic {@link mintOrResolveCatalogTx}, it SEEDS `terminal` from
110
+ * the frozen {@link RESERVED_TERMINAL_STATUS_NAMES} table on a miss — never
111
+ * from a call-site literal (`terminal:false` hardcoded at the call site was
112
+ * the drift's root cause) and never from a read-time name heuristic
113
+ * (`isStatusTerminal` stays name-blind, ADR-0002 D1). A name in the table
114
+ * seeds `terminal:true`; every other name seeds the pre-existing
115
+ * `terminal:false` default (§6.3.2: a novel or mistyped name must never
116
+ * silently close or exclude an item).
117
+ *
118
+ * Self-healing and idempotent, exactly like {@link resolveEdgeKindTx}: the
119
+ * exact `(kind,name)` lookup inside {@link mintOrResolveCatalogTx} finds the
120
+ * seeded row on every subsequent call, so reseeding never duplicates — the
121
+ * store converges to one live `status` row per name. A case-variant name
122
+ * (`Closed` vs a seeded `closed`) is REFUSED by {@link mintOrResolveCatalogTx}
123
+ * with {@link CaseVariantNameError} rather than folded onto the existing row or
124
+ * minted as a twin — the same exact-case stop every other flat catalog takes
125
+ * (see that function's doc comment).
126
+ *
127
+ * A uid-shaped `ref` that does not resolve still throws
128
+ * `CatalogNotFoundError` — minting never applies to a uid (§6.1).
129
+ */
130
+ export declare function mintOrResolveStatusTx(tx: AdapterTransaction, input: {
131
+ ref: string;
132
+ at?: string;
133
+ }): Promise<IResolvedCatalogRow>;
65
134
  /** `priority`'s mint rule (§6.3.2): "rank set to one past the current max rank (i.e. lowest urgency) — a novel priority can never silently outrank an existing one." */
66
135
  export declare function nextPriorityRankTx(tx: AdapterTransaction): Promise<number>;
67
136
  /**
@@ -25,7 +25,7 @@ export interface ICreateIssueInput {
25
25
  component?: string;
26
26
  /** catalog name or uid; default is the project's configured `policy.defaultKind`, falling back to the global `"issue"` row. An unresolved NAME mints; a uid-shaped ref that does not resolve throws (§6.1). */
27
27
  kind?: string;
28
- /** catalog name or uid; default is `policy.defaultStatus`, falling back to the global `"open"` row (minted with `terminal:false` if it does not yet exist). An unresolved NAME mints with `terminal:false`; a uid-shaped ref that does not resolve throws (§6.1). */
28
+ /** catalog name or uid; default is `policy.defaultStatus`, falling back to the global `"open"` row. An unresolved NAME is minted via `mintOrResolveStatusTx`: a name in the frozen reserved terminal table (`RESERVED_TERMINAL_STATUS_NAMES`, `catalog.ts`) seeds `terminal:true`, every other name seeds `terminal:false` (a novel/typo name must never silently close an item, §6.3.2); a uid-shaped ref that does not resolve throws (§6.1). */
29
29
  status?: string;
30
30
  /** catalog name or uid; genuinely OPTIONAL — §6.3.2 states minting behavior for a GIVEN unresolved name but, unlike `kind`/`status`, states no fallback for the omitted case; omitted therefore writes no `has_priority` edge at all (a deliberate reading of §6.3.2's more precise per-field text over §4's summary prose — see this project's own README/CHANGELOG note on this slice for the citation). An unresolved NAME mints with `rank` = one past the current max (lowest urgency); a uid-shaped ref that does not resolve throws. */
31
31
  priority?: string;
package/write/errors.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * All named classes here are `E_VALIDATION`- or `E_CONSTRAINT`-class
19
19
  * members of the same union (SPEC.md §4c, "Two failure-signaling shapes,
20
20
  * reconciled into one contract") — `IssueNotFoundError`, `InvalidArgumentError`,
21
- * `CatalogNotFoundError`, `ClaimHeldError`, `SingleValuedRelationConflictError`,
21
+ * `CatalogNotFoundError`, `CaseVariantNameError`, `ClaimHeldError`, `SingleValuedRelationConflictError`,
22
22
  * `NoteRequiredError`, `CitationRequiredError`, `CitationUnverifiableError` are
23
23
  * `E_VALIDATION` (never retryable — thrown before any driver call runs);
24
24
  * `StaleSupersedeError` is the one deliberate `E_CONSTRAINT` this spec's own
@@ -162,6 +162,42 @@ export declare class CatalogNotFoundError extends BacklogWriteError {
162
162
  readonly retryable = false;
163
163
  constructor(catalogKind: string, ref: string);
164
164
  }
165
+ /**
166
+ * A flat-catalog write named a token that differs from an EXISTING LIVE row of
167
+ * the SAME kind only by letter case (`IN_PROGRESS` vs `in_progress`, `high` vs
168
+ * `HIGH`). Catalog identity is EXACT-case (`DATA_MODEL.md:70-78`; the
169
+ * `(kind, name)` resolve in `write/catalog.ts` is `WHERE name = ?`), so the
170
+ * write path REFUSES the variant rather than fold-resolving it onto the
171
+ * existing row or minting a twin. Accepting it would grow a second live row
172
+ * that case-folds to the first — the case-fragment defect the uniqueness guard
173
+ * (`store/catalog-invariant-guard.ts`) exists to catch, which is exactly how
174
+ * the store wedged (BUG: a mixed-case write minted a twin, then the guard
175
+ * refused every read).
176
+ *
177
+ * The message names BOTH spellings and tells the caller which one to use: the
178
+ * one already live. The ambiguity is stopped here, at the single write call
179
+ * site, instead of aborting every read.
180
+ *
181
+ * `E_VALIDATION`, never retryable — this is a before-any-mint decision, so
182
+ * retrying the identical payload fails identically.
183
+ */
184
+ export declare class CaseVariantNameError extends BacklogWriteError {
185
+ /** The flat-catalog kind (`status`, `priority`, `kind`, `agent`). */
186
+ readonly catalogKind: string;
187
+ /** The name of the row ALREADY LIVE for this kind — the spelling to use. */
188
+ readonly canonicalName: string;
189
+ /** The differing name the caller supplied — the spelling that was refused. */
190
+ readonly offendingName: string;
191
+ readonly code: "E_VALIDATION";
192
+ readonly retryable = false;
193
+ constructor(
194
+ /** The flat-catalog kind (`status`, `priority`, `kind`, `agent`). */
195
+ catalogKind: string,
196
+ /** The name of the row ALREADY LIVE for this kind — the spelling to use. */
197
+ canonicalName: string,
198
+ /** The differing name the caller supplied — the spelling that was refused. */
199
+ offendingName: string);
200
+ }
165
201
  /** A caller-supplied argument is missing, blank, or fails a project-declared invariant. */
166
202
  export declare class InvalidArgumentError extends BacklogWriteError {
167
203
  readonly field: string;
@@ -6,7 +6,7 @@ export interface ITransitionInput {
6
6
  uid: string;
7
7
  /** The identity of the acting agent or person (§6.3's opening rule). REQUIRED. */
8
8
  by: string;
9
- /** catalog name or uid. An unresolved NAME mints a new status row with `terminal:false` (exactly like `create`'s own `status` field, §6.3.2/§6.1); a uid-shaped ref that does not resolve throws `CatalogNotFoundError('status', ref)`. */
9
+ /** catalog name or uid. An unresolved NAME is minted via `mintOrResolveStatusTx` (exactly like `create`'s own `status` field, §6.3.2/§6.1): a name in the frozen reserved terminal table (`RESERVED_TERMINAL_STATUS_NAMES`, `catalog.ts`) seeds `terminal:true`, every other name seeds `terminal:false`; a uid-shaped ref that does not resolve throws `CatalogNotFoundError('status', ref)`. */
10
10
  toStatus: string;
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;
@@ -42,8 +42,9 @@ export interface ITransitionOutcome {
42
42
  * `update` — this file's own doc comment on `update.ts` explains why that
43
43
  * reuses this error class rather than a new one), `CatalogNotFoundError('status',
44
44
  * toStatus)` (uid-shaped `toStatus` only — an unresolved NAME instead
45
- * auto-mints with `terminal:false`, exactly as `create`'s own `status` field,
46
- * §6.1), a project-declared `requiredFields` entry (§2 — here, always just
45
+ * auto-mints via `mintOrResolveStatusTx`, seeding `terminal` from the frozen
46
+ * reserved table exactly as `create`'s own `status` field does (§6.1), a
47
+ * project-declared `requiredFields` entry (§2 — here, always just
47
48
  * `status`, see this file's own doc comment) left blank,
48
49
  * `NoteRequiredError` (policy-gated), `CitationRequiredError`
49
50
  * (policy-gated, terminal-only), `CitationUnverifiableError(target,