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
@@ -10,30 +10,31 @@
10
10
  * for columns added after a table first shipped, drops of retired derived
11
11
  * tables and columns, and one in-place rebuild of the (derived, cheap) FTS
12
12
  * table when its layout is older than this release's. It never drops
13
- * `entries`, `embeddings`, `utility_scores`, the extracted graph
14
- * (`graph_meta`, `graph_files`, `graph_file_*`), or `llm_enrichment_cache`
15
- * to cross a version boundary; the only from-scratch
16
- * rebuild is the SQLITE_CORRUPT path in `index-connection.ts`. A layout newer
17
- * than this release's is refused, naming the upgrade
18
- * ({@link newerIndexLayoutError}).
13
+ * `entries`, `embeddings`, `utility_scores`, or `llm_enrichment_cache` to
14
+ * cross a version boundary; the only from-scratch rebuild is the
15
+ * SQLITE_CORRUPT path in `index-connection.ts`. A layout newer than this
16
+ * release's is refused, naming the upgrade ({@link newerIndexLayoutError}).
17
+ * The one exception is the LLM entity graph (`graph_meta`, `graph_files`,
18
+ * `graph_file_*`), retired in 0.9.17-alpha.9: those tables are dropped
19
+ * unconditionally below (index.db is a regenerable cache, and declared links
20
+ * — `asset_links` — now back `akm show`'s `links` field, which replaced the
21
+ * graph's `related` list).
19
22
  */
20
23
  import { createRequire } from "node:module";
24
+ import path from "node:path";
21
25
  import { ConfigError } from "../../core/errors.js";
22
26
  import { warn } from "../../core/warn.js";
23
27
  import { sha256Hex } from "../../runtime.js";
24
28
  import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, entriesFtsDdl, isContentlessFtsDdl, missingEntryColumns, readTableSql, supportsContentlessDelete, tableExists, } from "./index-entry-schema.js";
25
29
  import { rebuildFts } from "./index-fts-repository.js";
30
+ import { rebuildAllEntryLinks } from "./index-links-repository.js";
26
31
  import { getMeta, setMeta } from "./index-meta-repository.js";
27
32
  // ── Constants ───────────────────────────────────────────────────────────────
28
33
  export const DB_VERSION = CANONICAL_INDEX_DB_VERSION;
29
34
  /** `index_meta` key set when the writable opener migrated the layout; cleared once `akm index` VACUUMs. */
30
35
  export const VACUUM_PENDING_META = "vacuumPending";
