@davesheffer/hunch 1.4.1 → 1.5.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/README.md +91 -281
- package/dist/cli/index.js +219 -29
- package/dist/core/autoreview.js +52 -0
- package/dist/core/drift.js +25 -3
- package/dist/core/refrepair.js +33 -0
- package/dist/extractors/git.js +31 -1
- package/dist/integrations/hooks.js +4 -0
- package/dist/mcp/server.js +23 -22
- package/dist/store/hunchStore.js +26 -5
- package/dist/synthesis/provider.js +58 -0
- package/dist/synthesis/synthesize.js +32 -17
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { indexRepo } from "../extractors/indexer.js";
|
|
|
29
29
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
30
30
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
31
31
|
import { selectProvider } from "../synthesis/provider.js";
|
|
32
|
-
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, commitAndPushHunch, pullHunch, gitUntrackCached, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
|
|
32
|
+
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, workingFiles, commitFiles, asOfDate, stagedDiff, workingDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, commitAndPushHunch, pullHunch, gitUntrackCached, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
|
|
33
33
|
import { writeTeamConfig, ensureTeamOverlay, readTeamConfig } from "../integrations/team.js";
|
|
34
34
|
import { runbookId, decisionId } from "../core/ids.js";
|
|
35
35
|
import { deriveForbids, effectiveForbids } from "../core/constraintmatch.js";
|
|
@@ -52,6 +52,7 @@ import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpol
|
|
|
52
52
|
import { injectionMode } from "../core/hookcache.js";
|
|
53
53
|
import { PIPELINE_LOOP, UNVERIFIED_NAG, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, savePipelineState, stopVerdict, } from "../core/pipeline.js";
|
|
54
54
|
import { draftDuplicateOf } from "../core/dupdetect.js";
|
|
55
|
+
import { planAutoReview, planMutations } from "../core/autoreview.js";
|
|
55
56
|
import { loadGoldenSet, evaluateGraphLift } from "../eval/harness.js";
|
|
56
57
|
import { loadGuardCases, evalGuards, generateGuardCases } from "../eval/guards.js";
|
|
57
58
|
import { computeDrift } from "../core/drift.js";
|
|
@@ -68,6 +69,7 @@ import { mergeHunchJson } from "../store/merge.js";
|
|
|
68
69
|
import { movePublicMemoryToPrivate } from "../store/privateMigrate.js";
|
|
69
70
|
import { ENTITY_KINDS } from "../core/types.js";
|
|
70
71
|
import { planCompaction } from "../store/compact.js";
|
|
72
|
+
import { repairDecisionReference } from "../core/refrepair.js";
|
|
71
73
|
import { resolveInvocation } from "./invocation.js";
|
|
72
74
|
const program = new Command();
|
|
73
75
|
program.name("hunch").description("Hunch — an Engineering Memory OS: a git-native reasoning graph for your codebase.").version(HUNCH_VERSION);
|
|
@@ -138,7 +140,7 @@ program
|
|
|
138
140
|
}
|
|
139
141
|
if (isGitRepo(root)) {
|
|
140
142
|
const syncToOverlay = !!(opts.privateSync || opts.sharedSync);
|
|
141
|
-
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: opts.autoCommit });
|
|
143
|
+
const h = installPostCommitHook(root, inv.shell, { private: syncToOverlay, commit: opts.autoCommit, localOnly: syncToOverlay });
|
|
142
144
|
console.log(` ✓ post-commit hook ${h.action} (learning loop)${syncToOverlay ? " — syncs to the shared overlay" : ""}${opts.autoCommit ? " — auto-commit on" : ""}`);
|
|
143
145
|
const m = installMergeDriver(root, inv.shell);
|
|
144
146
|
console.log(` ✓ team merge driver ${m.action}`);
|
|
@@ -320,7 +322,16 @@ program
|
|
|
320
322
|
return opts.quiet ? undefined : fail("--private/--overlay needs HUNCH_PRIVATE_DIR set to an overlay store");
|
|
321
323
|
}
|
|
322
324
|
store.json.ensureDirs();
|
|
323
|
-
const r = await syncCommit(store, root, sha ?? headSha(root), {
|
|
325
|
+
const r = await syncCommit(store, root, sha ?? headSha(root), {
|
|
326
|
+
force: opts.force,
|
|
327
|
+
private: toOverlay,
|
|
328
|
+
// A split-private overlay is sensitive/local by definition. A shared store
|
|
329
|
+
// is an explicit team policy and may keep its configured synthesis provider.
|
|
330
|
+
localOnly: toOverlay && store.mode === "private",
|
|
331
|
+
deep: opts.deep,
|
|
332
|
+
verify: opts.verify,
|
|
333
|
+
samples: parseSamples(opts.samples),
|
|
334
|
+
});
|
|
324
335
|
if (r.status === "written") {
|
|
325
336
|
store.reindex();
|
|
326
337
|
// Don't rewrite grounding from the hook — it would dirty the working tree on
|
|
@@ -455,7 +466,7 @@ function configureOverlay(dir, opts, mode) {
|
|
|
455
466
|
// 4) route post-commit synthesis to the overlay (local hook, never committed)
|
|
456
467
|
let hookNote = "";
|
|
457
468
|
if (opts.hook && isGitRepo(root)) {
|
|
458
|
-
const h = installPostCommitHook(root, inv.shell, { private: true, commit: opts.autoCommit });
|
|
469
|
+
const h = installPostCommitHook(root, inv.shell, { private: true, commit: opts.autoCommit, localOnly: mode === "private" });
|
|
459
470
|
hookNote = ` ✓ post-commit hook ${h.action} — captured decisions route here${opts.autoCommit ? " (auto-commit+push on)" : ""}\n`;
|
|
460
471
|
}
|
|
461
472
|
// 5) one-time migration: MOVE existing public memory INTO the overlay, then make
|
|
@@ -1047,16 +1058,24 @@ program
|
|
|
1047
1058
|
.description("Capture a Bug from a failing test (symptom + suspect ranking).")
|
|
1048
1059
|
.requiredOption("--test <id>", "failing test id/name")
|
|
1049
1060
|
.requiredOption("--message <msg>", "failure message / stack")
|
|
1061
|
+
.option("--private", "keep the bug and its failure text in the private overlay; uses deterministic local synthesis")
|
|
1050
1062
|
.action(async (opts) => {
|
|
1051
1063
|
const { store, root } = storeFor();
|
|
1064
|
+
if (opts.private && !store.hasPrivate) {
|
|
1065
|
+
store.close();
|
|
1066
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
1067
|
+
}
|
|
1052
1068
|
store.json.ensureDirs();
|
|
1053
|
-
const r = await recordFailure(store, root, { test: opts.test, message: opts.message });
|
|
1069
|
+
const r = await recordFailure(store, root, { test: opts.test, message: opts.message }, { private: opts.private });
|
|
1054
1070
|
store.reindex();
|
|
1055
|
-
|
|
1071
|
+
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${r.bug.id}`);
|
|
1072
|
+
console.log(`✓ recorded bug ${r.bug.id} via ${r.provider}: "${r.bug.title}"${opts.private ? " [private overlay; local-only synthesis]" : ""}`);
|
|
1056
1073
|
if (r.bug.lineage.recurrence_of)
|
|
1057
1074
|
console.log(` ↳ recurrence of ${r.bug.lineage.recurrence_of}`);
|
|
1058
1075
|
if (r.constraint)
|
|
1059
1076
|
console.log(` ↳ promoted constraint ${r.constraint.id} [${r.constraint.severity}]: ${r.constraint.statement}`);
|
|
1077
|
+
if (flush === "pushed")
|
|
1078
|
+
console.log(" ↳ private memory committed + pushed");
|
|
1060
1079
|
store.close();
|
|
1061
1080
|
});
|
|
1062
1081
|
// ---- record-constraint (human-authored invariant) -------------------------
|
|
@@ -1073,11 +1092,16 @@ program
|
|
|
1073
1092
|
.option("--forbid-dep <names>", "comma-sep imports that BREAK the rule (parsed-import precise; e.g. lodash) — blocks the real violation, immune to staleness")
|
|
1074
1093
|
.option("--forbid-symbol <names>", "comma-sep identifier names that break the rule")
|
|
1075
1094
|
.option("--match <regex>", "textual line regex (lint-grade last resort; prefer --forbid-dep/--forbid-symbol)")
|
|
1095
|
+
.option("--private", "write the invariant into the private overlay (local enforcement only; never included in public CI output)")
|
|
1076
1096
|
.action((statement, opts) => {
|
|
1077
1097
|
const SEV = ["advisory", "warning", "blocking"];
|
|
1078
1098
|
if (!SEV.includes(opts.severity))
|
|
1079
1099
|
return fail(`--severity must be one of: ${SEV.join(", ")}`);
|
|
1080
1100
|
const { store, root } = storeFor();
|
|
1101
|
+
if (opts.private && !store.hasPrivate) {
|
|
1102
|
+
store.close();
|
|
1103
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
1104
|
+
}
|
|
1081
1105
|
store.json.ensureDirs();
|
|
1082
1106
|
const scope = opts.scope.split(",").map((s) => toPosixTarget(s.trim())).filter(Boolean);
|
|
1083
1107
|
const csv = (s) => (s ? s.split(",").map((x) => x.trim()).filter(Boolean) : []);
|
|
@@ -1107,10 +1131,14 @@ program
|
|
|
1107
1131
|
valid_from: new Date().toISOString(),
|
|
1108
1132
|
valid_to: null,
|
|
1109
1133
|
provenance: { source: "human_confirmed", confidence: 1, evidence: [], last_verified: new Date().toISOString() },
|
|
1110
|
-
});
|
|
1134
|
+
}, opts.private);
|
|
1111
1135
|
store.reindex();
|
|
1112
|
-
|
|
1113
|
-
|
|
1136
|
+
// Public grounding is a publishable artifact. Private rules stay local and
|
|
1137
|
+
// are surfaced by local checks/MCP, never copied into committed agent docs.
|
|
1138
|
+
if (store.captureHome(!!opts.private) === "public")
|
|
1139
|
+
refreshExistingGrounding(root, store);
|
|
1140
|
+
const flush = flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture ${c.id}`);
|
|
1141
|
+
console.log(`✓ recorded ${c.severity} constraint ${c.id}: "${c.statement}" (scope: ${scope.join(", ") || "repo"})${opts.private ? " [private overlay]" : ""}`);
|
|
1114
1142
|
if (derived && c.forbids?.deps.length)
|
|
1115
1143
|
console.log(` ↳ matcher: forbids import of ${c.forbids.deps.join(", ")} (precise, immune to staleness)`);
|
|
1116
1144
|
if (c.severity === "blocking" && !effectiveForbids(c)) {
|
|
@@ -1119,6 +1147,8 @@ program
|
|
|
1119
1147
|
console.log(` ⚠ scope-only — this will downgrade to advisory once a file in scope is changed after today.`);
|
|
1120
1148
|
console.log(` To block the actual violation across the file's life, add --forbid-dep <pkg> (or --forbid-symbol / --match).`);
|
|
1121
1149
|
}
|
|
1150
|
+
if (flush === "pushed")
|
|
1151
|
+
console.log(" ↳ private memory committed + pushed");
|
|
1122
1152
|
store.close();
|
|
1123
1153
|
});
|
|
1124
1154
|
// ---- test (failure-learning loop) -----------------------------------------
|
|
@@ -1127,8 +1157,13 @@ program
|
|
|
1127
1157
|
.description("Run the test suite; capture failures as Bugs (suspects + recurrence → Constraints), mark passing tests' bugs fixed.")
|
|
1128
1158
|
.argument("[cmd...]", "test command to run (default: `npm test`)")
|
|
1129
1159
|
.option("--dry-run", "show what would be captured without writing")
|
|
1160
|
+
.option("--private", "keep captured test failures in the private overlay and use deterministic local synthesis")
|
|
1130
1161
|
.action(async (cmd, opts) => {
|
|
1131
1162
|
const { store, root } = storeFor();
|
|
1163
|
+
if (opts.private && !store.hasPrivate) {
|
|
1164
|
+
store.close();
|
|
1165
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
1166
|
+
}
|
|
1132
1167
|
store.json.ensureDirs();
|
|
1133
1168
|
// Run as a shell string (not argv) so the npm/test-runner shim resolves on
|
|
1134
1169
|
// Windows and avoids Node's DEP0190 args+shell warning — same lesson as the
|
|
@@ -1155,7 +1190,7 @@ program
|
|
|
1155
1190
|
store.close();
|
|
1156
1191
|
return;
|
|
1157
1192
|
}
|
|
1158
|
-
const cap = await captureTestRun(store, root, { report, status: run.status, cmd: cmdStr, output });
|
|
1193
|
+
const cap = await captureTestRun(store, root, { report, status: run.status, cmd: cmdStr, output, private: opts.private });
|
|
1159
1194
|
for (const { bug, constraint } of cap.results) {
|
|
1160
1195
|
if (constraint)
|
|
1161
1196
|
console.log(` ⚠ ${bug.id} "${bug.title}" → promoted constraint ${constraint.id} [${constraint.severity}]`);
|
|
@@ -1165,6 +1200,8 @@ program
|
|
|
1165
1200
|
for (const b of cap.fixed)
|
|
1166
1201
|
console.log(` ✓ ${b.id} "${b.title}" → fixed (test passing)`);
|
|
1167
1202
|
store.reindex();
|
|
1203
|
+
if (cap.results.length || cap.fixed.length)
|
|
1204
|
+
flushCapture(store, hunchPaths(root).hunch, !!opts.private, `hunch: capture test results`);
|
|
1168
1205
|
store.close();
|
|
1169
1206
|
const recurrences = cap.results.filter((r) => r.bug.lineage.recurrence_of).length;
|
|
1170
1207
|
const promoted = cap.results.filter((r) => r.constraint).length;
|
|
@@ -1229,6 +1266,7 @@ program
|
|
|
1229
1266
|
.command("check")
|
|
1230
1267
|
.description("Flag changes that touch a do-not-break invariant — the local guardrail AND the CI/PR Constraint Guard. Also flags (advisory) symbols you add that already exist elsewhere — possible re-implementation/sprawl.")
|
|
1231
1268
|
.option("--staged", "check git staged files (default)")
|
|
1269
|
+
.option("--working", "check all working-tree edits vs HEAD (staged, unstaged, and untracked files)")
|
|
1232
1270
|
.option("--commit <sha>", "check a specific commit's files")
|
|
1233
1271
|
.option("--base <ref>", "check a PR/branch: files changed vs <ref> (e.g. origin/main) — for CI")
|
|
1234
1272
|
.option("--strict", "exit non-zero ONLY on a direct, high-confidence, non-stale blocking invariant (near/stale/low-confidence stay advisory)")
|
|
@@ -1236,9 +1274,9 @@ program
|
|
|
1236
1274
|
.option("--blast", "also print the dependency blast radius of the changed files")
|
|
1237
1275
|
.option("--public-only", "exclude the private overlay (HUNCH_PRIVATE_DIR) from the report — use for any output that may be posted publicly (the CI PR comment passes this)")
|
|
1238
1276
|
.action((opts) => {
|
|
1239
|
-
const sources = [opts.commit && "--commit", opts.base && "--base", opts.staged && "--staged"].filter(Boolean);
|
|
1277
|
+
const sources = [opts.commit && "--commit", opts.base && "--base", opts.staged && "--staged", opts.working && "--working"].filter(Boolean);
|
|
1240
1278
|
if (sources.length > 1)
|
|
1241
|
-
return fail(`pick one of --staged / --commit / --base (got ${sources.join(", ")})`);
|
|
1279
|
+
return fail(`pick one of --staged / --working / --commit / --base (got ${sources.join(", ")})`);
|
|
1242
1280
|
const markdown = opts.format === "markdown";
|
|
1243
1281
|
const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], vetoes: [], redundant: [], strictBlockers: 0, regBlocking: 0, vetoBlocking: 0 };
|
|
1244
1282
|
const { store, root } = storeFor();
|
|
@@ -1251,7 +1289,8 @@ program
|
|
|
1251
1289
|
store.reindex(); // blast radius walks the edge graph — make the index current
|
|
1252
1290
|
const files = opts.commit ? commitFiles(opts.commit, root)
|
|
1253
1291
|
: opts.base ? rangeFiles(opts.base, root)
|
|
1254
|
-
:
|
|
1292
|
+
: opts.working ? workingFiles(root)
|
|
1293
|
+
: stagedFiles(root);
|
|
1255
1294
|
if (!files.length) {
|
|
1256
1295
|
console.log(markdown ? renderMarkdown(emptyReport) : "No changed files to check.");
|
|
1257
1296
|
store.close();
|
|
@@ -1261,7 +1300,7 @@ program
|
|
|
1261
1300
|
// code) + REDUNDANT (adds a symbol already defined elsewhere — advisory) + the
|
|
1262
1301
|
// hardened strict gate + causal `why` citations — all assembled by the shared
|
|
1263
1302
|
// store.buildCheckReport (also used by the hunch_merge_verdict tool).
|
|
1264
|
-
const diff = opts.commit ? commitDiff(opts.commit, root) : opts.base ? rangeDiff(opts.base, root) : stagedDiff(root);
|
|
1303
|
+
const diff = opts.commit ? commitDiff(opts.commit, root) : opts.base ? rangeDiff(opts.base, root) : opts.working ? workingDiff(root) : stagedDiff(root);
|
|
1265
1304
|
const report = store.buildCheckReport(files, diff, {
|
|
1266
1305
|
strict: !!opts.strict,
|
|
1267
1306
|
lastChange: (f) => lastChangeDate(f, root),
|
|
@@ -1774,29 +1813,40 @@ program
|
|
|
1774
1813
|
.option("--accept-verified", "batch-accept every Critic-verified, well-grounded draft (>= --min-grounded)")
|
|
1775
1814
|
.option("--reject-duplicates", "batch-reject drafts that near-duplicate an accepted record (deterministic term+file similarity — hygiene, not judgment)")
|
|
1776
1815
|
.option("--min-grounded <n>", "grounded-ness threshold for the ready group / --accept-verified", String(READY_MIN_GROUNDED))
|
|
1816
|
+
.option("--private", "include local private/shared-overlay drafts; terminal output may contain private memory")
|
|
1777
1817
|
.action((opts) => {
|
|
1778
1818
|
const { store, root } = storeFor();
|
|
1779
1819
|
const minGrounded = Number.isFinite(Number(opts.minGrounded)) ? Number(opts.minGrounded) : READY_MIN_GROUNDED;
|
|
1820
|
+
if (opts.private && !store.hasPrivate) {
|
|
1821
|
+
store.close();
|
|
1822
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
1823
|
+
}
|
|
1824
|
+
const decisions = () => opts.private ? store.recs("decisions") : store.json.loadAll("decisions");
|
|
1825
|
+
let publicGroundingChanged = false;
|
|
1780
1826
|
if (opts.accept) {
|
|
1781
|
-
const d = store.json.get("decisions", opts.accept);
|
|
1827
|
+
const d = opts.private ? store.getRec("decisions", opts.accept) : store.json.get("decisions", opts.accept);
|
|
1782
1828
|
if (!d) {
|
|
1783
1829
|
store.close();
|
|
1784
1830
|
return fail(`decision ${opts.accept} not found`);
|
|
1785
1831
|
}
|
|
1832
|
+
const inPrivate = !!store.getPrivateRec("decisions", d.id);
|
|
1786
1833
|
const { source, armed } = acceptDecision(store, d);
|
|
1787
1834
|
store.reindex();
|
|
1788
|
-
|
|
1835
|
+
if (!inPrivate) {
|
|
1836
|
+
refreshExistingGrounding(root, store);
|
|
1837
|
+
publicGroundingChanged = true;
|
|
1838
|
+
}
|
|
1789
1839
|
console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
|
|
1790
1840
|
}
|
|
1791
1841
|
else if (opts.reject) {
|
|
1792
|
-
const ok2 = store.json.delete("decisions", opts.reject);
|
|
1842
|
+
const ok2 = opts.private ? store.deleteWhereItLives("decisions", opts.reject) : store.json.delete("decisions", opts.reject);
|
|
1793
1843
|
store.reindex();
|
|
1794
1844
|
console.log(ok2 ? `✓ rejected and removed ${opts.reject}` : `decision ${opts.reject} not found`);
|
|
1795
1845
|
}
|
|
1796
1846
|
else if (opts.rejectDuplicates) {
|
|
1797
1847
|
// Deterministic hygiene, not a trust decision (dec_a466655539 stays intact):
|
|
1798
1848
|
// only drafts, only against ACCEPTED records, conservative threshold.
|
|
1799
|
-
const all =
|
|
1849
|
+
const all = decisions();
|
|
1800
1850
|
const drafts = all.filter((d) => d.status === "proposed" && !d.provenance.source.includes("human_confirmed"));
|
|
1801
1851
|
const dupes = drafts
|
|
1802
1852
|
.map((d) => ({ d, m: draftDuplicateOf(d, all) }))
|
|
@@ -1807,7 +1857,7 @@ program
|
|
|
1807
1857
|
else {
|
|
1808
1858
|
let removed = 0;
|
|
1809
1859
|
for (const { d, m } of dupes) {
|
|
1810
|
-
if (store.json.delete("decisions", d.id))
|
|
1860
|
+
if ((opts.private ? store.deleteWhereItLives("decisions", d.id) : store.json.delete("decisions", d.id)))
|
|
1811
1861
|
removed++;
|
|
1812
1862
|
console.log(` ✗ ${d.id} — "${d.title}"\n duplicate of ${m.of.id} — "${m.of.title}" (${Math.round(m.score * 100)}%)`);
|
|
1813
1863
|
}
|
|
@@ -1818,24 +1868,28 @@ program
|
|
|
1818
1868
|
else if (opts.acceptVerified) {
|
|
1819
1869
|
// Batch path: only Critic-verified, well-grounded drafts qualify — still the
|
|
1820
1870
|
// human-driven accept gate (the operator runs this), just over a safe subset.
|
|
1821
|
-
const proposed =
|
|
1871
|
+
const proposed = decisions().filter((d) => d.status === "proposed");
|
|
1822
1872
|
const { ready } = partitionReview(proposed, minGrounded);
|
|
1823
1873
|
if (!ready.length) {
|
|
1824
1874
|
console.log(`✓ No Critic-verified drafts at grounded ≥ ${minGrounded} to batch-accept.`);
|
|
1825
1875
|
}
|
|
1826
1876
|
else {
|
|
1827
1877
|
let armedTotal = 0;
|
|
1828
|
-
for (const it of ready)
|
|
1878
|
+
for (const it of ready) {
|
|
1879
|
+
if (!store.getPrivateRec("decisions", it.d.id))
|
|
1880
|
+
publicGroundingChanged = true;
|
|
1829
1881
|
armedTotal += acceptDecision(store, it.d).armed;
|
|
1882
|
+
}
|
|
1830
1883
|
store.reindex();
|
|
1831
|
-
|
|
1884
|
+
if (publicGroundingChanged)
|
|
1885
|
+
refreshExistingGrounding(root, store); // committed grounding stays public-only
|
|
1832
1886
|
console.log(`✓ accepted ${ready.length} verified draft(s); ${armedTotal} tripwire(s) now blocking.`);
|
|
1833
1887
|
for (const it of ready)
|
|
1834
1888
|
console.log(` ${it.d.id} grounded=${it.synth.grounded ?? "?"} ${it.d.title}`);
|
|
1835
1889
|
}
|
|
1836
1890
|
}
|
|
1837
1891
|
else {
|
|
1838
|
-
const drafts =
|
|
1892
|
+
const drafts = decisions().filter((d) => d.status === "proposed" || d.provenance.confidence < 0.6);
|
|
1839
1893
|
const { ready, scrutiny } = partitionReview(drafts, minGrounded);
|
|
1840
1894
|
if (!ready.length && !scrutiny.length) {
|
|
1841
1895
|
console.log("✓ No low-confidence drafts to review.");
|
|
@@ -1849,7 +1903,7 @@ program
|
|
|
1849
1903
|
}
|
|
1850
1904
|
if (scrutiny.length) {
|
|
1851
1905
|
console.log(`⚠ ${scrutiny.length} need scrutiny — unverified / low-grounded (lowest confidence first):\n`);
|
|
1852
|
-
const all =
|
|
1906
|
+
const all = decisions();
|
|
1853
1907
|
let dupCount = 0;
|
|
1854
1908
|
for (const it of scrutiny) {
|
|
1855
1909
|
printReviewItem(it);
|
|
@@ -1867,6 +1921,109 @@ program
|
|
|
1867
1921
|
}
|
|
1868
1922
|
store.close();
|
|
1869
1923
|
});
|
|
1924
|
+
// ---- auto-review (harness-driven triage) ----------------------------------
|
|
1925
|
+
/** One line per plan entry. */
|
|
1926
|
+
function printAutoEntry(e) {
|
|
1927
|
+
console.log(` ${e.d.id} ${e.d.title.slice(0, 66)}\n ${dim(e.reason)}`);
|
|
1928
|
+
}
|
|
1929
|
+
program
|
|
1930
|
+
.command("auto-review")
|
|
1931
|
+
.description("Harness-driven draft triage: delegate relevance to the coding-assistant CLI, then dedup, auto-confirm the verified+relevant, and delete duplicates/irrelevant. Dry-run unless --apply.")
|
|
1932
|
+
.option("--apply", "execute the plan (accept/delete). Without it, print the plan and change nothing.")
|
|
1933
|
+
.option("--min-grounded <n>", "grounded-ness threshold for the auto-accept gate", String(READY_MIN_GROUNDED))
|
|
1934
|
+
.option("--min-reject-confidence <n>", "minimum harness confidence to DELETE an irrelevant draft (else kept for a human)", "0.7")
|
|
1935
|
+
.option("--no-llm", "skip the harness judgment (dedup + grounding only — no relevance deletion)")
|
|
1936
|
+
.option("--private", "include local private/shared-overlay drafts; private drafts are never sent to an LLM judge")
|
|
1937
|
+
.action(async (opts) => {
|
|
1938
|
+
const { store, root } = storeFor();
|
|
1939
|
+
try {
|
|
1940
|
+
if (opts.private && !store.hasPrivate)
|
|
1941
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
1942
|
+
const minGrounded = Number.isFinite(Number(opts.minGrounded)) ? Number(opts.minGrounded) : READY_MIN_GROUNDED;
|
|
1943
|
+
const minRejectConfidence = Number.isFinite(Number(opts.minRejectConfidence)) ? Number(opts.minRejectConfidence) : 0.7;
|
|
1944
|
+
const all = opts.private ? store.recs("decisions") : store.json.loadAll("decisions");
|
|
1945
|
+
// Same draft set `hunch review` / `hunch status` triage.
|
|
1946
|
+
const drafts = all.filter((d) => d.status === "proposed" || d.provenance.confidence < 0.6);
|
|
1947
|
+
if (!drafts.length) {
|
|
1948
|
+
console.log("✓ No drafts to auto-review.");
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
// Delegate relevance to the harness (subscription CLI) — feature-detected,
|
|
1952
|
+
// and any per-draft failure degrades to "not judged" (kept for a human).
|
|
1953
|
+
const verdicts = new Map();
|
|
1954
|
+
if (opts.llm !== false && !opts.private) {
|
|
1955
|
+
const provider = await selectProvider();
|
|
1956
|
+
if (provider.judgeDraft) {
|
|
1957
|
+
// The candidate pool for duplicate_of / restatement: the LIVE, vouched records.
|
|
1958
|
+
const existing = all
|
|
1959
|
+
.filter((d) => d.provenance.source.includes("human_confirmed") && d.status !== "superseded" && d.status !== "rejected")
|
|
1960
|
+
.map((d) => ({ id: d.id, title: d.title, decision: d.decision }));
|
|
1961
|
+
console.log(`Judging ${drafts.length} draft(s) via ${provider.name} (subscription)…`);
|
|
1962
|
+
for (const d of drafts) {
|
|
1963
|
+
try {
|
|
1964
|
+
verdicts.set(d.id, await provider.judgeDraft(d, existing.filter((e) => e.id !== d.id)));
|
|
1965
|
+
}
|
|
1966
|
+
catch {
|
|
1967
|
+
/* transient / unparseable — leave unjudged, planner keeps it for a human */
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
console.log(dim("No subscription CLI available — relevance judgment skipped (dedup + grounding only)."));
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
else if (opts.private && opts.llm !== false) {
|
|
1976
|
+
console.log(dim("Private review stays local — harness judgment skipped (dedup + grounding only)."));
|
|
1977
|
+
}
|
|
1978
|
+
const plan = planAutoReview(drafts, all, verdicts, { minGrounded, minRejectConfidence });
|
|
1979
|
+
printAutoReviewPlan(plan);
|
|
1980
|
+
if (!opts.apply) {
|
|
1981
|
+
const n = planMutations(plan);
|
|
1982
|
+
console.log(`\n${dim(`Dry run — nothing changed. Re-run with --apply to ${n ? `apply ${n} change(s)` : "confirm (no changes)"}.`)}`);
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
// Apply: accept the verified+relevant, delete duplicates + irrelevant.
|
|
1986
|
+
let accepted = 0, deleted = 0, armedTotal = 0, publicAccepted = false;
|
|
1987
|
+
for (const e of plan.accept) {
|
|
1988
|
+
if (!store.getPrivateRec("decisions", e.d.id))
|
|
1989
|
+
publicAccepted = true;
|
|
1990
|
+
armedTotal += acceptDecision(store, e.d).armed;
|
|
1991
|
+
accepted++;
|
|
1992
|
+
}
|
|
1993
|
+
for (const e of [...plan.rejectDuplicate, ...plan.rejectIrrelevant]) {
|
|
1994
|
+
if ((opts.private ? store.deleteWhereItLives("decisions", e.d.id) : store.json.delete("decisions", e.d.id)))
|
|
1995
|
+
deleted++;
|
|
1996
|
+
}
|
|
1997
|
+
if (accepted || deleted) {
|
|
1998
|
+
store.reindex();
|
|
1999
|
+
if (publicAccepted)
|
|
2000
|
+
refreshExistingGrounding(root, store); // committed grounding stays public-only
|
|
2001
|
+
}
|
|
2002
|
+
console.log(`\n✓ auto-review applied: ${accepted} accepted${armedTotal ? ` (${armedTotal} tripwire(s) now blocking)` : ""}, ${deleted} deleted, ${plan.keep.length} kept for review.`);
|
|
2003
|
+
}
|
|
2004
|
+
finally {
|
|
2005
|
+
store.close();
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
/** Print the four buckets of an auto-review plan (skipping empty ones). */
|
|
2009
|
+
function printAutoReviewPlan(plan) {
|
|
2010
|
+
if (plan.accept.length) {
|
|
2011
|
+
console.log(`\n✓ ACCEPT — verified, grounded, harness-relevant (${plan.accept.length}):`);
|
|
2012
|
+
plan.accept.forEach(printAutoEntry);
|
|
2013
|
+
}
|
|
2014
|
+
if (plan.rejectDuplicate.length) {
|
|
2015
|
+
console.log(`\n✗ DELETE (duplicate) — restates an accepted record (${plan.rejectDuplicate.length}):`);
|
|
2016
|
+
plan.rejectDuplicate.forEach(printAutoEntry);
|
|
2017
|
+
}
|
|
2018
|
+
if (plan.rejectIrrelevant.length) {
|
|
2019
|
+
console.log(`\n✗ DELETE (irrelevant) — harness judged not worth keeping (${plan.rejectIrrelevant.length}):`);
|
|
2020
|
+
plan.rejectIrrelevant.forEach(printAutoEntry);
|
|
2021
|
+
}
|
|
2022
|
+
if (plan.keep.length) {
|
|
2023
|
+
console.log(`\n⏳ KEEP for human review (${plan.keep.length}):`);
|
|
2024
|
+
plan.keep.forEach(printAutoEntry);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1870
2027
|
// ---- mcp ------------------------------------------------------------------
|
|
1871
2028
|
program
|
|
1872
2029
|
.command("mcp")
|
|
@@ -2014,23 +2171,24 @@ program
|
|
|
2014
2171
|
.description("PR impact: the dependency + memory surface of a change — dependent files reached, invariants direct/near, and the decisions concerned. Read-only, advisory (gating is `hunch check`). Omit base and --commit to inspect staged changes.")
|
|
2015
2172
|
.argument("[base]", "diff against this base ref (e.g. origin/main) for a branch/PR")
|
|
2016
2173
|
.option("--commit <sha>", "impact of a single commit")
|
|
2174
|
+
.option("--working", "impact all working-tree edits vs HEAD (staged, unstaged, and untracked files)")
|
|
2017
2175
|
.action((base, opts) => {
|
|
2018
2176
|
const { store, root } = storeFor();
|
|
2019
2177
|
try {
|
|
2020
|
-
if (base && opts.commit)
|
|
2021
|
-
return fail("Pass
|
|
2178
|
+
if ((base && opts.commit) || (opts.working && (base || opts.commit)))
|
|
2179
|
+
return fail("Pass exactly one of [base] / --commit / --working (or omit all for staged changes).");
|
|
2022
2180
|
if (base && !revExists(base, root))
|
|
2023
2181
|
return fail(`base ref "${base}" does not resolve.`);
|
|
2024
2182
|
if (opts.commit && !revExists(opts.commit, root))
|
|
2025
2183
|
return fail(`commit "${opts.commit}" does not resolve.`);
|
|
2026
2184
|
store.reindex(); // reflect out-of-band JSON edits before reading the graph
|
|
2027
|
-
const files = opts.commit ? commitFiles(opts.commit, root) : base ? rangeFiles(base, root) : stagedFiles(root);
|
|
2028
|
-
const scope = opts.commit ? `commit ${opts.commit}` : base ? `${base}..HEAD` : "staged changes";
|
|
2185
|
+
const files = opts.commit ? commitFiles(opts.commit, root) : base ? rangeFiles(base, root) : opts.working ? workingFiles(root) : stagedFiles(root);
|
|
2186
|
+
const scope = opts.commit ? `commit ${opts.commit}` : base ? `${base}..HEAD` : opts.working ? "working changes" : "staged changes";
|
|
2029
2187
|
if (!files.length) {
|
|
2030
2188
|
console.log(`No changed files in ${scope}.`);
|
|
2031
2189
|
return;
|
|
2032
2190
|
}
|
|
2033
|
-
const diff = opts.commit ? commitDiff(opts.commit, root) : base ? rangeDiff(base, root) : stagedDiff(root);
|
|
2191
|
+
const diff = opts.commit ? commitDiff(opts.commit, root) : base ? rangeDiff(base, root) : opts.working ? workingDiff(root) : stagedDiff(root);
|
|
2034
2192
|
console.log(renderImpact(store.prImpact(files, diff), scope));
|
|
2035
2193
|
}
|
|
2036
2194
|
finally {
|
|
@@ -2238,6 +2396,38 @@ program
|
|
|
2238
2396
|
store.close();
|
|
2239
2397
|
}
|
|
2240
2398
|
});
|
|
2399
|
+
// ---- repair-ref (atomic decision reference correction) --------------------
|
|
2400
|
+
program
|
|
2401
|
+
.command("repair-ref")
|
|
2402
|
+
.description("Atomically repair one exact file reference in a decision's scope and provenance evidence (never changes the decision itself).")
|
|
2403
|
+
.argument("<decision>", "decision id containing the stale reference")
|
|
2404
|
+
.requiredOption("--from <path>", "exact stale path to replace")
|
|
2405
|
+
.requiredOption("--to <path>", "exact current path (use private:<path> for a private-overlay file)")
|
|
2406
|
+
.option("--private", "require the decision to be in the configured private overlay")
|
|
2407
|
+
.action((id, opts) => {
|
|
2408
|
+
const { store } = storeFor();
|
|
2409
|
+
try {
|
|
2410
|
+
if (opts.from === opts.to)
|
|
2411
|
+
return fail("--from and --to must be different paths");
|
|
2412
|
+
if (opts.private && !store.hasPrivate)
|
|
2413
|
+
return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
|
|
2414
|
+
// `getRec` is deliberately overlay-first. An explicit --private prevents a
|
|
2415
|
+
// same-id public record from being silently amended instead of private memory.
|
|
2416
|
+
const d = opts.private ? store.getPrivateRec("decisions", id) : store.getRec("decisions", id);
|
|
2417
|
+
if (!d)
|
|
2418
|
+
return fail(`decision "${id}" not found${opts.private ? " in the private overlay" : ""}`);
|
|
2419
|
+
const repaired = repairDecisionReference(d, opts.from, opts.to);
|
|
2420
|
+
if (!repaired)
|
|
2421
|
+
return fail(`decision "${id}" does not contain the exact reference "${opts.from}"`);
|
|
2422
|
+
store.putWhereItLives("decisions", repaired.decision);
|
|
2423
|
+
store.reindex();
|
|
2424
|
+
console.log(`✓ repaired ${id}: ${repaired.relatedFiles} related file reference(s) + ${repaired.evidence} provenance evidence reference(s).`);
|
|
2425
|
+
console.log(` ${opts.from} → ${opts.to}`);
|
|
2426
|
+
}
|
|
2427
|
+
finally {
|
|
2428
|
+
store.close();
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2241
2431
|
// ---- compact (bound Hunch growth) -----------------------------------------
|
|
2242
2432
|
program
|
|
2243
2433
|
.command("compact")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { draftDuplicateOf } from "./dupdetect.js";
|
|
2
|
+
import { parseSynth, isReady, READY_MIN_GROUNDED } from "./reviewqueue.js";
|
|
3
|
+
const DEFAULT_MIN_REJECT_CONFIDENCE = 0.7;
|
|
4
|
+
/** Build the plan. `verdicts` maps draft id → harness verdict (absent → the draft
|
|
5
|
+
* was not judged, e.g. no CLI available; it can still be dup-rejected or kept). */
|
|
6
|
+
export function planAutoReview(drafts, allDecisions, verdicts, cfg = {}) {
|
|
7
|
+
const minGrounded = cfg.minGrounded ?? READY_MIN_GROUNDED;
|
|
8
|
+
const minReject = cfg.minRejectConfidence ?? DEFAULT_MIN_REJECT_CONFIDENCE;
|
|
9
|
+
const plan = { accept: [], rejectDuplicate: [], rejectIrrelevant: [], keep: [] };
|
|
10
|
+
for (const d of drafts) {
|
|
11
|
+
const verdict = verdicts.get(d.id);
|
|
12
|
+
const synth = parseSynth(d.provenance?.evidence);
|
|
13
|
+
const grounded = synth.grounded;
|
|
14
|
+
const base = { d, verdict, grounded };
|
|
15
|
+
// 1) Duplicate — deterministic match against accepted records, or the harness
|
|
16
|
+
// naming an existing decision. Deterministic wins first (cheapest, surest).
|
|
17
|
+
const detDup = draftDuplicateOf(d, allDecisions);
|
|
18
|
+
if (detDup) {
|
|
19
|
+
plan.rejectDuplicate.push({ ...base, action: "rejectDuplicate", reason: `near-duplicate of ${detDup.of.id} "${detDup.of.title}" (${Math.round(detDup.score * 100)}%)` });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (verdict?.duplicate_of && verdict.duplicate_of !== d.id && allDecisions.some((x) => x.id === verdict.duplicate_of)) {
|
|
23
|
+
plan.rejectDuplicate.push({ ...base, action: "rejectDuplicate", reason: `harness: restates ${verdict.duplicate_of} — ${verdict.reason}` });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// 2) Confidently-irrelevant — delete only on a strong harness "no".
|
|
27
|
+
if (verdict && !verdict.relevant && verdict.confidence >= minReject) {
|
|
28
|
+
plan.rejectIrrelevant.push({ ...base, action: "rejectIrrelevant", reason: `harness: not relevant (conf ${verdict.confidence}) — ${verdict.reason}` });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// 3) Accept — ONLY when the Critic verified + grounded it (isReady) AND the
|
|
32
|
+
// harness judged it relevant. The harness can VETO an accept, never create
|
|
33
|
+
// one on its own (dec_a466655539: the human vouch / Critic gate is the floor).
|
|
34
|
+
const ready = isReady(d, synth, minGrounded);
|
|
35
|
+
if (ready && verdict?.relevant) {
|
|
36
|
+
plan.accept.push({ ...base, action: "accept", reason: `verified + grounded ${grounded ?? "?"} ≥ ${minGrounded}, harness-relevant — ${verdict.reason}` });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// 4) Keep for a human — the safe default (unverified, ungrounded, unjudged, or
|
|
40
|
+
// a low-confidence irrelevant call).
|
|
41
|
+
const why = !verdict ? "not judged (no harness)"
|
|
42
|
+
: !ready ? (verdict.relevant ? "relevant but not Critic-verified/grounded — needs human confirm" : `irrelevant but low confidence (${verdict.confidence})`)
|
|
43
|
+
: "kept";
|
|
44
|
+
plan.keep.push({ ...base, action: "keep", reason: why });
|
|
45
|
+
}
|
|
46
|
+
return plan;
|
|
47
|
+
}
|
|
48
|
+
/** Total drafts the plan would mutate (accept + both delete buckets). */
|
|
49
|
+
export function planMutations(plan) {
|
|
50
|
+
return plan.accept.length + plan.rejectDuplicate.length + plan.rejectIrrelevant.length;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=autoreview.js.map
|
package/dist/core/drift.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `hunch wiki --heal`, never a gate.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
16
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
17
17
|
import { toPosixTarget } from "./paths.js";
|
|
18
18
|
import { currentForTopic, isLive } from "./topics.js";
|
|
19
19
|
import { parseDocAnchors } from "./docanchors.js";
|
|
@@ -36,7 +36,7 @@ export function computeDrift(store, root) {
|
|
|
36
36
|
for (const f of d.related_files ?? []) {
|
|
37
37
|
if (!f || f.includes("*"))
|
|
38
38
|
continue; // skip globs / empties
|
|
39
|
-
if (!
|
|
39
|
+
if (!referenceExists(store, root, d.id, f)) {
|
|
40
40
|
findings.push({ kind: "dead-ref", id: d.id, detail: `references missing file "${f}"` });
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -66,7 +66,7 @@ export function computeDrift(store, root) {
|
|
|
66
66
|
for (const f of d.related_files ?? []) {
|
|
67
67
|
if (!f || f.includes("*") || liveFiles.has(toPosixTarget(f)))
|
|
68
68
|
continue;
|
|
69
|
-
if (!
|
|
69
|
+
if (!referenceExists(store, root, d.id, f))
|
|
70
70
|
continue; // missing file is history → dead-ref's job
|
|
71
71
|
findings.push({
|
|
72
72
|
kind: "anchor-stale",
|
|
@@ -120,6 +120,28 @@ export function computeDrift(store, root) {
|
|
|
120
120
|
findings.push(...computeWikiDrift(store, root));
|
|
121
121
|
return { findings };
|
|
122
122
|
}
|
|
123
|
+
/** Resolve a decision file reference without making private-memory paths depend on
|
|
124
|
+
* the current machine's overlay location. Normal references are code-repo-relative.
|
|
125
|
+
* A `private:<path>` reference is valid only when the decision itself is in the
|
|
126
|
+
* private overlay and resolves from that overlay repo's root. This lets a private
|
|
127
|
+
* decision cite private docs while preventing a public record from silently
|
|
128
|
+
* depending on unsharable local files. */
|
|
129
|
+
function referenceExists(store, root, decisionId, ref) {
|
|
130
|
+
const prefix = "private:";
|
|
131
|
+
if (!ref.startsWith(prefix))
|
|
132
|
+
return existsSync(join(root, ref));
|
|
133
|
+
const privatePath = ref.slice(prefix.length);
|
|
134
|
+
if (!privatePath || isAbsolute(privatePath) || !store.privateDir || !store.getPrivateRec("decisions", decisionId))
|
|
135
|
+
return false;
|
|
136
|
+
const privateRoot = dirname(store.privateDir);
|
|
137
|
+
const candidate = resolve(privateRoot, privatePath);
|
|
138
|
+
// A private-scoped reference is an overlay-repo-relative path, not an escape
|
|
139
|
+
// hatch into arbitrary local files.
|
|
140
|
+
const rel = relative(privateRoot, candidate);
|
|
141
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\\\" : "/"}`) || isAbsolute(rel))
|
|
142
|
+
return false;
|
|
143
|
+
return existsSync(candidate);
|
|
144
|
+
}
|
|
123
145
|
function safeRead(path) {
|
|
124
146
|
try {
|
|
125
147
|
return readFileSync(path, "utf8");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function replaceExact(values, from, to) {
|
|
2
|
+
let changed = 0;
|
|
3
|
+
const replaced = values.map((value) => {
|
|
4
|
+
if (value !== from)
|
|
5
|
+
return value;
|
|
6
|
+
changed++;
|
|
7
|
+
return to;
|
|
8
|
+
});
|
|
9
|
+
// A decision may already cite the destination. Keep the reference list a set
|
|
10
|
+
// after the repair so a correction cannot create duplicate scope/evidence.
|
|
11
|
+
return { values: [...new Set(replaced)], changed };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Return a corrected copy of a decision, or `null` when the source reference is
|
|
15
|
+
* not present. The decision's semantic content and verification timestamp are
|
|
16
|
+
* intentionally preserved: this repairs a locator, it does not re-approve intent.
|
|
17
|
+
*/
|
|
18
|
+
export function repairDecisionReference(decision, from, to) {
|
|
19
|
+
const files = replaceExact(decision.related_files, from, to);
|
|
20
|
+
const evidence = replaceExact(decision.provenance.evidence, from, to);
|
|
21
|
+
if (!files.changed && !evidence.changed)
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
decision: {
|
|
25
|
+
...decision,
|
|
26
|
+
related_files: files.values,
|
|
27
|
+
provenance: { ...decision.provenance, evidence: evidence.values },
|
|
28
|
+
},
|
|
29
|
+
relatedFiles: files.changed,
|
|
30
|
+
evidence: evidence.changed,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=refrepair.js.map
|