@smartmemory/stratum 0.3.4 → 0.4.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.
Files changed (73) hide show
  1. package/dist/cli/guard.js +63 -6
  2. package/dist/cli/guard.js.map +1 -1
  3. package/dist/cli/learn.js +226 -0
  4. package/dist/cli/learn.js.map +1 -0
  5. package/dist/cli/stratum.js +3 -1
  6. package/dist/cli/stratum.js.map +1 -1
  7. package/dist/connectors/background.js +53 -9
  8. package/dist/connectors/background.js.map +1 -1
  9. package/dist/connectors/base.js +16 -0
  10. package/dist/connectors/base.js.map +1 -1
  11. package/dist/connectors/cancellation.js +119 -0
  12. package/dist/connectors/cancellation.js.map +1 -0
  13. package/dist/connectors/claude-bg-worker.js +9 -1
  14. package/dist/connectors/claude-bg-worker.js.map +1 -1
  15. package/dist/connectors/claude.js +179 -86
  16. package/dist/connectors/claude.js.map +1 -1
  17. package/dist/connectors/codex.js +219 -62
  18. package/dist/connectors/codex.js.map +1 -1
  19. package/dist/connectors/runner.js +44 -3
  20. package/dist/connectors/runner.js.map +1 -1
  21. package/dist/contracts/events.json +34 -1
  22. package/dist/contracts/guard-signers.allowed +36 -0
  23. package/dist/contracts/mcp-surface.json +1085 -104
  24. package/dist/engine/checkpoint.js +8 -1
  25. package/dist/engine/checkpoint.js.map +1 -1
  26. package/dist/engine/engine.js +403 -42
  27. package/dist/engine/engine.js.map +1 -1
  28. package/dist/engine/ledger.js +12 -0
  29. package/dist/engine/ledger.js.map +1 -1
  30. package/dist/engine/receipts.js +91 -0
  31. package/dist/engine/receipts.js.map +1 -0
  32. package/dist/engine/state.js +51 -1
  33. package/dist/engine/state.js.map +1 -1
  34. package/dist/guard/authorization.js +71 -0
  35. package/dist/guard/authorization.js.map +1 -0
  36. package/dist/guard/descriptors.js +248 -0
  37. package/dist/guard/descriptors.js.map +1 -0
  38. package/dist/guard/errors.js +12 -0
  39. package/dist/guard/errors.js.map +1 -1
  40. package/dist/guard/evidence.js +24 -3
  41. package/dist/guard/evidence.js.map +1 -1
  42. package/dist/guard/sshsig.js +264 -0
  43. package/dist/guard/sshsig.js.map +1 -0
  44. package/dist/guard/store.js +13 -0
  45. package/dist/guard/store.js.map +1 -1
  46. package/dist/guard/transition.js +484 -33
  47. package/dist/guard/transition.js.map +1 -1
  48. package/dist/guard/trust.js +78 -0
  49. package/dist/guard/trust.js.map +1 -0
  50. package/dist/judge/judged.js +1 -1
  51. package/dist/judge/pricing.js +1 -0
  52. package/dist/judge/pricing.js.map +1 -1
  53. package/dist/learn/apply.js +600 -0
  54. package/dist/learn/apply.js.map +1 -0
  55. package/dist/learn/candidate.js +186 -0
  56. package/dist/learn/candidate.js.map +1 -0
  57. package/dist/learn/classify.js +181 -0
  58. package/dist/learn/classify.js.map +1 -0
  59. package/dist/learn/harvest.js +138 -0
  60. package/dist/learn/harvest.js.map +1 -0
  61. package/dist/learn/smartmemory_egress.js +330 -0
  62. package/dist/learn/smartmemory_egress.js.map +1 -0
  63. package/dist/mcp/server.js +150 -22
  64. package/dist/mcp/server.js.map +1 -1
  65. package/dist/policy/bundle.js +195 -0
  66. package/dist/policy/bundle.js.map +1 -0
  67. package/dist/policy/events.js +51 -0
  68. package/dist/policy/events.js.map +1 -0
  69. package/dist/policy/smartmemory_client.js +332 -0
  70. package/dist/policy/smartmemory_client.js.map +1 -0
  71. package/dist/policy/types.js +2 -0
  72. package/dist/policy/types.js.map +1 -0
  73. package/package.json +3 -2