31
- /**
32
- * The value written to `graph_meta.schema_version`, a NOT NULL column in every
33
- * released layout. Releases up to 0.9.17-alpha.5 write 4 and nothing ever
34
- * compares it; the index layout version gates the graph tables' shape.
35
- */
36
- export const GRAPH_SCHEMA_VERSION = 4;
36
+ /** The layout that added declared links (`asset_links`, #935). */
37
+ const DECLARED_LINKS_LAYOUT = 26;
37
38
  /**
38
39
  * The refusal for an index a newer akm wrote. Readers and the writable opener
39
40
  * both raise it: a newer layout may lack tables or columns this release reads
@@ -65,97 +66,15 @@ const REGISTRY_INDEX_CACHE_DDL = `
65
66
  CREATE INDEX IF NOT EXISTS idx_registry_cache_fetched
66
67
  ON registry_index_cache(fetched_at);
67
68
  `;
68
- /**
69
- * Create the graph-extraction tables (`graph_meta`/`graph_files`/`graph_file_entities`/
70
- * `graph_file_relations`).
71
- *
72
- * graph_files is self-keyed on (stash_root, file_path, body_hash) and is not
73
- * tied to entries.id (#624-P1): re-upserting an entries row never disturbs the
74
- * extracted graph, and a content change yields a distinct key. A UNIQUE index
75
- * on (stash_root, file_path) still enforces one graph_files row per path.
76
- */
77
- function ensureGraphTables(db) {
78
- db.exec(`
79
- CREATE TABLE IF NOT EXISTS graph_meta (
80
- stash_root TEXT PRIMARY KEY,
81
- schema_version INTEGER NOT NULL,
82
- generated_at TEXT NOT NULL,
83
- considered_files INTEGER NOT NULL DEFAULT 0,
84
- extracted_files INTEGER NOT NULL DEFAULT 0,
85
- entity_count INTEGER NOT NULL DEFAULT 0,
86
- relation_count INTEGER NOT NULL DEFAULT 0,
87
- extraction_coverage REAL NOT NULL DEFAULT 0,
88
- density REAL NOT NULL DEFAULT 0,
89
- extractor_id TEXT,
90
- extraction_run_id TEXT,
91
- model TEXT,
92
- prompt_version TEXT,
93
- batch_size INTEGER,
94
- cache_hits INTEGER NOT NULL DEFAULT 0,
95
- cache_misses INTEGER NOT NULL DEFAULT 0,
96
- truncation_count INTEGER NOT NULL DEFAULT 0,
97
- failure_count INTEGER NOT NULL DEFAULT 0
98
- );
99
-
100
- CREATE TABLE IF NOT EXISTS graph_files (
101
- stash_root TEXT NOT NULL,
102
- file_path TEXT NOT NULL,
103
- file_order INTEGER NOT NULL,
104
- file_type TEXT NOT NULL,
105
- body_hash TEXT NOT NULL,
106
- confidence REAL,
107
- status TEXT NOT NULL DEFAULT 'extracted',
108
- reason TEXT,
109
- extraction_run_id TEXT,
110
- PRIMARY KEY (stash_root, file_path, body_hash)
111
- );
112
-
113
- CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_files_path
114
- ON graph_files(stash_root, file_path);
115
-
116
- CREATE INDEX IF NOT EXISTS idx_graph_files_stash_order
117
- ON graph_files(stash_root, file_order);
118
-
119
- CREATE TABLE IF NOT EXISTS graph_file_entities (
120
- stash_root TEXT NOT NULL,
121
- file_path TEXT NOT NULL,
122
- body_hash TEXT NOT NULL,
123
- entity_order INTEGER NOT NULL,
124
- entity_norm TEXT NOT NULL,
125
- entity TEXT NOT NULL,
126
- PRIMARY KEY (stash_root, file_path, body_hash, entity_order),
127
- FOREIGN KEY (stash_root, file_path, body_hash)
128
- REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
129
- );
130
-
131
- CREATE INDEX IF NOT EXISTS idx_graph_file_entities_entity_norm
132
- ON graph_file_entities(stash_root, entity_norm);
133
-
134
- CREATE TABLE IF NOT EXISTS graph_file_relations (
135
- stash_root TEXT NOT NULL,
136
- file_path TEXT NOT NULL,
137
- body_hash TEXT NOT NULL,
138
- relation_order INTEGER NOT NULL,
139
- from_entity_norm TEXT NOT NULL,
140
- from_entity TEXT NOT NULL,
141
- to_entity_norm TEXT NOT NULL,
142
- to_entity TEXT NOT NULL,
143
- relation_type TEXT,
144
- confidence REAL,
145
- PRIMARY KEY (stash_root, file_path, body_hash, relation_order),
146
- FOREIGN KEY (stash_root, file_path, body_hash)
147
- REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
148
- );
149
- `);
150
- }
151
69
  /**
152
70
  * An `entries` table missing a required column cannot be read or written by
153
71
  * this release (the last such change was v20→v21, which removed the
154
72
  * transitional `entry_key`/`dir_path`/... columns and made `item_ref` the
155
73
  * key). Recreate only the tables keyed by `entries.id` — their ids are about
156
- * to be re-minted, so the rows would dangle anyway. Graph rows (keyed by
157
- * path) and the LLM enrichment cache (keyed by ref) are kept. The next index
158
- * run re-walks every source.
74
+ * to be re-minted, so the rows would dangle anyway. The LLM enrichment cache
75
+ * (keyed by ref) is kept. The LLM entity-graph tables are unconditionally
76
+ * dropped elsewhere in this file regardless of this recreation (retired
77
+ * 0.9.17-alpha.9), not kept. The next index run re-walks every source.
159
78
  */
