akm-cli 0.9.17 → 0.9.19-alpha.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/STABILITY.md +2 -1
  3. package/dist/assets/hints/cli-hints-full.md +4 -2
  4. package/dist/assets/hints/cli-hints-short.md +5 -3
  5. package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
  6. package/dist/commands/feedback-cli.js +1 -1
  7. package/dist/commands/health/checks.js +6 -6
  8. package/dist/commands/health.js +3 -3
  9. package/dist/commands/improve/consolidate/coverage.js +132 -0
  10. package/dist/commands/improve/consolidate/pair-pass.js +31 -21
  11. package/dist/commands/improve/consolidate.js +46 -13
  12. package/dist/commands/improve/distill.js +8 -4
  13. package/dist/commands/improve/eligibility.js +39 -9
  14. package/dist/commands/improve/improve-cli.js +9 -6
  15. package/dist/commands/improve/improve.js +13 -4
  16. package/dist/commands/improve/ledger.js +2 -2
  17. package/dist/commands/improve/loop-stages.js +2 -0
  18. package/dist/commands/improve/preparation.js +3 -1
  19. package/dist/commands/improve/reflect.js +2 -2
  20. package/dist/commands/improve/stage.js +43 -12
  21. package/dist/commands/proposal/diff-format.js +21 -0
  22. package/dist/commands/proposal/proposal-cli.js +48 -10
  23. package/dist/commands/proposal/proposal-types.js +11 -0
  24. package/dist/commands/proposal/proposal.js +60 -5
  25. package/dist/commands/proposal/repository.js +250 -18
  26. package/dist/commands/read/knowledge.js +13 -11
  27. package/dist/commands/read/remember-cli.js +7 -3
  28. package/dist/commands/sources/source-clone.js +1 -1
  29. package/dist/commands/tasks/tasks-cli.js +1 -1
  30. package/dist/commands/tasks/tasks.js +10 -3
  31. package/dist/core/mutation-target.js +8 -3
  32. package/dist/core/write-source.js +3 -2
  33. package/dist/indexer/usage/usage-events.js +2 -1
  34. package/dist/output/shapes/helpers.js +7 -0
  35. package/dist/output/shapes/passthrough.js +1 -0
  36. package/dist/output/shapes/proposal/reopen.js +14 -0
  37. package/dist/output/shapes.js +2 -0
  38. package/dist/output/text/helpers.js +1 -1
  39. package/dist/output/text/proposal/proposal.js +3 -1
  40. package/dist/output/text/proposal-format.js +87 -32
  41. package/dist/scripts/akm-migrate-node.js +102 -25
  42. package/dist/scripts/akm-migrate.js +102 -25
  43. package/dist/storage/repositories/improve-ledger-repository.js +65 -6
  44. package/dist/storage/repositories/index-vec-repository.js +13 -8
  45. package/dist/storage/repositories/proposals-repository.js +23 -0
  46. package/dist/storage/sqlite-read-snapshot.js +46 -2
  47. package/dist/storage/state-db-integrity.js +12 -9
  48. package/dist/tasks/run/load-task.js +5 -1
  49. package/docs/migration/README.md +1 -0
  50. package/docs/migration/release-notes/0.9.19.md +134 -0
  51. package/docs/migration/release-notes/README.md +5 -0
  52. package/docs/migration/v0.7-to-v0.8.md +2 -2
  53. package/docs/migration/v0.8-to-v0.9.md +5 -1
  54. package/docs/reference/cli.md +189 -28
  55. package/docs/reference/configuration.md +9 -8
  56. package/docs/reference/data-and-telemetry.md +24 -16
  57. package/package.json +1 -1
@@ -168,11 +168,11 @@ function parseWriteRefs(rawRefs, flag) {
168
168
  }
169
169
  return parsedRefs;
170
170
  }
