@adhd/backlog 1.0.5 → 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 (58) hide show
  1. package/CHANGELOG.md +53 -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 +35 -1
  7. package/index.d.ts +23 -3
  8. package/index.js +106 -58
  9. package/index.mjs +11627 -7068
  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 +20 -13
  18. package/query/redirect.d.ts +30 -0
  19. package/query/resolve.d.ts +48 -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 +4 -4
  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 +63 -0
  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 +159 -0
  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 +21 -1
  58. package/write/update.d.ts +23 -1
@@ -26,7 +26,7 @@ export type CatalogInvariantViolation = {
26
26
  */
27
27
  export declare function renderCatalogInvariantViolations(violations: ReadonlyArray<CatalogInvariantViolation>): string;
28
28
  /**
29
- * The status/priority catalog violates an invariant this build's read layer
29
+ * The status/priority/kind catalog violates an invariant this build's read layer
30
30
  * depends on. Carries the offending rows as structured fields so a caller can
31
31
  * render them; the message itself names every row AND the repair.
32
32
  */
@@ -35,7 +35,7 @@ export declare class CatalogInvariantError extends Error {
35
35
  constructor(violations: ReadonlyArray<CatalogInvariantViolation>);
36
36
  }
37
37
  /**
38
- * Read every live status/priority row and return the invariant violations.
38
+ * Read every live status/priority/kind row and return the invariant violations.
39
39
  * A pure read — it never writes, so it is safe against any store, including
40
40
  * the live one.
41
41
  *
@@ -46,7 +46,7 @@ export declare class CatalogInvariantError extends Error {
46
46
  */
47
47
  export declare function inspectCatalogInvariants(adapter: StoreAdapter): Promise<CatalogInvariantViolation[]>;
48
48
  /**
49
- * Assert the store's status/priority catalog satisfies both invariants.
49
+ * Assert the store's status/priority/kind catalog satisfies both invariants.
50
50
  *
51
51
  * Passes (no throw) on an empty store, and on a catalog whose reserved
52
52
  * terminal statuses are flagged and whose same-kind names are fold-unique.
@@ -62,7 +62,7 @@ export declare function inspectCatalogInvariants(adapter: StoreAdapter): Promise
62
62
  * unrelated write — into an outage.
63
63
  *
64
64
  * BOUNDED BY CONSTRUCTION: it must not scan the issue graph per call. It reads
65
- * only the (small) status/priority catalog — see
65
+ * only the (small) status/priority/kind catalog — see
66
66
  * {@link inspectCatalogInvariants}.
67
67
  */
68
68
  export declare function assertCatalogInvariants(adapter: StoreAdapter): Promise<void>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * vocabulary.ts — the ONE pinned list of the data verbs this package mounts
3
+ * (SPEC.md §6.7), plus nothing else.
4
+ *
5
+ * ## Why this is its own leaf module
6
+ *
7
+ * `BACKLOG_VERBS` is read by two callers that must never drift apart:
8
+ *
9
+ * - `server.ts` — the mount-time carve-out check and the four-transport
10
+ * surface projection; and
11
+ * - `query/views/catalog.ts` — the generated `verb` catalog, which must
12
+ * enumerate the SAME advertised surface a consumer reads.
13
+ *
14
+ * If `catalog.ts` imported `server.ts` for the list, the whole query layer
15
+ * would transitively load the server's fastify/MCP plugin graph (a runtime
16
+ * cycle `server → api → query → catalog → server`), which is both a startup
17
+ * cost and a needless coupling. Declaring the constant in a module with zero
18
+ * imports keeps `catalog.ts` on the leaf and lets `server.ts` re-export the
19
+ * identical binding, so there is still exactly ONE list.
20
+ *
21
+ * The comment body below is reproduced verbatim from `server.ts` (its former
22
+ * home) so the "why this list is pinned" rationale stays attached to the
23
+ * value rather than the file it happens to live in.
24
+ */
25
+ /**
26
+ * SPEC.md §6.7 — the data verbs the whole surface consolidates onto:
27
+ * the nine issue verbs plus `lookup`, §3a's registry CRUD verbs, and the
28
+ * three §5 stats/rollup reads (`priority-matrix`/`part-of-rollup`/
29
+ * `open-curve`), mounted as `backlog_<verb>`. Pinned here, next to the
30
+ * carve-out it is the complement of, because it is the ONE list four separate
31
+ * surfaces are checked against: the three apigen mounts derive their names
32
+ * from the operation descriptors via `describeMountedSurface`, and `cli.ts`'s
33
+ * argv parser — which is deliberately NOT an apigen mount, because apigen's
34
+ * `parseArgs` cannot express the §2.1b positional form, projects `string[]`
35
+ * as a JSON-valued flag where §7.3 wants comma-separated, and only sets
36
+ * `process.exitCode` on a thrown `ApiError` (so an `ok:false` envelope would
37
+ * exit 0, contradicting `BACKLOG_EXIT_CODE`) — has to be checked against this
38
+ * list rather than derived from the mount.
39
+ *
40
+ * That asymmetry is exactly how a split brain starts, and this repo already
41
+ * has one open as BUG-BACKLOG-MCP-CLI-SPLIT-BRAIN-001. `server.verbs.spec.ts`
42
+ * asserts BOTH sides against this constant so a verb added to one surface and
43
+ * forgotten on the other fails a test instead of shipping.
44
+ *
45
+ * Order is the SPEC.md §4/§6 declaration order, not alphabetical; compare as
46
+ * sets.
47
+ */
48
+ export declare const BACKLOG_VERBS: readonly string[];
@@ -0,0 +1,139 @@
1
+ /**
2
+ * anchor-check.ts — the cheap-first mechanical anchor ladder (C3, DESIGN §2
3
+ * Primitive 2).
4
+ *
5
+ * An attestation's `anchor` is a `locator + digest`. This module parses the
6
+ * closed locator grammar and resolves a `path:` anchor against the subject
7
+ * project's git work tree, in the cheapest useful order:
8
+ *
9
+ * 1. `existsAtHead` — `git cat-file -e HEAD:<relpath>` (exit 0 =
10
+ * present). No file read, no network, no agent.
11
+ * 2. `changedSinceFiling`— `git log --since=<t> -- <relpath>` non-empty.
12
+ * 3. full re-resolve — ONLY when `opts.full` is set (`recheck`, and the
13
+ * verdict's own rung 5): hash the HEAD blob and compare to the anchor's
14
+ * `digest`.
15
+ *
16
+ * Every outcome carries the rung that produced it (`method`), so `verified` /
17
+ * `stale` / `unknown` / `unverified` are EXPLICIT states with a `reason` —
18
+ * never an absent field, never a silent success.
19
+ *
20
+ * The whole module is pure with respect to the store (no writes); it shells to
21
+ * the local `git` CLI the repo already uses. A missing `git`, a non-repo root,
22
+ * or an unresolvable path degrades to `unknown`, never a thrown exception —
23
+ * `unknown` is a first-class value here (Wikidata deprecate-with-reason, C2PA
24
+ * claim-vs-assertion). `refuted` is deliberately absent: a mechanical anchor
25
+ * proves present/matching/stale/absent only, never "the claim is contradicted".
26
+ */
27
+ /** The closed set of mechanical-check states. `refuted` is deliberately absent (see module header). */
28
+ export type AttestationCheckState = 'unverified' | 'verified' | 'stale' | 'unknown';
29
+ /**
30
+ * One rung-result of the anchor ladder. `method` names which rung actually ran
31
+ * (`'exists_at_head'` | `'changed_since'` | `'full_resolve'` | `'none'`), so a
32
+ * cheap pre-filter can be proven cheap (AC7) and a full re-resolve is visible.
33
+ */
34
+ export interface IAttestCheck {
35
+ state: AttestationCheckState;
36
+ method: string;
37
+ checked_at: string;
38
+ checked_by: string;
39
+ reason?: string;
40
+ }
41
+ /**
42
+ * An anchor is `locator + digest`. The locator grammar is closed:
43
+ * `path:<file>[:<line>]` | `url:<url>` | `query:<cql>` | `registry:<ref>` |
44
+ * `commit:<sha>` (C5: ancestry of the default branch).
45
+ * A bare hand-maintained `path:line` is INSUFFICIENT without a `digest` — the
46
+ * digest is the whole point of content-addressing the claim.
47
+ */
48
+ export interface IAttestationAnchor {
49
+ locator: string;
50
+ digest: string;
51
+ }
52
+ /** The parsed form of a locator (one arm per grammar production). */
53
+ export type IParsedAnchor = {
54
+ scheme: 'path';
55
+ target: string;
56
+ line?: number;
57
+ } | {
58
+ scheme: 'url';
59
+ target: string;
60
+ } | {
61
+ scheme: 'query';
62
+ target: string;
63
+ } | {
64
+ scheme: 'registry';
65
+ target: string;
66
+ } | {
67
+ scheme: 'commit';
68
+ target: string;
69
+ } | {
70
+ scheme: 'revision';
71
+ target: string;
72
+ };
73
+ /**
74
+ * The closed set of locator SCHEMES the grammar names (C3 added
75
+ * `path|url|query|registry`; C5 adds `commit:<sha>`; C10 adds `revision:<uid>`).
76
+ * Kept as an exported alias so a consumer (the C5 gate) can branch on the
77
+ * scheme without re-listing it.
78
+ */
79
+ export type AnchorLocatorKind = 'path' | 'url' | 'query' | 'registry' | 'commit' | 'revision';
80
+ /** Options for {@link checkAnchor}. */
81
+ export interface ICheckAnchorOptions {
82
+ /** The subject project's root (git work tree) — absent means `unknown`. */
83
+ root?: string;
84
+ /** ISO filing time for the changed-since rung. Absent skips rung 2. */
85
+ sinceISO?: string;
86
+ /** Run the full re-resolve (rung 3: hash HEAD content and compare to `digest`). */
87
+ full?: boolean;
88
+ now: string;
89
+ by: string;
90
+ }
91
+ /**
92
+ * Parse `locator` against the closed grammar, throwing
93
+ * {@link AnchorLocatorInvalidError} for anything outside it (a blank locator,
94
+ * an unknown scheme, an empty body). `path:` accepts an optional trailing
95
+ * `:<digits>` line number; every other scheme carries its body verbatim.
96
+ */
97
+ export declare function parseAnchor(locator: string): IParsedAnchor;
98
+ /** Whether `root` is (inside) a git work tree — the precondition for rungs 1–3. */
99
+ export declare function isGitWorkTree(root: string): boolean;
100
+ /**
101
+ * Rung 1 — is `relpath` present at `HEAD`? Exit 0 = present; any other exit
102
+ * (missing object, not a repo) = not present. Cheap: no content is read.
103
+ */
104
+ export declare function existsAtHead(root: string, relpath: string): boolean;
105
+ /**
106
+ * Rung 2 — did any commit touch `relpath` at/after `sinceISO`? `git log
107
+ * --since` compares against committer date, so this is a genuine "has the
108
+ * tracked file moved since we filed" probe with no content read.
109
+ */
110
+ export declare function changedSinceFiling(root: string, relpath: string, sinceISO: string): boolean;
111
+ /** The mechanical check a `commit:` anchor names — the one string every refusal carries as `performed_check`. */
112
+ export declare const DEFAULT_BRANCH_ANCESTOR_CHECK: "default_branch_ancestor";
113
+ /** The result of the default-branch check: whether `sha` is an ancestor of the resolved default branch, and the named check that produced it. */
114
+ export interface IDefaultBranchCheck {
115
+ onDefaultBranch: boolean;
116
+ performed: string;
117
+ }
118
+ /**
119
+ * Is `<sha>` an ancestor of the repo's default branch? Shells
120
+ * `git merge-base --is-ancestor <sha> <default>`, exactly as C3's anchor ladder
121
+ * shells `git cat-file`/`git log`. Returns `{onDefaultBranch:false,
122
+ * performed:'default_branch_ancestor'}` when the repo or the branch cannot be
123
+ * resolved (fail-closed, never a silent pass) — the `performed` field is
124
+ * present in every outcome so the caller can name the mechanical check.
125
+ */
126
+ export declare function isShaOnDefaultBranch(repoRoot: string, sha: string, defaultBranch?: string): Promise<IDefaultBranchCheck>;
127
+ /**
128
+ * Run the cheap-first ladder against `anchor` and return the rung-result.
129
+ *
130
+ * A non-`path` locator (`url:`/`query:`/`registry:`) has no mechanical checker
131
+ * wired, so it is explicitly `unverified` — never an absent field. A `path:`
132
+ * anchor with no resolvable root, or a root that is not a git work tree, is
133
+ * `unknown` (a genuine "could not determine", distinct from "gone").
134
+ *
135
+ * Throws {@link AnchorLocatorInvalidError} (a caller-input mistake) when the
136
+ * locator is outside the closed grammar; every other failure is a reported
137
+ * state, not a throw.
138
+ */
139
+ export declare function checkAnchor(anchor: IAttestationAnchor, opts: ICheckAnchorOptions): IAttestCheck;
@@ -0,0 +1,64 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { AttestationCheckState, IAttestCheck, IAttestationAnchor } from './anchor-check.js';
3
+
4
+ export type { AttestationCheckState, IAttestCheck, IAttestationAnchor };
5
+ /**
6
+ * An issue revision reference: the numeric monotonic counter, or — for C10's
7
+ * `SPEC`-node annotations — an opaque `sha256:<hex>` token. Additive only: an
8
+ * `issue` subject with a numeric revision is byte-for-byte unchanged.
9
+ */
10
+ export type AttestRevisionRef = number | string;
11
+ /** The claim an attestation asserts. `kind` is open vocabulary (e.g. `'published-artifact'`, `'live-system'`); `body` is free text. */
12
+ export interface IAttestClaim {
13
+ kind: string;
14
+ body?: string;
15
+ }
16
+ export interface IAttestInput {
17
+ /** The LOGICAL subject: chain-head `uid` + the content `revision` observed. */
18
+ subject: {
19
+ id: string;
20
+ revision: AttestRevisionRef;
21
+ };
22
+ claim: IAttestClaim;
23
+ anchor: IAttestationAnchor;
24
+ by: string;
25
+ }
26
+ export interface IAttestOutcome {
27
+ attestationUid: string;
28
+ subject: {
29
+ id: string;
30
+ revision: AttestRevisionRef;
31
+ };
32
+ check: IAttestCheck;
33
+ }
34
+ export interface IRecheckInput {
35
+ attestationUid: string;
36
+ by: string;
37
+ }
38
+ export interface IRecheckOutcome {
39
+ attestationUid: string;
40
+ checks: IAttestCheck[];
41
+ }
42
+ /**
43
+ * Create an attestation — a new `attestation` node + `attests` edge against
44
+ * the LIVE subject issue, plus one `audit` row. The subject node is NEVER
45
+ * mutated (no `UPDATE node … WHERE rowid = subject`).
46
+ *
47
+ * Errors: `InvalidArgumentError` (blank `subject.id`/`claim.kind`/`by`, an
48
+ * absent `subject.revision`), `AnchorLocatorInvalidError` (a locator outside
49
+ * the closed grammar, or a blank `anchor.digest`),
50
+ * `StaleSupersedeError`/`IssueNotFoundError` (the subject uid is superseded or
51
+ * unknown), `WriteContentionError`/`WriteIOError` (an exhausted driver-level
52
+ * retry on the underlying `immediate` transaction).
53
+ */
54
+ export declare function attest(handle: IWriteStoreHandle, input: IAttestInput): Promise<IAttestOutcome>;
55
+ /**
56
+ * Re-run the anchor ladder against an existing attestation and APPEND the new
57
+ * check to `checks[]` (earlier entries copied forward verbatim — never a
58
+ * truncated history). Replaces the echoed `check` with the newest entry and
59
+ * writes one `audit` row on the attestation node.
60
+ *
61
+ * Errors: `InvalidArgumentError` (blank `by`), `AttestationNotFoundError` (no
62
+ * live `attestation` node carries the uid).
63
+ */
64
+ export declare function recheck(handle: IWriteStoreHandle, input: IRecheckInput): Promise<IRecheckOutcome>;
@@ -1,8 +1,8 @@
1
1
  import { IWriteStoreHandle } from './tx.js';
