akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -0,0 +1,130 @@
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
+ * Find graph files that share entities with the given file.
6
+ *
7
+ * Implementation: SQL self-join on graph_file_entities, scoped by stash_root,
8
+ * grouped by file_path, ordered by shared-entity count desc. Touches ~50-200
9
+ * rows instead of loading the entire snapshot into memory. Cold-call latency
10
+ * drops from ~30-60ms (full snapshot parse) to ~2-5ms on typical stashes.
11
+ *
12
+ * #624-P1: the graph tables are keyed on (stash_root, file_path, body_hash) —
13
+ * NOT entries.id — so candidates are identified by file_path (the unique index
14
+ * idx_graph_files_path guarantees one graph_files row per path).
15
+ *
16
+ * The returned `ref` field carries the canonical indexed `concept_id`, never a
17
+ * value re-derived from presentation fields. It is undefined
18
+ * when the graph row has no matching entry with current indexed provenance.
19
+ */
20
+ export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
21
+ if (!db) {
22
+ // Fallback: opening a transient DB here is not currently a use case (all
23
+ // callers pass a handle), so degrade to empty rather than reopening.
24
+ return [];
25
+ }
26
+ // Confirm the target file has a graph row; without it there is nothing to
27
+ // relate. (Identity is file_path within the stash — one row per path.)
28
+ const row = db
29
+ .prepare("SELECT 1 AS present FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
30
+ .get(stashRoot, filePath);
31
+ if (row === undefined)
32
+ return [];
33
+ const effectiveLimit = Math.max(1, limit);
34
+ // Shared-entity count per candidate file_path. The target's entities are the
35
+ // rows for `filePath`; candidates are any OTHER file_path in the stash that
36
+ // shares a normalized entity.
37
+ const candidateRows = db
38
+ .prepare(`SELECT gf.file_path AS file_path,
39
+ gf.file_type AS file_type,
40
+ COUNT(*) AS shared
41
+ FROM graph_file_entities target
42
+ JOIN graph_file_entities e
43
+ ON e.stash_root = target.stash_root
44
+ AND e.entity_norm = target.entity_norm
45
+ AND e.file_path != target.file_path
46
+ JOIN graph_files gf
47
+ ON gf.stash_root = e.stash_root
48
+ AND gf.file_path = e.file_path
49
+ AND gf.body_hash = e.body_hash
50
+ WHERE target.file_path = ?
51
+ AND target.stash_root = ?
52
+ GROUP BY gf.file_path
53
+ ORDER BY shared DESC, gf.file_path ASC
54
+ LIMIT ?`)
55
+ .all(filePath, stashRoot, effectiveLimit);
56
+ if (candidateRows.length === 0)
57
+ return [];
58
+ const candidatePaths = candidateRows.map((r) => r.file_path);
59
+ const placeholders = candidatePaths.map(() => "?").join(",");
60
+ // Pull the shared entity names (joined by normalized casing) for display, in
61
+ // a fixed order so identical calls return identical results.
62
+ const sharedRows = db
63
+ .prepare(`SELECT e.file_path AS file_path, e.entity AS entity
64
+ FROM graph_file_entities e
65
+ JOIN graph_file_entities target
66
+ ON target.stash_root = e.stash_root
67
+ AND target.entity_norm = e.entity_norm
68
+ WHERE e.file_path IN (${placeholders})
69
+ AND e.stash_root = ?
70
+ AND target.file_path = ?
71
+ AND target.stash_root = ?
72
+ ORDER BY e.file_path, e.entity`)
73
+ .all(...candidatePaths, stashRoot, filePath, stashRoot);
74
+ const sharedByPath = new Map();
75
+ for (const row of sharedRows) {
76
+ let bucket = sharedByPath.get(row.file_path);
77
+ if (!bucket) {
78
+ bucket = new Set();
79
+ sharedByPath.set(row.file_path, bucket);
80
+ }
81
+ bucket.add(row.entity);
82
+ }
83
+ // Relation count for each candidate (relations where either endpoint
84
+ // matches one of the shared entities).
85
+ const relationCountByPath = new Map();
86
+ const relationRows = db
87
+ .prepare(`SELECT file_path, from_entity, to_entity
88
+ FROM graph_file_relations
89
+ WHERE file_path IN (${placeholders})
90
+ AND stash_root = ?`)
91
+ .all(...candidatePaths, stashRoot);
92
+ for (const row of relationRows) {
93
+ const shared = sharedByPath.get(row.file_path);
94
+ if (!shared)
95
+ continue;
96
+ if (shared.has(row.from_entity) || shared.has(row.to_entity)) {
97
+ relationCountByPath.set(row.file_path, (relationCountByPath.get(row.file_path) ?? 0) + 1);
98
+ }
99
+ }
100
+ // This related list is scoped to one source root, so the user-facing ref is
101
+ // the indexed bundle-less conceptId.
102
+ const refByPath = new Map();
103
+ try {
104
+ const entryRows = db
105
+ .prepare(`SELECT file_path, concept_id FROM entries
106
+ WHERE file_path IN (${placeholders})
107
+ ORDER BY file_path, concept_id`)
108
+ .all(...candidatePaths);
109
+ // Several entries can index one file; the first concept id wins.
110
+ for (const row of entryRows) {
111
+ if (!refByPath.has(row.file_path))
112
+ refByPath.set(row.file_path, row.concept_id);
113
+ }
114
+ }
115
+ catch {
116
+ /* ignore — refs are best-effort */
117
+ }
118
+ return candidateRows.map((row) => {
119
+ const sharedSet = sharedByPath.get(row.file_path) ?? new Set();
120
+ const sharedEntities = [...sharedSet].sort((a, b) => a.localeCompare(b));
121
+ const ref = refByPath.get(row.file_path);
122
+ return {
123
+ ...(ref ? { ref } : {}),
124
+ path: row.file_path,
125
+ type: row.file_type,
126
+ sharedEntities,
127
+ relationCount: relationCountByPath.get(row.file_path) ?? 0,
128
+ };
129
+ });
130
+ }
@@ -20,7 +20,6 @@
20
20
  * no-stale-age-window rationale.
