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
@@ -13,76 +13,31 @@
13
13
  * a lock entry is "managed" (installed from a registry and overwritten on
14
14
  * `akm bundle update`); a bundle with no lock is a plain filesystem/git/website source.
15
15
  */
16
- import { createHash } from "node:crypto";
17
16
  import fs from "node:fs";
18
17
  import path from "node:path";
19
18
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
20
19
  import { isWithin, resolveStashDir } from "../../core/common.js";
21
- import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig, resolveSecret, } from "../../core/config/config.js";
20
+ import { bundleComponentConfig, getSources, loadConfig, resolveSecret } from "../../core/config/config.js";
22
21
  import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
23
22
  import { isPathAbsent } from "../../core/path-access.js";
24
23
  import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
25
- import { beginImmediateTransaction, getStateDbPath, openStateDatabase } from "../../core/state-db.js";
26
24
  import { warn } from "../../core/warn.js";
27
25
  import { resolveGitContentRoot } from "../../core/write-source.js";
28
26
  import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
29
- import { akmIndex, runEmbeddingPass } from "../../indexer/indexer.js";
30
- import { compareAndSwapLockfileSnapshot, publishLockfileUpdate, readLockfile, readLockfileForUpdate, } from "../../integrations/lockfile.js";
27
+ import { akmIndex } from "../../indexer/indexer.js";
28
+ import { readLockfile, upsertLockEntry } from "../../integrations/lockfile.js";
31
29
  import { parseRegistryRef } from "../../registry/resolve.js";
32
- import { sha256Hex } from "../../runtime.js";
33
30
  import { getCachePaths, parseGitRepoUrl, runGit, syncMirroredRepo } from "../../sources/providers/git.js";
31
+ import { replaceDirectory } from "../../sources/providers/git-install.js";
34
32
  import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
35
33
  import { createWebsiteProvider } from "../../sources/providers/website.js";
36
34
  import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
37
35
  import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
38
- import { getWebsiteCachePaths } from "../../sources/website-url.js";
39
- import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
36
+ import { closeDatabase, openReadonlyExistingDatabase } from "../../storage/repositories/index-connection.js";
40
37
  import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
41
- import { auditStashForDangerousKeys, scanStashForDangerousKeys } from "./dangerous-env-audit.js";
38
+ import { auditStashForDangerousKeys } from "./dangerous-env-audit.js";
42
39
  import { removeInstalledRegistryEntry } from "./source-add.js";
43
40
  import { removeStash } from "./source-manage.js";
44
- import { cleanupStagingParent, createStagingParent, prepareDirectoryPublication, prepareWritableGitPublication, } from "./update-transaction.js";
45
- function canonicalJson(value) {
46
- if (Array.isArray(value))
47
- return `[${value.map(canonicalJson).join(",")}]`;
48
- if (value && typeof value === "object") {
49
- const entries = Object.entries(value)
50
- .filter(([, entry]) => entry !== undefined)
51
- // Codepoint order, NOT localeCompare: this string feeds sha256Hex for the
52
- // bundle-audit generation, and localeCompare is ICU/locale-dependent — the
53
- // same object could hash differently on two machines. Matches the other
54
- // canonicalJson implementations (workflows/ir/plan-hash.ts).
55
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
56
- return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
57
- }
58
- return JSON.stringify(value);
59
- }
60
- function bundleAuditGeneration(entry) {
61
- return sha256Hex(canonicalJson(entry ?? null));
62
- }
63
- function bundleAuditFence(config, id) {
64
- const bundle = config.bundles?.[id];
65
- if (!bundle)
66
- throw new ConfigError(`Source "${id}" disappeared before update staging.`);
67
- return {
68
- componentRoot: bundleComponentConfig(bundle)?.root ?? ".",
69
- generation: bundleAuditGeneration(bundle),
70
- writable: bundleComponentConfig(bundle)?.writable ?? bundle.writable === true,
71
- };
72
- }
73
- function assertBundleAuditFence(config, id, expectedGeneration) {
74
- const current = config.bundles?.[id];
75
- if (bundleAuditGeneration(current) !== expectedGeneration) {
76
- throw new ConfigError(`Source "${id}" config changed after its staged bytes were audited; refusing to publish without re-staging and re-auditing.`);
77
- }
78
- }
79
- function currentPlainSource(config, id, type) {
80
- const source = getSources(config).find((candidate) => candidate.name === id && candidate.type === type);
81
- if (!source) {
82
- throw new ConfigError(`Source "${id}" changed or disappeared before update staging; retry to re-resolve it.`);
83
- }
84
- return source;
85
- }
86
41
  /** Enumerate the registry-managed installs (lock-backed bundles) in a config. */
87
42
  function listManagedInstalls(config) {
88
43
  const bundles = config.bundles ?? {};
@@ -105,7 +60,6 @@ function listManagedInstalls(config) {
105
60
  writable: componentWritable ?? bundle.writable === true,
106
61
  componentRoot,
107
62
  requiredRoots: lock.localRoot ? [path.resolve(lock.localRoot, componentRoot)] : [],
108
- auditConfigGeneration: bundleAuditGeneration(bundle),
109
63
  ...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
110
64
  });
111
65
  }
@@ -169,8 +123,8 @@ function describeComponents(entry, bundleRoot) {
169
123
  const names = Object.keys(configuredComponents);
170
124
  if (names.length === 0) {
171
125
  // No explicit component at all — the implicit single component every
172
- // bundle gets (spec §1.2 rule 5; `deriveInstallations`/`componentForSource`
173
- // apply the same "main"-shaped default at index time).
126
+ // bundle gets (spec §1.2 rule 5; `deriveInstallations` applies the same
127
+ // "main"-shaped default at index time).
174
128
  const adapter = bundleRoot !== undefined ? detectAdapterId(bundleRoot) : "akm";
175
129
  return [{ name: "main", adapter, detected: true }];
176
130
  }
@@ -415,237 +369,19 @@ function incompleteFilesystemOutcome(id) {
415
369
  reason: `Filesystem source "${id}" was not scanned completely; preserving its last-known-good index rows.`,
416
370
  };
417
371
  }