2
2
  import { NodeRecord } from '@adhd/sox-graph-store';
3
3
 
4
- /** The two catalog kinds this repair collapses (D3's whole scope). */
5
- export type CatalogKind = 'status' | 'priority';
4
+ /** The catalog kinds this repair collapses (D3's scope, widened to `kind` by C8). */
5
+ export type CatalogKind = 'status' | 'priority' | 'kind';
6
6
  /**
7
7
  * A status fold group that CANNOT be safely collapsed: two or more LIVE rows
8
8
  * share a fold, yet none carries the write path's lowercase spelling. Merging
@@ -75,16 +75,23 @@ export interface IMergeJournal {
75
75
  *
76
76
  * Grouping is by {@link catalogNameFold} alone. Within a fold group the
77
77
  * canonical is the member already carrying the write path's spelling for that
78
- * kind — lowercase for `status`, uppercase for `priority`.
78
+ * kind — lowercase for `status`, uppercase for `priority`, lowercase for the
79
+ * OPEN `kind` vocabulary (its canonical rule).
79
80
  *
80
81
  * A `priority` group with no upper-spelled member falls back to its
81
82
  * 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.
83
+ * before). A `status` OR `kind` group with no lower-spelled member is NOT
84
+ * planned at all: any canonical would carry a spelling the write path / fold
85
+ * lookup would not re-find, so merging would regenerate the fragment on the
86
+ * next ordinary `create`. It is surfaced in `unmergeable` instead.
87
+ *
88
+ * `liveKinds` is OPTIONAL and ADDITIVE (C8): existing two-argument callers
89
+ * keep their exact behavior, and a caller that wants the `kind` repair passes
90
+ * the live `kind` rows. (A kind cannot be planned without its rows entering
91
+ * somewhere; the spec's "unchanged signature" is honoured as "unchanged for
92
+ * every existing caller".)
86
93
  */
