akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4
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 +731 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +361 -751
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -441
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -1,432 +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
|
-
* R5 — Longitudinal collapse detector
|
|
6
|
-
* (docs/architecture/specs/improve-collapse-churn-detector-design.md).
|
|
7
|
-
*
|
|
8
|
-
* Detects the measured collapse failure mode of LLM-consolidated memory
|
|
9
|
-
* stores: repeated merges destroy information — canary retrieval recall
|
|
10
|
-
* downtrends, distinct-content entropy downtrends, or the store shrinks
|
|
11
|
-
* while generation counts rise.
|
|
12
|
-
*
|
|
13
|
-
* The CHURN alert class described in the design doc was removed: production
|
|
14
|
-
* never threaded a real accepted-change-volume signal in (the value was a
|
|
15
|
-
* hardcoded 0 from the 0.9.0 confidence-gate deletion onward), so the alert
|
|
16
|
-
* could never fire. See the design doc's status note for the removal
|
|
17
|
-
* rationale. `improve_cycle_metrics.accepted_actions` stays in the schema and
|
|
18
|
-
* is written as 0 because deployed `state.db` files already contain it and
|
|
19
|
-
* removing it would be a destructive schema change.
|
|
20
|
-
*
|
|
21
|
-
* Hard invariants: deterministic only (FTS BM25 + hashing — never an LLM,
|
|
22
|
-
* never an embedding model); bounded storage (< 2 KB per qualifying cycle,
|
|
23
|
-
* 365-day retention); fail-open (an error warns and skips, never breaks an
|
|
24
|
-
* improve run); runs only on cycles where consolidate did work.
|
|
25
|
-
*
|
|
26
|
-
* Observe-only in v1: alerts land in `improve_cycle_metrics.alerts_json`, the
|
|
27
|
-
* events log (`collapse_detector_alert`), and the `akm health` advisory —
|
|
28
|
-
* nothing is ever blocked.
|
|
29
|
-
*
|
|
30
|
-
* @module collapse-detector
|
|
31
|
-
*/
|
|
32
|
-
import { randomBytes } from "node:crypto";
|
|
33
|
-
import { parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
34
|
-
import { conceptIdFromTypeName } from "../../core/asset/resolve-ref.js";
|
|
35
|
-
import { getImproveProcessConfig } from "../../core/config/config.js";
|
|
36
|
-
import { appendEvent } from "../../core/events.js";
|
|
37
|
-
import { withStateDb } from "../../core/state-db.js";
|
|
38
|
-
import { warn } from "../../core/warn.js";
|
|
39
|
-
import { deactivateCanarySet, getActiveCanaries, getCanariesBySetId, insertCanaries, insertCycleMetrics, listActiveCanarySetIds, queryRecentCycleMetrics, } from "../../storage/repositories/canaries-repository.js";
|
|
40
|
-
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
41
|
-
import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
|
|
42
|
-
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
43
|
-
import { computeBigramDiversity, DEFAULT_MAX_GENERATION } from "./anti-collapse.js";
|
|
44
|
-
import { getAllRankScores } from "./salience.js";
|
|
45
|
-
// ── Defaults (mirrored in config-schema.ts ImproveCollapseDetectorSchema) ────
|
|
46
|
-
export const DEFAULT_CANARY_COUNT = 40; // owner-approved 30–50 range
|
|
47
|
-
export const DEFAULT_CANARY_K = 10;
|
|
48
|
-
export const DEFAULT_WINDOW_CYCLES = 5;
|
|
49
|
-
export const DEFAULT_RECALL_DROP_THRESHOLD = 0.15;
|
|
50
|
-
export const DEFAULT_ENTROPY_DROP_THRESHOLD = 0.05;
|
|
51
|
-
export const DEFAULT_RETENTION_DAYS = 365;
|
|
52
|
-
/** Deterministic bigram-diversity sample cap (cost bound at 10k assets). */
|
|
53
|
-
const DIVERSITY_SAMPLE_CAP = 2000;
|
|
54
|
-
/**
|
|
55
|
-
* Minimum merge-floor violations in one cycle before the advisory alert fires.
|
|
56
|
-
* The specificity floor is deliberately strict (Phase-1 tuning pending), so a
|
|
57
|
-
* couple of borderline merges per cycle must not flip `akm health` to warn —
|
|
58
|
-
* that alert fatigue would drown the real collapse signals.
|
|
59
|
-
*/
|
|
60
|
-
const MERGE_FLOOR_ALERT_MIN = 3;
|
|
61
|
-
/** The learning-store types the detector measures. */
|
|
62
|
-
const LEARNING_TYPES = new Set(["memory", "lesson", "knowledge"]);
|
|
63
|
-
// ── Canary set ────────────────────────────────────────────────────────────────
|
|
64
|
-
/** Deterministic query string for one anchor entry: name tokens + top tags + description head. */
|
|
65
|
-
function buildCanaryQuery(entry) {
|
|
66
|
-
const nameTokens = entry.entry.name.split(/[-_/.]+/).filter((t) => t.length > 1);
|
|
67
|
-
const tags = (entry.entry.tags ?? []).slice(0, 3);
|
|
68
|
-
const descriptionHead = (entry.entry.description ?? "").split(/\s+/).slice(0, 6);
|
|
69
|
-
const parts = [...nameTokens, ...tags, ...descriptionHead].filter((t) => t.length > 0);
|
|
70
|
-
return [...new Set(parts)].join(" ");
|
|
71
|
-
}
|
|
72
|
-
/** Build the mint candidate list (deterministic given index + salience tables). */
|
|
73
|
-
function buildMintList(stateDb, entries, cfg) {
|
|
74
|
-
const canaryCount = cfg.canaryCount ?? DEFAULT_CANARY_COUNT;
|
|
75
|
-
const rankScores = getAllRankScores(stateDb);
|
|
76
|
-
// Chunk-8 WI-8.5c: the canary anchor + salience correlation both key on the
|
|
77
|
-
// SHORT conceptId (`<stash-subdir>/<name>`, D-R2) — the same spelling the
|
|
78
|
-
// improve candidate refs and content xrefs now carry (post WI-8.5a).
|
|
79
|
-
const candidates = entries
|
|
80
|
-
.filter((e) => LEARNING_TYPES.has(e.entry.type))
|
|
81
|
-
.map((e) => {
|
|
82
|
-
const ref = conceptIdFromTypeName(e.entry.type, e.entry.name);
|
|
83
|
-
return { e, ref, score: rankScores.get(ref) ?? 0 };
|
|
84
|
-
})
|
|
85
|
-
.sort((a, b) => b.score - a.score || (a.ref < b.ref ? -1 : 1));
|
|
86
|
-
// Type-stratified top slice: ⅓ per learning type, backfill from global order.
|
|
87
|
-
const perType = Math.ceil(canaryCount / 3);
|
|
88
|
-
const picked = new Map();
|
|
89
|
-
for (const type of LEARNING_TYPES) {
|
|
90
|
-
let taken = 0;
|
|
91
|
-
for (const c of candidates) {
|
|
92
|
-
if (taken >= perType || picked.size >= canaryCount)
|
|
93
|
-
break;
|
|
94
|
-
if (c.e.entry.type === type && !picked.has(c.ref)) {
|
|
95
|
-
picked.set(c.ref, c);
|
|
96
|
-
taken++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
for (const c of candidates) {
|
|
101
|
-
if (picked.size >= canaryCount)
|
|
102
|
-
break;
|
|
103
|
-
if (!picked.has(c.ref))
|
|
104
|
-
picked.set(c.ref, c);
|
|
105
|
-
}
|
|
106
|
-
return [...picked.values()]
|
|
107
|
-
.map((c) => ({ anchorRef: c.ref, query: buildCanaryQuery(c.e) }))
|
|
108
|
-
.filter((c) => c.query.length > 0);
|
|
109
|
-
}
|
|
110
|
-
/** Collision-safe mint token (same-millisecond mints happen in tests + concurrent runs). */
|
|
111
|
-
function newCanarySetId() {
|
|
112
|
-
return `canary-${Date.now().toString(36)}-${randomBytes(2).toString("hex")}`;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Mint (or return) the active canary set. Deterministic given the index +
|
|
116
|
-
* salience tables: rank the three learning types by `asset_salience.rank_score`
|
|
117
|
-
* (fallback 0, tie-broken by ref), take a type-stratified top slice
|
|
118
|
-
* (⅓ per type, backfilled from the global ranking when a type is short).
|
|
119
|
-
*
|
|
120
|
-
* Returns `null` when the index has no mintable learning entries — a cycle
|
|
121
|
-
* with no canary set is NOT recorded (a fresh unused set id every cycle would
|
|
122
|
-
* mean the trend window never fills and recall reads as a fake 0).
|
|
123
|
-
*
|
|
124
|
-
* NEVER auto-refreshes: once minted the set is frozen until an explicit
|
|
125
|
-
* `bun scripts/refresh-canary-set.ts --refresh` — silent re-baselining is how
|
|
126
|
-
* a slow collapse hides. Rows are read back BY OUR OWN set id (never "newest
|
|
127
|
-
* active") so a concurrent mint in another process cannot relabel this run's
|
|
128
|
-
* metrics.
|
|
129
|
-
*/
|
|
130
|
-
export function ensureCanarySet(stateDb, indexDb, cfg, preloadedEntries) {
|
|
131
|
-
const existing = getActiveCanaries(stateDb);
|
|
132
|
-
if (existing.length > 0) {
|
|
133
|
-
return { canarySetId: existing[0].canary_set_id, canaries: existing };
|
|
134
|
-
}
|
|
135
|
-
const minted = buildMintList(stateDb, preloadedEntries ?? getAllEntries(indexDb), cfg);
|
|
136
|
-
if (minted.length === 0)
|
|
137
|
-
return null;
|
|
138
|
-
const canarySetId = newCanarySetId();
|
|
139
|
-
insertCanaries(stateDb, canarySetId, minted);
|
|
140
|
-
return { canarySetId, canaries: getCanariesBySetId(stateDb, canarySetId) };
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Explicit canary re-mint (the ONLY refresh path —
|
|
144
|
-
* `bun scripts/refresh-canary-set.ts --refresh`). Mint-first, deactivate-after:
|
|
145
|
-
* when the index is empty or unreadable the current baseline is left untouched
|
|
146
|
-
* instead of destroyed. Deactivates ALL other active sets (not just the
|
|
147
|
-
* newest) so stragglers from an interrupted refresh can never resurrect.
|
|
148
|
-
*/
|
|
149
|
-
export function refreshCanarySet(stateDb, indexDb, cfg) {
|
|
150
|
-
const minted = buildMintList(stateDb, getAllEntries(indexDb), cfg);
|
|
151
|
-
if (minted.length === 0)
|
|
152
|
-
return null; // nothing mintable — keep the old baseline
|
|
153
|
-
const canarySetId = newCanarySetId();
|
|
154
|
-
insertCanaries(stateDb, canarySetId, minted);
|
|
155
|
-
for (const oldSetId of listActiveCanarySetIds(stateDb)) {
|
|
156
|
-
if (oldSetId !== canarySetId)
|
|
157
|
-
deactivateCanarySet(stateDb, oldSetId);
|
|
158
|
-
}
|
|
159
|
-
return { canarySetId, canaries: getCanariesBySetId(stateDb, canarySetId) };
|
|
160
|
-
}
|
|
161
|
-
// ── Cycle metrics ─────────────────────────────────────────────────────────────
|
|
162
|
-
/**
|
|
163
|
-
* Name-free content fingerprint text for entropy metrics. The indexed
|
|
164
|
-
* search_text EMBEDS the (unique) entry name, which would pin the
|
|
165
|
-
* distinct-content ratio at 1.0 forever; convergence shows up in the
|
|
166
|
-
* description/tags/heading fields, so those are what get hashed. Bounded body
|
|
167
|
-
* content is searchable but deliberately excluded from this fingerprint: the
|
|
168
|
-
* detector measures the structured surface generic merged assets converge on.
|
|
169
|
-
*/
|
|
170
|
-
function contentFingerprint(entry) {
|
|
171
|
-
const parts = [entry.description ?? "", (entry.tags ?? []).join(" "), (entry.toc ?? []).map((h) => h.text).join(" ")];
|
|
172
|
-
return parts.filter((t) => t.length > 0).join(" ");
|
|
173
|
-
}
|
|
174
|
-
/** FNV-1a 64-bit over lowercased whitespace-collapsed text (distinct-content hashing). */
|
|
175
|
-
export function normHash(text) {
|
|
176
|
-
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
177
|
-
let hash = 0xcbf29ce484222325n;
|
|
178
|
-
const prime = 0x100000001b3n;
|
|
179
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
180
|
-
hash ^= BigInt(normalized.charCodeAt(i));
|
|
181
|
-
hash = (hash * prime) & 0xffffffffffffffffn;
|
|
182
|
-
}
|
|
183
|
-
return hash.toString(16);
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Score one canary against the live index, merge-following via canonical
|
|
187
|
-
* `xrefs`. A hit is the anchor ref itself or any returned entry whose
|
|
188
|
-
* provenance contains the anchor.
|
|
189
|
-
* Returns the 0-based rank of the first hit, or -1.
|
|
190
|
-
*/
|
|
191
|
-
function scoreCanary(indexDb, canary, k) {
|
|
192
|
-
const results = searchFts(indexDb, canary.query, k);
|
|
193
|
-
const anchorConceptId = canaryConceptId(canary.anchor_ref);
|
|
194
|
-
for (let i = 0; i < Math.min(results.length, k); i++) {
|
|
195
|
-
const r = results[i];
|
|
196
|
-
// Persisted canaries may be fully qualified after a state migration while
|
|
197
|
-
// index refs and xrefs remain short conceptIds. Compare identity, not display
|
|
198
|
-
// spelling, so migration alone cannot collapse recall to zero.
|
|
199
|
-
const ref = conceptIdFromTypeName(r.entry.type, r.entry.name);
|
|
200
|
-
if (ref === anchorConceptId)
|
|
201
|
-
return i;
|
|
202
|
-
const provenance = r.entry.xrefs ?? [];
|
|
203
|
-
if (provenance.some((sourceRef) => canaryConceptId(sourceRef) === anchorConceptId))
|
|
204
|
-
return i;
|
|
205
|
-
}
|
|
206
|
-
return -1;
|
|
207
|
-
}
|
|
208
|
-
function canaryConceptId(ref) {
|
|
209
|
-
try {
|
|
210
|
-
return parseBundleRef(ref).conceptId;
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return ref;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Compute one qualifying cycle's store-health snapshot. One `entries` scan +
|
|
218
|
-
* `canaryCount` FTS queries; no LLM, no embedding model, no filesystem reads.
|
|
219
|
-
* Returns `null` when no canary set exists AND none is mintable (empty index)
|
|
220
|
-
* — such a cycle is not measurable and must not be recorded.
|
|
221
|
-
*/
|
|
222
|
-
export function computeCycleMetrics(stateDb, indexDb, args) {
|
|
223
|
-
const k = args.cfg.k ?? DEFAULT_CANARY_K;
|
|
224
|
-
const maxGeneration = args.maxGeneration ?? DEFAULT_MAX_GENERATION;
|
|
225
|
-
// Single entries scan — shared by the canary mint (if one is needed) and
|
|
226
|
-
// the store-shape metrics below.
|
|
227
|
-
const all = getAllEntries(indexDb);
|
|
228
|
-
const canarySet = ensureCanarySet(stateDb, indexDb, args.cfg, all);
|
|
229
|
-
if (canarySet === null)
|
|
230
|
-
return null;
|
|
231
|
-
const { canarySetId, canaries } = canarySet;
|
|
232
|
-
// ── Canary retrieval metrics ───────────────────────────────────────────────
|
|
233
|
-
const ranks = [];
|
|
234
|
-
let recallSum = 0;
|
|
235
|
-
let ndcgSum = 0;
|
|
236
|
-
let mrrSum = 0;
|
|
237
|
-
for (const canary of canaries) {
|
|
238
|
-
const rank = scoreCanary(indexDb, canary, k);
|
|
239
|
-
ranks.push([canary.id, rank]);
|
|
240
|
-
if (rank >= 0) {
|
|
241
|
-
recallSum += 1;
|
|
242
|
-
mrrSum += 1 / (rank + 1);
|
|
243
|
-
// Single-relevant nDCG@k closed form: ideal DCG is 1, so the score is
|
|
244
|
-
// just the discount at the hit rank.
|
|
245
|
-
ndcgSum += 1 / Math.log2(rank + 2);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const n = Math.max(1, canaries.length);
|
|
249
|
-
// ── Store-shape metrics (same single entries scan) ────────────────────────
|
|
250
|
-
const byType = new Map();
|
|
251
|
-
const contentHashes = new Set();
|
|
252
|
-
let learningTotal = 0;
|
|
253
|
-
let overGeneration = 0;
|
|
254
|
-
const learningTexts = [];
|
|
255
|
-
for (const e of all) {
|
|
256
|
-
byType.set(e.entry.type, (byType.get(e.entry.type) ?? 0) + 1);
|
|
257
|
-
if (!LEARNING_TYPES.has(e.entry.type))
|
|
258
|
-
continue;
|
|
259
|
-
learningTotal++;
|
|
260
|
-
const fingerprint = contentFingerprint(e.entry);
|
|
261
|
-
contentHashes.add(normHash(fingerprint));
|
|
262
|
-
if ((e.entry.generation ?? 0) > maxGeneration)
|
|
263
|
-
overGeneration++;
|
|
264
|
-
learningTexts.push({ key: e.itemRef, text: fingerprint });
|
|
265
|
-
}
|
|
266
|
-
// Deterministic diversity sample: sort by item ref, take every ⌈N/cap⌉-th row.
|
|
267
|
-
learningTexts.sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
268
|
-
const step = Math.max(1, Math.ceil(learningTexts.length / DIVERSITY_SAMPLE_CAP));
|
|
269
|
-
let diversitySum = 0;
|
|
270
|
-
let diversityCount = 0;
|
|
271
|
-
for (let i = 0; i < learningTexts.length; i += step) {
|
|
272
|
-
diversitySum += computeBigramDiversity(learningTexts[i].text);
|
|
273
|
-
diversityCount++;
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
run_id: args.runId,
|
|
277
|
-
ts: (args.now ?? new Date()).toISOString(),
|
|
278
|
-
pass: args.pass,
|
|
279
|
-
canary_set_id: canarySetId,
|
|
280
|
-
mean_recall: recallSum / n,
|
|
281
|
-
mean_ndcg: ndcgSum / n,
|
|
282
|
-
mean_mrr: mrrSum / n,
|
|
283
|
-
canary_ranks_json: JSON.stringify(ranks),
|
|
284
|
-
store_total: learningTotal,
|
|
285
|
-
store_by_type_json: JSON.stringify(Object.fromEntries([...byType.entries()].sort())),
|
|
286
|
-
distinct_content_ratio: learningTotal === 0 ? 1 : contentHashes.size / learningTotal,
|
|
287
|
-
mean_bigram_diversity: diversityCount === 0 ? 1 : diversitySum / diversityCount,
|
|
288
|
-
over_generation_count: overGeneration,
|
|
289
|
-
// Always 0 — the CHURN alert this fed was removed. The column is NOT NULL
|
|
290
|
-
// in a released migration body, so it is written rather than dropped.
|
|
291
|
-
accepted_actions: 0,
|
|
292
|
-
merge_floor_violations: args.mergeFloorViolations,
|
|
293
|
-
alerts_json: "[]",
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
// ── Alert evaluation (pure) ───────────────────────────────────────────────────
|
|
297
|
-
function median(values) {
|
|
298
|
-
if (values.length === 0)
|
|
299
|
-
return Number.NaN;
|
|
300
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
301
|
-
const mid = Math.floor(sorted.length / 2);
|
|
302
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Evaluate the §1 alert definitions. PURE — history rows (oldest-first, NOT
|
|
306
|
-
* including `current`) plus the current row in, alerts out. A window shorter
|
|
307
|
-
* than `windowCycles` never fires (no baseline yet); the merge-floor advisory
|
|
308
|
-
* is per-cycle and fires regardless of window depth.
|
|
309
|
-
*/
|
|
310
|
-
export function evaluateCollapseAlerts(history, current, cfg) {
|
|
311
|
-
const alerts = [];
|
|
312
|
-
// MERGE-FLOOR advisory: per-cycle, window-independent. Gated on a minimum
|
|
313
|
-
// count — the specificity floor is deliberately strict pre-tuning, and one
|
|
314
|
-
// or two borderline merges per cycle must not generate alert fatigue.
|
|
315
|
-
if (current.merge_floor_violations >= MERGE_FLOOR_ALERT_MIN) {
|
|
316
|
-
alerts.push({
|
|
317
|
-
kind: "merge-floor",
|
|
318
|
-
detail: `${current.merge_floor_violations} merge(s) failed the information floor this cycle (provenance shrank or specificity below threshold)`,
|
|
319
|
-
metrics: { mergeFloorViolations: current.merge_floor_violations },
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
const W = cfg.windowCycles ?? DEFAULT_WINDOW_CYCLES;
|
|
323
|
-
const hist = history.slice(-W);
|
|
324
|
-
if (hist.length < W)
|
|
325
|
-
return alerts; // no baseline yet
|
|
326
|
-
// Window is non-empty here: `windowCycles` is schema-bounded to ≥2 (default 5),
|
|
327
|
-
// so `hist.length >= W >= 2`. `windowStart` is the oldest row in the window.
|
|
328
|
-
const windowStart = hist[0];
|
|
329
|
-
const recallDrop = cfg.recallDropThreshold ?? DEFAULT_RECALL_DROP_THRESHOLD;
|
|
330
|
-
const entropyDrop = cfg.entropyDropThreshold ?? DEFAULT_ENTROPY_DROP_THRESHOLD;
|
|
331
|
-
// COLLAPSE 1 — canary recall drop vs window median (median, not previous
|
|
332
|
-
// cycle, so one noisy cycle can neither fire nor mask the alert).
|
|
333
|
-
const medianRecall = median(hist.map((h) => h.mean_recall));
|
|
334
|
-
if (current.mean_recall <= medianRecall - recallDrop) {
|
|
335
|
-
alerts.push({
|
|
336
|
-
kind: "collapse-recall",
|
|
337
|
-
detail: `mean canary recall ${current.mean_recall.toFixed(3)} dropped ≥${recallDrop} below the ${W}-cycle median ${medianRecall.toFixed(3)}`,
|
|
338
|
-
metrics: { currentRecall: current.mean_recall, medianRecall, threshold: recallDrop },
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
// COLLAPSE 2 — monotonic distinct-content-ratio decline over the window.
|
|
342
|
-
const series = [...hist.map((h) => h.distinct_content_ratio), current.distinct_content_ratio];
|
|
343
|
-
const monotonicNonIncreasing = series.every((v, i) => i === 0 || v <= series[i - 1]);
|
|
344
|
-
const totalDecline = windowStart.distinct_content_ratio - current.distinct_content_ratio;
|
|
345
|
-
if (monotonicNonIncreasing && totalDecline >= entropyDrop) {
|
|
346
|
-
alerts.push({
|
|
347
|
-
kind: "collapse-entropy",
|
|
348
|
-
detail: `distinct-content ratio declined monotonically by ${totalDecline.toFixed(3)} (≥${entropyDrop}) over ${W} cycles — store content is converging`,
|
|
349
|
-
metrics: {
|
|
350
|
-
windowStart: windowStart.distinct_content_ratio,
|
|
351
|
-
current: current.distinct_content_ratio,
|
|
352
|
-
decline: totalDecline,
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
// COLLAPSE 3 — store shrinking BECAUSE of re-merging (not deletion hygiene).
|
|
357
|
-
const maxStore = Math.max(...hist.map((h) => h.store_total));
|
|
358
|
-
if (current.store_total < 0.8 * maxStore && current.over_generation_count > windowStart.over_generation_count) {
|
|
359
|
-
alerts.push({
|
|
360
|
-
kind: "collapse-shrink",
|
|
361
|
-
detail: `store shrank >20% (${current.store_total} vs window max ${maxStore}) while over-generation count rose (${windowStart.over_generation_count} → ${current.over_generation_count})`,
|
|
362
|
-
metrics: {
|
|
363
|
-
storeTotal: current.store_total,
|
|
364
|
-
windowMax: maxStore,
|
|
365
|
-
overGeneration: current.over_generation_count,
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
return alerts;
|
|
370
|
-
}
|
|
371
|
-
// ── Orchestrator ─────────────────────────────────────────────────────────────
|
|
372
|
-
/**
|
|
373
|
-
* Run the detector for one qualifying cycle: ensure canaries → compute →
|
|
374
|
-
* evaluate against stored history → persist the row → append one
|
|
375
|
-
* `collapse_detector_alert` event per fired alert. FAIL-OPEN: any error warns
|
|
376
|
-
* and returns undefined — an improve run is never broken by its own
|
|
377
|
-
* instrumentation.
|
|
378
|
-
*/
|
|
379
|
-
export function runCollapseDetector(args) {
|
|
380
|
-
const cfg = args.config.improve?.collapseDetector ?? {};
|
|
381
|
-
if (cfg.enabled === false)
|
|
382
|
-
return undefined;
|
|
383
|
-
try {
|
|
384
|
-
let indexDb;
|
|
385
|
-
try {
|
|
386
|
-
indexDb = openExistingDatabase(args.indexDbPath);
|
|
387
|
-
const db = indexDb;
|
|
388
|
-
// Over-generation threshold mirrors the guard actually in effect —
|
|
389
|
-
// reading the same config key keeps the two aligned when tuned.
|
|
390
|
-
const antiCollapse = getImproveProcessConfig("consolidate", args.improveProfile)?.antiCollapse;
|
|
391
|
-
const maxGeneration = antiCollapse?.maxGeneration ?? DEFAULT_MAX_GENERATION;
|
|
392
|
-
return withStateDb((stateDb) => {
|
|
393
|
-
const row = computeCycleMetrics(stateDb, db, {
|
|
394
|
-
runId: args.runId,
|
|
395
|
-
pass: args.pass,
|
|
396
|
-
mergeFloorViolations: args.mergeFloorViolations,
|
|
397
|
-
cfg,
|
|
398
|
-
maxGeneration,
|
|
399
|
-
});
|
|
400
|
-
if (row === null)
|
|
401
|
-
return undefined; // empty index — nothing to measure
|
|
402
|
-
const windowCycles = cfg.windowCycles ?? DEFAULT_WINDOW_CYCLES;
|
|
403
|
-
const history = queryRecentCycleMetrics(stateDb, row.canary_set_id, windowCycles);
|
|
404
|
-
const alerts = evaluateCollapseAlerts(history, row, cfg);
|
|
405
|
-
row.alerts_json = JSON.stringify(alerts.map((a) => a.kind));
|
|
406
|
-
insertCycleMetrics(stateDb, row);
|
|
407
|
-
for (const alert of alerts) {
|
|
408
|
-
appendEvent({
|
|
409
|
-
eventType: "collapse_detector_alert",
|
|
410
|
-
ref: undefined,
|
|
411
|
-
metadata: {
|
|
412
|
-
kind: alert.kind,
|
|
413
|
-
detail: alert.detail,
|
|
414
|
-
metrics: alert.metrics,
|
|
415
|
-
canarySetId: row.canary_set_id,
|
|
416
|
-
runId: args.runId,
|
|
417
|
-
},
|
|
418
|
-
}, args.eventsCtx);
|
|
419
|
-
}
|
|
420
|
-
return row;
|
|
421
|
-
}, { path: args.eventsCtx?.dbPath, borrowed: args.eventsCtx?.db });
|
|
422
|
-
}
|
|
423
|
-
finally {
|
|
424
|
-
if (indexDb)
|
|
425
|
-
closeDatabase(indexDb);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
catch (err) {
|
|
429
|
-
warn(`[collapse-detector] skipped (fail-open): ${err instanceof Error ? err.message : String(err)}`);
|
|
430
|
-
return undefined;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
@@ -1,48 +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
|
-
// Eligibility / safety predicates for consolidate: "may we touch this memory?"
|
|
5
|
-
// One reason to change — the policy for what consolidate is allowed to act on.
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
8
|
-
import { hasHotCaptureMode } from "../../proposal/validators/proposal-quality-validators.js";
|
|
9
|
-
export function isConsolidationEligibleMemoryName(name) {
|
|
10
|
-
return !name.endsWith(".derived");
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Returns true when the memory file has `captureMode: hot` in its frontmatter.
|
|
14
|
-
*
|
|
15
|
-
* Hot memories are USER-EXPLICIT (written via `akm remember` on the hot path).
|
|
16
|
-
* The consolidate LLM is forbidden from deleting or auto-merging them — the
|
|
17
|
-
* user wrote them on purpose and only the user can decide to retire them.
|
|
18
|
-
*
|
|
19
|
-
* Reads the file once per check; consolidate runs against ~10 memories per
|
|
20
|
-
* chunk so the IO cost is trivial. Returns false on any read/parse error
|
|
21
|
-
* (fail-safe: an unreadable or unparseable file is treated as HOT — protected
|
|
22
|
-
* — because a deletion shield must not fail open; a missing file is not-hot).
|
|
23
|
-
*
|
|
24
|
-
* Defends against four observed defect classes (see
|
|
25
|
-
* `memories/akm-improve-critical-review-2026-05-20`):
|
|
26
|
-
* - LLM marks a memory contradicted then deletes (dangling contradictedBy)
|
|
27
|
-
* - LLM merges two unrelated memories sharing a topic keyword
|
|
28
|
-
* - LLM judges a recent durable design memo as "redundant"
|
|
29
|
-
* - Cascade deletes (LLM uses ref:X as `contradictedBy` for ref:Y then deletes both)
|
|
30
|
-
*/
|
|
31
|
-
export function isHotCapturedMemory(filePath) {
|
|
32
|
-
if (!fs.existsSync(filePath))
|
|
33
|
-
return false;
|
|
34
|
-
try {
|
|
35
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
36
|
-
const parsed = parseFrontmatter(content);
|
|
37
|
-
return hasHotCaptureMode(parsed.data);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// Fail CLOSED. This predicate is a deletion shield: "hot" memories are
|
|
41
|
-
// protected from consolidate's merge/delete. Returning false on a read or
|
|
42
|
-
// parse failure marked exactly the memories we could not inspect as fair
|
|
43
|
-
// game — the one direction a protection check must never fail. A missing
|
|
44
|
-
// file stays false (nothing to protect); an unreadable one is protected
|
|
45
|
-
// until someone can actually read it.
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,146 +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
|
-
// R12a: merge/delete/contradict are no longer requested (see
|
|
5
|
-
// CONSOLIDATE_PLAN_JSON_SCHEMA / consolidate-system.md) — only promote is
|
|
6
|
-
// ever executed. An op of one of those retired shapes — e.g. from a model
|
|
7
|
-
// that ignores the schema — is rejected here so it degrades to the generic
|
|
8
|
-
// "skipping invalid operation" warning in the chunk-judge loop rather than
|
|
9
|
-
// being treated as an actionable (if advisory) plan entry.
|
|
10
|
-
export function isValidOp(op) {
|
|
11
|
-
if (typeof op !== "object" || op === null)
|
|
12
|
-
return false;
|
|
13
|
-
const o = op;
|
|
14
|
-
if (o.op === "promote") {
|
|
15
|
-
return typeof o.ref === "string" && typeof o.knowledgeRef === "string";
|
|
16
|
-
}
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
export function mergePlans(chunks, knownRefs) {
|
|
20
|
-
const mergeOps = new Map();
|
|
21
|
-
const deleteOps = new Map();
|
|
22
|
-
const promoteOps = new Map();
|
|
23
|
-
// C-3 / #382: contradict ops keyed by `ref|contradictedByRef` to deduplicate.
|
|
24
|
-
const contradictOps = new Map();
|
|
25
|
-
const warnings = [];
|
|
26
|
-
for (const chunk of chunks) {
|
|
27
|
-
for (const op of chunk) {
|
|
28
|
-
if (op.op === "merge") {
|
|
29
|
-
// Drop ops whose primary the LLM hallucinated (not in the loaded memory
|
|
30
|
-
// pool). Without this guard, a hallucinated primary flows all the way to
|
|
31
|
-
// Phase B where !memoryByRef.has(primary) fires and charges every real
|
|
32
|
-
// secondary with merge_primary_missing — masking LLM hallucinations as
|
|
33
|
-
// filter regressions in health metrics.
|
|
34
|
-
if (knownRefs && !knownRefs.has(op.primary)) {
|
|
35
|
-
warnings.push(`mergePlans: primary ${op.primary} not in loaded memory pool (LLM hallucination) — dropping op before execution.`);
|
|
36
|
-
// Use a dedicated skip reason so dashboards can distinguish
|
|
37
|
-
// hallucinated primaries from stale-DB regressions.
|
|
38
|
-
// Secondaries are real refs; they are NOT charged here — they remain
|
|
39
|
-
// available for other ops to claim.
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
// Filter hallucinated secondaries while preserving real ones.
|
|
43
|
-
let mergeOp = op;
|
|
44
|
-
if (knownRefs) {
|
|
45
|
-
const filteredSecondaries = op.secondaries.filter((sec) => {
|
|
46
|
-
if (!knownRefs.has(sec)) {
|
|
47
|
-
warnings.push(`mergePlans: secondary ${sec} not in loaded memory pool (LLM hallucination) — dropping from op.`);
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
return true;
|
|
51
|
-
});
|
|
52
|
-
if (filteredSecondaries.length !== op.secondaries.length) {
|
|
53
|
-
mergeOp = { ...op, secondaries: filteredSecondaries };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (mergeOp.secondaries.length > 1) {
|
|
57
|
-
warnings.push(`Merge: ${mergeOp.primary} proposed multiple secondaries; retaining one secondary for review.`);
|
|
58
|
-
mergeOp = { ...mergeOp, secondaries: mergeOp.secondaries.slice(0, 1) };
|
|
59
|
-
}
|
|
60
|
-
// merge wins over delete
|
|
61
|
-
if (deleteOps.has(mergeOp.primary)) {
|
|
62
|
-
deleteOps.delete(mergeOp.primary);
|
|
63
|
-
}
|
|
64
|
-
for (const sec of mergeOp.secondaries) {
|
|
65
|
-
if (deleteOps.has(sec))
|
|
66
|
-
deleteOps.delete(sec);
|
|
67
|
-
}
|
|
68
|
-
mergeOps.set(mergeOp.primary, mergeOp);
|
|
69
|
-
}
|
|
70
|
-
else if (op.op === "delete") {
|
|
71
|
-
// merge and promote both win over delete. A promote is non-destructive
|
|
72
|
-
// (creates a proposal) but the source memory is counted in `promoted`;
|
|
73
|
-
// if a delete also fires, the ref lands in both `promoted` and
|
|
74
|
-
// `skipReasons`, breaking the invariant by +1.
|
|
75
|
-
if (!mergeOps.has(op.ref) && !promoteOps.has(op.ref)) {
|
|
76
|
-
deleteOps.set(op.ref, op);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else if (op.op === "promote") {
|
|
80
|
-
// C-2 / #381: when both a promote and a merge target the same ref,
|
|
81
|
-
// queue the promote FIRST rather than discarding it. The promote op
|
|
82
|
-
// routes through createProposal (the human-gated proposal queue), so
|
|
83
|
-
// it is non-destructive. The merge follows after the proposal is
|
|
84
|
-
// created. This preserves the human reviewer's ability to inspect the
|
|
85
|
-
// promotion before the source memory is merged/deleted.
|
|
86
|
-
// AGM K*8 — retain the maximally informative consistent subset.
|
|
87
|
-
promoteOps.set(op.ref, op);
|
|
88
|
-
}
|
|
89
|
-
else if (op.op === "contradict") {
|
|
90
|
-
// Deduplicate by ref+contradictedByRef pair.
|
|
91
|
-
const key = `${op.ref}|${op.contradictedByRef}`;
|
|
92
|
-
if (!contradictOps.has(key)) {
|
|
93
|
-
contradictOps.set(key, op);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Second pass: enforce merge-wins-over-delete and deduplicate secondaries.
|
|
99
|
-
//
|
|
100
|
-
// 1. Delete/secondary ordering bug: the per-chunk loop removes delete ops
|
|
101
|
-
// for secondaries that were already in deleteOps, but misses the case
|
|
102
|
-
// where the delete chunk came first. A full sweep here fixes both orders.
|
|
103
|
-
//
|
|
104
|
-
// 2. Cross-merge secondary dedup: if ref A is a secondary in two merge ops,
|
|
105
|
-
// only the first (insertion-order) retains it. Without this, a successful
|
|
106
|
-
// merge credits A to mergedSecondaries and a later merge's emitMerge-
|
|
107
|
-
// FailureSkips also charges A to skipReasons — double-counting A while
|
|
108
|
-
// processed has it only once.
|
|
109
|
-
//
|
|
110
|
-
// 3. Primary-as-secondary dedup: if ref A is a primary in one merge op and
|
|
111
|
-
// a secondary in another, remove A from the secondary list. Both merges
|
|
112
|
-
// would otherwise claim A (merged++ for A, then mergedSecondaries++ for A)
|
|
113
|
-
// breaking the invariant the same way.
|
|
114
|
-
// Also remove delete ops for any ref claimed by a promote op (handles the
|
|
115
|
-
// case where the delete chunk appeared before the promote chunk).
|
|
116
|
-
for (const ref of promoteOps.keys()) {
|
|
117
|
-
deleteOps.delete(ref);
|
|
118
|
-
}
|
|
119
|
-
const claimedSecondaries = new Set();
|
|
120
|
-
for (const mergeOp of mergeOps.values()) {
|
|
121
|
-
deleteOps.delete(mergeOp.primary);
|
|
122
|
-
mergeOp.secondaries = mergeOp.secondaries.filter((sec) => {
|
|
123
|
-
if (mergeOps.has(sec)) {
|
|
124
|
-
warnings.push(`Merge: secondary ${sec} is also a merge primary — removing from secondary list to avoid double-count.`);
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
if (claimedSecondaries.has(sec)) {
|
|
128
|
-
warnings.push(`Merge: secondary ${sec} appears in multiple merge ops — retaining in first op only.`);
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
claimedSecondaries.add(sec);
|
|
132
|
-
deleteOps.delete(sec);
|
|
133
|
-
return true;
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
// C-2 / #381: promote ops are ordered BEFORE merge ops so that the
|
|
137
|
-
// human-gated proposal queue entry is created before any destructive merge.
|
|
138
|
-
// Phase B processes ops in array order, so promote executes first.
|
|
139
|
-
const ops = [
|
|
140
|
-
...promoteOps.values(),
|
|
141
|
-
...mergeOps.values(),
|
|
142
|
-
...deleteOps.values(),
|
|
143
|
-
...contradictOps.values(),
|
|
144
|
-
];
|
|
145
|
-
return { ops, warnings };
|
|
146
|
-
}
|