418
- let updateTransactionHookForTests;
419
- /** TEST-ONLY. Inject faults at source-update transaction boundaries. */
420
- export function _setUpdateTransactionHookForTests(hook) {
421
- updateTransactionHookForTests = hook;
422
- }
423
- function updateTransactionHook(point, id, context) {
424
- updateTransactionHookForTests?.(point, id, context);
425
- }
426
- function gitHead(repoDir) {
427
- const result = runGit(["-C", repoDir, "rev-parse", "HEAD"]);
428
- return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
429
- }
430
- function invalidWritableGitPublication(repoDir) {
431
- return {
432
- publish() {
433
- throw new ConfigError(`Writable Git source at ${repoDir} is not a valid checkout; refusing to replace its live directory.`);
434
- },
435
- rollback() { },
436
- commit() { },
437
- };
438
- }
439
- function writableGitPublication(opts) {
440
- const auditedTargetHead = opts.auditedTargetHead ?? gitHead(opts.stagedRepo);
441
- if (!opts.expectedOldHead || !auditedTargetHead)
442
- return invalidWritableGitPublication(opts.liveRepo);
443
- return prepareWritableGitPublication({
444
- stagedRepo: opts.stagedRepo,
445
- liveRepo: opts.liveRepo,
446
- expectedOldHead: opts.expectedOldHead,
447
- auditedTargetHead,
448
- });
449
- }
450
- const UPDATE_STATE_SCHEMA = "akm_update_state";
451
- function sqliteStringLiteral(value) {
452
- return `'${value.replaceAll("'", "''")}'`;
453
- }
454
- function openUnifiedUpdateTransaction() {
455
- // Keep canonical state activity registered for the entire attached
456
- // transaction so maintenance/migration cannot replace state.db underneath
457
- // the borrowed index connection.
458
- const stateActivity = openStateDatabase();
459
- try {
460
- const statePath = getStateDbPath();
461
- const db = openIndexDatabase(undefined, {
462
- beforeSchema: (candidate) => {
463
- candidate.exec(`ATTACH DATABASE ${sqliteStringLiteral(statePath)} AS "${UPDATE_STATE_SCHEMA}"`);
464
- // openIndexDatabase invokes this before ensureSchema, so the outer
465
- // transaction begins before the first update-owned index mutation.
466
- beginImmediateTransaction(candidate);
467
- },
468
- });
469
- return {
470
- db,
471
- stateActivity,
472
- deferred: { db, stateSchema: UPDATE_STATE_SCHEMA },
473
- };
474
- }
475
- catch (error) {
476
- stateActivity.close();
477
- throw error;
478
- }
479
- }
480
- function rollbackUnifiedUpdateTransaction(transaction) {
481
- if (!transaction.db.inTransaction)
482
- return undefined;
483
- try {
484
- transaction.db.exec("ROLLBACK");
485
- return undefined;
486
- }
487
- catch (error) {
488
- return error;
489
- }
490
- }
491
- function commitUnifiedUpdateTransaction(transaction) {
492
- if (!transaction.db.inTransaction) {
493
- throw new Error("Source update index returned without its active unified index/state transaction.");
494
- }
495
- transaction.db.exec("COMMIT");
496
- }
497
- function closeUnifiedUpdateTransaction(transaction, committed) {
498
- let closeError = rollbackUnifiedUpdateTransaction(transaction);
499
- try {
500
- transaction.db.close();
501
- }
502
- catch (error) {
503
- closeError ??= error;
504
- }
505
- try {
506
- transaction.stateActivity.close();
507
- }
508
- catch (error) {
509
- closeError ??= error;
510
- }
511
- if (closeError) {
512
- warn(committed
513
- ? `[akm bundle update] committed, but closing its database handles failed: ${String(closeError)}`
514
- : `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
515
- }
516
- }
517
- /**
518
- * Run the embedding phase AFTER the coordinator's atomic commit, on its own
519
- * fresh connection (#954). Before this, `akmUpdate` called
520
- * `akmIndex()` for its embedding phase too, INSIDE this same unified
521
- * `BEGIN IMMEDIATE` — so every per-batch commit the materializer opened
522
- * nested as an unobservable SAVEPOINT, and a SIGKILL mid-run lost every
523
- * embedding of the run rather than just the one in flight. The
524
- * ambient-transaction drift guard (#954) now rejects that outright, so
525
- * `akmIndex`'s own embedding phase is skipped for a deferred update
526
- * transaction and this runs instead, once content/lock/index/state are
527
- * already durably committed.
528
- *
529
- * A failing pass (provider down, timeout) does NOT fail the update: the
530
- * bundle content and index are already committed successfully, exactly like
531
- * a plain `akm index` whose embedding phase fails — only the reported
532
- * `verification` reflects the shortfall (`semanticStatus: "blocked"`).
533
- */
534
- async function runPostCommitEmbeddingPass(index) {
535
- const config = loadConfig();
536
- let db;
537
- try {
538
- const embeddingDim = config.embedding?.dimension;
539
- db = openIndexDatabase(getDbPath(), embeddingDim ? { embeddingDim } : undefined);
540
- const { verification } = await runEmbeddingPass({ db, config, onProgress: () => { } });
541
- return { ...index, verification };
542
- }
543
- catch (error) {
544
- const message = error instanceof Error ? error.message : String(error);
545
- warn(`[akm bundle update] post-commit embedding pass failed: ${message}`);
546
- return {
547
- ...index,
548
- verification: { ...index.verification, ok: false, semanticStatus: "blocked", message },
549
- };
550
- }
551
- finally {
552
- if (db)
553
- closeDatabase(db);
554
- }
555
- }
556
- function pathAtOrBelow(candidate, root) {
557
- const relative = path.relative(path.resolve(root), path.resolve(candidate));
558
- return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
559
- }
560
- function remapStagedPath(candidate, stagedRoot, liveRoot) {
561
- if (!pathAtOrBelow(candidate, stagedRoot))
562
- return candidate;
563
- return path.join(liveRoot, path.relative(stagedRoot, candidate));
372
+ function createStagingParent(parent) {
373
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
374
+ return fs.mkdtempSync(path.join(parent, ".akm-update-stage-"));
564
375
  }
565
- function dangerousFindingGeneration(findings) {
566
- return JSON.stringify(findings
567
- .map(({ envRef, keyName, relPath }) => ({ envRef, keyName, relPath }))
568
- .sort((left, right) => `${left.relPath}\0${left.envRef}\0${left.keyName}`.localeCompare(`${right.relPath}\0${right.envRef}\0${right.keyName}`)));
569
- }
570
- async function contentGeneration(root) {
571
- const hash = createHash("sha256");
572
- const resolvedRoot = path.resolve(root);
573
- const walk = async (directory) => {
574
- const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
575
- for (const entry of entries) {
576
- if (entry.name === ".git")
577
- continue;
578
- const absolute = path.join(directory, entry.name);
579
- const relative = path.relative(resolvedRoot, absolute).replaceAll(path.sep, "/");
580
- const before = fs.lstatSync(absolute);
581
- if (before.isDirectory()) {
582
- hash.update(`D\0${relative}\0${before.mode & 0o777}\0`);
583
- await walk(absolute);
584
- }
585
- else if (before.isSymbolicLink()) {
586
- hash.update(`L\0${relative}\0${fs.readlinkSync(absolute)}\0`);
587
- }
588
- else if (before.isFile()) {
589
- hash.update(`F\0${relative}\0${before.mode & 0o777}\0${before.size}\0`);
590
- for await (const chunk of fs.createReadStream(absolute))
591
- hash.update(chunk);
592
- const after = fs.lstatSync(absolute);
593
- if (after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
594
- throw new ConfigError(`Source content changed while ${relative} was being audited; retry the update.`);
595
- }
596
- hash.update("\0");
597
- }
598
- else {
599
- throw new ConfigError(`Unsupported filesystem entry ${absolute}; refusing to audit an unstable source tree.`);
600
- }
601
- }
602
- };
603
- if (!fs.statSync(resolvedRoot).isDirectory())
604
- throw new ConfigError(`Expected ${resolvedRoot} to be a directory.`);
605
- await walk(resolvedRoot);
606
- return hash.digest("hex");
607
- }
608
- async function verifyPublishedAudit(prepared, id) {
609
- const liveRoot = prepared.publishedAuditRoot;
610
- if (!liveRoot)
611
- return;
612
- const resolveLiveAuditRoot = () => {
613
- const physicalRoot = canonicalWritablePath(liveRoot, `published component root for "${id}"`);
614
- const containmentRoot = prepared.publishedAuditContainmentRoot;
615
- if (containmentRoot && !pathAtOrBelow(physicalRoot, containmentRoot)) {
616
- throw new ConfigError(`Published component root ${liveRoot} resolves outside its physical checkout at ${containmentRoot}.`, "DANGEROUS_ENV_AUDIT_FAILED");
617
- }
618
- const expected = prepared.publishedAuditExpectedPhysicalRoot;
619
- if (expected && path.resolve(physicalRoot) !== path.resolve(expected)) {
620
- throw new ConfigError(`Published component root ${liveRoot} resolves to ${physicalRoot}, not its audited staged target ${expected}.`, "DANGEROUS_ENV_AUDIT_FAILED");
621
- }
622
- return physicalRoot;
623
- };
624
- const beforeRoot = resolveLiveAuditRoot();
625
- const beforeAudit = await contentGeneration(beforeRoot);
626
- let findings;
627
- try {
628
- const scanRoot = resolveLiveAuditRoot();
629
- if (scanRoot !== beforeRoot)
630
- throw new Error("configured component symlink changed during re-audit");
631
- findings = scanStashForDangerousKeys(scanRoot);
632
- }
633
- catch (error) {
634
- throw new ConfigError(`Dangerous environment-key re-audit failed after materializing "${id}"; rolling the update back. ${error instanceof Error ? error.message : String(error)}`, "DANGEROUS_ENV_AUDIT_FAILED");
635
- }
636
- const afterRoot = resolveLiveAuditRoot();
637
- const afterAudit = await contentGeneration(afterRoot);
638
- if (afterRoot !== beforeRoot ||
639
- beforeAudit !== prepared.auditedContentGeneration ||
640
- afterAudit !== prepared.auditedContentGeneration ||
641
- dangerousFindingGeneration(findings) !== prepared.approvedDangerousFindings) {
642
- throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
643
- }
376
+ /** A bundle's single component root and writability. */
377
+ function bundleComponent(config, id) {
378
+ const bundle = config.bundles?.[id];
379
+ const component = bundleComponentConfig(bundle);
380
+ return { root: component?.root ?? ".", writable: component?.writable ?? bundle?.writable === true };
644
381
  }
645
- async function auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys) {
646
- updateTransactionHook("staged", id);
382
+ async function auditStagedUpdate(auditRoot, ref, allowDangerousEnvKeys) {
647
383
  const decision = await auditStashForDangerousKeys({
648
- stashRoot: prepared.auditRoot,
384
+ stashRoot: auditRoot,
649
385
  ref,
650
386
  allowDangerousKeys: allowDangerousEnvKeys,
651
387
  isTTY: process.stdin.isTTY === true,
@@ -654,347 +390,179 @@ async function auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys) {
654
390
  });
655
391
  if (decision.blocked)
656
392
  throw new NotFoundError(decision.error, decision.code);
657
- prepared.approvedDangerousFindings = dangerousFindingGeneration(decision.findings);
658
- prepared.auditedContentGeneration = await contentGeneration(prepared.auditRoot);
659
- updateTransactionHook("audited", id);
660
393
  }
661
- function canonicalWritablePath(candidate, description) {
662
- try {
663
- return fs.realpathSync(candidate);
664
- }
665
- catch (error) {
666
- throw new ConfigError(`Cannot resolve ${description} at ${candidate}; refusing writable Git update. ${error instanceof Error ? error.message : String(error)}`);
394
+ function gitOrThrow(repoDir, args) {
395
+ const result = runGit(["-C", repoDir, ...args], { timeout: 120_000 });
396
+ if (result.status !== 0) {
397
+ throw new UsageError(`git ${args[0]} failed in ${repoDir}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
667
398
  }
399
+ return result.stdout.trim();
668
400
  }
669
- function containedWritablePath(candidate, physicalRepo, description) {
670
- const physicalCandidate = canonicalWritablePath(candidate, description);
671
- if (!pathAtOrBelow(physicalCandidate, physicalRepo)) {
672
- throw new ConfigError(`${description} ${candidate} resolves outside its physical checkout at ${physicalRepo}.`);
673
- }
674
- return physicalCandidate;
401
+ /**
402
+ * Publish an audited writable checkout: fast-forward the live checkout to
403
+ * exactly the staged commit. Git refuses a non-fast-forward or a merge that
404
+ * would overwrite local work, and changes nothing in that case.
405
+ */
406
+ function fastForwardCheckout(liveRepo, stagedRepo) {
407
+ const head = gitOrThrow(stagedRepo, ["rev-parse", "HEAD"]);
408
+ gitOrThrow(liveRepo, ["fetch", "--no-tags", stagedRepo, head]);
409
+ gitOrThrow(liveRepo, ["merge", "--ff-only", "--no-overwrite-ignore", head]);
675
410
  }
676
- function prepareWritableManagedUpdate(managed, force) {
677
- const physicalLocalRoot = canonicalWritablePath(managed.localRoot, "managed content root");
678
- const topLevel = runGit(["-C", physicalLocalRoot, "rev-parse", "--show-toplevel"]);
679
- // Production writable installs are Git checkouts. Falling back to the
680
- // locked root keeps the provider seam testable; the real Git provider still
681
- // refuses a copied non-checkout before publication.
682
- const liveRepo = topLevel.status === 0 && topLevel.stdout.trim()
683
- ? canonicalWritablePath(topLevel.stdout.trim(), "managed Git top-level")
684
- : physicalLocalRoot;
685
- if (!pathAtOrBelow(physicalLocalRoot, liveRepo)) {
686
- throw new ConfigError(`Writable Git content root ${managed.localRoot} resolves outside its physical checkout at ${liveRepo}.`);
687
- }
688
- // Keep the configured component path as a lexical identity relative to the
689
- // content root. Resolving it to today's symlink target here would pin the old
690
- // target and miss an upstream commit that retargets the symlink.
691
- const lexicalLocalRoot = path.resolve(managed.localRoot);
692
- const configuredLiveRoots = managed.requiredRoots.map((root) => {
693
- const lexicalRoot = path.resolve(root);
694
- if (!pathAtOrBelow(lexicalRoot, lexicalLocalRoot)) {
695
- throw new ConfigError(`Configured writable Git component root ${lexicalRoot} escapes content root ${lexicalLocalRoot}.`);
696
- }
697
- return path.resolve(physicalLocalRoot, path.relative(lexicalLocalRoot, lexicalRoot));
698
- });
699
- const physicalRequiredRoots = configuredLiveRoots.map((root) => containedWritablePath(root, liveRepo, `Configured component root for "${managed.installId}"`));
700
- // The Git provider must preserve ordinary component directories, but not a
701
- // symlink's current physical target. The configured lexical path is audited
702
- // again after sync, so retaining the old target here would reject a valid
703
- // upstream retarget before the new target can reach that audit.
704
- const preservedPhysicalRequiredRoots = physicalRequiredRoots.filter((root, index) => configuredLiveRoots[index] === root);
705
- const expectedOldHead = gitHead(liveRepo);
706
- const stagingParent = createStagingParent(path.dirname(liveRepo));
707
- const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
708
- fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
709
- const stagedContentRoot = remapStagedPath(physicalLocalRoot, liveRepo, stagedRepo);
710
- const stagedPhysicalRequiredRoots = preservedPhysicalRequiredRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
711
- const stagedConfiguredRoots = configuredLiveRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
712
- return syncFromRef(managed.ref, {
713
- force,
714
- writable: true,
715
- writableRoot: stagedContentRoot,
716
- ...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
717
- ...(managed.credential ? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) } : {}),
718
- })
719
- .then((staged) => {
720
- const auditedTargetHead = gitHead(stagedRepo);
721
- const physicalStagedRepo = canonicalWritablePath(stagedRepo, "staged managed Git checkout");
722
- const auditedConfiguredRoots = stagedConfiguredRoots.map((root) => containedWritablePath(root, physicalStagedRepo, `Post-sync component root for "${managed.installId}"`));
723
- const expectedPublishedRoots = auditedConfiguredRoots.map((root) => path.join(liveRepo, path.relative(physicalStagedRepo, root)));
724
- const synced = {
725
- ...staged,
726
- resolvedRevision: auditedTargetHead ?? staged.resolvedRevision,
727
- contentDir: remapStagedPath(staged.contentDir, stagedRepo, liveRepo),
728
- cacheDir: remapStagedPath(staged.cacheDir, stagedRepo, liveRepo),
729
- extractedDir: remapStagedPath(staged.extractedDir, stagedRepo, liveRepo),
730
- };
731
- return {
732
- synced,
733
- auditRoot: auditedConfiguredRoots[0] ?? staged.contentDir,
734
- publishedAuditRoot: configuredLiveRoots[0] ?? synced.contentDir,
735
- publishedAuditContainmentRoot: liveRepo,
736
- publishedAuditExpectedPhysicalRoot: expectedPublishedRoots[0] ?? synced.contentDir,
737
- publication: writableGitPublication({ stagedRepo, liveRepo, expectedOldHead, auditedTargetHead }),
738
- cleanup: () => cleanupStagingParent(stagingParent),
739
- };
740
- })
741
- .catch((error) => {
742
- cleanupStagingParent(stagingParent);
743
- throw error;
411
+ /**
412
+ * Reindex after an update. Sources are not re-fetched (the update just did
413
+ * that) and detected adapters are not written to config.json — an update
414
+ * leaves config alone; the next `akm index` records them.
415
+ */
416
+ function reindexAfterUpdate(stashDir) {
417
+ return akmIndex({ stashDir, full: true, hydrateSources: false, persistDetectedAdapters: false });
418
+ }
419
+ /** Publish, then reindex, holding the asset-mutation lease so no other writer interleaves. */
420
+ function publishAndIndex(stashDir, publish) {
421
+ return withAssetMutationLease("source-update", async () => {
422
+ await publish();
423
+ return reindexAfterUpdate(stashDir);
744
424
  });
745
425
  }
746
- async function prepareManagedUpdate(managed, force) {
747
- if (managed.writable && managed.localRoot)
748
- return prepareWritableManagedUpdate(managed, force);
426
+ /** Fetch a registry-managed install into staging without touching its live content. */
427
+ async function stageManagedUpdate(managed, force) {
428
+ const credential = managed.credential
429
+ ? resolveSecret(managed.credential, storeSecretResolver.resolveSecret)
430
+ : undefined;
431
+ if (managed.writable && managed.localRoot) {
432
+ // A writable install is a checkout that may carry local commits: the
433
+ // provider fast-forwards a copy, the copy is audited, and the live
434
+ // checkout is then fast-forwarded to exactly the audited commit.
435
+ const topLevel = runGit(["-C", managed.localRoot, "rev-parse", "--show-toplevel"]);
436
+ if (topLevel.status !== 0 || !topLevel.stdout.trim()) {
437
+ throw new UsageError(`Writable Git install at ${managed.localRoot} is not a checkout; refusing to update it.`);
438
+ }
439
+ const liveRepo = topLevel.stdout.trim();
440
+ const stagingParent = createStagingParent(path.dirname(liveRepo));
441
+ try {
442
+ const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
443
+ fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
444
+ const stagedRoot = path.join(stagedRepo, path.relative(liveRepo, fs.realpathSync(managed.localRoot)));
445
+ const fetched = await syncFromRef(managed.ref, {
446
+ force,
447
+ writable: true,
448
+ writableRoot: stagedRoot,
449
+ writableRequiredRoots: managed.requiredRoots.map((root) => path.join(stagedRoot, path.relative(managed.localRoot, root))),
450
+ ...(credential ? { credential } : {}),
451
+ });
452
+ return {
453
+ synced: { ...fetched, contentDir: managed.localRoot, cacheDir: liveRepo, extractedDir: liveRepo },
454
+ staged: {
455
+ auditRoot: path.resolve(fetched.contentDir, managed.componentRoot),
456
+ publish: () => fastForwardCheckout(liveRepo, stagedRepo),
457
+ cleanup: () => cleanupDirectoryBestEffort(stagingParent, "bundle update"),
458
+ },
459
+ };
460
+ }
461
+ catch (error) {
462
+ cleanupDirectoryBestEffort(stagingParent, "bundle update");
463
+ throw error;
464
+ }
465
+ }
749
466
  if (managed.source === "local") {
467
+ // The user's own directory: nothing to fetch or publish.
750
468
  const synced = await syncFromRef(managed.ref, { force, writable: managed.writable });
751
469
  return {
752
470
  synced,
753
- auditRoot: resolveComponentAuditRoot(synced.contentDir, managed.componentRoot, managed.installId),
754
- cleanup: () => { },
471
+ staged: { auditRoot: path.resolve(synced.contentDir, managed.componentRoot), publish() { }, cleanup() { } },
755
472
  };
756
473
  }
757
- const canonicalCacheRoot = getRegistryCacheDir();
758
- const stagingParent = createStagingParent(canonicalCacheRoot);
474
+ const cacheRoot = getRegistryCacheDir();
475
+ const stagingParent = createStagingParent(cacheRoot);
759
476
  try {
760
- const staged = await syncFromRef(managed.ref, {
477
+ const fetched = await syncFromRef(managed.ref, {
761
478
  force,
762
479
  writable: false,
763
480
  cacheRootDir: stagingParent,
764
- ...(managed.credential
765
- ? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) }
766
- : {}),
481
+ ...(credential ? { credential } : {}),
767
482
  });
768
- if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
769
- // Test/provider seams may return an already-isolated candidate. It remains
770
- // invisible until the lock points at it, so no directory promotion is needed.
771
- return {
772
- synced: staged,
773
- auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
774
- cleanup: () => cleanupStagingParent(stagingParent),
775
- };
776
- }
777
- const liveCacheDir = path.join(canonicalCacheRoot, path.relative(stagingParent, staged.cacheDir));
778
- const synced = {
779
- ...staged,
780
- contentDir: remapStagedPath(staged.contentDir, staged.cacheDir, liveCacheDir),
781
- cacheDir: liveCacheDir,
782
- extractedDir: remapStagedPath(staged.extractedDir, staged.cacheDir, liveCacheDir),
783
- };
483
+ const liveCacheDir = path.join(cacheRoot, path.relative(stagingParent, fetched.cacheDir));
484
+ const toLive = (candidate) => path.join(liveCacheDir, path.relative(fetched.cacheDir, candidate));
784
485
  return {
785
- synced,
786
- auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
787
- publication: prepareDirectoryPublication(staged.cacheDir, liveCacheDir),
788
- cleanup: () => cleanupStagingParent(stagingParent),
486
+ synced: {
487
+ ...fetched,
488
+ cacheDir: liveCacheDir,
489
+ contentDir: toLive(fetched.contentDir),
490
+ extractedDir: toLive(fetched.extractedDir),
491
+ },
492
+ staged: {
493
+ auditRoot: path.resolve(fetched.contentDir, managed.componentRoot),
494
+ publish: () => replaceDirectory(fetched.cacheDir, liveCacheDir),
495
+ cleanup: () => cleanupDirectoryBestEffort(stagingParent, "bundle update"),
496
+ },
789
497
  };
790
498
  }
791
499
  catch (error) {
792
- cleanupStagingParent(stagingParent);
500
+ cleanupDirectoryBestEffort(stagingParent, "bundle update");
793
501
  throw error;
794
502
  }
795
503
  }
796
- function resolveComponentAuditRoot(contentRoot, componentRoot, id) {
797
- const resolved = path.resolve(contentRoot, componentRoot);
798
- if (!pathAtOrBelow(resolved, contentRoot)) {
799
- throw new ConfigError(`Component root "${componentRoot}" escapes staged bundle "${id}".`);
800
- }
801
- return resolved;
802
- }
803
- async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, full, expectedConfigGeneration) {
804
- try {
805
- await auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys);
806
- return await withAssetMutationLease("source-update", async () => {
807
- let transaction;
808
- let releaseConfigFence;
809
- let contentPublished = false;
810
- let committed = false;
811
- try {
812
- const configFence = acquireConfigReadFence();
813
- releaseConfigFence = configFence.release;
814
- assertBundleAuditFence(configFence.config, id, expectedConfigGeneration);
815
- updateTransactionHook("fenced", id);
816
- if (prepared.publication) {
817
- prepared.publication.publish();
818
- contentPublished = true;
819
- }
820
- await verifyPublishedAudit(prepared, id);
821
- updateTransactionHook("published", id);
822
- updateTransactionHook("before-index", id);
823
- transaction = openUnifiedUpdateTransaction();
824
- const index = await akmIndex({
825
- stashDir,
826
- ...(full ? { full: true } : {}),
827
- hydrateSources: false,
828
- persistDetectedAdapters: false,
829
- deferredUpdateTransaction: transaction.deferred,
830
- });
831
- updateTransactionHook("indexed", id, { db: transaction.db });
832
- // Re-fence the exact published worktree after the potentially long
833
- // index pass and immediately before the coordinator commit. This
834
- // catches cooperating or observable external writes that land after
835
- // the first post-materialization audit.
836
- await verifyPublishedAudit(prepared, id);
837
- updateTransactionHook("before-commit", id, { db: transaction.db });
838
- commitUnifiedUpdateTransaction(transaction);
839
- committed = true;
840
- prepared.publication?.commit();
841
- return await runPostCommitEmbeddingPass(index);
842
- }
843
- catch (error) {
844
- let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
845
- try {
846
- if (contentPublished)
847
- prepared.publication?.rollback();
848
- }
849
- catch (rollbackError) {
850
- recoveryError ??= rollbackError;
851
- }
852
- if (recoveryError)
853
- throw recoveryError;
854
- throw error;
855
- }
856
- finally {
857
- try {
858
- if (transaction)
859
- closeUnifiedUpdateTransaction(transaction, committed);
860
- }
861
- finally {
862
- releaseConfigFence?.();
863
- }
864
- }
865
- });
866
- }
867
- finally {
868
- prepared.cleanup();
869
- }
870
- }
871
- async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
504
+ /** Fetch a plain git source into staging, audit it, publish it, and reindex. */
505
+ async function syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnvKeys) {
506
+ const id = gitSource.name ?? gitSource.url ?? "";
507
+ const ref = gitSource.url ?? "";
872
508
  if (!gitSource.url)
873
- throw new ConfigError(`Git source "${gitSource.name ?? "git"}" has no URL.`);
874
- const repo = parseGitRepoUrl(gitSource.url);
875
- const livePaths = getCachePaths(repo.canonicalUrl);
509
+ throw new ConfigError(`Git source "${id}" has no URL.`);
510
+ const component = bundleComponent(config, id);
511
+ const canonicalUrl = parseGitRepoUrl(gitSource.url).canonicalUrl;
512
+ const livePaths = getCachePaths(canonicalUrl);
876
513
  const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
877
- const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
878
514
  try {
879
- const hasLiveCheckout = writable && fs.existsSync(livePaths.repoDir);
880
- const expectedOldHead = hasLiveCheckout ? gitHead(livePaths.repoDir) : undefined;
881
- if (hasLiveCheckout) {
515
+ const stagedPaths = getCachePaths(canonicalUrl, stagingParent);
516
+ // A writable mirror may carry local commits: pull into a copy, then
517
+ // fast-forward the live checkout once the copy passes the audit.
518
+ const liveCheckout = component.writable && fs.existsSync(livePaths.repoDir);
519
+ if (liveCheckout)
882
520
  fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
883
- }
884
- const staged = await syncMirroredRepo(gitSource, {
521
+ await syncMirroredRepo(gitSource, {
885
522
  force: true,
886
- writable,
523
+ writable: component.writable,
887
524
  cacheRootDir: stagingParent,
888
525
  ...(gitSource.credential
889
526
  ? { credential: resolveSecret(gitSource.credential, storeSecretResolver.resolveSecret) }
890
527
  : {}),
891
528
  });
892
- const configuredStagedAuditRoot = resolveComponentAuditRoot(resolveGitContentRoot(staged.contentDir), componentRoot, gitSource.name ?? "git");
893
- if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
894
- if (pathAtOrBelow(configuredStagedAuditRoot, livePaths.rootDir)) {
895
- throw new ConfigError(`Git provider for "${gitSource.name ?? gitSource.url}" wrote into the active cache instead of the update staging root; refusing to index it.`);
896
- }
897
- // A provider test seam may hand back an already-isolated candidate.
898
- // Audit it, but do not move an unowned directory into the canonical
899
- // cache. Real providers honor cacheRootDir and use the promotion below.
900
- return {
901
- auditRoot: configuredStagedAuditRoot,
902
- cleanup: () => cleanupStagingParent(stagingParent),
903
- };
904
- }
905
- let auditRoot = configuredStagedAuditRoot;
906
- let publishedAuditRoot;
907
- let publishedAuditContainmentRoot;
908
- let publishedAuditExpectedPhysicalRoot;
909
- if (writable) {
910
- const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
911
- auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
912
- publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
913
- if (hasLiveCheckout) {
914
- publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
915
- }
916
- else {
917
- const physicalCacheParent = canonicalWritablePath(path.dirname(livePaths.rootDir), "plain Git cache parent");
918
- publishedAuditContainmentRoot = path.join(physicalCacheParent, path.basename(livePaths.rootDir), path.relative(livePaths.rootDir, livePaths.repoDir));
919
- }
920
- publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
921
- }
922
- return {
923
- auditRoot,
924
- ...(publishedAuditRoot ? { publishedAuditRoot } : {}),
925
- ...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
926
- ...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
927
- publication: writable && hasLiveCheckout
928
- ? writableGitPublication({
929
- stagedRepo: stagedPaths.repoDir,
930
- liveRepo: livePaths.repoDir,
931
- expectedOldHead,
932
- })
933
- : prepareDirectoryPublication(staged.cacheDir, livePaths.rootDir),
934
- cleanup: () => cleanupStagingParent(stagingParent),
935
- };
936
- }
937
- catch (error) {
938
- cleanupStagingParent(stagingParent);
939
- throw error;
940
- }
941
- }
942
- async function syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys) {
943
- const id = gitSource.name ?? gitSource.url ?? "";
944
- const currentConfig = loadConfig();
945
- const currentSource = currentPlainSource(currentConfig, id, "git");
946
- const ref = currentSource.url ?? "";
947
- const fence = bundleAuditFence(currentConfig, id);
948
- const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
949
- const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
950
- return { item: { id, kind: "git", ref }, index };
951
- }
952
- async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
953
- if (!websiteSource.url)
954
- throw new ConfigError(`Website source "${websiteSource.name ?? "website"}" has no URL.`);
955
- const livePaths = getWebsiteCachePaths(websiteSource.url);
956
- const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
957
- const stagedPaths = getWebsiteCachePaths(websiteSource.url, stagingParent);
958
- try {
959
- const provider = createWebsiteProvider(websiteSource);
960
- await provider.sync({
961
- force: true,
962
- secrets: storeSecretResolver,
963
- ensureWebsiteMirror: (config, options) => ensureWebsiteMirror(config, {
964
- ...options,
965
- resolveSecret: options?.resolveSecret ?? storeSecretResolver.resolveSecret,
966
- cacheRootDir: stagingParent,
967
- }),
529
+ await auditStagedUpdate(path.resolve(resolveGitContentRoot(stagedPaths.repoDir), component.root), ref, allowDangerousEnvKeys);
530
+ const index = await publishAndIndex(stashDir, () => {
531
+ if (liveCheckout)
532
+ fastForwardCheckout(livePaths.repoDir, stagedPaths.repoDir);
533
+ else
534
+ replaceDirectory(stagedPaths.rootDir, livePaths.rootDir);
968
535
  });
969
- return {
970
- auditRoot: resolveComponentAuditRoot(stagedPaths.stashDir, componentRoot, websiteSource.name ?? "website"),
971
- publication: prepareDirectoryPublication(stagedPaths.rootDir, livePaths.rootDir),
972
- cleanup: () => cleanupStagingParent(stagingParent),
973
- };
536
+ return { item: { id, kind: "git", ref }, index };
974
537
  }
975
- catch (error) {
976
- cleanupStagingParent(stagingParent);
977
- throw error;
538
+ finally {
539
+ cleanupDirectoryBestEffort(stagingParent, "bundle update");
978
540
  }
979
541
  }
980
- async function syncWebsitePlainSource(websiteSource, stashDir, allowDangerousEnvKeys) {
542
+ /**
543
+ * Re-crawl a website source and reindex. The website provider already builds
544
+ * its snapshot in a staging directory and swaps it in, so a killed refresh
545
+ * keeps the previous mirror; a snapshot holds only knowledge pages, so there
546
+ * is no env file to audit.
547
+ */
548
+ async function syncWebsitePlainSource(websiteSource, stashDir) {
981
549
  const id = websiteSource.name ?? websiteSource.url ?? "";
982
- const currentConfig = loadConfig();
983
- const currentSource = currentPlainSource(currentConfig, id, "website");
984
- const ref = currentSource.url ?? "";
985
- const fence = bundleAuditFence(currentConfig, id);
986
- const prepared = await prepareWebsitePlainUpdate(currentSource, fence.componentRoot);
987
- const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
988
- return { item: { id, kind: "website", ref }, index };
550
+ await createWebsiteProvider(websiteSource).sync({
551
+ force: true,
552
+ secrets: storeSecretResolver,
553
+ ensureWebsiteMirror,
554
+ });
555
+ const index = await reindexAfterUpdate(stashDir);
556
+ return { item: { id, kind: "website", ref: websiteSource.url ?? "" }, index };
989
557
  }
990
558
  /** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
991
- async function updateGitSource(stashDir, target, all, gitSource, allowDangerousEnvKeys) {
992
- const synced = await syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys);
559
+ async function updateGitSource(config, stashDir, target, all, gitSource, allowDangerousEnvKeys) {
560
+ const synced = await syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnvKeys);
993
561
  return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
994
562
  }
995
563
  /** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
996
- async function updateWebsiteSource(stashDir, target, all, websiteSource, allowDangerousEnvKeys) {
997
- const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowDangerousEnvKeys);
564
+ async function updateWebsiteSource(stashDir, target, all, websiteSource) {
565
+ const synced = await syncWebsitePlainSource(websiteSource, stashDir);
998
566
  return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
999
567
  }
1000
568
  /** Reconcile a filesystem source's current bytes without provider hydration. */
@@ -1025,29 +593,23 @@ async function updateFilesystemSource(stashDir, target, all, filesystemSource) {
1025
593
  * {@link updateManagedInstall} verbatim rather than duplicating its lock/config
1026
594
  * bookkeeping.
1027
595
  */
1028
- function managedInstallViewOfPlainNpm(npmSource) {
596
+ function managedInstallViewOfPlainNpm(config, npmSource) {
1029
597
  const id = npmSource.name ?? npmSource.path ?? "";
1030
- const config = loadConfig();
1031
- const currentSource = currentPlainSource(config, id, "npm");
1032
- const spec = currentSource.path ?? "";
1033
- const ref = spec.startsWith("npm:") ? spec : `npm:${spec}`;
1034
- const fence = bundleAuditFence(config, id);
598
+ const spec = npmSource.path ?? "";
1035
599
  return {
1036
600
  bundleKey: id,
1037
601
  installId: id,
1038
602
  source: "npm",
1039
- ref,
603
+ ref: spec.startsWith("npm:") ? spec : `npm:${spec}`,
1040
604
  localRoot: "",
1041
- resolvedVersion: undefined,
1042
- resolvedRevision: undefined,
1043
605
  writable: false,
1044
- componentRoot: fence.componentRoot,
606
+ componentRoot: bundleComponent(config, id).root,
1045
607
  requiredRoots: [],
1046
- auditConfigGeneration: fence.generation,
1047
608
  };
1048
609
  }
1049
610
  /**
1050
- * Sync a single registry-managed install and return the processed record.
611
+ * Sync a single registry-managed install and return the processed record:
612
+ * fetch into staging, audit, publish, write its lock entry, reindex.
1051
613
  *
1052
614
  * `yes` gates ONLY the destructive branch below (deleting a previous
1053
615
  * `localRoot` whose content moved) — it has no effect on the sync itself. A
@@ -1058,16 +620,9 @@ function managedInstallViewOfPlainNpm(npmSource) {
1058
620
  * command).
1059
621
  */
1060
622
  async function updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys) {
1061
- // No pre-cleanup of the old root, even under --force: the providers already
1062
- // re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
1063
- // only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
1064
- // succeeds would turn any sync failure (network down, bad ref) into losing a
1065
- // previously-working install. The old root is cleaned up below only after
1066
- // config/lock publication and reindexing both succeed.
1067
- const prepared = await prepareManagedUpdate(managed, force);
623
+ const { synced, staged } = await stageManagedUpdate(managed, force);
1068
624
  try {
1069
- await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowDangerousEnvKeys);
1070
- const synced = prepared.synced;
625
+ await auditStagedUpdate(staged.auditRoot, managed.ref, allowDangerousEnvKeys);
1071
626
  const installedEntry = {
1072
627
  id: managed.installId,
1073
628
  // Preserve the original source classification. syncFromRef() re-derives the
@@ -1093,10 +648,9 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowDangerou
1093
648
  if (!confirmed)
1094
649
  throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
1095
650
  }
1096
- return await withAssetMutationLease("source-update", async () => {
1097
- const oldLockSnapshot = readLockfileForUpdate();
1098
- const oldLocks = oldLockSnapshot.entries;
1099
- const desiredLock = {
651
+ const index = await publishAndIndex(stashDir, async () => {
652
+ staged.publish();
653
+ await upsertLockEntry({
1100
654
  id: managed.bundleKey,
1101
655
  // Preserve the STORED install kind: a `github:`-ref entry recorded as
1102
656
  // source "git" must not be reclassified by the sync flow's re-derivation.
@@ -1107,142 +661,42 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowDangerou
1107
661
  integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
1108
662
  localRoot: synced.contentDir,
1109
663
  installedAt: synced.syncedAt,
1110
- };
1111
- const desiredLocks = [...oldLocks.filter((entry) => entry.id !== desiredLock.id), desiredLock];
1112
- const desiredLockGeneration = generationHash(desiredLocks);
1113
- let transaction;
1114
- let releaseConfigFence;
1115
- let contentPublished = false;
1116
- let lockPublished = false;
1117
- let publishedLockSnapshot;
1118
- let committed = false;
1119
- let index;
1120
- try {
1121
- const configFence = acquireConfigReadFence();
1122
- releaseConfigFence = configFence.release;
1123
- assertBundleAuditFence(configFence.config, managed.bundleKey, managed.auditConfigGeneration);
1124
- updateTransactionHook("fenced", managed.installId);
1125
- if (prepared.publication) {
1126
- prepared.publication.publish();
1127
- contentPublished = true;
1128
- }
1129
- await verifyPublishedAudit(prepared, managed.installId);
1130
- publishedLockSnapshot = await publishLockfileUpdate(oldLockSnapshot, desiredLocks);
1131
- if (!publishedLockSnapshot)
1132
- throw concurrentGenerationError(managed);
1133
- lockPublished = true;
1134
- updateTransactionHook("published", managed.installId);
1135
- updateTransactionHook("before-index", managed.installId);
1136
- transaction = openUnifiedUpdateTransaction();
1137
- index = await akmIndex({
1138
- stashDir,
1139
- full: true,
1140
- hydrateSources: false,
1141
- persistDetectedAdapters: false,
1142
- deferredUpdateTransaction: transaction.deferred,
1143
- });
1144
- const currentLockSnapshot = readLockfileForUpdate();
1145
- if (generationHash(currentLockSnapshot.entries) !== desiredLockGeneration ||
1146
- currentLockSnapshot.raw !== publishedLockSnapshot.raw ||
1147
- currentLockSnapshot.mode !== publishedLockSnapshot.mode) {
1148
- throw concurrentGenerationError(managed);
1149
- }
1150
- updateTransactionHook("indexed", managed.installId, { db: transaction.db });
1151
- // Indexing can be long enough for a local writer to race the first
1152
- // post-materialization audit. Re-verify the exact bytes/findings at the
1153
- // final cross-store commit boundary while every AKM fence is held.
1154
- await verifyPublishedAudit(prepared, managed.installId);
1155
- updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
1156
- commitUnifiedUpdateTransaction(transaction);
1157
- committed = true;
1158
- }
1159
- catch (error) {
1160
- let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
1161
- let concurrentGeneration = false;
1162
- let concurrentGenerationOwnsPublishedRoot = false;
1163
- if (lockPublished) {
1164
- try {
1165
- if (!publishedLockSnapshot ||
1166
- !(await compareAndSwapLockfileSnapshot(publishedLockSnapshot, oldLockSnapshot))) {
1167
- concurrentGeneration = true;
1168
- const concurrentLocks = readLockfileForUpdate().entries;
1169
- const concurrentEntry = concurrentLocks.find((entry) => entry.id === desiredLock.id);
1170
- concurrentGenerationOwnsPublishedRoot =
1171
- prepared.publication !== undefined &&
1172
- concurrentEntry?.localRoot !== undefined &&
1173
- path.resolve(concurrentEntry.localRoot) === path.resolve(synced.contentDir);
1174
- }
1175
- }
1176
- catch (compensationError) {
1177
- recoveryError ??= compensationError;
1178
- }
1179
- }
1180
- if (!concurrentGenerationOwnsPublishedRoot) {
1181
- try {
1182
- if (contentPublished)
1183
- prepared.publication?.rollback();
1184
- }
1185
- catch (rollbackError) {
1186
- recoveryError ??= rollbackError;
1187
- }
1188
- }
1189
- if (recoveryError)
1190
- throw recoveryError;
1191
- if (concurrentGeneration)
1192
- throw concurrentGenerationError(managed);
1193
- throw error;
1194
- }
1195
- finally {
1196
- try {
1197
- if (transaction)
1198
- closeUnifiedUpdateTransaction(transaction, committed);
1199
- }
1200
- finally {
1201
- releaseConfigFence?.();
1202
- }
664
+ });
665
+ });
666
+ if (movedRoot) {
667
+ if (referencesRoot(managed.localRoot, loadConfig(), readLockfile())) {
668
+ warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
1203
669
  }
1204
- prepared.publication?.commit();
1205
- index = await runPostCommitEmbeddingPass(index);
1206
- if (movedRoot) {
1207
- const currentConfig = loadConfig();
1208
- const currentLocks = readLockfile();
1209
- if (referencesRoot(managed.localRoot, currentConfig, currentLocks)) {
1210
- warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
1211
- }
1212
- else {
1213
- cleanupDirectoryBestEffort(managed.localRoot, "update");
1214
- }
670
+ else {
671
+ cleanupDirectoryBestEffort(managed.localRoot, "update");
1215
672
  }
1216
- const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
1217
- const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
1218
- return {
1219
- index,
1220
- item: {
1221
- id: managed.installId,
1222
- source: managed.source,
1223
- ref: managed.ref,
1224
- previous: {
1225
- resolvedVersion: managed.resolvedVersion,
1226
- resolvedRevision: managed.resolvedRevision,
1227
- cacheDir: managed.localRoot,
1228
- },
1229
- installed: { ...installedEntry, extractedDir: synced.extractedDir },
1230
- changed: {
1231
- version: versionChanged,
1232
- revision: revisionChanged,
1233
- any: versionChanged || revisionChanged,
1234
- },
673
+ }
674
+ const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
675
+ const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
676
+ return {
677
+ index,
678
+ item: {
679
+ id: managed.installId,
680
+ source: managed.source,
681
+ ref: managed.ref,
682
+ previous: {
683
+ resolvedVersion: managed.resolvedVersion,
684
+ resolvedRevision: managed.resolvedRevision,
685
+ cacheDir: managed.localRoot,
1235
686
  },
1236
- };
1237
- });
687
+ installed: { ...installedEntry, extractedDir: synced.extractedDir },
688
+ changed: {
689
+ version: versionChanged,
690
+ revision: revisionChanged,
691
+ any: versionChanged || revisionChanged,
692
+ },
693
+ },
694
+ };
1238
695
  }
1239
696
  finally {
1240
- prepared.cleanup();
697
+ staged.cleanup();
1241
698
  }
1242
699
  }
1243
- function generationHash(value) {
1244
- return sha256Hex(JSON.stringify(value));
1245
- }
1246
700
  function referencesRoot(target, config, locks) {
1247
701
  const resolvedTarget = path.resolve(target);
1248
702
  const locksById = new Map(locks.map((entry) => [entry.id, entry]));
@@ -1264,9 +718,6 @@ function isRootAtOrBelow(candidate, root) {
1264
718
  const lexicallyWithin = relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
1265
719
  return lexicallyWithin || isWithin(candidate, root);
1266
720
  }
1267
- function concurrentGenerationError(managed) {
1268
- return new ConfigError(`Managed source "${managed.installId}" changed concurrently; retained both materialized roots and deleted nothing.`, "INVALID_CONFIG_FILE", "Inspect `akm bundle list` and retry the update after the concurrent source operation completes.");
1269
- }
1270
721
  // ── akmUpdate dispatcher ─────────────────────────────────────────────────────
1271
722
  export async function akmUpdate(input) {
1272
723
  const stashDir = input?.stashDir ?? resolveStashDir();
@@ -1310,7 +761,7 @@ export async function akmUpdate(input) {
1310
761
  return false;
1311
762
  });
1312
763
  if (gitMatch)
1313
- return updateGitSource(stashDir, target, all, gitMatch, allowDangerousEnvKeys);
764
+ return updateGitSource(config, stashDir, target, all, gitMatch, allowDangerousEnvKeys);
1314
765
  const websiteMatch = stashes.find((s) => {
1315
766
  if (s.type !== "website")
1316
767
  return false;
@@ -1323,7 +774,7 @@ export async function akmUpdate(input) {
1323
774
  return false;
1324
775
  });
1325
776
  if (websiteMatch)
1326
- return updateWebsiteSource(stashDir, target, all, websiteMatch, allowDangerousEnvKeys);
777
+ return updateWebsiteSource(stashDir, target, all, websiteMatch);
1327
778
  // Plain npm source (bundle without a lock) — sync via the registry
1328
779
  // pipeline and promote to a managed install (see
1329
780
  // managedInstallViewOfPlainNpm's doc comment for why npm can't stay
@@ -1338,7 +789,7 @@ export async function akmUpdate(input) {
1338
789
  return false;
1339
790
  });
1340
791
  if (npmMatch) {
1341
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
792
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
1342
793
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
1343
794
  }
1344
795
  const filesystemMatch = stashes.find((source) => {
@@ -1374,10 +825,10 @@ export async function akmUpdate(input) {
1374
825
  // `--all` must account for EVERY configured source, not only the
1375
826
  // registry-managed (lock-backed) ones (R-015) — `selectManagedTargets`
1376
827
  // above returns only `installs` for `all`, so plain sources were
1377
- // previously never even looked at. Git/npm plain sources are synced here
1378
- // too (git and website through staged promotion; npm promoted to managed,
1379
- // same as the single-target path above). Filesystem sources reflect their
1380
- // files in place and are reported as intentionally skipped.
828
+ // previously never even looked at. Git/website/npm plain sources are synced
829
+ // here too (npm is promoted to managed, same as the single-target path
830
+ // above). Filesystem sources reflect their files in place and are reported
831
+ // as intentionally skipped.
1381
832
  if (all) {
1382
833
  const managedKeys = new Set(managedInstalls.map((m) => m.bundleKey));
1383
834
  const plainSources = getSources(config).filter((source) => source.enabled !== false && !managedKeys.has(source.name ?? ""));
@@ -1386,17 +837,17 @@ export async function akmUpdate(input) {
1386
837
  const id = plain.name ?? plain.path ?? plain.url ?? "";
1387
838
  try {
1388
839
  if (plain.type === "git") {
1389
- const updated = await syncGitPlainSource(plain, stashDir, allowDangerousEnvKeys);
840
+ const updated = await syncGitPlainSource(config, plain, stashDir, allowDangerousEnvKeys);
1390
841
  plainSynced.push(updated.item);
1391
842
  latestIndex = updated.index;
1392
843
  }
1393
844
  else if (plain.type === "npm") {
1394
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowDangerousEnvKeys);
845
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, yes, stashDir, allowDangerousEnvKeys);
1395
846
  processed.push(updated.item);
1396
847
  latestIndex = updated.index;
1397
848
  }
1398
849
  else if (plain.type === "website") {
1399
- const updated = await syncWebsitePlainSource(plain, stashDir, allowDangerousEnvKeys);
850
+ const updated = await syncWebsitePlainSource(plain, stashDir);
1400
851
  plainSynced.push(updated.item);
1401
852
  latestIndex = updated.index;
1402
853
  }