87
- export declare function planCaseFragmentMerge(liveStatuses: readonly NodeRecord[], livePriorities: readonly NodeRecord[]): IMergePlan;
94
+ export declare function planCaseFragmentMerge(liveStatuses: readonly NodeRecord[], livePriorities: readonly NodeRecord[], liveKinds?: readonly NodeRecord[]): IMergePlan;
88
95
  /**
89
96
  * Apply the plan in ONE `executeWriteTransaction` (`BEGIN IMMEDIATE` + bounded
90
97
  * busy-only retry, §4c) — every group in the plan commits together or not at
@@ -62,6 +62,16 @@ export interface IMintOrResolveInput {
62
62
  * of any one issue's logical write, so they keep their own clock.
63
63
  */
64
64
  at?: string;
65
+ /**
66
+ * A DEPRECATED catalog name
67
+ * (`meta.lifecycle === 'deprecated'`, or a `kind` in the frozen
68
+ * {@link DEPRECATED_KIND_NAMES} set) is REFUSED on mint/resolve unless this
69
+ * is `true` — a typed caller decision, never an env var. Refusal throws
70
+ * `InvalidArgumentError('kind', …)` naming `meta.replacedBy` (or the in-code
71
+ * {@link DEPRECATED_KIND_REPLACEMENTS} fallback), so the caller is told the
72
+ * term to use instead. Only `kind` currently has a deprecated vocabulary.
73
+ */
74
+ allowDeprecated?: boolean;
65
75
  }
