@snag-run/core 0.3.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/dist/lint.js ADDED
@@ -0,0 +1,153 @@
1
+ import { isCodeIntakeKind, scopePathsFromRef, sourceRefProblem, } from "./schema.js";
2
+ /**
3
+ * Run the soft lint checks over a generation analysis. Returns warnings;
4
+ * an empty array means clean. Never throws on lint findings.
5
+ *
6
+ * `subject` is optional: when it is a code intake (`module`/`capability`), each
7
+ * failure node is additionally checked for a valid in-scope `file:line` sourceRef
8
+ * (#272) and an `uncited-code-failure` warning is emitted for any that lack one.
9
+ * Omitted/spec subjects skip that check entirely.
10
+ */
11
+ export function lintGeneration(generation, subject) {
12
+ const { nodes, edges } = generation;
13
+ const warnings = [];
14
+ const incoming = new Map();
15
+ for (const node of nodes)
16
+ incoming.set(node.id, 0);
17
+ for (const edge of edges) {
18
+ incoming.set(edge.to, (incoming.get(edge.to) ?? 0) + 1);
19
+ }
20
+ // Orphan failure node: a failure mode with no incoming edge — it branches
21
+ // off nothing on the map.
22
+ for (const node of nodes) {
23
+ if (node.type === "failure" && (incoming.get(node.id) ?? 0) === 0) {
24
+ warnings.push({
25
+ code: "orphan-failure",
26
+ message: `failure node '${node.id}' has no incoming edge`,
27
+ nodeId: node.id,
28
+ });
29
+ }
30
+ }
31
+ // Happy-spine connectivity: following only `happy` edges from a root should
32
+ // reach a terminal node. If no terminal is reachable, warn.
33
+ const happyAdj = new Map();
34
+ for (const node of nodes)
35
+ happyAdj.set(node.id, []);
36
+ for (const edge of edges) {
37
+ if (edge.type === "happy")
38
+ happyAdj.get(edge.from)?.push(edge.to);
39
+ }
40
+ const hasHappyIncoming = new Set(edges.filter((e) => e.type === "happy").map((e) => e.to));
41
+ const spineRoots = nodes.filter((n) => n.type !== "failure" && !hasHappyIncoming.has(n.id));
42
+ const terminalIds = new Set(nodes.filter((n) => n.type === "terminal").map((n) => n.id));
43
+ const reachesTerminal = (startId) => {
44
+ const stack = [startId];
45
+ const visited = new Set();
46
+ while (stack.length > 0) {
47
+ const id = stack.pop();
48
+ if (visited.has(id))
49
+ continue;
50
+ visited.add(id);
51
+ if (terminalIds.has(id))
52
+ return true;
53
+ for (const next of happyAdj.get(id) ?? [])
54
+ stack.push(next);
55
+ }
56
+ return false;
57
+ };
58
+ if (terminalIds.size === 0 || !spineRoots.some((r) => reachesTerminal(r.id))) {
59
+ warnings.push({
60
+ code: "no-terminal-reach",
61
+ message: "the happy-path spine does not reach a terminal node",
62
+ });
63
+ }
64
+ // Unreachable node: a non-failure node not reachable (over any edge type)
65
+ // from a spine entry point. A spine entry is a flow node that *starts* the
66
+ // happy path — it has an outgoing happy edge but no incoming happy edge — so
67
+ // a wholly disconnected island is not itself treated as a root.
68
+ // (Failure nodes are covered by orphan-failure above.)
69
+ const hasHappyOutgoing = new Set(edges.filter((e) => e.type === "happy").map((e) => e.from));
70
+ const spineEntries = spineRoots.filter((r) => hasHappyOutgoing.has(r.id));
71
+ const adj = new Map();
72
+ for (const node of nodes)
73
+ adj.set(node.id, []);
74
+ for (const edge of edges)
75
+ adj.get(edge.from)?.push(edge.to);
76
+ const reachable = new Set();
77
+ const stack = spineEntries.map((r) => r.id);
78
+ while (stack.length > 0) {
79
+ const id = stack.pop();
80
+ if (reachable.has(id))
81
+ continue;
82
+ reachable.add(id);
83
+ for (const next of adj.get(id) ?? [])
84
+ stack.push(next);
85
+ }
86
+ for (const node of nodes) {
87
+ if (node.type === "failure")
88
+ continue;
89
+ if (!reachable.has(node.id)) {
90
+ warnings.push({
91
+ code: "unreachable-node",
92
+ message: `node '${node.id}' is unreachable from the spine`,
93
+ nodeId: node.id,
94
+ });
95
+ }
96
+ }
97
+ // Degenerate duplicates (#247): cluster the non-failure (spine) nodes by
98
+ // normalized type + label. A cluster of two or more is the signature of
99
+ // degenerate over-generation — the model emitting repeated `start` / `end` /
100
+ // `Flow End` nodes (distinct ids, identical meaning) instead of stopping. One
101
+ // warning per cluster, naming the type/label and count, so a junk map is
102
+ // flagged loudly rather than silently emitted. Failure nodes are deliberately
103
+ // excluded: an exhaustive FMEA is expected to carry many, varied failure modes.
104
+ const spineClusters = new Map();
105
+ for (const node of nodes) {
106
+ if (node.type === "failure")
107
+ continue;
108
+ const key = `${node.type} ${node.label.trim().toLowerCase()}`;
109
+ const cluster = spineClusters.get(key);
110
+ if (cluster)
111
+ cluster.ids.push(node.id);
112
+ else {
113
+ spineClusters.set(key, {
114
+ type: node.type,
115
+ label: node.label,
116
+ ids: [node.id],
117
+ });
118
+ }
119
+ }
120
+ for (const { type, label, ids } of spineClusters.values()) {
121
+ if (ids.length < 2)
122
+ continue;
123
+ warnings.push({
124
+ code: "degenerate-duplicate",
125
+ message: `${ids.length} '${type}' nodes share the label ` +
126
+ `'${label}' (ids: ${ids.join(", ")}) — likely degenerate ` +
127
+ `over-generation; the map may be junk`,
128
+ nodeId: ids[0],
129
+ });
130
+ }
131
+ // Ungrounded failure nodes on a code map (#272): for a `module`/`capability`
132
+ // intake, every failure node should cite the `file:line` that produces it.
133
+ // Flag any whose sourceRef is missing or not a valid in-scope citation — the
134
+ // speculative-garbage signal — using the discovered scope (`subject.ref`) as the
135
+ // path allow-list. Spec/absent subjects are skipped (no source to cite).
136
+ if (subject && isCodeIntakeKind(subject.kind)) {
137
+ const scopePaths = scopePathsFromRef(subject.ref);
138
+ for (const node of nodes) {
139
+ if (node.type !== "failure")
140
+ continue;
141
+ const problem = sourceRefProblem(node.sourceRef, scopePaths);
142
+ if (problem) {
143
+ warnings.push({
144
+ code: "uncited-code-failure",
145
+ message: `failure node '${node.id}': ${problem}`,
146
+ nodeId: node.id,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ return warnings;
152
+ }
153
+ //# sourceMappingURL=lint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * One structured harness-log event emitted during a generation run. `phase`
3
+ * discriminates the event; the remaining fields are phase-specific and optional
4
+ * so the type stays small and forward-compatible. The engine emits these; the
5
+ * CLI decides what (if anything) to render based on the resolved `engine` level.
6
+ *
7
+ * - `resolved` — config resolution settled (opts > SNAG_* env > default),
8
+ * emitted BEFORE any LLM call so it survives a failed run;
9
+ * carries the resolved `adapter` + `model` ids (#120).
10
+ * - `gather-start` — phase-1 gather loop is about to run.
11
+ * - `step` — one agent step finished (a tool call); carries `step`
12
+ * (1-based index), `tool` (name), and a concise `args`
13
+ * string (e.g. the read path / grep pattern).
14
+ * - `gather-done` — phase-1 finished; carries the total `step` count, the
15
+ * discovered `files`, `finishReason`, and `stoppedAtCap`.
16
+ * - `emit-start` — phase-2 structured emit is about to run.
17
+ * - `emit-done` — phase-2 finished.
18
+ * - `usage` — token usage for one LLM call, or the per-run aggregate
19
+ * (#292). Carries `call` (which LLM call the counts are
20
+ * for: `"gather"` | `"emit"` | `"run"` for the aggregate)
21
+ * and `tokenUsage` (input/output/total). A run that fans
22
+ * out into a gather + an emit emits one `usage` per call
23
+ * plus a final `call: "run"` aggregate.
24
+ * - `emit-failure` — the phase-2 emit failed schema validation (#279). A
25
+ * verbose-tier diagnostic dump carrying the `gatherSummary`
26
+ * (the only context the emit had, on the agentic paths) and
27
+ * the `rejectedObject` (the malformed text the model
28
+ * produced), so a failed run is diagnosable without a paid
29
+ * re-run. Emitted just before the engine throws; the CLI
30
+ * gates it to the verbose `engine` level.
31
+ */
32
+ export interface HarnessLogEvent {
33
+ phase: "resolved" | "gather-start" | "step" | "gather-done" | "emit-start" | "emit-done" | "usage" | "emit-failure";
34
+ /** Resolved adapter id for a `resolved` event (matches `meta.adapter`). */
35
+ adapter?: string;
36
+ /** Resolved model id for a `resolved` event (matches `meta.model`). */
37
+ model?: string;
38
+ /** 1-based step index for a `step` event; total step count for `gather-done`. */
39
+ step?: number;
40
+ /** Tool name for a `step` event (e.g. `"read"`, `"grep"`). */
41
+ tool?: string;
42
+ /** Concise tool-call argument summary for a `step` event (e.g. a path/pattern). */
43
+ args?: string;
44
+ /** Discovered file set for a `gather-done` event (the files read into context). */
45
+ files?: readonly string[];
46
+ /** AI SDK final finish reason for a `gather-done` event. */
47
+ finishReason?: string;
48
+ /** Whether the gather loop hit its hard step ceiling (`gather-done`). */
49
+ stoppedAtCap?: boolean;
50
+ /** Which LLM call a `usage` event reports: the phase-1 `"gather"` call, the
51
+ * phase-2 `"emit"` call, or `"run"` for the per-run aggregate (#292). */
52
+ call?: "gather" | "emit" | "run";
53
+ /** Token usage for a `usage` event (input/output/total). All non-negative
54
+ * integers; absent provider counts are coerced to 0 (#292). */
55
+ tokenUsage?: {
56
+ input: number;
57
+ output: number;
58
+ total: number;
59
+ };
60
+ /** The gather summary that fed the failed emit (`emit-failure`, #279) — the
61
+ * only context the model had on the agentic paths. Absent on the doc path
62
+ * (no gather) and when no gather text was captured. */
63
+ gatherSummary?: string;
64
+ /** The rejected/partial object the model produced (`emit-failure`, #279) — the
65
+ * raw malformed text the AI SDK exposes on `NoObjectGeneratedError`, so the
66
+ * failure can be read directly. Absent when the SDK exposed no text. */
67
+ rejectedObject?: string;
68
+ }
69
+ /** The harness-log sink the engine calls. Default is a no-op (library stays silent). */
70
+ export type OnLogFn = (event: HarnessLogEvent) => void;
71
+ /** The no-op default sink — the engine emits nothing unless the caller wires one. */
72
+ export declare const noopOnLog: OnLogFn;
73
+ /**
74
+ * Build a concise, single-line argument summary for a step event from a tool
75
+ * call's input object. Defensive against the SDK step shape — an odd/missing
76
+ * input degrades to `undefined`, never a throw. Picks the most salient field per
77
+ * tool (path for read/glob, pattern for grep, ref for git) and falls back to a
78
+ * compact JSON of the input.
79
+ */
80
+ export declare function summarizeToolArgs(tool: string | undefined, input: unknown): string | undefined;
81
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EACD,UAAU,GACV,cAAc,GACd,MAAM,GACN,aAAa,GACb,YAAY,GACZ,WAAW,GACX,OAAO,GACP,cAAc,CAAC;IACnB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;6EACyE;IACzE,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;IACjC;mEAC+D;IAC/D,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D;;2DAEuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;4EAEwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wFAAwF;AACxF,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAEvD,qFAAqF;AACrF,eAAO,MAAM,SAAS,EAAE,OAAkB,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,SAAS,CAwCpB"}
package/dist/log.js ADDED
@@ -0,0 +1,63 @@
1
+ // Harness logging seam (#45) — a small, structured, side-channel event type the
2
+ // engine emits during a generation run, and the no-op default sink.
3
+ //
4
+ // The engine library stays SILENT unless a caller passes `onLog`: the CLI is the
5
+ // sink and does all level filtering + formatting (per the `engine` component
6
+ // level). Logging is purely additive — it never touches map output, the schema,
7
+ // or `meta`. All formatting/channel decisions (stderr, prefixes) live in the CLI.
8
+ /** The no-op default sink — the engine emits nothing unless the caller wires one. */
9
+ export const noopOnLog = () => { };
10
+ /**
11
+ * Build a concise, single-line argument summary for a step event from a tool
12
+ * call's input object. Defensive against the SDK step shape — an odd/missing
13
+ * input degrades to `undefined`, never a throw. Picks the most salient field per
14
+ * tool (path for read/glob, pattern for grep, ref for git) and falls back to a
15
+ * compact JSON of the input.
16
+ */
17
+ export function summarizeToolArgs(tool, input) {
18
+ if (input === null || typeof input !== "object")
19
+ return undefined;
20
+ const obj = input;
21
+ const pick = (key) => {
22
+ const v = obj[key];
23
+ return typeof v === "string" && v !== "" ? v : undefined;
24
+ };
25
+ switch (tool) {
26
+ case "read": {
27
+ const path = pick("path");
28
+ if (path === undefined)
29
+ return undefined;
30
+ // Surface a ranged read (#278) as `path:start-end` (whole-file reads stay
31
+ // bare `path`), so a verbose gather trace shows exactly which spans the
32
+ // agent paged — e.g. `worktree.go:200-299`.
33
+ const offset = typeof obj.offset === "number" ? obj.offset : undefined;
34
+ const limit = typeof obj.limit === "number" ? obj.limit : undefined;
35
+ if (offset === undefined && limit === undefined)
36
+ return path;
37
+ const start = offset !== undefined ? offset : 1;
38
+ return limit !== undefined ? `${path}:${start}-${start + limit - 1}` : `${path}:${start}+`;
39
+ }
40
+ case "glob":
41
+ return pick("pattern") ?? pick("glob");
42
+ case "grep": {
43
+ const p = pick("pattern");
44
+ return p === undefined ? undefined : `"${p}"`;
45
+ }
46
+ case "git_diff":
47
+ case "git_log":
48
+ return pick("ref") ?? pick("path");
49
+ default:
50
+ break;
51
+ }
52
+ // Fallback: a compact one-line JSON of the input, trimmed for log readability.
53
+ try {
54
+ const json = JSON.stringify(obj);
55
+ if (!json || json === "{}")
56
+ return undefined;
57
+ return json.length > 120 ? `${json.slice(0, 117)}...` : json;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,oEAAoE;AACpE,EAAE;AACF,iFAAiF;AACjF,6EAA6E;AAC7E,gFAAgF;AAChF,kFAAkF;AA8ElF,qFAAqF;AACrF,MAAM,CAAC,MAAM,SAAS,GAAY,GAAG,EAAE,GAAE,CAAC,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAwB,EACxB,KAAc;IAEd,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,IAAI,GAAG,CAAC,GAAW,EAAsB,EAAE;QAC/C,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3D,CAAC,CAAC;IACF,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YACzC,0EAA0E;YAC1E,wEAAwE;YACxE,4CAA4C;YAC5C,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YACvE,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;YACpE,IAAI,MAAM,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YAC7D,MAAM,KAAK,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,KAAK,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC;QAC7F,CAAC;QACD,KAAK,MAAM;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;YAC1B,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAChD,CAAC;QACD,KAAK,UAAU,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC;YACE,MAAM;IACV,CAAC;IACD,+EAA+E;IAC/E,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QAC7C,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,66 @@
1
+ import { type FailureMap } from "./schema.js";
2
+ import { type AnchorVocab } from "./anchor.js";
3
+ /** A proposal node that reused a prior node's id. */
4
+ export interface CarriedNode {
5
+ /** The prior id reused on the proposal node (the stable identity). */
6
+ readonly priorId: string;
7
+ /** The id the proposal originally minted for this node (replaced by `priorId`). */
8
+ readonly proposalId: string;
9
+ /** The normalized anchor the two matched on (`""` for a fingerprint fast-path
10
+ * match, which does not depend on an anchor). */
11
+ readonly anchor: string;
12
+ /** Which layer produced the match. */
13
+ readonly via: "fingerprint" | "anchor";
14
+ }
15
+ /** A "may continue" annotation for an ambiguous (multi-mode) anchor bucket: the
16
+ * exact-anchor spine cannot tell whether `proposalId` continues `priorId`, so both
17
+ * are surfaced (proposal as `new`, prior as `removed`) and this pair flags the
18
+ * possible continuation for the deferred embedding/human confirm step (#308).
19
+ * A candidate is NEVER an auto-carry — `proposalId` keeps its fresh id. */
20
+ export interface CandidateContinuation {
21
+ /** The proposal node's freshly minted id (also present in {@link ReconcileDelta.new}). */
22
+ readonly proposalId: string;
23
+ /** The prior node it may continue (also present in {@link ReconcileDelta.removed}). */
24
+ readonly priorId: string;
25
+ /** The shared normalized anchor that put both in the same ambiguous bucket. */
26
+ readonly anchor: string;
27
+ /** A short human-readable reason the pair could not be auto-disambiguated. */
28
+ readonly reason: string;
29
+ }
30
+ /** The structured outcome of a reconcile pass over failure nodes. The
31
+ * carried/new/removed sets partition every failure node across the two maps;
32
+ * `candidates` is an ADVISORY overlay (its proposal ids are a subset of `new`
33
+ * and its prior ids a subset of `removed`) — it does not change the partition. */
34
+ export interface ReconcileDelta {
35
+ /** Proposal nodes that reused a prior id (anchor- or fingerprint-matched). */
36
+ readonly carried: readonly CarriedNode[];
37
+ /** Proposal node ids with no prior match — kept as freshly minted. */
38
+ readonly new: readonly string[];
39
+ /** Prior node ids with no confident proposal match — removal candidates. A
40
+ * prior node in an ambiguous bucket lands here AND is flagged in `candidates`. */
41
+ readonly removed: readonly string[];
42
+ /** "May continue" pairs from ambiguous (multi-mode) anchor buckets that the
43
+ * exact-anchor spine could not auto-disambiguate — orphan-and-flag (#306). */
44
+ readonly candidates: readonly CandidateContinuation[];
45
+ }
46
+ export interface ReconcileOptions {
47
+ /** The closed source vocabulary the two maps' refs are normalized against. */
48
+ readonly anchors: AnchorVocab;
49
+ }
50
+ /**
51
+ * Reconcile a freshly generated `proposal` against the committed `prior` map,
52
+ * matching failure nodes so an unchanged mode keeps its prior id. Pure: it reads
53
+ * both maps + the supplied anchor vocabulary and returns a {@link ReconcileDelta}
54
+ * — it does not mutate either map (apply the delta with {@link applyReconcile}).
55
+ */
56
+ export declare function reconcile(prior: FailureMap, proposal: FailureMap, options: ReconcileOptions): ReconcileDelta;
57
+ /**
58
+ * Apply a {@link ReconcileDelta} to the proposal map: rewrite every carried
59
+ * node's id to the prior id it reused, and rewrite every edge endpoint with the
60
+ * same mapping so references stay intact. The result is re-validated through
61
+ * {@link failureMapSchema}, so an id collision or dangling edge introduced by the
62
+ * carry surfaces as a hard error rather than a corrupt map. This is the map that
63
+ * should be persisted — `proposal` is never mutated.
64
+ */
65
+ export declare function applyReconcile(proposal: FailureMap, delta: ReconcileDelta): FailureMap;
66
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AA6BA,OAAO,EAEL,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;qDACiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,QAAQ,CAAC;CACxC;AAED;;;;2EAI2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;kFAGkF;AAClF,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;sFACkF;IAClF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;kFAC8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAgDD;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,gBAAgB,GACxB,cAAc,CA6EhB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,cAAc,GACpB,UAAU,CAeZ"}
@@ -0,0 +1,173 @@
1
+ // reconcile() — the OSS, embedding-free identity-carry pass (#305, ADR 0002).
2
+ //
3
+ // "Generation proposes, reconcile disposes": each `snag map` run mints fresh
4
+ // LLM ids, so a regenerated map of an unchanged subject would churn every node
5
+ // id (and, later, drop every carried verdict). reconcile() matches a freshly
6
+ // generated PROPOSAL against the committed PRIOR map and carries the prior node's
7
+ // id onto the matching proposal node, so the persisted artifact has a stable,
8
+ // low-churn history that git records as a small delta.
9
+ //
10
+ // This is the spine: exact-anchor carry only. Two layers, cheapest first:
11
+ // 0. byte-identical content fast-path — a prior/proposal pair whose semantic
12
+ // `fingerprint` is identical AND unique on both sides. Rarely fires (the
13
+ // model rewords every run) but free and certain when it does.
14
+ // 1. exact source-anchor — bucket both sides by normalized source anchor
15
+ // (heading / symbol); a bucket holding exactly one mode on EACH side carries.
16
+ //
17
+ // Multi-mode / ambiguous buckets cannot be carried by the exact-anchor spine
18
+ // alone — when an anchor holds ≥2 modes on either side, which prior id continues
19
+ // which proposal node needs the embedding matcher, which is out of scope (the
20
+ // embedding/verdict layer is the backend, #308). Rather than guess (a wrong carry
21
+ // would corrupt a carried verdict) or silently drop, reconcile() orphan-and-flags
22
+ // such a bucket (#306): every proposal node in it is emitted `new` (fresh id),
23
+ // every prior node is emitted `removed` (a removal CANDIDATE, not a confirmed
24
+ // drop), and each (proposal × prior) cross-pair is recorded in `candidates` as a
25
+ // "may continue" annotation for the deferred human/embedding confirm step.
26
+ // The append-only, id-keyed discipline (ADR 0002) is honored by construction: a
27
+ // carry reuses a prior id, it never mutates one in place, and an uncertain match
28
+ // is never silently merged.
29
+ import { failureMapSchema, } from "./schema.js";
30
+ import { anchorForRef } from "./anchor.js";
31
+ function isFailure(node) {
32
+ return node.type === "failure";
33
+ }
34
+ /** Index nodes by `fingerprint`, keeping ONLY fingerprints that occur exactly
35
+ * once (a duplicate fingerprint is ambiguous — never use it to carry). Nodes
36
+ * without a fingerprint are skipped. */
37
+ function uniqueByFingerprint(nodes) {
38
+ const counts = new Map();
39
+ for (const node of nodes) {
40
+ if (node.fingerprint)
41
+ counts.set(node.fingerprint, (counts.get(node.fingerprint) ?? 0) + 1);
42
+ }
43
+ const index = new Map();
44
+ for (const node of nodes) {
45
+ if (node.fingerprint && counts.get(node.fingerprint) === 1)
46
+ index.set(node.fingerprint, node);
47
+ }
48
+ return index;
49
+ }
50
+ /** Bucket failure nodes by their normalized source anchor. Nodes that anchor to
51
+ * nothing are dropped (they cannot carry — see anchor.ts). */
52
+ function bucketByAnchor(nodes, vocab) {
53
+ const buckets = new Map();
54
+ for (const node of nodes) {
55
+ const anchor = anchorForRef(node.sourceRef, vocab);
56
+ if (anchor === null)
57
+ continue;
58
+ const bucket = buckets.get(anchor);
59
+ if (bucket)
60
+ bucket.push(node);
61
+ else
62
+ buckets.set(anchor, [node]);
63
+ }
64
+ return buckets;
65
+ }
66
+ /** A short, human-readable explanation of why an anchor bucket could not be
67
+ * auto-disambiguated, for {@link CandidateContinuation.reason}. */
68
+ function ambiguityReason(anchor, priorCount, proposalCount) {
69
+ const modes = (n) => `${n} ${n === 1 ? "mode" : "modes"}`;
70
+ return (`shares anchor '${anchor}' with ${modes(priorCount)} prior / ` +
71
+ `${modes(proposalCount)} proposal — needs confirmation`);
72
+ }
73
+ /**
74
+ * Reconcile a freshly generated `proposal` against the committed `prior` map,
75
+ * matching failure nodes so an unchanged mode keeps its prior id. Pure: it reads
76
+ * both maps + the supplied anchor vocabulary and returns a {@link ReconcileDelta}
77
+ * — it does not mutate either map (apply the delta with {@link applyReconcile}).
78
+ */
79
+ export function reconcile(prior, proposal, options) {
80
+ const priorFailures = prior.nodes.filter(isFailure);
81
+ const proposalFailures = proposal.nodes.filter(isFailure);
82
+ const carried = [];
83
+ const matchedPrior = new Set();
84
+ const matchedProposal = new Set();
85
+ // Layer 0: byte-identical fingerprint fast-path.
86
+ const priorByFingerprint = uniqueByFingerprint(priorFailures);
87
+ const proposalByFingerprint = uniqueByFingerprint(proposalFailures);
88
+ for (const [fingerprint, priorNode] of priorByFingerprint) {
89
+ const proposalNode = proposalByFingerprint.get(fingerprint);
90
+ if (!proposalNode)
91
+ continue;
92
+ carried.push({
93
+ priorId: priorNode.id,
94
+ proposalId: proposalNode.id,
95
+ anchor: "",
96
+ via: "fingerprint",
97
+ });
98
+ matchedPrior.add(priorNode.id);
99
+ matchedProposal.add(proposalNode.id);
100
+ }
101
+ // Layer 1: exact source-anchor, single mode on each side.
102
+ const priorBuckets = bucketByAnchor(priorFailures.filter((n) => !matchedPrior.has(n.id)), options.anchors);
103
+ const proposalBuckets = bucketByAnchor(proposalFailures.filter((n) => !matchedProposal.has(n.id)), options.anchors);
104
+ // Layer 2: orphan-and-flag ambiguous buckets (#306). An anchor present on both
105
+ // sides is either an unambiguous 1↔1 carry or a multi-mode bucket we cannot
106
+ // disambiguate without embeddings. We record `candidates` for the latter but do
107
+ // NOT mark its nodes matched, so they flow through to `new`/`removed` below: a
108
+ // multi-mode prior node is a removal CANDIDATE (the candidate pairs are its
109
+ // "might continue" signal), never a silent carry or silent drop.
110
+ const candidates = [];
111
+ for (const [anchor, priorBucket] of priorBuckets) {
112
+ const proposalBucket = proposalBuckets.get(anchor);
113
+ if (!proposalBucket)
114
+ continue;
115
+ if (priorBucket.length === 1 && proposalBucket.length === 1) {
116
+ // Unambiguous 1↔1 — the spine carries.
117
+ const priorNode = priorBucket[0];
118
+ const proposalNode = proposalBucket[0];
119
+ carried.push({
120
+ priorId: priorNode.id,
121
+ proposalId: proposalNode.id,
122
+ anchor,
123
+ via: "anchor",
124
+ });
125
+ matchedPrior.add(priorNode.id);
126
+ matchedProposal.add(proposalNode.id);
127
+ }
128
+ else {
129
+ // Multi-mode — orphan-and-flag every cross-pair instead of guessing.
130
+ const reason = ambiguityReason(anchor, priorBucket.length, proposalBucket.length);
131
+ for (const proposalNode of proposalBucket) {
132
+ for (const priorNode of priorBucket) {
133
+ candidates.push({
134
+ proposalId: proposalNode.id,
135
+ priorId: priorNode.id,
136
+ anchor,
137
+ reason,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ carried,
145
+ new: proposalFailures.filter((n) => !matchedProposal.has(n.id)).map((n) => n.id),
146
+ removed: priorFailures.filter((n) => !matchedPrior.has(n.id)).map((n) => n.id),
147
+ candidates,
148
+ };
149
+ }
150
+ /**
151
+ * Apply a {@link ReconcileDelta} to the proposal map: rewrite every carried
152
+ * node's id to the prior id it reused, and rewrite every edge endpoint with the
153
+ * same mapping so references stay intact. The result is re-validated through
154
+ * {@link failureMapSchema}, so an id collision or dangling edge introduced by the
155
+ * carry surfaces as a hard error rather than a corrupt map. This is the map that
156
+ * should be persisted — `proposal` is never mutated.
157
+ */
158
+ export function applyReconcile(proposal, delta) {
159
+ const rename = new Map();
160
+ for (const carried of delta.carried)
161
+ rename.set(carried.proposalId, carried.priorId);
162
+ const nodes = proposal.nodes.map((node) => {
163
+ const renamed = rename.get(node.id);
164
+ return renamed ? { ...node, id: renamed } : node;
165
+ });
166
+ const edges = proposal.edges.map((edge) => ({
167
+ ...edge,
168
+ from: rename.get(edge.from) ?? edge.from,
169
+ to: rename.get(edge.to) ?? edge.to,
170
+ }));
171
+ return failureMapSchema.parse({ ...proposal, nodes, edges });
172
+ }
173
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}