21
21
  */
22
22
  import { releaseLock } from "../core/file-lock.js";
23
- import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../core/maintenance-barrier.js";
24
23
  import { getIndexRebuildLockPath } from "../core/paths.js";
25
24
  import { formatLockHolderPid, tryAcquireRunLock } from "../core/run-lock.js";
26
25
  import { warn, warnVerbose } from "../core/warn.js";
@@ -49,21 +48,14 @@ export function tryAcquireIndexRebuildLock(skipIfLocked) {
49
48
  `(${info.reason}); that process is no longer running.`);
50
49
  },
51
50
  });
51
+ const result = acquire();
52
+ if (result.state === "acquired")
53
+ return result;
52
54
  if (skipIfLocked) {
53
- const result = tryWithMaintenanceStartBarrier(acquire);
54
- if (!result) {
55
- warn("[index] maintenance barrier held; skipping (--skip-if-locked)");
56
- return { state: "skipped", holder: { pid: null, startedAt: null, launcherPid: null } };
57
- }
58
- if (result.state === "acquired")
59
- return result;
60
55
  warn(`[index] another index run holds the lock (PID ${formatLockHolderPid(result.holder)}, started ${result.holder.startedAt}); ` +
61
56
  "skipping (--skip-if-locked)");
62
57
  return { state: "skipped", holder: result.holder };
63
58
  }
64
- const result = withMaintenanceStartBarrier(acquire);
65
- if (result.state === "acquired")
66
- return result;
67
59
  warn(`[index] another index run is active (pid ${formatLockHolderPid(result.holder)}, started ${result.holder.startedAt}); ` +
68
60
  "this run will contend with it — pass --skip-if-locked for scheduled runs");
69
61
  return { state: "contended", holder: result.holder };
@@ -25,8 +25,8 @@
25
25
  import { AsyncLocalStorage } from "node:async_hooks";
26
26
  import fs from "node:fs";
27
27
  import path from "node:path";
