@ijfw/memory-server 1.4.4 → 1.5.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/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-guards.js — v1.5.0-major S08: incident-driven worktree safety guards.
|
|
3
|
+
*
|
|
4
|
+
* Lifted from GSD executor (battle-tested across many incidents):
|
|
5
|
+
* #3097 — cwd-drift assertion
|
|
6
|
+
* #3099 — absolute-path containment check
|
|
7
|
+
* #2924 — protected-ref deny-list (refuse commit to main/master/develop/trunk/release/*)
|
|
8
|
+
*
|
|
9
|
+
* We hit ALL THREE during v1.5.0-major dispatch (multiple subagents drifted
|
|
10
|
+
* cwd into other worktrees; W11-A0 accidentally committed to main because
|
|
11
|
+
* branch creation failed). Three small functions, large blast-radius reduction.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { realpathSync } from 'node:fs';
|
|
16
|
+
import { resolve, isAbsolute, relative } from 'node:path';
|
|
17
|
+
|
|
18
|
+
// r15-H2: resolve symlinks before containment check. Without this, a path like
|
|
19
|
+
// <toplevel>/escape-link/foo (where escape-link → /etc) trivially escapes the
|
|
20
|
+
// toplevel even though `relative(toplevel, path)` reports a non-`..` result for
|
|
21
|
+
// the lexical path. realpathSync resolves the link first. If the path does not
|
|
22
|
+
// exist yet (e.g. a not-yet-created file we're about to write), realpathSync
|
|
23
|
+
// throws ENOENT — we fall back to the input path since a not-yet-existing
|
|
24
|
+
// target is still a valid containment surface to check lexically.
|
|
25
|
+
function safeRealpath(p) {
|
|
26
|
+
try { return realpathSync(p); } catch { return p; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PROTECTED_REF_PATTERNS = [
|
|
30
|
+
/^main$/, /^master$/, /^develop$/, /^trunk$/,
|
|
31
|
+
/^release\//, /^prod$/, /^production$/,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* #3097 — Assert that current working directory matches the captured spawn-time
|
|
36
|
+
* git toplevel. Returns the captured toplevel for later use. Throws if drift detected.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} capturedToplevel - what was `git rev-parse --show-toplevel` at spawn
|
|
39
|
+
* @param {string} cwd - current working directory (default: process.cwd())
|
|
40
|
+
* @returns {string} the captured toplevel (passthrough on success)
|
|
41
|
+
* @throws Error if cwd drifted off the captured toplevel
|
|
42
|
+
*/
|
|
43
|
+
export function assertNoCwdDrift(capturedToplevel, cwd = process.cwd()) {
|
|
44
|
+
if (!capturedToplevel) throw new Error('worktree-guards: capturedToplevel required (call captureSpawnToplevel() at spawn)');
|
|
45
|
+
let currentTop;
|
|
46
|
+
try {
|
|
47
|
+
currentTop = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' }).trim();
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`worktree-guards: cwd is not a git tree: ${cwd}`);
|
|
50
|
+
}
|
|
51
|
+
if (currentTop !== capturedToplevel) {
|
|
52
|
+
throw new Error(`worktree-guards: cwd drift detected! spawn=${capturedToplevel}, now=${currentTop}`);
|
|
53
|
+
}
|
|
54
|
+
return capturedToplevel;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Capture the spawn-time toplevel for later drift assertion.
|
|
59
|
+
*/
|
|
60
|
+
export function captureSpawnToplevel(cwd = process.cwd()) {
|
|
61
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' }).trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* #3099 — Assert that an absolute file path is contained within the toplevel.
|
|
66
|
+
* Refuses operations that would read/write outside the expected worktree.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} absolutePath - path to check
|
|
69
|
+
* @param {string} toplevel - git toplevel (from captureSpawnToplevel)
|
|
70
|
+
* @returns {string} the path (passthrough on success)
|
|
71
|
+
* @throws if path escapes toplevel
|
|
72
|
+
*/
|
|
73
|
+
export function assertPathWithinToplevel(absolutePath, toplevel) {
|
|
74
|
+
if (!isAbsolute(absolutePath)) {
|
|
75
|
+
throw new Error(`worktree-guards: path must be absolute (got: ${absolutePath})`);
|
|
76
|
+
}
|
|
77
|
+
// r15-H2: resolve symlinks on BOTH path AND toplevel before comparing.
|
|
78
|
+
// A path that lexically starts with toplevel can still escape via a symlink
|
|
79
|
+
// (e.g. <toplevel>/escape-link/foo where escape-link → /etc). Resolving
|
|
80
|
+
// both ends through realpath catches the real-fs destination.
|
|
81
|
+
const resolvedPath = safeRealpath(absolutePath);
|
|
82
|
+
const resolvedTop = safeRealpath(toplevel);
|
|
83
|
+
const rel = relative(resolvedTop, resolvedPath);
|
|
84
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
85
|
+
throw new Error(`worktree-guards: path escapes toplevel! path=${absolutePath} (resolved: ${resolvedPath}), toplevel=${toplevel} (resolved: ${resolvedTop})`);
|
|
86
|
+
}
|
|
87
|
+
return absolutePath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* #2924 — Assert the current HEAD is NOT pointing at a protected ref before commit.
|
|
92
|
+
* Subagents must commit to a wave branch (wave/W*-*), never directly to main/master/etc.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} cwd
|
|
95
|
+
* @returns {string} the current branch name (passthrough on success)
|
|
96
|
+
* @throws if HEAD is on a protected ref
|
|
97
|
+
*/
|
|
98
|
+
export function assertNotProtectedRef(cwd = process.cwd()) {
|
|
99
|
+
const branch = execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim();
|
|
100
|
+
if (!branch) {
|
|
101
|
+
throw new Error('worktree-guards: detached HEAD — cannot commit safely');
|
|
102
|
+
}
|
|
103
|
+
for (const pattern of PROTECTED_REF_PATTERNS) {
|
|
104
|
+
if (pattern.test(branch)) {
|
|
105
|
+
throw new Error(`worktree-guards: refuse to commit to protected ref: ${branch}. Switch to a wave/feature branch first.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return branch;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convenience: run all 3 guards in sequence before a commit. Returns the verified branch.
|
|
113
|
+
*/
|
|
114
|
+
export function preCommitGuards(capturedToplevel, paths = []) {
|
|
115
|
+
assertNoCwdDrift(capturedToplevel);
|
|
116
|
+
for (const p of paths) assertPathWithinToplevel(resolve(p), capturedToplevel);
|
|
117
|
+
return assertNotProtectedRef();
|
|
118
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-recovery.js — v1.5.0-major S10: transactional cleanup tail for worktrees.
|
|
3
|
+
*
|
|
4
|
+
* Pattern lifted from GSD code-fixer (crash-safe recovery on next run):
|
|
5
|
+
* 1. BEFORE opening the crash window (git worktree remove), write a sentinel
|
|
6
|
+
* file at .planning/<phase>/.worktree-recovery-pending.json with:
|
|
7
|
+
* { worktreePath, branch, phase, ts, op: 'remove' }
|
|
8
|
+
* 2. Run the destructive op.
|
|
9
|
+
* 3. On success, DELETE the sentinel.
|
|
10
|
+
*
|
|
11
|
+
* Next run scans for orphan sentinels and offers/runs cleanup. Survives:
|
|
12
|
+
* - Process SIGKILL between steps 1+2
|
|
13
|
+
* - Power loss / OOM between steps 2+3
|
|
14
|
+
* - Network filesystem partition during step 2
|
|
15
|
+
*
|
|
16
|
+
* Use this for ANY destructive op where a partial state on disk is worse
|
|
17
|
+
* than a fully-committed or fully-rolled-back state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { writeFile, readFile, unlink, readdir, mkdir } from 'node:fs/promises';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { randomBytes } from 'node:crypto';
|
|
25
|
+
|
|
26
|
+
const SENTINEL_PREFIX = '.worktree-recovery-pending.';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write a recovery sentinel, run the op, delete on success.
|
|
30
|
+
* @param {object} sentinelData - serialised into JSON; should include worktreePath + op
|
|
31
|
+
* @param {function} op - async function to execute; if it throws, sentinel remains
|
|
32
|
+
* @param {string} projectRoot - parent project root
|
|
33
|
+
* @returns the op's return value
|
|
34
|
+
*/
|
|
35
|
+
export async function withRecoverySentinel(sentinelData, op, projectRoot) {
|
|
36
|
+
const sentinelDir = join(projectRoot, '.planning', 'worktree-recovery');
|
|
37
|
+
await mkdir(sentinelDir, { recursive: true });
|
|
38
|
+
const sentinelId = randomBytes(6).toString('hex');
|
|
39
|
+
const sentinelPath = join(sentinelDir, `${SENTINEL_PREFIX}${sentinelId}.json`);
|
|
40
|
+
const payload = { ...sentinelData, sentinel_id: sentinelId, ts: new Date().toISOString() };
|
|
41
|
+
|
|
42
|
+
await writeFile(sentinelPath, JSON.stringify(payload, null, 2), 'utf8');
|
|
43
|
+
try {
|
|
44
|
+
const result = await op();
|
|
45
|
+
try { await unlink(sentinelPath); } catch { /* missing → ok */ }
|
|
46
|
+
return result;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Leave sentinel in place for next run to recover.
|
|
49
|
+
err.sentinelPath = sentinelPath;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Scan for orphan sentinels (op didn't complete on previous run).
|
|
56
|
+
* @returns array of sentinel objects with full payload + path
|
|
57
|
+
*/
|
|
58
|
+
export async function listPendingRecoveries(projectRoot) {
|
|
59
|
+
const sentinelDir = join(projectRoot, '.planning', 'worktree-recovery');
|
|
60
|
+
try {
|
|
61
|
+
const entries = await readdir(sentinelDir);
|
|
62
|
+
const pending = [];
|
|
63
|
+
for (const name of entries) {
|
|
64
|
+
if (!name.startsWith(SENTINEL_PREFIX)) continue;
|
|
65
|
+
const fullPath = join(sentinelDir, name);
|
|
66
|
+
try {
|
|
67
|
+
const raw = await readFile(fullPath, 'utf8');
|
|
68
|
+
pending.push({ ...JSON.parse(raw), path: fullPath });
|
|
69
|
+
} catch { /* malformed, skip */ }
|
|
70
|
+
}
|
|
71
|
+
return pending;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.code === 'ENOENT') return [];
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Attempt to complete a pending recovery. Currently handles op:'remove' for worktrees.
|
|
80
|
+
* Returns { ok, action, details? }.
|
|
81
|
+
*/
|
|
82
|
+
export async function recoverPending(sentinel, projectRoot) {
|
|
83
|
+
if (sentinel.op === 'remove' && sentinel.worktreePath) {
|
|
84
|
+
try {
|
|
85
|
+
// Best-effort: if the worktree dir still exists, finish the remove.
|
|
86
|
+
if (existsSync(sentinel.worktreePath)) {
|
|
87
|
+
execFileSync('git', ['worktree', 'remove', '--force', sentinel.worktreePath], {
|
|
88
|
+
cwd: projectRoot, encoding: 'utf8',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Always prune to clean up dangling .git/worktrees/ refs.
|
|
92
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: projectRoot, encoding: 'utf8' });
|
|
93
|
+
await unlink(sentinel.path);
|
|
94
|
+
return { ok: true, action: 'completed-remove', worktreePath: sentinel.worktreePath };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return { ok: false, action: 'remove-failed', error: err.message };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { ok: false, action: 'unknown-op', op: sentinel.op };
|
|
100
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- A-Mem-style auto-linking on memory store.
|
|
2
|
+
//
|
|
3
|
+
// On every memory store call (post-dedup, pre-fact-extraction), this module:
|
|
4
|
+
// 1. Selects top-k neighbors via body lexical match (production
|
|
5
|
+
// memory_entries has columns id/body/source/session_id/created_at;
|
|
6
|
+
// no title, so neighbor scoring is body-only).
|
|
7
|
+
// 2. Asks the LLM (one call, env-gated) to return a JSON payload:
|
|
8
|
+
// classification: "ADD" | "UPDATE" | "NOOP"
|
|
9
|
+
// links: [{ target }]
|
|
10
|
+
// neighbor_edits: [{ id, add_tags: [...] }]
|
|
11
|
+
// 3. Applies the proposal to memory_links / memory_tags atomically.
|
|
12
|
+
//
|
|
13
|
+
// All steps are gated by llm-call.js env vars (IJFW_AUTOLINK_OFF=1,
|
|
14
|
+
// IJFW_AUTOLINK_BUDGET_USD=0, missing API key). When skipped, no writes.
|
|
15
|
+
//
|
|
16
|
+
// Test injections:
|
|
17
|
+
// { neighborsOnly: true } -> select neighbors, no LLM, no writes
|
|
18
|
+
// { dryProposal: <obj> } -> skip LLM, apply the supplied proposal
|
|
19
|
+
//
|
|
20
|
+
// Reference: arxiv 2502.12110 (A-Mem, NeurIPS 2025). This is the
|
|
21
|
+
// academically-validated "smarter with use" keystone.
|
|
22
|
+
|
|
23
|
+
import { llmCall, parseLlmJsonResponse } from '../lib/llm-call.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TOPK = 5;
|
|
26
|
+
const SYSTEM_PROMPT = [
|
|
27
|
+
'You are the IJFW memory auto-linker. Given a NEW MEMORY and TOP-K NEIGHBORS,',
|
|
28
|
+
'return STRICT JSON only (no prose). Schema:',
|
|
29
|
+
'{',
|
|
30
|
+
' "classification": "ADD" | "UPDATE" | "NOOP",',
|
|
31
|
+
' "links": [{ "target": "<lowercase-dash-collapsed-slug>" }],',
|
|
32
|
+
' "neighbor_edits": [{ "id": "<neighbor-id>", "add_tags": ["..."] }]',
|
|
33
|
+
'}',
|
|
34
|
+
'classification = UPDATE if the new memory directly supersedes a neighbor.',
|
|
35
|
+
'classification = NOOP if the new memory is fully duplicate.',
|
|
36
|
+
'links = neighbors the new memory should reference (max 3).',
|
|
37
|
+
'neighbor_edits = small tag additions to neighbors (max 2 per neighbor; max 3 total).',
|
|
38
|
+
'NEVER invent neighbors not in TOP-K. NEVER rewrite neighbor body.',
|
|
39
|
+
].join('\n');
|
|
40
|
+
|
|
41
|
+
function selectNeighbors(db, entry, k = DEFAULT_TOPK) {
|
|
42
|
+
// Body-only lexical proximity via tokenized LIKE -- light, deterministic,
|
|
43
|
+
// no FTS dependency. Production memory_entries has no title column.
|
|
44
|
+
//
|
|
45
|
+
// Tokenization: split on non-word chars, filter to tokens >=4 chars
|
|
46
|
+
// (skips stop-words and noise), keep first 3. Build OR-LIKE so we find
|
|
47
|
+
// any neighbor that shares at least one significant token. Ranking is
|
|
48
|
+
// implicit via the LIMIT; if multi-token scoring matters in the future,
|
|
49
|
+
// wrap in a CASE-WHEN sum.
|
|
50
|
+
const tokens = (entry.body || entry.title || '')
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.split(/[^a-z0-9]+/i)
|
|
53
|
+
.filter((t) => t.length >= 4)
|
|
54
|
+
.slice(0, 3);
|
|
55
|
+
if (tokens.length === 0) return [];
|
|
56
|
+
|
|
57
|
+
const orClauses = tokens.map(() => 'body LIKE ?').join(' OR ');
|
|
58
|
+
const params = tokens.map((t) => `%${t}%`);
|
|
59
|
+
let sql = `SELECT CAST(id AS TEXT) AS id, body, source
|
|
60
|
+
FROM memory_entries
|
|
61
|
+
WHERE (${orClauses})`;
|
|
62
|
+
if (entry.id != null) {
|
|
63
|
+
sql += ' AND CAST(id AS TEXT) != ?';
|
|
64
|
+
params.push(String(entry.id));
|
|
65
|
+
}
|
|
66
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
67
|
+
params.push(k);
|
|
68
|
+
return db.prepare(sql).all(...params);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function applyProposal(db, entry, proposal) {
|
|
72
|
+
const insLink = db.prepare(
|
|
73
|
+
'INSERT OR IGNORE INTO memory_links (from_id, to_target, line) VALUES (?, ?, 0)',
|
|
74
|
+
);
|
|
75
|
+
const insTag = db.prepare(
|
|
76
|
+
'INSERT OR IGNORE INTO memory_tags (memory_id, tag_path, depth) VALUES (?, ?, ?)',
|
|
77
|
+
);
|
|
78
|
+
let linksAdded = 0;
|
|
79
|
+
let neighborTagsAdded = 0;
|
|
80
|
+
const tx = db.transaction(() => {
|
|
81
|
+
for (const l of (proposal.links || []).slice(0, 3)) {
|
|
82
|
+
if (l && typeof l.target === 'string' && l.target) {
|
|
83
|
+
const res = insLink.run(String(entry.id), l.target);
|
|
84
|
+
if (res.changes) linksAdded++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (const ne of (proposal.neighbor_edits || []).slice(0, 5)) {
|
|
89
|
+
if (!ne || typeof ne.id !== 'string') continue;
|
|
90
|
+
for (const t of (ne.add_tags || []).slice(0, 2)) {
|
|
91
|
+
if (total >= 3) break;
|
|
92
|
+
if (typeof t === 'string' && t) {
|
|
93
|
+
const path = t.toLowerCase().replace(/^\/+|\/+$/g, '');
|
|
94
|
+
if (!path) continue;
|
|
95
|
+
const res = insTag.run(ne.id, path, path.split('/').length);
|
|
96
|
+
if (res.changes) {
|
|
97
|
+
neighborTagsAdded++;
|
|
98
|
+
total++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
tx();
|
|
105
|
+
return { links_added: linksAdded, neighbor_tags_added: neighborTagsAdded };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function autoLink(db, entry, opts = {}) {
|
|
109
|
+
// Env-gate check FIRST — before any DB work — so the off-path is a
|
|
110
|
+
// true no-op (no SELECT against memory_entries, no race against
|
|
111
|
+
// fire-and-forget db.close calls in test harnesses).
|
|
112
|
+
if (!opts.dryProposal && !opts.neighborsOnly) {
|
|
113
|
+
if (process.env.IJFW_AUTOLINK_OFF === '1') {
|
|
114
|
+
return { skipped: true, reason: 'autolink_off' };
|
|
115
|
+
}
|
|
116
|
+
const budget = process.env.IJFW_AUTOLINK_BUDGET_USD;
|
|
117
|
+
if (budget !== undefined && Number(budget) <= 0) {
|
|
118
|
+
return { skipped: true, reason: 'budget_exhausted' };
|
|
119
|
+
}
|
|
120
|
+
const hasKey = !!(process.env.IJFW_AUTOLINK_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
121
|
+
if (!hasKey) {
|
|
122
|
+
return { skipped: true, reason: 'no_key' };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const neighbors = selectNeighbors(db, entry, opts.k || DEFAULT_TOPK);
|
|
126
|
+
if (opts.neighborsOnly) return { skipped: true, neighbors };
|
|
127
|
+
|
|
128
|
+
let proposal = opts.dryProposal;
|
|
129
|
+
if (!proposal) {
|
|
130
|
+
const userPayload = JSON.stringify({
|
|
131
|
+
new_memory: { id: String(entry.id), body: entry.body },
|
|
132
|
+
top_k_neighbors: neighbors.map((n) => ({ id: n.id, body: (n.body || '').slice(0, 200) })),
|
|
133
|
+
});
|
|
134
|
+
const llm = await llmCall({
|
|
135
|
+
system: SYSTEM_PROMPT,
|
|
136
|
+
user: userPayload,
|
|
137
|
+
maxTokens: 512,
|
|
138
|
+
});
|
|
139
|
+
if (llm.skipped) {
|
|
140
|
+
return { skipped: true, reason: llm.reason, neighbors };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
proposal = parseLlmJsonResponse(llm.text);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { skipped: true, reason: 'parse_failed', neighbors, error: e.message };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const applied = applyProposal(db, entry, proposal);
|
|
149
|
+
return { skipped: false, neighbors, proposal, applied };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// v1.5.1 R5-1.2 -- one-time M2 (A-Mem auto-link) backfill for memory written
|
|
153
|
+
// during v1.5.0, when autoLink was NOT wired into the production write path.
|
|
154
|
+
//
|
|
155
|
+
// UNLIKE the M1 backfill (free, always-on), M2 backfill makes one LLM call
|
|
156
|
+
// per row -- backfilling over a large memory can cost real money. So M2
|
|
157
|
+
// backfill is OPT-IN and budget-gated:
|
|
158
|
+
//
|
|
159
|
+
// - IJFW_AUTOLINK_OFF=1 -> backfill is a no-op (kill switch)
|
|
160
|
+
// - IJFW_AUTOLINK_BACKFILL!=1 -> backfill is a no-op by default
|
|
161
|
+
// (M1-always, M2-opt-in is the safe
|
|
162
|
+
// default per R5-1.2)
|
|
163
|
+
// - IJFW_AUTOLINK_BUDGET_USD unset -> backfill is a no-op. A budget MUST be
|
|
164
|
+
// OR <= 0 explicitly configured. The per-call
|
|
165
|
+
// llm-call.js path treats an unset
|
|
166
|
+
// budget as "uncapped"; for a bulk
|
|
167
|
+
// backfill that is unsafe -- a large
|
|
168
|
+
// memory could spend without bound. So
|
|
169
|
+
// the backfill REQUIRES a positive cap.
|
|
170
|
+
// - no API key -> backfill is a no-op (autoLink skips)
|
|
171
|
+
//
|
|
172
|
+
// The per-row autoLink call independently re-checks the SAME env gates (off /
|
|
173
|
+
// budget / key), so even mid-run the backfill respects a budget that drops to
|
|
174
|
+
// zero or a kill switch that flips. Returns aggregate counts.
|
|
175
|
+
export async function backfillAutoLink(db, opts = {}) {
|
|
176
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
177
|
+
throw new Error('backfillAutoLink: db handle is invalid.');
|
|
178
|
+
}
|
|
179
|
+
const force = opts.force === true;
|
|
180
|
+
// Opt-in gate: M2 backfill only runs when explicitly enabled. M1 backfill
|
|
181
|
+
// (obsidian-parser.js) is the always-on default; M2 costs money so it is
|
|
182
|
+
// off unless the operator opts in via IJFW_AUTOLINK_BACKFILL=1.
|
|
183
|
+
if (!force && process.env.IJFW_AUTOLINK_BACKFILL !== '1') {
|
|
184
|
+
return { skipped: true, reason: 'backfill_not_enabled', rows: 0 };
|
|
185
|
+
}
|
|
186
|
+
if (process.env.IJFW_AUTOLINK_OFF === '1') {
|
|
187
|
+
return { skipped: true, reason: 'autolink_off', rows: 0 };
|
|
188
|
+
}
|
|
189
|
+
// Budget cap is MANDATORY for the backfill. An unset budget means
|
|
190
|
+
// llm-call.js runs uncapped -- fine for one-off write-time autoLink, but a
|
|
191
|
+
// bulk backfill over thousands of rows would spend without bound. Refuse
|
|
192
|
+
// unless the operator has set a positive IJFW_AUTOLINK_BUDGET_USD.
|
|
193
|
+
const budget = process.env.IJFW_AUTOLINK_BUDGET_USD;
|
|
194
|
+
if (budget === undefined || !(Number(budget) > 0)) {
|
|
195
|
+
return {
|
|
196
|
+
skipped: true,
|
|
197
|
+
reason: budget === undefined ? 'budget_not_set' : 'budget_exhausted',
|
|
198
|
+
rows: 0,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const hasKey = !!(process.env.IJFW_AUTOLINK_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
202
|
+
if (!hasKey) {
|
|
203
|
+
return { skipped: true, reason: 'no_key', rows: 0 };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const batchSize = Math.max(1, opts.batchSize || 200);
|
|
207
|
+
const result = {
|
|
208
|
+
skipped: false, rows: 0, linked: 0, links_added: 0,
|
|
209
|
+
neighbor_tags_added: 0, stopped_early: false,
|
|
210
|
+
};
|
|
211
|
+
let lastId = 0;
|
|
212
|
+
// eslint-disable-next-line no-constant-condition
|
|
213
|
+
while (true) {
|
|
214
|
+
let batch;
|
|
215
|
+
try {
|
|
216
|
+
batch = db
|
|
217
|
+
.prepare(
|
|
218
|
+
'SELECT id, body FROM memory_entries WHERE id > ? ORDER BY id ASC LIMIT ?',
|
|
219
|
+
)
|
|
220
|
+
.all(lastId, batchSize);
|
|
221
|
+
} catch {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (!batch || batch.length === 0) break;
|
|
225
|
+
for (const row of batch) {
|
|
226
|
+
lastId = row.id;
|
|
227
|
+
if (typeof row.body !== 'string' || row.body.length === 0) continue;
|
|
228
|
+
// Per-row re-check: a budget that drops to zero or a kill switch that
|
|
229
|
+
// flips mid-run stops the backfill before the next paid call. autoLink
|
|
230
|
+
// itself ALSO re-checks, but stopping here avoids the wasted SELECT.
|
|
231
|
+
if (process.env.IJFW_AUTOLINK_OFF === '1') {
|
|
232
|
+
result.stopped_early = true;
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
const b = process.env.IJFW_AUTOLINK_BUDGET_USD;
|
|
236
|
+
if (b === undefined || !(Number(b) > 0)) {
|
|
237
|
+
result.stopped_early = true;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
result.rows += 1;
|
|
241
|
+
let res;
|
|
242
|
+
try {
|
|
243
|
+
res = await autoLink(db, { id: row.id, body: row.body });
|
|
244
|
+
} catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (res && res.skipped) {
|
|
248
|
+
// autoLink skipped (budget exhausted / off / no key / parse fail).
|
|
249
|
+
// budget_exhausted + autolink_off mean stop the whole run.
|
|
250
|
+
if (res.reason === 'budget_exhausted' || res.reason === 'autolink_off') {
|
|
251
|
+
result.stopped_early = true;
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
result.linked += 1;
|
|
257
|
+
if (res && res.applied) {
|
|
258
|
+
result.links_added += res.applied.links_added || 0;
|
|
259
|
+
result.neighbor_tags_added += res.applied.neighbor_tags_added || 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (batch.length < batchSize) break;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export default { autoLink, backfillAutoLink };
|