@adhd/backlog 1.0.3 → 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.3",
3
+ "version": "1.0.4",
4
4
  "bin": {
5
5
  "adhd-backlog": "index.js"
6
6
  },
@@ -73,21 +73,32 @@ export interface IMintOrResolveInput {
73
73
  * that reads as non-terminal — the drift `catalog-repair.ts` exists to clean
74
74
  * up is not regenerated on the next mint.
75
75
  *
76
- * Membership is EXACT and case-sensitive — the same predicate the `(kind,name)`
77
- * lookup uses (`WHERE name = ?`). A case-variant (`Closed`) is deliberately
78
- * NOT a member: it is an exact miss, self-heals into its own row (never a
79
- * throw), and is left to the separately-owned case-fragment repair, which
80
- * folds names. Spelling the canonical rows here is what pins them; there is
81
- * exactly ONE such table in the codebase (`catalog-repair.ts` re-exports
82
- * this set rather than declaring its own).
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).
83
86
  */
84
87
  export declare const RESERVED_TERMINAL_STATUS_NAMES: ReadonlySet<string>;
85
- /** Whether `name` is EXACTLY one of {@link RESERVED_TERMINAL_STATUS_NAMES} — case-sensitive, the same predicate the `(kind,name)` resolve uses. */
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. */
86
89
  export declare function isReservedTerminalStatusName(name: string): boolean;
87
90
  /**
88
91
  * The flat-catalog find-then-create (§1, §4c, §6.1): `kind`/`status`/
89
92
  * `priority`/`agent` are mintable on an unresolved NAME; a uid-shaped `ref`
90
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.
91
102
  */
92
103
  export declare function mintOrResolveCatalogTx(tx: AdapterTransaction, input: IMintOrResolveInput): Promise<IResolvedCatalogRow>;
93
104
  /**
@@ -108,9 +119,10 @@ export declare function mintOrResolveCatalogTx(tx: AdapterTransaction, input: IM
108
119
  * exact `(kind,name)` lookup inside {@link mintOrResolveCatalogTx} finds the
109
120
  * seeded row on every subsequent call, so reseeding never duplicates — the
110
121
  * store converges to one live `status` row per name. A case-variant name
111
- * (`Closed` vs a seeded `closed`) is an exact miss, so it self-heals into its
112
- * own row rather than throwing — the pre-existing resolution property, not a
113
- * terminality decision (the case-fragment repair folds those separately).
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).
114
126
  *
115
127
  * A uid-shaped `ref` that does not resolve still throws
116
128
  * `CatalogNotFoundError` — minting never applies to a uid (§6.1).
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;