160
79
  function ensureEntriesLayout(db) {
161
80
  if (!tableExists(db, "entries"))
@@ -164,12 +83,13 @@ function ensureEntriesLayout(db) {
164
83
  if (missing.length === 0)
165
84
  return;
166
85
  warn(`Index database entries table predates the ${missing.join(", ")} column${missing.length === 1 ? "" : "s"} — ` +
167
- "recreating the entries-keyed tables (entries, full-text, embeddings, utility scores); graph data and the " +
168
- "LLM enrichment cache are kept. The next index run re-walks every source.");
86
+ "recreating the entries-keyed tables (entries, full-text, embeddings, utility scores); the " +
87
+ "LLM enrichment cache is kept. The next index run re-walks every source.");
169
88
  db.transaction(() => {
170
89
  for (const table of [
171
90
  "entries_fts",
172
91
  "entry_fragments",
92
+ "asset_links",
173
93
  "embeddings",
174
94
  "utility_scores_scoped",
175
95
  "utility_scores",
@@ -241,7 +161,7 @@ function ensureFtsLayout(db) {
241
161
  const entryCount = Number(db.prepare("SELECT COUNT(*) AS n FROM entries").get().n);
242
162
  if (entryCount > 0) {
243
163
  warn(`Rebuilding the full-text index for ${entryCount} entr${entryCount === 1 ? "y" : "ies"} ` +
244
- "(embeddings, utility scores, graph data and the LLM enrichment cache are kept).");
164
+ "(embeddings, utility scores, and the LLM enrichment cache are kept).");
245
165
  }
246
166
  db.transaction(() => {
247
167
  db.exec("DROP TABLE IF EXISTS entries_fts");
@@ -249,6 +169,25 @@ function ensureFtsLayout(db) {
249
169
  rebuildFts(db);
250
170
  })();
251
171
  }
172
+ /**
173
+ * Layout 26 stores declared links (#935). Every relation an older layout
174
+ * indexed already sits in `document_json`, so the links are derived from there
175
+ * in place, with no file read. The exception is a workflow's step targets and
176
+ * a task's target, which no earlier layout stored: the directories holding
177
+ * workflows and tasks lose their incremental cursor, so the next `akm index`
178
+ * re-reads those and nothing else. One transaction.
179
+ */
180
+ function migrateToDeclaredLinks(db) {
181
+ db.transaction(() => {
182
+ rebuildAllEntryLinks(db);
183
+ const rows = db
184
+ .prepare("SELECT DISTINCT file_path FROM entries WHERE type IN ('workflow', 'task')")
185
+ .all();
186
+ const forget = db.prepare("DELETE FROM index_dir_state WHERE dir_path = ?");
187
+ for (const dir of new Set(rows.map((row) => path.dirname(row.file_path))))
188
+ forget.run(dir);
189
+ })();
190
+ }
252
191
  function tableHasColumn(db, table, column) {
253
192
  const columns = db.prepare(`PRAGMA table_info(${table})`).all();
254
193
  return columns.some((existing) => existing.name === column);
@@ -287,6 +226,25 @@ export function ensureSchema(db) {
287
226
  db.exec("DROP TABLE IF EXISTS entry_fragments_fts");
288
227
  db.exec("DROP TABLE IF EXISTS utility_scores_scoped");
289
228
  db.exec("DROP TABLE IF EXISTS graph_extraction_queue");
229
+ // The LLM entity graph, retired in 0.9.17-alpha.9: declared links
230
+ // (`asset_links`) now back `akm show`'s `links` field (which replaced the
231
+ // graph's `related` list) and curate's support refs (#935), and the
232
+ // navigation eval measured vector kNN beating the graph's `related` list
233
+ // by 0.157 P@5. `graph_files` stands in for the whole set — all four
234
+ // tables are only ever created and dropped together. Gated on it (rather
235
+ // than the unconditional `DROP TABLE IF EXISTS` pattern used above) so
236
+ // this reclaim runs once: after the first writable open drops these
237
+ // tables, every later open finds `graph_files` already gone and skips the
238
+ // no-op DROPs and the repeat VACUUM flag below. An older release's
239
+ // `CREATE TABLE IF NOT EXISTS` still recreates them (empty) if it ever
240
+ // opens this index again — a later open here would then drop them again.
241
+ const hadGraphTables = tableExists(db, "graph_files");
242
+ if (hadGraphTables) {
243
+ db.exec("DROP TABLE IF EXISTS graph_meta");
244
+ db.exec("DROP TABLE IF EXISTS graph_files");
245
+ db.exec("DROP TABLE IF EXISTS graph_file_entities");
246
+ db.exec("DROP TABLE IF EXISTS graph_file_relations");
247
+ }
290
248
  // One float32 BLOB per entry, searched by an exact scan
291
249
  // (index-vec-repository.ts). `model` is the provider fingerprint the vector was generated under
292
250
  // (`deriveSemanticProviderFingerprint`); the embedding pass re-embeds only
@@ -339,8 +297,8 @@ export function ensureSchema(db) {
339
297
  ensureColumn(db, "index_dir_state", "row_count", "INTEGER");
340
298
  ensureColumn(db, "index_dir_state", "index_variant", "TEXT");
341
299
  // LLM enrichment result cache, keyed by a stable asset_ref string (the
342
- // absolute file path for graph/memory passes, `item_ref` for the
343
- // metadata-enhance pass) plus the body hash the result was produced for.
300
+ // absolute file path of the memory-inference pass) plus the body hash the
301
+ // result was produced for.
344
302
  db.exec(`
345
303
  CREATE TABLE IF NOT EXISTS llm_enrichment_cache (
346
304
  asset_ref TEXT NOT NULL,
@@ -354,7 +312,24 @@ export function ensureSchema(db) {
354
312
  CREATE INDEX IF NOT EXISTS idx_llm_cache_updated
355
313
  ON llm_enrichment_cache(updated_at);
356
314
  `);
357
- ensureGraphTables(db);
315
+ // Metadata-enhance retired (RS-D, 0.9.17-alpha.9): its rows were the only
316
+ // ones keyed by the default empty cache_variant (memory inference writes
317
+ // `memory-inference-v2`), so this is safe to run unconditionally on every
318
+ // writable open. The table
319
+ // itself stays — memory inference still reads it.
320
+ db.exec("DELETE FROM llm_enrichment_cache WHERE cache_variant = ''");
321
+ // The graph-extraction cache variant is retired along with the tables
322
+ // above; its rows would otherwise sit unread forever. Gated the same way,
323
+ // on the same one-time flag, so a rerun does not re-scan the cache table
324
+ // for rows that are already gone.
325
+ if (hadGraphTables) {
326
+ db.exec("DELETE FROM llm_enrichment_cache WHERE cache_variant LIKE 'graph-extraction:%'");
327
+ // The drops and delete above freed real space (measured ~68MB on a
328
+ // representative index): flag it the same way a version-gated layout
329
+ // migration does, since this reclaim is unconditional-on-version but
330
+ // still one-time-per-index (guarded by hadGraphTables above).
331
+ setMeta(db, VACUUM_PENDING_META, "1");
332
+ }
358
333
  dropVecMirror(db);
359
334
  // Meta keys only the sqlite-vec mirror read.
360
335
  db.exec("DELETE FROM index_meta WHERE key IN ('embeddingDim', 'vecFastPathReady')");
@@ -368,6 +343,9 @@ export function ensureSchema(db) {
368
343
  if (!hadFragmentSource && tableExists(db, "entries")) {
369
344
  db.exec("DELETE FROM index_dir_state");
370
345
  }
346
+ if (storedVersion > 0 && storedVersion < DECLARED_LINKS_LAYOUT && tableExists(db, "entries")) {
347
+ migrateToDeclaredLinks(db);
348
+ }
371
349
  // Migrating an existing layout drops tables and columns; the next `akm index`
372
350
  // VACUUMs the pages they leave free (`vacuumIndexDb`, indexer.ts), since a
373
351
  // writable open may run inside a caller's transaction, where VACUUM cannot.
@@ -168,6 +168,50 @@ function validatePresentMetadata(meta) {
168
168
  if (Object.hasOwn(meta, "eligibilitySource") && typeof meta.eligibilitySource !== "string") {
169
169
  invalidPresentField("eligibilitySource");
170
170
  }
171
+ if (Object.hasOwn(meta, "promotionSource") && typeof meta.promotionSource !== "string") {
172
+ invalidPresentField("promotionSource");
173
+ }
174
+ if (Object.hasOwn(meta, "promotionSourceHash") && typeof meta.promotionSourceHash !== "string") {
175
+ invalidPresentField("promotionSourceHash");
176
+ }
177
+ if (Object.hasOwn(meta, "retirement")) {
178
+ const retirement = meta.retirement;
179
+ if (typeof retirement !== "object" ||
180
+ retirement === null ||
181
+ typeof retirement.retiredRef !== "string" ||
182
+ typeof retirement.successorRef !== "string" ||
183
+ typeof retirement.cosine !== "number" ||
184
+ !Number.isFinite(retirement.cosine) ||
185
+ (retirement.judgeLabel !== "duplicate" &&
186
+ retirement.judgeLabel !== "subsumed" &&
187
+ retirement.judgeLabel !== "supersedes") ||
188
+ typeof retirement.judgeReason !== "string" ||
189
+ typeof retirement.retiredContentHash !== "string" ||
190
+ typeof retirement.successorContentHash !== "string" ||
191
+ (retirement.reason !== "duplicate" && retirement.reason !== "subsumed" && retirement.reason !== "superseded")) {
192
+ invalidPresentField("retirement");
193
+ }
194
+ }
195
+ if (Object.hasOwn(meta, "retiredArchive")) {
196
+ const archive = meta.retiredArchive;
197
+ if (typeof archive !== "object" ||
198
+ archive === null ||
199
+ !Array.isArray(archive.dirs) ||
200
+ archive.dirs.length === 0 ||
201
+ archive.dirs.some((dir) => typeof dir !== "string" || dir.length === 0)) {
202
+ invalidPresentField("retiredArchive");
203
+ }
204
+ }
205
+ if (Object.hasOwn(meta, "retireAcceptIntent")) {
206
+ const intent = meta.retireAcceptIntent;
207
+ if (typeof intent !== "object" ||
208
+ intent === null ||
209
+ typeof intent.assetPath !== "string" ||
210
+ intent.assetPath.length === 0 ||
211
+ typeof intent.backupContent !== "string") {
212
+ invalidPresentField("retireAcceptIntent");
213
+ }
214
+ }
171
215
  }
172
216
  /**
173
217
  * Convert a raw `ProposalRow` to the public `Proposal` shape.
@@ -233,6 +277,13 @@ export function proposalRowToProposal(row) {
233
277
  ...(typeof meta.eligibilitySource === "string"
234
278
  ? { eligibilitySource: meta.eligibilitySource }
235
279
  : {}),
280
+ ...(typeof meta.promotionSource === "string" ? { promotionSource: meta.promotionSource } : {}),
281
+ ...(typeof meta.promotionSourceHash === "string" ? { promotionSourceHash: meta.promotionSourceHash } : {}),
282
+ ...(meta.retirement !== undefined ? { retirement: meta.retirement } : {}),
283
+ ...(meta.retiredArchive !== undefined ? { retiredArchive: meta.retiredArchive } : {}),
284
+ ...(meta.retireAcceptIntent !== undefined
285
+ ? { retireAcceptIntent: meta.retireAcceptIntent }
286
+ : {}),
236
287
  };
237
288
  }
238
289
  /**
@@ -291,6 +342,16 @@ export function proposalToRowValues(proposal, stashDir) {
291
342
  metaObj.acceptedTarget = proposal.acceptedTarget;
292
343
  if (proposal.eligibilitySource !== undefined)
293
344
  metaObj.eligibilitySource = proposal.eligibilitySource;
345
+ if (proposal.promotionSource !== undefined)
346
+ metaObj.promotionSource = proposal.promotionSource;
347
+ if (proposal.promotionSourceHash !== undefined)
348
+ metaObj.promotionSourceHash = proposal.promotionSourceHash;
349
+ if (proposal.retirement !== undefined)
350
+ metaObj.retirement = proposal.retirement;
351
+ if (proposal.retiredArchive !== undefined)
352
+ metaObj.retiredArchive = proposal.retiredArchive;
353
+ if (proposal.retireAcceptIntent !== undefined)
354
+ metaObj.retireAcceptIntent = proposal.retireAcceptIntent;
294
355
  validatePresentMetadata(metaObj);
295
356
  return {
296
357
  id: proposal.id,
@@ -58,23 +58,6 @@ export function getAssetSalience(db, ref) {
58
58
  // Bun SQLite returns null (not undefined) when no row found.
59
59
  return row == null ? undefined : row;
60
60
  }
61
- /**
62
- * Load ALL rank scores from the asset_salience table (full-stash query).
63
- *
64
- * Used by the forgetting-safety report (plan §WS-1 step 7) to compute stash-wide
65
- * rank positions rather than pool-relative positions. Returns an empty Map when the
66
- * table is empty (first WS-1 run = no pre-existing rows).
67
- *
68
- * Order is unspecified; callers must sort before assigning 1-indexed positions.
69
- */
70
- export function getAllRankScores(db) {
71
- const rows = db.prepare("SELECT asset_ref, rank_score FROM asset_salience").all();
72
- const result = new Map();
73
- for (const row of rows) {
74
- result.set(row.asset_ref, row.rank_score);
75
- }
76
- return result;
77
- }
78
61
  // ── Plasticity helpers ────────────────────────────────────────────────────────
79
62
  /**
80
63
  * Increment `consecutive_no_ops` for an asset. Called after a no-op reflect/distill.
@@ -84,8 +67,7 @@ export function getAllRankScores(db) {
84
67
  * Invariant: recordNoOp must never originate rank_score semantics. If the asset has
85
68
  * no salience row yet (persistence's best-effort try/catch may have swallowed an
86
69
  * error), we do nothing — a no-op counter is meaningless without a rank_score row,
87
- * and a synthetic INSERT would fabricate a rank_score=0 entry that could produce
88
- * false catastrophic-forgetting signals in buildRankChangeReport.
70
+ * and a synthetic INSERT would fabricate a rank_score=0 entry with nothing behind it.
89
71
  */
90
72
  export function recordNoOp(db, ref) {
91
73
  db.prepare(`UPDATE asset_salience SET consecutive_no_ops = consecutive_no_ops + 1, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);