66
76
  /**
67
77
  * The reserved TERMINAL status vocabulary — the ONE in-code definition
@@ -94,6 +104,29 @@ export interface IMintOrResolveInput {
94
104
  export declare const RESERVED_TERMINAL_STATUS_NAMES: ReadonlySet<string>;
95
105
  /** 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. */
96
106
  export declare function isReservedTerminalStatusName(name: string): boolean;
107
+ /**
108
+ * The RETIRED `kind` vocabulary — the borrowed terms the C8 spec retires with
109
+ * a replacement, modelled on {@link RESERVED_TERMINAL_STATUS_NAMES}: a frozen
110
+ * in-code set, so a retired term is refused on mint even before (or without) a
111
+ * row-level `meta.lifecycle:'deprecated'` data change. Membership is decided on
112
+ * the FOLDED name ({@link catalogNameFold}), so `EPIC`/`epic` are one term.
113
+ *
114
+ * `EPIC` is the one entry: it is a borrowed Jira primitive re-expressed by the
115
+ * existing `FEAT` kind (DESIGN §2 Invariant 6 / §6 "re-express in place, never
116
+ * re-key"). The replacement lives in {@link DEPRECATED_KIND_REPLACEMENTS}.
117
+ */
118
+ export declare const DEPRECATED_KIND_NAMES: ReadonlySet<string>;
119
+ /**
120
+ * The term to use instead of each {@link DEPRECATED_KIND_NAMES} entry. Kept
121
+ * separate from the name set so a row's live `meta.replacedBy` (a reviewed
122
+ * repair's own decision) is preferred when present, with this map as the
123
+ * in-code fallback the error and the generated catalog both name.
124
+ */
125
+ export declare const DEPRECATED_KIND_REPLACEMENTS: ReadonlyMap<string, string>;
126
+ /** Whether `name` is a RETIRED `kind` term under the case fold — `EPIC`/`epic` both count. */
127
+ export declare function isDeprecatedKindName(name: string): boolean;
128
+ /** The in-code replacement for a deprecated `kind` name, or `undefined` when it is not one. */
129
+ export declare function deprecatedKindReplacement(name: string): string | undefined;
97
130
  /**
98
131
  * The flat-catalog find-then-create (§1, §4c, §6.1): `kind`/`status`/
99
132
  * `priority`/`agent` are mintable on an unresolved NAME; a uid-shaped `ref`
@@ -226,7 +259,30 @@ export interface IProjectPolicy {
226
259
  readonly allowedKinds: readonly string[];
227
260
  /** `project_field_requirement` — field names required on every mutating write for this project (§2). */
