@ijfw/memory-server 1.4.3 → 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/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 +1 -1
- 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 +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -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 +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- 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/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 +554 -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 +152 -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/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/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- 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.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 +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- 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 +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- 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 +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -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 +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -1,50 +1,236 @@
|
|
|
1
1
|
// --- cross-project-search: BM25 search across every registered IJFW project ---
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// server.js
|
|
3
|
+
// Canonical cross-project search surface. The legacy naive-keyword-count
|
|
4
|
+
// `searchAcrossProjects` in server.js was removed in v1.5.1 H1.5 (audit
|
|
5
|
+
// finding memory-engine.md F-FUN-4) — `scope:'all'` on the MCP search tool
|
|
6
|
+
// now routes here. Builds a corpus of (project, source, line) docs from each
|
|
5
7
|
// registered project's memory files, hands it to the BM25 ranker in
|
|
6
8
|
// search-bm25.js, and returns hits tagged with [project:<basename>].
|
|
7
9
|
//
|
|
8
10
|
// Pure + injectable. The caller supplies the registry reader and the
|
|
9
11
|
// per-project memory reader so this module can be unit-tested without
|
|
10
12
|
// touching the home directory.
|
|
13
|
+
//
|
|
14
|
+
// v1.5.0 audit-H3.4 (memory-engine.md F-SEC-1): registry entries are
|
|
15
|
+
// treated as UNTRUSTED. Each entry.path is:
|
|
16
|
+
// 1. realpath-resolved (symlinks resolved against the filesystem)
|
|
17
|
+
// 2. containment-checked against `allowedRoots` (default: $HOME)
|
|
18
|
+
// 3. silently skipped if it escapes the allowed roots OR fails realpath
|
|
19
|
+
// 4. logged to stderr ONCE per unique offending raw-path (Set dedup)
|
|
20
|
+
// Threat model: a compromised dep / malicious project that writes a
|
|
21
|
+
// registry entry pointing at /etc/passwd via a symlink in the user's
|
|
22
|
+
// home directory will be caught by step 1+2. Plain `isAbsolute()` was
|
|
23
|
+
// the only prior validation and was insufficient.
|
|
11
24
|
|
|
12
|
-
import { basename } from 'node:path';
|
|
25
|
+
import { basename, resolve, join } from 'node:path';
|
|
26
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
13
28
|
import { searchCorpus } from './search-bm25.js';
|
|
14
29
|
|
|
30
|
+
// v1.5.0 audit MED #10 (memory-engine.md F-SPD-2): per-project corpus
|
|
31
|
+
// cache keyed on (canonicalProjectPath, signature) where `signature` is
|
|
32
|
+
// the joined mtimes of the project's memory files. As long as nothing
|
|
33
|
+
// has changed under .ijfw/memory the cache hits and we skip the
|
|
34
|
+
// readProjectMemory() call entirely. Cache is module-local; tests can
|
|
35
|
+
// reset via _resetCorpusCache(). Cap at 64 entries (LRU-ish via Map
|
|
36
|
+
// insertion order) so a long-lived dashboard process can't OOM the cache.
|
|
37
|
+
const CORPUS_CACHE = new Map();
|
|
38
|
+
const CORPUS_CACHE_CAP = 64;
|
|
39
|
+
|
|
40
|
+
/** Reset the corpus mtime cache. Test-only. */
|
|
41
|
+
export function _resetCorpusCache() {
|
|
42
|
+
CORPUS_CACHE.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Files that participate in the cache signature. Match the readers used
|
|
46
|
+
// by readProjectMemory in server.js so an edit to any of them busts the
|
|
47
|
+
// cache. Missing files contribute "0" -- creation of a previously-missing
|
|
48
|
+
// file is a cache-bust on its own.
|
|
49
|
+
const CACHE_SIGNATURE_FILES = [
|
|
50
|
+
['.ijfw', 'memory', 'knowledge.md'],
|
|
51
|
+
['.ijfw', 'memory', 'project-journal.md'],
|
|
52
|
+
['.ijfw', 'memory', 'handoff.md'],
|
|
53
|
+
['.ijfw', 'memory', 'team-knowledge.md'],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function computeProjectSignature(canonicalProjectPath) {
|
|
57
|
+
let sig = '';
|
|
58
|
+
for (const parts of CACHE_SIGNATURE_FILES) {
|
|
59
|
+
const p = join(canonicalProjectPath, ...parts);
|
|
60
|
+
try {
|
|
61
|
+
const s = statSync(p);
|
|
62
|
+
sig += `${s.mtimeMs.toFixed(3)}:${s.size}|`;
|
|
63
|
+
} catch {
|
|
64
|
+
sig += '0|';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return sig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getCachedDocs(canonicalProjectPath) {
|
|
71
|
+
const sig = computeProjectSignature(canonicalProjectPath);
|
|
72
|
+
const cached = CORPUS_CACHE.get(canonicalProjectPath);
|
|
73
|
+
if (cached && cached.sig === sig) {
|
|
74
|
+
// LRU touch: re-insert at the end of the Map iteration order.
|
|
75
|
+
CORPUS_CACHE.delete(canonicalProjectPath);
|
|
76
|
+
CORPUS_CACHE.set(canonicalProjectPath, cached);
|
|
77
|
+
return cached.docs;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setCachedDocs(canonicalProjectPath, docs) {
|
|
83
|
+
const sig = computeProjectSignature(canonicalProjectPath);
|
|
84
|
+
CORPUS_CACHE.set(canonicalProjectPath, { sig, docs });
|
|
85
|
+
// Trim oldest entry if we're over the cap. Map iteration is insertion
|
|
86
|
+
// order so the first key is the oldest.
|
|
87
|
+
if (CORPUS_CACHE.size > CORPUS_CACHE_CAP) {
|
|
88
|
+
const oldestKey = CORPUS_CACHE.keys().next().value;
|
|
89
|
+
if (oldestKey !== undefined) CORPUS_CACHE.delete(oldestKey);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Module-level dedup set for "skipped offender" stderr noise control.
|
|
94
|
+
// Keyed by the raw entry.path (the attacker-controlled string) so the same
|
|
95
|
+
// malicious entry is reported only once per process lifetime. Exported for
|
|
96
|
+
// test reset via `_resetSkipLog()`.
|
|
97
|
+
const SKIP_LOG = new Set();
|
|
98
|
+
|
|
99
|
+
/** Reset the skip-log dedup set. Test-only. */
|
|
100
|
+
export function _resetSkipLog() {
|
|
101
|
+
SKIP_LOG.clear();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Default allowed roots: just the user's home directory.
|
|
105
|
+
* Tests can pass `opts.allowedRoots` to constrain or widen.
|
|
106
|
+
* Returned as resolved (realpath'd) absolute paths so containment
|
|
107
|
+
* comparison is apples-to-apples with the realpath'd entry. */
|
|
108
|
+
function defaultAllowedRoots() {
|
|
109
|
+
const roots = [];
|
|
110
|
+
try {
|
|
111
|
+
const home = homedir();
|
|
112
|
+
if (home && typeof home === 'string') {
|
|
113
|
+
// realpath the root itself so /var/root on macOS resolves to
|
|
114
|
+
// /private/var/root (same canonicalization as the entry side).
|
|
115
|
+
try { roots.push(realpathSync(home)); } catch { roots.push(resolve(home)); }
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
return roots;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True iff `child` is `root` or a path nested under `root`.
|
|
122
|
+
* Both inputs are expected to be already-canonical absolute paths.
|
|
123
|
+
* We compare with a trailing-separator guard so `/home/alice-evil`
|
|
124
|
+
* is NOT considered inside `/home/alice`. */
|
|
125
|
+
function isUnder(child, root) {
|
|
126
|
+
if (!child || !root) return false;
|
|
127
|
+
if (child === root) return true;
|
|
128
|
+
// Use both POSIX and Windows separators defensively. Path normalization
|
|
129
|
+
// has already happened via realpathSync; we just need the boundary check.
|
|
130
|
+
const withPosixSep = root.endsWith('/') ? root : root + '/';
|
|
131
|
+
const withWindowsSep = root.endsWith('\\') ? root : root + '\\';
|
|
132
|
+
return child.startsWith(withPosixSep) || child.startsWith(withWindowsSep);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Resolve + containment check.
|
|
136
|
+
* Returns the canonical path on success, or null if the entry must be
|
|
137
|
+
* skipped. On skip, logs to stderr once per unique raw path. */
|
|
138
|
+
function safeResolveProjectPath(rawPath, allowedRoots) {
|
|
139
|
+
if (!rawPath || typeof rawPath !== 'string') {
|
|
140
|
+
return _skip(rawPath, 'not-a-string');
|
|
141
|
+
}
|
|
142
|
+
let canonical;
|
|
143
|
+
try {
|
|
144
|
+
// realpathSync resolves symlinks. If the entry points to a symlink
|
|
145
|
+
// chain whose target escapes allowedRoots, this is where we catch it.
|
|
146
|
+
canonical = realpathSync(rawPath);
|
|
147
|
+
} catch {
|
|
148
|
+
// ENOENT / EACCES / ELOOP — entry path doesn't exist or is unreadable.
|
|
149
|
+
// Graceful skip; don't throw (a stale registry shouldn't crash search).
|
|
150
|
+
return _skip(rawPath, 'realpath-failed');
|
|
151
|
+
}
|
|
152
|
+
if (!allowedRoots.some(root => isUnder(canonical, root))) {
|
|
153
|
+
return _skip(rawPath, `escapes-allowed-roots (realpath=${canonical})`);
|
|
154
|
+
}
|
|
155
|
+
return canonical;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _skip(rawPath, reason) {
|
|
159
|
+
const key = String(rawPath || '<null>');
|
|
160
|
+
if (!SKIP_LOG.has(key)) {
|
|
161
|
+
SKIP_LOG.add(key);
|
|
162
|
+
try {
|
|
163
|
+
// Single-line, stderr-only — same posture as the rest of the engine's
|
|
164
|
+
// advisory warnings. Production callers (Claude Code) capture stderr
|
|
165
|
+
// and surface in the dashboard's diagnostics tab.
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`[ijfw cross-project-search] skipped registry entry: ${reason}: ${key}\n`
|
|
168
|
+
);
|
|
169
|
+
} catch { /* never let a logging failure break search */ }
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
15
174
|
// Build a corpus of line-level docs from the provided projects.
|
|
16
175
|
// projects: [{ path, hash?, iso? }]
|
|
17
176
|
// readProjectMemory(path) -> { knowledge, journal, handoff } (strings)
|
|
177
|
+
// opts.allowedRoots: optional array of canonical absolute roots; entries
|
|
178
|
+
// whose realpath escapes ALL of them are skipped.
|
|
179
|
+
// Default: [realpath(homedir())].
|
|
18
180
|
// Returns [{ id, text, meta }] where meta carries project + source + lineNo.
|
|
19
|
-
export function buildCorpus(projects, readProjectMemory) {
|
|
181
|
+
export function buildCorpus(projects, readProjectMemory, opts = {}) {
|
|
182
|
+
const allowedRoots = Array.isArray(opts.allowedRoots) && opts.allowedRoots.length
|
|
183
|
+
? opts.allowedRoots
|
|
184
|
+
: defaultAllowedRoots();
|
|
20
185
|
const docs = [];
|
|
21
186
|
for (const entry of projects) {
|
|
22
|
-
|
|
23
|
-
const
|
|
187
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
188
|
+
const canonical = safeResolveProjectPath(entry.path, allowedRoots);
|
|
189
|
+
if (canonical === null) continue; // skipped (symlink-escape or missing)
|
|
190
|
+
|
|
191
|
+
// v1.5.0 audit MED #10: try the mtime cache before re-reading.
|
|
192
|
+
// The cache is opt-out via opts.useCache=false (tests / consistency runs).
|
|
193
|
+
const useCache = opts.useCache !== false;
|
|
194
|
+
let projectDocs = useCache ? getCachedDocs(canonical) : null;
|
|
195
|
+
if (projectDocs) {
|
|
196
|
+
for (const d of projectDocs) docs.push(d);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const tag = basename(canonical);
|
|
201
|
+
// Pass the CANONICAL path to the reader, not the raw one — so a reader
|
|
202
|
+
// that joins with `.ijfw/memory/...` walks the canonical tree, not a
|
|
203
|
+
// possibly-spoofed symlink chain.
|
|
204
|
+
const mem = readProjectMemory(canonical) || {};
|
|
205
|
+
projectDocs = [];
|
|
24
206
|
for (const [source, content] of Object.entries(mem)) {
|
|
25
207
|
if (typeof content !== 'string' || content.length === 0) continue;
|
|
26
208
|
const lines = content.split('\n');
|
|
27
209
|
for (let i = 0; i < lines.length; i++) {
|
|
28
210
|
const line = lines[i];
|
|
29
211
|
if (line.trim().length === 0) continue;
|
|
30
|
-
|
|
212
|
+
projectDocs.push({
|
|
31
213
|
id: `${tag}:${source}:${i + 1}`,
|
|
32
214
|
text: line,
|
|
33
|
-
meta: { project: tag, projectPath:
|
|
215
|
+
meta: { project: tag, projectPath: canonical, source, lineNo: i + 1 },
|
|
34
216
|
});
|
|
35
217
|
}
|
|
36
218
|
}
|
|
219
|
+
if (useCache) setCachedDocs(canonical, projectDocs);
|
|
220
|
+
for (const d of projectDocs) docs.push(d);
|
|
37
221
|
}
|
|
38
222
|
return docs;
|
|
39
223
|
}
|
|
40
224
|
|
|
41
225
|
// Run a BM25-ranked search across the corpus produced from `projects`.
|
|
42
226
|
// Returns [{ content, source, line, project, score, snippet }], capped at limit.
|
|
227
|
+
// opts.allowedRoots: forwarded to buildCorpus (see above).
|
|
228
|
+
// opts.limit: 1..50, default 10.
|
|
43
229
|
export function crossProjectSearch(query, projects, readProjectMemory, opts = {}) {
|
|
44
230
|
const limit = clamp(opts.limit, 1, 50, 10);
|
|
45
231
|
if (!query || typeof query !== 'string') return [];
|
|
46
232
|
|
|
47
|
-
const docs = buildCorpus(projects, readProjectMemory);
|
|
233
|
+
const docs = buildCorpus(projects, readProjectMemory, opts);
|
|
48
234
|
if (docs.length === 0) return [];
|
|
49
235
|
|
|
50
236
|
const hits = searchCorpus(query, docs, { limit });
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>IJFW · Planning Docs</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #fafafa; color: #222; }
|
|
11
|
+
header { padding: 12px 20px; border-bottom: 1px solid #ddd; background: #fff; }
|
|
12
|
+
header h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
13
|
+
header .sub { color: #777; font-size: 12px; margin-top: 4px; }
|
|
14
|
+
main { padding: 20px; max-width: 900px; margin: 0 auto; }
|
|
15
|
+
.path-input { width: 100%; padding: 8px 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; border: 1px solid #ccc; border-radius: 4px; }
|
|
16
|
+
.roots { color: #777; font-size: 12px; margin: 8px 0 16px; }
|
|
17
|
+
.roots code { background: #eee; padding: 1px 5px; border-radius: 3px; }
|
|
18
|
+
.doc { background: #fff; padding: 24px 28px; border: 1px solid #ddd; border-radius: 6px; min-height: 300px; }
|
|
19
|
+
.doc h1 { font-size: 22px; margin: 0 0 12px; }
|
|
20
|
+
.doc h2 { font-size: 18px; margin: 24px 0 10px; padding-bottom: 4px; border-bottom: 1px solid #eee; }
|
|
21
|
+
.doc h3 { font-size: 15px; margin: 20px 0 8px; }
|
|
22
|
+
.doc pre { background: #f4f4f4; padding: 12px; border-radius: 4px; overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
23
|
+
.doc code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
24
|
+
.doc pre code { background: transparent; padding: 0; }
|
|
25
|
+
.doc table { border-collapse: collapse; margin: 12px 0; }
|
|
26
|
+
.doc th, .doc td { border: 1px solid #ddd; padding: 6px 10px; text-align: left; font-size: 13px; }
|
|
27
|
+
.doc th { background: #f4f4f4; }
|
|
28
|
+
.doc blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #666; margin: 12px 0; }
|
|
29
|
+
.err { color: #c00; font-style: italic; }
|
|
30
|
+
.hint { color: #777; font-style: italic; }
|
|
31
|
+
@media (prefers-color-scheme: dark) {
|
|
32
|
+
body { background: #1a1a1a; color: #ddd; }
|
|
33
|
+
header { background: #222; border-color: #333; }
|
|
34
|
+
.doc, .path-input { background: #222; border-color: #333; color: #ddd; }
|
|
35
|
+
.doc pre, .doc code, .roots code, .doc th { background: #2a2a2a; }
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<header>
|
|
41
|
+
<h1>IJFW · Planning Docs</h1>
|
|
42
|
+
<div class="sub">Browse .planning/, .ijfw/memory/, and .ijfw/wave-*/ docs from this project.</div>
|
|
43
|
+
</header>
|
|
44
|
+
<main>
|
|
45
|
+
<input type="text" id="path" class="path-input" placeholder=".planning/1.4.4/HANDOFF-1.4.4.md" autofocus>
|
|
46
|
+
<div class="roots">Allowed roots: .planning/ · .ijfw/memory/ · .ijfw/wave-*/STATE.md · .ijfw/wave-*/SUMMARY.md</div>
|
|
47
|
+
<div id="doc" class="doc"></div>
|
|
48
|
+
</main>
|
|
49
|
+
<script>
|
|
50
|
+
// Tiny markdown shim — produces a safe DOMFragment via DOMParser, no innerHTML.
|
|
51
|
+
// Renders the subset used in IJFW planning docs: headings, paragraphs, code blocks,
|
|
52
|
+
// inline code/bold/italic, links, lists, blockquotes, tables. Not a full CommonMark.
|
|
53
|
+
|
|
54
|
+
function setText(el, text) {
|
|
55
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
56
|
+
el.appendChild(document.createTextNode(text));
|
|
57
|
+
}
|
|
58
|
+
function setStatus(el, text, cls) {
|
|
59
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
60
|
+
const div = document.createElement('div');
|
|
61
|
+
div.className = cls;
|
|
62
|
+
div.textContent = text;
|
|
63
|
+
el.appendChild(div);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeNode(tag, text) {
|
|
67
|
+
const el = document.createElement(tag);
|
|
68
|
+
if (text !== undefined) el.appendChild(document.createTextNode(text));
|
|
69
|
+
return el;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Renders a single line of markdown-inline content into an array of DOM nodes.
|
|
73
|
+
// Handles `code`, **bold**, *italic*, [text](url). All text is added via
|
|
74
|
+
// createTextNode — no HTML parsing of user content.
|
|
75
|
+
function renderInlineToNodes(s) {
|
|
76
|
+
const nodes = [];
|
|
77
|
+
let i = 0;
|
|
78
|
+
while (i < s.length) {
|
|
79
|
+
// Code span: `...`
|
|
80
|
+
if (s[i] === '`') {
|
|
81
|
+
const end = s.indexOf('`', i + 1);
|
|
82
|
+
if (end !== -1) {
|
|
83
|
+
nodes.push(makeNode('code', s.slice(i + 1, end)));
|
|
84
|
+
i = end + 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Bold: **...**
|
|
89
|
+
if (s[i] === '*' && s[i + 1] === '*') {
|
|
90
|
+
const end = s.indexOf('**', i + 2);
|
|
91
|
+
if (end !== -1) {
|
|
92
|
+
nodes.push(makeNode('strong', s.slice(i + 2, end)));
|
|
93
|
+
i = end + 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Italic: *...*
|
|
98
|
+
if (s[i] === '*') {
|
|
99
|
+
const end = s.indexOf('*', i + 1);
|
|
100
|
+
if (end !== -1 && end - i > 1) {
|
|
101
|
+
nodes.push(makeNode('em', s.slice(i + 1, end)));
|
|
102
|
+
i = end + 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Link: [text](url)
|
|
107
|
+
if (s[i] === '[') {
|
|
108
|
+
const close = s.indexOf(']', i + 1);
|
|
109
|
+
if (close !== -1 && s[close + 1] === '(') {
|
|
110
|
+
const urlEnd = s.indexOf(')', close + 2);
|
|
111
|
+
if (urlEnd !== -1) {
|
|
112
|
+
const a = document.createElement('a');
|
|
113
|
+
a.textContent = s.slice(i + 1, close);
|
|
114
|
+
// r13-L-01: tightened URL guard.
|
|
115
|
+
// ALLOW: http://, https://, and same-origin relative paths (no protocol).
|
|
116
|
+
// BLOCK: javascript:, data:, mailto:, vbscript:, file:, AND protocol-relative
|
|
117
|
+
// URLs starting with `//` (would open cross-origin without scheme).
|
|
118
|
+
const url = s.slice(close + 2, urlEnd);
|
|
119
|
+
const isAllowed = (
|
|
120
|
+
/^https?:\/\//.test(url) || // explicit http/https
|
|
121
|
+
(!url.startsWith('//') && !/^[^:/?#]+:/.test(url)) // relative, no protocol, no `//`
|
|
122
|
+
);
|
|
123
|
+
if (isAllowed) {
|
|
124
|
+
a.href = url;
|
|
125
|
+
a.target = '_blank';
|
|
126
|
+
a.rel = 'noopener';
|
|
127
|
+
}
|
|
128
|
+
nodes.push(a);
|
|
129
|
+
i = urlEnd + 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Plain text — accumulate until next special marker
|
|
135
|
+
let j = i;
|
|
136
|
+
while (j < s.length && !'`*['.includes(s[j])) j++;
|
|
137
|
+
if (j === i) j = i + 1;
|
|
138
|
+
nodes.push(document.createTextNode(s.slice(i, j)));
|
|
139
|
+
i = j;
|
|
140
|
+
}
|
|
141
|
+
return nodes;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderMarkdownToFragment(md) {
|
|
145
|
+
const frag = document.createDocumentFragment();
|
|
146
|
+
const lines = md.split('\n');
|
|
147
|
+
let i = 0;
|
|
148
|
+
while (i < lines.length) {
|
|
149
|
+
const line = lines[i];
|
|
150
|
+
// Fenced code block
|
|
151
|
+
if (/^```/.test(line)) {
|
|
152
|
+
const buf = [];
|
|
153
|
+
i++;
|
|
154
|
+
while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i++; }
|
|
155
|
+
i++;
|
|
156
|
+
const pre = document.createElement('pre');
|
|
157
|
+
const code = document.createElement('code');
|
|
158
|
+
code.textContent = buf.join('\n');
|
|
159
|
+
pre.appendChild(code);
|
|
160
|
+
frag.appendChild(pre);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Headings
|
|
164
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
165
|
+
if (h) {
|
|
166
|
+
const tag = 'h' + h[1].length;
|
|
167
|
+
const el = document.createElement(tag);
|
|
168
|
+
for (const n of renderInlineToNodes(h[2])) el.appendChild(n);
|
|
169
|
+
frag.appendChild(el);
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Table: header row + separator row + rows
|
|
174
|
+
if (/^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|?\s*-/.test(lines[i + 1])) {
|
|
175
|
+
const tbl = document.createElement('table');
|
|
176
|
+
const head = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
177
|
+
const tr = document.createElement('tr');
|
|
178
|
+
for (const c of head) {
|
|
179
|
+
const th = document.createElement('th');
|
|
180
|
+
for (const n of renderInlineToNodes(c)) th.appendChild(n);
|
|
181
|
+
tr.appendChild(th);
|
|
182
|
+
}
|
|
183
|
+
tbl.appendChild(tr);
|
|
184
|
+
i += 2;
|
|
185
|
+
while (i < lines.length && /^\s*\|/.test(lines[i])) {
|
|
186
|
+
const cells = lines[i].split('|').slice(1, -1).map((c) => c.trim());
|
|
187
|
+
const row = document.createElement('tr');
|
|
188
|
+
for (const c of cells) {
|
|
189
|
+
const td = document.createElement('td');
|
|
190
|
+
for (const n of renderInlineToNodes(c)) td.appendChild(n);
|
|
191
|
+
row.appendChild(td);
|
|
192
|
+
}
|
|
193
|
+
tbl.appendChild(row);
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
frag.appendChild(tbl);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// Bullet list
|
|
200
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
201
|
+
const ul = document.createElement('ul');
|
|
202
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
|
|
203
|
+
const li = document.createElement('li');
|
|
204
|
+
for (const n of renderInlineToNodes(lines[i].replace(/^\s*[-*]\s+/, ''))) li.appendChild(n);
|
|
205
|
+
ul.appendChild(li);
|
|
206
|
+
i++;
|
|
207
|
+
}
|
|
208
|
+
frag.appendChild(ul);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Blockquote
|
|
212
|
+
if (/^>\s?/.test(line)) {
|
|
213
|
+
const bq = document.createElement('blockquote');
|
|
214
|
+
let first = true;
|
|
215
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
216
|
+
if (!first) bq.appendChild(document.createElement('br'));
|
|
217
|
+
for (const n of renderInlineToNodes(lines[i].replace(/^>\s?/, ''))) bq.appendChild(n);
|
|
218
|
+
first = false;
|
|
219
|
+
i++;
|
|
220
|
+
}
|
|
221
|
+
frag.appendChild(bq);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Blank
|
|
225
|
+
if (line.trim() === '') { i++; continue; }
|
|
226
|
+
// Paragraph
|
|
227
|
+
const p = document.createElement('p');
|
|
228
|
+
const buf = [line];
|
|
229
|
+
i++;
|
|
230
|
+
while (i < lines.length && lines[i].trim() !== '' && !/^(#{1,6}\s|```|>|\s*[-*]\s|\s*\|)/.test(lines[i])) {
|
|
231
|
+
buf.push(lines[i]);
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
for (const n of renderInlineToNodes(buf.join(' '))) p.appendChild(n);
|
|
235
|
+
frag.appendChild(p);
|
|
236
|
+
}
|
|
237
|
+
return frag;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function load(path) {
|
|
241
|
+
const doc = document.getElementById('doc');
|
|
242
|
+
setStatus(doc, 'Loading…', 'hint');
|
|
243
|
+
try {
|
|
244
|
+
const r = await fetch('/api/planning?path=' + encodeURIComponent(path));
|
|
245
|
+
if (!r.ok) {
|
|
246
|
+
const err = await r.json().catch(() => ({ error: 'unknown' }));
|
|
247
|
+
setStatus(doc, r.status + ': ' + (err.error || 'failed'), 'err');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const j = await r.json();
|
|
251
|
+
while (doc.firstChild) doc.removeChild(doc.firstChild);
|
|
252
|
+
doc.appendChild(renderMarkdownToFragment(j.body || ''));
|
|
253
|
+
} catch (e) {
|
|
254
|
+
setStatus(doc, e.message, 'err');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Initial hint
|
|
259
|
+
setStatus(document.getElementById('doc'),
|
|
260
|
+
'Enter a relative path above (e.g. .planning/1.4.4/HANDOFF-1.4.4.md) and press Enter.',
|
|
261
|
+
'hint');
|
|
262
|
+
|
|
263
|
+
const input = document.getElementById('path');
|
|
264
|
+
input.addEventListener('keydown', (e) => {
|
|
265
|
+
if (e.key === 'Enter' && input.value.trim()) load(input.value.trim());
|
|
266
|
+
});
|
|
267
|
+
// Auto-load if ?path= in URL
|
|
268
|
+
const params = new URLSearchParams(location.search);
|
|
269
|
+
const init = params.get('path');
|
|
270
|
+
if (init) { input.value = init; load(init); }
|
|
271
|
+
</script>
|
|
272
|
+
</body>
|
|
273
|
+
</html>
|