akm-cli 0.9.0-rc.2 → 0.9.0-rc.3
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 +34 -8
- package/dist/assets/improve-strategies/catchup.json +3 -1
- package/dist/assets/improve-strategies/consolidate.json +3 -1
- package/dist/assets/improve-strategies/frequent.json +3 -1
- package/dist/assets/improve-strategies/graph-refresh.json +3 -1
- package/dist/assets/improve-strategies/memory-focus.json +4 -1
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -0
- package/dist/assets/improve-strategies/quick.json +3 -1
- package/dist/assets/improve-strategies/recombine-only.json +2 -0
- package/dist/assets/improve-strategies/reflect-distill.json +1 -0
- package/dist/assets/improve-strategies/synthesize.json +2 -0
- package/dist/assets/tasks/core/backup.yml +2 -2
- package/dist/cli/config-migrate.js +554 -26
- package/dist/cli.js +3 -1
- package/dist/commands/backup-cli.js +6 -4
- package/dist/commands/config-cli.js +10 -3
- package/dist/commands/feedback-cli.js +24 -7
- package/dist/commands/health/checks.js +94 -15
- package/dist/commands/health/surfaces.js +2 -1
- package/dist/commands/improve/anti-collapse.js +3 -3
- package/dist/commands/improve/collapse-detector.js +5 -5
- package/dist/commands/improve/consolidate.js +123 -66
- package/dist/commands/improve/distill/promote-memory.js +8 -6
- package/dist/commands/improve/distill/quality-gate.js +1 -1
- package/dist/commands/improve/distill-guards.js +1 -1
- package/dist/commands/improve/distill-promotion-policy.js +32 -26
- package/dist/commands/improve/distill.js +52 -36
- package/dist/commands/improve/eligibility.js +9 -8
- package/dist/commands/improve/extract-prompt.js +2 -2
- package/dist/commands/improve/extract.js +43 -11
- package/dist/commands/improve/improve-auto-accept.js +14 -28
- package/dist/commands/improve/improve-cli.js +4 -2
- package/dist/commands/improve/improve-strategies.js +2 -1
- package/dist/commands/improve/improve.js +49 -8
- package/dist/commands/improve/loop-stages.js +11 -6
- package/dist/commands/improve/preparation.js +51 -17
- package/dist/commands/improve/procedural.js +10 -6
- package/dist/commands/improve/recombine.js +31 -7
- package/dist/commands/improve/reflect.js +24 -14
- package/dist/commands/improve/salience.js +5 -1
- package/dist/commands/improve/source-identity.js +43 -0
- package/dist/commands/migrate-cli.js +36 -0
- package/dist/commands/mv-cli.js +647 -258
- package/dist/commands/proposal/drain.js +16 -4
- package/dist/commands/proposal/proposal-cli.js +3 -2
- package/dist/commands/proposal/proposal.js +16 -55
- package/dist/commands/proposal/repository.js +664 -58
- package/dist/commands/read/curate.js +4 -2
- package/dist/commands/read/knowledge.js +4 -1
- package/dist/commands/read/search.js +6 -2
- package/dist/commands/read/show.js +8 -7
- package/dist/commands/sources/self-update.js +156 -112
- package/dist/commands/sources/sources-cli.js +7 -2
- package/dist/core/common.js +36 -3
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-schema.js +15 -3
- package/dist/core/config/config.js +14 -13
- package/dist/core/file-lock.js +2 -2
- package/dist/core/migration-backup.js +816 -171
- package/dist/core/migration-operation.js +44 -0
- package/dist/core/state/migrations.js +4 -7
- package/dist/core/state-db.js +22 -7
- package/dist/core/write-source.js +86 -18
- package/dist/indexer/db/db.js +109 -53
- package/dist/indexer/index-writer-lock.js +38 -37
- package/dist/indexer/index-written-assets.js +73 -56
- package/dist/indexer/indexer.js +5 -1
- package/dist/indexer/search/search-source.js +2 -2
- package/dist/indexer/usage/usage-events.js +8 -2
- package/dist/integrations/agent/engine-resolution.js +14 -7
- package/dist/scripts/migrate-storage.js +867 -990
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +405 -566
- package/dist/setup/detected-engines.js +7 -13
- package/dist/setup/engine-config.js +14 -4
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +1 -1
- package/dist/setup/steps/tasks.js +1 -1
- package/dist/sources/providers/git-stash.js +58 -21
- package/dist/sources/providers/git.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +138 -3
- package/dist/storage/repositories/events-repository.js +24 -0
- package/dist/storage/repositories/proposals-repository.js +14 -0
- package/dist/tasks/embedded.js +2 -0
- package/dist/workflows/db.js +24 -13
- package/dist/workflows/validator.js +24 -2
- package/docs/migration/release-notes/0.9.0.md +24 -7
- package/docs/migration/v0.8-to-v0.9.md +124 -19
- package/package.json +1 -1
|
@@ -21,13 +21,14 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
24
25
|
import proceduralSystemPrompt from "../../assets/prompts/procedural-system.md" with { type: "text" };
|
|
25
26
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
26
27
|
import { resolveStashDir } from "../../core/common.js";
|
|
27
28
|
import { loadConfig } from "../../core/config/config.js";
|
|
28
29
|
import { appendEvent } from "../../core/events.js";
|
|
29
30
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
30
|
-
import {
|
|
31
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
31
32
|
import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
|
|
32
33
|
import { parseWorkflow } from "../../workflows/parser.js";
|
|
33
34
|
import { createProposal, isProposalSkipped } from "../proposal/repository.js";
|
|
@@ -212,10 +213,11 @@ function kebab(s) {
|
|
|
212
213
|
.replace(/^-|-$/g, "") || "step");
|
|
213
214
|
}
|
|
214
215
|
/** Build the exact workflow markdown the parser accepts. */
|
|
215
|
-
export function assembleWorkflowMarkdown(doc) {
|
|
216
|
+
export function assembleWorkflowMarkdown(doc, xrefs = []) {
|
|
216
217
|
const lines = [
|
|
217
218
|
"---",
|
|
218
219
|
`description: ${JSON.stringify(doc.description)}`,
|
|
220
|
+
...(xrefs.length > 0 ? ["xrefs:", ...xrefs.map((ref) => ` - ${ref}`)] : []),
|
|
219
221
|
"---",
|
|
220
222
|
"",
|
|
221
223
|
`# Workflow: ${doc.title}`,
|
|
@@ -268,7 +270,8 @@ export async function akmProcedural(opts) {
|
|
|
268
270
|
let db;
|
|
269
271
|
try {
|
|
270
272
|
db = openExistingDatabase();
|
|
271
|
-
|
|
273
|
+
const selectedRoot = path.resolve(stashDir);
|
|
274
|
+
entries = getAllEntries(db).filter((entry) => path.resolve(entry.stashDir) === selectedRoot);
|
|
272
275
|
}
|
|
273
276
|
catch (e) {
|
|
274
277
|
warnings.push(`procedural: failed to open index — ${String(e)}`);
|
|
@@ -301,7 +304,7 @@ export async function akmProcedural(opts) {
|
|
|
301
304
|
}
|
|
302
305
|
// Procedural output is a workflow (non-wiki) → stash authoring standards.
|
|
303
306
|
// Resolved ONCE per run and passed to each sequence prompt.
|
|
304
|
-
const standardsContext =
|
|
307
|
+
const standardsContext = resolveStandardsContext("workflow:_procedural", stashDir);
|
|
305
308
|
for (const cluster of clusters) {
|
|
306
309
|
if (opts.signal?.aborted) {
|
|
307
310
|
warnings.push("aborted-mid-run");
|
|
@@ -345,7 +348,8 @@ export async function akmProcedural(opts) {
|
|
|
345
348
|
}
|
|
346
349
|
// Assemble + locally validate the workflow markdown. Never queue an
|
|
347
350
|
// unparseable workflow.
|
|
348
|
-
const
|
|
351
|
+
const xrefs = cluster.members.map((member) => member.ref).sort();
|
|
352
|
+
const content = assembleWorkflowMarkdown(doc, xrefs);
|
|
349
353
|
const parsed = parseWorkflow(content, { path: workflowRef });
|
|
350
354
|
if (!parsed.ok) {
|
|
351
355
|
appendEvent({
|
|
@@ -365,7 +369,7 @@ export async function akmProcedural(opts) {
|
|
|
365
369
|
ref: workflowRef,
|
|
366
370
|
source: "procedural",
|
|
367
371
|
sourceRun,
|
|
368
|
-
payload: { content, frontmatter: { description: doc.description } },
|
|
372
|
+
payload: { content, frontmatter: { description: doc.description, xrefs } },
|
|
369
373
|
eligibilitySource,
|
|
370
374
|
}, opts.ctx);
|
|
371
375
|
if (isProposalSkipped(proposalResult)) {
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
* not re-promoted on every subsequent run. Hypotheses NOT re-induced in a run
|
|
26
26
|
* have their consecutive streak reset (decay-to-zero).
|
|
27
27
|
*
|
|
28
|
-
* NAMESPACE note: the ref stays
|
|
29
|
-
*
|
|
28
|
+
* NAMESPACE note: the ref stays stable for BOTH passes. A unanimous source-memory
|
|
29
|
+
* scope is preserved; unscoped members produce a flat lesson ref rather than an
|
|
30
|
+
* invented type scope. The ref is the promotion TARGET asset (a lesson in both the hypothesis
|
|
30
31
|
* and promoted states), so re-induction must map to the same ref and the ref
|
|
31
32
|
* cannot encode the proposal type. The hypothesis-vs-lesson distinction is
|
|
32
33
|
* carried ONLY by the proposal frontmatter `type` field. On promotion the prior
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
*/
|
|
40
41
|
import { createHash } from "node:crypto";
|
|
41
42
|
import fs from "node:fs";
|
|
43
|
+
import path from "node:path";
|
|
42
44
|
import recombineSystemPrompt from "../../assets/prompts/recombine-system.md" with { type: "text" };
|
|
43
45
|
import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
|
|
44
46
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
@@ -46,7 +48,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
46
48
|
import { loadConfig } from "../../core/config/config.js";
|
|
47
49
|
import { appendEvent } from "../../core/events.js";
|
|
48
50
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
49
|
-
import {
|
|
51
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
50
52
|
import { withStateDbAsync } from "../../core/state-db.js";
|
|
51
53
|
import { closeDatabase, getAllEntries, getEntitiesByEntryIds, openExistingDatabase, } from "../../indexer/db/db.js";
|
|
52
54
|
import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis, markRecombineHypothesisPromoted, recordRecombineInduction, } from "../../storage/repositories/recombine-repository.js";
|
|
@@ -427,7 +429,24 @@ export function deriveRecombineLessonRef(cluster) {
|
|
|
427
429
|
.replace(/^-|-$/g, "");
|
|
428
430
|
const memberKey = recombineMemberKey(cluster);
|
|
429
431
|
const hash = createHash("sha256").update(memberKey, "utf8").digest("hex").slice(0, 8);
|
|
430
|
-
|
|
432
|
+
const scopeResult = recombineClusterScope(cluster);
|
|
433
|
+
if (!scopeResult.ok)
|
|
434
|
+
throw new Error("Cannot derive a lesson ref for a cluster with mixed scopes.");
|
|
435
|
+
return `lesson:${scopeResult.scope ? `${scopeResult.scope}/` : ""}${slug || "cluster"}-${hash}`;
|
|
436
|
+
}
|
|
437
|
+
function recombineClusterScope(cluster) {
|
|
438
|
+
const scopes = new Set(cluster.members.map((member) => {
|
|
439
|
+
const parts = member.entry.name.split("/");
|
|
440
|
+
return parts.length > 1 ? parts[0] : "";
|
|
441
|
+
}));
|
|
442
|
+
if (scopes.size !== 1)
|
|
443
|
+
return { ok: false };
|
|
444
|
+
const scope = [...scopes][0]
|
|
445
|
+
.toLowerCase()
|
|
446
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
447
|
+
.replace(/-+/g, "-")
|
|
448
|
+
.replace(/^-|-$/g, "");
|
|
449
|
+
return { ok: true, scope };
|
|
431
450
|
}
|
|
432
451
|
function validatePromotedLessonFrontmatter(ref, frontmatter) {
|
|
433
452
|
const descCheck = isValidDescription(frontmatter.description, ref);
|
|
@@ -511,7 +530,8 @@ export async function akmRecombine(opts) {
|
|
|
511
530
|
let db;
|
|
512
531
|
try {
|
|
513
532
|
db = openExistingDatabase();
|
|
514
|
-
|
|
533
|
+
const selectedRoot = path.resolve(stashDir);
|
|
534
|
+
entries = getAllEntries(db, "memory").filter((entry) => path.resolve(entry.stashDir) === selectedRoot);
|
|
515
535
|
if (relatednessSource === "graph" || relatednessSource === "both") {
|
|
516
536
|
try {
|
|
517
537
|
entityByEntryId = getEntitiesByEntryIds(db, entries.map((e) => e.id));
|
|
@@ -565,7 +585,7 @@ export async function akmRecombine(opts) {
|
|
|
565
585
|
const seenThisRun = new Set();
|
|
566
586
|
// Recombine output is knowledge/lesson (non-wiki) → stash authoring
|
|
567
587
|
// standards. Resolved ONCE per run and passed to each cluster prompt.
|
|
568
|
-
const standardsContext =
|
|
588
|
+
const standardsContext = resolveStandardsContext("lesson:_recombined", stashDir);
|
|
569
589
|
// #625 — open the confirmation-count store once per run via the ctx seam,
|
|
570
590
|
// reusing a long-lived ctx.db handle when the caller provided one (mirrors
|
|
571
591
|
// proposals.ts). Only handles WE opened are closed by the seam.
|
|
@@ -576,6 +596,10 @@ export async function akmRecombine(opts) {
|
|
|
576
596
|
break;
|
|
577
597
|
}
|
|
578
598
|
clustersFormed += 1;
|
|
599
|
+
if (!recombineClusterScope(cluster).ok) {
|
|
600
|
+
warnings.push(`recombine: skipped ${cluster.signature} cluster with mixed scopes`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
579
603
|
// #9 — promotion is terminal. If this cluster Jaccard-matches a
|
|
580
604
|
// hypothesis that was ALREADY promoted, generating again can at best
|
|
581
605
|
// re-queue a redundant `type: hypothesis` for a settled ref (promotion
|
|
@@ -709,7 +733,7 @@ export async function akmRecombine(opts) {
|
|
|
709
733
|
type: proposalType,
|
|
710
734
|
description: generalization.description,
|
|
711
735
|
...(generalization.when_to_use ? { when_to_use: generalization.when_to_use } : {}),
|
|
712
|
-
|
|
736
|
+
xrefs: sourceRefs,
|
|
713
737
|
};
|
|
714
738
|
const content = assembleContent(frontmatter, generalization.body);
|
|
715
739
|
if (promote && stateDb) {
|
|
@@ -46,7 +46,9 @@ import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/
|
|
|
46
46
|
import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
|
|
47
47
|
import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
|
|
48
48
|
import { deriveLessonRef, runLessonQualityJudge } from "./distill.js";
|
|
49
|
+
import { findAssetFilePath } from "./eligibility.js";
|
|
49
50
|
import { classifyReflectChange } from "./reflect-noise.js";
|
|
51
|
+
import { bareImproveRef, durableImproveRef } from "./source-identity.js";
|
|
50
52
|
const MAX_FEEDBACK_LINES = 10;
|
|
51
53
|
const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
52
54
|
/**
|
|
@@ -55,12 +57,13 @@ const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
|
55
57
|
* all assets so `akm reflect` can operate in a general "review recent
|
|
56
58
|
* signals" mode. Best-effort — a missing or empty events stream returns `[]`.
|
|
57
59
|
*/
|
|
58
|
-
function readRecentFeedback(ref) {
|
|
60
|
+
function readRecentFeedback(ref, legacyRef) {
|
|
59
61
|
try {
|
|
60
|
-
const result = readEvents({ type: "feedback", ...(ref ? { ref } : {}) });
|
|
62
|
+
const result = readEvents({ type: "feedback", ...(ref && !legacyRef ? { ref } : {}) });
|
|
63
|
+
const events = ref && legacyRef ? result.events.filter((event) => event.ref === ref || event.ref === legacyRef) : result.events;
|
|
61
64
|
const lines = [];
|
|
62
65
|
const limit = ref ? MAX_FEEDBACK_LINES : MAX_GLOBAL_FEEDBACK_LINES;
|
|
63
|
-
for (const event of
|
|
66
|
+
for (const event of events.slice(-limit)) {
|
|
64
67
|
const md = (event.metadata ?? {});
|
|
65
68
|
const signal = typeof md.signal === "string" ? md.signal : "?";
|
|
66
69
|
const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : "";
|
|
@@ -187,7 +190,7 @@ function hasRelatedSkillSource(content, skillRef) {
|
|
|
187
190
|
const sources = parsed.data.sources;
|
|
188
191
|
return Array.isArray(sources) && sources.some((source) => typeof source === "string" && source.trim() === skillRef);
|
|
189
192
|
}
|
|
190
|
-
async function readRelatedLessons(stash, ref, parsedRef) {
|
|
193
|
+
async function readRelatedLessons(stash, ref, parsedRef, sourceName) {
|
|
191
194
|
if (parsedRef.type !== "skill")
|
|
192
195
|
return [];
|
|
193
196
|
const related = new Map();
|
|
@@ -198,7 +201,7 @@ async function readRelatedLessons(stash, ref, parsedRef) {
|
|
|
198
201
|
related.set(derivedLessonRef, { ref: derivedLessonRef, content: fs.readFileSync(derivedLessonPath, "utf8") });
|
|
199
202
|
}
|
|
200
203
|
try {
|
|
201
|
-
const feedbackEvents = readEvents({ type: "distill_invoked", ref }).events;
|
|
204
|
+
const feedbackEvents = readEvents({ type: "distill_invoked", ref: durableImproveRef(ref, sourceName) }).events;
|
|
202
205
|
for (const event of feedbackEvents) {
|
|
203
206
|
const lessonRef = typeof event.metadata?.lessonRef === "string" ? event.metadata.lessonRef : undefined;
|
|
204
207
|
if (lessonRef?.startsWith("lesson:"))
|
|
@@ -210,10 +213,10 @@ async function readRelatedLessons(stash, ref, parsedRef) {
|
|
|
210
213
|
}
|
|
211
214
|
for (const candidateRef of candidateRefs) {
|
|
212
215
|
try {
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
216
|
+
const filePath = await findAssetFilePath(durableImproveRef(candidateRef, sourceName), stash);
|
|
217
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
215
218
|
continue;
|
|
216
|
-
const content = fs.readFileSync(
|
|
219
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
217
220
|
related.set(candidateRef, { ref: candidateRef, content });
|
|
218
221
|
}
|
|
219
222
|
catch {
|
|
@@ -698,7 +701,7 @@ export async function akmReflect(options = {}) {
|
|
|
698
701
|
// attempt regardless of downstream success/failure.
|
|
699
702
|
appendEvent({
|
|
700
703
|
eventType: "reflect_invoked",
|
|
701
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
704
|
+
...(options.ref ? { ref: durableImproveRef(options.ref, options.sourceName) } : {}),
|
|
702
705
|
metadata: {
|
|
703
706
|
...(options.task ? { task: options.task } : {}),
|
|
704
707
|
...(options.engine ? { engine: options.engine } : {}),
|
|
@@ -760,9 +763,16 @@ export async function akmReflect(options = {}) {
|
|
|
760
763
|
}
|
|
761
764
|
else {
|
|
762
765
|
try {
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
+
const qualifiedRef = durableImproveRef(options.ref, options.sourceName);
|
|
767
|
+
const localFilePath = await findAssetFilePath(qualifiedRef, stash);
|
|
768
|
+
if (localFilePath && fs.existsSync(localFilePath)) {
|
|
769
|
+
assetContent = fs.readFileSync(localFilePath, "utf8");
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
const entry = await lookup(parseAssetRef(qualifiedRef));
|
|
773
|
+
if (entry?.filePath && fs.existsSync(entry.filePath)) {
|
|
774
|
+
assetContent = fs.readFileSync(entry.filePath, "utf8");
|
|
775
|
+
}
|
|
766
776
|
}
|
|
767
777
|
}
|
|
768
778
|
catch {
|
|
@@ -809,9 +819,9 @@ export async function akmReflect(options = {}) {
|
|
|
809
819
|
// 4. Build the shared prompt inputs — feedback, hints, lessons, rejected
|
|
810
820
|
// proposals. These are stable across refinement iterations; only the
|
|
811
821
|
// `priorDraft` field changes per-iteration (R-1 / #372).
|
|
812
|
-
const feedback = readRecentFeedback(options.ref);
|
|
822
|
+
const feedback = readRecentFeedback(options.ref ? durableImproveRef(options.ref, options.sourceName) : undefined, options.ref && options.legacyBareState ? bareImproveRef(options.ref) : undefined);
|
|
813
823
|
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
814
|
-
const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(stash, options.ref, parsedRef) : [];
|
|
824
|
+
const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(stash, options.ref, parsedRef, options.sourceName) : [];
|
|
815
825
|
// Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
|
|
816
826
|
// reproducing proposals that have already been reviewed and refused.
|
|
817
827
|
const rejectedProposals = readRejectedProposals(stash, options.ref);
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
*
|
|
46
46
|
* @module salience
|
|
47
47
|
*/
|
|
48
|
+
import path from "node:path";
|
|
48
49
|
import { makeAssetRef } from "../../core/asset/asset-ref.js";
|
|
49
50
|
import { getAllEntries, getUtilityScoresByIds } from "../../indexer/db/db.js";
|
|
50
51
|
import { WARM_START_CAP } from "./outcome-loop.js";
|
|
@@ -424,14 +425,17 @@ export function buildRankChangeReport(oldRanks, newRanks, oldTopN = 200, forgett
|
|
|
424
425
|
* @param indexDb - An open read-capable index database connection.
|
|
425
426
|
* @param refs - The set of asset refs to look up.
|
|
426
427
|
*/
|
|
427
|
-
export function getLastUseMsByRef(indexDb, refs) {
|
|
428
|
+
export function getLastUseMsByRef(indexDb, refs, stashDir) {
|
|
428
429
|
const result = new Map();
|
|
429
430
|
if (refs.length === 0)
|
|
430
431
|
return result;
|
|
431
432
|
const refSet = new Set(refs);
|
|
432
433
|
const allEntries = getAllEntries(indexDb);
|
|
434
|
+
const selectedRoot = stashDir ? path.resolve(stashDir) : undefined;
|
|
433
435
|
const idToRef = new Map();
|
|
434
436
|
for (const indexed of allEntries) {
|
|
437
|
+
if (selectedRoot && path.resolve(indexed.stashDir) !== selectedRoot)
|
|
438
|
+
continue;
|
|
435
439
|
const ref = makeAssetRef(indexed.entry.type, indexed.entry.name);
|
|
436
440
|
if (refSet.has(ref))
|
|
437
441
|
idToRef.set(indexed.id, ref);
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import path from "node:path";
|
|
5
|
+
import { parseAssetRef, refToString } from "../../core/asset/asset-ref.js";
|
|
6
|
+
/** Key durable improve state by source without changing filesystem-facing refs. */
|
|
7
|
+
export function durableImproveRef(ref, sourceName) {
|
|
8
|
+
if (!sourceName)
|
|
9
|
+
return ref;
|
|
10
|
+
const parsed = parseAssetRef(ref);
|
|
11
|
+
return refToString({ ...parsed, origin: parsed.origin ?? sourceName });
|
|
12
|
+
}
|
|
13
|
+
/** Remove a durable source origin before filesystem/index lookups. */
|
|
14
|
+
export function bareImproveRef(ref) {
|
|
15
|
+
const parsed = parseAssetRef(ref);
|
|
16
|
+
return refToString({ type: parsed.type, name: parsed.name });
|
|
17
|
+
}
|
|
18
|
+
/** Qualified key first, with the pre-cutover bare key as an optional fallback. */
|
|
19
|
+
export function improveStateReadRefs(ref, sourceName, includeLegacyBare = false) {
|
|
20
|
+
const durable = durableImproveRef(ref, sourceName);
|
|
21
|
+
const bare = bareImproveRef(ref);
|
|
22
|
+
return includeLegacyBare && durable !== bare ? [durable, bare] : [durable];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Bare improve state predates source-qualified refs and belongs only to the
|
|
26
|
+
* historical local stash. Named sources at any other root must never inherit it.
|
|
27
|
+
*/
|
|
28
|
+
export function shouldReadLegacyBareImproveState(sourceName, sourcePath, config) {
|
|
29
|
+
if (!sourceName || !sourcePath)
|
|
30
|
+
return false;
|
|
31
|
+
if (config.stashDir)
|
|
32
|
+
return path.resolve(sourcePath) === path.resolve(config.stashDir);
|
|
33
|
+
if (sourceName !== "stash")
|
|
34
|
+
return false;
|
|
35
|
+
if (!config.defaultWriteTarget)
|
|
36
|
+
return true;
|
|
37
|
+
if (config.defaultWriteTarget !== "stash")
|
|
38
|
+
return false;
|
|
39
|
+
const configuredStash = config.sources?.find((source) => source.name === "stash");
|
|
40
|
+
return (configuredStash?.type === "filesystem" &&
|
|
41
|
+
typeof configuredStash.path === "string" &&
|
|
42
|
+
path.resolve(configuredStash.path) === path.resolve(sourcePath));
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import { defineGroupCommand, defineJsonCommand } from "../cli/shared.js";
|
|
5
|
+
import { UsageError } from "../core/errors.js";
|
|
6
|
+
const configArg = {
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "Complete operator-prepared current config; optional when the active config is current",
|
|
9
|
+
};
|
|
10
|
+
export const migrateCommand = defineGroupCommand({
|
|
11
|
+
meta: { name: "migrate", description: "Inspect or apply config and durable database migrations" },
|
|
12
|
+
subCommands: {
|
|
13
|
+
status: defineJsonCommand({
|
|
14
|
+
meta: { name: "status", description: "Read-only cross-artifact migration eligibility check" },
|
|
15
|
+
args: { config: configArg },
|
|
16
|
+
async run({ args }) {
|
|
17
|
+
const { runMigrationStatus } = await import("../cli/config-migrate.js");
|
|
18
|
+
await runMigrationStatus({ preparedConfigPath: args.config });
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
apply: defineJsonCommand({
|
|
22
|
+
meta: { name: "apply", description: "Create a verified backup and atomically apply pending migrations" },
|
|
23
|
+
args: {
|
|
24
|
+
config: configArg,
|
|
25
|
+
dryRun: { type: "boolean", default: false, description: "Run the same eligibility checks without mutation" },
|
|
26
|
+
},
|
|
27
|
+
async run({ args }) {
|
|
28
|
+
const { runMigrationApply } = await import("../cli/config-migrate.js");
|
|
29
|
+
await runMigrationApply({ preparedConfigPath: args.config, dryRun: args.dryRun });
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
defaultRun() {
|
|
34
|
+
throw new UsageError("Choose `migrate status` or `migrate apply`.", "MISSING_REQUIRED_ARGUMENT");
|
|
35
|
+
},
|
|
36
|
+
});
|