akm-cli 0.9.6 → 0.9.8-beta.1
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 +408 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +9 -20
- package/dist/commands/improve/preparation.js +53 -37
- package/dist/commands/improve/reflect.js +14 -18
- package/dist/commands/lint/base-linter.js +182 -19
- package/dist/commands/lint/index.js +21 -9
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +51 -18
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +14 -5
- package/dist/core/asset/frontmatter.js +212 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +155 -251
- package/dist/scripts/akm-migrate.js +155 -251
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +7 -1
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
import fs from "node:fs";
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { isScalar, parseDocument } from "yaml";
|
|
37
|
-
import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
37
|
+
import { assetPathCandidatesForName, assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
38
38
|
import { BUNDLE_REF_RE } from "../../core/asset/asset-ref.js";
|
|
39
|
-
import { spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
|
|
39
|
+
import { removeFrontmatterListValues, rewriteFrontmatterListValue, spliceFrontmatterLine, } from "../../core/asset/frontmatter.js";
|
|
40
40
|
import { checkUnquotedDescriptionColon } from "../../core/asset/frontmatter-lint.js";
|
|
41
|
-
import {
|
|
41
|
+
import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
|
|
42
|
+
import { conceptIdFromTypeName, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
42
43
|
import { localDateStamp } from "../../core/common.js";
|
|
43
44
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
44
45
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -191,6 +192,37 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
191
192
|
if (resolveRefPathInStash(relPath, refType, refName, root) !== null)
|
|
192
193
|
return true;
|
|
193
194
|
}
|
|
195
|
+
// #884: a memory pruned by `analyzeMemoryCleanup` was ARCHIVED, not deleted —
|
|
196
|
+
// its bytes and identity live on under `.akm/memory-cleanup/archive`. Inbound
|
|
197
|
+
// belief edges to it are satisfied, not dangling, so resolve the tombstone
|
|
198
|
+
// rather than reporting `missing-ref`. Checked only after every live location
|
|
199
|
+
// misses: a tombstone must never shadow a real file, and the scan then costs
|
|
200
|
+
// one directory read per root instead of one per ref.
|
|
201
|
+
//
|
|
202
|
+
// Existence ONLY. `resolveRefPathInStash` deliberately does NOT consult the
|
|
203
|
+
// archive: it hands back a path callers MUTATE (SPEC-5 `--supersedes`
|
|
204
|
+
// demotion), and writing into an archived tombstone would corrupt the audit
|
|
205
|
+
// record while leaving the live stash untouched.
|
|
206
|
+
return memoryArchiveHasRef(refType, refName, stashRoots);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* True when `(refType, refName)` names a memory that prune archived in any
|
|
210
|
+
* root. Mirrors `resolveRefPathInStash`'s candidate set so a ref that resolved
|
|
211
|
+
* through the `.derived.md` twin (#882) still resolves once archived.
|
|
212
|
+
*/
|
|
213
|
+
function memoryArchiveHasRef(refType, refName, stashRoots) {
|
|
214
|
+
if (refType !== "memory")
|
|
215
|
+
return false; // only memories are ever archived
|
|
216
|
+
const typeDir = stashDirFor(refType);
|
|
217
|
+
if (typeDir === undefined)
|
|
218
|
+
return false;
|
|
219
|
+
const candidates = assetPathCandidatesForName(refType, typeDir, refName);
|
|
220
|
+
for (const root of stashRoots) {
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
if (isArchivedRelPath(candidate, root))
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
194
226
|
return false;
|
|
195
227
|
}
|
|
196
228
|
/**
|
|
@@ -198,8 +230,9 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
198
230
|
* the same reachability rules (in the same order) as
|
|
199
231
|
* {@link refExistsInAnyStash}, which delegates here. Returns the absolute path
|
|
200
232
|
* of the file that makes the ref "exist" — for a multi-file skill directory
|
|
201
|
-
* that is its `SKILL.md` primary
|
|
202
|
-
*
|
|
233
|
+
* that is its `SKILL.md` primary, for a `memory` ref its `.derived.md` twin
|
|
234
|
+
* when the plain `.md` is absent (#882, see `assetPathCandidatesForName`) —
|
|
235
|
+
* or `null` when the ref does not resolve in this root.
|
|
203
236
|
*
|
|
204
237
|
* Extracted for SPEC-5 (`--supersedes` demotion): write commands need the
|
|
205
238
|
* superseded asset's actual file to mutate, and forking a second resolver
|
|
@@ -207,10 +240,14 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
207
240
|
* (the contract pins `refToRelPath` + `refExistsInAnyStash`; this is the
|
|
208
241
|
* shared internal both build on).
|
|
209
242
|
*/
|
|
210
|
-
export function resolveRefPathInStash(relPath,
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
243
|
+
export function resolveRefPathInStash(relPath, refType, refName, root) {
|
|
244
|
+
const typeDir = stashDirFor(refType);
|
|
245
|
+
const candidates = typeDir === undefined ? [relPath] : assetPathCandidatesForName(refType, typeDir, refName);
|
|
246
|
+
for (const candidate of candidates) {
|
|
247
|
+
const absPath = path.join(root, candidate);
|
|
248
|
+
if (fs.existsSync(absPath))
|
|
249
|
+
return absPath;
|
|
250
|
+
}
|
|
214
251
|
return null;
|
|
215
252
|
}
|
|
216
253
|
/**
|
|
@@ -281,11 +318,31 @@ function scanBundleRefs(scanBody, allRoots) {
|
|
|
281
318
|
*/
|
|
282
319
|
function classifyConceptRef(rawConceptId, allRoots) {
|
|
283
320
|
const conceptId = rawConceptId.split("#", 1)[0];
|
|
284
|
-
const parts = typeNameFromConceptId(conceptId);
|
|
321
|
+
const parts = typeNameFromConceptId(conceptId) ?? legacyTypeSlugParts(conceptId);
|
|
285
322
|
if (parts === undefined)
|
|
286
323
|
return null; // foreign type / not a local asset ref
|
|
287
324
|
return localRefMissingRelPath(parts.type, parts.name, allRoots);
|
|
288
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Read-only fallback for the retired `type:slug` xref grammar (see
|
|
328
|
+
* resolve-ref.ts Q-02 — the write boundary no longer emits or accepts it).
|
|
329
|
+
* Stash content written before that retirement still carries it in frontmatter
|
|
330
|
+
* `xrefs:` lists, and `typeNameFromConceptId` (conceptId-only) returns
|
|
331
|
+
* `undefined` for it, so without this it was silently skipped rather than
|
|
332
|
+
* validated (#882). `refToRelPath`/`refExistsInAnyStash` already key off a bare
|
|
333
|
+
* `(type, slug)` pair — the same shape `type:slug` already is — so this only
|
|
334
|
+
* needs to split the token; no new resolution logic.
|
|
335
|
+
*/
|
|
336
|
+
function legacyTypeSlugParts(rawConceptId) {
|
|
337
|
+
const colon = rawConceptId.indexOf(":");
|
|
338
|
+
if (colon <= 0)
|
|
339
|
+
return undefined;
|
|
340
|
+
const type = rawConceptId.slice(0, colon);
|
|
341
|
+
const name = rawConceptId.slice(colon + 1);
|
|
342
|
+
if (!name || name.includes("/") || name.includes(":"))
|
|
343
|
+
return undefined;
|
|
344
|
+
return stashDirFor(type) === undefined ? undefined : { type, name };
|
|
345
|
+
}
|
|
289
346
|
/**
|
|
290
347
|
* Returns an array of {ref, resolvedRelPath} for every local AKM ref in the
|
|
291
348
|
* PROSE body that does not resolve to a real file under any of the provided
|
|
@@ -364,6 +421,12 @@ function dedupeMissing(rows) {
|
|
|
364
421
|
* excluded because it can point at merged-away or pruned assets.
|
|
365
422
|
*/
|
|
366
423
|
const XREF_FRONTMATTER_KEYS = ["xrefs", "supersededBy", "contradictedBy"];
|
|
424
|
+
/**
|
|
425
|
+
* The belief-graph subset of {@link XREF_FRONTMATTER_KEYS} — the only channels
|
|
426
|
+
* `--prune-dangling-edges` will repair (#884). Typed as `readonly string[]` so
|
|
427
|
+
* `.includes` accepts any xref key without a cast.
|
|
428
|
+
*/
|
|
429
|
+
const BELIEF_EDGE_KEYS = ["supersededBy", "contradictedBy"];
|
|
367
430
|
/**
|
|
368
431
|
* Return the `refs:` array from frontmatter when it is present and is an
|
|
369
432
|
* array of strings; otherwise return `null` to signal the caller should
|
|
@@ -597,12 +660,22 @@ export function runBaseChecks(ctx) {
|
|
|
597
660
|
});
|
|
598
661
|
}
|
|
599
662
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Flush accumulated mutations to disk. Called after the fixable checks above
|
|
665
|
+
* AND again at the very end, because the `missing-ref` section below can also
|
|
666
|
+
* mutate (`--prune-dangling-edges`, #884) and used to run PAST this point —
|
|
667
|
+
* its edits were computed and then silently dropped.
|
|
668
|
+
*
|
|
669
|
+
* Mirrors the two stub-delete call sites in `commands/lint/index.ts`
|
|
670
|
+
* (`appendMemoryStubIssue`/`appendWorkflowStubIssue`), which already report
|
|
671
|
+
* a failed mutation as `fixed: "failed"` instead of throwing. Without this
|
|
672
|
+
* the sweep aborted mid-run on the first unwritable file and the caller got
|
|
673
|
+
* an exception instead of a result naming the fixes that HAD landed.
|
|
674
|
+
*/
|
|
675
|
+
const flushIfModified = () => {
|
|
676
|
+
if (!modified)
|
|
677
|
+
return;
|
|
678
|
+
modified = false;
|
|
606
679
|
try {
|
|
607
680
|
fs.writeFileSync(ctx.filePath, currentRaw, "utf8");
|
|
608
681
|
// Propagate the mutated raw back so subclasses can re-parse if needed
|
|
@@ -615,7 +688,8 @@ export function runBaseChecks(ctx) {
|
|
|
615
688
|
issue.detail = `${issue.detail} — could not write fix: ${reason}`;
|
|
616
689
|
}
|
|
617
690
|
}
|
|
618
|
-
}
|
|
691
|
+
};
|
|
692
|
+
flushIfModified();
|
|
619
693
|
// ── 3. stale-path ──────────────────────────────────────────────────────
|
|
620
694
|
// M3: checkStalePath returns all stale matches; push one issue per path.
|
|
621
695
|
// M4: Also scan ctx.frontmatter for stale paths (absolute paths in frontmatter).
|
|
@@ -636,6 +710,26 @@ export function runBaseChecks(ctx) {
|
|
|
636
710
|
}
|
|
637
711
|
}
|
|
638
712
|
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
713
|
+
const missingRefPass = runMissingRefChecks(ctx, currentRaw, shouldRun, pendingFixes);
|
|
714
|
+
issues.push(...missingRefPass.issues);
|
|
715
|
+
if (missingRefPass.modified) {
|
|
716
|
+
currentRaw = missingRefPass.raw;
|
|
717
|
+
modified = true;
|
|
718
|
+
}
|
|
719
|
+
flushIfModified();
|
|
720
|
+
return issues;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The `missing-ref` pass, extracted from {@link runBaseChecks} so that function
|
|
724
|
+
* stays under the src fn-size ratchet after #884 gave this pass its own
|
|
725
|
+
* (opt-in) mutation path. Pure move plus the explicit state hand-off: `raw` in,
|
|
726
|
+
* `{raw, modified}` out, `issues`/`pendingFixes` appended in place.
|
|
727
|
+
*/
|
|
728
|
+
function runMissingRefChecks(ctx, raw, shouldRun, pendingFixes) {
|
|
729
|
+
const issues = [];
|
|
730
|
+
let currentRaw = raw;
|
|
731
|
+
let modified = false;
|
|
732
|
+
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
639
733
|
// Carve-out for assets that declare an explicit `refs:` array in
|
|
640
734
|
// frontmatter (e.g. session-checkpoint memories captured by the
|
|
641
735
|
// claude-code hook). The frontmatter array is the *authoritative*
|
|
@@ -650,7 +744,9 @@ export function runBaseChecks(ctx) {
|
|
|
650
744
|
// still run `checkMissingRefs` against the array itself to catch
|
|
651
745
|
// refs that were valid at capture time but later removed from the
|
|
652
746
|
// stash.
|
|
653
|
-
if (shouldRun("missing-ref"))
|
|
747
|
+
if (!shouldRun("missing-ref"))
|
|
748
|
+
return { issues, raw, modified };
|
|
749
|
+
{
|
|
654
750
|
const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
|
|
655
751
|
// An explicit `refs:` array is a REF LIST (each value is a whole ref —
|
|
656
752
|
// short conceptIds included); a bare body is PROSE (anchored refs only).
|
|
@@ -690,7 +786,74 @@ export function runBaseChecks(ctx) {
|
|
|
690
786
|
const values = readRefStringOrArray(ctx.data?.[key]);
|
|
691
787
|
if (values === null)
|
|
692
788
|
continue;
|
|
789
|
+
// `lint --fix` migration for the retired `type:slug` xref grammar
|
|
790
|
+
// (see legacyTypeSlugParts): rewrite a value to its conceptId form
|
|
791
|
+
// ONLY when the rewritten spelling still resolves to a real asset —
|
|
792
|
+
// a dangling legacy ref must stay reported as `missing-ref` in its
|
|
793
|
+
// original spelling, never rewritten into a differently-spelled
|
|
794
|
+
// dangling ref. Runs before the missing-ref scan below so a value
|
|
795
|
+
// this loop rewrites is checked (and reported, if still dangling)
|
|
796
|
+
// under its ORIGINAL spelling by that scan, and never double-fixed.
|
|
797
|
+
if (ctx.fix) {
|
|
798
|
+
const legacyRewrites = new Map();
|
|
799
|
+
for (const raw of values) {
|
|
800
|
+
const value = raw.trim();
|
|
801
|
+
const parts = legacyTypeSlugParts(value);
|
|
802
|
+
if (parts === undefined)
|
|
803
|
+
continue;
|
|
804
|
+
const relPath = refToRelPath(parts.type, parts.name);
|
|
805
|
+
if (relPath === null)
|
|
806
|
+
continue;
|
|
807
|
+
if (!refExistsInAnyStash(relPath, parts.type, parts.name, [ctx.stashRoot, ...(ctx.extraStashRoots ?? [])]))
|
|
808
|
+
continue; // dangling — leave reported as missing-ref, unrewritten
|
|
809
|
+
legacyRewrites.set(value, conceptIdFromTypeName(parts.type, parts.name));
|
|
810
|
+
}
|
|
811
|
+
if (legacyRewrites.size > 0) {
|
|
812
|
+
const rewritten = rewriteFrontmatterListValue(currentRaw, key, legacyRewrites);
|
|
813
|
+
if (rewritten !== null) {
|
|
814
|
+
currentRaw = rewritten;
|
|
815
|
+
modified = true;
|
|
816
|
+
for (const [oldValue, newValue] of legacyRewrites) {
|
|
817
|
+
const issue = {
|
|
818
|
+
file: ctx.relPath,
|
|
819
|
+
issue: "missing-ref",
|
|
820
|
+
detail: `legacy xref grammar migrated: ${key} ${oldValue} -> ${newValue}`,
|
|
821
|
+
fixed: true,
|
|
822
|
+
};
|
|
823
|
+
issues.push(issue);
|
|
824
|
+
pendingFixes.push(issue);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
693
829
|
const missingXrefs = checkMissingRefsInList(values, ctx.stashRoot, ctx.extraStashRoots);
|
|
830
|
+
// #884 opt-in repair. Scoped to the BELIEF channels only: an edge whose
|
|
831
|
+
// target has neither a file nor a prune tombstone asserts a
|
|
832
|
+
// relationship to a memory that no longer exists in any form, and
|
|
833
|
+
// carrying it forward corrupts every belief-graph read. `xrefs` is
|
|
834
|
+
// excluded — a stale xref is an ordinary broken link (the 3 skill refs
|
|
835
|
+
// in #884), and repairing it by DELETION would throw away a pointer the
|
|
836
|
+
// author may simply need to re-target.
|
|
837
|
+
const repairable = ctx.pruneDanglingEdges === true && BELIEF_EDGE_KEYS.includes(key) && missingXrefs.length > 0;
|
|
838
|
+
if (repairable) {
|
|
839
|
+
const dropped = missingXrefs.map(({ ref }) => ref);
|
|
840
|
+
const rewritten = removeFrontmatterListValues(currentRaw, key, dropped);
|
|
841
|
+
if (rewritten !== null) {
|
|
842
|
+
currentRaw = rewritten;
|
|
843
|
+
modified = true;
|
|
844
|
+
for (const { ref, resolvedRelPath } of missingXrefs) {
|
|
845
|
+
const issue = {
|
|
846
|
+
file: ctx.relPath,
|
|
847
|
+
issue: "missing-ref",
|
|
848
|
+
detail: `dangling ${key} edge dropped: ${ref} (no file and no prune tombstone at ${resolvedRelPath})`,
|
|
849
|
+
fixed: true,
|
|
850
|
+
};
|
|
851
|
+
issues.push(issue);
|
|
852
|
+
pendingFixes.push(issue);
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
694
857
|
for (const { ref, resolvedRelPath } of missingXrefs) {
|
|
695
858
|
issues.push({
|
|
696
859
|
file: ctx.relPath,
|
|
@@ -702,5 +865,5 @@ export function runBaseChecks(ctx) {
|
|
|
702
865
|
}
|
|
703
866
|
}
|
|
704
867
|
}
|
|
705
|
-
return issues;
|
|
868
|
+
return { issues, raw: currentRaw, modified };
|
|
706
869
|
}
|
|
@@ -11,14 +11,13 @@ import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
|
11
11
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
12
12
|
import { conceptIdForStashFile, displayRefForConceptId } from "../../core/asset/resolve-ref.js";
|
|
13
13
|
import { deriveBundleIds } from "../../core/bundle-id.js";
|
|
14
|
-
import { isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
|
|
14
|
+
import { compareCodePoints, isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
|
|
15
15
|
import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
|
|
16
16
|
import { UsageError } from "../../core/errors.js";
|
|
17
17
|
import { warn } from "../../core/warn.js";
|
|
18
18
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
19
19
|
import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail } from "../../tasks/source-v3.js";
|
|
20
20
|
import { resolveWorkflowSourceDomains } from "../../workflows/source-files.js";
|
|
21
|
-
import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/compare.js";
|
|
22
21
|
import { runBaseChecks } from "./base-linter.js";
|
|
23
22
|
import { checkEnvForDangerousKeys } from "./env-key-rules.js";
|
|
24
23
|
import { isAdvisoryLintIssue } from "./types.js";
|
|
@@ -87,7 +86,7 @@ function collectWorkflowFiles(dir) {
|
|
|
87
86
|
const results = [];
|
|
88
87
|
for (const entry of fs
|
|
89
88
|
.readdirSync(dir, { withFileTypes: true })
|
|
90
|
-
.sort((left, right) =>
|
|
89
|
+
.sort((left, right) => compareCodePoints(left.name, right.name))) {
|
|
91
90
|
if (entry.name === ".git")
|
|
92
91
|
continue;
|
|
93
92
|
const full = path.join(dir, entry.name);
|
|
@@ -103,7 +102,7 @@ function collectWorkflowFiles(dir) {
|
|
|
103
102
|
if (extension === ".md" || extension === ".yml")
|
|
104
103
|
results.push(full);
|
|
105
104
|
}
|
|
106
|
-
return results.sort(
|
|
105
|
+
return results.sort(compareCodePoints);
|
|
107
106
|
}
|
|
108
107
|
/**
|
|
109
108
|
* Resolve the already-enumerated candidates through the same workflow-source
|
|
@@ -131,7 +130,7 @@ function resolveWorkflowLintOwnership(stashRoot, files) {
|
|
|
131
130
|
fixed: false,
|
|
132
131
|
});
|
|
133
132
|
}
|
|
134
|
-
return { files: ownedFiles.sort(
|
|
133
|
+
return { files: ownedFiles.sort(compareCodePoints), issues };
|
|
135
134
|
}
|
|
136
135
|
// ── Non-akm adapter dispatch (real `adapter.validate()`, not a re-implementation) ──
|
|
137
136
|
//
|
|
@@ -385,8 +384,8 @@ function assertFixTargetWritable(stashRoot, sources) {
|
|
|
385
384
|
return;
|
|
386
385
|
// Same error kind and code `write-source.ts#ensureWritable` raises for the
|
|
387
386
|
// identical refusal, so a scripted caller classifies both the same way.
|
|
388
|
-
throw new UsageError(`lint
|
|
389
|
-
"Run `akm lint` without --fix to report findings, or set `writable: true` on the bundle.", "INVALID_FLAG_VALUE");
|
|
387
|
+
throw new UsageError(`lint: bundle "${stashRoot}" is configured \`writable: false\`; refusing to modify it. ` +
|
|
388
|
+
"Run `akm lint` without --fix / --prune-dangling-edges to report findings, or set `writable: true` on the bundle.", "INVALID_FLAG_VALUE");
|
|
390
389
|
}
|
|
391
390
|
/** True when the issue represents a file deletion that was successfully applied. */
|
|
392
391
|
function isFileDeletion(issue) {
|
|
@@ -523,7 +522,9 @@ export function lintAssetFile(ctx, subdir) {
|
|
|
523
522
|
*/
|
|
524
523
|
function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
525
524
|
const fix = options.fix === true;
|
|
526
|
-
|
|
525
|
+
// #884: the dangling-edge repair writes too, so it clears the same
|
|
526
|
+
// read-only gate `--fix` does.
|
|
527
|
+
if (fix || options.pruneDanglingEdges === true)
|
|
527
528
|
assertFixTargetWritable(stashRoot, sources);
|
|
528
529
|
const fixed = [];
|
|
529
530
|
const flagged = [];
|
|
@@ -627,7 +628,18 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
627
628
|
try {
|
|
628
629
|
issues = [
|
|
629
630
|
...fileIssues,
|
|
630
|
-
...lintAssetFile({
|
|
631
|
+
...lintAssetFile({
|
|
632
|
+
filePath,
|
|
633
|
+
relPath,
|
|
634
|
+
raw,
|
|
635
|
+
data,
|
|
636
|
+
body,
|
|
637
|
+
frontmatter,
|
|
638
|
+
fix,
|
|
639
|
+
pruneDanglingEdges: options.pruneDanglingEdges === true,
|
|
640
|
+
stashRoot,
|
|
641
|
+
extraStashRoots,
|
|
642
|
+
}, subdir),
|
|
631
643
|
];
|
|
632
644
|
}
|
|
633
645
|
catch (e) {
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
* Legacy `extraParams` -> first-class-field lift (#852, following #815), as
|
|
6
|
+
* an `akm migrate` concern.
|
|
7
|
+
*
|
|
8
|
+
* This used to run silently, in memory, on every config load
|
|
9
|
+
* (`liftLegacyEngineExtraParams` called from `parseAndValidateConfigText`)
|
|
10
|
+
* and never wrote the result back — so the lift, and its warning, recurred
|
|
11
|
+
* forever. config.json is akm-owned plain JSON; the lift is deterministic
|
|
12
|
+
* and total, so — like the dead-residue cleanup in `./dead-residue.ts` —
|
|
13
|
+
* it belongs in `akm migrate`, run once, persisted. `parseAndValidateConfigText`
|
|
14
|
+
* now fails closed on an unmigrated config instead of lifting it.
|
|
15
|
+
*/
|
|
16
|
+
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, writeConfigAtomic, } from "../../core/config/config-io.js";
|
|
17
|
+
import { liftLegacyEngineExtraParams } from "../../core/extra-params.js";
|
|
18
|
+
function readRawConfig(configPath) {
|
|
19
|
+
const text = readConfigText(configPath);
|
|
20
|
+
if (text === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return parseConfigText(text, configPath);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read-only: what `akm migrate apply` would lift (or flag as conflicting) in
|
|
26
|
+
* `config.json`'s `extraParams`. Never touches disk. Returns an empty plan
|
|
27
|
+
* when the config file does not exist.
|
|
28
|
+
*/
|
|
29
|
+
export function findConfigExtraParamsLift(configPath) {
|
|
30
|
+
const raw = readRawConfig(configPath);
|
|
31
|
+
if (!raw)
|
|
32
|
+
return { lifted: [], conflicts: [] };
|
|
33
|
+
const { lifted, conflicts } = liftLegacyEngineExtraParams(raw);
|
|
34
|
+
return { lifted, conflicts };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Persist the legacy extraParams -> first-class field lift to `config.json`,
|
|
38
|
+
* once. Best-effort like `removeDeadResidue`: a genuine conflict (extraParams
|
|
39
|
+
* and the first-class field set to different values) is left untouched here
|
|
40
|
+
* — `parseAndValidateConfigText` already hard-rejects that config at every
|
|
41
|
+
* load with the exact mismatch, so a second, weaker error here would only be
|
|
42
|
+
* noise — and is reported back via `conflicts` instead.
|
|
43
|
+
*/
|
|
44
|
+
export function applyConfigExtraParamsLift(configPath) {
|
|
45
|
+
const raw = readRawConfig(configPath);
|
|
46
|
+
if (!raw)
|
|
47
|
+
return { applied: false, lifted: [], conflicts: [] };
|
|
48
|
+
const { config, lifted, conflicts } = liftLegacyEngineExtraParams(raw);
|
|
49
|
+
if (conflicts.length > 0 || lifted.length === 0) {
|
|
50
|
+
return { applied: false, lifted, conflicts };
|
|
51
|
+
}
|
|
52
|
+
const release = acquireConfigLock();
|
|
53
|
+
try {
|
|
54
|
+
backupExistingConfig(configPath);
|
|
55
|
+
writeConfigAtomic(configPath, config);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
release();
|
|
59
|
+
}
|
|
60
|
+
return { applied: true, lifted, conflicts: [] };
|
|
61
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead pre-0.9.0 `$STASH/.akm` residue: superseded filesystem layouts with no
|
|
3
|
+
* live reader or writer anywhere in src/ (each verified by grep before being
|
|
4
|
+
* listed — see #889 for the audit). The largest, `.akm/proposals/`, was
|
|
5
|
+
* replaced by the `proposals` table in state.db in 0.9.0 and measured 135 MB
|
|
6
|
+
* on a real bundle.
|
|
7
|
+
*
|
|
8
|
+
* This lives under `migrate/` because removing a superseded layout IS
|
|
9
|
+
* migration — the tail end of the moves that created these paths' replacements.
|
|
10
|
+
* It was first shipped as a bolted-on `akm health --clean-dead-residue` flag
|
|
11
|
+
* plus a health advisory; that was the wrong shape (a special-purpose switch
|
|
12
|
+
* apologizing for migrations that did not finish their own job) and was
|
|
13
|
+
* removed. `akm migrate status` reports what is here; `akm migrate apply`
|
|
14
|
+
* removes it, exactly as it applies every other pending migration.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
const DEAD_RESIDUE_PATHS = [
|
|
19
|
+
{ name: "proposals", reason: "superseded by the `proposals` table in $DATA/state.db (0.9.0)" },
|
|
20
|
+
{ prefix: "runs.archived-", reason: "orphaned archive of a directory that no longer exists" },
|
|
21
|
+
{
|
|
22
|
+
name: "archive",
|
|
23
|
+
reason: "legacy consolidation archive; current prune writes .akm/memory-cleanup/archive/ instead",
|
|
24
|
+
},
|
|
25
|
+
{ name: "graph.json", reason: "superseded by the graph_* tables in $DATA/index.db" },
|
|
26
|
+
{ name: "consolidate-journal.json", reason: "legacy consolidation journal; unused" },
|
|
27
|
+
{ name: "proposals.db", reason: "empty legacy database file" },
|
|
28
|
+
{ name: "mv-transactions", reason: "legacy fs-txn journal location; unused" },
|
|
29
|
+
];
|
|
30
|
+
function dirSizeBytes(target) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const entryPath = path.join(target, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
total += dirSizeBytes(entryPath);
|
|
43
|
+
}
|
|
44
|
+
else if (entry.isFile()) {
|
|
45
|
+
try {
|
|
46
|
+
total += fs.statSync(entryPath).size;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Skip files that vanish or are inaccessible between readdir and stat.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return total;
|
|
54
|
+
}
|
|
55
|
+
function sizeOf(target) {
|
|
56
|
+
const st = fs.statSync(target);
|
|
57
|
+
return st.isDirectory() ? dirSizeBytes(target) : st.size;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Find every Tier-1 dead-residue path that actually exists under
|
|
61
|
+
* `$STASH/.akm`, with its computed size. Read-only — never deletes.
|
|
62
|
+
*/
|
|
63
|
+
export function findDeadResidueEntries(stashDir) {
|
|
64
|
+
const akmDir = path.join(stashDir, ".akm");
|
|
65
|
+
let names;
|
|
66
|
+
try {
|
|
67
|
+
names = fs.readdirSync(akmDir);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const found = [];
|
|
73
|
+
for (const spec of DEAD_RESIDUE_PATHS) {
|
|
74
|
+
const matches = spec.name !== undefined ? [spec.name] : names.filter((n) => n.startsWith(spec.prefix ?? "\0"));
|
|
75
|
+
for (const name of matches) {
|
|
76
|
+
if (!names.includes(name))
|
|
77
|
+
continue;
|
|
78
|
+
const absolutePath = path.join(akmDir, name);
|
|
79
|
+
let sizeBytes;
|
|
80
|
+
try {
|
|
81
|
+
sizeBytes = sizeOf(absolutePath);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue; // vanished between readdir and stat
|
|
85
|
+
}
|
|
86
|
+
found.push({ relativePath: path.join(".akm", name), absolutePath, sizeBytes, reason: spec.reason });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Delete every Tier-1 dead-residue path found under `$STASH/.akm`. Only
|
|
93
|
+
* invoked when the caller has explicitly opted in (`akm health
|
|
94
|
+
* --clean-dead-residue`) — never as a side effect of a plain `akm health`
|
|
95
|
+
* read. Best-effort per-path: one failure does not abort the rest.
|
|
96
|
+
*/
|
|
97
|
+
export function removeDeadResidue(stashDir) {
|
|
98
|
+
const entries = findDeadResidueEntries(stashDir);
|
|
99
|
+
return entries.map((entry) => {
|
|
100
|
+
try {
|
|
101
|
+
fs.rmSync(entry.absolutePath, { recursive: true, force: true });
|
|
102
|
+
return { relativePath: entry.relativePath, sizeBytes: entry.sizeBytes, removed: true };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
relativePath: entry.relativePath,
|
|
107
|
+
sizeBytes: entry.sizeBytes,
|
|
108
|
+
removed: false,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
* Stale durable-transaction journals (`$DATA/txn/<rootNs24>/<id>/journal.json`,
|
|
6
|
+
* see `core/fs-txn.ts`) that never got recovered — a crash mid-transaction
|
|
7
|
+
* left a journal on disk with no process left to finish or roll it back.
|
|
8
|
+
* `akm health` used to only report these and point at a troubleshooting doc
|
|
9
|
+
* (issue: an advisory that tells the user to go read docs instead of the
|
|
10
|
+
* tool recovering its own interrupted state). Recovery IS a migration
|
|
11
|
+
* concern like dead `.akm/` residue: `akm migrate status` names what is
|
|
12
|
+
* here for the stash root, `akm migrate apply` recovers it, exactly like
|
|
13
|
+
* `dead-residue.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Recovery goes through {@link recoverTxnsForRoot}, which requires every
|
|
16
|
+
* live kind's registrar to be IMPORTED first so its handler is registered
|
|
17
|
+
* (see fs-txn.ts's module docs). `proposal/repository.ts` registers the
|
|
18
|
+
* `proposal`/`proposal-reject` kinds specifically so "ANY recovery entry
|
|
19
|
+
* point ... can finish or roll back an interrupted proposal mutation for a
|
|
20
|
+
* root it touches" (its own comment on the registration) — this module is
|
|
21
|
+
* exactly that kind of entry point.
|
|
22
|
+
*/
|
|
23
|
+
import { canonicalTxnRoot, listTxnJournalsTolerant, recoverTxnsForRoot } from "../../core/fs-txn.js";
|
|
24
|
+
// Side-effect import: registers the `proposal`/`proposal-reject` txn kinds
|
|
25
|
+
// so recovery below can roll them forward/back for the stash root.
|
|
26
|
+
import "../proposal/repository.js";
|
|
27
|
+
/**
|
|
28
|
+
* Find every durable-transaction journal bound to `stashDir`'s namespace.
|
|
29
|
+
* Read-only, tolerant of a corrupt journal (counted, not thrown on) — mirrors
|
|
30
|
+
* `findDeadResidueEntries`'s read-only/never-mutates contract.
|
|
31
|
+
*/
|
|
32
|
+
export function findStaleTxnEntries(stashDir) {
|
|
33
|
+
const root = canonicalTxnRoot(stashDir);
|
|
34
|
+
const { matches } = listTxnJournalsTolerant((j) => canonicalTxnRoot(j.root) === root);
|
|
35
|
+
return matches.map(({ journal }) => journalToEntry(journal));
|
|
36
|
+
}
|
|
37
|
+
function journalToEntry(journal) {
|
|
38
|
+
return { transactionId: journal.transactionId, kind: journal.kind, phase: journal.phase, root: journal.root };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Recover every durable transaction bound to `stashDir`'s namespace: roll
|
|
42
|
+
* back journals before their kind's commit point, roll forward the rest.
|
|
43
|
+
* The counterpart to {@link findStaleTxnEntries}, invoked only from `akm
|
|
44
|
+
* migrate apply`.
|
|
45
|
+
*/
|
|
46
|
+
export async function recoverStaleTxns(stashDir) {
|
|
47
|
+
const recovered = await recoverTxnsForRoot(stashDir);
|
|
48
|
+
return recovered.map(journalToEntry);
|
|
49
|
+
}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
5
|
+
import { resolveStashDir } from "../core/common.js";
|
|
6
|
+
import { ConfigError } from "../core/errors.js";
|
|
7
|
+
import { getConfigPath } from "../core/paths.js";
|
|
8
|
+
import { applyConfigExtraParamsLift, findConfigExtraParamsLift } from "./migrate/config-extra-params.js";
|
|
9
|
+
import { findDeadResidueEntries, removeDeadResidue } from "./migrate/dead-residue.js";
|
|
10
|
+
import { findStaleTxnEntries, recoverStaleTxns } from "./migrate/stale-txn.js";
|
|
5
11
|
import { runMigrationTool } from "./migration-tool.js";
|
|
6
12
|
async function callMigrateTool(args, runTool) {
|
|
7
13
|
const result = await runTool(args);
|
|
@@ -79,6 +85,25 @@ export function resolveGenerationStatus(call, label) {
|
|
|
79
85
|
* got to look at a stable tree in that case.
|
|
80
86
|
*/
|
|
81
87
|
export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runTool = runMigrationTool) {
|
|
88
|
+
// Superseded pre-0.9.0 .akm layouts are a migration concern like any other:
|
|
89
|
+
// status names them, apply removes them. (First shipped as a bolted-on
|
|
90
|
+
// `health --clean-dead-residue` flag; folded here where it belongs.) The
|
|
91
|
+
// legacy extraParams -> first-class-field config lift (#852) is the same
|
|
92
|
+
// shape: status names it, apply persists it once instead of the old
|
|
93
|
+
// permanent silent lift on every config load.
|
|
94
|
+
// No configured bundle means there is no stash to scan — an empty domain,
|
|
95
|
+
// not an error — so migrate still works before `akm bundle create`. Any
|
|
96
|
+
// OTHER ConfigError propagates.
|
|
97
|
+
let stashDir;
|
|
98
|
+
try {
|
|
99
|
+
stashDir = resolveStashDir();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!(error instanceof ConfigError) || error.code !== "STASH_DIR_NOT_FOUND")
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const configPath = getConfigPath();
|
|
106
|
+
const applyResidue = command === "migrate-apply" && !genOneArgs.includes("--dry-run");
|
|
82
107
|
const first = await callMigrateTool(genOneArgs, runTool);
|
|
83
108
|
if (first.status !== EXIT_CODES.SUCCESS && first.status !== EXIT_CODES.GENERAL) {
|
|
84
109
|
process.exitCode = first.status;
|
|
@@ -95,7 +120,23 @@ export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runT
|
|
|
95
120
|
return;
|
|
96
121
|
}
|
|
97
122
|
const combined = combineMigrationPlans(first, second);
|
|
98
|
-
|
|
123
|
+
// The stash-scoped sections need a bundle to scan; no configured bundle is
|
|
124
|
+
// an empty domain, not an error, so migrate still works before
|
|
125
|
+
// `akm bundle create`. The config lift is config-scoped and always runs.
|
|
126
|
+
const stashSections = stashDir === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {
|
|
129
|
+
deadResidue: applyResidue
|
|
130
|
+
? { removed: removeDeadResidue(stashDir) }
|
|
131
|
+
: { pending: findDeadResidueEntries(stashDir) },
|
|
132
|
+
staleTxns: applyResidue
|
|
133
|
+
? { recovered: await recoverStaleTxns(stashDir) }
|
|
134
|
+
: { pending: findStaleTxnEntries(stashDir) },
|
|
135
|
+
};
|
|
136
|
+
const configExtraParams = applyResidue
|
|
137
|
+
? applyConfigExtraParamsLift(configPath)
|
|
138
|
+
: { pending: findConfigExtraParamsLift(configPath) };
|
|
139
|
+
output(command, { ...combined, ...stashSections, configExtraParams });
|
|
99
140
|
if (combined.status === "blocked")
|
|
100
141
|
process.exitCode = EXIT_CODES.GENERAL;
|
|
101
142
|
}
|