@davesheffer/hunch 0.20.0 → 0.21.1
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 +7 -0
- package/dist/cli/index.js +33 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/git.js +12 -0
- package/dist/integrations/gitignore.js +34 -4
- package/dist/integrations/providers.js +16 -1
- package/dist/mcp/server.js +7 -3
- package/dist/store/hunchStore.js +19 -4
- package/dist/store/jsonStore.js +18 -0
- package/dist/store/privateMigrate.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,6 +179,13 @@ the CI PR comment render *public-only*, so a private record can't reach a public
|
|
|
179
179
|
sensitive items with `private: true` (`hunch_record_decision` / `hunch_record_correction`);
|
|
180
180
|
post-commit synthesis can route there too, and `hunch private --auto-commit` (opt-in)
|
|
181
181
|
auto-commits + pushes each capture to the private repo — recursion-safe, staging only `.hunch/`.
|
|
182
|
+
|
|
183
|
+
Already published a repo *with* its `.hunch/` memory and want it private after the fact?
|
|
184
|
+
`hunch private --repo <url> --migrate` does it in one shot: it **moves** your existing public
|
|
185
|
+
records into the overlay (union by id — nothing is lost), empties the public store, untracks +
|
|
186
|
+
gitignores the `.hunch/` memory tree, and regenerates the assistant grounding (CLAUDE.md, AGENTS.md,
|
|
187
|
+
…) so the repo becomes **code-only**. It commits the private overlay for you and prints the one
|
|
188
|
+
`git` command to commit the now-clean public repo.
|
|
182
189
|
→ [docs](https://hunch-pi.vercel.app/docs#private)
|
|
183
190
|
|
|
184
191
|
## Continuous learning (CI)
|
package/dist/cli/index.js
CHANGED
|
@@ -28,16 +28,16 @@ import { indexRepo } from "../extractors/indexer.js";
|
|
|
28
28
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
29
29
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
30
30
|
import { selectProvider } from "../synthesis/provider.js";
|
|
31
|
-
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists, commitAndPushHunch } from "../extractors/git.js";
|
|
31
|
+
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists, commitAndPushHunch, gitUntrackCached } from "../extractors/git.js";
|
|
32
32
|
import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkreport.js";
|
|
33
33
|
import { partitionReview, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
|
|
34
34
|
import { installPostCommitHook, installPreCommitHook } from "../integrations/hooks.js";
|
|
35
35
|
import { installMergeDriver } from "../integrations/mergeDriver.js";
|
|
36
|
-
import { ensureGitignore } from "../integrations/gitignore.js";
|
|
36
|
+
import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS } from "../integrations/gitignore.js";
|
|
37
37
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
38
38
|
import { updateClaudeMd } from "../integrations/claudemd.js";
|
|
39
39
|
import { writeMcpJson, writeSlashCommands, installClaudeHooks } from "../integrations/scaffold.js";
|
|
40
|
-
import { scaffoldProviders } from "../integrations/providers.js";
|
|
40
|
+
import { scaffoldProviders, regenerateGrounding } from "../integrations/providers.js";
|
|
41
41
|
import { healClaudeConfigCaseSplit } from "../integrations/claudeConfig.js";
|
|
42
42
|
import { formatContext } from "../core/format.js";
|
|
43
43
|
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness } from "../core/config.js";
|
|
@@ -46,6 +46,8 @@ import { draftTripwires, knownRepoDeps } from "../synthesis/tripwires.js";
|
|
|
46
46
|
import { constraintId } from "../core/ids.js";
|
|
47
47
|
import { readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js";
|
|
48
48
|
import { mergeHunchJson } from "../store/merge.js";
|
|
49
|
+
import { movePublicMemoryToPrivate } from "../store/privateMigrate.js";
|
|
50
|
+
import { ENTITY_KINDS } from "../core/types.js";
|
|
49
51
|
import { planCompaction } from "../store/compact.js";
|
|
50
52
|
import { resolveInvocation } from "./invocation.js";
|
|
51
53
|
const program = new Command();
|
|
@@ -288,6 +290,7 @@ program
|
|
|
288
290
|
.option("--no-hook", "don't switch the post-commit hook to private sync")
|
|
289
291
|
.option("--auto-commit", "opt-in: also git add+commit+push the private repo after each capture (post-commit hook AND MCP private writes)")
|
|
290
292
|
.option("--sync", "flush the configured private store now (git add+commit+push) — catches records made via MCP between commits")
|
|
293
|
+
.option("--migrate", "ONE-TIME: move this repo's EXISTING public .hunch memory into the overlay, then make the public repo code-only — untrack + gitignore the memory tree and regenerate grounding so no memory is published here")
|
|
291
294
|
.action((dir, opts) => {
|
|
292
295
|
const root = findRoot();
|
|
293
296
|
if (opts.sync) {
|
|
@@ -334,9 +337,36 @@ program
|
|
|
334
337
|
const h = installPostCommitHook(root, inv.shell, { private: true, commit: opts.autoCommit });
|
|
335
338
|
hookNote = ` ✓ post-commit hook ${h.action} — captured decisions route here${opts.autoCommit ? " (auto-commit+push on)" : ""}\n`;
|
|
336
339
|
}
|
|
340
|
+
// 5) one-time migration: MOVE existing public memory INTO the overlay, then make
|
|
341
|
+
// THIS repo code-only. Records are absorbed (union by id) BEFORE the public
|
|
342
|
+
// store is emptied, so an interrupted run never loses memory.
|
|
343
|
+
let migrateNote = "";
|
|
344
|
+
if (opts.migrate) {
|
|
345
|
+
const pub = new JsonStore(paths);
|
|
346
|
+
const priv = new JsonStore(hunchPathsForDir(hunchDir));
|
|
347
|
+
const res = movePublicMemoryToPrivate(pub, priv);
|
|
348
|
+
for (const kind of ENTITY_KINDS)
|
|
349
|
+
pub.dropAll(kind); // public store now empty on disk
|
|
350
|
+
if (isGitRepo(root))
|
|
351
|
+
gitUntrackCached(root, HUNCH_MEMORY_DIRS); // stop publishing it
|
|
352
|
+
ignoreHunchMemory(root);
|
|
353
|
+
const gstore = new HunchStore(paths); // public store is empty → grounding shows no memory
|
|
354
|
+
const grounding = regenerateGrounding(root, gstore);
|
|
355
|
+
gstore.close();
|
|
356
|
+
commitAndPushHunch(hunchDir, "hunch: absorb public memory into private overlay"); // durable
|
|
357
|
+
const breakdown = Object.entries(res.moved).map(([k, n]) => `${n} ${k}`).join(", ") || "0 records";
|
|
358
|
+
migrateNote =
|
|
359
|
+
` ✓ migrated public memory → overlay (${breakdown}); public store emptied\n` +
|
|
360
|
+
` ✓ untracked + gitignored the .hunch memory tree — this repo is now CODE-ONLY\n` +
|
|
361
|
+
` ✓ regenerated ${grounding.length} grounding file(s) (CLAUDE.md, AGENTS.md, …) — no public memory shown\n` +
|
|
362
|
+
` ✓ committed + pushed the private overlay (best-effort)\n` +
|
|
363
|
+
` next: review, then commit the PUBLIC repo:\n` +
|
|
364
|
+
` git add -A && git commit -m "chore: move engineering memory to a private overlay" && git push\n`;
|
|
365
|
+
}
|
|
337
366
|
console.log(`✓ private overlay enabled → ${hunchDir}\n` +
|
|
338
367
|
` ✓ recorded in .hunch/local.json (gitignored) — auto-detected, no env var or shell-profile edit\n` +
|
|
339
368
|
hookNote +
|
|
369
|
+
migrateNote +
|
|
340
370
|
` record sensitive items with private:true (hunch_record_decision / hunch_record_correction)\n` +
|
|
341
371
|
` override per-shell with HUNCH_PRIVATE_DIR; CI / public PR comments stay public-only.`);
|
|
342
372
|
});
|
package/dist/core/types.js
CHANGED
|
@@ -79,7 +79,7 @@ export const RetiredSignalSchema = z.object({
|
|
|
79
79
|
* existed in code, so its prose is turned into a testable set/regex. Carries its
|
|
80
80
|
* OWN provenance, separate from the decision's: an LLM may DRAFT a tripwire
|
|
81
81
|
* (advisory only); only a `human_confirmed` tripwire may BLOCK a commit — for every
|
|
82
|
-
* tier. One predictable rule (dec_a466655539).
|
|
82
|
+
* tier. One predictable rule (dec_a466655539). */
|
|
83
83
|
export const RejectedTripwireSchema = z.object({
|
|
84
84
|
alternative: z.string().describe("the rejected approach's human text — printed verbatim in the receipt"),
|
|
85
85
|
scope: z.array(z.string()).default([]).describe("glob(s) it applies to, e.g. vscode-extension/**"),
|
package/dist/extractors/git.js
CHANGED
|
@@ -39,6 +39,18 @@ export function commitAndPushHunch(hunchDir, message) {
|
|
|
39
39
|
export function headSha(cwd) {
|
|
40
40
|
return gitSafe(["rev-parse", "HEAD"], cwd);
|
|
41
41
|
}
|
|
42
|
+
/** Stop tracking `paths` in git (remove from the INDEX only — keep the working-tree
|
|
43
|
+
* files). Used by `hunch private --migrate` to un-publish the .hunch memory tree
|
|
44
|
+
* without deleting it locally. `--ignore-unmatch` makes an already-untracked path a
|
|
45
|
+
* no-op rather than an error; best-effort (a non-repo dir just no-ops). */
|
|
46
|
+
export function gitUntrackCached(cwd, paths) {
|
|
47
|
+
if (paths.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
execFileSync("git", ["-C", cwd, "rm", "-r", "--cached", "--quiet", "--ignore-unmatch", "--", ...paths], { stdio: "ignore" });
|
|
51
|
+
}
|
|
52
|
+
catch { /* best-effort: not a repo / nothing tracked */ }
|
|
53
|
+
}
|
|
42
54
|
/** Resolve any commit-ish (short sha / HEAD / branch) to a canonical full sha.
|
|
43
55
|
* Returns the input unchanged if it can't be resolved (e.g. not a git repo). */
|
|
44
56
|
export function revParse(ref, cwd) {
|
|
@@ -27,24 +27,54 @@ const ENTRIES = [
|
|
|
27
27
|
// fully separate private repo.
|
|
28
28
|
".hunch-private/",
|
|
29
29
|
];
|
|
30
|
-
|
|
30
|
+
// `hunch private --migrate` makes the repo CODE-ONLY: the engineering-memory tree
|
|
31
|
+
// (one curated subdir per kind) moves to a private overlay and must stop being
|
|
32
|
+
// published here. The derived index + pointer are already covered by the block
|
|
33
|
+
// above; this is a SEPARATE marked block so re-running `private` doesn't have to
|
|
34
|
+
// re-touch the block above, and so the two concerns read clearly in the file.
|
|
35
|
+
const MEM_MARK = "# >>> hunch private-only (engineering memory kept in a private overlay; not published here) >>>";
|
|
36
|
+
const MEM_END = "# <<< hunch private-only <<<";
|
|
37
|
+
const MEM_ENTRIES = [
|
|
38
|
+
".hunch/decisions/",
|
|
39
|
+
".hunch/bugs/",
|
|
40
|
+
".hunch/constraints/",
|
|
41
|
+
".hunch/components/",
|
|
42
|
+
".hunch/symbols/",
|
|
43
|
+
".hunch/edges/",
|
|
44
|
+
];
|
|
45
|
+
/** Idempotent + merge-safe append of one marked block (con_8460b6770f): never
|
|
46
|
+
* rewrites the user's existing entries, and re-running is a no-op once the block
|
|
47
|
+
* (or an equivalent hand-written set of the same patterns) is present. */
|
|
48
|
+
function appendBlock(root, mark, entries, end) {
|
|
31
49
|
const path = join(root, ".gitignore");
|
|
32
|
-
const block = [
|
|
50
|
+
const block = [mark, ...entries, end].join("\n");
|
|
33
51
|
if (!existsSync(path)) {
|
|
34
52
|
writeFileSync(path, block + "\n");
|
|
35
53
|
return { path, action: "created" };
|
|
36
54
|
}
|
|
37
55
|
const cur = readFileSync(path, "utf8");
|
|
38
|
-
if (cur.includes(
|
|
56
|
+
if (cur.includes(mark))
|
|
39
57
|
return { path, action: "unchanged" }; // already managed
|
|
40
58
|
// Already covered by the user's OWN entries (e.g. a hand-written, commented
|
|
41
59
|
// section listing the same patterns)? Don't append a redundant managed block —
|
|
42
60
|
// that would leave two copies of every ignore. Keep the .gitignore clean.
|
|
43
61
|
const lines = new Set(cur.split("\n").map((l) => l.trim()));
|
|
44
|
-
if (
|
|
62
|
+
if (entries.every((e) => lines.has(e)))
|
|
45
63
|
return { path, action: "unchanged" };
|
|
46
64
|
const sep = cur.endsWith("\n") || cur.length === 0 ? "" : "\n";
|
|
47
65
|
writeFileSync(path, `${cur}${sep}${block}\n`);
|
|
48
66
|
return { path, action: "appended" };
|
|
49
67
|
}
|
|
68
|
+
export function ensureGitignore(root) {
|
|
69
|
+
return appendBlock(root, MARK, ENTRIES, END);
|
|
70
|
+
}
|
|
71
|
+
/** Ignore the engineering-memory tree so a private-migrated repo stays code-only.
|
|
72
|
+
* The kind subdirs the user's records live in (decisions/, bugs/, …) move to the
|
|
73
|
+
* private overlay; this stops git from re-publishing them. The `.hunch/` dir, its
|
|
74
|
+
* manifest, and the local.json pointer are left tracked/managed elsewhere. */
|
|
75
|
+
export function ignoreHunchMemory(root) {
|
|
76
|
+
return appendBlock(root, MEM_MARK, MEM_ENTRIES, MEM_END);
|
|
77
|
+
}
|
|
78
|
+
/** The .hunch memory subdirs un-published by a private migration (git pathspecs). */
|
|
79
|
+
export const HUNCH_MEMORY_DIRS = MEM_ENTRIES.map((e) => e.replace(/\/$/, ""));
|
|
50
80
|
//# sourceMappingURL=gitignore.js.map
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
|
-
import { renderHunchSection, upsertSection } from "./claudemd.js";
|
|
22
|
+
import { renderHunchSection, upsertSection, updateClaudeMd } from "./claudemd.js";
|
|
23
23
|
/** Strip // line and block comments + trailing commas (JSONC → JSON). String-aware
|
|
24
24
|
* (double-quoted, with escapes) so a // inside a value isn't mangled. VS Code's
|
|
25
25
|
* .vscode/mcp.json is JSONC, so we must tolerate comments. */
|
|
@@ -226,6 +226,21 @@ export function writeWindsurfRule(root, store) {
|
|
|
226
226
|
writeFileSync(file, body);
|
|
227
227
|
return file;
|
|
228
228
|
}
|
|
229
|
+
/** Rewrite the auto-maintained Hunch section in EVERY assistant grounding doc
|
|
230
|
+
* (CLAUDE.md, AGENTS.md, Copilot instructions, Cursor + Windsurf rules) from the
|
|
231
|
+
* current store — without touching the MCP/provider config files. `hunch private
|
|
232
|
+
* --migrate` calls this AFTER emptying the public store so the committed public
|
|
233
|
+
* docs reflect that no engineering memory is published here (renderHunchSection
|
|
234
|
+
* reads the public store only, so private records never leak into them). */
|
|
235
|
+
export function regenerateGrounding(root, store) {
|
|
236
|
+
return [
|
|
237
|
+
updateClaudeMd(root, store),
|
|
238
|
+
writeAgentsMd(root, store),
|
|
239
|
+
writeCopilotInstructions(root, store),
|
|
240
|
+
writeCursorRule(root, store),
|
|
241
|
+
writeWindsurfRule(root, store),
|
|
242
|
+
];
|
|
243
|
+
}
|
|
229
244
|
/** Scaffold MCP config + grounding for all supported assistants. Returns a
|
|
230
245
|
* per-assistant summary for `hunch init` to print. Each assistant is isolated:
|
|
231
246
|
* a writer that refuses to clobber a malformed file degrades to a warning rather
|
package/dist/mcp/server.js
CHANGED
|
@@ -288,9 +288,13 @@ export function buildServer(root) {
|
|
|
288
288
|
else
|
|
289
289
|
store.json.put("decisions", rec);
|
|
290
290
|
// Invalidate, don't delete: closing the superseded decision's valid-time window
|
|
291
|
-
// (+ a supersedes edge) preserves the why-it-changed trail.
|
|
292
|
-
//
|
|
293
|
-
|
|
291
|
+
// (+ a supersedes edge) preserves the why-it-changed trail. Route the close to the
|
|
292
|
+
// same store the new record landed in — a private decision supersedes within the
|
|
293
|
+
// private overlay; a public one in the committed store. A private write never
|
|
294
|
+
// mutates the public store.
|
|
295
|
+
const superseded = decision.supersedes
|
|
296
|
+
? (decision.private ? store.supersedePrivate(decision.supersedes, rec) : store.supersede(decision.supersedes, rec))
|
|
297
|
+
: null;
|
|
294
298
|
store.reindex();
|
|
295
299
|
// Auto-flush the private repo when configured (hunch private --auto-commit), so a
|
|
296
300
|
// record made via MCP between public commits is committed+pushed immediately.
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -615,7 +615,22 @@ export class HunchStore {
|
|
|
615
615
|
* and write a `supersedes` edge. Returns the updated old decision, or null if it
|
|
616
616
|
* doesn't exist. All writes are atomic via json.put (con_902759b3dc). */
|
|
617
617
|
supersede(oldId, by) {
|
|
618
|
-
|
|
618
|
+
return this.supersedeIn(this.json, oldId, by);
|
|
619
|
+
}
|
|
620
|
+
/** Private-overlay counterpart of `supersede`: close + link the old decision inside
|
|
621
|
+
* the HUNCH_PRIVATE_DIR store, so a PRIVATE decision can supersede another private
|
|
622
|
+
* one (the MCP record path is private→private). A private write never mutates the
|
|
623
|
+
* committed public store. Returns null if no private store is configured or the old
|
|
624
|
+
* record isn't in it. */
|
|
625
|
+
supersedePrivate(oldId, by) {
|
|
626
|
+
if (!this.privateJson)
|
|
627
|
+
return null;
|
|
628
|
+
this.privateJson.ensureDirs();
|
|
629
|
+
return this.supersedeIn(this.privateJson, oldId, by);
|
|
630
|
+
}
|
|
631
|
+
/** Shared body for supersede / supersedePrivate against a specific store. */
|
|
632
|
+
supersedeIn(json, oldId, by) {
|
|
633
|
+
const old = json.get("decisions", oldId);
|
|
619
634
|
if (!old || old.id === by.id)
|
|
620
635
|
return null;
|
|
621
636
|
const closed = {
|
|
@@ -624,7 +639,7 @@ export class HunchStore {
|
|
|
624
639
|
superseded_by: by.id,
|
|
625
640
|
valid_to: old.valid_to ?? by.valid_from ?? null,
|
|
626
641
|
};
|
|
627
|
-
|
|
642
|
+
json.put("decisions", closed);
|
|
628
643
|
const edge = {
|
|
629
644
|
id: edgeId(by.id, oldId, "supersedes"),
|
|
630
645
|
from: by.id,
|
|
@@ -634,7 +649,7 @@ export class HunchStore {
|
|
|
634
649
|
strength: 1,
|
|
635
650
|
provenance: { source: "derived", confidence: 1, evidence: [by.id, oldId] },
|
|
636
651
|
};
|
|
637
|
-
|
|
652
|
+
json.put("edges", edge);
|
|
638
653
|
return closed;
|
|
639
654
|
}
|
|
640
655
|
/** Regression Guard: detect a change RE-INTRODUCING something an in-force
|
|
@@ -690,7 +705,7 @@ export class HunchStore {
|
|
|
690
705
|
* never did. Precision-first ladder (dep > symbol > pattern); the semantic tier is
|
|
691
706
|
* advisory and lives elsewhere. A hit `blocks` only when isVetoBlocker passes (a
|
|
692
707
|
* human-confirmed tripwire on an in-force, non-stale decision — dec_a466655539).
|
|
693
|
-
* Read-only; shared by buildCheckReport.
|
|
708
|
+
* Read-only; shared by buildCheckReport. */
|
|
694
709
|
vetoHits(an, files, staleDecisions = new Set()) {
|
|
695
710
|
const addedDeps = new Set(an.addedDeps);
|
|
696
711
|
const out = [];
|
package/dist/store/jsonStore.js
CHANGED
|
@@ -229,6 +229,24 @@ export class JsonStore {
|
|
|
229
229
|
this.invalidate(kind);
|
|
230
230
|
return true;
|
|
231
231
|
}
|
|
232
|
+
/** Remove EVERY record of a kind from disk (the kind dir's JSON files), keeping
|
|
233
|
+
* the dir itself so the layout/manifest survive. Used by `hunch private --migrate`
|
|
234
|
+
* to empty the PUBLIC store after its records have been moved into the private
|
|
235
|
+
* overlay. Returns the number of files removed. Invalidates the memoized load. */
|
|
236
|
+
dropAll(kind) {
|
|
237
|
+
const dir = this.paths.dir(kind);
|
|
238
|
+
if (!existsSync(dir))
|
|
239
|
+
return 0;
|
|
240
|
+
let n = 0;
|
|
241
|
+
for (const name of readdirSync(dir)) {
|
|
242
|
+
if (name.endsWith(".json")) {
|
|
243
|
+
rmSync(join(dir, name));
|
|
244
|
+
n++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.invalidate(kind);
|
|
248
|
+
return n;
|
|
249
|
+
}
|
|
232
250
|
/** Persist a schema migration: rewrite every LOADABLE record in its current shape.
|
|
233
251
|
* A record that still fails validation after migration is kept untouched (never
|
|
234
252
|
* deleted) and counted as `skipped`, so migration can't lose data. The caller
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move a repo's PUBLIC engineering memory into its PRIVATE overlay, for
|
|
3
|
+
* `hunch private --migrate` (turn a public-by-default repo into a code-only public
|
|
4
|
+
* repo whose memory lives in a separate private store).
|
|
5
|
+
*
|
|
6
|
+
* The merge is a UNION BY ID: every public record is absorbed into the private
|
|
7
|
+
* store, and a record that already exists on both sides (e.g. a decision present
|
|
8
|
+
* in both) is resolved by the same precedence the merge driver uses (human-confirmed
|
|
9
|
+
* › higher confidence › more recent › deterministic tiebreak). Private-only records
|
|
10
|
+
* are preserved untouched. This NEVER deletes from the public store — the CLI empties
|
|
11
|
+
* it separately (JsonStore.dropAll) only after this returns, so an interrupted move
|
|
12
|
+
* can't lose records.
|
|
13
|
+
*/
|
|
14
|
+
import { ENTITY_KINDS } from "../core/types.js";
|
|
15
|
+
import { mergeRecordsById } from "./merge.js";
|
|
16
|
+
/** Union every public record into `priv` (by id). Returns what was absorbed.
|
|
17
|
+
* Idempotent: re-running with the same input rewrites identical content. */
|
|
18
|
+
export function movePublicMemoryToPrivate(pub, priv) {
|
|
19
|
+
priv.ensureDirs();
|
|
20
|
+
const moved = {};
|
|
21
|
+
let total = 0;
|
|
22
|
+
for (const kind of ENTITY_KINDS) {
|
|
23
|
+
const pubRecs = pub.loadAll(kind);
|
|
24
|
+
if (pubRecs.length === 0)
|
|
25
|
+
continue;
|
|
26
|
+
const privRecs = priv.loadAll(kind);
|
|
27
|
+
// base=[] so both sides' records are treated as additions; collisions resolved
|
|
28
|
+
// by pickWinner. Public is "theirs", private "ours" — order is irrelevant since
|
|
29
|
+
// pickWinner is symmetric for equal-provenance records.
|
|
30
|
+
const merged = mergeRecordsById([], privRecs, pubRecs);
|
|
31
|
+
// replaceAll re-validates each record against its schema (records loaded here
|
|
32
|
+
// are already current-schema), and writes atomically per file (con_902759b3dc).
|
|
33
|
+
priv.replaceAll(kind, merged);
|
|
34
|
+
moved[kind] = pubRecs.length;
|
|
35
|
+
total += pubRecs.length;
|
|
36
|
+
}
|
|
37
|
+
return { moved, total };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=privateMigrate.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Hunch — an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",
|