akm-cli 0.9.17-alpha.8 → 0.9.17
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 +277 -1694
- package/STABILITY.md +9 -8
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +17 -19
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +46 -1
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/html-report.js +1 -4
- package/dist/commands/health/improve-metrics.js +25 -37
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/report-view-model.js +4 -14
- package/dist/commands/health/windows.js +0 -1
- package/dist/commands/health.js +13 -0
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate.js +38 -63
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +23 -5
- package/dist/commands/improve/improve.js +19 -30
- package/dist/commands/improve/ledger.js +3 -2
- package/dist/commands/improve/loop-stages.js +5 -85
- package/dist/commands/improve/memory/memory-belief.js +3 -1
- package/dist/commands/improve/memory/memory-improve.js +262 -11
- package/dist/commands/improve/planner.js +0 -5
- package/dist/commands/improve/preparation.js +20 -135
- package/dist/commands/improve/retrieval-scope.js +19 -4
- package/dist/commands/improve/salience.js +1 -14
- package/dist/commands/improve/stage.js +0 -1
- package/dist/commands/lint/base-linter.js +19 -11
- package/dist/commands/proposal/drain.js +8 -1
- package/dist/commands/proposal/proposal-cli.js +16 -2
- package/dist/commands/proposal/proposal-types.js +7 -0
- package/dist/commands/proposal/proposal.js +37 -6
- package/dist/commands/proposal/repository.js +613 -4
- package/dist/commands/proposal/validators/proposals.js +9 -0
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/show.js +0 -14
- package/dist/commands/sources/info.js +122 -18
- package/dist/commands/sources/stash-cli.js +23 -3
- package/dist/core/bundle-rename.js +1 -7
- package/dist/core/config/config-schema.js +8 -1
- package/dist/core/config/config.js +23 -48
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/schema/improve-processes.js +17 -42
- package/dist/core/config/schema/index-config.js +5 -25
- package/dist/core/file-change.js +13 -5
- package/dist/core/improve-result.js +22 -6
- package/dist/core/improve-types.js +0 -1
- package/dist/core/loopback.js +7 -12
- package/dist/core/parse.js +13 -16
- package/dist/core/state/migrations.js +15 -0
- package/dist/core/time.js +0 -20
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +2 -2
- package/dist/indexer/index-written-assets.js +2 -3
- package/dist/indexer/indexer.js +18 -418
- package/dist/indexer/passes/metadata.js +0 -19
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/llm/client.js +8 -10
- package/dist/llm/embedders/remote.js +1 -2
- package/dist/llm/feature-gate.js +0 -5
- package/dist/output/shapes/helpers.js +20 -4
- package/dist/output/text/command-format.js +9 -8
- package/dist/output/text/proposal-format.js +47 -1
- package/dist/output/text/show-format.js +0 -20
- package/dist/scripts/akm-migrate-node.js +923 -950
- package/dist/scripts/akm-migrate.js +923 -950
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/sources/providers/git-stash.js +83 -4
- package/dist/storage/repositories/improve-ledger-repository.js +48 -7
- package/dist/storage/repositories/index-connection.js +5 -2
- package/dist/storage/repositories/index-entries-repository.js +4 -7
- package/dist/storage/repositories/index-entry-schema.js +4 -2
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
- package/dist/storage/repositories/index-schema.js +55 -104
- package/dist/storage/repositories/proposals-repository.js +61 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.17.md +130 -41
- package/docs/migration/release-notes/README.md +7 -0
- package/docs/reference/cli.md +27 -21
- package/docs/reference/configuration.md +21 -12
- package/docs/reference/data-and-telemetry.md +0 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -342
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/indexer/db/graph-db.js +0 -399
- package/dist/indexer/graph/graph-extraction.js +0 -809
- package/dist/indexer/graph/graph-related.js +0 -131
- package/dist/indexer/graph/graph-types.js +0 -4
- package/dist/llm/graph-extract.js +0 -892
- package/dist/llm/metadata-enhance.js +0 -95
|
@@ -7,11 +7,12 @@ import { assembleAsset } from "../../../core/asset/asset-serialize.js";
|
|
|
7
7
|
import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
8
8
|
import { MEMORY_ARCHIVE_REL } from "../../../core/asset/memory-archive.js";
|
|
9
9
|
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
10
|
-
import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
|
|
10
|
+
import { asNonEmptyString, groupBy, stringArray, toPosix } from "../../../core/common.js";
|
|
11
11
|
import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
|
|
12
12
|
import { warn } from "../../../core/warn.js";
|
|
13
13
|
import { recordWrittenPath } from "../../../core/write-provenance.js";
|
|
14
14
|
import { walkMarkdownFiles } from "../../../indexer/walk/walker.js";
|
|
15
|
+
import { checkGitPathSafety, isGitBackedStash } from "../../../sources/providers/git-stash.js";
|
|
15
16
|
import { contentHash } from "../content-hash.js";
|
|
16
17
|
import { isDerivedMemory, memoryIdentityRef, parseMemoryName, resolveParentRef } from "./derived-ref.js";
|
|
17
18
|
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
@@ -463,17 +464,80 @@ function stronglyConnectedComponents(refs, edges) {
|
|
|
463
464
|
}
|
|
464
465
|
return { components, componentIndexByRef };
|
|
465
466
|
}
|
|
466
|
-
|
|
467
|
+
/**
|
|
468
|
+
* The `.derived` twin of a non-derived memory file, if one exists on disk:
|
|
469
|
+
* `<name>.derived.md` beside it, the naming convention
|
|
470
|
+
* `indexer/passes/memory-inference.ts`'s `derivedChildPath` writes.
|
|
471
|
+
* `undefined` for a knowledge or lesson ref (no such twin exists), or for a
|
|
472
|
+
* memory that is already itself `.derived` (it has no further twin).
|
|
473
|
+
*
|
|
474
|
+
* Used by `akm proposal accept` (alpha.9) to take a retired or promoted
|
|
475
|
+
* memory's derived child along when it archives the memory.
|
|
476
|
+
*/
|
|
477
|
+
export function derivedTwinPath(filePath, refType) {
|
|
478
|
+
if (refType !== "memory" || filePath.endsWith(`${DERIVED_SUFFIX}.md`))
|
|
479
|
+
return undefined;
|
|
480
|
+
const twin = `${filePath.slice(0, -3)}${DERIVED_SUFFIX}.md`;
|
|
481
|
+
return fs.existsSync(twin) ? twin : undefined;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* True for a retire-proposal-caused archive (alpha.9: the consolidate pair
|
|
485
|
+
* pass, or O1's promotion retirement) — distinguished from a memory-cleanup
|
|
486
|
+
* family-prune candidate by carrying a `proposalId`. The two paths differ in
|
|
487
|
+
* how `previousBeliefState` is derived (below) and in which extra tombstone
|
|
488
|
+
* fields apply.
|
|
489
|
+
*/
|
|
490
|
+
function isRetireCandidate(candidate) {
|
|
491
|
+
return candidate.proposalId !== undefined;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* The tombstone's `previousBeliefState`. Memory cleanup's own family-prune
|
|
495
|
+
* candidates keep their original reason-based inference (unchanged, so
|
|
496
|
+
* existing behavior is not disturbed by this generalization). A
|
|
497
|
+
* retire-proposal candidate has no such reason vocabulary to infer from, so
|
|
498
|
+
* it reads the asset's ACTUAL frontmatter `beliefState` instead — more
|
|
499
|
+
* correct, and available because every retire path already has the file on
|
|
500
|
+
* disk right before the move.
|
|
501
|
+
*/
|
|
502
|
+
function resolvePreviousBeliefState(candidate, filePath) {
|
|
503
|
+
if (!isRetireCandidate(candidate))
|
|
504
|
+
return priorBeliefStateForArchive(candidate);
|
|
505
|
+
try {
|
|
506
|
+
return resolveBeliefState(parseFrontmatter(fs.readFileSync(filePath, "utf8")).data);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return "active";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Move `filePath` into the recoverable cleanup archive
|
|
514
|
+
* (`.akm/memory-cleanup/archive/<stamp>-<ref>/`) with a `cleanup.md`
|
|
515
|
+
* tombstone, journaling both ends (`recordWrittenPath`) so a LATER sync
|
|
516
|
+
* commits the move — `akm sync`, or the batched auto-sync an `akm improve`
|
|
517
|
+
* run does at its own end (`docs/architecture/improvement.md`, "Auto-sync").
|
|
518
|
+
* This call does not itself commit anything: a standalone `akm proposal
|
|
519
|
+
* accept` (the only way a retire proposal is ever accepted — triage never
|
|
520
|
+
* auto-accepts one) leaves the move journaled but uncommitted until
|
|
521
|
+
* something later reads that journal, unless the write target's `kind` is
|
|
522
|
+
* `"git"`, in which case the caller's own `commitWriteTargetBoundary` commits
|
|
523
|
+
* (and maybe pushes) immediately as part of the SAME accept.
|
|
524
|
+
*
|
|
525
|
+
* Generalized in alpha.9 to cover any memory, knowledge or lesson file in a
|
|
526
|
+
* writable bundle — not only `.derived` memories — so `akm proposal accept`
|
|
527
|
+
* can archive a consolidate pair-pass `retire` proposal's target, or (O1) an
|
|
528
|
+
* accepted promotion's source memory, through the same one encoding memory
|
|
529
|
+
* cleanup already used (D27: never two coexisting encodings). A
|
|
530
|
+
* retire-proposal candidate (one carrying `proposalId`) additionally stamps
|
|
531
|
+
* `proposalId`, `successorRefs` and `retiredAt` on the tombstone.
|
|
532
|
+
*/
|
|
533
|
+
export function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
467
534
|
const archivedAt = new Date().toISOString();
|
|
535
|
+
const previousBeliefState = resolvePreviousBeliefState(candidate, filePath);
|
|
468
536
|
const originalPath = path.relative(stashDir, filePath).replace(/\\/g, "/");
|
|
469
537
|
const archiveDir = createArchiveDir(stashDir, candidate.ref, archivedAt);
|
|
470
538
|
const archivedPath = path.join(archiveDir, originalPath);
|
|
471
539
|
fs.mkdirSync(path.dirname(archivedPath), { recursive: true });
|
|
472
|
-
|
|
473
|
-
// #652: an archive is a delete + a create. BOTH ends are journaled so the
|
|
474
|
-
// sync stages the removal of the original alongside the archived copy.
|
|
475
|
-
recordWrittenPath(filePath);
|
|
476
|
-
recordWrittenPath(archivedPath);
|
|
540
|
+
const retiring = isRetireCandidate(candidate);
|
|
477
541
|
const archiveRef = path.relative(stashDir, archivedPath).replace(/\\/g, "/");
|
|
478
542
|
const auditPath = path.join(archiveDir, "cleanup.md");
|
|
479
543
|
const auditRef = path.relative(stashDir, auditPath).replace(/\\/g, "/");
|
|
@@ -482,29 +546,216 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
|
482
546
|
kind: "memory-cleanup-archive",
|
|
483
547
|
archivedAt,
|
|
484
548
|
beliefState: "archived",
|
|
485
|
-
previousBeliefState
|
|
549
|
+
previousBeliefState,
|
|
486
550
|
ref: candidate.ref,
|
|
487
|
-
parentRef: candidate.parentRef,
|
|
551
|
+
...(candidate.parentRef ? { parentRef: candidate.parentRef } : {}),
|
|
488
552
|
reason: candidate.reason,
|
|
489
553
|
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
490
554
|
originalPath,
|
|
491
555
|
archivedPath: archiveRef,
|
|
556
|
+
...(retiring ? { proposalId: candidate.proposalId, retiredAt: archivedAt } : {}),
|
|
557
|
+
...(retiring && candidate.successorRefs && candidate.successorRefs.length > 0
|
|
558
|
+
? { successorRefs: candidate.successorRefs }
|
|
559
|
+
: {}),
|
|
492
560
|
}, "Archived derived memory for recoverable cleanup.\n");
|
|
561
|
+
// 4c (third review round): write the tombstone BEFORE moving the file — a
|
|
562
|
+
// crash in between used to leave a file already at archivedPath with no
|
|
563
|
+
// cleanup.md to explain it (unrecoverable: revert refuses on a missing
|
|
564
|
+
// tombstone, and nothing else knows this archive dir exists). Reordered,
|
|
565
|
+
// a crash here instead leaves, at worst, a tombstone describing a move
|
|
566
|
+
// that has not happened yet, with the file still at its original
|
|
567
|
+
// location — the ordinary "nothing archived yet" state every caller
|
|
568
|
+
// already handles.
|
|
493
569
|
fs.writeFileSync(auditPath, auditAsset, "utf8");
|
|
494
570
|
recordWrittenPath(auditPath);
|
|
571
|
+
fs.renameSync(filePath, archivedPath);
|
|
572
|
+
// #652: an archive is a delete + a create. BOTH ends are journaled so the
|
|
573
|
+
// sync stages the removal of the original alongside the archived copy.
|
|
574
|
+
recordWrittenPath(filePath);
|
|
575
|
+
recordWrittenPath(archivedPath);
|
|
495
576
|
return {
|
|
496
577
|
ref: candidate.ref,
|
|
497
|
-
parentRef: candidate.parentRef,
|
|
578
|
+
...(candidate.parentRef ? { parentRef: candidate.parentRef } : {}),
|
|
498
579
|
reason: candidate.reason,
|
|
499
580
|
beliefState: "archived",
|
|
500
|
-
previousBeliefState
|
|
581
|
+
previousBeliefState,
|
|
501
582
|
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
502
583
|
originalPath,
|
|
503
584
|
archivedPath: archiveRef,
|
|
504
585
|
auditPath: auditRef,
|
|
505
586
|
archivedAt,
|
|
587
|
+
...(retiring ? { proposalId: candidate.proposalId, retiredAt: archivedAt } : {}),
|
|
588
|
+
...(retiring && candidate.successorRefs && candidate.successorRefs.length > 0
|
|
589
|
+
? { successorRefs: candidate.successorRefs }
|
|
590
|
+
: {}),
|
|
506
591
|
};
|
|
507
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* How long a retirement's archived bytes stay on disk after `retiredAt`
|
|
595
|
+
* before the purge sweep deletes them. Git history keeps the bytes (D27;
|
|
596
|
+
* plan §5.4 "Purge").
|
|
597
|
+
*/
|
|
598
|
+
export const RETIRE_GRACE_DAYS = 30;
|
|
599
|
+
const RETIRE_GRACE_MS = RETIRE_GRACE_DAYS * 24 * 60 * 60 * 1000;
|
|
600
|
+
/** The one file every archive dir keeps forever — never deleted by the purge sweep. */
|
|
601
|
+
const TOMBSTONE_FILENAME = "cleanup.md";
|
|
602
|
+
const EMPTY_ARCHIVE_PURGE_RESULT = { purgedDirs: 0, purgedFiles: 0 };
|
|
603
|
+
/**
|
|
604
|
+
* The purge sweep (0.9.17-alpha.9 plan §5.4, §8 step 8): deterministic, no
|
|
605
|
+
* LLM, run once at improve-run start. Deletes the archived asset bytes —
|
|
606
|
+
* never `cleanup.md` — of every retirement whose tombstone `retiredAt` is
|
|
607
|
+
* more than {@link RETIRE_GRACE_DAYS} old AND whose archived files are all
|
|
608
|
+
* git-tracked and clean at the time of the sweep (see below). Git history
|
|
609
|
+
* keeps the bytes (D27); the tombstone, and ref resolution through it
|
|
610
|
+
* (`core/asset/memory-archive.ts`), are unaffected — only the tombstone's
|
|
611
|
+
* own `originalPath` file(s) are removed.
|
|
612
|
+
*
|
|
613
|
+
* Git-backed bundles only: a bundle with no `.git` of its own has no history
|
|
614
|
+
* to fall back on, so its archive is left untouched (`akm health` reports
|
|
615
|
+
* its size instead — see `health/archive-usage.ts`). Every deleted path is
|
|
616
|
+
* journaled (`recordWrittenPath`) so the end-of-run sync commits the
|
|
617
|
+
* removal, the same way it commits the archive move itself (#652).
|
|
618
|
+
*
|
|
619
|
+
* `.git` presence is necessary but NOT sufficient: `proposal accept` only
|
|
620
|
+
* commits for a `kind: "git"` write target (`core/write-source.ts`
|
|
621
|
+
* `commitWriteTargetBoundary`), and improve's own auto-sync stages only the
|
|
622
|
+
* paths its own run wrote. A filesystem-kind bundle that merely happens to
|
|
623
|
+
* have a `.git` directory (e.g. the owner committing by hand, or an old
|
|
624
|
+
* repo that was never configured as a git source) can carry retirements
|
|
625
|
+
* that were archived but never committed — deleting those would lose the
|
|
626
|
+
* only surviving copy. So every archived file under a directory past grace
|
|
627
|
+
* is checked against `git ls-files` (tracked), `git status --porcelain
|
|
628
|
+
* -uall` (clean), and `git ls-files -v` (verifiable — an assume-unchanged
|
|
629
|
+
* or skip-worktree file hides its own edits from `git status`, so it is
|
|
630
|
+
* never trusted as clean either) — each computed ONCE per sweep, not per
|
|
631
|
+
* directory; a directory with even one untracked, modified, or
|
|
632
|
+
* unverifiable file (tombstone included) is left whole for a later sweep.
|
|
633
|
+
* If any of those three git calls itself fails (a broken submodule can fail
|
|
634
|
+
* `git status` while `git ls-files` still succeeds, or git can be missing
|
|
635
|
+
* from `PATH` entirely), the whole sweep purges nothing and warns once —
|
|
636
|
+
* an empty result from a FAILED check is never treated the same as a
|
|
637
|
+
* verified-empty one.
|
|
638
|
+
*
|
|
639
|
+
* A memory-cleanup family-prune archive (not a retire proposal's) carries no
|
|
640
|
+
* `retiredAt` in its tombstone at all, so it is never a candidate here —
|
|
641
|
+
* this sweep only ever touches retirements, never that older archive class.
|
|
642
|
+
*/
|
|
643
|
+
export function purgeGracedArchive(stashDir, now = new Date()) {
|
|
644
|
+
if (!isGitBackedStash(stashDir))
|
|
645
|
+
return EMPTY_ARCHIVE_PURGE_RESULT;
|
|
646
|
+
const archiveRoot = path.join(stashDir, MEMORY_ARCHIVE_REL);
|
|
647
|
+
let entries;
|
|
648
|
+
try {
|
|
649
|
+
entries = fs.readdirSync(archiveRoot, { withFileTypes: true });
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return EMPTY_ARCHIVE_PURGE_RESULT; // no archive yet
|
|
653
|
+
}
|
|
654
|
+
const cutoffMs = now.getTime() - RETIRE_GRACE_MS;
|
|
655
|
+
// One git inspection per sweep, not per directory. All three sets are
|
|
656
|
+
// repo-relative POSIX paths, matched below against each archived file's
|
|
657
|
+
// own repo-relative path — a file is safe to delete only if it is in
|
|
658
|
+
// `tracked`, NOT in `dirty`, and NOT in `unverifiable`.
|
|
659
|
+
//
|
|
660
|
+
// Each of the three git calls can itself fail independently — a broken
|
|
661
|
+
// submodule can make `git status` exit nonzero while `git ls-files`
|
|
662
|
+
// succeeds, or vice versa (round-3 review, probes G8/G9). `[]` from a
|
|
663
|
+
// failed call is indistinguishable from a genuinely empty result once it
|
|
664
|
+
// is in a Set, so this checks `ok` FIRST: any failure purges nothing this
|
|
665
|
+
// sweep rather than silently trusting whichever check happened to
|
|
666
|
+
// succeed — a `dirty`/`unverifiable` set that came back empty ONLY
|
|
667
|
+
// because the call failed must never read as "nothing to protect".
|
|
668
|
+
// G10: assume-unchanged / skip-worktree files never show up as dirty even
|
|
669
|
+
// when genuinely modified — `checkGitPathSafety` treats them the same as
|
|
670
|
+
// "not tracked" below, so such a file (and its whole retirement) is left
|
|
671
|
+
// for a later sweep.
|
|
672
|
+
const gitSafety = checkGitPathSafety(stashDir, MEMORY_ARCHIVE_REL);
|
|
673
|
+
if (!gitSafety.ok) {
|
|
674
|
+
warn(`[improve] archive purge: skipped this sweep — could not determine the archive's git state at ${stashDir} ` +
|
|
675
|
+
"(git status/ls-files failed); nothing was purged.");
|
|
676
|
+
return EMPTY_ARCHIVE_PURGE_RESULT;
|
|
677
|
+
}
|
|
678
|
+
let purgedDirs = 0;
|
|
679
|
+
let purgedFiles = 0;
|
|
680
|
+
for (const entry of entries) {
|
|
681
|
+
// `Dirent.isDirectory()` reflects `lstat`, so it is false for a symlink
|
|
682
|
+
// even when the symlink points at a directory — a symlinked
|
|
683
|
+
// `archive/<name>` is skipped here, never followed (N1). Everything
|
|
684
|
+
// below only ever joins path components onto `archiveRoot` through
|
|
685
|
+
// `entry.name`/`readdirSync` results, so a purge can never reach
|
|
686
|
+
// outside `.akm/memory-cleanup/archive/`.
|
|
687
|
+
if (!entry.isDirectory())
|
|
688
|
+
continue;
|
|
689
|
+
const dir = path.join(archiveRoot, entry.name);
|
|
690
|
+
let data;
|
|
691
|
+
try {
|
|
692
|
+
data = parseFrontmatter(fs.readFileSync(path.join(dir, TOMBSTONE_FILENAME), "utf8")).data;
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
continue; // not a tombstone dir, or unreadable — never guess
|
|
696
|
+
}
|
|
697
|
+
const retiredAt = data.retiredAt;
|
|
698
|
+
if (typeof retiredAt !== "string")
|
|
699
|
+
continue; // family-prune archive, not a retirement — out of scope
|
|
700
|
+
const retiredMs = Date.parse(retiredAt);
|
|
701
|
+
if (!Number.isFinite(retiredMs) || retiredMs >= cutoffMs)
|
|
702
|
+
continue; // "more than" the grace period — exactly at it is not enough
|
|
703
|
+
const allFiles = listFilesRecursive(dir); // tombstone included — the whole entry must be a clean, committed unit
|
|
704
|
+
const isSafeToPurge = allFiles.every((filePath) => gitSafety.isSafe(toPosix(path.relative(stashDir, filePath))));
|
|
705
|
+
if (!isSafeToPurge)
|
|
706
|
+
continue; // untracked, modified, or unverifiable entry — skip the whole directory this sweep (B1, G10)
|
|
707
|
+
let children;
|
|
708
|
+
try {
|
|
709
|
+
children = fs.readdirSync(dir);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
let purgedAnyInThisDir = false;
|
|
715
|
+
for (const child of children) {
|
|
716
|
+
if (child === TOMBSTONE_FILENAME)
|
|
717
|
+
continue;
|
|
718
|
+
const childPath = path.join(dir, child);
|
|
719
|
+
// The archived original path may be nested (e.g. `memories/sub/foo.md`
|
|
720
|
+
// under this dir) — the journal (like git) tracks FILES, so every leaf
|
|
721
|
+
// under childPath is recorded individually, not the directory itself.
|
|
722
|
+
const filesUnderChild = listFilesRecursive(childPath);
|
|
723
|
+
try {
|
|
724
|
+
fs.rmSync(childPath, { recursive: true, force: true });
|
|
725
|
+
for (const filePath of filesUnderChild)
|
|
726
|
+
recordWrittenPath(filePath);
|
|
727
|
+
purgedFiles += filesUnderChild.length;
|
|
728
|
+
purgedAnyInThisDir = purgedAnyInThisDir || filesUnderChild.length > 0;
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// Best-effort: a locked or already-gone entry is skipped, not fatal to the run.
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (purgedAnyInThisDir)
|
|
735
|
+
purgedDirs++;
|
|
736
|
+
}
|
|
737
|
+
return { purgedDirs, purgedFiles };
|
|
738
|
+
}
|
|
739
|
+
/** Every file under `target` (itself included if it's a file), for individual journaling before a recursive delete. */
|
|
740
|
+
function listFilesRecursive(target) {
|
|
741
|
+
let stat;
|
|
742
|
+
try {
|
|
743
|
+
stat = fs.lstatSync(target);
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
if (!stat.isDirectory())
|
|
749
|
+
return stat.isFile() ? [target] : [];
|
|
750
|
+
let children;
|
|
751
|
+
try {
|
|
752
|
+
children = fs.readdirSync(target);
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
return children.flatMap((child) => listFilesRecursive(path.join(target, child)));
|
|
758
|
+
}
|
|
508
759
|
function persistBeliefStateTransition(filePath, transition) {
|
|
509
760
|
mutateFrontmatter(filePath, (parsed) => {
|
|
510
761
|
const nextFrontmatter = {
|
|
@@ -114,11 +114,6 @@ export function buildImproveExecutionPlan(input) {
|
|
|
114
114
|
wouldRun: input.stageConfig.extract.enabled,
|
|
115
115
|
reason: input.stageConfig.extract.reason,
|
|
116
116
|
},
|
|
117
|
-
{
|
|
118
|
-
name: "graph-extraction",
|
|
119
|
-
wouldRun: input.stageConfig.graphExtraction.enabled,
|
|
120
|
-
reason: input.stageConfig.graphExtraction.reason,
|
|
121
|
-
},
|
|
122
117
|
{
|
|
123
118
|
name: "memory-inference",
|
|
124
119
|
wouldRun: input.stageConfig.memoryInference.enabled,
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
* Candidate selection reads the improve ledger plus one set of signals: a ref
|
|
10
10
|
* is eligible for a source when feedback newer than its last attempt landed and
|
|
11
11
|
* no ledger window holds it. Refs without recent feedback can still be picked
|
|
12
|
-
* by the fallback lanes (proactive maintenance, high salience
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* writes nothing.
|
|
12
|
+
* by the fallback lanes (proactive maintenance, high salience); the survivors
|
|
13
|
+
* are ranked by salience, checked on disk and capped. A plan-only run
|
|
14
|
+
* evaluates the same selectors against read snapshots and writes nothing.
|
|
16
15
|
*/
|
|
17
16
|
import fs from "node:fs";
|
|
18
17
|
import path from "node:path";
|
|
@@ -36,13 +35,13 @@ import { computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolida
|
|
|
36
35
|
import { assetTypeOf, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, resolveImproveScope, withIndexDb, } from "./eligibility.js";
|
|
37
36
|
import { akmExtract, countNewExtractCandidates } from "./extract.js";
|
|
38
37
|
import { computeValenceScore } from "./feedback-valence.js";
|
|
39
|
-
import { isLedgerBlocked, lastAttemptByRef, ledgerRowFor, loadLedgerSnapshot, stateKey,
|
|
38
|
+
import { isLedgerBlocked, lastAttemptByRef, ledgerRowFor, loadLedgerSnapshot, stateKey, } from "./ledger.js";
|
|
40
39
|
import { applyMemoryCleanup } from "./memory/memory-improve.js";
|
|
41
40
|
import { getAllAssetOutcomes, getAssetOutcome, getOutcomeScoresByRef, OUTCOME_SCORE_MAX, outcomeScoreToSalience, projectAssetOutcome, updateAssetOutcome, } from "./outcome-loop.js";
|
|
42
41
|
import { projectMemoryCleanup, selectEffectiveImproveRefs } from "./planner.js";
|
|
43
42
|
import { DEFAULT_DUE_DAYS, DEFAULT_MAX_PER_RUN, selectProactiveMaintenanceRefs } from "./proactive-maintenance.js";
|
|
44
43
|
import { isInRetrievalScope, loadRetrievalScope } from "./retrieval-scope.js";
|
|
45
|
-
import {
|
|
44
|
+
import { computeSalience, getAssetSalience, getLastUseMsByRef, isContentEncodingRow, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
|
|
46
45
|
import { attributeStage, errMessage } from "./stage.js";
|
|
47
46
|
/** The candidate's durable state key (salience, outcome, ledger). */
|
|
48
47
|
const keyOf = (r) => stateKey(r.ref, r.itemRef);
|
|
@@ -79,13 +78,7 @@ export function pickDefined(source, keys) {
|
|
|
79
78
|
out[key] = source[key];
|
|
80
79
|
return out;
|
|
81
80
|
}
|
|
82
|
-
export const CONSOLIDATION_CONFIG_KEYS = [
|
|
83
|
-
"enabled",
|
|
84
|
-
"minPoolSize",
|
|
85
|
-
"limit",
|
|
86
|
-
"maxChunkSize",
|
|
87
|
-
"incrementalSince",
|
|
88
|
-
];
|
|
81
|
+
export const CONSOLIDATION_CONFIG_KEYS = ["enabled", "minPoolSize", "limit", "maxChunkSize"];
|
|
89
82
|
/** Emit an aggregate `improve_skipped` row (never one per ref). */
|
|
90
83
|
export function recordImproveSkip(eventsCtx, ref, metadata) {
|
|
91
84
|
appendEvent({ eventType: "improve_skipped", ref, metadata }, eventsCtx);
|
|
@@ -120,8 +113,6 @@ function planConsolidationPass(args) {
|
|
|
120
113
|
writeTarget: options.writeTarget,
|
|
121
114
|
target: options.target,
|
|
122
115
|
limit: processConfig?.limit,
|
|
123
|
-
incrementalSince: processConfig?.incrementalSince,
|
|
124
|
-
neighborsPerChanged: processConfig?.neighborsPerChanged,
|
|
125
116
|
maxChunkSize: processConfig?.maxChunkSize,
|
|
126
117
|
}, primaryStashDir, [], args.existingKnowledgeBodyHashes ?? loadExistingKnowledgeBodyHashes(primaryStashDir), { readOnly: args.eventsCtx?.readOnly === true })
|
|
127
118
|
: { poolSize: 0, candidatePoolSize: 0, judgedUnchanged: 0 };
|
|
@@ -215,8 +206,6 @@ async function runConsolidationPass(args) {
|
|
|
215
206
|
existingKnowledgeBodyHashes,
|
|
216
207
|
sourceRun: `consolidate-${Date.now()}`,
|
|
217
208
|
limit: processConfig?.limit,
|
|
218
|
-
incrementalSince: processConfig?.incrementalSince,
|
|
219
|
-
neighborsPerChanged: processConfig?.neighborsPerChanged,
|
|
220
209
|
maxChunkSize: processConfig?.maxChunkSize,
|
|
221
210
|
signal: args.budgetSignal,
|
|
222
211
|
p90ChunkSecondsDefault: processConfig?.p90ChunkSecondsDefault,
|
|
@@ -648,8 +637,8 @@ export function partitionBySignalDelta(args) {
|
|
|
648
637
|
}
|
|
649
638
|
/**
|
|
650
639
|
* Pick the loop's refs: signal delta, the fallback lanes (unless
|
|
651
|
-
* `--require-feedback-signal`), lane attribution, salience
|
|
652
|
-
*
|
|
640
|
+
* `--require-feedback-signal`), lane attribution, salience, the
|
|
641
|
+
* no-op-dampened ranking, the disk check and the limit.
|
|
653
642
|
*/
|
|
654
643
|
async function selectLoopCandidates(args, postCleanupRefs, validationFailureRefs, actions, persist) {
|
|
655
644
|
const { scope, options, primaryStashDir, eventsCtx, improveProfile } = args;
|
|
@@ -664,9 +653,9 @@ async function selectLoopCandidates(args, postCleanupRefs, validationFailureRefs
|
|
|
664
653
|
const processableRefs = [...partition.eligibleRefs, ...partition.distillOnlyRefs];
|
|
665
654
|
const signalFiltered = processableRefs.filter((c) => snapshot.feedback.get(c.ref)?.hasSignal === true);
|
|
666
655
|
const signalBearingSet = new Set(signalFiltered.map((r) => r.ref));
|
|
667
|
-
// The fallback lanes (proactive, high salience
|
|
668
|
-
//
|
|
669
|
-
//
|
|
656
|
+
// The fallback lanes (proactive, high salience) have no usage evidence of
|
|
657
|
+
// their own: they pick only what retrieval returned or new material improve
|
|
658
|
+
// never processed (#986). Evaluated once per candidate.
|
|
670
659
|
const fallbackEligible = postCleanupRefs.filter((c) => !validationFailureRefs.has(c.ref));
|
|
671
660
|
const allowFallbacks = options.requireFeedbackSignal !== true;
|
|
672
661
|
const retrievalScope = scope.mode === "ref" || !allowFallbacks
|
|
@@ -689,7 +678,7 @@ async function selectLoopCandidates(args, postCleanupRefs, validationFailureRefs
|
|
|
689
678
|
: [];
|
|
690
679
|
// An explicit ref scope always acts on its ref; otherwise usage signals gate the pool.
|
|
691
680
|
const signalAndRetrievalRefs = dedupeRefs([...signalFiltered, ...proactive.proactiveRefs, ...highSalienceRefs]);
|
|
692
|
-
|
|
681
|
+
const mergedRefs = scope.mode === "ref" ? processableRefs : options.requireFeedbackSignal ? signalFiltered : signalAndRetrievalRefs;
|
|
693
682
|
// Lane attribution, weakest first so the strongest wins: high-salience <
|
|
694
683
|
// proactive < signal-delta, and an explicit ref scope over everything.
|
|
695
684
|
const sourceByRef = new Map();
|
|
@@ -704,22 +693,7 @@ async function selectLoopCandidates(args, postCleanupRefs, validationFailureRefs
|
|
|
704
693
|
sourceByRef.set(r.ref, "scope");
|
|
705
694
|
for (const r of mergedRefs)
|
|
706
695
|
r.eligibilitySource = sourceByRef.get(r.ref) ?? "unknown";
|
|
707
|
-
|
|
708
|
-
// the retrieval scope, and never a ref whose reflect window is still open.
|
|
709
|
-
const forgettingEligible = fallbackEligible.filter((c) => !unscoped.has(c.ref) &&
|
|
710
|
-
!isLedgerBlocked(ledgerRowFor(snapshot.ledger, "reflect", c.ref, c.itemRef), snapshot.nowIso));
|
|
711
|
-
const scored = scoreSalience(args, mergedRefs, snapshot.feedback, retrieval.retrievalCounts, persist);
|
|
712
|
-
mergedRefs = applyForgettingSafety({
|
|
713
|
-
pendingForgettingRefs: scored.pendingForgettingRefs,
|
|
714
|
-
scope,
|
|
715
|
-
mergedRefs,
|
|
716
|
-
eligibleRefs: forgettingEligible,
|
|
717
|
-
allowFallbacks,
|
|
718
|
-
eligibilitySourceByRef: sourceByRef,
|
|
719
|
-
highSalienceRefs,
|
|
720
|
-
proactiveRefs: proactive.proactiveRefs,
|
|
721
|
-
signalFiltered,
|
|
722
|
-
});
|
|
696
|
+
const salienceMap = scoreSalience(args, mergedRefs, snapshot.feedback, retrieval.retrievalCounts, persist);
|
|
723
697
|
// Rank by salience; a ref skipped as a no-op repeatedly sorts lower (its stored rank is untouched).
|
|
724
698
|
const noOps = new Map();
|
|
725
699
|
withRunState(eventsCtx, persist, (db) => {
|
|
@@ -727,7 +701,7 @@ async function selectLoopCandidates(args, postCleanupRefs, validationFailureRefs
|
|
|
727
701
|
noOps.set(r.ref, getAssetSalience(db, keyOf(r))?.consecutive_no_ops ?? 0);
|
|
728
702
|
});
|
|
729
703
|
const effectiveScore = (ref) => {
|
|
730
|
-
const rank =
|
|
704
|
+
const rank = salienceMap.get(ref)?.rankScore ?? 0;
|
|
731
705
|
return (noOps.get(ref) ?? 0) >= SALIENCE_NO_OP_DAMPEN_THRESHOLD ? rank * SALIENCE_NO_OP_DAMPEN_FACTOR : rank;
|
|
732
706
|
};
|
|
733
707
|
const sorted = [...mergedRefs].sort((a, b) => effectiveScore(b.ref) - effectiveScore(a.ref) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
|
|
@@ -907,8 +881,7 @@ function selectHighSalienceLane(options, improveProfile, eventsCtx, candidates,
|
|
|
907
881
|
/**
|
|
908
882
|
* Score the merged refs: update `asset_outcome` (projected on a plan-only
|
|
909
883
|
* run), compute each salience vector (keeping a stored content-derived
|
|
910
|
-
* encoding score), then persist
|
|
911
|
-
* falls from the top 200 to below 500 becomes a forgetting-safety candidate.
|
|
884
|
+
* encoding score), then persist it.
|
|
912
885
|
*/
|
|
913
886
|
function scoreSalience(args, mergedRefs, feedback, retrievalCounts, persist) {
|
|
914
887
|
const { options, eventsCtx } = args;
|
|
@@ -950,58 +923,13 @@ function scoreSalience(args, mergedRefs, feedback, retrievalCounts, persist) {
|
|
|
950
923
|
outcomeWeightEnabled,
|
|
951
924
|
}));
|
|
952
925
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
// Positions are stash-wide: every stored row of this source, with this
|
|
956
|
-
// run's scores overlaid under the same keys.
|
|
957
|
-
const before = new Map();
|
|
958
|
-
for (const [ref, score] of getAllRankScores(db)) {
|
|
959
|
-
const boundary = ref.indexOf("//");
|
|
960
|
-
if (options.sourceName && (boundary >= 0 ? ref.slice(0, boundary) : undefined) !== options.sourceName)
|
|
961
|
-
continue;
|
|
962
|
-
before.set(ref, score);
|
|
963
|
-
}
|
|
964
|
-
let forgetting = [];
|
|
965
|
-
if (before.size > 0) {
|
|
966
|
-
const after = new Map(before);
|
|
967
|
-
for (const r of mergedRefs)
|
|
968
|
-
after.set(keyOf(r), salienceMap.get(r.ref)?.rankScore ?? 0);
|
|
969
|
-
const report = buildRankChangeReport(toRankPositions(before), toRankPositions(after));
|
|
970
|
-
if (report.forgettingCandidates.length > 0) {
|
|
971
|
-
const drops = report.forgettingCandidates
|
|
972
|
-
.slice(0, 5)
|
|
973
|
-
.map((e) => `${e.ref} (#${e.oldRank}→#${e.newRank})`)
|
|
974
|
-
.join(", ");
|
|
975
|
-
warn(`[improve/salience] WS-1 rank-change report: ${report.forgettingCandidates.length} asset(s) fell from top-200 to below position 500. Top drops: ${drops}`);
|
|
976
|
-
forgetting = report.forgettingCandidates.map((e) => refByKey.get(e.ref) ?? e.ref);
|
|
977
|
-
}
|
|
978
|
-
if (persist) {
|
|
979
|
-
appendEvent({
|
|
980
|
-
eventType: "improve_salience_rank_change",
|
|
981
|
-
ref: undefined,
|
|
982
|
-
metadata: {
|
|
983
|
-
stashSize: before.size,
|
|
984
|
-
totalChanged: report.allChanges.length,
|
|
985
|
-
forgettingCandidates: report.forgettingCandidates.length,
|
|
986
|
-
topDrops: report.forgettingCandidates
|
|
987
|
-
.slice(0, 10)
|
|
988
|
-
.map((e) => ({ ref: e.ref, oldRank: e.oldRank, newRank: e.newRank })),
|
|
989
|
-
},
|
|
990
|
-
}, eventsCtx);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
if (persist) {
|
|
926
|
+
if (persist) {
|
|
927
|
+
withRunState(eventsCtx, persist, (db) => {
|
|
994
928
|
for (const r of mergedRefs)
|
|
995
929
|
upsertAssetSalience(db, keyOf(r), salienceMap.get(r.ref), now);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
return { salienceMap, pendingForgettingRefs };
|
|
1000
|
-
}
|
|
1001
|
-
/** 1-indexed positions by score desc (ref asc on ties). */
|
|
1002
|
-
function toRankPositions(scores) {
|
|
1003
|
-
const sorted = [...scores.entries()].sort(([refA, a], [refB, b]) => b !== a ? b - a : refA < refB ? -1 : refA > refB ? 1 : 0);
|
|
1004
|
-
return new Map(sorted.map(([ref], i) => [ref, i + 1]));
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return salienceMap;
|
|
1005
933
|
}
|
|
1006
934
|
/**
|
|
1007
935
|
* Update each ref's outcome row and return its outcome salience, normalized
|
|
@@ -1086,49 +1014,6 @@ function updateOutcomeScores(args) {
|
|
|
1086
1014
|
});
|
|
1087
1015
|
return out;
|
|
1088
1016
|
}
|
|
1089
|
-
/**
|
|
1090
|
-
* Forgetting safety: inject this plan's own candidates that fell out of the
|
|
1091
|
-
* top ranks, past the signal gate (never for a ref scope or with
|
|
1092
|
-
* `--require-feedback-signal`). Attribution afterwards: high-salience <
|
|
1093
|
-
* proactive < forgetting-safety < signal-delta.
|
|
1094
|
-
*/
|
|
1095
|
-
export function applyForgettingSafety(args) {
|
|
1096
|
-
const { eligibilitySourceByRef } = args;
|
|
1097
|
-
let mergedRefs = args.mergedRefs;
|
|
1098
|
-
if (args.pendingForgettingRefs.length === 0 || args.scope.mode === "ref" || !args.allowFallbacks)
|
|
1099
|
-
return mergedRefs;
|
|
1100
|
-
const present = new Set(mergedRefs.map((r) => r.ref));
|
|
1101
|
-
const byRef = new Map(args.eligibleRefs.map((c) => [c.ref, c]));
|
|
1102
|
-
const byItemRef = new Map(args.eligibleRefs.flatMap((c) => (c.itemRef ? [[c.itemRef, c]] : [])));
|
|
1103
|
-
const added = [];
|
|
1104
|
-
const forgetting = new Set();
|
|
1105
|
-
for (const stored of args.pendingForgettingRefs) {
|
|
1106
|
-
// A qualified spelling must match this plan's exact item_ref.
|
|
1107
|
-
const candidate = byItemRef.get(stored) ?? (stored.includes("//") ? undefined : byRef.get(stripBundle(stored)));
|
|
1108
|
-
if (!candidate || forgetting.has(candidate.ref))
|
|
1109
|
-
continue;
|
|
1110
|
-
forgetting.add(candidate.ref);
|
|
1111
|
-
if (!present.has(candidate.ref)) {
|
|
1112
|
-
added.push(candidate);
|
|
1113
|
-
present.add(candidate.ref);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if (added.length > 0)
|
|
1117
|
-
mergedRefs = dedupeRefs([...mergedRefs, ...added]);
|
|
1118
|
-
if (forgetting.size === 0)
|
|
1119
|
-
return mergedRefs;
|
|
1120
|
-
for (const r of args.highSalienceRefs)
|
|
1121
|
-
eligibilitySourceByRef.set(r.ref, "high-salience");
|
|
1122
|
-
for (const r of args.proactiveRefs)
|
|
1123
|
-
eligibilitySourceByRef.set(r.ref, "proactive");
|
|
1124
|
-
for (const ref of forgetting)
|
|
1125
|
-
eligibilitySourceByRef.set(ref, "forgetting-safety");
|
|
1126
|
-
for (const r of args.signalFiltered)
|
|
1127
|
-
eligibilitySourceByRef.set(r.ref, "signal-delta");
|
|
1128
|
-
for (const r of mergedRefs)
|
|
1129
|
-
r.eligibilitySource = eligibilitySourceByRef.get(r.ref) ?? "unknown";
|
|
1130
|
-
return mergedRefs;
|
|
1131
|
-
}
|
|
1132
1017
|
/** Drop candidates whose file vanished since planning, with one aggregate event. */
|
|
1133
1018
|
async function dropRefsMissingOnDisk(sorted, options, eventsCtx, persist) {
|
|
1134
1019
|
const actionableRefs = [];
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Fresh feedback and an explicit `--scope <ref>` are usage evidence of their
|
|
9
9
|
* own, so the signal-delta and scope lanes need no check. The fallback lanes
|
|
10
|
-
* (proactive maintenance, high salience
|
|
11
|
-
*
|
|
10
|
+
* (proactive maintenance, high salience) and consolidation pick assets
|
|
11
|
+
* without such evidence, so they pick only inside this scope.
|
|
12
12
|
*/
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import { daysToMs } from "../../core/common.js";
|
|
@@ -16,7 +16,7 @@ import { warn } from "../../core/warn.js";
|
|
|
16
16
|
import { listUsedEntryRefs, USAGE_EVENT_RETENTION_DAYS } from "../../indexer/usage/usage-events.js";
|
|
17
17
|
import { listImproveLedgerRows } from "../../storage/repositories/improve-ledger-repository.js";
|
|
18
18
|
import { listProposalRefSources } from "../../storage/repositories/proposals-repository.js";
|
|
19
|
-
import { readLedgerDb, stripBundle } from "./ledger.js";
|
|
19
|
+
import { PAIR_PASS_LEDGER_SOURCE, readLedgerDb, stripBundle } from "./ledger.js";
|
|
20
20
|
/**
|
|
21
21
|
* Ledger and proposal sources that bring material in. Every other source is an
|
|
22
22
|
* improve stage reworking an asset (reflect, distill, consolidate, schema repair).
|
|
@@ -43,7 +43,22 @@ export function loadRetrievalScope(access, stashDir) {
|
|
|
43
43
|
}
|
|
44
44
|
if (!stashDir)
|
|
45
45
|
return;
|
|
46
|
-
|
|
46
|
+
// Blocker 1 (second review round): a pair-pass ledger row must NOT mark
|
|
47
|
+
// an asset "processed" — unlike every other stage, the pair pass judges
|
|
48
|
+
// material against its NEIGHBOURS, not on its own merits, so its own
|
|
49
|
+
// attempt is not usage evidence the fallback lanes (proactive,
|
|
50
|
+
// high-salience) or promotion retries should be starved by. Left in
|
|
51
|
+
// the ledger for the pair pass's OWN eligibility
|
|
52
|
+
// (selectInitiators reads content_hash directly, never this scope).
|
|
53
|
+
// Proposal rows are untouched: a MINTED retire proposal is real
|
|
54
|
+
// evidence something happened to the asset.
|
|
55
|
+
for (const row of listImproveLedgerRows(db, stashDir)) {
|
|
56
|
+
if (row.source === PAIR_PASS_LEDGER_SOURCE)
|
|
57
|
+
continue;
|
|
58
|
+
if (!CAPTURE_SOURCES.has(row.source))
|
|
59
|
+
processed.add(stripBundle(row.ref));
|
|
60
|
+
}
|
|
61
|
+
for (const row of listProposalRefSources(db, stashDir)) {
|
|
47
62
|
if (!CAPTURE_SOURCES.has(row.source))
|
|
48
63
|
processed.add(stripBundle(row.ref));
|
|
49
64
|
}
|
|
@@ -62,7 +62,7 @@ export function computeSalience(inputs) {
|
|
|
62
62
|
const rankScore = clamp01((we * encoding + wo * outcome + wr * retrieval) * sizePenalty);
|
|
63
63
|
return { encoding, outcome, retrieval, rankScore, encodingSource };
|
|
64
64
|
}
|
|
65
|
-
export {
|
|
65
|
+
export { getAssetSalience, getConsecutiveNoOps, recordNoOp, resetConsecutiveNoOps, upsertAssetSalience, } from "../../storage/repositories/salience-repository.js";
|
|
66
66
|
/** Whether a stored row's encoding salience is content-derived (unknown provenance is not). */
|
|
67
67
|
export function isContentEncodingRow(row) {
|
|
68
68
|
return row.encoding_source === "content";
|
|
@@ -73,19 +73,6 @@ export function isContentEncodingRow(row) {
|
|
|
73
73
|
*/
|
|
74
74
|
export const SALIENCE_NO_OP_DAMPEN_THRESHOLD = 3;
|
|
75
75
|
export const SALIENCE_NO_OP_DAMPEN_FACTOR = 0.5;
|
|
76
|
-
/** Forgetting safety: compare 1-indexed rank positions between two rankings. */
|
|
77
|
-
export function buildRankChangeReport(oldRanks, newRanks, oldTopN = 200, forgettingThreshold = 500) {
|
|
78
|
-
const allChanges = [];
|
|
79
|
-
for (const [ref, oldRank] of oldRanks) {
|
|
80
|
-
const newRank = newRanks.get(ref);
|
|
81
|
-
if (newRank !== undefined)
|
|
82
|
-
allChanges.push({ ref, oldRank, newRank, rankDelta: newRank - oldRank });
|
|
83
|
-
}
|
|
84
|
-
const forgettingCandidates = allChanges
|
|
85
|
-
.filter((c) => c.oldRank <= oldTopN && c.newRank > forgettingThreshold)
|
|
86
|
-
.sort((a, b) => b.rankDelta - a.rankDelta);
|
|
87
|
-
return { forgettingCandidates, allChanges };
|
|
88
|
-
}
|
|
89
76
|
/** `ref → last retrieval (ms)` from the index's utility scores; absent means never retrieved. */
|
|
90
77
|
export function getLastUseMsByRef(indexDb, candidates) {
|
|
91
78
|
const result = new Map();
|