28
+ import { TransientError } from "../core/errors.js";
28
29
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../core/file-lock.js";
29
- import { tryAcquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
30
30
  import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
31
31
  import { warn } from "../core/warn.js";
32
32
  import { sleepSync } from "../runtime.js";
@@ -92,21 +92,12 @@ function createLease(lockPath, ownership) {
92
92
  }
93
93
  function tryAcquireAssetMutationLease(lockPath, purpose) {
94
94
  while (true) {
95
- const releaseBarrier = tryAcquireMaintenanceBarrier();
96
- if (!releaseBarrier)
95
+ const ownership = tryAcquireLockSync(lockPath, buildPayload(purpose));
96
+ if (ownership)
97
+ return createLease(lockPath, ownership);
98
+ const probe = probeLock(lockPath);
99
+ if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
97
100
  return undefined;
98
- try {
99
- const ownership = tryAcquireLockSync(lockPath, buildPayload(purpose));
100
- if (ownership)
101
- return createLease(lockPath, ownership);
102
- // No `staleAfterMs`: only a verifiably dead holder is ever reclaimed.
103
- const probe = probeLock(lockPath);
104
- if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
105
- return undefined;
106
- }
107
- finally {
108
- releaseBarrier();
109
- }
110
101
  }
111
102
  }
112
103
  export async function acquireAssetMutationLease(options) {
@@ -129,7 +120,7 @@ export async function acquireAssetMutationLease(options) {
129
120
  // attempt, so a caller with maxWaitMs:0 still gets one chance at a free lock
130
121
  // instead of throwing before it ever tries.
131
122
  if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
132
- throw new Error(`timed out waiting for asset mutation lease for ${options.purpose}`);
123
+ throw new TransientError(`timed out waiting for asset mutation lease for ${options.purpose}`, "ASSET_MUTATION_LEASE_HELD");
133
124
  }
134
125
  const waitedMs = Date.now() - startedAt;
135
126
  if (waitedMs - lastWaitNoticeMs >= 15000) {
@@ -180,7 +171,7 @@ export function withAssetMutationLeaseSync(purpose, run) {
180
171
  if (!lease) {
181
172
  const waitedMs = Date.now() - startedAt;
182
173
  if (waitedMs >= maxWaitMs) {
183
- throw new Error(`timed out waiting for asset mutation lease for ${purpose}`);
174
+ throw new TransientError(`timed out waiting for asset mutation lease for ${purpose}`, "ASSET_MUTATION_LEASE_HELD");
184
175
  }
185
176
  // #956: the sync path used to wait up to 10 minutes with zero
186
177
  // progress feedback. Mirror the async path's 15s onWait cadence, but
@@ -33,9 +33,8 @@ import { closeDatabase, openExistingDatabase } from "../storage/repositories/ind
33
33
  import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
34
34
  import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
35
35
  import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
36
- import { getMarkdownFragmentContent, hasMarkdownFragmentContent, setMarkdownFragmentContent, } from "./passes/metadata.js";
36
+ import { withFileSize } from "./passes/metadata.js";
37
37
  import { drainDirDocuments } from "./scan/drain-dir.js";
38
- import { buildSearchText } from "./search/search-fields.js";
39
38
  import { buildFileContext } from "./walk/file-context.js";
40
39
  /**
41
40
  * Busy-timeout (ms) for write-path index upserts. A real write — unlike the
@@ -66,161 +65,150 @@ export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
66
65
  */
67
66
  export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
68
67
  try {
69
- return await (async () => {
70
- // #956: a live `akm index` rebuild holds index.db under one long
71
- // transaction (persistDirRecords), which this fast path's own 5s
72
- // busy_timeout would just contend with pointlessly. Interactive
73
- // commands (remember, import, extract session assets) must never wait
74
- // on it — skip the inline upsert/embedding entirely; the write itself
75
- // (file + commit) has already succeeded by the time this runs, and the
76
- // rebuild in progress will pick up the change on its own.
77
- const rebuildProbe = probeLock(getIndexRebuildLockPath());
78
- if (rebuildProbe.state === "held") {
79
- // #956: name the launcher pid alongside the holder pid when known —
80
- // every process listing and task log shows the launcher's pid, not
81
- // the bun/node child's.
82
- const holderLabel = formatLockHolderPid({
83
- pid: rebuildProbe.holderPid,
84
- launcherPid: rebuildProbe.launcherPid ?? null,
85
- });
86
- warn(`index rebuild in progress (pid ${holderLabel}); the next index pass will index ${filePaths.join(", ")}`);
87
- return true;
88
- }
89
- const dbPath = getDbPath();
90
- // `true` here means "the index is in the state the caller expects" — and
91
- // `acceptProposal` advances its journal to `index-finalized` on the
92
- // strength of it. Only a genuinely ABSENT index earns that answer: an
93
- // index we cannot read has NOT been updated, so it falls through to
94
- // `openExistingDatabase` and surfaces as the honest `false` (#791).
95
- if (isPathAbsent(dbPath))
96
- return true;
97
- // The full walk never descends into dot-directories (for example `.meta/`)
98
- // — mirror that dot-segment skip here so this fast path indexes exactly
99
- // what a full run would. Sensitive/
100
- // infra abstention is the adapter's job now (see the `akmAdapter` note
101
- // below), not a path pre-filter.
102
- const files = filePaths.filter((f) => {
103
- const rel = path.relative(stashDir, f);
104
- return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
68
+ // #956: a live `akm index` rebuild holds index.db under one long
69
+ // transaction (persistDirRecords), which this fast path's own 5s
70
+ // busy_timeout would just contend with pointlessly. Interactive
71
+ // commands (remember, import, extract session assets) must never wait
72
+ // on it — skip the inline upsert/embedding entirely; the write itself
73
+ // (file + commit) has already succeeded by the time this runs, and the
74
+ // rebuild in progress will pick up the change on its own.
75
+ const rebuildProbe = probeLock(getIndexRebuildLockPath());
76
+ if (rebuildProbe.state === "held") {
77
+ // #956: name the launcher pid alongside the holder pid when known —
78
+ // every process listing and task log shows the launcher's pid, not
79
+ // the bun/node child's.
80
+ const holderLabel = formatLockHolderPid({
81
+ pid: rebuildProbe.holderPid,
82
+ launcherPid: rebuildProbe.launcherPid ?? null,
105
83
  });
106
- if (files.length === 0)
107
- return true;
108
- // Generate metadata BEFORE opening the DB so the write window stays
109
- // short. One drain call per file keeps the entry↔path pairing exact and
110
- // reuses the full-index recognize engine (F4a M-core-2 item 5): broken
111
- // workflows drop, valid workflow docs are cached for the side-table upsert.
112
- const component = deriveInstallations([
113
- { path: stashDir, writable: true, ...(options.bundleId ? { registryId: options.bundleId } : {}) },
114
- ])[0]?.components[0];
115
- if (!component)
116
- throw new Error(`Could not derive bundle provenance for ${stashDir}`);
117
- const pairs = [];
118
- const unindexable = new Set();
119
- const rejectedConceptIds = new Set();
120
- for (const file of files) {
121
- if (!fs.existsSync(file)) {
122
- let authoredDanglingSymlink = false;
123
- try {
124
- authoredDanglingSymlink = fs.lstatSync(file).isSymbolicLink();
125
- }
126
- catch {
127
- // A genuinely absent path has no source identity to preflight.
128
- }
129
- if (!authoredDanglingSymlink) {
130
- unindexable.add(file);
131
- continue;
132
- }
84
+ warn(`index rebuild in progress (pid ${holderLabel}); the next index pass will index ${filePaths.join(", ")}`);
85
+ return true;
86
+ }
87
+ const dbPath = getDbPath();
88
+ // `true` here means "the index is in the state the caller expects" — and
89
+ // `acceptProposal` advances its journal to `index-finalized` on the
90
+ // strength of it. Only a genuinely ABSENT index earns that answer: an
91
+ // index we cannot read has NOT been updated, so it falls through to
92
+ // `openExistingDatabase` and surfaces as the honest `false` (#791).
93
+ if (isPathAbsent(dbPath))
94
+ return true;
95
+ // The full walk never descends into dot-directories (for example `.meta/`)
96
+ // — mirror that dot-segment skip here so this fast path indexes exactly
97
+ // what a full run would. Sensitive/
98
+ // infra abstention is the adapter's job now (see the `akmAdapter` note
99
+ // below), not a path pre-filter.
100
+ const files = filePaths.filter((f) => {
101
+ const rel = path.relative(stashDir, f);
102
+ return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
103
+ });
104
+ if (files.length === 0)
105
+ return true;
106
+ // Generate metadata BEFORE opening the DB so the write window stays
107
+ // short. One drain call per file keeps the entry↔path pairing exact and
108
+ // reuses the full-index recognize engine (F4a M-core-2 item 5): broken
109
+ // workflows drop, valid workflow docs are cached for the side-table upsert.
110
+ const component = deriveInstallations([
111
+ { path: stashDir, writable: true, ...(options.bundleId ? { registryId: options.bundleId } : {}) },
112
+ ])[0]?.components[0];
113
+ if (!component)
114
+ throw new Error(`Could not derive bundle provenance for ${stashDir}`);
115
+ const pairs = [];
116
+ const unindexable = new Set();
117
+ const rejectedConceptIds = new Set();
118
+ for (const file of files) {
119
+ if (!fs.existsSync(file)) {
120
+ let authoredDanglingSymlink = false;
121
+ try {
122
+ authoredDanglingSymlink = fs.lstatSync(file).isSymbolicLink();
123
+ }
124
+ catch {
125
+ // A genuinely absent path has no source identity to preflight.
133
126
  }
134
- const ctx = buildFileContext(stashDir, file);
135
- // Hardcoded `akmAdapter` on purpose (owner ruling 2026-07-21): this
136
- // write-path fast path only ever runs for assets a first-class akm
137
- // mutation command just wrote into a managed akm source, so the akm
138
- // adapter is always the right recognizer here — no per-component
139
- // dispatch needed.
140
- const drained = drainDirDocuments(akmAdapter, component, [ctx]);
141
- for (const rejectedPath of drained.rejectedPaths)
142
- unindexable.add(rejectedPath);
143
- for (const conceptId of drained.rejectedConceptIds)
144
- rejectedConceptIds.add(conceptId);
145
- const entry = drained.entries[0];
146
- // A broken workflow drains to zero entries and is treated as
147
- // unindexable; valid peer sources have already compiled to source IR.
148
- const conceptId = drained.conceptIdByFile.get(ctx.absPath);
149
- if (entry && conceptId)
150
- pairs.push({ file, entry, conceptId, contentHash: drained.hashByFile.get(ctx.absPath) });
151
- else
127
+ if (!authoredDanglingSymlink) {
152
128
  unindexable.add(file);
153
- }
154
- const db = openExistingDatabase(dbPath);
155
- try {
156
- db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
157
- if (getEntryCount(db) === 0)
158
- return true;
159
- const targetEntryIds = new Set();
160
- let mutated = false;
161
- db.transaction(() => {
162
- const unindexableEntryIds = new Set();
163
- for (const file of unindexable) {
164
- const rows = db
165
- .prepare(`SELECT id FROM entries
166
- WHERE file_path = ?
167
- AND bundle_id = ?
168
- AND adapter_id = ?`)
169
- .all(file, component.id, component.adapter);
170
- for (const row of rows)
171
- unindexableEntryIds.add(row.id);
172
- }
173
- for (const conceptId of rejectedConceptIds) {
174
- const itemRef = `${component.id}//${conceptId}`;
175
- const rows = db
176
- .prepare(`SELECT id FROM entries
177
- WHERE bundle_id = ? AND adapter_id = ?
178
- AND type = 'workflow'
179
- AND (concept_id = ? OR item_ref = ?)`)
180
- .all(component.id, component.adapter, conceptId, itemRef);
181
- for (const row of rows)
182
- unindexableEntryIds.add(row.id);
183
- }
184
- deleteEntriesByIds(db, [...unindexableEntryIds]);
185
- mutated ||= unindexableEntryIds.size > 0;
186
- for (const { file, entry, conceptId, contentHash } of pairs) {
187
- let entryWithSize = entry;
188
- try {
189
- entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
190
- if (hasMarkdownFragmentContent(entry)) {
191
- setMarkdownFragmentContent(entryWithSize, getMarkdownFragmentContent(entry));
192
- }
193
- }
194
- catch {
195
- // stat raced a delete — index without the size, like the full walk does.
196
- }
197
- // Real provenance (F4a M-core-2 item 5): populate item_ref/content_hash
198
- // via the SAME derivation the full-index writer uses, so a write-path
199
- // row is never a NULL-item_ref straggler.
200
- const provenance = deriveEntryProvenance({ bundleId: component.id, componentId: component.id, adapterId: component.adapter }, entry.type, entry.name, conceptId);
201
- // A materialized file has one current owner. If a targeted write is
202
- // the ownership handoff (for example an explicitly named bundle
203
- // replacing an earlier path-derived bootstrap identity), remove the
204
- // superseded physical-path row before publishing the canonical ref.
205
- const supersededIds = db
206
- .prepare("SELECT id FROM entries WHERE file_path = ? AND item_ref <> ?")
207
- .all(file, provenance.itemRef);
208
- deleteEntriesByIds(db, supersededIds.map((row) => row.id));
209
- targetEntryIds.add(upsertEntry(db, file, entryWithSize, buildSearchText(entry), provenance, contentHash));
210
- mutated = true;
211
- }
212
- })();
213
- if (mutated) {
214
- const config = loadConfig();
215
- await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
216
- publishTargetedEmbeddingMeta(db, config);
129
+ continue;
217
130
  }
218
131
  }
219
- finally {
220
- closeDatabase(db);
132
+ const ctx = buildFileContext(stashDir, file);
133
+ // Hardcoded `akmAdapter` on purpose (owner ruling 2026-07-21): this
134
+ // write-path fast path only ever runs for assets a first-class akm
135
+ // mutation command just wrote into a managed akm source, so the akm
136
+ // adapter is always the right recognizer here — no per-component
137
+ // dispatch needed.
138
+ const drained = drainDirDocuments(akmAdapter, component, [ctx]);
139
+ for (const rejectedPath of drained.rejectedPaths)
140
+ unindexable.add(rejectedPath);
141
+ for (const conceptId of drained.rejectedConceptIds)
142
+ rejectedConceptIds.add(conceptId);
143
+ const entry = drained.entries[0];
144
+ // A broken workflow drains to zero entries and is treated as
145
+ // unindexable; valid peer sources have already compiled to source IR.
146
+ const conceptId = drained.conceptIdByFile.get(ctx.absPath);
147
+ if (entry && conceptId)
148
+ pairs.push({ file, entry, conceptId, contentHash: drained.hashByFile.get(ctx.absPath) });
149
+ else
150
+ unindexable.add(file);
151
+ }
152
+ const db = openExistingDatabase(dbPath);
153
+ try {
154
+ db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
155
+ if (getEntryCount(db) === 0)
156
+ return true;
157
+ const targetEntryIds = new Set();
158
+ let mutated = false;
159
+ db.transaction(() => {
160
+ const unindexableEntryIds = new Set();
161
+ for (const file of unindexable) {
162
+ const rows = db
163
+ .prepare(`SELECT id FROM entries
164
+ WHERE file_path = ?
165
+ AND bundle_id = ?
166
+ AND adapter_id = ?`)
167
+ .all(file, component.id, component.adapter);
168
+ for (const row of rows)
169
+ unindexableEntryIds.add(row.id);
170
+ }
171
+ for (const conceptId of rejectedConceptIds) {
172
+ const itemRef = `${component.id}//${conceptId}`;
173
+ const rows = db
174
+ .prepare(`SELECT id FROM entries
175
+ WHERE bundle_id = ? AND adapter_id = ?
176
+ AND type = 'workflow'
177
+ AND (concept_id = ? OR item_ref = ?)`)
178
+ .all(component.id, component.adapter, conceptId, itemRef);
179
+ for (const row of rows)
180
+ unindexableEntryIds.add(row.id);
181
+ }
182
+ deleteEntriesByIds(db, [...unindexableEntryIds]);
183
+ mutated ||= unindexableEntryIds.size > 0;
184
+ for (const { file, entry, conceptId, contentHash } of pairs) {
185
+ const entryWithSize = withFileSize(entry, file);
186
+ // Real provenance (F4a M-core-2 item 5): populate item_ref/content_hash
187
+ // via the SAME derivation the full-index writer uses, so a write-path
188
+ // row is never a NULL-item_ref straggler.
189
+ const provenance = deriveEntryProvenance({ bundleId: component.id, componentId: component.id, adapterId: component.adapter }, entry.type, entry.name, conceptId);
190
+ // A materialized file has one current owner. If a targeted write is
191
+ // the ownership handoff (for example an explicitly named bundle
192
+ // replacing an earlier path-derived bootstrap identity), remove the
193
+ // superseded physical-path row before publishing the canonical ref.
194
+ const supersededIds = db
195
+ .prepare("SELECT id FROM entries WHERE file_path = ? AND item_ref <> ?")
196
+ .all(file, provenance.itemRef);
197
+ deleteEntriesByIds(db, supersededIds.map((row) => row.id));
198
+ targetEntryIds.add(upsertEntry(db, file, entryWithSize, provenance, contentHash));
199
+ mutated = true;
200
+ }
201
+ })();
202
+ if (mutated) {
203
+ const config = loadConfig();
204
+ await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
205
+ publishTargetedEmbeddingMeta(db, config);
221
206
  }
222
- return true;
223
- })();
207
+ }
208
+ finally {
209
+ closeDatabase(db);
210
+ }
211
+ return true;
224
212
  }
225
213
  catch (error) {
226
214
  // A permission fault is the one failure the next full index will NOT heal,