@adhd/backlog 0.1.8 → 1.0.0
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/CHANGELOG.md +93 -44
- package/README.md +332 -81
- package/api.d.ts +146 -0
- package/cli.d.ts +45 -18
- package/env.d.ts +23 -3
- package/envelope.d.ts +163 -0
- package/index.d.ts +11 -10
- package/index.js +531 -173
- package/index.mjs +29814 -15647
- package/install-skill.d.ts +23 -0
- package/package.json +50 -15
- package/query/card.d.ts +31 -0
- package/query/get.d.ts +11 -0
- package/query/index.d.ts +67 -0
- package/query/markdown.d.ts +11 -0
- package/query/query.d.ts +131 -0
- package/query/resolve.d.ts +123 -0
- package/query/types.d.ts +450 -0
- package/query/views/registry.d.ts +43 -0
- package/query/views/semantic.d.ts +101 -0
- package/query/views/stats.d.ts +109 -0
- package/search-shortcut.d.ts +79 -0
- package/serve.d.ts +18 -0
- package/server.d.ts +139 -4
- package/skill/SKILL.md +619 -138
- package/store/graph-backlog-store.d.ts +80 -17
- package/store/immediate-retry.d.ts +24 -13
- package/store/type-policy.d.ts +4 -0
- package/store/vocabulary-guard.d.ts +52 -0
- package/version-info.d.ts +15 -0
- package/write/audit.d.ts +36 -0
- package/write/bootstrap.d.ts +123 -0
- package/write/catalog.d.ts +351 -0
- package/write/claim-lease.d.ts +21 -0
- package/write/claim.d.ts +80 -0
- package/write/create-issue.d.ts +250 -0
- package/write/delete.d.ts +39 -0
- package/write/embed-drain.d.ts +68 -0
- package/write/embedding-observer.d.ts +80 -0
- package/write/errors.d.ts +303 -0
- package/write/issue-status.d.ts +10 -0
- package/write/move.d.ts +70 -0
- package/write/relate.d.ts +52 -0
- package/write/transition.d.ts +60 -0
- package/write/tx.d.ts +344 -0
- package/write/update.d.ts +81 -0
- package/client.d.ts +0 -169
- package/markdown.d.ts +0 -75
- package/migration-admin.d.ts +0 -26
- package/model.d.ts +0 -437
- package/store/audit-log.d.ts +0 -16
- package/store/claim.d.ts +0 -24
- package/store/crud.d.ts +0 -62
- package/store/ids.d.ts +0 -24
- package/store/lifecycle.d.ts +0 -36
- package/store/mapping.d.ts +0 -101
- package/store/mutate-metadata.d.ts +0 -8
- package/store/query.d.ts +0 -68
- package/store/repo-migration.d.ts +0 -51
- package/store/serve-lock.d.ts +0 -42
- package/store/structure.d.ts +0 -66
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { IComponentSummary, ILocationSummary, ILocationType, IProjectSummary } from '../query/types.js';
|
|
2
|
+
import { IEdgeKindRule, IWriteStoreHandle } from './tx.js';
|
|
3
|
+
import { AdapterTransaction } from '@adhd/sox-store-adapter';
|
|
4
|
+
|
|
5
|
+
/** Whether `ref` should be treated as a `uid` (exact resolve-or-throw) rather than a business `name` (find, and for the flat catalogs, find-then-mint). */
|
|
6
|
+
export declare function isUidShaped(ref: string): boolean;
|
|
7
|
+
export interface IResolvedCatalogRow {
|
|
8
|
+
rowid: number;
|
|
9
|
+
uid: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
export interface IResolvedProjectRow extends IResolvedCatalogRow {
|
|
13
|
+
metadata: Record<string, unknown> | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `project` (§1, §6.1): resolved-ONLY, `uid` or `name`, NEVER minted by an
|
|
17
|
+
* issue verb — minting a project is the explicit `upsertProject` registry
|
|
18
|
+
* verb's job alone (§3a), out of scope for this slice.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveProjectTx(tx: AdapterTransaction, ref: string): Promise<IResolvedProjectRow>;
|
|
21
|
+
/**
|
|
22
|
+
* `component`, scoped within `project` (§6.1, §6.3.2): resolved-ONLY, `uid`
|
|
23
|
+
* or `name` — an unresolved name throws `CatalogNotFoundError('component', name)`
|
|
24
|
+
* rather than silently forking a new component under the given project.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveComponentTx(tx: AdapterTransaction, input: {
|
|
27
|
+
projectUid: string;
|
|
28
|
+
ref: string;
|
|
29
|
+
}): Promise<IResolvedCatalogRow>;
|
|
30
|
+
/**
|
|
31
|
+
* `project`'s reserved default component, `(root)` (§3, §6.1, §6.3.2,
|
|
32
|
+
* §8 AC-23): what `createIssue` resolves to when `component` is omitted.
|
|
33
|
+
* A row `upsertProject` already guarantees is live for every project — NEVER
|
|
34
|
+
* minted here, in either case (§1). Resolved via `owns_project` (the SAME
|
|
35
|
+
* traversal §3a's registry uses), not a bare name lookup, so a `(root)` row
|
|
36
|
+
* belonging to a DIFFERENT project can never be mismatched onto this one.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveDefaultComponentTx(tx: AdapterTransaction, input: {
|
|
39
|
+
projectRowid: number;
|
|
40
|
+
}): Promise<IResolvedCatalogRow>;
|
|
41
|
+
export type FlatCatalogKind = 'kind' | 'status' | 'priority' | 'agent';
|
|
42
|
+
export interface IMintOrResolveInput {
|
|
43
|
+
catalogKind: FlatCatalogKind;
|
|
44
|
+
ref: string;
|
|
45
|
+
/** Metadata to seed onto a MINTED row only — never read or applied when `ref` resolves to an existing row. Evaluated lazily (only on an actual mint) since e.g. `priority`'s rank needs a fresh in-tx MAX query (§6.3.2). */
|
|
46
|
+
mintMetadata?: (tx: AdapterTransaction) => Promise<Record<string, unknown>>;
|
|
47
|
+
/**
|
|
48
|
+
* The caller's single logical-write timestamp, forwarded to `writeNodeTx` so a
|
|
49
|
+
* minted catalog row carries the SAME `t_created`/`t_valid` as the issue that
|
|
50
|
+
* caused the mint. Omitted, `writeNodeTx` stamps its own `nowISO()`, which is
|
|
51
|
+
* what produced per-row drift within one `createIssue`.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately NOT threaded into `resolveEdgeKindTx`'s `edge_kind` rows: those
|
|
54
|
+
* are schema-infrastructure rows describing the relation table itself, not part
|
|
55
|
+
* of any one issue's logical write, so they keep their own clock.
|
|
56
|
+
*/
|
|
57
|
+
at?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The flat-catalog find-then-create (§1, §4c, §6.1): `kind`/`status`/
|
|
61
|
+
* `priority`/`agent` are mintable on an unresolved NAME; a uid-shaped `ref`
|
|
62
|
+
* that does not resolve instead throws — minting NEVER applies to a uid.
|
|
63
|
+
*/
|
|
64
|
+
export declare function mintOrResolveCatalogTx(tx: AdapterTransaction, input: IMintOrResolveInput): Promise<IResolvedCatalogRow>;
|
|
65
|
+
/** `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
|
+
export declare function nextPriorityRankTx(tx: AdapterTransaction): Promise<number>;
|
|
67
|
+
/**
|
|
68
|
+
* §3's fixed edge table — the ONE declared shape for every `rel` (§2's
|
|
69
|
+
* closing statement: "the SAME check for every row, never two different
|
|
70
|
+
* checks"). `audits`' `source_kind: '*'` is the one declared sentinel (§2).
|
|
71
|
+
*/
|
|
72
|
+
export declare const EDGE_KIND_TABLE: readonly IEdgeKindRule[];
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the `edge_kind` catalog row for `rel`, inside `tx` — the SAME
|
|
75
|
+
* `tx.executeGet` lookup §1 describes for the VALIDATION half of every edge
|
|
76
|
+
* write. `edge_kind` is NEVER caller-mintable (§1, §6.1): every row is
|
|
77
|
+
* "seeded once from §3's fixed edge table," normally by a dedicated
|
|
78
|
+
* bootstrap/seed step (§8.6 step 2 is the ETL's own version of this same
|
|
79
|
+
* seeding).
|
|
80
|
+
*
|
|
81
|
+
* **Documented deviation for this slice.** This task ships the write-layer
|
|
82
|
+
* core only (`errors.ts`/`tx.ts`/`catalog.ts`/`audit.ts`/`create-issue.ts`) —
|
|
83
|
+
* no dedicated seed/bootstrap script exists yet to have run before
|
|
84
|
+
* `createIssue` is first called. Rather than make `createIssue` depend on an
|
|
85
|
+
* out-of-scope script, this function self-heals: on a miss, it seeds the
|
|
86
|
+
* row from {@link EDGE_KIND_TABLE} — the IDENTICAL fixed §3 shape a real seed
|
|
87
|
+
* script would write, never a caller-supplied one — inside the SAME `tx` the
|
|
88
|
+
* calling verb already opened. This satisfies "seeded once" (idempotent:
|
|
89
|
+
* the `tx.executeGet` above finds it on every subsequent call) without
|
|
90
|
+
* requiring a separate bootstrap step to exist first. A future seed script
|
|
91
|
+
* making this self-heal path dead code is expected and fine — the row shape
|
|
92
|
+
* it would find already-present is identical either way.
|
|
93
|
+
*
|
|
94
|
+
* **A malformed row (should never happen — only this module ever writes
|
|
95
|
+
* one) is guarded, never crashed on, and never left to accumulate.** `meta`
|
|
96
|
+
* is read via {@link parseMetaObject} — a non-JSON `meta` value degrades to
|
|
97
|
+
* `{}` instead of throwing a raw `SyntaxError` out of the transaction
|
|
98
|
+
* callback (Finding 1: unguarded, that would misclassify as a retryable
|
|
99
|
+
* `E_IO` at `executeWriteTransaction`'s boundary and retry forever against a
|
|
100
|
+
* deterministic parse failure). Either that degraded `{}`, or a
|
|
101
|
+
* successfully-parsed object missing one of the three expected keys, takes
|
|
102
|
+
* the SAME "malformed" branch below: the row is invalidated and a correct
|
|
103
|
+
* replacement is minted from {@link EDGE_KIND_TABLE} in the SAME `tx`, so the
|
|
104
|
+
* next call's SELECT (now a deterministic `ORDER BY rowid` — the prior
|
|
105
|
+
* ordering-free `LIMIT 1` made which duplicate got read unspecified, Finding
|
|
106
|
+
* 2) can never re-find the malformed row and re-mint yet another duplicate.
|
|
107
|
+
* The store converges to exactly one live `edge_kind` row per `rel` after
|
|
108
|
+
* this call, rather than growing an unbounded set of malformed duplicates.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveEdgeKindTx(tx: AdapterTransaction, rel: string): Promise<IEdgeKindRule>;
|
|
111
|
+
/**
|
|
112
|
+
* Per-project policy (§2): DATA, realized as `project.meta.metadata.policy`
|
|
113
|
+
* — §2's table has no dedicated node/edge of its own (it is 1:1 with
|
|
114
|
+
* `project`, never many-to-many), so it lives inside the SAME metadata blob
|
|
115
|
+
* `project`'s other declared fields (`repoUrl`, `monorepo`, …, §3a) already
|
|
116
|
+
* occupy. Every field defaults exactly as §2 states when the project carries
|
|
117
|
+
* no `policy` object at all (a project seeded before policy existed, or
|
|
118
|
+
* minimally).
|
|
119
|
+
*/
|
|
120
|
+
export interface IProjectPolicy {
|
|
121
|
+
readonly transitionRequiresNote: boolean;
|
|
122
|
+
readonly citationRequired: boolean;
|
|
123
|
+
readonly citationRequiresSha: boolean;
|
|
124
|
+
readonly defaultStatus?: string;
|
|
125
|
+
readonly defaultKind?: string;
|
|
126
|
+
readonly dedupeScanEnabled: boolean;
|
|
127
|
+
readonly dedupeThreshold: number;
|
|
128
|
+
readonly claimStaleAfterMin: number;
|
|
129
|
+
/** `project_status` — empty means "no restriction" (§2). */
|
|
130
|
+
readonly allowedStatuses: readonly string[];
|
|
131
|
+
/** `project_kind` — empty means "no restriction" (§2). */
|
|
132
|
+
readonly allowedKinds: readonly string[];
|
|
133
|
+
/** `project_field_requirement` — field names required on every mutating write for this project (§2). */
|
|
134
|
+
readonly requiredFields: readonly string[];
|
|
135
|
+
}
|
|
136
|
+
export declare function resolveProjectPolicy(project: IResolvedProjectRow): IProjectPolicy;
|
|
137
|
+
/**
|
|
138
|
+
* Whether a resolved project has a known filesystem `path` (§8.4/§8.5) — the
|
|
139
|
+
* precondition for citation content-addressing to be POSSIBLE at all. When it
|
|
140
|
+
* does not, `computeCitationSha` can only ever return the `"unverified"`
|
|
141
|
+
* sentinel (`create-issue.ts`/`transition.ts` both short-circuit on a
|
|
142
|
+
* non-string/empty `metadata.path`), so `project_policy.citationRequiresSha`
|
|
143
|
+
* has nothing to gate: the gate applies only where verification is possible.
|
|
144
|
+
* A path-less project therefore records `sha:"unverified"` verbatim, matching
|
|
145
|
+
* the ETL's own precedent (`tools/etl/citation.ts`). Kept beside
|
|
146
|
+
* {@link resolveProjectPolicy} — it reads the same resolved project row. The
|
|
147
|
+
* PREDICATE itself is shared by BOTH write paths so the `create`/`transition`
|
|
148
|
+
* gates never drift on WHICH projects are verifiable; the surrounding
|
|
149
|
+
* `computeCitationSha` hashing body is NOT shared — it stays a per-file copy
|
|
150
|
+
* in each verb (this package's established per-file-duplication convention —
|
|
151
|
+
* see `transition.ts`'s own doc comment on its copy).
|
|
152
|
+
*
|
|
153
|
+
* A TYPE PREDICATE, not a bare `boolean`: both `computeCitationSha` call sites
|
|
154
|
+
* (and the waiver log beside each gate) need the narrowed `metadata.path` as a
|
|
155
|
+
* `string` immediately after the guard, and a predicate is the one form that
|
|
156
|
+
* gives them that without a non-null assertion or a re-read of the same field.
|
|
157
|
+
*/
|
|
158
|
+
export declare function projectHasKnownPath(project: IResolvedProjectRow): project is IResolvedProjectRow & {
|
|
159
|
+
metadata: Record<string, unknown> & {
|
|
160
|
+
path: string;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
export interface IUpsertProjectInput {
|
|
164
|
+
/** The business key `project` is upserted by (§3a, §4). */
|
|
165
|
+
name: string;
|
|
166
|
+
path?: string;
|
|
167
|
+
repoUrl?: string;
|
|
168
|
+
monorepo?: boolean;
|
|
169
|
+
description?: string;
|
|
170
|
+
/** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
|
|
171
|
+
by: string;
|
|
172
|
+
}
|
|
173
|
+
export interface IUpsertProjectOutcome {
|
|
174
|
+
uid: string;
|
|
175
|
+
/** `false` when an existing LIVE `project` row with this `name` was found and updated in place; `true` only on a genuine first mint. */
|
|
176
|
+
created: boolean;
|
|
177
|
+
project: IProjectSummary;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create-or-update a `project` by `name` (§3a, §4). One `immediate`
|
|
181
|
+
* transaction: find the LIVE `project` row by `name` — on a HIT, merge the
|
|
182
|
+
* supplied fields into the EXISTING `meta` blob (never a wholesale replace;
|
|
183
|
+
* an existing `meta.policy` — §2's per-project policy, read back by
|
|
184
|
+
* {@link resolveProjectPolicy} — survives every subsequent `upsertProject`
|
|
185
|
+
* untouched) and audit `'updated'`; on a MISS, mint the `project` row AND,
|
|
186
|
+
* "on first creation ONLY," the reserved default `component` row named
|
|
187
|
+
* `(root)` plus its `owns_project` edge, in the SAME transaction (§4) — audit
|
|
188
|
+
* `'created'`. A repeat `upsertProject` against an existing project finds
|
|
189
|
+
* `(root)` already live (via the existing-project branch, which never touches
|
|
190
|
+
* it) and writes nothing further for it: idempotent, never a second row.
|
|
191
|
+
*
|
|
192
|
+
* Race-safety (§4c): the find-then-create SELECT and the INSERT/UPDATE both
|
|
193
|
+
* run inside the SAME `BEGIN IMMEDIATE` transaction two concurrent processes
|
|
194
|
+
* cannot both hold at once — one commits, the other's driver-level conflict
|
|
195
|
+
* surfaces as `E_CONTENTION` and is retried by {@link executeWriteTransaction}
|
|
196
|
+
* (250ms/500ms backoff, 3 attempts, §4c), never a racing check-then-insert.
|
|
197
|
+
*
|
|
198
|
+
* Errors: `InvalidArgumentError` (`name`/`by` missing or blank),
|
|
199
|
+
* `WriteContentionError`/`WriteIOError` (§4c, an exhausted or unclassified
|
|
200
|
+
* driver-level failure on the underlying `immediate` transaction).
|
|
201
|
+
*/
|
|
202
|
+
export declare function upsertProject(handle: IWriteStoreHandle, input: IUpsertProjectInput): Promise<IUpsertProjectOutcome>;
|
|
203
|
+
/**
|
|
204
|
+
* Transaction-PARTICIPANT core of {@link upsertProject} — the exact body
|
|
205
|
+
* that used to run inside its `executeWriteTransaction` callback, extracted
|
|
206
|
+
* so a caller that already owns an open `immediate` transaction (the ETL's
|
|
207
|
+
* `tools/etl/import-item.ts` bundles an entire source item's writes into ONE
|
|
208
|
+
* transaction, SPEC.md §7a) can participate in it instead of nesting a
|
|
209
|
+
* second `executeWriteTransaction` call — nesting would either deadlock or
|
|
210
|
+
* silently open two separate transactions where one atomic one was intended.
|
|
211
|
+
* `upsertProject` itself is now a thin wrapper: open the transaction, hand it
|
|
212
|
+
* to this function. Same validation contract as `upsertProject` — callers of
|
|
213
|
+
* THIS function are responsible for having already run `assertNonBlank`/
|
|
214
|
+
* `assertNotBareRoleLiteral` on `input.name`/`input.by` (this function does
|
|
215
|
+
* not re-validate, matching every other `*Tx` helper in this module).
|
|
216
|
+
*/
|
|
217
|
+
export declare function upsertProjectTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertProjectInput): Promise<IUpsertProjectOutcome>;
|
|
218
|
+
export interface IUpsertComponentInput {
|
|
219
|
+
/** `project` reference — `uid` or `name` (§6.1's disambiguation-by-shape rule), resolved-ONLY, never minted. */
|
|
220
|
+
project: string;
|
|
221
|
+
/** The business key `component` is upserted by, scoped to `project` (§3a, §4: "upsert by `(project, name)`"). */
|
|
222
|
+
name: string;
|
|
223
|
+
path?: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
/** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
|
|
226
|
+
by: string;
|
|
227
|
+
}
|
|
228
|
+
export interface IUpsertComponentOutcome {
|
|
229
|
+
uid: string;
|
|
230
|
+
/** `false` when an existing LIVE `component` row with this `(project, name)` was found and updated in place. */
|
|
231
|
+
created: boolean;
|
|
232
|
+
component: IComponentSummary;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create-or-update a `component` by `(project, name)` (§3a, §4). `project`
|
|
236
|
+
* is resolved-ONLY ({@link resolveProjectTx}) — an unresolved `project`
|
|
237
|
+
* throws `CatalogNotFoundError` before any component-side work runs. One
|
|
238
|
+
* `immediate` transaction thereafter: find the LIVE `component` row scoped to
|
|
239
|
+
* the resolved project's `uid` — on a HIT, merge the supplied fields into the
|
|
240
|
+
* EXISTING `meta` (never a wholesale replace, same rule as
|
|
241
|
+
* {@link upsertProject}) and audit `'updated'`; on a MISS, mint the row.
|
|
242
|
+
* Either way, the `owns_project` edge (project → component, §3a's registry
|
|
243
|
+
* edge set) is written with the SAME `ON CONFLICT` upsert `writeEdgeTx`
|
|
244
|
+
* itself already performs — a no-op re-livening when the edge already exists,
|
|
245
|
+
* so a component whose edge somehow drifted from its `meta.projectUid` (it
|
|
246
|
+
* never should, both are written in the SAME transaction on creation) is
|
|
247
|
+
* self-healed rather than left stale.
|
|
248
|
+
*
|
|
249
|
+
* Errors: `InvalidArgumentError` (`project`/`name`/`by` missing or blank),
|
|
250
|
+
* `CatalogNotFoundError` (`project` does not resolve),
|
|
251
|
+
* `WriteContentionError`/`WriteIOError` (§4c).
|
|
252
|
+
*/
|
|
253
|
+
export declare function upsertComponent(handle: IWriteStoreHandle, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
|
|
254
|
+
/**
|
|
255
|
+
* Transaction-PARTICIPANT core of {@link upsertComponent} — same extraction
|
|
256
|
+
* rationale as {@link upsertProjectTx}: lets a caller that already owns an
|
|
257
|
+
* open `immediate` transaction (the ETL) participate instead of nesting a
|
|
258
|
+
* second `executeWriteTransaction`. `upsertComponent` itself is now a thin
|
|
259
|
+
* wrapper: validate, open the transaction, hand it to this function. Callers
|
|
260
|
+
* of THIS function are responsible for having already run the same
|
|
261
|
+
* `assertNonBlank`/`assertNotBareRoleLiteral` validation `upsertComponent`
|
|
262
|
+
* runs before opening its transaction.
|
|
263
|
+
*/
|
|
264
|
+
export declare function upsertComponentTx(tx: AdapterTransaction, handle: Pick<IWriteStoreHandle, 'typePolicy'>, input: IUpsertComponentInput): Promise<IUpsertComponentOutcome>;
|
|
265
|
+
export interface IUpsertLocationInput {
|
|
266
|
+
/**
|
|
267
|
+
* `component` reference — `uid` or `name` (§6.1's disambiguation-by-shape
|
|
268
|
+
* rule). A bare NAME is ambiguous on its own (component names are unique
|
|
269
|
+
* only WITHIN a project, §6.3.2), so a name reference REQUIRES `project`
|
|
270
|
+
* to scope it; a `uid` reference never needs `project` (a documented
|
|
271
|
+
* judgment call — SPEC.md §3a states the call convention
|
|
272
|
+
* `upsertLocation({component, locType, value})` without a `project` field,
|
|
273
|
+
* but never states how a bare component name resolves; this reading stays
|
|
274
|
+
* consistent with every OTHER verb's own `project`-scoped component
|
|
275
|
+
* resolution — §6.1, §6.3.2 — rather than inventing a global name-uniqueness
|
|
276
|
+
* scan `resolveComponentTx` itself does not perform).
|
|
277
|
+
*/
|
|
278
|
+
component: string;
|
|
279
|
+
project?: string;
|
|
280
|
+
locType: ILocationType;
|
|
281
|
+
value: string;
|
|
282
|
+
/** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
|
|
283
|
+
by: string;
|
|
284
|
+
}
|
|
285
|
+
export interface IUpsertLocationOutcome {
|
|
286
|
+
uid: string;
|
|
287
|
+
/** `false` when a LIVE `location` row with this exact `(component, locType, value)` already existed — a genuine no-op: identity IS the row's entire content, so nothing is written or audited (§4a's "never disguised as a real write" rule, `move.ts`'s stated-no-op pattern). */
|
|
288
|
+
created: boolean;
|
|
289
|
+
location: ILocationSummary;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Create-or-find a `location` by `(component, locType, value)` (§3a, §4).
|
|
293
|
+
* `component` is resolved-ONLY — by `uid` directly, or by `name` scoped to
|
|
294
|
+
* `project` (see {@link IUpsertLocationInput.component}'s doc comment for the
|
|
295
|
+
* resolved ambiguity). One `immediate` transaction: find a LIVE `location`
|
|
296
|
+
* row with the exact triple — a HIT is a genuine no-op (the triple IS the
|
|
297
|
+
* row's whole identity; there is no fourth field left to merge, unlike
|
|
298
|
+
* `project`/`component`), so NOTHING is written or audited and `created:
|
|
299
|
+
* false` is returned; a MISS mints the row plus its `has_location` edge
|
|
300
|
+
* (component → location, §3a's registry edge set) and audits `'created'`.
|
|
301
|
+
*
|
|
302
|
+
* An invalidated (`rmLocation`-ed) row is never resurrected or matched here —
|
|
303
|
+
* the SELECT is `t_invalid IS NULL`-scoped like every other resolution in
|
|
304
|
+
* this module, so re-`upsertLocation`-ing the identical triple after an
|
|
305
|
+
* `rmLocation` mints a genuinely NEW row with a NEW `uid`, leaving the
|
|
306
|
+
* invalidated row's own audit trail untouched (the same bi-temporal
|
|
307
|
+
* non-resurrection rule `delete.ts` documents for `issue`).
|
|
308
|
+
*
|
|
309
|
+
* Errors: `InvalidArgumentError` (`component`/`value`/`by` missing or blank,
|
|
310
|
+
* `locType` not one of `path`/`url`/`tool`, or a bare `component` NAME given
|
|
311
|
+
* without `project`), `CatalogNotFoundError` (`project`/`component` does not
|
|
312
|
+
* resolve), `WriteContentionError`/`WriteIOError` (§4c).
|
|
313
|
+
*/
|
|
314
|
+
export declare function upsertLocation(handle: IWriteStoreHandle, input: IUpsertLocationInput): Promise<IUpsertLocationOutcome>;
|
|
315
|
+
export interface IRmLocationInput {
|
|
316
|
+
uid: string;
|
|
317
|
+
/** The acting agent's or person's identity (§6.3's opening rule). REQUIRED. */
|
|
318
|
+
by: string;
|
|
319
|
+
/** Optional explanation recorded on the invalidation's audit row and merged into the location's own `meta` (mirrors `delete.ts`'s `reason`, which SPEC.md §6.3.7 requires for `issue` — kept optional here since §3a/§4 state no such requirement for `location`). */
|
|
320
|
+
reason?: string;
|
|
321
|
+
}
|
|
322
|
+
export interface IRmLocationOutcome {
|
|
323
|
+
uid: string;
|
|
324
|
+
invalidated: true;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Soft-invalidate a LIVE `location` by `uid` (§3a, §4: "invalidate"; §4c's
|
|
328
|
+
* table: "invalidate-by-uid after confirming the row is live, for uniformity
|
|
329
|
+
* with every other verb above"). One `immediate` transaction: resolve `uid`
|
|
330
|
+
* → LIVE `location` node (`resolveByUidTx`, never a bare `getNodeByUid`, §4c)
|
|
331
|
+
* → hand-composed guarded `UPDATE node SET t_invalid = ?, meta = ? WHERE
|
|
332
|
+
* rowid = ?` (merging `invalidatedReason`/`invalidatedAt` into the EXISTING
|
|
333
|
+
* `meta`, never a wholesale replace — the SAME shape `delete.ts` uses for
|
|
334
|
+
* `issue`) → invalidate the owning `has_location` edge
|
|
335
|
+
* ({@link invalidateEdgeTx}, resolved via the location's own
|
|
336
|
+
* `meta.componentUid`) → `writeAudit` (action `'deleted'`, matching
|
|
337
|
+
* `delete.ts`'s own vocabulary), all against the SAME `tx`.
|
|
338
|
+
*
|
|
339
|
+
* No `issue` node ever references a `location` directly — per §3a's fixed
|
|
340
|
+
* edge table ({@link EDGE_KIND_TABLE}), `has_location` (component → location)
|
|
341
|
+
* is the ONLY rel touching `location` at all — so the sole referencing edge
|
|
342
|
+
* this invalidates is the owning component's own `has_location` edge; no
|
|
343
|
+
* issue-side fallout exists to reconcile.
|
|
344
|
+
*
|
|
345
|
+
* Errors: `InvalidArgumentError` (`uid`/`by` missing or blank),
|
|
346
|
+
* `CatalogNotFoundError` (no LIVE `location` node carries `uid` — including
|
|
347
|
+
* an ALREADY-`rmLocation`-ed `uid`, the same non-resurrection rule
|
|
348
|
+
* {@link IUpsertLocationOutcome}'s doc comment describes),
|
|
349
|
+
* `WriteContentionError`/`WriteIOError` (§4c).
|
|
350
|
+
*/
|
|
351
|
+
export declare function rmLocation(handle: IWriteStoreHandle, input: IRmLocationInput): Promise<IRmLocationOutcome>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claim-lease.ts — the ephemeral claim lease's read-side logic, shared by
|
|
3
|
+
* every write verb that needs to know whether a live claim currently blocks
|
|
4
|
+
* it. `claim.ts` (§6.3.5) is the only verb that MUTATES `claimedBy`/
|
|
5
|
+
* `claimedAt`; this module only ever reads and interprets that pair, so
|
|
6
|
+
* `transition.ts` (§6.3.5's own "a live claim by someone else blocks a
|
|
7
|
+
* status change" rule) can enforce the SAME staleness semantics without
|
|
8
|
+
* duplicating the age-math or silently drifting from it over time.
|
|
9
|
+
*/
|
|
10
|
+
/** Pulls the raw `claimedBy`/`claimedAt` pair off an issue's metadata, typed and undefined-safe. */
|
|
11
|
+
export declare function extractClaimMeta(metadata: Record<string, unknown> | undefined): {
|
|
12
|
+
claimedBy: string | undefined;
|
|
13
|
+
claimedAt: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A missing `claimedAt` (should not happen alongside a real `claimedBy`, but
|
|
17
|
+
* never trusted) is treated as infinitely old — the same fail-open choice
|
|
18
|
+
* `claim.ts`'s own reclaim branch already makes.
|
|
19
|
+
*/
|
|
20
|
+
export declare function claimAgeMinutes(claimedAt: string | undefined, now: string): number;
|
|
21
|
+
export declare function isClaimStale(claimedAt: string | undefined, now: string, staleAfterMin: number): boolean;
|
package/write/claim.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { IWriteStoreHandle } from './tx.js';
|
|
2
|
+
|
|
3
|
+
export interface IClaimInput {
|
|
4
|
+
/** The `issue` uid to claim/release/renew (§6.3, an "Issue verb"). */
|
|
5
|
+
uid: string;
|
|
6
|
+
/** The claimant — the acting agent or person. REQUIRED on every action (§6.3's opening rule). */
|
|
7
|
+
by: string;
|
|
8
|
+
action: 'claim' | 'release' | 'renew';
|
|
9
|
+
/** Human-confirmed override of a NON-stale claim (§6.2). Only consulted on `action:'claim'` when the current claimant differs and the lease is not yet stale. */
|
|
10
|
+
force?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface IClaimOutcome {
|
|
13
|
+
uid: string;
|
|
14
|
+
status: 'claimed' | 'held' | 'reclaimed-stale' | 'renewed' | 'released' | 'release-noop';
|
|
15
|
+
claimedBy?: string;
|
|
16
|
+
claimedAt?: string;
|
|
17
|
+
heldBy?: string;
|
|
18
|
+
heldSince?: string;
|
|
19
|
+
previousClaimant?: string;
|
|
20
|
+
wasClaimedBy?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `claim` / `release` / `renew` an issue's lease (§6.3.5). One `immediate`
|
|
24
|
+
* transaction; the CAS decision and the metadata touch both run against the
|
|
25
|
+
* SAME `tx` the transaction opened, so two concurrent callers against the
|
|
26
|
+
* SAME `uid` serialize through the transaction's RESERVED lock — the loser's
|
|
27
|
+
* own re-fetch (inside its own attempt, after the winner commits) sees the
|
|
28
|
+
* winner's already-written `claimedBy`/`claimedAt`, never a stale
|
|
29
|
+
* pre-transaction read.
|
|
30
|
+
*
|
|
31
|
+
* Rule table (SPEC.md §6.3.5, restated here for the metadata-only shape the
|
|
32
|
+
* lease now lives in):
|
|
33
|
+
*
|
|
34
|
+
* | action | current `claimedBy` | outcome |
|
|
35
|
+
* |-----------|-------------------------------------------------------|---------|
|
|
36
|
+
* | `claim` | unset | write `{claimedBy, claimedAt}`; `status:'claimed'` |
|
|
37
|
+
* | `claim` | `== by` | no-op write of `claimedAt`; `status:'held'` |
|
|
38
|
+
* | `claim` | `!= by`, age < `claim_stale_after_min`, `!force` | throws `ClaimHeldError` |
|
|
39
|
+
* | `claim` | `!= by`, age ≥ threshold, OR `force:true` | write `{claimedBy, claimedAt}`; `status:'reclaimed-stale'`, `previousClaimant` |
|
|
40
|
+
* | `release` | `== by` | clear both fields; `status:'released'` |
|
|
41
|
+
* | `release` | `!= by` or unset | no write; `status:'release-noop'`, `wasClaimedBy` |
|
|
42
|
+
* | `renew` | `== by` | bump `claimedAt`; `status:'renewed'` |
|
|
43
|
+
* | `renew` | `!= by` or unset | throws `ClaimHeldError` |
|
|
44
|
+
*
|
|
45
|
+
* **Audit.** Every branch that performs a real write emits exactly one
|
|
46
|
+
* `audit` node via {@link writeAudit}, inside the SAME transaction (§4a "no
|
|
47
|
+
* bare mutation"). `writeAudit`'s own doc comment enumerates the audit
|
|
48
|
+
* `action` vocabulary as `'claimed'`/`'released'`/`'renewed'`/
|
|
49
|
+
* `'reclaimed-stale'` — four values, not five — so the `claim`-when-`== by`
|
|
50
|
+
* ("held", an idempotent re-affirmation of an already-owned claim) branch
|
|
51
|
+
* reuses `action:'claimed'`; its `IClaimOutcome.status` is still the distinct
|
|
52
|
+
* `'held'` value the rule table names, carried in the OUTCOME, not the audit
|
|
53
|
+
* action string. The two branches the rule table marks "no write"
|
|
54
|
+
* (`release-noop`) perform no touch and emit no audit — nothing changed, so
|
|
55
|
+
* §4a's "every STATE CHANGE writes an audit node" does not apply. This
|
|
56
|
+
* reading resolves a genuine tension in SPEC.md §6.3.5 (see this package's
|
|
57
|
+
* write-verb report for the full citation) between "every branch writes an
|
|
58
|
+
* audit node" and the rule table's own "no write" / four-action vocabulary;
|
|
59
|
+
* it is the only reading consistent with `writeAudit`'s own doc comment.
|
|
60
|
+
*
|
|
61
|
+
* **`renew` against an unclaimed issue.** The rule table assigns this to the
|
|
62
|
+
* SAME `ClaimHeldError` outcome as "held by someone else" — but there is no
|
|
63
|
+
* real holder to report. SPEC.md does not resolve this case explicitly; this
|
|
64
|
+
* implementation reports `ClaimHeldError('', '')` (empty-string sentinels,
|
|
65
|
+
* never a fabricated agent name or timestamp) so the thrown error's shape is
|
|
66
|
+
* still exactly `ClaimHeldError` per the rule table, without inventing data
|
|
67
|
+
* the graph does not have.
|
|
68
|
+
*
|
|
69
|
+
* Errors: `InvalidArgumentError` (`uid`/`by` missing/blank, or `action`
|
|
70
|
+
* outside the closed `'claim'|'release'|'renew'` set — the TS type restricts
|
|
71
|
+
* this at compile time but a transport boundary, e.g. CLI/MCP JSON input, can
|
|
72
|
+
* still send an arbitrary string), `IssueNotFoundError` (no live `issue` node
|
|
73
|
+
* carries `uid`), `ClaimHeldError` (per the rule table above),
|
|
74
|
+
* `IssueTerminalError` (`action:'claim'` only — the issue's current status
|
|
75
|
+
* is `terminal`; `release`/`renew` are exempt, since cleanup on an issue
|
|
76
|
+
* that got closed out from under the claimant must still succeed),
|
|
77
|
+
* `WriteContentionError`/`WriteIOError` (§4c — an exhausted driver-level
|
|
78
|
+
* retry on the underlying `immediate` transaction).
|
|
79
|
+
*/
|
|
80
|
+
export declare function claim(handle: IWriteStoreHandle, input: IClaimInput): Promise<IClaimOutcome>;
|