akm-cli 0.9.17-alpha.7 → 0.9.17-alpha.9

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 (116) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/STABILITY.md +9 -8
  3. package/dist/akm +55 -22
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  15. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  16. package/dist/assets/templates/html/health.html +3 -5
  17. package/dist/cli/retired-commands.js +1 -1
  18. package/dist/commands/health/archive-usage.js +98 -0
  19. package/dist/commands/health/data-dir-usage.js +25 -13
  20. package/dist/commands/health/html-report.js +1 -4
  21. package/dist/commands/health/improve-metrics.js +0 -25
  22. package/dist/commands/health/md-report.js +1 -6
  23. package/dist/commands/health/report-view-model.js +4 -14
  24. package/dist/commands/health/windows.js +0 -1
  25. package/dist/commands/health.js +13 -0
  26. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  27. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  28. package/dist/commands/improve/consolidate.js +38 -63
  29. package/dist/commands/improve/extract-prompt.js +1 -2
  30. package/dist/commands/improve/improve-cli.js +1 -1
  31. package/dist/commands/improve/improve-strategies.js +23 -5
  32. package/dist/commands/improve/improve.js +19 -30
  33. package/dist/commands/improve/ledger.js +3 -2
  34. package/dist/commands/improve/loop-stages.js +5 -84
  35. package/dist/commands/improve/memory/memory-belief.js +3 -1
  36. package/dist/commands/improve/memory/memory-improve.js +269 -11
  37. package/dist/commands/improve/planner.js +0 -5
  38. package/dist/commands/improve/preparation.js +20 -135
  39. package/dist/commands/improve/retrieval-scope.js +19 -4
  40. package/dist/commands/improve/salience.js +1 -14
  41. package/dist/commands/improve/stage.js +0 -1
  42. package/dist/commands/lint/base-linter.js +19 -11
  43. package/dist/commands/proposal/drain.js +8 -1
  44. package/dist/commands/proposal/proposal-cli.js +16 -2
  45. package/dist/commands/proposal/proposal-types.js +7 -0
  46. package/dist/commands/proposal/proposal.js +37 -6
  47. package/dist/commands/proposal/repository.js +613 -4
  48. package/dist/commands/proposal/validators/proposals.js +9 -0
  49. package/dist/commands/read/curate.js +40 -13
  50. package/dist/commands/read/knowledge.js +3 -2
  51. package/dist/commands/read/show.js +55 -16
  52. package/dist/commands/sources/info.js +3 -0
  53. package/dist/commands/sources/stash-cli.js +2 -2
  54. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  55. package/dist/core/adapter/adapters/akm-metadata.js +31 -0
  56. package/dist/core/bundle-rename.js +1 -7
  57. package/dist/core/config/config-schema.js +8 -1
  58. package/dist/core/config/config.js +23 -48
  59. package/dist/core/config/engine-semantics.js +0 -2
  60. package/dist/core/config/schema/improve-processes.js +17 -42
  61. package/dist/core/config/schema/index-config.js +5 -25
  62. package/dist/core/file-change.js +13 -5
  63. package/dist/core/improve-result.js +16 -5
  64. package/dist/core/improve-types.js +0 -1
  65. package/dist/core/loopback.js +7 -12
  66. package/dist/core/parse.js +13 -16
  67. package/dist/core/state/migrations.js +15 -0
  68. package/dist/core/time.js +0 -20
  69. package/dist/indexer/db/llm-cache.js +2 -2
  70. package/dist/indexer/ensure-index.js +2 -2
  71. package/dist/indexer/index-written-assets.js +2 -3
  72. package/dist/indexer/indexer.js +18 -418
  73. package/dist/indexer/links/declared-links.js +90 -0
  74. package/dist/indexer/passes/metadata.js +0 -19
  75. package/dist/indexer/scan/doc-to-entry.js +1 -0
  76. package/dist/indexer/walk/walker.js +3 -4
  77. package/dist/llm/client.js +8 -10
  78. package/dist/llm/embedders/remote.js +1 -2
  79. package/dist/llm/feature-gate.js +0 -5
  80. package/dist/output/shapes/helpers.js +23 -4
  81. package/dist/output/text/command-format.js +0 -8
  82. package/dist/output/text/proposal-format.js +47 -1
  83. package/dist/output/text/show-format.js +13 -17
  84. package/dist/scripts/akm-migrate-node.js +2754 -2836
  85. package/dist/scripts/akm-migrate.js +2754 -2836
  86. package/dist/setup/steps/connection.js +5 -6
  87. package/dist/setup/steps/platforms.js +2 -2
  88. package/dist/sources/providers/git-stash.js +55 -4
  89. package/dist/storage/repositories/improve-ledger-repository.js +48 -7
  90. package/dist/storage/repositories/index-entries-repository.js +16 -13
  91. package/dist/storage/repositories/index-entry-schema.js +22 -3
  92. package/dist/storage/repositories/index-links-repository.js +143 -0
  93. package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
  94. package/dist/storage/repositories/index-schema.js +82 -104
  95. package/dist/storage/repositories/proposals-repository.js +61 -0
  96. package/dist/storage/repositories/salience-repository.js +1 -19
  97. package/dist/tasks/source/task-to-v4.js +462 -74
  98. package/docs/migration/release-notes/0.9.17.md +7 -5
  99. package/docs/reference/cli.md +33 -21
  100. package/docs/reference/configuration.md +21 -12
  101. package/docs/reference/data-and-telemetry.md +0 -1
  102. package/package.json +1 -1
  103. package/schemas/akm-config.json +0 -342
  104. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  105. package/dist/assets/prompts/contradiction-judge.md +0 -33
  106. package/dist/assets/prompts/graph-extract-system.md +0 -1
  107. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  108. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  109. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  110. package/dist/indexer/db/graph-db.js +0 -431
  111. package/dist/indexer/graph/graph-extraction.js +0 -807
  112. package/dist/indexer/graph/graph-related.js +0 -131
  113. package/dist/indexer/graph/graph-types.js +0 -4
  114. package/dist/llm/graph-extract.js +0 -903
  115. package/dist/llm/metadata-enhance.js +0 -95
  116. package/dist/tasks/source/task-to-v3.js +0 -453