228
261
  readonly requiredFields: readonly string[];
262
+ /**
263
+ * C9 — how wide the CREATE-TIME advisory similarity scan looks. `'same-project'`
264
+ * (the default) is byte-for-byte today's behaviour (AC1); `'multi-project'`
265
+ * adds sibling projects reachable via a shared `repoUrl`/`component.meta.path`;
266
+ * `'store-wide'` drops the id restriction entirely. This NEVER widens the
267
+ * WRITE path — the scan is advisory and writes nothing (AC3) — it only widens
268
+ * which existing items may be surfaced as similar candidates.
269
+ */
270
+ readonly similarityScope: ISimilarityScope;
271
+ /**
272
+ * C9 — cosine threshold for CROSS-project candidates. Deliberately a DISTINCT
273
+ * value from `dedupeThreshold` (which stays calibrated for same-project
274
+ * byte-identical refiles): sharing one number either re-suppresses legitimate
275
+ * cross-repo filings or admits boilerplate (AC7/AC8). Enforced distinct by
276
+ * test, not by a runtime assertion.
277
+ */
278
+ readonly similarityCrossProjectThreshold: number;
279
+ /** C9 — minimum top-vs-next cosine gap for a cross-project candidate (margin guard). A candidate that is not meaningfully more similar than the runner-up is ambiguous and is not surfaced. */
280
+ readonly similarityCrossProjectMargin: number;
281
+ /** C9 — minimum title-token Jaccard overlap required as the second independent signal for a cross-project candidate (AC7). Cosine alone is never sufficient cross-project. */
282
+ readonly similarityCrossProjectTokenOverlap: number;
229
283
  }
