akm-cli 0.9.0-beta.6 → 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 +663 -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 -14
- 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 +153 -147
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- 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 -1083
- 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 +1313 -1278
- 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 +243 -599
- 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 +55 -4
- 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 +160 -7
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +32 -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 +510 -2537
- 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 +115 -0
- 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 +212 -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 +7 -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 -882
- package/dist/commands/read/curate.js +419 -103
- 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 +29 -41
- 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 +389 -58
- 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 +21 -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 +142 -1091
- 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 +356 -780
- 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 +103 -24
- 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 +106 -0
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +291 -310
- 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 +11 -1
- 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 +57 -15
- 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 +2718 -2354
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
- 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/tar-utils.js +16 -8
- 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
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
* - Zep / Graphiti (arXiv:2501.13956 §3) — unified belief-revision pipeline
|
|
28
28
|
* - MemOS (arXiv:2507.03724) — formal archive/merge/transition with shared state model
|
|
29
29
|
*/
|
|
30
|
-
import
|
|
31
|
-
import { assembleAsset } from "../../../core/asset/asset-serialize.js";
|
|
32
|
-
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
30
|
+
import { mutateFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
33
31
|
// ── Contradiction edge writer ─────────────────────────────────────────────────
|
|
34
32
|
/**
|
|
35
33
|
* Write `contradictedBy` and `beliefState: contradicted` edges to a memory
|
|
@@ -47,16 +45,17 @@ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
|
47
45
|
* @param contradictedByRef - The ref that contradicts this memory.
|
|
48
46
|
*/
|
|
49
47
|
export function writeContradictEdge(filePath, contradictedByRef) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
mutateFrontmatter(filePath, (parsed) => {
|
|
49
|
+
const existing = Array.isArray(parsed.data.contradictedBy)
|
|
50
|
+
? parsed.data.contradictedBy
|
|
51
|
+
: [];
|
|
52
|
+
if (existing.includes(contradictedByRef))
|
|
53
|
+
return null; // Already written — idempotent.
|
|
54
|
+
const nextContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
|
|
55
|
+
return {
|
|
56
|
+
...parsed.data,
|
|
57
|
+
contradictedBy: nextContradictedBy,
|
|
58
|
+
beliefState: "contradicted",
|
|
59
|
+
};
|
|
60
|
+
});
|
|
62
61
|
}
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import fs from "node:fs";
|
|
37
37
|
import path from "node:path";
|
|
38
|
-
import
|
|
39
|
-
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
38
|
+
import contradictionJudgeTemplate from "../../../assets/prompts/contradiction-judge.md" with { type: "text" };
|
|
39
|
+
import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
40
40
|
import { getDefaultLlmConfig } from "../../../core/config/config.js";
|
|
41
41
|
import { chatCompletion, parseEmbeddedJsonResponse } from "../../../llm/client.js";
|
|
42
42
|
import { tryLlmFeature } from "../../../llm/feature-gate.js";
|
|
@@ -51,6 +51,13 @@ const MAX_FAMILY_SIZE = 8;
|
|
|
51
51
|
* families. Prevents runaway LLM usage on stashes with many memories.
|
|
52
52
|
*/
|
|
53
53
|
const MAX_PAIRS_PER_RUN = 20;
|
|
54
|
+
/**
|
|
55
|
+
* Minimum confidence required to write a contradiction edge. Below this
|
|
56
|
+
* threshold the LLM may be flagging topic-overlap rather than genuine logical
|
|
57
|
+
* exclusivity (investigation 2026-06-18). Absent confidence fields default to
|
|
58
|
+
* 1.0 for backward compatibility with older judge responses.
|
|
59
|
+
*/
|
|
60
|
+
const CONTRADICT_CONFIDENCE_THRESHOLD = 0.92;
|
|
54
61
|
/**
|
|
55
62
|
* Truncation limit for memory body content sent to the LLM judge.
|
|
56
63
|
* Keeps prompts compact while preserving the key factual claims.
|
|
@@ -58,34 +65,13 @@ const MAX_PAIRS_PER_RUN = 20;
|
|
|
58
65
|
const BODY_TRUNCATION = 800;
|
|
59
66
|
// ── Prompt builder ────────────────────────────────────────────────────────────
|
|
60
67
|
function buildContradictionJudgePrompt(a, b) {
|
|
61
|
-
return
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"",
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"Content:",
|
|
69
|
-
"```",
|
|
70
|
-
a.body.slice(0, BODY_TRUNCATION),
|
|
71
|
-
"```",
|
|
72
|
-
"",
|
|
73
|
-
"Memory B:",
|
|
74
|
-
`Ref: ${b.ref}`,
|
|
75
|
-
`Description: ${b.description || "(none)"}`,
|
|
76
|
-
"Content:",
|
|
77
|
-
"```",
|
|
78
|
-
b.body.slice(0, BODY_TRUNCATION),
|
|
79
|
-
"```",
|
|
80
|
-
"",
|
|
81
|
-
"Answer ONLY with valid JSON — no prose, no code fences:",
|
|
82
|
-
'{"contradicts": true|false, "reason": "<one sentence explaining why or why not>"}',
|
|
83
|
-
"",
|
|
84
|
-
"A contradiction means the memories make mutually exclusive factual claims about the",
|
|
85
|
-
"same topic (e.g. Memory A says 'always use VPN' while Memory B says 'VPN is optional').",
|
|
86
|
-
"If the memories are complementary, about different topics, or one supersedes the other",
|
|
87
|
-
"without direct conflict, return false.",
|
|
88
|
-
].join("\n");
|
|
68
|
+
return contradictionJudgeTemplate
|
|
69
|
+
.replace("{{A_REF}}", a.ref)
|
|
70
|
+
.replace("{{A_DESCRIPTION}}", a.description || "(none)")
|
|
71
|
+
.replace("{{A_BODY}}", a.body.slice(0, BODY_TRUNCATION))
|
|
72
|
+
.replace("{{B_REF}}", b.ref)
|
|
73
|
+
.replace("{{B_DESCRIPTION}}", b.description || "(none)")
|
|
74
|
+
.replace("{{B_BODY}}", b.body.slice(0, BODY_TRUNCATION));
|
|
89
75
|
}
|
|
90
76
|
// ── Filesystem helpers ────────────────────────────────────────────────────────
|
|
91
77
|
function* walkMarkdownFilesLocal(root) {
|
|
@@ -139,19 +125,42 @@ function resolveParentRef(filePath, frontmatter, memoriesRootDir) {
|
|
|
139
125
|
*/
|
|
140
126
|
/** Returns true if the edge was newly written, false if it already existed. */
|
|
141
127
|
function writeContradictedByEdge(filePath, contradictedByRef) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
128
|
+
return mutateFrontmatter(filePath, (parsed) => {
|
|
129
|
+
const existing = Array.isArray(parsed.data.contradictedBy)
|
|
130
|
+
? parsed.data.contradictedBy
|
|
131
|
+
: [];
|
|
132
|
+
if (existing.includes(contradictedByRef))
|
|
133
|
+
return null; // Edge already written.
|
|
134
|
+
const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
|
|
135
|
+
return {
|
|
136
|
+
...parsed.data,
|
|
137
|
+
contradictedBy: updatedContradictedBy,
|
|
138
|
+
beliefState: "contradicted",
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Deterministically pick, for a confirmed-contradiction pair, the LOSER memory
|
|
144
|
+
* that receives the single directed `contradictedBy` edge (SCC-resolved to
|
|
145
|
+
* `contradicted`) and the WINNER ref that survives as the current belief.
|
|
146
|
+
*
|
|
147
|
+
* A SINGLE directed edge is essential. Writing mutual A↔B edges forms a 2-cycle
|
|
148
|
+
* that {@link resolveFamilyContradictions} collapses into one strongly-connected
|
|
149
|
+
* SINK component and refreshes BOTH members back to active — erasing the
|
|
150
|
+
* contradiction on every run (the self-erasing bug this fix removes).
|
|
151
|
+
*
|
|
152
|
+
* Direction = lexicographic ref order: the ref that sorts LATER is the loser.
|
|
153
|
+
* This is a **total order** over the family's (distinct) refs, so the induced
|
|
154
|
+
* edges are always acyclic — a family of any size resolves to a DAG with a
|
|
155
|
+
* single sink, never a cycle that the resolver would refresh back to active.
|
|
156
|
+
* It is also immutable across runs (unlike file mtime, which the resolver
|
|
157
|
+
* bumps when it rewrites loser files), so detection is idempotent. Ref order
|
|
158
|
+
* carries no recency meaning — no derived-memory writer sets a `createdAt`/
|
|
159
|
+
* timestamp today — but the mechanism only needs a stable, acyclic direction;
|
|
160
|
+
* eliminating worst-case self-erasure, not ranking by recency, is the goal.
|
|
161
|
+
*/
|
|
162
|
+
function pickContradictionLoser(a, b) {
|
|
163
|
+
return a.ref < b.ref ? { loser: b, winnerRef: a.ref } : { loser: a, winnerRef: b.ref };
|
|
155
164
|
}
|
|
156
165
|
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
157
166
|
/**
|
|
@@ -222,18 +231,22 @@ export async function detectAndWriteContradictions(stashDir, config, chat = chat
|
|
|
222
231
|
const b = family[j];
|
|
223
232
|
if (!a || !b)
|
|
224
233
|
continue;
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
// Resolve the directed edge up front (independent of the judge — it is
|
|
235
|
+
// decided by lexicographic ref order). Skip when that single loser→winner
|
|
236
|
+
// edge already exists (no new information; avoids re-judging resolved
|
|
237
|
+
// pairs across runs).
|
|
238
|
+
//
|
|
239
|
+
// Legacy mutual A↔B edges written by the pre-fix pass self-heal: the
|
|
240
|
+
// skip fires this run, but the SCC resolver treats the 2-cycle as a sink
|
|
241
|
+
// and refreshes both to active — DELETING both `contradictedBy` arrays
|
|
242
|
+
// (memory-improve.ts persistBeliefStateTransition). The next detection
|
|
243
|
+
// run then sees no edge, re-judges, and writes the single canonical edge.
|
|
244
|
+
const aParsed = parseFrontmatter(fs.readFileSync(a.filePath, "utf8"));
|
|
245
|
+
const bParsed = parseFrontmatter(fs.readFileSync(b.filePath, "utf8"));
|
|
246
|
+
const { loser, winnerRef } = pickContradictionLoser(a, b);
|
|
247
|
+
const loserData = loser === a ? aParsed.data : bParsed.data;
|
|
248
|
+
const loserCB = Array.isArray(loserData.contradictedBy) ? loserData.contradictedBy : [];
|
|
249
|
+
if (loserCB.includes(winnerRef))
|
|
237
250
|
continue;
|
|
238
251
|
const prompt = buildContradictionJudgePrompt(a, b);
|
|
239
252
|
const judgeResult = await tryLlmFeature("memory_contradiction_detection", config, async () => {
|
|
@@ -256,14 +269,24 @@ export async function detectAndWriteContradictions(stashDir, config, chat = chat
|
|
|
256
269
|
}
|
|
257
270
|
if (!parsed?.contradicts)
|
|
258
271
|
continue;
|
|
259
|
-
//
|
|
272
|
+
// Confidence gate: absent field defaults to 1.0 (backward compat with
|
|
273
|
+
// pre-confidence responses). Do NOT default to 0 — that would silently
|
|
274
|
+
// disable all detection during the rollout period.
|
|
275
|
+
const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 1.0;
|
|
276
|
+
if (confidence < CONTRADICT_CONFIDENCE_THRESHOLD) {
|
|
277
|
+
result.warnings.push(`Pair ${a.ref} / ${b.ref}: confidence ${confidence.toFixed(2)} below ${CONTRADICT_CONFIDENCE_THRESHOLD} threshold — skipped.`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Write a SINGLE directed contradiction edge: the losing (older) memory
|
|
281
|
+
// gets `contradictedBy` pointing to the winner. A mutual A↔B pair forms
|
|
282
|
+
// a 2-cycle that the SCC resolver refreshes back to active, erasing the
|
|
283
|
+
// contradiction every run (see pickContradictionLoser).
|
|
260
284
|
try {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
result.edgesWritten += (wroteA ? 1 : 0) + (wroteB ? 1 : 0);
|
|
285
|
+
const wrote = writeContradictedByEdge(loser.filePath, winnerRef);
|
|
286
|
+
result.edgesWritten += wrote ? 1 : 0;
|
|
264
287
|
}
|
|
265
288
|
catch (err) {
|
|
266
|
-
result.warnings.push(`Failed to write contradiction edge ${
|
|
289
|
+
result.warnings.push(`Failed to write contradiction edge ${loser.ref} -> ${winnerRef}: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
290
|
}
|
|
268
291
|
}
|
|
269
292
|
if (totalPairsChecked >= MAX_PAIRS_PER_RUN)
|
|
@@ -5,8 +5,8 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { makeAssetRef, parseAssetRef } from "../../../core/asset/asset-ref.js";
|
|
7
7
|
import { assembleAsset } from "../../../core/asset/asset-serialize.js";
|
|
8
|
-
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
9
|
-
import {
|
|
8
|
+
import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
9
|
+
import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
|
|
10
10
|
const DERIVED_SUFFIX = ".derived";
|
|
11
11
|
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
12
12
|
const records = collectDerivedMemories(stashDir, options.parentRef);
|
|
@@ -491,27 +491,27 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
|
491
491
|
};
|
|
492
492
|
}
|
|
493
493
|
function persistBeliefStateTransition(filePath, transition) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
nextFrontmatter.contradictedBy = [...currentBeliefRefs];
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
delete nextFrontmatter.contradictedBy;
|
|
506
|
-
if (parsed.data.supersededBy !== undefined && refArray(parsed.data.supersededBy).length === 0) {
|
|
507
|
-
delete nextFrontmatter.supersededBy;
|
|
494
|
+
mutateFrontmatter(filePath, (parsed) => {
|
|
495
|
+
const nextFrontmatter = {
|
|
496
|
+
...parsed.data,
|
|
497
|
+
beliefState: transition.toState,
|
|
498
|
+
};
|
|
499
|
+
const currentBeliefRefs = [...new Set(transition.currentBeliefRefs ?? [])].sort();
|
|
500
|
+
if (transition.toState === "contradicted") {
|
|
501
|
+
nextFrontmatter.contradictedBy = [...currentBeliefRefs];
|
|
508
502
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
503
|
+
else {
|
|
504
|
+
delete nextFrontmatter.contradictedBy;
|
|
505
|
+
if (parsed.data.supersededBy !== undefined && refArray(parsed.data.supersededBy).length === 0) {
|
|
506
|
+
delete nextFrontmatter.supersededBy;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (currentBeliefRefs.length > 0)
|
|
510
|
+
nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
|
|
511
|
+
else
|
|
512
|
+
delete nextFrontmatter.currentBeliefRefs;
|
|
513
|
+
return nextFrontmatter;
|
|
514
|
+
});
|
|
515
515
|
}
|
|
516
516
|
function appendBeliefStateTransitionLog(stashDir, transitions) {
|
|
517
517
|
const logDir = path.join(stashDir, ".akm", "memory-cleanup");
|
|
@@ -580,8 +580,8 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
|
|
|
580
580
|
continue;
|
|
581
581
|
if (!isDerivedMemory(name, parsed.data))
|
|
582
582
|
continue;
|
|
583
|
-
const title =
|
|
584
|
-
const description =
|
|
583
|
+
const title = asNonEmptyString(parsed.data.title) ?? extractHeading(parsed.content) ?? "";
|
|
584
|
+
const description = asNonEmptyString(parsed.data.description) ?? "";
|
|
585
585
|
const tags = stringArray(parsed.data.tags);
|
|
586
586
|
const searchHints = stringArray(parsed.data.searchHints);
|
|
587
587
|
const body = parsed.content.trim();
|
|
@@ -637,7 +637,7 @@ function isFrozenHistoricalBeliefState(state) {
|
|
|
637
637
|
return state === "deprecated";
|
|
638
638
|
}
|
|
639
639
|
function resolveBeliefState(frontmatter) {
|
|
640
|
-
const explicit =
|
|
640
|
+
const explicit = asNonEmptyString(frontmatter.beliefState);
|
|
641
641
|
if (explicit === "active" ||
|
|
642
642
|
explicit === "asserted" ||
|
|
643
643
|
explicit === "deprecated" ||
|
|
@@ -651,10 +651,10 @@ function isDerivedMemory(name, frontmatter) {
|
|
|
651
651
|
return frontmatter.inferred === true || name.endsWith(DERIVED_SUFFIX);
|
|
652
652
|
}
|
|
653
653
|
function resolveParentRef(name, frontmatter) {
|
|
654
|
-
const fromSource = parseMemoryRef(
|
|
654
|
+
const fromSource = parseMemoryRef(asNonEmptyString(frontmatter.source));
|
|
655
655
|
if (fromSource)
|
|
656
656
|
return fromSource;
|
|
657
|
-
const derivedFrom =
|
|
657
|
+
const derivedFrom = asNonEmptyString(frontmatter.derivedFrom);
|
|
658
658
|
if (derivedFrom)
|
|
659
659
|
return makeAssetRef("memory", derivedFrom);
|
|
660
660
|
if (name.endsWith(DERIVED_SUFFIX)) {
|
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* EMA decay factor for the expected-retrieval rolling mean (α).
|
|
7
|
+
* New expected = α × new_count + (1−α) × old_expected.
|
|
8
|
+
* At α = 0.3 the window is ≈ 3 cycles.
|
|
9
|
+
*/
|
|
10
|
+
export const OUTCOME_EMA_ALPHA = 0.3;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum K improve cycles for the eligibility-trace window. Retrievals older
|
|
13
|
+
* than K cycles contribute via the EMA decay naturally (they are already baked
|
|
14
|
+
* into `expected_retrieval_rate`).
|
|
15
|
+
*/
|
|
16
|
+
export const OUTCOME_TRACE_CYCLES = 5;
|
|
17
|
+
/**
|
|
18
|
+
* Warm-start cap: the maximum `outcome_score` a brand-new row can be seeded with
|
|
19
|
+
* (from the utility EMA). Prevents a `[0,1]`-range utility value from generating
|
|
20
|
+
* a score that the first negative delta could catastrophically invert. Clipped to
|
|
21
|
+
* the maximum plausible first differential update: `max(0, 1 − 0) = 1`, so cap
|
|
22
|
+
* at a modest 0.3 to avoid spurious rank-flip on first cycle.
|
|
23
|
+
*/
|
|
24
|
+
export const WARM_START_CAP = 0.3;
|
|
25
|
+
/**
|
|
26
|
+
* Penalty cap: the minimum outcome_score is capped at this value so a single
|
|
27
|
+
* very-negative run can't send the score to −∞.
|
|
28
|
+
*/
|
|
29
|
+
export const OUTCOME_SCORE_MIN = -1.0;
|
|
30
|
+
/**
|
|
31
|
+
* Saturation ceiling: the maximum outcome_score. Biological RPE saturates —
|
|
32
|
+
* a fully predicted reward produces zero response, not an ever-growing one —
|
|
33
|
+
* so a long-lived popular asset must not accrue unbounded outcome mass that
|
|
34
|
+
* would dominate ranking once the outcome weight is enabled (analysis G2).
|
|
35
|
+
* 1.5 comfortably exceeds the max plausible single-cycle raw update while
|
|
36
|
+
* keeping the normalised outcomeSalience spread meaningful.
|
|
37
|
+
*/
|
|
38
|
+
export const OUTCOME_SCORE_MAX = 1.5;
|
|
39
|
+
/**
|
|
40
|
+
* Diversity floor: `outcomeSalience` for any asset is at least this fraction
|
|
41
|
+
* of the maximum observed `outcome_score` in the table, so rare-but-correct
|
|
42
|
+
* assets cannot be permanently outcompeted. 0 = disabled (pure competition).
|
|
43
|
+
*/
|
|
44
|
+
export const DIVERSITY_FLOOR_FRACTION = 0.1;
|
|
45
|
+
/**
|
|
46
|
+
* Default review_pressure increment per negative-feedback retrieval cycle.
|
|
47
|
+
* Each cycle where negative_feedback_count > 0, pressure increases by this
|
|
48
|
+
* much (and decreases by REVIEW_PRESSURE_DECAY if no new negatives).
|
|
49
|
+
*/
|
|
50
|
+
export const REVIEW_PRESSURE_INCREMENT = 1;
|
|
51
|
+
/**
|
|
52
|
+
* Decay per non-negative cycle. Keeps review_pressure from accumulating forever
|
|
53
|
+
* when an asset's quality problems are addressed and feedback turns positive.
|
|
54
|
+
*/
|
|
55
|
+
export const REVIEW_PRESSURE_DECAY = 1;
|
|
56
|
+
/**
|
|
57
|
+
* Upsert one asset's outcome row.
|
|
58
|
+
*
|
|
59
|
+
* On first call for a ref (no prior row): warm-starts from `utilityScore`.
|
|
60
|
+
* On subsequent calls: applies the differential update formula.
|
|
61
|
+
*
|
|
62
|
+
* Returns the resulting `outcome_score` and `review_pressure` so the caller
|
|
63
|
+
* can pass them to `computeSalience` without a second read.
|
|
64
|
+
*/
|
|
65
|
+
export function updateAssetOutcome(db, inputs) {
|
|
66
|
+
const now = inputs.now ?? Date.now();
|
|
67
|
+
const valence = inputs.valence ?? 0;
|
|
68
|
+
// Fetch existing row (or undefined for first insert).
|
|
69
|
+
const existing = getAssetOutcome(db, inputs.ref);
|
|
70
|
+
const isNewRow = existing === undefined;
|
|
71
|
+
let outcomeScore;
|
|
72
|
+
let reviewPressure;
|
|
73
|
+
let expectedRetrievalRate;
|
|
74
|
+
if (isNewRow) {
|
|
75
|
+
// ── Warm-start ─────────────────────────────────────────────────────────
|
|
76
|
+
// Seed from utility EMA, clipped to [0, WARM_START_CAP].
|
|
77
|
+
// The warm-start is a non-negative seed; the first real differential update
|
|
78
|
+
// will adjust it up or down from there.
|
|
79
|
+
const seedScore = Math.min(WARM_START_CAP, Math.max(0, inputs.utilityScore ?? 0));
|
|
80
|
+
outcomeScore = seedScore;
|
|
81
|
+
reviewPressure = 0;
|
|
82
|
+
// Seed expected_retrieval_rate = 0 (no delta history yet).
|
|
83
|
+
// Seeding with currentRetrievalCount would produce a large spurious negative
|
|
84
|
+
// prediction error on the first real cycle (delta ≪ cumulative count).
|
|
85
|
+
expectedRetrievalRate = 0;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// ── Differential update ────────────────────────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// retrieval_delta = current − stored (non-negative — we never go backwards)
|
|
91
|
+
const retrievalDelta = Math.max(0, inputs.currentRetrievalCount - existing.retrieval_count);
|
|
92
|
+
// Differential prediction-error term:
|
|
93
|
+
// outcome = (retrieval_delta − expected_delta) + valence
|
|
94
|
+
//
|
|
95
|
+
// Prediction error is computed against the PRIOR stored EMA (before folding
|
|
96
|
+
// in this cycle's observation), so the current delta cannot leak into its own
|
|
97
|
+
// expectation. Negative values are intentional — they signal below-average cycles.
|
|
98
|
+
const expectedDelta = existing.expected_retrieval_rate;
|
|
99
|
+
const predictionError = retrievalDelta - expectedDelta;
|
|
100
|
+
// Advance the EMA over the OBSERVED delta (not the cumulative count).
|
|
101
|
+
// expected' = α × delta + (1−α) × prior_expected
|
|
102
|
+
expectedRetrievalRate =
|
|
103
|
+
OUTCOME_EMA_ALPHA * retrievalDelta + (1 - OUTCOME_EMA_ALPHA) * existing.expected_retrieval_rate;
|
|
104
|
+
// Running sum (EMA approach): new score = α × update + (1−α) × old
|
|
105
|
+
// so the score tracks the moving signal, not the cumulative sum.
|
|
106
|
+
const rawUpdate = predictionError + valence;
|
|
107
|
+
const newScore = OUTCOME_EMA_ALPHA * rawUpdate + (1 - OUTCOME_EMA_ALPHA) * existing.outcome_score;
|
|
108
|
+
// Clip to [OUTCOME_SCORE_MIN, OUTCOME_SCORE_MAX] — the ceiling is the RPE
|
|
109
|
+
// saturation analog (G2): without it, long-lived popular assets accumulate
|
|
110
|
+
// unbounded positive mass (live max was 3.13) and would dominate rank_score
|
|
111
|
+
// the moment the outcome weight is enabled. Stored legacy scores above the
|
|
112
|
+
// ceiling converge back under it on their next differential update.
|
|
113
|
+
outcomeScore = Math.min(OUTCOME_SCORE_MAX, Math.max(OUTCOME_SCORE_MIN, newScore));
|
|
114
|
+
// ── review_pressure (#613) ─────────────────────────────────────────────
|
|
115
|
+
// New negatives this cycle.
|
|
116
|
+
const newNegatives = Math.max(0, inputs.negativeFeedbackCount - existing.negative_feedback_count);
|
|
117
|
+
if (newNegatives > 0) {
|
|
118
|
+
reviewPressure = Math.min(10, existing.review_pressure + REVIEW_PRESSURE_INCREMENT * newNegatives);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Decay when no new negatives.
|
|
122
|
+
reviewPressure = Math.max(0, existing.review_pressure - REVIEW_PRESSURE_DECAY);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Upsert the row.
|
|
126
|
+
db.prepare(`INSERT INTO asset_outcome
|
|
127
|
+
(asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
|
|
128
|
+
negative_feedback_count, accepted_change_count, review_pressure,
|
|
129
|
+
outcome_score, updated_at)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
131
|
+
ON CONFLICT(asset_ref) DO UPDATE SET
|
|
132
|
+
last_retrieved_at = excluded.last_retrieved_at,
|
|
133
|
+
retrieval_count = excluded.retrieval_count,
|
|
134
|
+
expected_retrieval_rate= excluded.expected_retrieval_rate,
|
|
135
|
+
negative_feedback_count= excluded.negative_feedback_count,
|
|
136
|
+
accepted_change_count = excluded.accepted_change_count,
|
|
137
|
+
review_pressure = excluded.review_pressure,
|
|
138
|
+
outcome_score = excluded.outcome_score,
|
|
139
|
+
updated_at = excluded.updated_at`).run(inputs.ref, inputs.lastRetrievedAt, inputs.currentRetrievalCount, expectedRetrievalRate, inputs.negativeFeedbackCount, inputs.acceptedChangeCount, reviewPressure, outcomeScore, now);
|
|
140
|
+
return { outcomeScore, reviewPressure, isNewRow };
|
|
141
|
+
}
|
|
142
|
+
// ── Reader ────────────────────────────────────────────────────────────────────
|
|
143
|
+
/**
|
|
144
|
+
* Load the outcome row for one asset, or `undefined` if not yet written.
|
|
145
|
+
*/
|
|
146
|
+
export function getAssetOutcome(db, ref) {
|
|
147
|
+
const row = db
|
|
148
|
+
.prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
|
|
149
|
+
negative_feedback_count, accepted_change_count, review_pressure,
|
|
150
|
+
outcome_score, updated_at
|
|
151
|
+
FROM asset_outcome WHERE asset_ref = ?`)
|
|
152
|
+
.get(ref);
|
|
153
|
+
return row == null ? undefined : row;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Load ALL asset_outcome rows. Used for the proxy-adequacy tripwire computation.
|
|
157
|
+
*/
|
|
158
|
+
export function getAllAssetOutcomes(db) {
|
|
159
|
+
return db
|
|
160
|
+
.prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
|
|
161
|
+
negative_feedback_count, accepted_change_count, review_pressure,
|
|
162
|
+
outcome_score, updated_at
|
|
163
|
+
FROM asset_outcome ORDER BY asset_ref`)
|
|
164
|
+
.all();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build a Map<ref, outcome_score> for a set of refs in one query.
|
|
168
|
+
* Used by `salience.ts` to populate `outcomeSalience`.
|
|
169
|
+
*/
|
|
170
|
+
export function getOutcomeScoresByRef(db, refs) {
|
|
171
|
+
const result = new Map();
|
|
172
|
+
if (refs.length === 0)
|
|
173
|
+
return result;
|
|
174
|
+
const CHUNK = 500;
|
|
175
|
+
for (let i = 0; i < refs.length; i += CHUNK) {
|
|
176
|
+
const chunk = refs.slice(i, i + CHUNK);
|
|
177
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
178
|
+
const rows = db
|
|
179
|
+
.prepare(`SELECT asset_ref, outcome_score FROM asset_outcome WHERE asset_ref IN (${placeholders})`)
|
|
180
|
+
.all(...chunk);
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
result.set(row.asset_ref, row.outcome_score);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
// ── outcomeSalience projection ────────────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Convert a raw `outcome_score` (differential, may be negative) to a
|
|
190
|
+
* `outcomeSalience` value in [0, 1] suitable for use in the salience projection.
|
|
191
|
+
*
|
|
192
|
+
* Approach:
|
|
193
|
+
* 1. Clip negative scores to 0 (a negative outcome just means "below expected",
|
|
194
|
+
* not "irrelevant"; it should not harm the base retrieval/encoding ranking).
|
|
195
|
+
* 2. Apply the diversity floor so rare-but-correct assets always retain a
|
|
196
|
+
* minimum `outcomeSalience` relative to the stash-wide maximum.
|
|
197
|
+
* 3. Normalise by `maxScore` (the stash-wide max outcome_score) so the term
|
|
198
|
+
* lives in [0, 1]. When maxScore ≤ 0 (all seeds, nothing observed yet),
|
|
199
|
+
* return the floor (or 0 if DIVERSITY_FLOOR_FRACTION = 0).
|
|
200
|
+
*
|
|
201
|
+
* @param outcomeScore - Raw outcome_score from asset_outcome.
|
|
202
|
+
* @param maxScore - Maximum outcome_score across ALL rows (≥ 0 after clip).
|
|
203
|
+
* Callers must compute this once per batch and pass it in.
|
|
204
|
+
*/
|
|
205
|
+
export function outcomeScoreToSalience(outcomeScore, maxScore) {
|
|
206
|
+
const clipped = Math.max(0, outcomeScore);
|
|
207
|
+
if (maxScore <= 0) {
|
|
208
|
+
// No positive scores observed yet — return diversity floor.
|
|
209
|
+
return DIVERSITY_FLOOR_FRACTION;
|
|
210
|
+
}
|
|
211
|
+
const normalised = clipped / maxScore;
|
|
212
|
+
// Apply diversity floor.
|
|
213
|
+
return Math.max(DIVERSITY_FLOOR_FRACTION, normalised);
|
|
214
|
+
}
|
|
215
|
+
// ── Proxy-adequacy tripwire ───────────────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Dead-proxy threshold: |corr| below this means outcome_score carries no
|
|
218
|
+
* information about improvement need (pure noise).
|
|
219
|
+
*/
|
|
220
|
+
export const PROXY_DEAD_CORR_THRESHOLD = 0.1;
|
|
221
|
+
/**
|
|
222
|
+
* Minimum sample size before the dead-proxy check fires. Below this, a
|
|
223
|
+
* near-zero correlation is indistinguishable from small-sample noise.
|
|
224
|
+
*/
|
|
225
|
+
export const PROXY_DEAD_MIN_N = 500;
|
|
226
|
+
/**
|
|
227
|
+
* Compute `corr(outcome_score, accepted_change_rate)` across all asset_outcome
|
|
228
|
+
* rows. Returns `{correlation: NaN, n, isInverted: false}` when there is
|
|
229
|
+
* insufficient data (fewer than 3 rows or zero variance in either variable).
|
|
230
|
+
*
|
|
231
|
+
* A NEGATIVE correlation means: assets with a HIGH outcome_score have a LOW
|
|
232
|
+
* accepted_change_rate — i.e. the assets the proxy rates as "doing well"
|
|
233
|
+
* (frequently retrieved) are precisely the ones that rarely yield an accepted
|
|
234
|
+
* improvement. That inverts the proxy: high outcome_score ≠ "doing well", so the
|
|
235
|
+
* coarse retrieval-delta signal is no longer trustworthy and the 0.10+ rich
|
|
236
|
+
* signal is due. (`isInverted = correlation < -0.3`.)
|
|
237
|
+
*/
|
|
238
|
+
export function computeProxyAdequacy(rows) {
|
|
239
|
+
const n = rows.length;
|
|
240
|
+
if (n < 3)
|
|
241
|
+
return { correlation: Number.NaN, n, isInverted: false, isDead: false };
|
|
242
|
+
// accepted_change_rate per row.
|
|
243
|
+
const xs = rows.map((r) => r.outcome_score);
|
|
244
|
+
const ys = rows.map((r) => r.accepted_change_count / Math.max(1, r.retrieval_count));
|
|
245
|
+
const meanX = xs.reduce((a, b) => a + b, 0) / n;
|
|
246
|
+
const meanY = ys.reduce((a, b) => a + b, 0) / n;
|
|
247
|
+
let covXY = 0;
|
|
248
|
+
let varX = 0;
|
|
249
|
+
let varY = 0;
|
|
250
|
+
for (let i = 0; i < n; i++) {
|
|
251
|
+
const dx = xs[i] - meanX;
|
|
252
|
+
const dy = ys[i] - meanY;
|
|
253
|
+
covXY += dx * dy;
|
|
254
|
+
varX += dx * dx;
|
|
255
|
+
varY += dy * dy;
|
|
256
|
+
}
|
|
257
|
+
covXY /= n;
|
|
258
|
+
varX /= n;
|
|
259
|
+
varY /= n;
|
|
260
|
+
const denom = Math.sqrt(varX) * Math.sqrt(varY);
|
|
261
|
+
if (denom < 1e-12)
|
|
262
|
+
return { correlation: Number.NaN, n, isInverted: false, isDead: false };
|
|
263
|
+
const correlation = covXY / denom;
|
|
264
|
+
// Inverted proxy: negative correlation between outcome and accepted_change_rate
|
|
265
|
+
// means high-outcome assets are also high-need — the opposite of "useful".
|
|
266
|
+
const isInverted = correlation < -0.3;
|
|
267
|
+
// Dead proxy: near-zero correlation at scale — the score is noise.
|
|
268
|
+
const isDead = n >= PROXY_DEAD_MIN_N && Math.abs(correlation) < PROXY_DEAD_CORR_THRESHOLD;
|
|
269
|
+
return { correlation, n, isInverted, isDead };
|
|
270
|
+
}
|