@@ -0,0 +1,90 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { stashDirFor } from "../../core/asset/asset-placement.js";
5
+ import { isBundleSlug, parseBundleRef } from "../../core/asset/asset-ref.js";
6
+ /** Channel order is the order links are stored and shown in. */
7
+ const CHANNELS = [
8
+ ["xref", (doc) => doc.xrefs],
9
+ ["superseded_by", (doc) => doc.supersededBy],
10
+ ["contradicted_by", (doc) => doc.contradictedBy],
11
+ ["belief_peer", (doc) => doc.currentBeliefRefs],
12
+ ["derived_from", (doc) => doc.derivedFrom],
13
+ ["cites", (doc) => doc.sources],
14
+ ["links_to", (doc) => doc.links],
15
+ ["uses", (doc) => doc.uses],
16
+ ];
17
+ /** The links an indexed document declares, in channel then authored order; one per (kind, target). */
18
+ export function declaredLinks(doc, owner) {
19
+ const links = [];
20
+ const seen = new Set();
21
+ for (const [kind, read] of CHANNELS) {
22
+ for (const raw of stringValues(read(doc))) {
23
+ const target = linkTarget(raw, owner.conceptId);
24
+ if (!target)
25
+ continue;
26
+ const bundle = target.bundle ?? owner.bundleId;
27
+ if (bundle === owner.bundleId && target.conceptId === owner.conceptId)
28
+ continue;
29
+ const key = `${kind}\0${bundle}\0${target.conceptId}`;
30
+ if (seen.has(key))
31
+ continue;
32
+ seen.add(key);
33
+ links.push({ kind, raw, ...target });
34
+ }
35
+ }
36
+ return links;
37
+ }
38
+ function stringValues(value) {
39
+ if (typeof value === "string")
40
+ return [value];
41
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
42
+ }
43
+ /**
44
+ * The target a token names, or `undefined` when the token is not an asset ref
45
+ * (a session id, a URL, prose, a template placeholder). Retired spellings
46
+ * convert in memory: `<type>:<name>` (`memory:x`), `wiki:<wiki>/<path>` (0.8
47
+ * wikis, now knowledge under `wikis/`), a `.md` suffix, and a `#fragment`.
48
+ * Inside a wiki page, `raw/…` and `pages/…` are relative to the wiki root.
49
+ */
50
+ function linkTarget(raw, ownerConceptId) {
51
+ const token = raw.trim();
52
+ if (!token || /[\s<*]/.test(token) || token.includes("$(") || token.includes("${"))
53
+ return;
54
+ let bundle;
55
+ let body = token;
56
+ const boundary = token.indexOf("//");
57
+ if (boundary >= 0) {
58
+ bundle = token.slice(0, boundary);
59
+ body = token.slice(boundary + 2);
60
+ if (!isBundleSlug(bundle))
61
+ return;
62
+ }
63
+ const conceptId = legacyConceptId(body.split("#", 1)[0] ?? "", ownerConceptId);
64
+ if (conceptId === undefined || conceptId.length <= 1 || conceptId.includes("//"))
65
+ return;
66
+ try {
67
+ return { ...(bundle ? { bundle } : {}), conceptId: parseBundleRef(conceptId).conceptId };
68
+ }
69
+ catch {
70
+ return;
71
+ }
72
+ }
73
+ function legacyConceptId(body, ownerConceptId) {
74
+ const stripped = body.replace(/\.md$/i, "");
75
+ const colon = stripped.indexOf(":");
76
+ if (colon >= 0) {
77
+ const type = stripped.slice(0, colon);
78
+ const name = stripped.slice(colon + 1);
79
+ if (!name || name.includes(":"))
80
+ return;
81
+ if (type === "wiki")
82
+ return `knowledge/wikis/${name}`;
83
+ const stashDir = stashDirFor(type);
84
+ return stashDir === undefined ? undefined : `${stashDir}/${name}`;
85
+ }
86
+ const wikiRoot = /^((?:[^/]+\/)*?wikis\/[^/]+\/)/.exec(ownerConceptId)?.[1];
87
+ if (wikiRoot && /^(raw|pages)\//.test(stripped))
88
+ return `${wikiRoot}${stripped}`;
89
+ return stripped;
90
+ }
@@ -743,25 +743,6 @@ function mergeAliases(existing, generated) {
743
743
  const merged = normalizeTerms([...(existing ?? []), ...generated]);
744
744
  return merged.length > 0 ? merged : undefined;
745
745
  }
746
- // ── Enrichment Completeness ─────────────────────────────────────────────────
747
- /**
748
- * Returns `true` when a stash entry already has enough LLM-quality metadata
749
- * that calling the LLM would produce no meaningful improvement.
750
- *
751
- * An entry is considered complete when ALL of the following hold:
752
- * - `description` is a non-empty string
753
- * - `tags` is a non-empty array
754
- * - `searchHints` is a non-empty array
755
- *
756
- * This predicate is used by `enhanceDirsWithLlm` to skip the LLM call for
757
- * entries that were previously enriched and already carry all three fields.
758
- */
759
- export function isEnrichmentComplete(entry) {
760
- const hasDescription = typeof entry.description === "string" && entry.description.trim().length > 0;
761
- const hasTags = Array.isArray(entry.tags) && entry.tags.length > 0;
762
- const hasSearchHints = Array.isArray(entry.searchHints) && entry.searchHints.length > 0;
763
- return hasDescription && hasTags && hasSearchHints;
764
- }
765
746
  // ── Native Markdown search projection ──────────────────────────────────────
766
747
  /**
767
748
  * Maximum native Markdown prose carried by the low-weight `content` field.
@@ -138,6 +138,7 @@ export function indexDocumentToStashEntry(doc) {
138
138
  entry.wikiRole = dj.wikiRole;
139
139
  assignStringList(entry, "sources", dj.sources);
140
140
  assignStringList(entry, "evidenceSources", dj.evidenceSources);
141
+ assignStringList(entry, "uses", dj.uses);
141
142
  // D2 (#730): OKF v0.2 provenance promoteProposal stamps onto AKM-native
142
143
  // writes, carried via DOCUMENT_JSON_CARRIED_FIELDS (akm-adapter.ts) —
143
144
  // unpacked back to a first-class member here exactly like `sources`/
@@ -169,10 +169,9 @@ function isInsideGitRepo(dir) {
169
169
  /**
170
170
  * Recursively collect every `.md` file under `root`.
171
171
  *
172
- * Shared by graph-extraction and memory-inference so the walk logic lives in
173
- * exactly one place. Mirrors the `complete` tracking of the other walkers in
174
- * this file: a directory that cannot be read makes the result incomplete
175
- * instead of silently looking like a clean, empty scan.
172
+ * Used by memory-inference. Mirrors the `complete` tracking of the other
173
+ * walkers in this file: a directory that cannot be read makes the result
174
+ * incomplete instead of silently looking like a clean, empty scan.
176
175
  */
177
176
  export function walkMarkdownFiles(root) {
178
177
  const files = [];
@@ -101,10 +101,9 @@ function retryBackoffMs() {
101
101
  * narrating about a document) does not get misclassified as a provider
102
102
  * context-limit error (#496).
103
103
  *
104
- * Canonical home: `graph-extract.ts` re-exports this so the index-pass
105
- * graph extractor and the retry classifier (`isRetryable`) share one
106
- * definition — retrying a context overflow cannot shrink the input, so it
107
- * must never be retried.
104
+ * Canonical home: `structured-call.ts`'s failure classifier and the retry
105
+ * classifier (`isRetryable`) here share one definition — retrying a context
106
+ * overflow cannot shrink the input, so it must never be retried.
108
107
  */
109
108
  export function isContextSizeError(message) {
110
109
  const lower = message.toLowerCase();
@@ -120,13 +119,12 @@ export function isContextSizeError(message) {
120
119
  /**
121
120
  * Codes describing a failure to reach or get a usable response from the
122
121
  * provider transport itself, as opposed to a malformed-but-received response
123
- * (`parse_error`) or a request-shape rejection (`rate_limited`). Shared by
124
- * {@link isRetryable} (which additionally requires evidence the failure is
125
- * transient) and the batch graph-extraction storm guard in `graph-extract.ts`
126
- * (which treats any of these as "the provider is down, stop retrying
127
- * per-asset") so the two classifications cannot drift apart.
122
+ * (`parse_error`) or a request-shape rejection (`rate_limited`). Used by
123
+ * {@link isRetryable}, which additionally requires evidence the failure is
124
+ * transient. Not exported — its one external caller (batched LLM
125
+ * entity-graph extraction) was retired in 0.9.17-alpha.9.
128
126
  */
129
- export function isTransportFailure(err) {
127
+ function isTransportFailure(err) {
130
128
  return err.code === "provider_error" || err.code === "network_error" || err.code === "provider_html_error";
131
129
  }
132
130
  /**
@@ -187,8 +187,7 @@ export function isContextExceededResponse(status, body) {
187
187
  * Resolve the effective in-flight request window for `RemoteEmbedder.embedBatch`.
188
188
  * Default (unset `embedding.concurrency`): 1 for a loopback endpoint, 2 for a
189
189
  * remote one, via the shared `defaultConcurrencyForEndpoint`
190
- * (`src/core/loopback.ts`), the same lowest-common-denominator rule
191
- * `getDefaultLlmConcurrency` (`src/indexer/indexer.ts`) uses.
190
+ * (`src/core/loopback.ts`).
192
191
  *
193
192
  * `embedding.concurrency` (#954) overrides this default in
194
193
  * either direction, bounded 1-16 at the config schema — added after field
@@ -7,8 +7,6 @@
7
7
  */
8
8
  const FEATURE_LOCATION = {
9
9
  memory_inference: (cfg) => cfg.index?.memory?.enabled ?? true,
10
- graph_extraction: (cfg) => cfg.index?.graph?.enabled ?? true,
11
- metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
12
10
  // #951: a real implementation of the dead `curate_rerank` key removed in
13
11
  // 0.8.0. Off by default — it requires a `search.curateRerank.endpoint` a
14
12
  // caller must explicitly configure.
@@ -82,9 +80,6 @@ export function isProcessEnabled(section, processName, config) {
82
80
  if (processName === "memory_inference" || processName === "memoryInference") {
83
81
  return isLlmFeatureEnabled(config, "memory_inference");
84
82
  }
85
- if (processName === "graph_extraction" || processName === "graphExtraction") {
86
- return isLlmFeatureEnabled(config, "graph_extraction");
87
- }
88
83
  }
89
84
  return false;
90
85
  }
@@ -56,11 +56,25 @@ export function shapeProposalProducerOutput(result, detail) {
56
56
  }
57
57
  export function shapeProposalEntry(entry, detail) {
58
58
  if (detail === "brief") {
59
- return pickFields(entry, ["id", "ref", "status", "source", "createdAt"]);
59
+ // `proposal list`'s default detail level IS "brief" (no `--detail` flag
60
+ // given projects here, not "normal") — `retirement` itself is not
61
+ // projected until "normal", so a flagged retire proposal's risk would
62
+ // otherwise be invisible by default. A minimal boolean marker, not the
63
+ // full risk object, keeps brief's shape small while still letting a
64
+ // reviewer see it without an extra flag.
65
+ const retirement = entry.retirement;
66
+ return {
67
+ ...pickFields(entry, ["id", "ref", "status", "source", "createdAt"]),
68
+ ...(retirement?.continuityRisk ? { continuityRisk: true } : {}),
69
+ };
60
70
  }
61
71
  if (detail === "normal") {
62
72
  // `confidence` and `gateDecision` (#577) explain why a proposal is pending,
63
73
  // so they are projected at `normal` for `akm proposal list/show` when present.
74
+ // `retirement` (alpha.9) is the same kind of field for a consolidate
75
+ // `retire` proposal: without it, "list/show" carries no reason a reviewer
76
+ // could act on (its `payload.content` is empty by design — a retire
77
+ // proposal deletes, it does not write).
64
78
  return pickFields(entry, [
65
79
  "id",
66
80
  "ref",
@@ -72,6 +86,7 @@ export function shapeProposalEntry(entry, detail) {
72
86
  "confidence",
73
87
  "gateDecision",
74
88
  "review",
89
+ "retirement",
75
90
  ]);
76
91
  }
77
92
  // full: project everything including the payload.
@@ -87,6 +102,10 @@ export function shapeProposalEntry(entry, detail) {
87
102
  "gateDecision",
88
103
  "payload",
89
104
  "review",
105
+ "retirement",
106
+ "retiredArchive",
107
+ "promotionSource",
108
+ "promotionSourceHash",
90
109
  ]);
91
110
  }
92
111
  export function shapeProposalListOutput(result, detail) {
@@ -357,7 +376,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
357
376
  "workflowParameters",
358
377
  "steps",
359
378
  "keys",
360
- "related",
379
+ "links",
361
380
  ...FRAGMENT_PROVENANCE_FIELDS,
362
381
  ...FRAGMENT_CONTEXT_FIELDS,
363
382
  ]);
@@ -381,7 +400,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
381
400
  "run",
382
401
  "origin",
383
402
  "keys",
384
- "related",
403
+ "links",
385
404
  ...FRAGMENT_PROVENANCE_FIELDS,
386
405
  ...FRAGMENT_CONTEXT_FIELDS,
387
406
  ]);
@@ -410,7 +429,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
410
429
  "cwd",
411
430
  "activeRun",
412
431
  "keys",
413
- "related",
432
+ "links",
414
433
  ...FRAGMENT_PROVENANCE_FIELDS,
415
434
  ...FRAGMENT_CONTEXT_FIELDS,
416
435
  // ref, path, and editable are always projected — at every --detail level,
@@ -407,14 +407,6 @@ export function formatIndexPlain(r) {
407
407
  for (const message of warnings)
408
408
  out += `\n - ${String(message)}`;
409
409
  }
410
- const notices = Array.isArray(indexResult.notices) ? indexResult.notices : [];
411
- for (const notice of notices) {
412
- const severity = notice.severity === "info" ? "info" : "warning";
413
- const field = typeof notice.field === "string" ? ` field=${notice.field}` : "";
414
- out +=
415
- `\n notice[${severity}] ${notice.code} adapter=${notice.adapter}${field}` +
416
- (notice.message ? `: ${notice.message}` : "");
417
- }
418
410
  const verification = indexResult.verification;
419
411
  if (verification?.ok === false && verification.message) {
420
412
  out += `\nVerification: ${String(verification.message)}`;
@@ -100,7 +100,16 @@ export function formatProposalListPlain(r) {
100
100
  // #577: surface the gate verdict inline so the queue explains itself.
101
101
  const gate = formatGateDecisionSummary(p.gateDecision);
102
102
  const gateSuffix = gate ? ` ${gate}` : "";
103
- lines.push(`${id} [${status}] ${ref} source=${source} ${created}${gateSuffix}`);
103
+ // A retire proposal's continuityRisk excludes it from bulk accept —
104
+ // visible in the default list, not just `proposal show`, since the owner
105
+ // reviews the backlog list-first. `list`'s default (brief) shape
106
+ // projects a `continuityRisk: true` marker instead of the full
107
+ // `retirement` object (only present from `--detail normal` up), so both
108
+ // shapes are checked here.
109
+ const retirement = p.retirement;
110
+ const flagged = p.continuityRisk === true || Boolean(retirement?.continuityRisk);
111
+ const continuitySuffix = flagged ? " ⚠ continuity-risk" : "";
112
+ lines.push(`${id} [${status}] ${ref} source=${source} ${created}${gateSuffix}${continuitySuffix}`);
104
113
  }
105
114
  return lines.join("\n").trimEnd();
106
115
  }
@@ -141,6 +150,43 @@ export function formatProposalShowPlain(r) {
141
150
  if (review.decidedAt)
142
151
  lines.push(`review.decidedAt: ${String(review.decidedAt)}`);
143
152
  }
153
+ // alpha.9: a consolidate retire proposal writes no content (`payload.content`
154
+ // is empty by design) — this is the reason a reviewer needs instead. `diff`
155
+ // shows the body being retired.
156
+ const retirement = p.retirement;
157
+ if (retirement) {
158
+ 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
+ }
189
+ }
144
190
  const validation = r.validation;
145
191
  if (validation) {
146
192
  const findings = Array.isArray(validation.findings) ? validation.findings : [];
@@ -54,17 +54,20 @@ export function formatShowPlain(r, detail) {
54
54
  if (r.schemaVersion !== undefined)
55
55
  lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
56
56
  }
57
- const related = typeof r.related === "object" && r.related !== null ? r.related : undefined;
58
- const relatedHits = related && Array.isArray(related.hits) ? related.hits : [];
59
- if (related) {
57
+ const links = typeof r.links === "object" && r.links !== null ? r.links : undefined;
58
+ if (links) {
60
59
  lines.push("");
61
- lines.push(`related: ${String(related.total ?? relatedHits.length)}`);
62
- for (const hit of relatedHits) {
63
- lines.push(` - ${String(hit.type ?? "?")}: ${formatRelatedLabel(hit)}`);
64
- const shared = Array.isArray(hit.sharedEntities) ? hit.sharedEntities.map(String) : [];
65
- if (shared.length > 0)
66
- lines.push(` shared: ${shared.join(", ")}`);
67
- lines.push(` relationCount: ${String(hit.relationCount ?? 0)}`);
60
+ lines.push("links:");
61
+ for (const part of ["outgoing", "incoming", "unresolved"]) {
62
+ const groups = links[part];
63
+ if (typeof groups !== "object" || groups === null)
64
+ continue;
65
+ lines.push(` ${part}:`);
66
+ for (const [kind, group] of Object.entries(groups)) {
67
+ const refs = Array.isArray(group.refs) ? group.refs.map(String) : [];
68
+ const more = (group.total ?? refs.length) > refs.length ? ` (+${(group.total ?? 0) - refs.length} more)` : "";
69
+ lines.push(` ${kind}: ${refs.join(", ")}${more}`);
70
+ }
68
71
  }
69
72
  }
70
73
  const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
@@ -98,10 +101,3 @@ export function formatShowPlain(r, detail) {
98
101
  appendShowDirectives(lines, r);
99
102
  return lines.length > 0 ? lines.join("\n") : null;
100
103
  }
101
- function formatRelatedLabel(hit) {
102
- const ref = typeof hit.ref === "string" ? hit.ref : undefined;
103
- if (ref)
104
- return ref;
105
- const pathValue = typeof hit.path === "string" ? hit.path : "?";
106
- return pathValue.split("/").pop() ?? pathValue;
107
- }