@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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +22 -0
- package/dist/anchor.d.ts +58 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +145 -0
- package/dist/anchor.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +19 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +153 -0
- package/dist/lint.js.map +1 -0
- package/dist/log.d.ts +81 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +63 -0
- package/dist/log.js.map +1 -0
- package/dist/reconcile.d.ts +66 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +173 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/schema.d.ts +567 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +573 -0
- package/dist/schema.js.map +1 -0
- package/dist/severity.d.ts +5 -0
- package/dist/severity.d.ts.map +1 -0
- package/dist/severity.js +16 -0
- package/dist/severity.js.map +1 -0
- package/package.json +46 -0
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
|
package/dist/lint.js.map
ADDED
|
@@ -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
|
package/dist/log.js.map
ADDED
|
@@ -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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"}
|