171
- function resolveWriteRefRoots(target) {
171
+ function resolveWriteRefRoots(target, targetFlag) {
172
172
  const cfg = loadConfig();
173
173
  let writeTarget;
174
174
  try {
175
- writeTarget = resolveWriteTarget(cfg, target);
175
+ writeTarget = resolveWriteTarget(cfg, target, { flag: targetFlag });
176
176
  }
177
177
  catch (error) {
178
178
  if (!target)
@@ -297,11 +297,11 @@ export const XREF_SOFT_CAP = 5;
297
297
  * {@link XREF_SOFT_CAP} refs emits a stderr warning (soft cap) but still
298
298
  * returns them all.
299
299
  */
300
- export function resolveXrefsForWrite(rawXrefs, target) {
300
+ export function resolveXrefsForWrite(rawXrefs, target, targetFlag) {
301
301
  const parsedRefs = parseWriteRefs(rawXrefs, "--xref");
302
302
  if (parsedRefs.length === 0)
303
303
  return [];
304
- const { roots } = resolveWriteRefRoots(target);
304
+ const { roots } = resolveWriteRefRoots(target, targetFlag);
305
305
  const unresolved = [];
306
306
  const xrefs = [];
307
307
  for (const parsed of parsedRefs) {
@@ -386,13 +386,13 @@ function isParseableYamlMapping(frontmatter) {
386
386
  }
387
387
  }
388
388
  /** Resolve any qualified supersedes ref as the mutation target for remember/import. */
389
- export function resolveSupersedesWriteTarget(rawRefs, target) {
389
+ export function resolveSupersedesWriteTarget(rawRefs, target, targetFlag) {
390
390
  const config = loadConfig();
391
391
  let effectiveTarget = target;
392
392
  for (const parsed of parseWriteRefs(rawRefs, "--supersedes")) {
393
393
  if (!parsed.origin)
394
394
  continue;
395
- const resolved = resolveMutationTarget(config, { type: parsed.type, name: parsed.name, origin: parsed.origin }, effectiveTarget).target;
395
+ const resolved = resolveMutationTarget(config, { type: parsed.type, name: parsed.name, origin: parsed.origin }, effectiveTarget, { flag: targetFlag }).target;
396
396
  effectiveTarget = resolved.selector ?? resolved.source.name;
397
397
  }
398
398
  return effectiveTarget;
@@ -428,17 +428,17 @@ const SUPERSEDE_REJECTED_TYPES = new Set(["secret", "env", "task", "script"]);
428
428
  * constraint (and never dirtying a non-target source outside its boundary
429
429
  * commit). A ref that resolves only in another configured source — read-only
430
430
  * OR writable-but-not-the-target — is returned with `writable: false` and a
431
- * reason (naming the `--target` remedy when the source is writable); the
431
+ * reason (naming the target-flag remedy when the source is writable); the
432
432
  * caller writes the correction anyway and reports the demotion as not
433
433
  * applied.
434
434
  *
435
435
  * Returns the deduplicated plan in argv order; empty input returns [].
436
436
  */
437
- export function resolveSupersedesForWrite(rawRefs, target) {
437
+ export function resolveSupersedesForWrite(rawRefs, target, targetFlag) {
438
438
  const parsedRefs = parseWriteRefs(rawRefs, "--supersedes");
439
439
  if (parsedRefs.length === 0)
440
440
  return [];
441
- const { roots } = resolveWriteRefRoots(target);
441
+ const { roots } = resolveWriteRefRoots(target, targetFlag);
442
442
  const plan = [];
443
443
  const unresolved = [];
444
444
  for (const parsed of parsedRefs) {
@@ -491,7 +491,7 @@ export function resolveSupersedesForWrite(rawRefs, target) {
491
491
  : {
492
492
  reason: namedWritableSource
493
493
  ? `resolves outside the write target and the working stash, in writable source "${namedWritableSource}" at ${root}; ` +
494
- `re-run with --target ${namedWritableSource} to demote it there`
494
+ `re-run with ${targetFlag ?? "--target"} ${namedWritableSource} to demote it there`
495
495
  : `resolves outside the write target and the working stash, in a read-only source at ${root}; ` +
496
496
  "demotion only applies to assets in the write target or the working stash",
497
497
  }),
@@ -518,7 +518,9 @@ export async function writeMarkdownAsset(options) {
518
518
  const subPath = normalizeCreateSubPath(options.path);
519
519
  const baseName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
520
520
  const normalizedName = combineCreatePath(subPath, baseName);
521
- const resolved = resolveMutationTarget(cfg, { type: options.type, name: normalizedName }, options.target);
521
+ const resolved = resolveMutationTarget(cfg, { type: options.type, name: normalizedName }, options.target, {
522
+ flag: options.targetFlag,
523
+ });
522
524
  const { target } = resolved;
523
525
  const { source, config } = target;
524
526
  const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
@@ -33,6 +33,8 @@ async function fetchSimilarMemories(query, excludeRef, eventSource) {
33
33
  return [];
34
34
  }
35
35
  }
36
+ /** The flag `remember` takes for its destination, as its errors spell it. */
37
+ const TARGET_FLAG = "--bundle";
36
38
  /**
37
39
  * `--target` was renamed to `--bundle` on `remember` in 0.9 (S8). citty is
38
40
  * non-strict, so the retired spelling is silently absorbed rather than
@@ -153,8 +155,8 @@ export const rememberCommand = defineJsonCommand({
153
155
  // untouched. Refs resolvable only in a configured extra stash source are
154
156
  // accepted (cross-stash provenance).
155
157
  const rawSupersedes = parseAllFlagValues("--supersedes");
156
- const writeTarget = resolveSupersedesWriteTarget(rawSupersedes, args.bundle);
157
- const xrefs = resolveXrefsForWrite(parseAllFlagValues("--xref"), writeTarget);
158
+ const writeTarget = resolveSupersedesWriteTarget(rawSupersedes, args.bundle, TARGET_FLAG);
159
+ const xrefs = resolveXrefsForWrite(parseAllFlagValues("--xref"), writeTarget, TARGET_FLAG);
158
160
  // Collect and validate --supersedes occurrences (repeatable). Same
159
161
  // before-any-write validation contract: an unresolvable ref exits 2 with
160
162
  // nothing written AND nothing demoted (no partial correction). The
@@ -162,7 +164,7 @@ export const rememberCommand = defineJsonCommand({
162
164
  // (correction provenance per the back-linking conventions); the demotion
163
165
  // itself runs inside writeMarkdownAsset, ordered before the git boundary
164
166
  // commit.
165
- const supersedes = resolveSupersedesForWrite(rawSupersedes, writeTarget);
167
+ const supersedes = resolveSupersedesForWrite(rawSupersedes, writeTarget, TARGET_FLAG);
166
168
  for (const s of supersedes) {
167
169
  if (!xrefs.includes(s.ref))
168
170
  xrefs.push(s.ref);
@@ -206,6 +208,7 @@ export const rememberCommand = defineJsonCommand({
206
208
  preferredName: inferAssetName(body, "memory"),
207
209
  force: args.force,
208
210
  target: writeTarget,
211
+ targetFlag: TARGET_FLAG,
209
212
  path: args.path,
210
213
  supersedes,
211
214
  });
@@ -313,6 +316,7 @@ export const rememberCommand = defineJsonCommand({
313
316
  preferredName: inferAssetName(body, "memory"),
314
317
  force: args.force,
315
318
  target: writeTarget,
319
+ targetFlag: TARGET_FLAG,
316
320
  path: args.path,
317
321
  supersedes,
318
322
  });
@@ -39,7 +39,7 @@ export async function akmClone(options) {
39
39
  // are not bundle slugs).
40
40
  const parsed = parseQualifiedRefInput(options.sourceRef);
41
41
  const config = hasUnmanagedDest ? undefined : loadConfig();
42
- const resolvedWriteTarget = config ? resolveWriteTarget(config, options.target) : undefined;
42
+ const resolvedWriteTarget = config ? resolveWriteTarget(config, options.target, { flag: "--bundle" }) : undefined;
43
43
  const writeTarget = resolvedWriteTarget ? prepareWriteTargetForMutation(resolvedWriteTarget) : undefined;
44
44
  // An unmanaged --dest does not require any configured write target.
45
45
  let allSources;
@@ -212,7 +212,7 @@ const tasksAddCommand = defineJsonCommand({
212
212
  },
213
213
  command: {
214
214
  type: "string",
215
- description: 'Exact shell string to run on the schedule (no AI agent), e.g. "akm improve --strategy frequent".',
215
+ description: 'Exact shell string to run on the schedule (no AI agent), e.g. "akm improve --strategy reflect-distill".',
216
216
  },
217
217
  engine: { type: "string", description: "Engine to use for prompt targets (default: defaults.engine)" },
218
218
  model: { type: "string", description: "Model override for prompt targets" },
@@ -688,7 +688,7 @@ function taskAssetRef(id) {
688
688
  /** The bundle `task add` writes into: its write target, config, stash path, and name. */
689
689
  function resolveTaskBundle(target) {
690
690
  const config = loadConfig();
691
- const resolved = prepareWriteTargetForMutation(resolveWriteTarget(config, target, { requireWritable: true }));
691
+ const resolved = prepareWriteTargetForMutation(resolveWriteTarget(config, target, { requireWritable: true, flag: "--bundle" }));
692
692
  return { resolved, config, stashDir: resolved.source.path, bundleName: resolved.source.name };
693
693
  }
694
694
  function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
@@ -696,7 +696,8 @@ function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
696
696
  if (bundle === bundleName) {
697
697
  return { file: await resolveAssetPath(bundleRoot, type, name), bundleRoot };
698
698
  }
699
- const target = resolveWriteTarget(config, bundle, { requireWritable: false });
699
+ // `bundle` is the qualifier of an asset ref in the task, not a flag.
700
+ const target = resolveWriteTarget(config, bundle, { requireWritable: false, flag: "The asset ref's bundle" });
700
701
  return {
701
702
  file: await resolveAssetPath(target.source.path, type, name),
702
703
  bundleRoot: target.source.path,
@@ -723,7 +724,13 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
723
724
  else {
724
725
  const configured = resolveActiveConfiguredSources(config).some((source) => source.name === selector);
725
726
  const implicit = configured ? undefined : resolveImplicitScheduledBundleTarget(config, selector);
726
- resolved = implicit ?? resolveWriteTarget(config, selector, { requireWritable: false });
727
+ resolved =
728
+ implicit ??
729
+ resolveWriteTarget(config, selector, {
730
+ requireWritable: false,
731
+ // The selector is `--bundle` when given, else the task ref's own bundle qualifier.
732
+ flag: flagBundle !== undefined ? "--bundle" : "The task ref's bundle",
733
+ });
727
734
  }
728
735
  if (refBundle && resolved.source.name !== refBundle) {
729
736
  throw new UsageError(`Task ref bundle ${JSON.stringify(refBundle)} does not match the resolved source.`, "INVALID_FLAG_VALUE");
@@ -50,9 +50,14 @@ function resolveExplicitMutationTarget(config, explicitTarget, options) {
50
50
  }
51
51
  }
52
52
  }
53
- /** Reconcile a qualified mutation ref with `--target`, then resolve the write destination. */
53
+ /**
54
+ * Reconcile a qualified mutation ref with the explicit target (`--target`, or
55
+ * the `options.flag` the caller's command spells), then resolve the write
56
+ * destination.
57
+ */
54
58
  export function resolveMutationTarget(config, ref, explicitTarget, options = {}) {
55
- const writeOptions = { requireWritable: options.requireWritable };
59
+ const flag = options.flag ?? "--target";
60
+ const writeOptions = { requireWritable: options.requireWritable, flag };
56
61
  const qualifiedTarget = ref.origin ? resolveBundleWriteTarget(config, ref.origin, writeOptions) : undefined;
57
62
  const explicitResolved = explicitTarget
58
63
  ? resolveExplicitMutationTarget(config, explicitTarget, writeOptions)
@@ -60,7 +65,7 @@ export function resolveMutationTarget(config, ref, explicitTarget, options = {})
60
65
  if (qualifiedTarget &&
61
66
  explicitResolved &&
62
67
  path.resolve(qualifiedTarget.source.path) !== path.resolve(explicitResolved.source.path)) {
63
- throw new UsageError(`Qualified ref bundle "${ref.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or select the same bundle.`);
68
+ throw new UsageError(`Qualified ref bundle "${ref.origin}" conflicts with ${flag} "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop ${flag} or select the same bundle.`);
64
69
  }
65
70
  let target = qualifiedTarget ?? explicitResolved ?? resolveWriteTarget(config, undefined, writeOptions);
66
71
  const bundleId = ref.origin ?? canonicalBundleIdForTarget(config, target);
@@ -59,16 +59,17 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
59
59
  const allConfiguredSources = resolveConfiguredSources(akmConfig);
60
60
  const configuredSources = resolveActiveConfiguredSources(akmConfig);
61
61
  const requireWritable = options.requireWritable !== false;
62
+ const flag = options.flag ?? "--target";
62
63
  if (explicitTarget) {
63
64
  const match = configuredSources.find((s) => s.name === explicitTarget);
64
65
  if (!match) {
65
66
  if (allConfiguredSources.some((source) => source.name === explicitTarget)) {
66
67
  throw new UsageError(`Bundle "${explicitTarget}" is disabled.`, "INVALID_FLAG_VALUE");
67
68
  }
68
- throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
69
+ throw new UsageError(`${flag} must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
69
70
  }
70
71
  if (requireWritable && !resolveWritable({ type: match.type, writable: match.writable })) {
71
- throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass --target to a different source.`);
72
+ throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass ${flag} to a different source.`);
72
73
  }
73
74
  return adaptConfiguredSource(match);
74
75
  }
@@ -85,7 +85,8 @@ export function countFeedbackSignals(db, entryId) {
85
85
  * Count usage events of a given `event_type`.
86
86
  *
87
87
  * Lifted verbatim from `akm improve` (improve.ts) where the show-event count
88
- * was hand-rolled inline to drive the zero-feedback fallback warning.
88
+ * was hand-rolled inline to drive the warning that the retrieval scope matches
89
+ * only search-retrieved assets.
89
90
  */
90
91
  export function countUsageEventsByType(db, eventType) {
91
92
  return db.prepare("SELECT COUNT(*) AS cnt FROM usage_events WHERE event_type = ?").get(eventType)
@@ -86,6 +86,7 @@ export function shapeProposalEntry(entry, detail) {
86
86
  "confidence",
87
87
  "gateDecision",
88
88
  "review",
89
+ "reviewHistory",
89
90
  "retirement",
90
91
  ]);
91
92
  }
@@ -102,6 +103,7 @@ export function shapeProposalEntry(entry, detail) {
102
103
  "gateDecision",
103
104
  "payload",
104
105
  "review",
106
+ "reviewHistory",
105
107
  "retirement",
106
108
  "retiredArchive",
107
109
  "promotionSource",
@@ -167,6 +169,11 @@ export function shapeProposalDiffOutput(result, detail) {
167
169
  isNew: result.isNew,
168
170
  unified: result.unified,
169
171
  ...(result.targetPath !== undefined ? { targetPath: result.targetPath } : {}),
172
+ // A retire proposal (#997): the diff alone is a file leaving, so the pair
173
+ // verdict and the accept/revert note travel with it at every detail level.
174
+ ...(result.op !== undefined ? { op: result.op } : {}),
175
+ ...(result.retirement !== undefined ? { retirement: result.retirement } : {}),
176
+ ...(result.note !== undefined ? { note: result.note } : {}),
170
177
  };
171
178
  if (detail === "full") {
172
179
  return { schemaVersion: result.schemaVersion, ...base };
@@ -53,6 +53,7 @@ const PASSTHROUGH_COMMANDS = [
53
53
  "proposal-accept-batch",
54
54
  "proposal-drain",
55
55
  "proposal-reject-batch",
56
+ "proposal-reopen-batch",
56
57
  "proposal-revert",
57
58
  "registry-add",
58
59
  "registry-list",
@@ -0,0 +1,14 @@
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
+ // Output shape registration for `akm proposal reopen` (#997). One reopened
5
+ // proposal is the same envelope `reject` returns (`ok`, `id`, `ref`, an
6
+ // optional `reason` — here the reopen reason — and the shaped proposal), so it
7
+ // shares that shaper; several are the passthrough `proposal-reopen-batch`.
8
+ import { shapeProposalRejectOutput } from "../helpers.js";
9
+ export const proposalReopenShapes = [
10
+ {
11
+ command: "proposal-reopen",
12
+ handler: (result, detail) => shapeProposalRejectOutput(result, detail),
13
+ },
14
+ ];
@@ -29,6 +29,7 @@ import { proposalDiffShapes } from "./shapes/proposal/diff.js";
29
29
  import { proposalListShapes } from "./shapes/proposal/list.js";
30
30
  import { proposalProducerShapes } from "./shapes/proposal/producer.js";
31
31
  import { proposalRejectShapes } from "./shapes/proposal/reject.js";
32
+ import { proposalReopenShapes } from "./shapes/proposal/reopen.js";
32
33
  import { proposalShowShapes } from "./shapes/proposal/show.js";
33
34
  import { getOutputShapeHandler, registerOutputShapes } from "./shapes/registry.js";
34
35
  import { registrySearchShapes } from "./shapes/registry-search.js";
@@ -51,6 +52,7 @@ const BUILT_IN_OUTPUT_SHAPES = [
51
52
  ...proposalShowShapes,
52
53
  ...proposalAcceptShapes,
53
54
  ...proposalRejectShapes,
55
+ ...proposalReopenShapes,
54
56
  ...proposalDiffShapes,
55
57
  ...proposalProducerShapes,
56
58
  ...envListShapes,
@@ -18,6 +18,6 @@
18
18
  export { formatAddPlain, formatBundleRenamePlain, formatBundleShowPlain, formatClonePlain, formatConfigPlain, formatCuratePlain, formatEnvCreatePlain, formatEnvExportPlain, formatEnvListPlain, formatEnvRemovePlain, formatEventLine, formatEventsPlain, formatFeedbackPlain, formatImportPlain, formatIndexPlain, formatInfoPlain, formatInitPlain, formatListPlain, formatModelsListPlain, formatRegistryAddPlain, formatRegistryListPlain, formatRegistryRemovePlain, formatRegistrySearchPlain, formatRememberPlain, formatRemovePlain, formatSearchPlain, formatSyncPlain, formatUpdatePlain, formatUpgradePlain, } from "./command-format.js";
19
19
  export { formatHealthPlain } from "./health-format.js";
20
20
  export { formatLintPlain } from "./lint-format.js";
21
- export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "./proposal-format.js";
21
+ export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalReopenBatchPlain, formatProposalReopenPlain, formatProposalShowPlain, } from "./proposal-format.js";
22
22
  export { formatShowPlain } from "./show-format.js";
23
23
  export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowPlanPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
@@ -2,12 +2,14 @@
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
  // Output text formatters for `akm proposal *` (#225).
5
- import { formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "../helpers.js";
5
+ import { formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalRejectPlain, formatProposalReopenBatchPlain, formatProposalReopenPlain, formatProposalShowPlain, } from "../helpers.js";
6
6
  export const proposalFormatters = [
7
7
  { command: "proposal-list", handler: (r) => formatProposalListPlain(r) },
8
8
  { command: "proposal-show", handler: (r) => formatProposalShowPlain(r) },
9
9
  { command: "proposal-accept", handler: (r) => formatProposalAcceptPlain(r) },
10
10
  { command: "proposal-reject", handler: (r) => formatProposalRejectPlain(r) },
11
+ { command: "proposal-reopen", handler: (r) => formatProposalReopenPlain(r) },
12
+ { command: "proposal-reopen-batch", handler: (r) => formatProposalReopenBatchPlain(r) },
11
13
  { command: "proposal-diff", handler: (r) => formatProposalDiffPlain(r) },
12
14
  { command: "proposal-drain", handler: (r) => formatProposalDrainPlain(r) },
13
15
  ];
@@ -113,6 +113,47 @@ export function formatProposalListPlain(r) {
113
113
  }
114
114
  return lines.join("\n").trimEnd();
115
115
  }
116
+ /**
117
+ * A retire proposal's verdict lines — the pair judge's label and reason, and
118
+ * the continuity check's failing queries — as `proposal show` and `proposal
119
+ * diff` both print them. `retirement` is the stored block, which `proposal
120
+ * diff` reports under the same keys.
121
+ */
122
+ function retireVerdictLines(retirement) {
123
+ const lines = [
124
+ `retire.label: ${String(retirement.judgeLabel)} (cosine=${String(retirement.cosine)})`,
125
+ `retire.reason: ${String(retirement.judgeReason)}`,
126
+ ];
127
+ // Item 1 (continuity check): flagged, but still minted — never swept by a
128
+ // bulk accept, only acceptable by id, so a reviewer must see it.
129
+ const continuityRisk = retirement.continuityRisk;
130
+ if (continuityRisk) {
131
+ const failingQueries = typeof continuityRisk.failingQueries === "number" ? continuityRisk.failingQueries : 0;
132
+ const unverifiedQueries = typeof continuityRisk.unverifiedQueries === "number" ? continuityRisk.unverifiedQueries : 0;
133
+ const summary = [];
134
+ if (failingQueries > 0) {
135
+ summary.push(`${failingQueries} of the retired asset's own quer${failingQueries === 1 ? "y" : "ies"} would not have found the successor top 10`);
136
+ }
137
+ // S2: a query the search call never ran, or that fell back to
138
+ // keyword-only ranking, is never silently trusted OR silently
139
+ // dropped — it excludes the proposal from bulk accept on its own.
140
+ if (unverifiedQueries > 0) {
141
+ summary.push(`${unverifiedQueries} quer${unverifiedQueries === 1 ? "y" : "ies"} unverified (search failed or used the keyword-only fallback)`);
142
+ }
143
+ lines.push(`retire.continuityRisk: ${summary.join("; ")} — excluded from bulk accept`);
144
+ // N3 / S4: the actual failing query text, not just the count — a
145
+ // reviewer deciding whether to accept by id needs to see what would
146
+ // stop resolving, not just how many queries.
147
+ const ranks = Array.isArray(continuityRisk.ranks) ? continuityRisk.ranks : [];
148
+ for (const rank of ranks) {
149
+ const successorRank = rank.successorRank === null || rank.successorRank === undefined
150
+ ? "absent from top 10"
151
+ : `#${String(rank.successorRank)}`;
152
+ lines.push(` - "${String(rank.query)}": retired #${String(rank.retiredRank)}, successor ${successorRank}`);
153
+ }
154
+ }
155
+ return lines;
156
+ }
116
157
  export function formatProposalShowPlain(r) {
117
158
  const p = r.proposal;
118
159
  const lines = [];
@@ -150,42 +191,24 @@ export function formatProposalShowPlain(r) {
150
191
  if (review.decidedAt)
151
192
  lines.push(`review.decidedAt: ${String(review.decidedAt)}`);
152
193
  }
194
+ // `akm proposal reopen` (#997): the rejection each reopen undid, so a
195
+ // pending proposal that was once rejected says so.
196
+ const history = Array.isArray(p.reviewHistory) ? p.reviewHistory : [];
197
+ for (const entry of history) {
198
+ const undone = entry.review;
199
+ const was = undone
200
+ ? `${String(undone.outcome ?? "?")}${undone.reason ? `: ${String(undone.reason)}` : ""} (${String(undone.decidedAt ?? "?")})`
201
+ : "an unrecorded review";
202
+ const why = entry.reopenReason ? ` (${String(entry.reopenReason)})` : "";
203
+ lines.push(`reopened: ${String(entry.reopenedAt)}${why}, undoing ${was}`);
204
+ }
153
205
  // alpha.9: a consolidate retire proposal writes no content (`payload.content`
154
206
  // is empty by design) — this is the reason a reviewer needs instead. `diff`
155
207
  // shows the body being retired.
156
208
  const retirement = p.retirement;
157
209
  if (retirement) {
158
210
  lines.push(`retire: ${String(retirement.retiredRef)} -> ${String(retirement.successorRef)}`);
159
- lines.push(`retire.label: ${String(retirement.judgeLabel)} (cosine=${String(retirement.cosine)})`);
160
- lines.push(`retire.reason: ${String(retirement.judgeReason)}`);
161
- // Item 1 (continuity check): flagged, but still minted — never swept by a
162
- // bulk accept, only acceptable by id, so a reviewer must see it here.
163
- const continuityRisk = retirement.continuityRisk;
164
- if (continuityRisk) {
165
- const failingQueries = typeof continuityRisk.failingQueries === "number" ? continuityRisk.failingQueries : 0;
166
- const unverifiedQueries = typeof continuityRisk.unverifiedQueries === "number" ? continuityRisk.unverifiedQueries : 0;
167
- const summary = [];
168
- if (failingQueries > 0) {
169
- summary.push(`${failingQueries} of the retired asset's own quer${failingQueries === 1 ? "y" : "ies"} would not have found the successor top 10`);
170
- }
171
- // S2: a query the search call never ran, or that fell back to
172
- // keyword-only ranking, is never silently trusted OR silently
173
- // dropped — it excludes the proposal from bulk accept on its own.
174
- if (unverifiedQueries > 0) {
175
- summary.push(`${unverifiedQueries} quer${unverifiedQueries === 1 ? "y" : "ies"} unverified (search failed or used the keyword-only fallback)`);
176
- }
177
- lines.push(`retire.continuityRisk: ${summary.join("; ")} — excluded from bulk accept`);
178
- // N3 / S4: the actual failing query text, not just the count — a
179
- // reviewer deciding whether to accept by id needs to see what would
180
- // stop resolving, not just how many queries.
181
- const ranks = Array.isArray(continuityRisk.ranks) ? continuityRisk.ranks : [];
182
- for (const rank of ranks) {
183
- const successorRank = rank.successorRank === null || rank.successorRank === undefined
184
- ? "absent from top 10"
185
- : `#${String(rank.successorRank)}`;
186
- lines.push(` - "${String(rank.query)}": retired #${String(rank.retiredRank)}, successor ${successorRank}`);
187
- }
188
- }
211
+ lines.push(...retireVerdictLines(retirement));
189
212
  }
190
213
  const validation = r.validation;
191
214
  if (validation) {
@@ -215,7 +238,10 @@ export function formatProposalShowPlain(r) {
215
238
  }
216
239
  }
217
240
  const payload = p.payload;
218
- if (payload && typeof payload.content === "string") {
241
+ // A retire proposal's payload is empty by design (it archives a file, it
242
+ // writes none): a bare `payload:` heading reads as "replaced by nothing" —
243
+ // the misreading #997 is about — so the retirement lines above stand alone.
244
+ if (!retirement && payload && typeof payload.content === "string") {
219
245
  lines.push("");
220
246
  lines.push("payload:");
221
247
  lines.push(payload.content);
@@ -229,6 +255,18 @@ export function formatProposalRejectPlain(r) {
229
255
  const reason = r.reason ? ` (${String(r.reason)})` : "";
230
256
  return `Rejected proposal ${String(r.id)} (${String(r.ref)})${reason}`;
231
257
  }
258
+ export function formatProposalReopenPlain(r) {
259
+ const reason = r.reason ? ` (${String(r.reason)})` : "";
260
+ return `Reopened proposal ${String(r.id)} (${String(r.ref)}) [pending]${reason}`;
261
+ }
262
+ export function formatProposalReopenBatchPlain(r) {
263
+ const results = Array.isArray(r.results) ? r.results : [];
264
+ const reason = results[0]?.reason ? ` (${String(results[0].reason)})` : "";
265
+ return [
266
+ `Reopened ${results.length} proposal(s) [pending]${reason}`,
267
+ ...results.map((result) => ` ${String(result.id)} ${String(result.ref)}`),
268
+ ].join("\n");
269
+ }
232
270
  export function formatProposalDrainPlain(r) {
233
271
  const applyMode = String(r.applyMode ?? "queue");
234
272
  const promoted = Array.isArray(r.promoted) ? r.promoted : [];
@@ -257,10 +295,27 @@ export function formatProposalDrainPlain(r) {
257
295
  return lines.join("\n").trimEnd();
258
296
  }
259
297
  export function formatProposalDiffPlain(r) {
298
+ const unified = typeof r.unified === "string" ? r.unified : "";
299
+ if (r.op === "delete") {
300
+ // #997: a retire proposal archives its target — it does not "update" it —
301
+ // so the header says so, and the pair verdict and the accept/revert note
302
+ // sit above the file that is leaving.
303
+ const retirement = r.retirement;
304
+ const subject = retirement
305
+ ? `${String(retirement.retiredRef)} -> ${String(retirement.successorRef)}`
306
+ : String(r.ref);
307
+ const lines = [`# proposal ${String(r.id)} (retire: ${subject})`];
308
+ if (retirement)
309
+ lines.push(...retireVerdictLines(retirement));
310
+ if (typeof r.note === "string")
311
+ lines.push(`note: ${r.note}`);
312
+ if (unified)
313
+ lines.push(unified);
314
+ return lines.join("\n");
315
+ }
260
316
  const header = r.isNew
261
317
  ? `# proposal ${String(r.id)} (new asset: ${String(r.ref)})`
262
318
  : `# proposal ${String(r.id)} (update: ${String(r.ref)})`;
263
- const unified = typeof r.unified === "string" ? r.unified : "";
264
319
  if (!unified)
265
320
  return `${header}\n(no changes)`;
266
321
  return `${header}\n${unified}`;