@@ -0,0 +1,186 @@
1
+ import { createHash } from "node:crypto";
2
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
3
+ import { isAbsolute, join, resolve, sep } from "node:path";
4
+ /**
5
+ * Turns a durable cluster into a staged, immutable candidate.
6
+ *
7
+ * Authoring is TEMPLATED, not model-driven. A template cannot generalize beyond its
8
+ * inputs, which removes the largest source of harm a semantic-consistency critic exists
9
+ * to catch, and it makes candidate identity deterministic — the same evidence always
10
+ * produces the same bytes.
11
+ *
12
+ * The candidate carries the RENDERED note, not merely a description of one. Without the
13
+ * real text there is nothing for the behavioral critic to inspect and no source for the
14
+ * `after` bytes an apply must write and snapshot.
15
+ */
16
+ export const SCHEMA_VERSION = "learn-1.0";
17
+ export const TEMPLATE_VERSION = "1";
18
+ export class CandidateError extends Error {
19
+ }
20
+ const SECTION = "## Harvested notes";
21
+ /**
22
+ * v1 writes only inside the project's own `.stratum/learn/`. Promotion to a project's
23
+ * primary memory file, or to user scope, is a separate authorized step by design.
24
+ */
25
+ export function targetPathFor(workspaceRoot) {
26
+ if (!isAbsolute(workspaceRoot))
27
+ throw new CandidateError("workspaceRoot must be absolute");
28
+ const root = resolve(workspaceRoot);
29
+ const target = resolve(join(root, ".stratum", "learn", "NOTES.md"));
30
+ // Path allowlist, enforced on the RESOLVED path — a convention would not survive
31
+ // a workspaceRoot containing `..`.
32
+ if (target !== root && !target.startsWith(root + sep)) {
33
+ throw new CandidateError(`target escapes workspace root: ${target}`);
34
+ }
35
+ if (root !== workspaceRoot.replace(/\/+$/, "")) {
36
+ throw new CandidateError(`workspaceRoot is not normalized: ${workspaceRoot}`);
37
+ }
38
+ return target;
39
+ }
40
+ export function authorCandidate(cluster) {
41
+ if (cluster.class !== "durable") {
42
+ throw new CandidateError(`cluster is ${cluster.class}, not durable`);
43
+ }
44
+ if (cluster.evidence.length === 0) {
45
+ throw new CandidateError("cluster carries no evidence");
46
+ }
47
+ // Re-derived, not trusted: `applyEligible` is a computed field on a plain object, so
48
+ // a caller that hand-builds a Cluster (or edits one) must not be able to smuggle an
49
+ // unattributed or mixed-provenance lesson into an apply.
50
+ if (cluster.unattributed || cluster.scope.workspaceRoot === null) {
51
+ throw new CandidateError("cluster is unattributed and cannot authorize a write");
52
+ }
53
+ if (cluster.mixedProvenance) {
54
+ throw new CandidateError("cluster is mixed-provenance and cannot authorize a write");
55
+ }
56
+ if (!cluster.applyEligible)
57
+ throw new CandidateError("cluster is not apply-eligible");
58
+ const workspaceRoot = cluster.scope.workspaceRoot;
59
+ if (workspaceRoot === null)
60
+ throw new CandidateError("durable cluster has no workspaceRoot");
61
+ const claim = renderClaim(cluster);
62
+ const rendered = {
63
+ content: renderNote(cluster, claim),
64
+ templateId: `learn/${cluster.shape}`,
65
+ templateVersion: TEMPLATE_VERSION,
66
+ insertion: { mode: "append-to-section", section: SECTION },
67
+ };
68
+ const clusterId = sha(cluster.key);
69
+ return {
70
+ clusterId,
71
+ // Binds the whole WRITE, not just the bytes: target path and insertion mode change
72
+ // what gets written (mode "create" discards the existing file), so an identity that
73
+ // omits them lets an edited sidecar row apply different effects under the same id.
74
+ revisionId: sha([
75
+ clusterId,
76
+ rendered.templateVersion,
77
+ rendered.content,
78
+ targetPathFor(workspaceRoot),
79
+ rendered.insertion.mode,
80
+ rendered.insertion.section,
81
+ ].join("\u0000")),
82
+ schemaVersion: SCHEMA_VERSION,
83
+ targetKind: "memory",
84
+ targetPath: targetPathFor(workspaceRoot),
85
+ scope: {
86
+ workspaceRoot,
87
+ flowName: cluster.scope.flowName,
88
+ stepIds: cluster.scope.stepIds,
89
+ specDigests: cluster.scope.specDigests,
90
+ },
91
+ claim,
92
+ rendered,
93
+ evidence: cluster.evidence,
94
+ observedValues: cluster.observedValues,
95
+ recurrence: cluster.recurrence,
96
+ groupingKey: cluster.groupingKey,
97
+ // Every lesson v1 can currently derive points at a contract, and contracts live in
98
+ // specs. Surfacing is the deliverable; the fix stays a human's call.
99
+ requiresHumanAction: cluster.shape === "schema" || cluster.shape === "ensure",
100
+ // §5.1 tripwire: exactly what the author read. The memory pool is not among it.
101
+ authoringInputsDigest: sha(JSON.stringify({ key: cluster.key, fingerprint: cluster.fingerprint, recurrence: cluster.recurrence })),
102
+ };
103
+ }
104
+ function renderClaim(cluster) {
105
+ const steps = cluster.scope.stepIds.map((s) => `\`${s}\``).join(", ");
106
+ const { records, distinctRuns, distinctPairs } = cluster.recurrence;
107
+ const seen = `${records} times across ${distinctRuns} runs / ${distinctPairs} run-step pairs`;
108
+ const { path, expected } = cluster.contract;
109
+ // A note that does not name the offending field is not actionable.
110
+ const field = path.length > 0 ? `\`${path}\`` : "the step output";
111
+ if (cluster.shape === "schema") {
112
+ const allowed = expected.length > 0
113
+ ? ` The contract allows ${expected.map((v) => `\`${v}\``).join(", ")}.`
114
+ : "";
115
+ const observed = cluster.observedValues.length > 0
116
+ ? ` Steps returned ${cluster.observedValues.map((v) => `\`${v}\``).join(", ")}.`
117
+ : "";
118
+ return `In flow \`${cluster.scope.flowName}\`, step(s) ${steps} produced ${field} that the declared contract rejected, ${seen}.${allowed}${observed}`;
119
+ }
120
+ return `In flow \`${cluster.scope.flowName}\`, step(s) ${steps} failed the same \`${cluster.shape}\` check on ${field}, ${seen}.`;
121
+ }
122
+ function renderNote(cluster, claim) {
123
+ const recovered = cluster.evidence.filter((record) => record.recovered).length;
124
+ // Only claim what the evidence shows. "Recovered on retry" is the invisible-waste
125
+ // story, and it is a different (weaker) finding when most of these actually failed.
126
+ const impact = recovered === cluster.evidence.length
127
+ ? "every one recovered on retry, so each cost an extra agent dispatch and nothing surfaced it"
128
+ : recovered === 0
129
+ ? "none recovered on retry"
130
+ : `${recovered} of ${cluster.evidence.length} recovered on retry, so those cost an extra agent dispatch each`;
131
+ const lines = [
132
+ `- **${cluster.scope.flowName}: recurring ${cluster.shape} failure on ${cluster.contract.path || "step output"}** — ${claim}`,
133
+ ` **Why it matters:** ${impact}.`,
134
+ ` **Fix target:** the declared contract or the step instruction — a spec change, which this loop deliberately will not make for you.`,
135
+ ` <!-- learn:${sha(cluster.key).slice(0, 12)} -->`,
136
+ ];
137
+ return lines.join("\n");
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Sidecar: append-only, its own file, its own schema. Never any other corpus.
141
+ // ---------------------------------------------------------------------------
142
+ export function sidecarPath(dir) {
143
+ return join(dir, "candidates.jsonl");
144
+ }
145
+ export async function readCandidates(dir) {
146
+ let raw;
147
+ try {
148
+ raw = await readFile(sidecarPath(dir), "utf8");
149
+ }
150
+ catch {
151
+ return [];
152
+ }
153
+ const out = [];
154
+ for (const line of raw.split("\n")) {
155
+ if (line.trim().length === 0)
156
+ continue;
157
+ try {
158
+ out.push(JSON.parse(line));
159
+ }
160
+ catch {
161
+ // A corrupt line must not hide the rest of the file.
162
+ }
163
+ }
164
+ return out;
165
+ }
166
+ /** Returns how many rows were newly written. Idempotent on `revisionId`. */
167
+ export async function appendCandidates(dir, candidates) {
168
+ const existing = new Set((await readCandidates(dir)).map((c) => c.revisionId));
169
+ const fresh = candidates.filter((c) => !existing.has(c.revisionId));
170
+ if (fresh.length === 0)
171
+ return 0;
172
+ await mkdir(dir, { recursive: true });
173
+ await appendFile(sidecarPath(dir), fresh.map((c) => JSON.stringify(c)).join("\n") + "\n", "utf8");
174
+ return fresh.length;
175
+ }
176
+ /** The newest revision of each cluster, by file order. */
177
+ export function latestPerCluster(candidates) {
178
+ const latest = new Map();
179
+ for (const candidate of candidates)
180
+ latest.set(candidate.clusterId, candidate);
181
+ return [...latest.values()];
182
+ }
183
+ function sha(value) {
184
+ return createHash("sha256").update(value).digest("hex");
185
+ }
186
+ //# sourceMappingURL=candidate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate.js","sourceRoot":"","sources":["../../src/learn/candidate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,181 @@
1
+ import { createHash } from "node:crypto";
2
+ export const DEFAULT_THRESHOLDS = { minRuns: 2, minPairs: 3 };
3
+ export function issueUnits(record) {
4
+ if (record.shape === "schema") {
5
+ const issues = parseIssues(record.reason);
6
+ if (issues.length > 0) {
7
+ return issues.map((issue) => {
8
+ // The declared contract: allowed enum options, or rejected key NAMES — both
9
+ // properties of the schema, not of the value that failed it.
10
+ const contract = {
11
+ code: String(issue.code ?? ""),
12
+ path: toStrings(issue.path).join("."),
13
+ expected: [
14
+ ...toStrings(issue.options),
15
+ ...toStrings(issue.keys),
16
+ ...(typeof issue.expected === "string" ? [issue.expected] : []),
17
+ ].sort(),
18
+ };
19
+ return {
20
+ record,
21
+ fingerprint: sha(JSON.stringify({ shape: record.shape, ...contract })),
22
+ contract,
23
+ ...(typeof issue.received === "string" ? { received: issue.received } : {}),
24
+ };
25
+ });
26
+ }
27
+ }
28
+ const text = record.shape === "budget"
29
+ ? "budget"
30
+ : record.reason.replace(/"[^"]*"/g, '""').replace(/\d+/g, "#").slice(0, 200);
31
+ const contract = { code: record.shape, path: "", expected: [] };
32
+ return [{ record, fingerprint: sha(JSON.stringify({ shape: record.shape, text })), contract }];
33
+ }
34
+ /** The primary violated contract of a record — its first issue in canonical order. */
35
+ export function contractFingerprint(record) {
36
+ const sorted = [...issueUnits(record)].sort((a, b) => a.fingerprint.localeCompare(b.fingerprint));
37
+ // issueUnits always returns at least one unit, but the compiler cannot know that.
38
+ return sorted[0]?.fingerprint ?? sha(JSON.stringify({ shape: record.shape, text: "" }));
39
+ }
40
+ function parseIssues(reason) {
41
+ try {
42
+ const parsed = JSON.parse(reason);
43
+ if (Array.isArray(parsed))
44
+ return parsed;
45
+ if (typeof parsed === "object" && parsed !== null)
46
+ return [parsed];
47
+ }
48
+ catch {
49
+ // Not a structured reason; the text path in contractShape handles it.
50
+ }
51
+ return [];
52
+ }
53
+ function toStrings(value) {
54
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
55
+ }
56
+ export function groupRecords(records, groupingKey) {
57
+ const groups = new Map();
58
+ for (const record of records) {
59
+ for (const unit of issueUnits(record)) {
60
+ const parts = [
61
+ record.workspaceRoot ?? "\u0000unattributed",
62
+ record.flowName,
63
+ record.shape,
64
+ unit.fingerprint,
65
+ ];
66
+ // The step id joins the key only for step-scoped grouping. Leaving it out is what
67
+ // recovers a contract-level lesson that no single step exhibits often enough.
68
+ if (groupingKey === "step-scoped")
69
+ parts.splice(2, 0, record.stepId ?? "\u0000flow");
70
+ const key = parts.join("\u0000");
71
+ const bucket = groups.get(key);
72
+ if (bucket === undefined)
73
+ groups.set(key, [unit]);
74
+ else
75
+ bucket.push(unit);
76
+ }
77
+ }
78
+ return groups;
79
+ }
80
+ export function classify(records, thresholds = DEFAULT_THRESHOLDS) {
81
+ const clusters = [];
82
+ const claimed = new Set();
83
+ // Step-agnostic first: a contract-level lesson subsumes the per-step view of the same
84
+ // issues, and emitting both would report one lesson twice.
85
+ for (const groupingKey of ["step-agnostic", "step-scoped"]) {
86
+ for (const [key, bucket] of groupRecords(records, groupingKey)) {
87
+ const cluster = build(key, groupingKey, bucket, thresholds);
88
+ if (groupingKey === "step-scoped" &&
89
+ cluster.class === "durable" &&
90
+ bucket.every((unit) => claimed.has(identity(unit)))) {
91
+ continue;
92
+ }
93
+ // Only an APPLY-ELIGIBLE aggregate subsumes its per-step view. A mixed-provenance
94
+ // or unattributed aggregate is discarded downstream, so letting it claim these
95
+ // units would silently drop clean per-step lessons with it.
96
+ if (cluster.class === "durable" && cluster.applyEligible) {
97
+ for (const unit of bucket)
98
+ claimed.add(identity(unit));
99
+ }
100
+ clusters.push(cluster);
101
+ }
102
+ }
103
+ return clusters.sort((a, b) => a.key.localeCompare(b.key));
104
+ }
105
+ function identity(unit) {
106
+ const { record } = unit;
107
+ return [record.runId, record.stepId ?? "", record.at, unit.fingerprint].join("\u0000");
108
+ }
109
+ function build(key, groupingKey, bucket, thresholds) {
110
+ const rows = bucket.map((unit) => unit.record);
111
+ const first = rows[0];
112
+ if (first === undefined)
113
+ throw new Error("cannot build a cluster from an empty bucket");
114
+ const runs = new Set(rows.map((r) => r.runId));
115
+ const pairs = new Set(rows.map((r) => `${r.runId}\u0000${r.stepId ?? ""}`));
116
+ const stepIds = [...new Set(rows.map((r) => r.stepId).filter((s) => s !== null))].sort();
117
+ const specDigests = [
118
+ ...new Set(rows.map((r) => r.specDigest).filter((s) => s !== undefined)),
119
+ ].sort();
120
+ const workspaceRoot = first.workspaceRoot ?? null;
121
+ const unattributed = workspaceRoot === null;
122
+ // Disjoint spec revisions AND disjoint steps: the records cannot be one contract seen
123
+ // across edits, so they are more likely two contracts colliding on one fingerprint.
124
+ const perDigestSteps = new Map();
125
+ for (const record of rows) {
126
+ if (record.specDigest === undefined || record.stepId === null)
127
+ continue;
128
+ const set = perDigestSteps.get(record.specDigest) ?? new Set();
129
+ set.add(record.stepId);
130
+ perDigestSteps.set(record.specDigest, set);
131
+ }
132
+ const stepSets = [...perDigestSteps.values()];
133
+ const mixedProvenance = stepSets.length > 1 &&
134
+ stepSets.every((a, i) => stepSets.every((b, j) => i === j || disjoint(a, b)));
135
+ const shape = first.shape;
136
+ let klass;
137
+ if (shape === "budget")
138
+ klass = "transient";
139
+ else if (runs.size >= thresholds.minRuns && pairs.size >= thresholds.minPairs)
140
+ klass = "durable";
141
+ else
142
+ klass = "step-local";
143
+ // Evidence is deduped source records: one record violating two constraints is
144
+ // evidence for two clusters, but only once within each.
145
+ const evidence = [];
146
+ const seen = new Set();
147
+ for (const record of rows) {
148
+ const id = [record.runId, record.stepId ?? "", record.at].join("\u0000");
149
+ if (seen.has(id))
150
+ continue;
151
+ seen.add(id);
152
+ evidence.push(record);
153
+ }
154
+ return {
155
+ key,
156
+ groupingKey,
157
+ shape,
158
+ class: klass,
159
+ fingerprint: bucket[0]?.fingerprint ?? "",
160
+ contract: bucket[0]?.contract ?? { code: shape, path: "", expected: [] },
161
+ scope: { workspaceRoot, flowName: first.flowName, stepIds, specDigests },
162
+ recurrence: { records: bucket.length, distinctRuns: runs.size, distinctPairs: pairs.size },
163
+ observedValues: [
164
+ ...new Set(bucket.map((u) => u.received).filter((v) => v !== undefined)),
165
+ ].sort(),
166
+ mixedProvenance,
167
+ unattributed,
168
+ applyEligible: klass === "durable" && !unattributed && !mixedProvenance,
169
+ evidence,
170
+ };
171
+ }
172
+ function disjoint(a, b) {
173
+ for (const value of a)
174
+ if (b.has(value))
175
+ return false;
176
+ return true;
177
+ }
178
+ function sha(value) {
179
+ return createHash("sha256").update(value).digest("hex");
180
+ }
181
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/learn/classify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,138 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export async function harvest(flowsDir) {
4
+ let names;
5
+ try {
6
+ names = (await readdir(flowsDir)).filter((name) => name.endsWith(".json")).sort();
7
+ }
8
+ catch {
9
+ // A missing corpus is an empty corpus, not an error.
10
+ return { records: [], skipped: 0, droppedEvents: 0 };
11
+ }
12
+ const records = [];
13
+ let skipped = 0;
14
+ let droppedEvents = 0;
15
+ for (const name of names) {
16
+ let run;
17
+ try {
18
+ run = JSON.parse(await readFile(join(flowsDir, name), "utf8"));
19
+ }
20
+ catch {
21
+ skipped += 1;
22
+ continue;
23
+ }
24
+ try {
25
+ droppedEvents += collect(run, records);
26
+ }
27
+ catch {
28
+ skipped += 1;
29
+ }
30
+ }
31
+ return { records, skipped, droppedEvents };
32
+ }
33
+ /** Returns the number of failure-shaped events that were malformed and dropped. */
34
+ function collect(run, out) {
35
+ const runId = str(run.id);
36
+ if (runId === undefined)
37
+ throw new Error("run has no id");
38
+ const flowName = str(run.flowName) ?? "";
39
+ const workspaceRoot = str(run.workspaceRoot);
40
+ const specDigest = str(run.revisionDigest);
41
+ const events = Array.isArray(run.events) ? run.events : [];
42
+ let dropped = 0;
43
+ for (const [index, event] of events.entries()) {
44
+ const type = str(event.type);
45
+ const detail = isRecord(event.detail) ? event.detail : undefined;
46
+ if (detail === undefined)
47
+ continue;
48
+ if (type === "result") {
49
+ if (!("failure" in detail))
50
+ continue;
51
+ // `failure` is a string in some drifted runs: failure-shaped but unusable.
52
+ const failure = isRecord(detail.failure) ? detail.failure : undefined;
53
+ if (failure === undefined) {
54
+ dropped += 1;
55
+ continue;
56
+ }
57
+ const reason = str(failure.reason);
58
+ if (reason === undefined) {
59
+ dropped += 1;
60
+ continue;
61
+ }
62
+ const stepId = str(event.stepId) ?? null;
63
+ out.push({
64
+ runId,
65
+ flowName,
66
+ stepId,
67
+ ...(specDigest !== undefined ? { specDigest } : {}),
68
+ attempt: num(failure.attempt) ?? num(detail.attempt) ?? 0,
69
+ reason,
70
+ shape: shapeOf(reason),
71
+ ...(workspaceRoot !== undefined ? { workspaceRoot } : {}),
72
+ at: str(event.at) ?? "",
73
+ recovered: stepId !== null && succeededAfter(events, stepId, index),
74
+ });
75
+ continue;
76
+ }
77
+ if (type === "budget_exhausted") {
78
+ const reason = str(detail.reason);
79
+ if (reason === undefined) {
80
+ dropped += 1;
81
+ continue;
82
+ }
83
+ out.push({
84
+ runId,
85
+ flowName,
86
+ stepId: null,
87
+ ...(specDigest !== undefined ? { specDigest } : {}),
88
+ attempt: num(detail.attempt) ?? 0,
89
+ reason,
90
+ shape: "budget",
91
+ ...(workspaceRoot !== undefined ? { workspaceRoot } : {}),
92
+ at: str(event.at) ?? "",
93
+ recovered: false,
94
+ });
95
+ }
96
+ }
97
+ return dropped;
98
+ }
99
+ /**
100
+ * Did this step succeed AFTER this failure? Position matters: a success recorded
101
+ * earlier in the run says nothing about whether this failure was recovered, and
102
+ * treating it as recovery overstates what the evidence shows.
103
+ */
104
+ function succeededAfter(events, stepId, afterIndex) {
105
+ for (let i = afterIndex + 1; i < events.length; i += 1) {
106
+ const event = events[i];
107
+ if (event === undefined || str(event.type) !== "result")
108
+ continue;
109
+ if (str(event.stepId) !== stepId)
110
+ continue;
111
+ const detail = isRecord(event.detail) ? event.detail : undefined;
112
+ if (detail !== undefined && !isRecord(detail.failure))
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ function shapeOf(reason) {
118
+ const trimmed = reason.trimStart();
119
+ if (trimmed.startsWith("[") || trimmed.startsWith("{"))
120
+ return "schema";
121
+ if (/budget exhausted/i.test(reason))
122
+ return "budget";
123
+ if (trimmed.startsWith("ensure"))
124
+ return "ensure";
125
+ if (trimmed.startsWith("gate"))
126
+ return "gate";
127
+ return "other";
128
+ }
129
+ function isRecord(value) {
130
+ return typeof value === "object" && value !== null && !Array.isArray(value);
131
+ }
132
+ function str(value) {
133
+ return typeof value === "string" ? value : undefined;
134
+ }
135
+ function num(value) {
136
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
137
+ }
138
+ //# sourceMappingURL=harvest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest.js","sourceRoot":"","sources":["../../src/learn/harvest.ts"],"names":[],"mappings":"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"}