akm-cli 0.9.0-beta.9 → 0.9.0-rc.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/CHANGELOG.md +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Staleness-detection pass for `akm improve` (Phase 4A,
|
|
6
|
-
* `.plans/0.8.0/self-improvement-enhancements-plan.md` lines 132-145).
|
|
7
|
-
*
|
|
8
|
-
* Activates the `deprecated` belief-state machinery shipped in Phase 1A
|
|
9
|
-
* (commit 7b6fffe). Without this pass, nothing in the pipeline ever wrote
|
|
10
|
-
* `beliefState: deprecated`, so the -0.15 ranking penalty and the
|
|
11
|
-
* `matchBeliefFilter("historical")` inclusion were dormant.
|
|
12
|
-
*
|
|
13
|
-
* Pipeline
|
|
14
|
-
* --------
|
|
15
|
-
* 1. Walk every memory under `<stash>/memories/` and select candidates
|
|
16
|
-
* whose belief state is NOT already excluded
|
|
17
|
-
* ({contradicted, archived, deprecated}) AND whose `lastConfirmedAt`
|
|
18
|
-
* is absent or older than the configured threshold (default 90 days).
|
|
19
|
-
* Files without `lastConfirmedAt` fall back to file `mtime`.
|
|
20
|
-
* 2. For each candidate, ask the configured validation-tier LLM
|
|
21
|
-
* (`resolveValidationRunner`) whether the candidate is still current
|
|
22
|
-
* given the top-K most-similar memories from the stash.
|
|
23
|
-
* Strict response contract: `YES\nSUPERSEDED_BY: <ref>` or `NO`.
|
|
24
|
-
* Anything else is treated as a parse error and the candidate is skipped.
|
|
25
|
-
* 3. YES → write `beliefState: "deprecated"`, `supersededBy: [<ref>]`,
|
|
26
|
-
* `lastConfirmedAt: <now>`. The supersededBy ref MUST exist in the
|
|
27
|
-
* stash (DB lookup); if it doesn't, treat as NO.
|
|
28
|
-
* NO → write only `lastConfirmedAt: <now>` (refreshes the staleness
|
|
29
|
-
* window). All other frontmatter fields stay untouched.
|
|
30
|
-
*
|
|
31
|
-
* Caching
|
|
32
|
-
* -------
|
|
33
|
-
* Uses the standard `withLlmCache` wrapper with cacheVariant
|
|
34
|
-
* `"staleness_detect"`, so re-running the pass on an unchanged file is a
|
|
35
|
-
* no-op (no LLM call).
|
|
36
|
-
*
|
|
37
|
-
* Feature gate
|
|
38
|
-
* ------------
|
|
39
|
-
* Default OFF. Enable via `features.index.staleness_detection.enabled` (or
|
|
40
|
-
* the boolean shorthand `features.index.staleness_detection = true`).
|
|
41
|
-
* Threshold-days knob lives at
|
|
42
|
-
* `features.index.staleness_detection.options.thresholdDays` (default 90).
|
|
43
|
-
*/
|
|
44
|
-
import { createHash } from "node:crypto";
|
|
45
|
-
import fs from "node:fs";
|
|
46
|
-
import path from "node:path";
|
|
47
|
-
import { assembleAsset } from "../../core/asset/asset-serialize.js";
|
|
48
|
-
import { parseFrontmatter, parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
|
|
49
|
-
import { concurrentMap } from "../../core/concurrent.js";
|
|
50
|
-
import { warn } from "../../core/warn.js";
|
|
51
|
-
import { resolveValidationRunner, runnerIsLlm } from "../../integrations/agent/runner.js";
|
|
52
|
-
import { chatCompletion } from "../../llm/client.js";
|
|
53
|
-
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
54
|
-
import { findEntryIdByRef } from "../db/db.js";
|
|
55
|
-
import { withLlmCache } from "../db/llm-cache.js";
|
|
56
|
-
import { walkMarkdownFiles } from "../walk/walker.js";
|
|
57
|
-
/** Frontmatter keys this pass touches. Constants so a future rename only needs to touch one site. */
|
|
58
|
-
const FM_BELIEF_STATE = "beliefState";
|
|
59
|
-
const FM_SUPERSEDED_BY = "supersededBy";
|
|
60
|
-
const FM_LAST_CONFIRMED_AT = "lastConfirmedAt";
|
|
61
|
-
/** Cache variant for `withLlmCache`. Keeps staleness results isolated from memory-inference cache rows. */
|
|
62
|
-
const CACHE_VARIANT = "staleness_detect";
|
|
63
|
-
/** Belief states excluded from staleness detection — already historical / archived. */
|
|
64
|
-
const EXCLUDED_BELIEF_STATES = new Set(["contradicted", "archived", "deprecated"]);
|
|
65
|
-
/** Default threshold in days before a memory is re-evaluated. */
|
|
66
|
-
const DEFAULT_THRESHOLD_DAYS = 90;
|
|
67
|
-
/** Top-K similar memories included in the LLM prompt. */
|
|
68
|
-
const TOP_K_SIMILAR = 5;
|
|
69
|
-
/**
|
|
70
|
-
* Top-level entry point. Returns a zero-counters result when the feature is
|
|
71
|
-
* disabled or no validation-tier runner is configured.
|
|
72
|
-
*/
|
|
73
|
-
export async function runStalenessDetectionPass(ctx) {
|
|
74
|
-
const { config, sources, signal, db } = ctx;
|
|
75
|
-
const start = Date.now();
|
|
76
|
-
const result = {
|
|
77
|
-
considered: 0,
|
|
78
|
-
deprecated: 0,
|
|
79
|
-
confirmed: 0,
|
|
80
|
-
skipped: 0,
|
|
81
|
-
durationMs: 0,
|
|
82
|
-
warnings: [],
|
|
83
|
-
};
|
|
84
|
-
// Feature gate — default OFF.
|
|
85
|
-
if (!isProcessEnabled("index", "staleness_detection", config)) {
|
|
86
|
-
result.durationMs = Date.now() - start;
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
// The pass only writes to the primary (writable) stash. Read-only sources
|
|
90
|
-
// would be clobbered by the next sync(), so we skip them entirely.
|
|
91
|
-
const primary = sources[0];
|
|
92
|
-
if (!primary) {
|
|
93
|
-
result.durationMs = Date.now() - start;
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
const runner = resolveValidationRunner(config);
|
|
97
|
-
if (!runner) {
|
|
98
|
-
result.warnings.push("staleness_detection: no validation runner configured; skipping pass");
|
|
99
|
-
result.durationMs = Date.now() - start;
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
if (!runnerIsLlm(runner)) {
|
|
103
|
-
// MVP scope: only the LLM runner kind is supported. Agent/SDK runners
|
|
104
|
-
// would require a different prompt-dispatch path that is out of scope
|
|
105
|
-
// for the initial Phase 4A implementation.
|
|
106
|
-
result.warnings.push(`staleness_detection: validation runner kind "${runner.kind}" not supported by MVP; configure an llm-kind validation profile`);
|
|
107
|
-
result.durationMs = Date.now() - start;
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
const configuredThreshold = config.index?.stalenessDetection?.thresholdDays;
|
|
111
|
-
const thresholdDays = typeof configuredThreshold === "number" && configuredThreshold >= 0 ? configuredThreshold : DEFAULT_THRESHOLD_DAYS;
|
|
112
|
-
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1000;
|
|
113
|
-
const now = Date.now();
|
|
114
|
-
const candidates = collectStaleCandidates(primary.path, now, thresholdMs);
|
|
115
|
-
result.considered = candidates.length;
|
|
116
|
-
if (candidates.length === 0) {
|
|
117
|
-
result.durationMs = Date.now() - start;
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
const allMemories = collectAllMemoriesForSimilarity(primary.path);
|
|
121
|
-
const nowIso = new Date(now).toISOString();
|
|
122
|
-
const concurrency = runner.connection.concurrency ?? 1;
|
|
123
|
-
const validate = (raw) => {
|
|
124
|
-
if (!raw || typeof raw !== "object")
|
|
125
|
-
return undefined;
|
|
126
|
-
const r = raw;
|
|
127
|
-
if (r.decision === "deprecated") {
|
|
128
|
-
if (typeof r.supersededBy === "string" && r.supersededBy.trim().length > 0) {
|
|
129
|
-
return { decision: "deprecated", supersededBy: r.supersededBy.trim() };
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
if (r.decision === "confirmed")
|
|
134
|
-
return { decision: "confirmed" };
|
|
135
|
-
return undefined;
|
|
136
|
-
};
|
|
137
|
-
const perResult = await concurrentMap(candidates, async (candidate) => {
|
|
138
|
-
if (signal?.aborted)
|
|
139
|
-
return undefined;
|
|
140
|
-
const cacheKey = candidate.filePath;
|
|
141
|
-
const cacheBody = `${candidate.filePath}\n${candidate.body}`;
|
|
142
|
-
const decision = db
|
|
143
|
-
? await withLlmCache(db, cacheKey, cacheBody, false, () => askValidator(runner.connection, candidate, allMemories, signal, runner.timeoutMs), validate, undefined, CACHE_VARIANT)
|
|
144
|
-
: await askValidator(runner.connection, candidate, allMemories, signal, runner.timeoutMs);
|
|
145
|
-
return { candidate, decision };
|
|
146
|
-
}, concurrency);
|
|
147
|
-
for (const entry of perResult) {
|
|
148
|
-
if (!entry)
|
|
149
|
-
continue;
|
|
150
|
-
const { candidate, decision } = entry;
|
|
151
|
-
if (!decision) {
|
|
152
|
-
result.skipped += 1;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (decision.decision === "deprecated") {
|
|
156
|
-
const targetRef = decision.supersededBy ?? "";
|
|
157
|
-
const validatedRef = validateSupersedingRef(targetRef, primary.path, db);
|
|
158
|
-
if (!validatedRef) {
|
|
159
|
-
// Spec line 153: never mark deprecated unless SUPERSEDED_BY exists.
|
|
160
|
-
// Refresh lastConfirmedAt instead — the candidate is still our best
|
|
161
|
-
// record until a real superseder shows up — and emit a warning.
|
|
162
|
-
result.warnings.push(`staleness_detection: ${candidate.ref} reported superseded by "${targetRef}" but that ref does not exist; refreshing instead`);
|
|
163
|
-
try {
|
|
164
|
-
writeLastConfirmed(candidate, nowIso);
|
|
165
|
-
result.confirmed += 1;
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
result.warnings.push(`staleness_detection: failed to refresh ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
169
|
-
result.skipped += 1;
|
|
170
|
-
}
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
writeDeprecated(candidate, validatedRef, nowIso);
|
|
175
|
-
result.deprecated += 1;
|
|
176
|
-
}
|
|
177
|
-
catch (err) {
|
|
178
|
-
result.warnings.push(`staleness_detection: failed to deprecate ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
-
result.skipped += 1;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
try {
|
|
184
|
-
writeLastConfirmed(candidate, nowIso);
|
|
185
|
-
result.confirmed += 1;
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
result.warnings.push(`staleness_detection: failed to refresh ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
189
|
-
result.skipped += 1;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
result.durationMs = Date.now() - start;
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
function collectStaleCandidates(stashRoot, now, thresholdMs) {
|
|
197
|
-
const memoriesDir = path.join(stashRoot, "memories");
|
|
198
|
-
if (!fs.existsSync(memoriesDir))
|
|
199
|
-
return [];
|
|
200
|
-
const out = [];
|
|
201
|
-
for (const filePath of walkMarkdownFiles(memoriesDir)) {
|
|
202
|
-
let raw;
|
|
203
|
-
let stat;
|
|
204
|
-
try {
|
|
205
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
206
|
-
stat = fs.statSync(filePath);
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const parsed = parseFrontmatter(raw);
|
|
212
|
-
const belief = typeof parsed.data[FM_BELIEF_STATE] === "string" ? parsed.data[FM_BELIEF_STATE] : "";
|
|
213
|
-
if (EXCLUDED_BELIEF_STATES.has(belief))
|
|
214
|
-
continue;
|
|
215
|
-
const lastConfirmedMs = parseDateMs(parsed.data[FM_LAST_CONFIRMED_AT]);
|
|
216
|
-
const signalMs = lastConfirmedMs ?? stat.mtimeMs;
|
|
217
|
-
const ageMs = now - signalMs;
|
|
218
|
-
if (ageMs < thresholdMs)
|
|
219
|
-
continue;
|
|
220
|
-
const name = toMemoryName(memoriesDir, filePath);
|
|
221
|
-
if (!name)
|
|
222
|
-
continue;
|
|
223
|
-
out.push({
|
|
224
|
-
filePath,
|
|
225
|
-
ref: `memory:${name}`,
|
|
226
|
-
name,
|
|
227
|
-
data: parsed.data,
|
|
228
|
-
body: parsed.content,
|
|
229
|
-
lastSignalMs: signalMs,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
|
-
function collectAllMemoriesForSimilarity(stashRoot) {
|
|
235
|
-
const memoriesDir = path.join(stashRoot, "memories");
|
|
236
|
-
if (!fs.existsSync(memoriesDir))
|
|
237
|
-
return [];
|
|
238
|
-
const out = [];
|
|
239
|
-
const now = Date.now();
|
|
240
|
-
for (const filePath of walkMarkdownFiles(memoriesDir)) {
|
|
241
|
-
let raw;
|
|
242
|
-
let stat;
|
|
243
|
-
try {
|
|
244
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
245
|
-
stat = fs.statSync(filePath);
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
const parsed = parseFrontmatter(raw);
|
|
251
|
-
const name = toMemoryName(memoriesDir, filePath);
|
|
252
|
-
if (!name)
|
|
253
|
-
continue;
|
|
254
|
-
const title = typeof parsed.data.title === "string" ? parsed.data.title : "";
|
|
255
|
-
const description = typeof parsed.data.description === "string" ? parsed.data.description : "";
|
|
256
|
-
const body = parsed.content;
|
|
257
|
-
out.push({
|
|
258
|
-
ref: `memory:${name}`,
|
|
259
|
-
name,
|
|
260
|
-
filePath,
|
|
261
|
-
body,
|
|
262
|
-
title,
|
|
263
|
-
description,
|
|
264
|
-
tokens: tokenize(`${title} ${description} ${body}`),
|
|
265
|
-
ageMs: now - stat.mtimeMs,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
return out;
|
|
269
|
-
}
|
|
270
|
-
function toMemoryName(memoriesDir, filePath) {
|
|
271
|
-
const rel = path.relative(memoriesDir, filePath);
|
|
272
|
-
if (!rel || rel.startsWith(".."))
|
|
273
|
-
return undefined;
|
|
274
|
-
return rel.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
275
|
-
}
|
|
276
|
-
function parseDateMs(value) {
|
|
277
|
-
if (typeof value !== "string")
|
|
278
|
-
return undefined;
|
|
279
|
-
const ms = Date.parse(value);
|
|
280
|
-
return Number.isFinite(ms) ? ms : undefined;
|
|
281
|
-
}
|
|
282
|
-
// ── Similarity (lightweight FTS-style fallback) ─────────────────────────────
|
|
283
|
-
/**
|
|
284
|
-
* Token-overlap similarity is the FTS fallback for the prompt's "similar
|
|
285
|
-
* memories" payload. Embedding-aware nearest-neighbor lookup is a future
|
|
286
|
-
* enhancement; the prompt only needs enough context to ground a YES/NO
|
|
287
|
-
* decision, which token overlap of body + description provides.
|
|
288
|
-
*/
|
|
289
|
-
function tokenize(text) {
|
|
290
|
-
const out = new Set();
|
|
291
|
-
const lower = text.toLowerCase();
|
|
292
|
-
const tokens = lower.match(/[a-z0-9][a-z0-9_-]{2,}/g);
|
|
293
|
-
if (!tokens)
|
|
294
|
-
return out;
|
|
295
|
-
for (const t of tokens)
|
|
296
|
-
out.add(t);
|
|
297
|
-
return out;
|
|
298
|
-
}
|
|
299
|
-
function pickSimilar(candidate, all) {
|
|
300
|
-
const candTokens = tokenize(`${typeof candidate.data.title === "string" ? candidate.data.title : ""} ${typeof candidate.data.description === "string" ? candidate.data.description : ""} ${candidate.body}`);
|
|
301
|
-
const candMs = candidate.lastSignalMs;
|
|
302
|
-
const scored = [];
|
|
303
|
-
for (const snap of all) {
|
|
304
|
-
if (snap.ref === candidate.ref)
|
|
305
|
-
continue;
|
|
306
|
-
// Prefer memories more recent than the candidate so the validator can
|
|
307
|
-
// see what may have superseded it.
|
|
308
|
-
if (snap.ageMs >= Date.now() - candMs)
|
|
309
|
-
continue;
|
|
310
|
-
let overlap = 0;
|
|
311
|
-
for (const t of candTokens)
|
|
312
|
-
if (snap.tokens.has(t))
|
|
313
|
-
overlap += 1;
|
|
314
|
-
if (overlap === 0)
|
|
315
|
-
continue;
|
|
316
|
-
scored.push({ snap, score: overlap });
|
|
317
|
-
}
|
|
318
|
-
scored.sort((a, b) => b.score - a.score);
|
|
319
|
-
return scored.slice(0, TOP_K_SIMILAR).map((s) => s.snap);
|
|
320
|
-
}
|
|
321
|
-
// ── LLM dispatch ────────────────────────────────────────────────────────────
|
|
322
|
-
const SYSTEM_PROMPT = "You are a belief-state classifier for a memory store. Given a candidate memory and a list of more-recent similar memories from the same store, decide whether the candidate is still current or has been superseded.\n\n" +
|
|
323
|
-
"Respond on the first line with exactly YES or NO.\n" +
|
|
324
|
-
"If YES, the second line MUST be of the form `SUPERSEDED_BY: <ref>` where <ref> is the exact ref of the superseding memory from the list provided. Do NOT invent refs.\n" +
|
|
325
|
-
"If NO, do not include any additional lines.\n" +
|
|
326
|
-
"No prose, no preamble, no markdown.";
|
|
327
|
-
async function askValidator(connection, candidate, allMemories, signal, timeoutMs) {
|
|
328
|
-
const similar = pickSimilar(candidate, allMemories);
|
|
329
|
-
if (similar.length === 0) {
|
|
330
|
-
// No more-recent similar memories — there is nothing the candidate could
|
|
331
|
-
// have been superseded by. Treat as confirmed without paying for an LLM call.
|
|
332
|
-
return { decision: "confirmed" };
|
|
333
|
-
}
|
|
334
|
-
const messages = [
|
|
335
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
336
|
-
{ role: "user", content: buildPrompt(candidate, similar) },
|
|
337
|
-
];
|
|
338
|
-
let raw;
|
|
339
|
-
try {
|
|
340
|
-
raw = await chatCompletion(connection, messages, {
|
|
341
|
-
...(typeof timeoutMs === "number" ? { timeoutMs } : {}),
|
|
342
|
-
...(signal ? { signal } : {}),
|
|
343
|
-
temperature: 0,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
catch (err) {
|
|
347
|
-
warn(`[improve] staleness detection LLM call failed for ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
return parseStalenessResponse(raw);
|
|
351
|
-
}
|
|
352
|
-
export function buildPrompt(candidate, similar) {
|
|
353
|
-
const lines = [];
|
|
354
|
-
lines.push(`Candidate memory: ${candidate.ref}`);
|
|
355
|
-
if (typeof candidate.data.title === "string" && candidate.data.title.trim().length > 0) {
|
|
356
|
-
lines.push(`Title: ${candidate.data.title.trim()}`);
|
|
357
|
-
}
|
|
358
|
-
if (typeof candidate.data.description === "string" && candidate.data.description.trim().length > 0) {
|
|
359
|
-
lines.push(`Description: ${candidate.data.description.trim()}`);
|
|
360
|
-
}
|
|
361
|
-
lines.push("Body:");
|
|
362
|
-
lines.push(candidate.body.trim());
|
|
363
|
-
lines.push("");
|
|
364
|
-
lines.push(`Similar more-recent memories (top ${similar.length}):`);
|
|
365
|
-
for (const s of similar) {
|
|
366
|
-
lines.push("---");
|
|
367
|
-
lines.push(`Ref: ${s.ref}`);
|
|
368
|
-
if (s.title)
|
|
369
|
-
lines.push(`Title: ${s.title}`);
|
|
370
|
-
if (s.description)
|
|
371
|
-
lines.push(`Description: ${s.description}`);
|
|
372
|
-
lines.push("Body:");
|
|
373
|
-
lines.push(s.body.trim());
|
|
374
|
-
}
|
|
375
|
-
lines.push("");
|
|
376
|
-
lines.push("Question: Given the more-recent similar memories above, has the candidate memory been superseded, or is it still current?");
|
|
377
|
-
return lines.join("\n");
|
|
378
|
-
}
|
|
379
|
-
/** Exported for direct unit testing. */
|
|
380
|
-
export function parseStalenessResponse(raw) {
|
|
381
|
-
if (typeof raw !== "string")
|
|
382
|
-
return undefined;
|
|
383
|
-
const trimmed = raw.trim();
|
|
384
|
-
if (trimmed.length === 0)
|
|
385
|
-
return undefined;
|
|
386
|
-
const lines = trimmed
|
|
387
|
-
.split(/\r?\n/)
|
|
388
|
-
.map((l) => l.trim())
|
|
389
|
-
.filter((l) => l.length > 0);
|
|
390
|
-
if (lines.length === 0)
|
|
391
|
-
return undefined;
|
|
392
|
-
const head = lines[0]?.toUpperCase();
|
|
393
|
-
if (head === "NO")
|
|
394
|
-
return { decision: "confirmed" };
|
|
395
|
-
if (head === "YES") {
|
|
396
|
-
// Find the SUPERSEDED_BY line; tolerate it being anywhere in the body
|
|
397
|
-
// but require an exact prefix match.
|
|
398
|
-
for (const line of lines.slice(1)) {
|
|
399
|
-
const m = line.match(/^SUPERSEDED_BY:\s*(\S.*)$/i);
|
|
400
|
-
if (m?.[1]) {
|
|
401
|
-
const ref = m[1].trim();
|
|
402
|
-
if (ref.length > 0)
|
|
403
|
-
return { decision: "deprecated", supersededBy: ref };
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return undefined; // YES without a SUPERSEDED_BY line is a parse error.
|
|
407
|
-
}
|
|
408
|
-
return undefined;
|
|
409
|
-
}
|
|
410
|
-
// ── Ref validation ──────────────────────────────────────────────────────────
|
|
411
|
-
/**
|
|
412
|
-
* Validate that the proposed `supersededBy` ref actually exists. We first try
|
|
413
|
-
* the indexed DB (canonical source of truth when available), then fall back to
|
|
414
|
-
* an on-disk filesystem probe under `<stash>/memories/<name>.md` so the pass
|
|
415
|
-
* works even on stashes that have never been indexed.
|
|
416
|
-
*/
|
|
417
|
-
function validateSupersedingRef(refStr, stashRoot, db) {
|
|
418
|
-
const trimmed = refStr.trim();
|
|
419
|
-
if (!trimmed)
|
|
420
|
-
return undefined;
|
|
421
|
-
if (db) {
|
|
422
|
-
try {
|
|
423
|
-
const id = findEntryIdByRef(db, trimmed);
|
|
424
|
-
if (typeof id === "number")
|
|
425
|
-
return trimmed;
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
// Fall through to filesystem probe.
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const m = trimmed.match(/^memory:(.+)$/);
|
|
432
|
-
if (!m)
|
|
433
|
-
return undefined;
|
|
434
|
-
const filePath = path.join(stashRoot, "memories", `${m[1]}.md`);
|
|
435
|
-
if (fs.existsSync(filePath))
|
|
436
|
-
return trimmed;
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
// ── Frontmatter writes ──────────────────────────────────────────────────────
|
|
440
|
-
/**
|
|
441
|
-
* Write `beliefState: deprecated`, `supersededBy`, and `lastConfirmedAt` to
|
|
442
|
-
* the candidate's frontmatter. All other fields are preserved verbatim. Uses
|
|
443
|
-
* the same atomic-write shape as `markParentProcessed()` in
|
|
444
|
-
* `memory-inference.ts`: re-read the file from disk, parse the YAML block,
|
|
445
|
-
* stitch a new block in front of the original body bytes.
|
|
446
|
-
*/
|
|
447
|
-
function writeDeprecated(candidate, supersededByRef, nowIso) {
|
|
448
|
-
const raw = fs.readFileSync(candidate.filePath, "utf8");
|
|
449
|
-
const block = parseFrontmatterBlock(raw);
|
|
450
|
-
const baseFm = block ? { ...parseFrontmatter(raw).data } : {};
|
|
451
|
-
const nextFm = {
|
|
452
|
-
...baseFm,
|
|
453
|
-
[FM_BELIEF_STATE]: "deprecated",
|
|
454
|
-
[FM_SUPERSEDED_BY]: dedupeStringArray([...stringArrayOrEmpty(baseFm[FM_SUPERSEDED_BY]), supersededByRef]),
|
|
455
|
-
[FM_LAST_CONFIRMED_AT]: nowIso,
|
|
456
|
-
};
|
|
457
|
-
writeFrontmatterAtomic(candidate.filePath, nextFm, block?.content ?? raw);
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Strict additive frontmatter write: ONLY `lastConfirmedAt` is touched.
|
|
461
|
-
* Every other field in the file is preserved as-is.
|
|
462
|
-
*/
|
|
463
|
-
function writeLastConfirmed(candidate, nowIso) {
|
|
464
|
-
const raw = fs.readFileSync(candidate.filePath, "utf8");
|
|
465
|
-
const block = parseFrontmatterBlock(raw);
|
|
466
|
-
const baseFm = block ? { ...parseFrontmatter(raw).data } : {};
|
|
467
|
-
const nextFm = {
|
|
468
|
-
...baseFm,
|
|
469
|
-
[FM_LAST_CONFIRMED_AT]: nowIso,
|
|
470
|
-
};
|
|
471
|
-
writeFrontmatterAtomic(candidate.filePath, nextFm, block?.content ?? raw);
|
|
472
|
-
}
|
|
473
|
-
function writeFrontmatterAtomic(filePath, frontmatter, body) {
|
|
474
|
-
fs.writeFileSync(filePath, assembleAsset(frontmatter, body), "utf8");
|
|
475
|
-
}
|
|
476
|
-
function stringArrayOrEmpty(value) {
|
|
477
|
-
if (!Array.isArray(value))
|
|
478
|
-
return [];
|
|
479
|
-
return value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
480
|
-
}
|
|
481
|
-
function dedupeStringArray(values) {
|
|
482
|
-
return [...new Set(values.map((v) => v.trim()).filter((v) => v.length > 0))];
|
|
483
|
-
}
|
|
484
|
-
// ── Body hash helper (exported for tests) ───────────────────────────────────
|
|
485
|
-
/** Internal helper exported only for testing — mirrors `computeBodyHash`. */
|
|
486
|
-
export function _stalenessBodyHash(filePath, body) {
|
|
487
|
-
return createHash("sha256").update(`${filePath}\n${body}`).digest("hex");
|
|
488
|
-
}
|