akm-cli 0.9.7 → 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 +218 -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/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 +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- 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 +4 -18
- 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 +1 -3
- package/dist/core/asset/frontmatter.js +96 -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/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 +142 -244
- package/dist/scripts/akm-migrate.js +142 -244
- 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/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- 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/launchd.js +8 -18
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- 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 +2 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -9,7 +9,9 @@ import { MEMORY_ARCHIVE_REL } from "../../../core/asset/memory-archive.js";
|
|
|
9
9
|
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
10
10
|
import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
|
|
11
11
|
import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
|
|
12
|
+
import { warn } from "../../../core/warn.js";
|
|
12
13
|
import { recordWrittenPath } from "../../../core/write-provenance.js";
|
|
14
|
+
import { walkMarkdownFiles } from "../../../indexer/walk/walker.js";
|
|
13
15
|
import { isDerivedMemory, memoryIdentityRef, parseMemoryName, resolveParentRef } from "./derived-ref.js";
|
|
14
16
|
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
15
17
|
const records = collectDerivedMemories(stashDir, options.parentRef);
|
|
@@ -574,7 +576,11 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
|
|
|
574
576
|
if (!fs.existsSync(memoriesDir))
|
|
575
577
|
return [];
|
|
576
578
|
const records = [];
|
|
577
|
-
|
|
579
|
+
const walked = walkMarkdownFiles(memoriesDir);
|
|
580
|
+
if (!walked.complete) {
|
|
581
|
+
warn(`memory improve: directory scan under ${memoriesDir} is incomplete — some derived memories may be missing`);
|
|
582
|
+
}
|
|
583
|
+
for (const filePath of walked.files) {
|
|
578
584
|
const name = toMemoryName(memoriesDir, filePath);
|
|
579
585
|
if (!name)
|
|
580
586
|
continue;
|
|
@@ -778,24 +784,6 @@ function firstNonEmpty(values) {
|
|
|
778
784
|
}
|
|
779
785
|
return undefined;
|
|
780
786
|
}
|
|
781
|
-
function* walkMarkdownFiles(root) {
|
|
782
|
-
let entries;
|
|
783
|
-
try {
|
|
784
|
-
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
785
|
-
}
|
|
786
|
-
catch {
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
for (const entry of entries) {
|
|
790
|
-
const full = path.join(root, entry.name);
|
|
791
|
-
if (entry.isDirectory()) {
|
|
792
|
-
yield* walkMarkdownFiles(full);
|
|
793
|
-
}
|
|
794
|
-
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
795
|
-
yield full;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
787
|
function toMemoryName(memoriesDir, filePath) {
|
|
800
788
|
const rel = path.relative(memoriesDir, filePath);
|
|
801
789
|
if (!rel || rel.startsWith(".."))
|
|
@@ -789,6 +789,14 @@ export async function runImprovePreparationStage(args) {
|
|
|
789
789
|
snapshot,
|
|
790
790
|
persist: !planOnly,
|
|
791
791
|
});
|
|
792
|
+
const eligibilitySourceByRef = stampEligibilitySource({
|
|
793
|
+
scope,
|
|
794
|
+
processableRefs: gathered.processableRefs,
|
|
795
|
+
mergedRefs: gathered.mergedRefs,
|
|
796
|
+
signalFiltered: gathered.signalFiltered,
|
|
797
|
+
proactiveRefs: gathered.proactiveRefs,
|
|
798
|
+
highSalienceRefs: gathered.highSalienceRefs,
|
|
799
|
+
});
|
|
792
800
|
// Shared admission boundary for every synthetic fallback lane. Cleanup and
|
|
793
801
|
// structural validation are exclusive selectors: no later rank/replay state
|
|
794
802
|
// may re-create a candidate they removed. Keep the exact surviving objects
|
|
@@ -800,7 +808,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
800
808
|
primaryStashDir,
|
|
801
809
|
eventsCtx,
|
|
802
810
|
mergedRefs: gathered.mergedRefs,
|
|
803
|
-
eligibilitySourceByRef
|
|
811
|
+
eligibilitySourceByRef,
|
|
804
812
|
feedbackSummary: gathered.feedbackSummary,
|
|
805
813
|
retrievalCounts: gathered.retrievalCounts,
|
|
806
814
|
signalFiltered: gathered.signalFiltered,
|
|
@@ -820,7 +828,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
820
828
|
eventsCtx,
|
|
821
829
|
mergedRefs: scored.mergedRefs,
|
|
822
830
|
salienceMap: scored.salienceMap,
|
|
823
|
-
eligibilitySourceByRef
|
|
831
|
+
eligibilitySourceByRef,
|
|
824
832
|
distillOnlyRefs: gathered.distillOnlyRefs,
|
|
825
833
|
validationFailureRefs,
|
|
826
834
|
summary: {
|
|
@@ -1054,7 +1062,9 @@ export function buildSnapshotManifest(args) {
|
|
|
1054
1062
|
/**
|
|
1055
1063
|
* Pass: candidate-gather — the signal-delta partition, the bulk feedback
|
|
1056
1064
|
* summary, retrieval signals, the Layer-2 proactive and Layer-3 high-salience
|
|
1057
|
-
* rescue lanes, the merged candidate set
|
|
1065
|
+
* rescue lanes, and the merged candidate set. Lane attribution stamping is a
|
|
1066
|
+
* separate pass — see `stampEligibilitySource` — run by the caller once
|
|
1067
|
+
* `mergedRefs` is known.
|
|
1058
1068
|
*/
|
|
1059
1069
|
function gatherCandidates(args) {
|
|
1060
1070
|
const { scope, options, primaryStashDir, eventsCtx, improveProfile, resolvedPlan, postCleanupRefs, persist } = args;
|
|
@@ -1152,23 +1162,44 @@ function gatherCandidates(args) {
|
|
|
1152
1162
|
// that want feedback-only runs.
|
|
1153
1163
|
const signalAndRetrievalRefs = dedupeRefs([...signalFiltered, ...proactiveRefs, ...highSalienceRefs]);
|
|
1154
1164
|
const mergedRefs = scope.mode === "ref" ? processableRefs : options.requireFeedbackSignal ? signalFiltered : signalAndRetrievalRefs;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1165
|
+
return {
|
|
1166
|
+
distillCooledRefs,
|
|
1167
|
+
preCooldownCount,
|
|
1168
|
+
distillOnlyRefs,
|
|
1169
|
+
feedbackSummary,
|
|
1170
|
+
signalFiltered,
|
|
1171
|
+
signalBearingSet,
|
|
1172
|
+
retrievalCounts,
|
|
1173
|
+
proactiveRefs,
|
|
1174
|
+
proactiveMaintenanceSummary,
|
|
1175
|
+
proactivePlan: proactive.proactivePlan,
|
|
1176
|
+
highSalienceRefs,
|
|
1177
|
+
signalAndRetrievalRefs,
|
|
1178
|
+
mergedRefs,
|
|
1179
|
+
processableRefs,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Attribution tagging: stamp each ref with the eligibility lane that selected
|
|
1184
|
+
* it. Every reflect/distill proposal must record WHICH lane chose its source
|
|
1185
|
+
* asset so downstream accept/reject/revert/retrieval outcomes can be sliced by
|
|
1186
|
+
* lane (does the PROACTIVE lane produce value vs the reactive lanes?). We
|
|
1187
|
+
* build the lane map here — the one place all three lanes are known — and
|
|
1188
|
+
* stamp it onto each ImproveEligibleRef object. Because the ref objects are
|
|
1189
|
+
* shared by reference across buckets, the stamp travels with the ref through
|
|
1190
|
+
* the sort, disk-check, and loop stages down to the reflect/distill event
|
|
1191
|
+
* emit sites and createProposal calls. See EligibilitySource for the lane
|
|
1192
|
+
* vocabulary.
|
|
1193
|
+
*
|
|
1194
|
+
* Precedence (prefer the most specific reactive signal):
|
|
1195
|
+
* scope > signal-delta > proactive > high-salience
|
|
1196
|
+
* A ref with real feedback is attributed to feedback even if it was also due
|
|
1197
|
+
* for proactive maintenance or had high encoding salience. We apply lanes
|
|
1198
|
+
* weakest-first so the strongest overwrites; the explicit --scope <ref> bypass
|
|
1199
|
+
* wins outright (user intent).
|
|
1200
|
+
*/
|
|
1201
|
+
function stampEligibilitySource(args) {
|
|
1202
|
+
const { scope, processableRefs, mergedRefs, signalFiltered, proactiveRefs, highSalienceRefs } = args;
|
|
1172
1203
|
const eligibilitySourceByRef = new Map();
|
|
1173
1204
|
for (const r of highSalienceRefs)
|
|
1174
1205
|
eligibilitySourceByRef.set(r.ref, "high-salience");
|
|
@@ -1188,22 +1219,7 @@ function gatherCandidates(args) {
|
|
|
1188
1219
|
// mergedRefs is always a subset of the four lanes above).
|
|
1189
1220
|
r.eligibilitySource = eligibilitySourceByRef.get(r.ref) ?? "unknown";
|
|
1190
1221
|
}
|
|
1191
|
-
return
|
|
1192
|
-
distillCooledRefs,
|
|
1193
|
-
preCooldownCount,
|
|
1194
|
-
distillOnlyRefs,
|
|
1195
|
-
feedbackSummary,
|
|
1196
|
-
signalFiltered,
|
|
1197
|
-
signalBearingSet,
|
|
1198
|
-
retrievalCounts,
|
|
1199
|
-
proactiveRefs,
|
|
1200
|
-
proactiveMaintenanceSummary,
|
|
1201
|
-
proactivePlan: proactive.proactivePlan,
|
|
1202
|
-
highSalienceRefs,
|
|
1203
|
-
signalAndRetrievalRefs,
|
|
1204
|
-
mergedRefs,
|
|
1205
|
-
eligibilitySourceByRef,
|
|
1206
|
-
};
|
|
1222
|
+
return eligibilitySourceByRef;
|
|
1207
1223
|
}
|
|
1208
1224
|
/**
|
|
1209
1225
|
* The signal-delta partition of postCleanupRefs into the four buckets (pass:
|
|
@@ -133,27 +133,23 @@ export const REFLECT_ALLOWED_TYPES = new Set([
|
|
|
133
133
|
const PROTECTED_FRONTMATTER_FIELDS = new Set(["name", "ref", "id", "slug", "type"]);
|
|
134
134
|
/**
|
|
135
135
|
* Read the last 1–3 archived rejected proposals for a given ref from the
|
|
136
|
-
* proposal store.
|
|
137
|
-
* or the ref is undefined
|
|
138
|
-
*
|
|
139
|
-
*
|
|
136
|
+
* proposal store. Returns `[]` when the proposals store is absent (not yet
|
|
137
|
+
* created) or the ref is undefined — `listProposalsReadOnly` already handles
|
|
138
|
+
* that case; a genuine read failure propagates instead of being swallowed,
|
|
139
|
+
* since silently dropping this Reflexion-style context risks re-proposing
|
|
140
|
+
* content that was already rejected (arXiv:2303.11366).
|
|
140
141
|
*/
|
|
141
142
|
function readRejectedProposals(stash, ref, proposalsCtx) {
|
|
142
143
|
if (!ref)
|
|
143
144
|
return [];
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
145
|
+
return listProposalsReadOnly(stash, { ref, status: "rejected", includeArchive: true }, proposalsCtx)
|
|
146
|
+
.sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime())
|
|
147
|
+
.slice(0, MAX_REJECTED_PROPOSALS)
|
|
148
|
+
.map((p) => ({
|
|
149
|
+
ref: p.ref,
|
|
150
|
+
reason: p.review?.reason ?? "no reason given",
|
|
151
|
+
contentPreview: proposalContent(p).slice(0, 500),
|
|
152
|
+
}));
|
|
157
153
|
}
|
|
158
154
|
/**
|
|
159
155
|
* Synthesize a tmp draft-file path for the agent/sdk file-write contract.
|
|
@@ -36,10 +36,10 @@ import path from "node:path";
|
|
|
36
36
|
import { isScalar, parseDocument } from "yaml";
|
|
37
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 { removeFrontmatterListValues, 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
41
|
import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
|
|
42
|
-
import { typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
42
|
+
import { conceptIdFromTypeName, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
43
43
|
import { localDateStamp } from "../../core/common.js";
|
|
44
44
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
45
45
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -786,6 +786,46 @@ function runMissingRefChecks(ctx, raw, shouldRun, pendingFixes) {
|
|
|
786
786
|
const values = readRefStringOrArray(ctx.data?.[key]);
|
|
787
787
|
if (values === null)
|
|
788
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
|
+
}
|
|
789
829
|
const missingXrefs = checkMissingRefsInList(values, ctx.stashRoot, ctx.extraStashRoots);
|
|
790
830
|
// #884 opt-in repair. Scoped to the BELIEF channels only: an edge whose
|
|
791
831
|
// target has neither a file nor a prune tombstone asserts a
|
|
@@ -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
|
//
|
|
@@ -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
|
}
|
|
@@ -15,7 +15,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
15
15
|
import { loadConfig } from "../../core/config/config.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
18
|
-
import {
|
|
18
|
+
import { diffProposal, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, recoverProposalTransactionsForStash, rejectProposalDurably, resolveProposalId, revertProposal, } from "./repository.js";
|
|
19
19
|
import { validateProposal } from "./validators/proposals.js";
|
|
20
20
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
21
21
|
function resolveStash(stashDir) {
|
|
@@ -131,26 +131,6 @@ export function akmProposalDiff(options) {
|
|
|
131
131
|
...(diff.targetPath ? { targetPath: diff.targetPath } : {}),
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
export function akmProposalCreate(options) {
|
|
135
|
-
const queue = resolveProposalQueue(options.stashDir, options.queue, options.config);
|
|
136
|
-
const stash = queue.stashDir;
|
|
137
|
-
const target = options.target ?? (queue.target ? { source: queue.target.source.name, root: queue.target.source.path } : undefined);
|
|
138
|
-
// Manual proposal creation (via `akm proposal create`) always bypasses
|
|
139
|
-
// dedup/cooldown guards — the operator is explicitly requesting a proposal.
|
|
140
|
-
const result = createProposal(stash, {
|
|
141
|
-
ref: options.ref,
|
|
142
|
-
source: options.source,
|
|
143
|
-
...(target !== undefined ? { target } : {}),
|
|
144
|
-
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
145
|
-
payload: options.payload,
|
|
146
|
-
force: true,
|
|
147
|
-
}, options.ctx);
|
|
148
|
-
if (isProposalSkipped(result)) {
|
|
149
|
-
// Should never happen with force:true — defensive only.
|
|
150
|
-
throw new Error(`Unexpected proposal skip: ${result.message}`);
|
|
151
|
-
}
|
|
152
|
-
return { schemaVersion: 1, ok: true, proposal: result };
|
|
153
|
-
}
|
|
154
134
|
/**
|
|
155
135
|
* Restore an accepted proposal's prior content from the backup captured at
|
|
156
136
|
* promotion time (Advantage D6c / Phase 6C).
|