@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
@@ -1,21 +1,16 @@
1
+ import { ISimilarCandidate, SimilarityScanDegradedReason } from './similarity-scan.js';
1
2
  import { IWriteStoreHandle } from './tx.js';
3
+ import { ICitation } from '../citation.js';
2
4
  import { StoreSearchBackend } from '@adhd/sox-hybrid-search';
3
5
  import { GraphBackend } from '@adhd/sox-graph-store';
4
6
 
5
7
  /**
6
- * A filing-time citation (§6.3.2, carried forward from the established `Citation` shape in
7
- * spirit — `blastRadius` stays best-effort, `model.ts:110-120`). Named
8
- * `ICitationInput` here (not the spec's bare `Citation`) per this repo's
9
- * "prefix shared/data interfaces with `I`" convention.
8
+ * A filing-time citation. `ICitation` — the ONE contract, defined in
9
+ * `../citation.ts` — is imported here and re-exported below so an existing
10
+ * importer of `ICitationInput` from this module keeps compiling; the shape and
11
+ * the persisted codec live in exactly one place (`../citation.ts`), never here.
10
12
  */
11
- export interface ICitationInput {
12
- file: string;
13
- lines?: string;
14
- context?: string;
15
- symbol?: string;
16
- /** Best-effort enrichment payload — not re-specified here; carried through verbatim into the citation node's metadata. */
17
- blastRadius?: unknown;
18
- }
13
+ export type { ICitation, ICitationInput } from '../citation.js';
19
14
  export interface ICreateIssueInput {
20
15
  title: string;
21
16
  body: string;
@@ -29,7 +24,7 @@ export interface ICreateIssueInput {
29
24
  status?: string;
30
25
  /** 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
26
  priority?: string;
32
- citations?: ICitationInput[];
27
+ citations?: ICitation[];
33
28
  /** catalog agent name/uid; defaults to `by`. An unresolved NAME mints; a uid-shaped ref that does not resolve throws (§6.1). */
34
29
  author?: string;
35
30
  /** Plain metadata scalar (§6.2) — no edge. */
@@ -48,6 +43,27 @@ export interface ICreateIssueInput {
48
43
  * every read/render path is byte-for-byte unchanged.
49
44
  */
50
45
  gitContext?: string;
46
+ /**
47
+ * A **dedupe-scoping hint only**: the uid of the item this one is being
48
+ * filed under. It names a parent so the pre-write similarity scan (§6.4
49
+ * point 1) EXCLUDES that item AND its `part_of` ancestor chain from the
50
+ * duplicate candidate set, so a child that deliberately restates its
51
+ * parent's intent is not suppressed as a duplicate OF that parent (defect
52
+ * c5460239: a child of C7 reproduced `created:false`/
53
+ * `duplicate-suppressed` with its own parent as the top candidate at
54
+ * 0.9811). A child restating its GRANDPARENT is likewise not suppressed,
55
+ * because the ancestry walk spans the whole `part_of` chain.
56
+ *
57
+ * It writes NO `part_of` edge — `relate` is the sole `part_of` writer
58
+ * (§6.3.6). Because it only scopes the read-side scan, it is invisible on
59
+ * the emitted card and nothing new is persisted; it has no effect once a
60
+ * create is not a duplicate. An unresolvable uid throws
61
+ * `IssueNotFoundError` (fail loud, ADR-0002 D5), never silently ignored.
62
+ *
63
+ * A uid ONLY, never a name (matching `relate`'s `sourceUid`/`targetUid` uid
64
+ * convention, §1/§6.1).
65
+ */
66
+ dedupeExcludeUid?: string;
51
67
  /** The acting identity — agent or person (§6.3's opening rule) — REQUIRED on every mutating verb. A missing/blank value throws `InvalidArgumentError('by', ...)` before any write runs. */
52
68
  by: string;
53
69
  /**
@@ -58,7 +74,16 @@ export interface ICreateIssueInput {
58
74
  * value (§6.4 point 3, first sentence).
59
75
  *
60
76
  * - `'abort'` — nothing is written; `{created:false,
61
- * reason:'duplicate-suppressed', duplicateCandidates}`.
77
+ * reason:'duplicate-suppressed', duplicateCandidates}`. If the scan was
78
+ * degraded in the ONE way that can NEVER resolve on its own —
79
+ * `no-embed-query`, the backend wired without `embedQuery` so the
80
+ * calibrated channel can never run — it also fails closed with
81
+ * `{created:false, reason:'duplicate-scan-degraded',
82
+ * duplicateScanDegraded:true, duplicateScanDegradedReason:'no-embed-query'}`
83
+ * (BUG 4e8fce2a). A `no-vector-scores` degrade (indistinguishable at read
84
+ * time from benign on-write embedding lag) or a `no-search-backend`
85
+ * degrade (dedupe never mounted) instead PROCEEDS and carries the
86
+ * `duplicateScanDegraded` signal.
62
87
  * - `'force'` — the write proceeds to a genuinely new, distinct `uid`
63
88
  * despite the match; `duplicateCandidates` is still reported.
64
89
  * - `'comment'` — no new issue node is written; a `note` node is attached
@@ -94,6 +119,54 @@ export interface IDuplicateCandidate {
94
119
  /** Cosine similarity in `[0,1]`, straight off the vector channel — directly comparable to `project_policy.dedupe_threshold`. */
95
120
  score: number;
96
121
  }
122
+ /**
123
+ * Why {@link scanForDuplicates} could not produce a calibrated comparison —
124
+ * i.e. why the gate did NOT actually scan for duplicates despite being asked
125
+ * to. See {@link IDuplicateScanOutcome.degraded}.
126
+ *
127
+ * C9: this is now an ALIAS of the shared scan's own reason union
128
+ * (`write/similarity-scan.ts`), so the create gate and the `view:'similar'`
129
+ * cluster block can never drift on what "degraded" means. The name and every
130
+ * member are unchanged, so existing importers/tests are unaffected.
131
+ */
132
+ export type DuplicateScanDegradedReason = SimilarityScanDegradedReason;
133
+ /**
134
+ * The result of {@link scanForDuplicates}. Its `candidates` arm is the
135
+ * pre-existing return value; `degraded` is the missing signal this type now
136
+ * carries (BUG 4e8fce2a).
137
+ *
138
+ * **`degraded` is the distinction the gate previously could not make.**
139
+ * Before this type existed, `scanForDuplicates` returned only
140
+ * `IDuplicateCandidate[]`, so an empty array meant EITHER "the scan ran and
141
+ * genuinely found nothing" OR "the scan could not run at all" — two states
142
+ * with opposite safety meanings that the caller had no way to tell apart. The
143
+ * abort branch (`createIssue`) then treated both as "no duplicates" and wrote
144
+ * the item, so `duplicateAction:'abort'` silently FAILED OPEN whenever the
145
+ * semantic substrate was degraded (no search backend, no `embedQuery`, or an
146
+ * empty/mismatched vector space). See {@link scanForDuplicates}'s own doc
147
+ * comment for exactly which conditions set this flag.
148
+ */
149
+ export interface IDuplicateScanOutcome {
150
+ candidates: IDuplicateCandidate[];
151
+ /**
152
+ * C9 — advisory CROSS-project candidates surfaced alongside the
153
+ * same-project `candidates`. Never suppress a create, never fire
154
+ * `comment`, and the scan writes no edge for them (AC3). Empty when the
155
+ * scope is `same-project` (the default).
156
+ */
157
+ similarCandidates?: ISimilarCandidate[];
158
+ /**
159
+ * `true` iff the scan could NOT perform a calibrated duplicate comparison
160
+ * for the in-scope issues — no vector channel ran, so `candidates` is empty
161
+ * because the gate is blind, NOT because the store is clean. Always `false`
162
+ * when the project genuinely has zero issues to compare against, or when
163
+ * `dedupeScanEnabled` is off: those are complete (if empty) scans, not
164
+ * degraded ones.
165
+ */
166
+ degraded: boolean;
167
+ /** Set iff `degraded` — the concrete reason, never a generic "unavailable". */
168
+ degradedReason?: DuplicateScanDegradedReason;
169
+ }
97
170
  /**
98
171
  * The search substrate `createIssue`'s duplicate gate needs (§6.4 point 1),
99
172
  * threaded alongside {@link IWriteStoreHandle} rather than folded into it:
@@ -109,19 +182,22 @@ export interface IDuplicateCandidate {
109
182
  *
110
183
  * `search.embedQuery` is declared OPTIONAL here (unlike
111
184
  * `IQueryStoreHandle.search.embedQuery`, which is mandatory) specifically to
112
- * express §6.4 point 4's degraded case: a `StoreSearchBackend` can be wired
113
- * (FTS/text always available, since it runs off the graph store directly)
114
- * while no embedding model/vector space is configured — the search
115
- * itself stays callable, just scoped to `signals:[{text}]` rather than
116
- * `signals:[{text},{vec}]` (and, having no vector channel, surfacing no
117
- * duplicate candidates — see {@link scanForDuplicates}). `search` itself stays OPTIONAL (no backend
185
+ * express §6.4 point 4's `no-embed-query` degraded case: a `StoreSearchBackend`
186
+ * can be wired (FTS/text runs off the graph store directly) while no embedding
187
+ * model/vector space is configured, so the CALIBRATED (vector) channel can
188
+ * never run. `scanForDuplicates` reports that as `degradedReason:
189
+ * 'no-embed-query'`, and `createIssue`'s `abort` FAILS CLOSED on it — a
190
+ * persistent configuration absence the operator must fix, never a transient.
191
+ * (It deliberately does NOT fall back to a text-only scan: BM25 is
192
+ * uncalibrated and would reintroduce the false-positive class — see
193
+ * {@link scanForDuplicates}.) `search` itself stays OPTIONAL (no backend
118
194
  * mounted at all) for the same "never silently go dark" posture §6.4 point 4
119
- * states, but applied one layer further out: `scanForDuplicates` treats a
120
- * wholly-absent backend as "scan unavailable" (zero candidates, `create`
121
- * proceeds normally) rather than throwing — filing an issue must never hard-
122
- * fail because the product-feature-only dedupe UX (§6.4's own framing: "a
123
- * missed warning, not a correctness defect") happens to be unwired in a given
124
- * environment.
195
+ * states, but applied one layer further out: `scanForDuplicates` reports a
196
+ * wholly-absent backend as `no-search-backend` (zero candidates, `create`
197
+ * PROCEEDS and carries the signal) rather than throwing — filing an issue must
198
+ * never hard-fail because the product-feature-only dedupe UX (§6.4's own
199
+ * framing: "a missed warning, not a correctness defect") happens to be
200
+ * unwired in a given environment.
125
201
  */
126
202
  export interface IDuplicateScanHandle {
127
203
  readonly graph?: GraphBackend;
@@ -202,7 +278,19 @@ export interface ICreateIssueCard {
202
278
  * `duplicateAction` — this is NOT an empty array in that case (§6.4 point
203
279
  * 3, first sentence).
204
280
  * - `reason` — present iff `!created` and the gate suppressed the write
205
- * (`duplicateAction:'abort'`, the default).
281
+ * (`duplicateAction:'abort'`, the default) OR refused it because the scan
282
+ * was degraded in the never-resolvable `no-embed-query` way
283
+ * (`'duplicate-scan-degraded'`, BUG 4e8fce2a — see
284
+ * {@link IDuplicateScanOutcome}).
285
+ * - `duplicateScanDegraded`/`duplicateScanDegradedReason` — present iff the
286
+ * pre-write scan could not run a calibrated comparison
287
+ * ({@link scanForDuplicates}'s `degraded`), on EVERY outcome: on the
288
+ * `'abort'` refusal for the never-resolvable `no-embed-query` degrade
289
+ * (`created:false`, `reason:'duplicate-scan-degraded'`), and on
290
+ * `'force'`/`'comment'`/the `no-vector-scores` and `no-search-backend`
291
+ * `'abort'` paths where the write proceeded anyway.
292
+ * This is the field that makes a degraded scan non-silent: absent on a
293
+ * healthy scan (the common case), so byte-for-byte unchanged there.
206
294
  * - `commentedOn` — present iff `duplicateAction:'comment'` fired.
207
295
  * - `supersededUid` — always absent from `createIssue` alone; only the
208
296
  * `supersedes` composition (§6.3.2, not yet built here) would set it.
@@ -212,12 +300,32 @@ export interface ICreateIssueResult {
212
300
  uid?: string;
213
301
  item?: ICreateIssueCard;
214
302
  duplicateCandidates?: IDuplicateCandidate[];
215
- reason?: 'duplicate-suppressed';
303
+ /**
304
+ * C9 — advisory cross-project similarity candidates. Present iff the
305
+ * configured `similarityScope` is wider than `same-project` and the scan
306
+ * surfaced at least one. Purely informational: it never changes whether the
307
+ * write proceeded, and the scan writes no `similar_to` edge (AC2/AC3).
308
+ */
309
+ similarCandidates?: ISimilarCandidate[];
310
+ reason?: 'duplicate-suppressed' | 'duplicate-scan-degraded';
311
+ /** Present iff the pre-write scan was degraded — see this interface's own doc comment. Paired with {@link duplicateScanDegradedReason}. */
312
+ duplicateScanDegraded?: boolean;
313
+ /** Why the scan was degraded. Present iff {@link duplicateScanDegraded}. */
314
+ duplicateScanDegradedReason?: DuplicateScanDegradedReason;
216
315
  supersededUid?: string;
217
316
  commentedOn?: {
218
317
  uid: string;
219
318
  noteId: string;
220
319
  };
320
+ /**
321
+ * C1 AC8 — how the issue's component was resolved. `'explicit'` when the
322
+ * caller supplied `component` and it resolved; `'default-root'` when the
323
+ * caller supplied none and the project's reserved `(root)` default was used.
324
+ * Makes "silently defaulted" distinguishable from "resolved": a supplied
325
+ * component that does NOT resolve still throws `CatalogNotFoundError` and
326
+ * never reaches this field.
327
+ */
328
+ placementResolved?: 'explicit' | 'default-root';
221
329
  }
222
330
  /**
223
331
  * Maximum length of the item-level `gitContext` disclosure scalar, enforced
@@ -246,7 +354,13 @@ export declare function assertGitContextWithinCap(value: string | undefined): vo
246
354
  * outside the project root AND every `citationAllowedExternalRoots` entry —
247
355
  * the carve-out, BUG c6d35272 — and the error names those roots),
248
356
  * `InvalidArgumentError('duplicateAction', ...)`
249
- * (an unrecognized value — §6.4), `WriteContentionError`/
357
+ * (an unrecognized value — §6.4), the `dedupeExcludeUid` resolution errors
358
+ * (`IssueNotFoundError` / `AmbiguousReferenceError` / `InvalidArgumentError` /
359
+ * `StaleSupersedeError` — via `resolveIssueByUid`, see
360
+ * {@link resolveDedupeExcludeIds}) when a `dedupeExcludeUid` was supplied but
361
+ * does not resolve to a live, non-superseded `issue` — fail loud, ADR-0002 D5;
362
+ * c5460239),
363
+ * `WriteContentionError`/
250
364
  * `WriteIOError` (§4c — an exhausted driver-level retry on the underlying
251
365
  * `immediate` transaction).
252
366
  */
package/write/errors.d.ts CHANGED
@@ -162,6 +162,35 @@ export declare class CatalogNotFoundError extends BacklogWriteError {
162
162
  readonly retryable = false;
163
163
  constructor(catalogKind: string, ref: string);
164
164
  }
165
+ /**
166
+ * A uid reference matched MORE THAN ONE live node by prefix. Carries the
167
+ * candidate set (`uid` + `kind` + `name`) as structured data so a caller can
168
+ * disambiguate and re-run, and renders every candidate into the message.
169
+ *
170
+ * The resolver NEVER auto-selects among the candidates — silently returning
171
+ * the wrong item is strictly worse than refusing. Distinct from
172
+ * `IssueNotFoundError` (an exact uid that is genuinely absent) so a caller can
173
+ * branch structurally: an ambiguous reference is actionable (re-run with a
174
+ * longer prefix or the full uid), a missing one is not.
175
+ *
176
+ * `E_VALIDATION`, never retryable — retrying the identical short reference
177
+ * resolves to the identical candidate set.
178
+ */
179
+ export declare class AmbiguousReferenceError extends BacklogWriteError {
180
+ readonly ref: string;
181
+ readonly candidates: ReadonlyArray<{
182
+ uid: string;
183
+ kind: string;
184
+ name: string;
185
+ }>;
186
+ readonly code: "E_VALIDATION";
187
+ readonly retryable = false;
188
+ constructor(ref: string, candidates: ReadonlyArray<{
189
+ uid: string;
190
+ kind: string;
191
+ name: string;
192
+ }>);
193
+ }
165
194
  /**
166
195
  * A flat-catalog write named a token that differs from an EXISTING LIVE row of
167
196
  * the SAME kind only by letter case (`IN_PROGRESS` vs `in_progress`, `high` vs
@@ -208,9 +237,16 @@ export declare class InvalidArgumentError extends BacklogWriteError {
208
237
  /** No live `issue` node carries the given `uid` (SPEC.md §6.1). */
209
238
  export declare class IssueNotFoundError extends BacklogWriteError {
210
239
  readonly uid: string;
240
+ readonly asPrefix: boolean;
211
241
  readonly code: "E_VALIDATION";
212
242
  readonly retryable = false;
213
- constructor(uid: string);
243
+ /**
244
+ * @param uid The uid (or uid prefix) the caller passed in.
245
+ * @param asPrefix Set by the prefix resolver when `uid` was a short prefix
246
+ * rather than a full uid, so the message says "no item matches" instead of
247
+ * implying the caller passed a complete uid that is simply absent.
248
+ */
249
+ constructor(uid: string, asPrefix?: boolean);
214
250
  }
215
251
  /**
216
252
  * `claim` (§6.3.5): the lease is held by someone else, not yet stale, and the
@@ -355,6 +391,81 @@ export declare class BacklogValidationError extends BacklogWriteError {
355
391
  readonly retryable = false;
356
392
  constructor(field: string, detail?: string);
357
393
  }
394
+ /**
395
+ * A supplied anchor locator is outside the closed grammar (`path:<file>[:<line>]`
396
+ * | `url:<url>` | `query:<cql>` | `registry:<ref>`), or is a bare `path:line`
397
+ * with no `digest`. A caller-input mistake, so `E_VALIDATION`, never retryable —
398
+ * nothing is written and retrying the identical locator fails identically.
399
+ */
400
+ export declare class AnchorLocatorInvalidError extends BacklogWriteError {
401
+ readonly locator: string;
402
+ readonly code: "E_VALIDATION";
403
+ readonly retryable = false;
404
+ constructor(locator: string, detail?: string);
405
+ }
406
+ /**
407
+ * `recheck` named an `attestation` uid that is not a live `attestation` node.
408
+ * Distinct from {@link IssueNotFoundError} (an `issue` uid) and
409
+ * {@link CatalogNotFoundError} so a caller can branch on the missing record
410
+ * kind; `E_VALIDATION`, never retryable.
411
+ */
412
+ export declare class AttestationNotFoundError extends BacklogWriteError {
413
+ readonly uid: string;
414
+ readonly code: "E_VALIDATION";
415
+ readonly retryable = false;
416
+ constructor(uid: string);
417
+ }
418
+ /**
419
+ * `obligate` (C4, DESIGN §2 Primitive 3) was handed a `requirement` outside the
420
+ * CLOSED predicate core — an unknown `op`, a missing/ill-typed leaf
421
+ * (`evidence.kind`, `relation.type`/`direction`, a non-array `all_of`/`any_of`
422
+ * `of`, a `not` whose `of` is not a predicate), an unexpected extra key, or an
423
+ * `evidence.min < 1`. Raised by `assertValidPredicate` BEFORE any transaction
424
+ * opens, so an invalid predicate never holds a write lock.
425
+ *
426
+ * `E_VALIDATION`, never retryable — nothing is written, and retrying the
427
+ * identical predicate fails identically. Distinct from `InvalidArgumentError`
428
+ * so a caller can tell "the argument envelope was malformed" from "the
429
+ * predicate grammar was violated".
430
+ */
431
+ export declare class InvalidPredicateError extends BacklogWriteError {
432
+ readonly detail: string;
433
+ readonly code: "E_VALIDATION";
434
+ readonly retryable = false;
435
+ constructor(detail: string);
436
+ }
437
+ /**
438
+ * `unobligate` named a uid that is not a live `obligation` node. Distinct from
439
+ * {@link IssueNotFoundError} (an `issue` uid) and {@link CatalogNotFoundError}
440
+ * so a caller can branch on the missing record kind; `E_VALIDATION`, never
441
+ * retryable.
442
+ */
443
+ export declare class ObligationNotFoundError extends BacklogWriteError {
444
+ readonly uid: string;
445
+ readonly code: "E_VALIDATION";
446
+ readonly retryable = false;
447
+ constructor(uid: string);
448
+ }
449
+ /**
450
+ * `appendSpecRevision`'s `base_revision` no longer matches the ticket's current
451
+ * spec pointer — a concurrent spec edit won the CAS. `E_VALIDATION`,
452
+ * `retryable: false`, and mapped EXPLICITLY to the envelope code
453
+ * `precondition_failed` in `api.ts`'s `ERROR_CLASS_TO_ENVELOPE_CODE` (C10 AC6):
454
+ * the `E_VALIDATION` fallthrough would otherwise return `validation`.
455
+ *
456
+ * A stale `base_revision` is NOT resource contention — no other actor holds the
457
+ * node; the *caller's own* read is out of date and must be re-read before
458
+ * retrying — so it belongs with the existing refusing-write preconditions
459
+ * (`IssueTerminalError`, `CitationUnverifiableError`), not with `conflict`
460
+ * (reserved for a resource another actor holds). No write runs.
461
+ */
462
+ export declare class SpecRevisionConflictError extends BacklogWriteError {
463
+ readonly expected: string;
464
+ readonly actual: string;
465
+ readonly code: "E_VALIDATION";
466
+ readonly retryable = false;
467
+ constructor(expected: string, actual: string);
468
+ }
358
469
  /**
359
470
  * Classify a caught, RAW driver-level error (never one of our own
360
471
  * {@link BacklogWriteError} subclasses — those are already decided and must
@@ -399,3 +510,87 @@ export declare function classifyDriverError(err: unknown): IWriteError;
399
510
  * immediately after `assertNonBlank('by', input.by)` at every mutating verb.
400
511
  */
401
512
  export declare function assertNotBareRoleLiteral(field: string, value: string): void;
513
+ /**
514
+ * (C5) A terminal transition was refused because a `block`-severity obligation
515
+ * is unsatisfied (DESIGN §2 Primitive 3). `E_VALIDATION` → the envelope maps
516
+ * it to `precondition_failed`, with the structured `IGateRefusal` in
517
+ * `error.details.refusal` (adhd ADR-0004 — object-shaped, never a `{result}`
518
+ * envelope). Thrown from INSIDE `transition`'s open `BEGIN IMMEDIATE`
519
+ * transaction and BEFORE any write, so the throw rolls the transaction back and
520
+ * a re-read shows the status unchanged.
521
+ *
522
+ * `E_VALIDATION`, never retryable — retrying the identical payload against
523
+ * unchanged obligations fails identically (the caller must satisfy or override
524
+ * the obligation first).
525
+ *
526
+ * Deliberately declared ATTACHED to `errors.ts`'s end (after every existing
527
+ * export) and importing `IGateRefusal` via an inline `import(...)` type, so
528
+ * this additive change shifts NO existing line and leaves `CONTRACT.md`'s
529
+ * `(errors.ts:NNN)` anchors (enforced by `contract-anchors.spec.ts`) true.
530
+ */
531
+ export declare class ObligationUnsatisfiedError extends BacklogWriteError {
532
+ readonly refusal: import('./gate.js').IGateRefusal;
533
+ readonly code: "E_VALIDATION";
534
+ readonly retryable = false;
535
+ constructor(refusal: import('./gate.js').IGateRefusal);
536
+ }
537
+ /**
538
+ * (C5) A claimed override of a refusing obligation is not permitted: the caller
539
+ * is not in the obligation's `override.actors`, or the override carries a blank
540
+ * reason. An override ALWAYS requires a recorded reason (DESIGN §2 Primitive 3
541
+ * — "not a configurable boolean"), so a blank one is refused, never treated as
542
+ * a silent pass.
543
+ *
544
+ * `E_VALIDATION` → `precondition_failed`, never retryable.
545
+ */
546
+ export declare class OverrideNotPermittedError extends BacklogWriteError {
547
+ readonly obligationUid: string;
548
+ readonly actor: string;
549
+ readonly code: "E_VALIDATION";
550
+ readonly retryable = false;
551
+ constructor(obligationUid: string, actor: string);
552
+ }
553
+ /**
554
+ * (C6) A verb's entry precondition was refused by a live block-severity verdict
555
+ * condition — today only `claim` (DESIGN §2 Primitive 3/4: taking blocked work
556
+ * is the observed failure). `E_VALIDATION` → the envelope maps it to
557
+ * `precondition_failed`, with the structured `ICondition` in
558
+ * `error.details.refusal` (adhd ADR-0004 — object-shaped, never a `{result}`
559
+ * envelope). Thrown from INSIDE `claim`'s open `BEGIN IMMEDIATE` transaction
560
+ * and BEFORE any write, so the throw rolls the transaction back and the item
561
+ * is left unclaimed.
562
+ *
563
+ * `E_VALIDATION`, never retryable — retrying the identical payload against an
564
+ * unchanged blocker fails identically (the caller must clear the blocker, or
565
+ * `force` and record it first).
566
+ *
567
+ * Declared ATTACHED to `errors.ts`'s end (after every existing export) and
568
+ * importing `ICondition` via an inline `import(...)` type, so this additive
569
+ * change shifts NO existing line and leaves `CONTRACT.md`'s `(errors.ts:NNN)`
570
+ * anchors (enforced by `contract-anchors.spec.ts`) true.
571
+ */
572
+ export declare class PreconditionRefusedError extends BacklogWriteError {
573
+ readonly refusal: import('../query/types.js').ICondition;
574
+ readonly code: "E_VALIDATION";
575
+ readonly retryable = false;
576
+ constructor(refusal: import('../query/types.js').ICondition);
577
+ }
578
+ /**
579
+ * `removeCitation` named a uid that is not a LIVE `citation` node — absent,
580
+ * already removed, or a uid naming a node of a DIFFERENT kind (the "foreign
581
+ * citation" refusal). Distinct from `IssueNotFoundError` (an `issue` uid) and
582
+ * `CatalogNotFoundError` (a registry/`catalog` ref) so a caller can branch on
583
+ * the missing record kind; `E_VALIDATION`, never retryable.
584
+ *
585
+ * Declared ATTACHED to `errors.ts`'s end (after every existing export, like
586
+ * `ObligationUnsatisfiedError`/`OverrideNotPermittedError`/
587
+ * `PreconditionRefusedError` before it), so this additive change shifts NO
588
+ * existing line and leaves `CONTRACT.md`'s `(errors.ts:NNN)` anchors
589
+ * (enforced by `contract-anchors.spec.ts`) true.
590
+ */
591
+ export declare class CitationNotFoundError extends BacklogWriteError {
592
+ readonly uid: string;
593
+ readonly code: "E_VALIDATION";
594
+ readonly retryable = false;
595
+ constructor(uid: string);
596
+ }
@@ -0,0 +1,67 @@
1
+ import { ITxNodeRow } from './tx.js';
2
+ import { IVerdict } from '../query/types.js';
3
+ import { AdapterTransaction } from '@adhd/sox-store-adapter';
4
+
5
+ /** The closed refusal-reason vocabulary (DESIGN §2 Primitive 4's governed core, scoped to the write gate). */
6
+ export type IGateReasonCode = 'EvidenceUnverified' | 'EvidenceStale' | 'BlockedBy' | 'MissingObligation' | 'ClaimStale' | 'ReferenceUnresolved' | 'Unknown';
7
+ /**
8
+ * The object-shaped refusal payload (adhd ADR-0004 — carried in
9
+ * `error.details.refusal`, never as a `{result}` envelope).
10
+ */
11
+ export interface IGateRefusal {
12
+ code: IGateReasonCode;
13
+ message: string;
14
+ /** The obligation that refused, when the refusal is obligation-scoped. */
15
+ obligationUid?: string;
16
+ /** The thing to fix — blocker uid / attestation uid. */
17
+ subject?: string;
18
+ /** For EvidenceUnverified: the `claim.kind` the obligation required. */
19
+ required_kind?: string;
20
+ /** The mechanical check that produced the refusal (e.g. 'default_branch_ancestor'). */
21
+ performed_check?: string;
22
+ }
23
+ export interface IGateEvaluation {
24
+ satisfied: boolean;
25
+ refusals: IGateRefusal[];
26
+ /** Pairs recorded as `satisfies` edges on success. */
27
+ satisfiedBy: Array<{
28
+ obligationUid: string;
29
+ attestationUid: string;
30
+ }>;
31
+ /** Obligations whose on_fail is 'warn' and predicate false — recorded, never refusing. */
32
+ warnings: IGateRefusal[];
33
+ }
34
+ /** The gated transition's inputs, threaded from `transition.ts`'s open `tx`. */
35
+ export interface IGateInput {
36
+ issueRowid: number;
37
+ fromStatus: string;
38
+ toStatus: string;
39
+ effectiveActor: string;
40
+ at: string;
41
+ /** A caller-supplied override attempt; honoured only for a listed actor + a non-blank reason. */
42
+ override?: {
43
+ reason: string;
44
+ };
45
+ }
46
+ /**
47
+ * Evaluate the transition-scoped obligations of `issueRowid` for `from → to`
48
+ * INSIDE the caller's open `tx`. Never writes. `effectiveActor` enables the
49
+ * listed-actor override.
50
+ *
51
+ * Returns a typed {@link IGateEvaluation}; the caller decides whether to throw
52
+ * ({@link ObligationUnsatisfiedError}) and writes the `satisfies` edges on
53
+ * success. Throws {@link OverrideNotPermittedError} when an override is
54
+ * claimed by an actor not listed on the refusing obligation, or carries a
55
+ * blank reason.
56
+ */
57
+ export declare function evaluateTransitionGateTx(tx: AdapterTransaction, input: IGateInput): Promise<IGateEvaluation>;
58
+ /**
59
+ * Derive the verdict INSIDE the caller's open tx, through rungs 1–2 only
60
+ * (claim is a precondition, never a full anchor re-resolve). Never writes;
61
+ * reuses C4's {@link evaluatePredicate} with the tx-scoped {@link
62
+ * makeTxResolver} and C6's own `orderConditions`/`computeActionable`.
63
+ */
64
+ export declare function evaluateVerdictTx(tx: AdapterTransaction, issueRow: ITxNodeRow, input: {
65
+ at: string;
66
+ claimStaleAfterMin: number;
67
+ }): Promise<IVerdict>;
@@ -0,0 +1,54 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+
3
+ export interface IMergeProjectInput {
4
+ /** The duplicate / retiring row — an exact uid. */
5
+ fromUid: string;
6
+ /** The canonical survivor — a uid or a name (resolved live). */
7
+ toUid: string;
8
+ /** identity; asserted non-blank + not a bare role literal */
9
+ by: string;
10
+ }
11
+ export interface IMergeProjectOutcome {
12
+ survivorUid: string;
13
+ retiredUid: string;
14
+ /** How many issues' `owns_component` chains were re-pointed onto the survivor. */
15
+ movedIssues: number;
16
+ /** Always true — the retired uid is never reusable. */
17
+ retired: true;
18
+ }
19
+ export interface IRmProjectInput {
20
+ uid: string;
21
+ reason: string;
22
+ by: string;
23
+ }
24
+ /**
25
+ * Merge duplicate project `fromUid` into canonical `toUid` in ONE
26
+ * `BEGIN IMMEDIATE` transaction: re-point every component owned by `fromUid`
27
+ * (its `owns_project` edges AND each component's `meta.projectUid`) onto
28
+ * `toUid`, set `fromUid.meta.redirectTo = toUid` + `fromUid.meta.retiredAt`,
29
+ * stamp `t_invalid`, and write an audit row.
30
+ *
31
+ * Idempotent: a second call with the same `(from,to)` is a no-op success
32
+ * (`movedIssues: 0`) — the retired `from` row is still readable by uid, its
33
+ * `meta.redirectTo` already names the survivor.
34
+ *
35
+ * @throws CatalogNotFoundError `fromUid` is not a `project` row, or `toUid`
36
+ * does not resolve to a LIVE project.
37
+ * @throws InvalidArgumentError `toUid` equals `fromUid`, or `fromUid` is
38
+ * already retired pointing at a DIFFERENT survivor (never silently
39
+ * re-chain).
40
+ */
41
+ export declare function mergeProject(handle: IWriteStoreHandle, input: IMergeProjectInput): Promise<IMergeProjectOutcome>;
42
+ /**
43
+ * Soft-retire a project WITHOUT a survivor: set `meta.retiredAt`, stamp
44
+ * `t_invalid`, and audit. No `redirectTo` is written, so its old name resolves
45
+ * to nothing (the row is retired, not redirected).
46
+ *
47
+ * Idempotent: a second call against an already-retired row is a success no-op.
48
+ *
49
+ * @throws CatalogNotFoundError `uid` is not a `project` row.
50
+ */
51
+ export declare function rmProject(handle: IWriteStoreHandle, input: IRmProjectInput): Promise<{
52
+ uid: string;
53
+ retired: true;
54
+ }>;