@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.
- package/CHANGELOG.md +69 -0
- package/README.md +194 -39
- package/api.d.ts +87 -0
- package/api.ir.json +1 -1
- package/citation.d.ts +176 -0
- package/envelope.d.ts +36 -2
- package/index.d.ts +23 -3
- package/index.js +105 -57
- package/index.mjs +11450 -6758
- package/ir-artifact.d.ts +7 -3
- package/lifecycle.d.ts +49 -0
- package/package.json +5 -5
- package/query/canonical.d.ts +16 -0
- package/query/card.d.ts +116 -4
- package/query/get.d.ts +8 -1
- package/query/index.d.ts +1 -0
- package/query/query.d.ts +32 -22
- package/query/redirect.d.ts +30 -0
- package/query/resolve.d.ts +75 -0
- package/query/similar-clusters.d.ts +8 -0
- package/query/similarity-signals.d.ts +47 -0
- package/query/spec-staleness.d.ts +21 -0
- package/query/types.d.ts +249 -34
- package/query/verdict-core.d.ts +21 -0
- package/query/verdict.d.ts +22 -0
- package/query/views/catalog.d.ts +47 -0
- package/query/views/registry.d.ts +27 -7
- package/query/views/report.d.ts +49 -0
- package/query/views/semantic.d.ts +28 -5
- package/query/views/stats.d.ts +38 -2
- package/readiness.d.ts +29 -0
- package/retry-policy.d.ts +44 -0
- package/serve.d.ts +1 -1
- package/server.d.ts +71 -3
- package/service-config.d.ts +109 -0
- package/service-errors.d.ts +51 -0
- package/skill/SKILL.md +688 -81
- package/store/catalog-invariant-guard.d.ts +13 -7
- package/vocabulary.d.ts +48 -0
- package/write/anchor-check.d.ts +139 -0
- package/write/attestation.d.ts +64 -0
- package/write/catalog-merge.d.ts +15 -8
- package/write/catalog.d.ts +72 -2
- package/write/citation-path.d.ts +31 -0
- package/write/citation.d.ts +157 -0
- package/write/create-issue.d.ts +143 -29
- package/write/errors.d.ts +196 -1
- package/write/gate.d.ts +67 -0
- package/write/merge-project.d.ts +54 -0
- package/write/obligation.d.ts +133 -0
- package/write/relate.d.ts +1 -1
- package/write/revision.d.ts +27 -0
- package/write/similarity-scan.d.ts +84 -0
- package/write/spec-revision.d.ts +131 -0
- package/write/spec-revision.reconcile.d.ts +48 -0
- package/write/transition.d.ts +13 -2
- package/write/tx.d.ts +44 -2
- package/write/uid-prefix.d.ts +78 -0
- package/write/update.d.ts +23 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { IWriteStoreHandle } from './tx.js';
|
|
2
|
+
|
|
3
|
+
/** `block` refuses the transition; `warn` records the shortfall but allows it (the C5 gate reads this). */
|
|
4
|
+
export type IObligationSeverity = 'block' | 'warn';
|
|
5
|
+
/** Which side of an edge the subject sits on: `in` = subject is `dst`, `out` = subject is `src`. */
|
|
6
|
+
export type IRelationDirection = 'in' | 'out';
|
|
7
|
+
/**
|
|
8
|
+
* The CLOSED predicate core (DESIGN §2 Primitive 3). No CEL, no dynamic leaf,
|
|
9
|
+
* no timeout — the grammar is exactly these six productions. Validated
|
|
10
|
+
* recursively by {@link assertValidPredicate}.
|
|
11
|
+
*
|
|
12
|
+
* - `evidence{kind,min?}` → distinct verified attestations of `kind` ≥ `min ?? 1`.
|
|
13
|
+
* - `blockers_terminal()` → every incoming live `blocks` source is terminal.
|
|
14
|
+
* - `relation{type,direction}` → a live edge of `type` touches the subject.
|
|
15
|
+
* - `all_of` / `any_of` / `not` → boolean composition. Empty `all_of` ⇒ `true`,
|
|
16
|
+
* empty `any_of` ⇒ `false` (documented, asserted).
|
|
17
|
+
*/
|
|
18
|
+
export type IPredicate = {
|
|
19
|
+
op: 'evidence';
|
|
20
|
+
kind: string;
|
|
21
|
+
min?: number;
|
|
22
|
+
} | {
|
|
23
|
+
op: 'blockers_terminal';
|
|
24
|
+
} | {
|
|
25
|
+
op: 'relation';
|
|
26
|
+
type: string;
|
|
27
|
+
direction: IRelationDirection;
|
|
28
|
+
} | {
|
|
29
|
+
op: 'all_of';
|
|
30
|
+
of: IPredicate[];
|
|
31
|
+
} | {
|
|
32
|
+
op: 'any_of';
|
|
33
|
+
of: IPredicate[];
|
|
34
|
+
} | {
|
|
35
|
+
op: 'not';
|
|
36
|
+
of: IPredicate;
|
|
37
|
+
};
|
|
38
|
+
export interface IObligationAppliesTo {
|
|
39
|
+
/** Absent ⇒ applies to any from-status. */
|
|
40
|
+
from?: string;
|
|
41
|
+
/**
|
|
42
|
+
* REQUIRED. Scopes a TRANSITION into that status: a concrete status name, or
|
|
43
|
+
* `'*'` for "any terminal transition". Evaluated by the C5 gate at the
|
|
44
|
+
* transition, and surfaced by the C6 verdict as a *prediction* of that gate.
|
|
45
|
+
*/
|
|
46
|
+
to: string;
|
|
47
|
+
}
|
|
48
|
+
export interface IObligationOverride {
|
|
49
|
+
/** Non-empty actor identities permitted to override this obligation — an override always requires a recorded reason at the C5 layer. */
|
|
50
|
+
actors: string[];
|
|
51
|
+
}
|
|
52
|
+
export interface IObligateInput {
|
|
53
|
+
uid: string;
|
|
54
|
+
applies_to: IObligationAppliesTo;
|
|
55
|
+
requirement: IPredicate;
|
|
56
|
+
on_fail: IObligationSeverity;
|
|
57
|
+
override?: IObligationOverride;
|
|
58
|
+
by: string;
|
|
59
|
+
}
|
|
60
|
+
export interface IObligateOutcome {
|
|
61
|
+
uid: string;
|
|
62
|
+
obligationUid: string;
|
|
63
|
+
}
|
|
64
|
+
export interface IUnobligateInput {
|
|
65
|
+
obligationUid: string;
|
|
66
|
+
by: string;
|
|
67
|
+
}
|
|
68
|
+
export interface IUnobligateOutcome {
|
|
69
|
+
obligationUid: string;
|
|
70
|
+
invalidated: true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The three leaf questions the grammar can ask. Implemented by each caller's
|
|
74
|
+
* own resolver — C5's tx-backed `evaluateTransitionGateTx` and C6's verdict
|
|
75
|
+
* resolver — because the leaf lookups are the ONLY part that touches storage;
|
|
76
|
+
* the composition is {@link evaluatePredicate}'s job alone.
|
|
77
|
+
*/
|
|
78
|
+
export interface IPredicateResolver {
|
|
79
|
+
/** Distinct attestations of `claim.kind === kind` whose `check.state === 'verified'`. */
|
|
80
|
+
countVerifiedAttestations(kind: string): Promise<number>;
|
|
81
|
+
/** `true` iff every incoming live `blocks` edge's source issue is terminal; a missing status is non-terminal (fail-closed). */
|
|
82
|
+
blockersAllTerminal(): Promise<boolean>;
|
|
83
|
+
/** `true` iff a live edge of `type` exists with the subject as `dst` (`in`) or `src` (`out`). */
|
|
84
|
+
relationExists(type: string, direction: IRelationDirection): Promise<boolean>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Recursive validator over the closed core — throws {@link InvalidPredicateError}
|
|
88
|
+
* on an unknown `op`, a missing/ill-typed leaf, an unexpected extra key, a
|
|
89
|
+
* non-array `all_of`/`any_of` `of`, or `evidence.min < 1`. Runs BEFORE any
|
|
90
|
+
* transaction opens.
|
|
91
|
+
*/
|
|
92
|
+
export declare function assertValidPredicate(pred: unknown): asserts pred is IPredicate;
|
|
93
|
+
/**
|
|
94
|
+
* Pure evaluator — the ONE implementation of the grammar's semantics, shared
|
|
95
|
+
* by the C5 write gate (tx-backed resolver) and the C6 verdict (graph-backed
|
|
96
|
+
* resolver). NOT re-implemented per caller (adhd ADR-0002).
|
|
97
|
+
*
|
|
98
|
+
* **Documented short-circuits:** an empty `all_of` is `true`, an empty `any_of`
|
|
99
|
+
* is `false`, and `not` of an empty `any_of` is therefore `true`.
|
|
100
|
+
*
|
|
101
|
+
* **Fail-closed:** a resolver rejection propagates — it is never coerced to
|
|
102
|
+
* `true`. A gate that cannot determine a leaf must refuse, not pass.
|
|
103
|
+
*/
|
|
104
|
+
export declare function evaluatePredicate(pred: IPredicate, resolver: IPredicateResolver): Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* Declare an obligation on a live issue. One `immediate` transaction: resolve
|
|
107
|
+
* the issue (`resolveLiveIssueTx`), write the `obligation` node, resolve
|
|
108
|
+
* `has_obligation`, write the edge, and audit `'obligated'`. The subject issue
|
|
109
|
+
* is NEVER mutated — no column, no `meta` touch, no stored status.
|
|
110
|
+
*
|
|
111
|
+
* Validation (`assertValidPredicate`, `applies_to.to` required, `on_fail` ∈
|
|
112
|
+
* `{block,warn}`, override shape) runs BEFORE `executeWriteTransaction`, so a
|
|
113
|
+
* malformed predicate never holds the write lock.
|
|
114
|
+
*
|
|
115
|
+
* Errors: `InvalidArgumentError` (blank `uid`/`by`, omitted/blank
|
|
116
|
+
* `applies_to.to`, bad `on_fail`, malformed override),
|
|
117
|
+
* `InvalidPredicateError` (the closed-core grammar), `IssueNotFoundError`,
|
|
118
|
+
* `StaleSupersedeError`, `WriteContentionError`/`WriteIOError` (§4c).
|
|
119
|
+
*/
|
|
120
|
+
export declare function obligate(handle: IWriteStoreHandle, input: IObligateInput): Promise<IObligateOutcome>;
|
|
121
|
+
/**
|
|
122
|
+
* Retire an obligation. One `immediate` transaction: resolve `obligationUid` to
|
|
123
|
+
* a live `obligation` node (else {@link ObligationNotFoundError}),
|
|
124
|
+
* soft-invalidate it (`UPDATE node SET t_invalid = ?, meta = <merged
|
|
125
|
+
* invalidatedAt/invalidatedReason> WHERE rowid = ?` — the `delete.ts` /
|
|
126
|
+
* `rmLocation` bi-temporal shape), invalidate the owning `has_obligation` edge,
|
|
127
|
+
* and audit `'unobligated'`. Never a hard delete — the row and its trail stay
|
|
128
|
+
* readable; it simply stops appearing on the card.
|
|
129
|
+
*
|
|
130
|
+
* Errors: `InvalidArgumentError` (blank `obligationUid`/`by`),
|
|
131
|
+
* `ObligationNotFoundError`, `WriteContentionError`/`WriteIOError` (§4c).
|
|
132
|
+
*/
|
|
133
|
+
export declare function unobligate(handle: IWriteStoreHandle, input: IUnobligateInput): Promise<IUnobligateOutcome>;
|
package/write/relate.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IWriteStoreHandle } from './tx.js';
|
|
2
2
|
|
|
3
3
|
/** The closed `rel` union `relate` accepts (§3/§6.3.6) — issue → issue in every case. */
|
|
4
|
-
export type RelateRel = 'relates_to' | 'supersedes' | 'blocks' | 'duplicate_of' | 'part_of';
|
|
4
|
+
export type RelateRel = 'relates_to' | 'supersedes' | 'blocks' | 'duplicate_of' | 'part_of' | 'similar_to';
|
|
5
5
|
export interface IRelateInput {
|
|
6
6
|
/** The relation's SOURCE `issue` uid (§1: uid is the only identifier — never a name, never repo-scoped). */
|
|
7
7
|
sourceUid: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* revision.ts — the issue content-revision counter (DESIGN §2 Primitive 1/4).
|
|
3
|
+
*
|
|
4
|
+
* One revision counter, one name: `subject.revision` (Primitive 2) and
|
|
5
|
+
* `source_revision` (Primitive 4) are the SAME quantity — a monotonic counter
|
|
6
|
+
* on the issue's own `meta.revision`, bumped on every mutating write. A check
|
|
7
|
+
* or a verdict is stale relative to this one counter, so it must be read
|
|
8
|
+
* through one function rather than each consumer re-deriving it.
|
|
9
|
+
*
|
|
10
|
+
* This module is pure (no store, no I/O): it reads only the `meta` blob a
|
|
11
|
+
* caller already has in hand. `C6`'s verdict layer owns the bump sites
|
|
12
|
+
* (`create-issue` seeds `revision: 0`; `update`/`transition`/`claim`/`relate`/
|
|
13
|
+
* `move` set `nextRevision(priorMeta)`); `attest`/`recheck` deliberately do
|
|
14
|
+
* NOT bump, because they never touch the subject.
|
|
15
|
+
*
|
|
16
|
+
* A never-mutated issue (no `meta.revision` at all, e.g. every issue created
|
|
17
|
+
* before the counter existed) reads as `0`, so the pre-counter corpus is
|
|
18
|
+
* consistent without a backfill.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The monotonic revision of an issue's content — `0` for a never-mutated
|
|
22
|
+
* issue. A non-finite or non-numeric stored value (a malformed `meta` blob)
|
|
23
|
+
* degrades to `0` rather than propagating `NaN` into a comparison.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readRevision(meta: Record<string, unknown> | undefined): number;
|
|
26
|
+
/** `readRevision(meta) + 1` — the revision a mutating write stamps onto its node. */
|
|
27
|
+
export declare function nextRevision(meta: Record<string, unknown> | undefined): number;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { StoreSearchBackend } from '@adhd/sox-hybrid-search';
|
|
2
|
+
import { GraphBackend } from '@adhd/sox-graph-store';
|
|
3
|
+
|
|
4
|
+
/** The search/graph substrate the scan needs — structurally compatible with the create gate's `IDuplicateScanHandle`. */
|
|
5
|
+
export interface ISimilarityScanHandle {
|
|
6
|
+
readonly graph?: GraphBackend;
|
|
7
|
+
readonly search?: {
|
|
8
|
+
readonly backend: StoreSearchBackend;
|
|
9
|
+
embedQuery?(text: string): Promise<Float32Array>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Why the scan could not produce a calibrated comparison — never a generic "unavailable". */
|
|
13
|
+
export type SimilarityScanDegradedReason = 'no-search-backend' | 'no-embed-query' | 'no-vector-scores';
|
|
14
|
+
/** One advisory similarity candidate, with the provenance a reviewer needs to judge it across projects. */
|
|
15
|
+
export interface ISimilarCandidate {
|
|
16
|
+
uid: string;
|
|
17
|
+
title: string;
|
|
18
|
+
/** Cosine similarity in `[0,1]`, straight off the vector channel. */
|
|
19
|
+
score: number;
|
|
20
|
+
scope: 'same-project' | 'cross-project';
|
|
21
|
+
provenance?: {
|
|
22
|
+
projectUid: string;
|
|
23
|
+
projectName: string;
|
|
24
|
+
componentUid?: string;
|
|
25
|
+
repoUrl?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Which independent signals fired, e.g. `['cosine','title-tokens']`. */
|
|
28
|
+
signals?: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface ISimilarityScanInput {
|
|
31
|
+
title: string;
|
|
32
|
+
body: string;
|
|
33
|
+
scope: 'same-project' | 'multi-project' | 'store-wide';
|
|
34
|
+
/** Required for `same-project`/`multi-project`: the filing project. */
|
|
35
|
+
projectUid?: string;
|
|
36
|
+
sameProjectThreshold: number;
|
|
37
|
+
crossProjectThreshold: number;
|
|
38
|
+
margin: number;
|
|
39
|
+
tokenOverlapMin: number;
|
|
40
|
+
limit?: number;
|
|
41
|
+
/**
|
|
42
|
+
* The filing item's own citation tokens / component path — the "A" side of
|
|
43
|
+
* the structural signal. Optional: absent means the structural signal can
|
|
44
|
+
* only ever fire on the candidate's side, which is the correct default for a
|
|
45
|
+
* caller with no new-item context (the view cluster scan).
|
|
46
|
+
*/
|
|
47
|
+
citationTokens?: readonly string[];
|
|
48
|
+
componentPath?: string;
|
|
49
|
+
/** Ids the scan must EXCLUDE (the declared `dedupeExcludeUid` parent + its `part_of` ancestor chain). */
|
|
50
|
+
excludeIds?: ReadonlySet<number>;
|
|
51
|
+
}
|
|
52
|
+
/** The richer outcome {@link scanSimilarCandidatesWithMeta} returns; {@link scanSimilarCandidates} is its candidates-only projection. */
|
|
53
|
+
export interface ISimilarityScanOutcome {
|
|
54
|
+
candidates: ISimilarCandidate[];
|
|
55
|
+
degraded: boolean;
|
|
56
|
+
degradedReason?: SimilarityScanDegradedReason;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Citation-derived structural tokens + owning component path for one issue.
|
|
60
|
+
*
|
|
61
|
+
* Exported so BOTH scan callers can supply the A-side of the AC7 structural
|
|
62
|
+
* signal from a real stored item: the `view:'similar'` cluster block derives
|
|
63
|
+
* its seed's context with this directly (the seed is already written), while
|
|
64
|
+
* the create-time gate derives the not-yet-written filing item's context from
|
|
65
|
+
* its input in `write/create-issue.ts`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function structuralContextFor(graph: GraphBackend, issueId: number): Promise<{
|
|
68
|
+
citationTokens: string[];
|
|
69
|
+
componentPath?: string;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* The scan, with its degraded-mode metadata. Returns `{candidates:[], degraded:false}`
|
|
73
|
+
* when the scope is a COMPLETE scan of nothing (the scope resolves to an empty
|
|
74
|
+
* candidate set, or every candidate was excluded); returns a degraded outcome
|
|
75
|
+
* when there ARE rows to compare against but the calibrated (vector) channel
|
|
76
|
+
* could not answer — the distinction the create gate's `abort` fails closed on.
|
|
77
|
+
*/
|
|
78
|
+
export declare function scanSimilarCandidatesWithMeta(handle: ISimilarityScanHandle, input: ISimilarityScanInput): Promise<ISimilarityScanOutcome>;
|
|
79
|
+
/**
|
|
80
|
+
* The candidates-only projection of {@link scanSimilarCandidatesWithMeta}
|
|
81
|
+
* (the C9 spec's stated signature): best-first, advisory, never writes, and
|
|
82
|
+
* `[]` when the embedding substrate is absent.
|
|
83
|
+
*/
|
|
84
|
+
export declare function scanSimilarCandidates(handle: ISimilarityScanHandle, input: ISimilarityScanInput): Promise<ISimilarCandidate[]>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { IWriteStoreHandle } from './tx.js';
|
|
2
|
+
import { GraphBackend, NodeRecord } from '@adhd/sox-graph-store';
|
|
3
|
+
|
|
4
|
+
/** The long-form file export an anchor points at: `locator + digest` (Primitive 2) — referenced, never embedded. */
|
|
5
|
+
export interface ISpecAnchor {
|
|
6
|
+
locator: string;
|
|
7
|
+
digest: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Immutable revision object metadata (node `kind: 'SPEC'`). The node's
|
|
11
|
+
* `content` column holds the FRAGMENT appended at this revision (the delta; the
|
|
12
|
+
* base revision's fragment IS the base document). No revision stores a full
|
|
13
|
+
* snapshot — the document is the fold over the chain's `content` values and
|
|
14
|
+
* `revision_token` is the hash of that fold.
|
|
15
|
+
*/
|
|
16
|
+
export interface ISpecRevisionMeta {
|
|
17
|
+
/** The ticket's logical id (the head resolved at append time) — see the module header. */
|
|
18
|
+
spec_of: string;
|
|
19
|
+
/** 1,2,3… monotonic within the spec. */
|
|
20
|
+
revision_seq: number;
|
|
21
|
+
/** `'sha256:<hex>'` of the FOLDED document at this revision. */
|
|
22
|
+
revision_token: string;
|
|
23
|
+
/** Predecessor revision uid (`null` for the base revision). */
|
|
24
|
+
prev_revision: string | null;
|
|
25
|
+
/** The long-form file export (Primitive 2). */
|
|
26
|
+
anchor: ISpecAnchor;
|
|
27
|
+
appended_by: string;
|
|
28
|
+
appended_at: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ISpecAppendInput {
|
|
31
|
+
/** The ticket's logical id — any uid on its `SUPERSEDES` chain is resolved forward. */
|
|
32
|
+
uid: string;
|
|
33
|
+
/** The delta appended at this revision (becomes the node's `content`). */
|
|
34
|
+
fragment: string;
|
|
35
|
+
/** The file export for this revision. */
|
|
36
|
+
anchor?: ISpecAnchor;
|
|
37
|
+
/** REQUIRED CAS token — the revision uid the caller read (`''` when the ticket had no spec yet). */
|
|
38
|
+
base_revision: string;
|
|
39
|
+
by: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ISpecAppendOutcome {
|
|
42
|
+
/** UNCHANGED — cross-checked by AC1. */
|
|
43
|
+
uid: string;
|
|
44
|
+
/** The NEW revision uid. */
|
|
45
|
+
spec_revision: string;
|
|
46
|
+
/** `'sha256:<hex>'`. */
|
|
47
|
+
spec_revision_token: string;
|
|
48
|
+
revision_seq: number;
|
|
49
|
+
}
|
|
50
|
+
/** Current revision + its token for a work item. */
|
|
51
|
+
export interface ISpecPointer {
|
|
52
|
+
revision_uid: string;
|
|
53
|
+
revision_token: string;
|
|
54
|
+
revision_seq: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The store surface {@link appendSpecRevision} needs. Beyond the ordinary write
|
|
58
|
+
* handle it requires the store's `GraphBackend`, because revision DISCOVERY must
|
|
59
|
+
* run the SAME live-kind traversal the read path (`deriveSpecHead`) and the
|
|
60
|
+
* reconciliation (`spec-revision.reconcile.ts`) use — and that traversal
|
|
61
|
+
* (`query/resolve.ts`'s `resolveEdgeScopedCandidates`) is a `GraphBackend` read
|
|
62
|
+
* that must run BEFORE the `BEGIN IMMEDIATE` transaction opens (a `GraphBackend`
|
|
63
|
+
* call never runs inside it — see `resolveLogicalHeadTx`). `GraphBacklogStore`
|
|
64
|
+
* and the test `TestIssueStore` both satisfy this shape.
|
|
65
|
+
*/
|
|
66
|
+
export type ISpecAppendStore = IWriteStoreHandle & {
|
|
67
|
+
readonly graph: GraphBackend;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Rowids of live items whose DECLARED catalog kind is `SPEC` — an
|
|
71
|
+
* `issue`-shaped node carrying a live `has_kind` edge to the `kind:'SPEC'`
|
|
72
|
+
* catalog row. This is the SHIPPED production shape: the reconciled corpus is
|
|
73
|
+
* `issue` nodes whose DECLARED kind is `SPEC`, and reconciliation stamps them
|
|
74
|
+
* `spec_of`/`revision_seq` WITHOUT changing the raw `node.kind` column. Any
|
|
75
|
+
* discovery keyed on the raw column alone is therefore blind to them.
|
|
76
|
+
*
|
|
77
|
+
* Uses the SAME live-kind traversal the read path's `filter.kind`
|
|
78
|
+
* (`query/views/semantic.ts`) and `spec-revision.reconcile.ts` use —
|
|
79
|
+
* `query/resolve.ts`'s {@link resolveEdgeScopedCandidates} on
|
|
80
|
+
* `has_kind`→`SPEC` — so read, write, and reconcile can never disagree on what
|
|
81
|
+
* "a live `kind:'SPEC'` item" is. Returns `undefined` when `SPEC` resolves to
|
|
82
|
+
* no live catalog row (nothing is declared SPEC).
|
|
83
|
+
*/
|
|
84
|
+
export declare function declaredSpecRowids(graph: GraphBackend): Promise<ReadonlySet<number> | undefined>;
|
|
85
|
+
/**
|
|
86
|
+
* Every live SPEC document — the UNION of two representations the store has
|
|
87
|
+
* held, both of which are specs:
|
|
88
|
+
*
|
|
89
|
+
* (a) a node whose RAW `kind` column is `SPEC` — the immutable revision object
|
|
90
|
+
* {@link appendSpecRevision} mints (and the shape the unit fixtures
|
|
91
|
+
* build); and
|
|
92
|
+
* (b) an `issue` node whose DECLARED catalog kind is `SPEC` — the reconciled
|
|
93
|
+
* production corpus (see {@link declaredSpecRowids}).
|
|
94
|
+
*
|
|
95
|
+
* This is the ONE discovery the read path ({@link deriveSpecHead}), the write
|
|
96
|
+
* path (`findChainHeadTx` via {@link declaredSpecRowids}), and the
|
|
97
|
+
* reconciliation all share, so they cannot drift on which items are specs. The
|
|
98
|
+
* union is load-bearing: a raw-kind-only reader missed shape (b) entirely.
|
|
99
|
+
*/
|
|
100
|
+
export declare function discoverLiveSpecNodes(graph: GraphBackend): Promise<NodeRecord[]>;
|
|
101
|
+
/**
|
|
102
|
+
* Append a revision. One `BEGIN IMMEDIATE` transaction (ADR-0001 + ADR-0012 —
|
|
103
|
+
* no temp-file/rename/flock; atomicity is the store's job):
|
|
104
|
+
*
|
|
105
|
+
* 1. resolve `uid` → logical head (input may be any `SUPERSEDES`-chain uid — AC9);
|
|
106
|
+
* 2. read the current revision; if it is not `base_revision` →
|
|
107
|
+
* {@link SpecRevisionConflictError} (stale base — no write; AC6);
|
|
108
|
+
* 3. write a NEW `kind:'SPEC'` node holding the fragment (AC1/AC8);
|
|
109
|
+
* 4. advance the ticket's `meta.spec_revision` IN PLACE via the CAS helper,
|
|
110
|
+
* bumping `meta.revision` — the uid is preserved (AC1);
|
|
111
|
+
* 5. `writeAudit(action:'spec-appended')`.
|
|
112
|
+
*
|
|
113
|
+
* NEVER update or delete a revision node (AC1/AC8).
|
|
114
|
+
*/
|
|
115
|
+
export declare function appendSpecRevision(handle: ISpecAppendStore, input: ISpecAppendInput): Promise<ISpecAppendOutcome>;
|
|
116
|
+
/**
|
|
117
|
+
* The derived chain head for a ticket — a live `SPEC` node whose
|
|
118
|
+
* `meta.spec_of` is in the ticket's `SUPERSEDES`-chain uid set with the max
|
|
119
|
+
* `meta.revision_seq` (read-path analogue of {@link findChainHeadTx}, backed by
|
|
120
|
+
* the same reconciliation-created functional index). `undefined` when the ticket has
|
|
121
|
+
* no live revision.
|
|
122
|
+
*/
|
|
123
|
+
export declare function deriveSpecHead(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
|
|
124
|
+
/** The pointer ON THE RECORD only (`meta.spec_revision`) — never the derived head. Used by the staleness cross-check. */
|
|
125
|
+
export declare function readPointerRecord(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
|
|
126
|
+
/**
|
|
127
|
+
* Current revision + its token for a work item (`undefined` if it has no spec).
|
|
128
|
+
* Prefers the on-record pointer; falls back to the derived chain head so a
|
|
129
|
+
* ticket whose pointer was not (yet) written — pre-reconciliation data — still reads.
|
|
130
|
+
*/
|
|
131
|
+
export declare function readSpecPointer(graph: GraphBackend, uid: string): Promise<ISpecPointer | undefined>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { IWriteStoreHandle } from './tx.js';
|
|
2
|
+
import { GraphBackend } from '@adhd/sox-graph-store';
|
|
3
|
+
|
|
4
|
+
/** The store surface the reconciliation needs — satisfied by `GraphBacklogStore` and the test store alike. */
|
|
5
|
+
export type ISpecReconcileStore = IWriteStoreHandle & {
|
|
6
|
+
readonly graph: GraphBackend;
|
|
7
|
+
};
|
|
8
|
+
export interface ISpecReconcileStamp {
|
|
9
|
+
revisionUid: string;
|
|
10
|
+
/** The logical id stamped onto `meta.spec_of` (the ticket's head). */
|
|
11
|
+
specOf: string;
|
|
12
|
+
revisionSeq: number;
|
|
13
|
+
prevRevision: string | null;
|
|
14
|
+
revisionToken: string;
|
|
15
|
+
/** `false` when the node already carries exactly this stamp (skip the write). */
|
|
16
|
+
needsStamp: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ISpecReconcileHead {
|
|
19
|
+
ticketUid: string;
|
|
20
|
+
revisionUid: string;
|
|
21
|
+
revisionToken: string;
|
|
22
|
+
revisionSeq: number;
|
|
23
|
+
/** `false` when the ticket's pointer already names this head with this token. */
|
|
24
|
+
needsPointer: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface ISpecReconcileReport {
|
|
27
|
+
/** How many live `SPEC` nodes were discovered. */
|
|
28
|
+
scanned: number;
|
|
29
|
+
stamps: ISpecReconcileStamp[];
|
|
30
|
+
heads: ISpecReconcileHead[];
|
|
31
|
+
/** `SPEC` nodes already stamped (idempotent no-op). */
|
|
32
|
+
alreadyStamped: number;
|
|
33
|
+
/** `SPEC` nodes with no live `part_of` parent — left untouched, reported. */
|
|
34
|
+
orphaned: string[];
|
|
35
|
+
/** Whether the lookup index exists after this call. */
|
|
36
|
+
indexCreated: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compute the reconciliation plan WITHOUT writing (the dry-run). Discovery is
|
|
40
|
+
* `query`-driven; nothing is mutated.
|
|
41
|
+
*/
|
|
42
|
+
export declare function planSpecRevisionReconcile(store: ISpecReconcileStore): Promise<ISpecReconcileReport>;
|
|
43
|
+
/**
|
|
44
|
+
* Apply the reconciliation. Idempotent: an already-stamped `SPEC` and an
|
|
45
|
+
* already-pointed ticket are both skipped, so a second run is a no-op. One
|
|
46
|
+
* `BEGIN IMMEDIATE` transaction (ADR-0001) — never a hard delete.
|
|
47
|
+
*/
|
|
48
|
+
export declare function applySpecRevisionReconcile(store: ISpecReconcileStore): Promise<ISpecReconcileReport>;
|
package/write/transition.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IWriteStoreHandle } from './tx.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ICitation } from '../citation.js';
|
|
3
3
|
|
|
4
4
|
export interface ITransitionInput {
|
|
5
5
|
/** The `issue` uid to transition (§6.3, an "Issue verb"). */
|
|
@@ -11,7 +11,7 @@ export interface ITransitionInput {
|
|
|
11
11
|
/** REQUIRED unless `project_policy.transition_requires_note` is `false` (default `true`) — optional in the type; enforced at runtime (`NoteRequiredError`), never at the TS level. */
|
|
12
12
|
note?: string;
|
|
13
13
|
/** REQUIRED (≥1) when `project_policy.citation_required` is `true` AND `toStatus` resolves to a terminal status. Written as `citation` nodes + `has_citation` edges exactly like `create`'s own citations. */
|
|
14
|
-
citations?:
|
|
14
|
+
citations?: ICitation[];
|
|
15
15
|
/**
|
|
16
16
|
* The item-level disclosure-contract git context — the SAME plain metadata
|
|
17
17
|
* scalar `create`'s own `gitContext` field writes (repo `AGENTS.md`'s "Cite
|
|
@@ -22,6 +22,17 @@ export interface ITransitionInput {
|
|
|
22
22
|
* per-citation `ref` — it rides the issue, never a `citation` node.
|
|
23
23
|
*/
|
|
24
24
|
gitContext?: string;
|
|
25
|
+
/**
|
|
26
|
+
* A recorded override of a refusing obligation (C5, DESIGN §2 Primitive 3).
|
|
27
|
+
* Honoured ONLY when the caller is in the refusing obligation's
|
|
28
|
+
* `override.actors`; a reason is ALWAYS required (never a configurable
|
|
29
|
+
* boolean) and is recorded on the audit row. A claimed override by a
|
|
30
|
+
* non-listed actor, or one with a blank reason, throws
|
|
31
|
+
* `OverrideNotPermittedError`.
|
|
32
|
+
*/
|
|
33
|
+
override?: {
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
25
36
|
}
|
|
26
37
|
export interface ITransitionOutcome {
|
|
27
38
|
uid: string;
|
package/write/tx.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { INodeReadExecutor } from './uid-prefix.js';
|
|
1
2
|
import { BacklogWriteError } from './errors.js';
|
|
2
3
|
import { TypePolicy } from '@adhd/sox-graph-store';
|
|
3
4
|
import { AdapterTransaction, StoreAdapter } from '@adhd/sox-store-adapter';
|
|
@@ -98,7 +99,28 @@ export declare function canonicalJSONStringify(value: Record<string, unknown>):
|
|
|
98
99
|
* issued against `tx` instead of the bare adapter — never a `getNodeByUid`
|
|
99
100
|
* call itself, which always runs against `this.adapter` (§4c).
|
|
100
101
|
*/
|
|
101
|
-
export declare function getNodeByUidTx(tx:
|
|
102
|
+
export declare function getNodeByUidTx(tx: INodeReadExecutor, uid: string): Promise<ITxNodeRow | null>;
|
|
103
|
+
/**
|
|
104
|
+
* uid reference → LIVE `issue`/catalog node row, inside a tx, accepting a
|
|
105
|
+
* UNIQUE uid PREFIX as well as an exact uid. This is the single write-side
|
|
106
|
+
* funnel every issue verb ({@link resolveLiveIssueTx}) and catalog
|
|
107
|
+
* uid-resolution (`catalog.ts`'s `resolveByUidTx`, hence `rm-location`) goes
|
|
108
|
+
* through, so the prefix contract is defined once rather than per verb.
|
|
109
|
+
*
|
|
110
|
+
* Exact match is the fast path and keeps winning: a full uid is looked up
|
|
111
|
+
* directly, and only when that misses is a prefix scan attempted. A prefix
|
|
112
|
+
* below `MIN_UID_PREFIX_LENGTH` throws an "too short" argument error; a prefix
|
|
113
|
+
* that matches zero rows throws the kind-appropriate not-found; a prefix that
|
|
114
|
+
* matches two or more throws {@link AmbiguousReferenceError} naming every
|
|
115
|
+
* candidate — never an arbitrary pick.
|
|
116
|
+
*
|
|
117
|
+
* @throws IssueNotFoundError (kind `issue`) / CatalogNotFoundError (any other
|
|
118
|
+
* kind) when nothing live matches.
|
|
119
|
+
* @throws AmbiguousReferenceError when a prefix matches more than one live row.
|
|
120
|
+
*/
|
|
121
|
+
export declare function resolveUidPrefixTx(exec: INodeReadExecutor, ref: string, opts?: {
|
|
122
|
+
expectedKind?: string;
|
|
123
|
+
}): Promise<ITxNodeRow>;
|
|
102
124
|
/**
|
|
103
125
|
* Resolves `uid` to the CURRENT, live `issue` row, or throws.
|
|
104
126
|
*
|
|
@@ -140,7 +162,7 @@ export declare function resolveLiveIssueTx(tx: AdapterTransaction, uid: string):
|
|
|
140
162
|
/** rowid → node, inside a tx (used to resolve an edge endpoint's kind without a redundant round trip when the caller doesn't already know it). */
|
|
141
163
|
export declare function getNodeByRowidTx(tx: AdapterTransaction, rowid: number): Promise<ITxNodeRow | null>;
|
|
142
164
|
export interface IWriteNodeTxInput {
|
|
143
|
-
/** The entity-type discriminator — `project`/`component`/`location`/`issue`/`kind`/`edge_kind`/`status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit` (§3). NEVER validated against a closed vocabulary here — the schema is open by design (§0 anti-antipattern 3); the write layer is the only composer of these literals. */
|
|
165
|
+
/** The entity-type discriminator — `project`/`component`/`location`/`issue`/`kind`/`edge_kind`/`status`/`priority`/`agent`/`note`/`citation`/`transition`/`audit`/`attestation`/`obligation`/`SPEC` (§3; the SPEC revision kind is added by C10, DESIGN §12). NEVER validated against a closed vocabulary here — the schema is open by design (§0 anti-antipattern 3); the write layer is the only composer of these literals. */
|
|
144
166
|
kind: string;
|
|
145
167
|
/** The business name (`issue.title`, a catalog row's name, …). Omit for a node with no name (none currently exist in §3's table, but the column is nullable). */
|
|
146
168
|
name?: string;
|
|
@@ -306,6 +328,26 @@ export interface IInvalidateEdgeTxInput {
|
|
|
306
328
|
* instead of the bare adapter.
|
|
307
329
|
*/
|
|
308
330
|
export declare function invalidateEdgeTx(tx: AdapterTransaction, input: IInvalidateEdgeTxInput): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Update a LIVE node's `meta` IN PLACE inside `tx`, CAS-guarded on the node's
|
|
333
|
+
* own `meta.revision` counter (SR-2 monotonic revision + SR-6 per-node CAS,
|
|
334
|
+
* per DESIGN §2 Primitive 1's "node's monotonic `revision`, bumped on every
|
|
335
|
+
* mutating write"). Preserves `uid` — this is the ONLY write path that mutates
|
|
336
|
+
* a node without superseding it, and it exists solely for the C10 spec pointer
|
|
337
|
+
* (`meta.spec_revision`) and the SR-2 counter it bumps. No other verb may adopt
|
|
338
|
+
* it without a new decision.
|
|
339
|
+
*
|
|
340
|
+
* `patch`'s keys are shallow-merged over the node's existing meta; the merged
|
|
341
|
+
* blob's `revision` is set to `expectedRevision + 1`. Returns the new revision
|
|
342
|
+
* number on success, or `null` when the node is missing OR the stored revision
|
|
343
|
+
* no longer equals `expectedRevision` (a concurrent writer won the CAS — the
|
|
344
|
+
* caller must surface a stale-base error, and because this runs inside the
|
|
345
|
+
* caller's own `BEGIN IMMEDIATE` transaction, a thrown error rolls the whole
|
|
346
|
+
* transaction back). A node with no `meta.revision` at all reads as revision
|
|
347
|
+
* `0` (matching `revision.ts`'s `readRevision`), so a pre-counter row is
|
|
348
|
+
* CAS-addressable without a backfill.
|
|
349
|
+
*/
|
|
350
|
+
export declare function updateNodeMetaTx(tx: AdapterTransaction, uid: string, patch: Record<string, unknown>, expectedRevision: number): Promise<number | null>;
|
|
309
351
|
/**
|
|
310
352
|
* The one transaction wrapper every write verb calls — `store.adapter.transaction(fn,
|
|
311
353
|
* {mode: resolveTransactionMode()})`, which is `'immediate'` (§4c: `BEGIN
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { InvalidArgumentError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
/** Whether `ref` is an exact, full uid (resolve-or-throw), never a business `name` and never a prefix. */
|
|
4
|
+
export declare function isUidShaped(ref: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The shortest uid prefix this resolver will accept. Eight hex characters —
|
|
7
|
+
* the first UUID block, the human-copyable unit, and 32 bits of address space;
|
|
8
|
+
* see this file's header for the full justification.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MIN_UID_PREFIX_LENGTH = 8;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `ref` is a proper prefix (length `>=` {@link MIN_UID_PREFIX_LENGTH},
|
|
13
|
+
* `< 36`) of some canonical uid string — hex characters with hyphens only at
|
|
14
|
+
* the canonical boundary positions.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isUidPrefixShaped(ref: string): boolean;
|
|
17
|
+
/** How a caller-supplied reference reads: an exact uid, a uid prefix, a too-short uid attempt, or not uid-shaped at all. */
|
|
18
|
+
export type UidRefKind = 'exact' | 'prefix' | 'too-short' | 'not-uid';
|
|
19
|
+
/**
|
|
20
|
+
* Classify `ref` for uid resolution. `too-short` is a uid ATTEMPT (hex/hyphen
|
|
21
|
+
* only) below {@link MIN_UID_PREFIX_LENGTH} — a strict uid context rejects it
|
|
22
|
+
* as too short, while a uid-or-name context falls back to a name lookup, so a
|
|
23
|
+
* genuine 6-character value that is itself a valid name is never swallowed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyUidRef(ref: string): UidRefKind;
|
|
26
|
+
/** One live node matching a uid PREFIX, projected to the fields the ambiguity decision and its error need. */
|
|
27
|
+
export interface IUidCandidate {
|
|
28
|
+
uid: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
name: string | null;
|
|
31
|
+
isSuperseded: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The minimal executor a prefix candidate read needs. Structural, so it is
|
|
35
|
+
* satisfied by `AdapterTransaction`, graph-store's `GraphTransaction`, and a
|
|
36
|
+
* bare `StoreAdapter` alike — nothing here depends on which one it is.
|
|
37
|
+
*/
|
|
38
|
+
export interface IUidPrefixQueryExecutor {
|
|
39
|
+
executeAll<T = Record<string, unknown>>(sql: string, args?: unknown[]): Promise<{
|
|
40
|
+
rows: T[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The executor a uid resolution needs: the prefix candidate read plus the
|
|
45
|
+
* exact single-row read. Structural, so `AdapterTransaction`,
|
|
46
|
+
* graph-store's `GraphTransaction`, and a bare `StoreAdapter` all satisfy it.
|
|
47
|
+
*/
|
|
48
|
+
export interface INodeReadExecutor extends IUidPrefixQueryExecutor {
|
|
49
|
+
executeGet<T = Record<string, unknown>>(sql: string, args?: unknown[]): Promise<T | null>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Every LIVE node whose uid starts with `prefix`.
|
|
53
|
+
*
|
|
54
|
+
* The range predicate `uid >= ? AND uid < ?` is an indexed prefix scan (the
|
|
55
|
+
* store keeps a unique index on `uid`) and, unlike a `LIKE` pattern, is
|
|
56
|
+
* case-exact against the lowercase uids the store mints — so the input is
|
|
57
|
+
* lower-cased once here and the caller never has to reason about collation.
|
|
58
|
+
* Soft-deleted rows are excluded in SQL (`t_invalid IS NULL`); a superseded
|
|
59
|
+
* row is returned (its `is_superseded` flag carried) so a caller that resolves
|
|
60
|
+
* to it can raise the same stale-reference error the exact path does.
|
|
61
|
+
*/
|
|
62
|
+
export declare function queryLiveUidPrefixCandidates(exec: IUidPrefixQueryExecutor, prefix: string): Promise<IUidCandidate[]>;
|
|
63
|
+
/**
|
|
64
|
+
* The ambiguity decision, shared by both paths. Returns the sole candidate, or
|
|
65
|
+
* `null` when there are none (the caller owns the not-found error, which
|
|
66
|
+
* differs by kind). Two or more candidates throw {@link AmbiguousReferenceError}
|
|
67
|
+
* — the resolver never auto-selects.
|
|
68
|
+
*/
|
|
69
|
+
export declare function selectUniqueUidCandidate(ref: string, candidates: readonly IUidCandidate[]): IUidCandidate | null;
|
|
70
|
+
/**
|
|
71
|
+
* The not-found error for a uid reference, keyed by the kind the caller asked
|
|
72
|
+
* for. `asPrefix` selects the prefix-specific wording ("no item matches")
|
|
73
|
+
* versus the exact-uid wording, so a caller can tell "this short reference
|
|
74
|
+
* matched nothing" from "this full uid is not here."
|
|
75
|
+
*/
|
|
76
|
+
export declare function missingUidError(expectedKind: string | undefined, ref: string, asPrefix: boolean): Error;
|
|
77
|
+
/** The too-short refusal — a uid attempt below {@link MIN_UID_PREFIX_LENGTH}. */
|
|
78
|
+
export declare function tooShortUidError(ref: string): InvalidArgumentError;
|
package/write/update.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IWriteStoreHandle } from './tx.js';
|
|
2
|
+
import { ICitation } from '../citation.js';
|
|
2
3
|
|
|
3
4
|
export interface IUpdateIssueInput {
|
|
4
5
|
/** The `issue` uid to update (§6.3, an "Issue verb"). */
|
|
@@ -17,6 +18,27 @@ export interface IUpdateIssueInput {
|
|
|
17
18
|
assignee?: string;
|
|
18
19
|
/** catalog agent name/uid; → `touch` + `authored_by` edge rewrite. An unresolved NAME mints; a uid-shaped ref that does not resolve throws `CatalogNotFoundError('agent', ref)`. */
|
|
19
20
|
author?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The DESIRED set of citations this issue should carry after this call — a
|
|
23
|
+
* DIFF-EMITTER, never a body rewrite (the architecture verdict): the live
|
|
24
|
+
* citation set is read inside the SAME `immediate` transaction, citations
|
|
25
|
+
* present live but absent from `citations` are removed (bi-temporally), and
|
|
26
|
+
* citations in `citations` but not live are added. Identity for the diff is
|
|
27
|
+
* `(file, lines, revision)` (§ `citationKey`), never `sha`/`context`.
|
|
28
|
+
*
|
|
29
|
+
* Routing a citation change through `body`/`supersede` is FORBIDDEN: that
|
|
30
|
+
* would mint a new issue uid and violate §1's "uid is the only identity".
|
|
31
|
+
* When `body` IS also given, the supersede mints the new node first, its
|
|
32
|
+
* live citations are carried forward, and THIS diff is applied against the
|
|
33
|
+
* carried-forward set on the new node — still inside the one transaction.
|
|
34
|
+
*
|
|
35
|
+
* Each citation's `sha` is computed (and policy-gated, exactly as `create`/
|
|
36
|
+
* `transition` do) before the transaction opens, so the write lock is never
|
|
37
|
+
* held across a filesystem/git read. `citations: []` clears every live
|
|
38
|
+
* citation. Omitted entirely ⇒ citations are untouched (and carried forward
|
|
39
|
+
* verbatim on a body edit).
|
|
40
|
+
*/
|
|
41
|
+
citations?: ICitation[];
|
|
20
42
|
/**
|
|
21
43
|
* §4b/§6.2 — waits for the fire-and-forget on-write embedding round-trip
|
|
22
44
|
* before `update` returns, when `true` and `handle.embedding` is
|
|
@@ -30,7 +52,7 @@ export interface IUpdateIssueInput {
|
|
|
30
52
|
*/
|
|
31
53
|
awaitEmbed?: boolean;
|
|
32
54
|
}
|
|
33
|
-
export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author';
|
|
55
|
+
export type IUpdateIssueChangedField = 'title' | 'body' | 'kind' | 'priority' | 'assignee' | 'author' | 'citations';
|
|
34
56
|
export interface IUpdateIssueOutcome {
|
|
35
57
|
/**
|
|
36
58
|
* The resulting CURRENT issue's uid — the SAME `input.uid` when `body` was
|