284
+ /** C9 — the typed scan breadth for the create-time advisory similarity scan. */
285
+ export type ISimilarityScope = 'same-project' | 'multi-project' | 'store-wide';
230
286
  export declare function resolveProjectPolicy(project: IResolvedProjectRow): IProjectPolicy;
231
287
  /**
232
288
  * Whether a resolved project has a known filesystem `path` (§8.4/§8.5) — the
@@ -356,6 +412,13 @@ export declare function upsertComponent(handle: IWriteStoreHandle, input: IUpser
356
412
  * runs before opening its transaction.
357
413
  */
358
414
  export declare function upsertComponentTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
415
+ /**
416
+ * The closed `location_type` vocabulary (§3, §3a). The ONE in-code definition:
417
+ * `upsertLocation` validates against it, and `query/views/catalog.ts`'s
418
+ * generated `location_type` catalog projects from it. Exported (rather than a
419
+ * private const) so the catalog cannot become a second, hand-maintained copy.
420
+ */
421
+ export declare const VALID_LOCATION_TYPES: readonly ILocationType[];
359
422
  export interface IUpsertLocationInput {
360
423
  /**
361
424
  * `component` reference — `uid` or `name` (§6.1's disambiguation-by-shape
@@ -131,3 +131,34 @@ export interface IResolvedCitationTarget {
131
131
  * candidate).
132
132
  */
133
133
  export declare function resolveCitationTarget(projectRoot: string, file: string, allowedRoots: readonly string[]): Promise<IResolvedCitationTarget>;
134
+ /**
135
+ * The TOOL's OWN installed evidence roots — where this package installs its
136
+ * skill (`install-skill.ts`'s host table: `.claude/skills/backlog`,
137
+ * `$CODEX_HOME/skills/backlog`, `~/.config/opencode/skills/backlog`).
138
+ *
139
+ * These are citable by DEFAULT, and deliberately NARROWER than the
140
+ * machine-global `~/.adhd/backlog` store home this module's header rejects:
141
+ * they grant only the tool's own installed docs (a stable, versioned,
142
+ * human-authored evidence tree), never the store. Typed and always-on, so a
143
+ * project does not have to allowlist the tool it is using in order to cite
144
+ * the tool's own documentation.
145
+ *
146
+ * `home` is injectable for tests; it defaults to the real home directory.
147
+ */
148
+ export declare function toolOwnedCitationRoots(home?: string): string[];
149
+ /** The minimal read-only executor {@link resolveSiblingProjectRootsTx} needs — satisfied structurally by `AdapterTransaction` and the bare `StoreAdapter`. */
150
+ export interface ISiblingRootExecutor {
151
+ executeAll<T = Record<string, unknown>>(sql: string, args?: unknown[]): Promise<{
152
+ rows: T[];
153
+ }>;
154
+ }
155
+ /**
156
+ * The `metadata.path` of every LIVE `project` row EXCEPT `excludeProjectUid`.
157
+ *
158
+ * A citation into a *sibling registered project* must be probed against that
159
+ * project's OWN root, not the citing item's — otherwise a legitimate
160
+ * cross-repo citation falsely resolves to `unverified` and the write is
161
+ * refused (AC4). Path-less sibling projects contribute nothing (there is no
162
+ * root to check against).
163
+ */
164
+ export declare function resolveSiblingProjectRootsTx(exec: ISiblingRootExecutor, excludeProjectUid: string): Promise<string[]>;
@@ -0,0 +1,157 @@
1
+ import { IWriteStoreHandle } from './tx.js';
2
+ import { IProjectPolicy, IResolvedProjectRow } from './catalog.js';
3
+ import { ICitation, ICitationRecord } from '../citation.js';
4
+ import { AdapterTransaction } from '@adhd/sox-store-adapter';
5
+
6
+ /**
7
+ * §8.5's two-branch citation-sha rule, in ONE place (the generalization of the
8
+ * copy `create-issue.ts`/`transition.ts` each used to carry).
9
+ *
10
+ * Branch 1: a PATH-LESS project cannot content-address anything, so every
11
+ * citation degrades to the `'unverified'` sentinel up front.
12
+ *
13
+ * Branch 2a (revision-pinned): when `revision` is given, the target is resolved
14
+ * against the git revision (`git show <revision>:<relpath>`) rather than the
15
+ * working tree — so a file that exists only on an unmerged branch is citable.
16
+ * The read is confined to the project's own git object database (git refuses a
17
+ * `..` path), so no filesystem containment check is needed for it.
18
+ *
19
+ * Branch 2b (working tree): the target must resolve (canonically) within the
20
+ * project root OR within one of `allowedExternalRoots` —
21
+ * `citation-path.ts`'s {@link resolveCitationTarget}. The only filesystem read
22
+ * happens after acceptance.
23
+ *
24
+ * §4c's error taxonomy is preserved exactly: ENOENT/ENOTDIR → `'unverified'`
25
+ * (the file genuinely is not there); EISDIR → `CitationTargetIsDirectoryError`;
26
+ * any other errno → `WriteIOError` (`citationReadError`).
27
+ */
28
+ export declare function computeCitationSha(project: IResolvedProjectRow, file: string, revision: string | undefined, allowedExternalRoots: readonly string[], siblingRoots?: readonly string[]): Promise<string>;
29
+ /**
30
+ * Compute the sha for every citation in `citations`, in caller order — NO
31
+ * policy gate (see {@link assertCitationShasVerifiable}). This is the
32
+ * filesystem/git half of the citation-sha pre-resolve, callable on its own by a
33
+ * caller (`update`'s diff-emitter) that must defer the gate until it knows
34
+ * which citations are actually being ADDED.
35
+ */
36
+ export declare function computeCitationShas(project: IResolvedProjectRow, citations: readonly ICitation[], allowedExternalRoots: readonly string[], siblingRoots?: readonly string[]): Promise<string[]>;
37
+ /**
38
+ * Enforce `project_policy.citationRequiresSha` over an already-computed sha
39
+ * list, paired index-for-index with `citations`. Only where verification is
40
+ * POSSIBLE (the project has a known `path`) is a `"unverified"` sha a hard
41
+ * `CitationUnverifiableError` naming the allowed roots; a path-less project
42
+ * records `sha:"unverified"` verbatim, with the deliberate waiver logged (DEBT
43
+ * a934e089) rather than silently applied. `verb` names the caller in that log.
44
+ *
45
+ * Split from {@link computeCitationShas} so `update`'s diff-emitter can compute
46
+ * shas for its whole desired set but gate ONLY the citations it actually adds —
47
+ * so an issue holding a citation whose file has since vanished can still be
48
+ * updated (that already-live citation is neither re-added nor re-gated; only a
49
+ * genuinely new, unverifiable citation is refused).
50
+ */
51
+ export declare function assertCitationShasVerifiable(project: IResolvedProjectRow, policy: IProjectPolicy, citations: readonly ICitation[], shas: readonly string[], verb: string): void;
52
+ /**
53
+ * Compute AND policy-gate the sha for every citation in `citations`, in caller
54
+ * order — the ONE implementation `createIssue`/`transition`'s pre-transaction
55
+ * citation loop and `addCitation`'s single-citation resolve share. Composes
56
+ * {@link computeCitationShas} + {@link assertCitationShasVerifiable}.
57
+ */
58
+ export declare function resolveCitationShas(project: IResolvedProjectRow, policy: IProjectPolicy, citations: readonly ICitation[], siblingRoots: readonly string[], verb: string): Promise<string[]>;
59
+ /** Mint ONE `citation` node + its `has_citation` edge against the issue, inside the caller's open `tx`. NEVER writes an audit row — the calling verb owns the ONE audit its §4a contract requires. */
60
+ export declare function appendCitationTx(tx: AdapterTransaction, handle: IWriteStoreHandle, params: {
61
+ issueRowid: number;
62
+ issueUid: string;
63
+ citation: ICitation;
64
+ sha: string;
65
+ at: string;
66
+ }): Promise<{
67
+ rowid: number;
68
+ uid: string;
69
+ }>;
70
+ /** A live citation attached to an issue — just the identity + diff key the diff-emitter needs. */
71
+ export interface ILiveCitationRow {
72
+ rowid: number;
73
+ uid: string;
74
+ /** {@link citationKey}-shaped (`file` + `lines`), read off the persisted `target`/`line`. */
75
+ key: string;
76
+ }
77
+ /**
78
+ * The LIVE citations currently attached to `issueRowid`, in edge order — the
79
+ * "live set" `update`'s diff-emitter compares a desired set against. A live
80
+ * `has_citation` edge whose target node is missing or invalidated is skipped
81
+ * defensively (the invariant is that the two are invalidated together, so this
82
+ * should never fire).
83
+ */
84
+ export declare function readLiveCitationsTx(tx: AdapterTransaction, issueRowid: number): Promise<ILiveCitationRow[]>;
85
+ /**
86
+ * Bi-temporally invalidate ONE citation: its node (`t_invalid` + a merged
87
+ * `meta.invalidatedReason`/`invalidatedAt`, never a wholesale replace — the
88
+ * `rmLocation`/`delete` shape) AND its owning `has_citation` edge. Hand-
89
+ * composed against `tx`.
90
+ *
91
+ * Never writes an audit row — the calling verb owns it (so `update` emits its
92
+ * single `'updated'` audit for a whole diff rather than one per citation).
93
+ */
94
+ export declare function removeCitationTx(tx: AdapterTransaction, params: {
95
+ citationRowid: number;
96
+ citationUid: string;
97
+ at: string;
98
+ reason?: string;
99
+ }): Promise<void>;
100
+ export interface IAddCitationInput {
101
+ /** The `issue` uid the citation is attached to. */
102
+ uid: string;
103
+ /** The citation to add — the SAME {@link ICitation} contract `create`/`transition` accept, including the optional `revision`. */
104
+ citation: ICitation;
105
+ /** The acting identity (§6.3's opening rule). REQUIRED. */
106
+ by: string;
107
+ }
108
+ export interface IAddCitationOutcome {
109
+ /** The minted `citation` node's own uid — the identity `removeCitation` addresses. */
110
+ uid: string;
111
+ /** The issue the citation was attached to (unchanged). */
112
+ issueUid: string;
113
+ /** The minted citation, projected (the read shape). */
114
+ citation: ICitationRecord;
115
+ }
116
+ /**
117
+ * Add ONE citation to an existing issue (§6.3, §4). One `immediate`
118
+ * transaction: resolve `uid` → live `issue` → mint the `citation` node + its
119
+ * `has_citation` edge → `writeAudit`, all against the SAME `tx` (never the
120
+ * bare-adapter `writeNode`/`writeEdge`, §4c). The cited file's sha is computed
121
+ * BEFORE the transaction opens, exactly as `create`/`transition` do, so the
122
+ * write lock is never held across a filesystem/git read.
123
+ *
124
+ * Errors: `InvalidArgumentError` (blank `uid`/`by`/`citation.file`),
125
+ * `IssueNotFoundError` (no live `issue` carries `uid`), `StaleSupersedeError`
126
+ * (`uid` names a superseded issue), `CitationUnverifiableError` (the target did
127
+ * not resolve to a real sha and the owning project requires one — including a
128
+ * `revision` that does not resolve), `WriteContentionError`/`WriteIOError`
129
+ * (§4c).
130
+ */
131
+ export declare function addCitation(handle: IWriteStoreHandle, input: IAddCitationInput): Promise<IAddCitationOutcome>;
132
+ export interface IRemoveCitationInput {
133
+ /** The `citation` uid to soft-remove — the citation's OWN uid, never a `(target,line)` re-specification. */
134
+ uid: string;
135
+ /** The acting identity (§6.3's opening rule). REQUIRED. */
136
+ by: string;
137
+ /** Optional explanation recorded on the invalidation and the audit row (mirrors `rmLocation`'s `reason`). */
138
+ reason?: string;
139
+ }
140
+ export interface IRemoveCitationOutcome {
141
+ uid: string;
142
+ invalidated: true;
143
+ }
144
+ /**
145
+ * Soft-remove ONE citation by its own `uid` (§6.3, §4). One `immediate`
146
+ * transaction: resolve `uid` → LIVE `citation` node (`resolveCitationNodeTx`,
147
+ * never a bare `getNodeByUid`) → bi-temporally invalidate the node AND its
148
+ * `has_citation` edge ({@link removeCitationTx}) → `writeAudit`, all against the
149
+ * SAME `tx`. The citation's ISSUE is untouched: its uid is preserved and no
150
+ * node is superseded.
151
+ *
152
+ * Errors: `InvalidArgumentError` (blank `uid`/`by`), `CitationNotFoundError`
153
+ * (no LIVE `citation` node carries `uid` — including an already-removed uid, or
154
+ * a uid that names a node of a different kind, the "foreign citation" refusal),
155
+ * `WriteContentionError`/`WriteIOError` (§4c).
156
+ */
157
+ export declare function removeCitation(handle: IWriteStoreHandle, input: IRemoveCitationInput): Promise<IRemoveCitationOutcome>;