agentera 3.0.0-dev.39 → 3.0.0-dev.41

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 (428) hide show
  1. package/README.md +1 -1
  2. package/bundle/CHANGELOG.md +55 -8
  3. package/bundle/README.md +3 -3
  4. package/bundle/references/artifacts/glossary-entry-contract.yaml +18 -5
  5. package/bundle/references/artifacts/state-storage-authority.yaml +865 -171
  6. package/bundle/references/cli/agent-ready-state-contract.yaml +12 -12
  7. package/bundle/references/cli/audience-namespace-cli-migration.yaml +5 -5
  8. package/bundle/references/cli/prime-consumer-compatibility.yaml +58 -18
  9. package/bundle/references/cli/vocabulary-index.yaml +1 -1
  10. package/bundle/references/cli/vocabulary.md +4 -3
  11. package/bundle/skills/agentera/SKILL.md +23 -4
  12. package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +3 -3
  13. package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +3 -1
  14. package/bundle/skills/agentera/capabilities/discuss/schemas/artifacts.yaml +2 -1
  15. package/bundle/skills/agentera/capabilities/discuss/schemas/validation.yaml +3 -1
  16. package/bundle/skills/agentera/capabilities/plan/schemas/artifacts.yaml +2 -2
  17. package/bundle/skills/agentera/capabilities/plan/schemas/validation.yaml +3 -1
  18. package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +3 -1
  19. package/bundle/skills/agentera/capabilities/status/schemas/artifacts.yaml +2 -1
  20. package/bundle/skills/agentera/capability_schema_contract.yaml +1 -1
  21. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +89 -30
  22. package/bundle/skills/agentera/schemas/artifacts/todo.yaml +103 -20
  23. package/dist/analytics/extractCorpus/cli.js.map +1 -1
  24. package/dist/analytics/extractCorpus/copilotSessions.js.map +1 -1
  25. package/dist/analytics/extractCorpus/core.js.map +1 -1
  26. package/dist/analytics/extractCorpus/corpus.js.map +1 -1
  27. package/dist/analytics/extractCorpus/coverageAudit.js.map +1 -1
  28. package/dist/analytics/extractCorpus/cursorSessions.js.map +1 -1
  29. package/dist/analytics/extractCorpus/evidenceTiers.js.map +1 -1
  30. package/dist/analytics/extractCorpus/extractCorpusParity.js.map +1 -1
  31. package/dist/analytics/extractCorpus/filesystemSources.js.map +1 -1
  32. package/dist/analytics/extractCorpus/index.js.map +1 -1
  33. package/dist/analytics/extractCorpus/jsonlSessions.js.map +1 -1
  34. package/dist/analytics/extractCorpus/sqliteCaps.js.map +1 -1
  35. package/dist/analytics/extractCorpus/sqliteSessions.js.map +1 -1
  36. package/dist/analytics/extractCorpus/tierReader.js.map +1 -1
  37. package/dist/analytics/extractCorpus.js.map +1 -1
  38. package/dist/analytics/glossaryAdviceResolution.js.map +1 -1
  39. package/dist/analytics/glossaryInputAcquisition.js.map +1 -1
  40. package/dist/analytics/personalGlossaryAdmission.js.map +1 -1
  41. package/dist/analytics/personalGlossaryProfile.js +2 -1
  42. package/dist/analytics/personalGlossaryProfile.js.map +1 -1
  43. package/dist/analytics/profileSignals.js.map +1 -1
  44. package/dist/analytics/usageStats.js.map +1 -1
  45. package/dist/audit/terminologyDrift.js.map +1 -1
  46. package/dist/bin/agentera.js.map +1 -1
  47. package/dist/capabilities/audit/instructions.js.map +1 -1
  48. package/dist/capabilities/build/instructions.js +2 -2
  49. package/dist/capabilities/build/instructions.js.map +1 -1
  50. package/dist/capabilities/design/instructions.js.map +1 -1
  51. package/dist/capabilities/discuss/instructions.js +2 -1
  52. package/dist/capabilities/discuss/instructions.js.map +1 -1
  53. package/dist/capabilities/document/instructions.js.map +1 -1
  54. package/dist/capabilities/index.js +1 -1
  55. package/dist/capabilities/index.js.map +1 -1
  56. package/dist/capabilities/optimize/instructions.js +1 -0
  57. package/dist/capabilities/optimize/instructions.js.map +1 -1
  58. package/dist/capabilities/orchestrate/instructions.js.map +1 -1
  59. package/dist/capabilities/plan/instructions.js +3 -2
  60. package/dist/capabilities/plan/instructions.js.map +1 -1
  61. package/dist/capabilities/profile/instructions.js.map +1 -1
  62. package/dist/capabilities/research/instructions.js.map +1 -1
  63. package/dist/capabilities/status/instructions.js +8 -1
  64. package/dist/capabilities/status/instructions.js.map +1 -1
  65. package/dist/capabilities/status/startupInstructions.js.map +1 -1
  66. package/dist/capabilities/vision/instructions.js.map +1 -1
  67. package/dist/cli/appContext.js.map +1 -1
  68. package/dist/cli/argvalidate.js.map +1 -1
  69. package/dist/cli/capabilityContext/benchmark.js +2 -1
  70. package/dist/cli/capabilityContext/benchmark.js.map +1 -1
  71. package/dist/cli/capabilityContext/bespoke.js.map +1 -1
  72. package/dist/cli/capabilityContext/build.js +6 -6
  73. package/dist/cli/capabilityContext/build.js.map +1 -1
  74. package/dist/cli/capabilityContext/closeout.js +11 -7
  75. package/dist/cli/capabilityContext/closeout.js.map +1 -1
  76. package/dist/cli/capabilityContext/contract.js +4 -4
  77. package/dist/cli/capabilityContext/contract.js.map +1 -1
  78. package/dist/cli/capabilityContext/evidence.js +4 -4
  79. package/dist/cli/capabilityContext/evidence.js.map +1 -1
  80. package/dist/cli/capabilityContext/index.js.map +1 -1
  81. package/dist/cli/capabilityContext/orchestration.js +12 -7
  82. package/dist/cli/capabilityContext/orchestration.js.map +1 -1
  83. package/dist/cli/capabilityContext/planState.js.map +1 -1
  84. package/dist/cli/capabilityContext/progress.js.map +1 -1
  85. package/dist/cli/capabilityContext/projectVerification.js.map +1 -1
  86. package/dist/cli/capabilityContext/shared.js +2 -2
  87. package/dist/cli/capabilityContext/shared.js.map +1 -1
  88. package/dist/cli/capabilityContext/slim.js.map +1 -1
  89. package/dist/cli/capabilityContext/startup.js +6 -2
  90. package/dist/cli/capabilityContext/startup.js.map +1 -1
  91. package/dist/cli/capabilityContext/types.js +19 -14
  92. package/dist/cli/capabilityContext/types.js.map +1 -1
  93. package/dist/cli/capabilityContext.js.map +1 -1
  94. package/dist/cli/commands/appHome.js.map +1 -1
  95. package/dist/cli/commands/capability.js.map +1 -1
  96. package/dist/cli/commands/compact.js.map +1 -1
  97. package/dist/cli/commands/doctor.js.map +1 -1
  98. package/dist/cli/commands/durability.js.map +1 -1
  99. package/dist/cli/commands/entityMigrate.js.map +1 -1
  100. package/dist/cli/commands/glossaryAdvice.js.map +1 -1
  101. package/dist/cli/commands/lint.js.map +1 -1
  102. package/dist/cli/commands/personalGlossary.js.map +1 -1
  103. package/dist/cli/commands/prime/briefOrientation.js +260 -44
  104. package/dist/cli/commands/prime/briefOrientation.js.map +1 -1
  105. package/dist/cli/commands/prime/buildExecutionRequest.js.map +1 -1
  106. package/dist/cli/commands/prime/bundleStatus.js.map +1 -1
  107. package/dist/cli/commands/prime/collectEntityOrientation.js +22 -13
  108. package/dist/cli/commands/prime/collectEntityOrientation.js.map +1 -1
  109. package/dist/cli/commands/prime/collectOrientationState.js +20 -28
  110. package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
  111. package/dist/cli/commands/prime/orientationOutput.js +4 -5
  112. package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
  113. package/dist/cli/commands/prime/types.js.map +1 -1
  114. package/dist/cli/commands/prime/v1Migration.js.map +1 -1
  115. package/dist/cli/commands/prime.js +1 -1
  116. package/dist/cli/commands/prime.js.map +1 -1
  117. package/dist/cli/commands/profileGrounding.js +29 -50
  118. package/dist/cli/commands/profileGrounding.js.map +1 -1
  119. package/dist/cli/commands/query.js.map +1 -1
  120. package/dist/cli/commands/report.js.map +1 -1
  121. package/dist/cli/commands/route.js.map +1 -1
  122. package/dist/cli/commands/schema.js +4 -43
  123. package/dist/cli/commands/schema.js.map +1 -1
  124. package/dist/cli/commands/state/decisions.js +1 -1
  125. package/dist/cli/commands/state/decisions.js.map +1 -1
  126. package/dist/cli/commands/state/docs.js.map +1 -1
  127. package/dist/cli/commands/state/experimentRecords.js +28 -15
  128. package/dist/cli/commands/state/experimentRecords.js.map +1 -1
  129. package/dist/cli/commands/state/experiments.js.map +1 -1
  130. package/dist/cli/commands/state/get.js +6 -3
  131. package/dist/cli/commands/state/get.js.map +1 -1
  132. package/dist/cli/commands/state/health.js.map +1 -1
  133. package/dist/cli/commands/state/index.js +3 -46
  134. package/dist/cli/commands/state/index.js.map +1 -1
  135. package/dist/cli/commands/state/list.js +41 -22
  136. package/dist/cli/commands/state/list.js.map +1 -1
  137. package/dist/cli/commands/state/objective.js.map +1 -1
  138. package/dist/cli/commands/state/plan.js.map +1 -1
  139. package/dist/cli/commands/state/planTasks.js +26 -13
  140. package/dist/cli/commands/state/planTasks.js.map +1 -1
  141. package/dist/cli/commands/state/plans.js +31 -19
  142. package/dist/cli/commands/state/plans.js.map +1 -1
  143. package/dist/cli/commands/state/progress.js.map +1 -1
  144. package/dist/cli/commands/state/shared.js.map +1 -1
  145. package/dist/cli/commands/state/todo.js +2 -1
  146. package/dist/cli/commands/state/todo.js.map +1 -1
  147. package/dist/cli/commands/state/write.js +52 -48
  148. package/dist/cli/commands/state/write.js.map +1 -1
  149. package/dist/cli/commands/upgrade.js.map +1 -1
  150. package/dist/cli/commands/upgradeVerify.js.map +1 -1
  151. package/dist/cli/commands/validate.js +44 -4
  152. package/dist/cli/commands/validate.js.map +1 -1
  153. package/dist/cli/commands/verify.js.map +1 -1
  154. package/dist/cli/contracts/bundleStatus.js.map +1 -1
  155. package/dist/cli/contracts/orientationState.js.map +1 -1
  156. package/dist/cli/dispatch/argvParser.js.map +1 -1
  157. package/dist/cli/dispatch/check.js.map +1 -1
  158. package/dist/cli/dispatch/commands.js.map +1 -1
  159. package/dist/cli/dispatch/index.js +12 -4
  160. package/dist/cli/dispatch/index.js.map +1 -1
  161. package/dist/cli/dispatch/lifecycle.js.map +1 -1
  162. package/dist/cli/dispatch/prime.js.map +1 -1
  163. package/dist/cli/dispatch/shared.js.map +1 -1
  164. package/dist/cli/dispatch/state.js +53 -86
  165. package/dist/cli/dispatch/state.js.map +1 -1
  166. package/dist/cli/dispatch.js.map +1 -1
  167. package/dist/cli/errors.js.map +1 -1
  168. package/dist/cli/help.js +134 -48
  169. package/dist/cli/help.js.map +1 -1
  170. package/dist/cli/migrationRequired.js.map +1 -1
  171. package/dist/cli/orientation/attention.js +8 -5
  172. package/dist/cli/orientation/attention.js.map +1 -1
  173. package/dist/cli/orientation/corpusCoverage.js.map +1 -1
  174. package/dist/cli/orientation.js +61 -38
  175. package/dist/cli/orientation.js.map +1 -1
  176. package/dist/cli/planArtifacts.js.map +1 -1
  177. package/dist/cli/planEvidence.js.map +1 -1
  178. package/dist/cli/planLifecycleState.js +3 -1
  179. package/dist/cli/planLifecycleState.js.map +1 -1
  180. package/dist/cli/planTaskIndex.js.map +1 -1
  181. package/dist/cli/prime-blob.js +2 -1
  182. package/dist/cli/prime-blob.js.map +1 -1
  183. package/dist/cli/profileAcquisition.js +168 -0
  184. package/dist/cli/profileAcquisition.js.map +1 -0
  185. package/dist/cli/startupCompletenessContract.js +5 -2
  186. package/dist/cli/startupCompletenessContract.js.map +1 -1
  187. package/dist/cli/stateQuery.js.map +1 -1
  188. package/dist/cli/structured.js.map +1 -1
  189. package/dist/cli/todoMarkdown.js +25 -11
  190. package/dist/cli/todoMarkdown.js.map +1 -1
  191. package/dist/cli/todoReadinessSelection.js +52 -17
  192. package/dist/cli/todoReadinessSelection.js.map +1 -1
  193. package/dist/cli/todoSeverity.js.map +1 -1
  194. package/dist/core/atomicWriter.js.map +1 -1
  195. package/dist/core/difflib.js.map +1 -1
  196. package/dist/core/envPaths.js.map +1 -1
  197. package/dist/core/jsonValue.js.map +1 -1
  198. package/dist/core/paths.js.map +1 -1
  199. package/dist/core/pyjson.js.map +1 -1
  200. package/dist/core/shell.js +5 -0
  201. package/dist/core/shell.js.map +1 -0
  202. package/dist/core/skillIdentity.js.map +1 -1
  203. package/dist/core/sourceRoot.js.map +1 -1
  204. package/dist/core/text.js.map +1 -1
  205. package/dist/core/toml.js.map +1 -1
  206. package/dist/core/yaml.js.map +1 -1
  207. package/dist/eval/evalSkills.js.map +1 -1
  208. package/dist/eval/hybridRouteEvaluation.js.map +1 -1
  209. package/dist/eval/semanticEval.js.map +1 -1
  210. package/dist/eval/semanticFixtures.js.map +1 -1
  211. package/dist/hooks/common.js.map +1 -1
  212. package/dist/hooks/compaction/apply.js.map +1 -1
  213. package/dist/hooks/compaction/dryRun.js.map +1 -1
  214. package/dist/hooks/compaction/index.js.map +1 -1
  215. package/dist/hooks/compaction/parse.js.map +1 -1
  216. package/dist/hooks/compaction/retention.js.map +1 -1
  217. package/dist/hooks/compaction/status.js.map +1 -1
  218. package/dist/hooks/compaction/types.js.map +1 -1
  219. package/dist/hooks/cursorPreToolUse.js.map +1 -1
  220. package/dist/hooks/cursorSessionStart.js.map +1 -1
  221. package/dist/hooks/projectHookInput.js.map +1 -1
  222. package/dist/hooks/sessionStart.js.map +1 -1
  223. package/dist/hooks/sessionStop.js.map +1 -1
  224. package/dist/hooks/validateArtifact/agentFacing.js.map +1 -1
  225. package/dist/hooks/validateArtifact/index.js.map +1 -1
  226. package/dist/hooks/validateArtifact/markdown.js.map +1 -1
  227. package/dist/hooks/validateArtifact/runtime.js.map +1 -1
  228. package/dist/hooks/validateArtifact/schema.js +6 -0
  229. package/dist/hooks/validateArtifact/schema.js.map +1 -1
  230. package/dist/hooks/validateArtifact/traversal.js.map +1 -1
  231. package/dist/hooks/validateArtifact/violations.js.map +1 -1
  232. package/dist/migrate/v2HandoffManifest.js.map +1 -1
  233. package/dist/registries/artifactProtocolIds.js.map +1 -1
  234. package/dist/registries/artifactRegistry.js.map +1 -1
  235. package/dist/registries/artifactSchemaProjection.js +46 -0
  236. package/dist/registries/artifactSchemaProjection.js.map +1 -0
  237. package/dist/registries/capabilityContract.js.map +1 -1
  238. package/dist/registries/evaluatorHandoffContract.js.map +1 -1
  239. package/dist/registries/evidenceTierContract.js.map +1 -1
  240. package/dist/registries/glossaryAdviceContract.js.map +1 -1
  241. package/dist/registries/glossaryCaveatContract.js.map +1 -1
  242. package/dist/registries/glossaryConsumerContractValidation.js +1 -5
  243. package/dist/registries/glossaryConsumerContractValidation.js.map +1 -1
  244. package/dist/registries/glossaryEntryContract.js +10 -0
  245. package/dist/registries/glossaryEntryContract.js.map +1 -1
  246. package/dist/registries/glossaryTermIdentity.js.map +1 -1
  247. package/dist/registries/glossaryTermOccurrence.js.map +1 -1
  248. package/dist/registries/hybridRoute.js.map +1 -1
  249. package/dist/registries/hybridRouteReceipt.js.map +1 -1
  250. package/dist/registries/packageRegistry.js.map +1 -1
  251. package/dist/registries/todoReadinessContract.js +49 -6
  252. package/dist/registries/todoReadinessContract.js.map +1 -1
  253. package/dist/registries/triggerLoader.js.map +1 -1
  254. package/dist/registries/verbosityBudgetContract.js.map +1 -1
  255. package/dist/release/releaseMetadata.js.map +1 -1
  256. package/dist/runtime/lifecycleAdapterContract.js.map +1 -1
  257. package/dist/runtime/lifecycleAdapters.js.map +1 -1
  258. package/dist/runtime/lifecycleAuthority.js.map +1 -1
  259. package/dist/runtime/lifecycleOperationContract.js.map +1 -1
  260. package/dist/runtime/lifecycleOperations.js.map +1 -1
  261. package/dist/runtime/lifecycleOwnershipJournal.js.map +1 -1
  262. package/dist/runtime/lifecyclePublication.js.map +1 -1
  263. package/dist/runtime/lifecycleSnapshot.js.map +1 -1
  264. package/dist/runtime/nativeResourceCleanup.js.map +1 -1
  265. package/dist/setup/codex/cli.js.map +1 -1
  266. package/dist/setup/codex/configToml.js.map +1 -1
  267. package/dist/setup/codex/constants.js.map +1 -1
  268. package/dist/setup/codex/installRoot.js.map +1 -1
  269. package/dist/setup/codex/state.js.map +1 -1
  270. package/dist/setup/codex.js.map +1 -1
  271. package/dist/setup/opencode.js.map +1 -1
  272. package/dist/setup/opencodeConstants.js.map +1 -1
  273. package/dist/setup/sharedSkill.js.map +1 -1
  274. package/dist/setup/smokeChecks.js.map +1 -1
  275. package/dist/state/archiveDiscovery.js +8 -5
  276. package/dist/state/archiveDiscovery.js.map +1 -1
  277. package/dist/state/archivePublication.js.map +1 -1
  278. package/dist/state/archiveRecovery.js.map +1 -1
  279. package/dist/state/archiveReplay.js.map +1 -1
  280. package/dist/state/canonicalMigrationRecord.js.map +1 -1
  281. package/dist/state/changelog.js.map +1 -1
  282. package/dist/state/decisionEntities.js +59 -20
  283. package/dist/state/decisionEntities.js.map +1 -1
  284. package/dist/state/decisionLegacyValidation.js.map +1 -1
  285. package/dist/state/decisionMigrationProvenance.js.map +1 -1
  286. package/dist/state/decisionOverlay.js.map +1 -1
  287. package/dist/state/decisionRevision.js.map +1 -1
  288. package/dist/state/decisionRevisionMigration.js.map +1 -1
  289. package/dist/state/directRetrieval.js.map +1 -1
  290. package/dist/state/durability.js.map +1 -1
  291. package/dist/state/entityCutover.js.map +1 -1
  292. package/dist/state/entityCutoverGit.js.map +1 -1
  293. package/dist/state/entityGlossaryAuthority.js.map +1 -1
  294. package/dist/state/entityListProjection.js +188 -0
  295. package/dist/state/entityListProjection.js.map +1 -0
  296. package/dist/state/entityListRuntimeRegistry.js +50 -0
  297. package/dist/state/entityListRuntimeRegistry.js.map +1 -0
  298. package/dist/state/entityMigrationCausality.js.map +1 -1
  299. package/dist/state/entityMigrationIdentity.js.map +1 -1
  300. package/dist/state/entityMigrationPreview.js +32 -3
  301. package/dist/state/entityMigrationPreview.js.map +1 -1
  302. package/dist/state/entityPublicationContext.js +607 -765
  303. package/dist/state/entityPublicationContext.js.map +1 -1
  304. package/dist/state/entityRetrievalHelp.js +476 -0
  305. package/dist/state/entityRetrievalHelp.js.map +1 -0
  306. package/dist/state/entityStorage.js.map +1 -1
  307. package/dist/state/exactReplacementRecovery.js +19 -0
  308. package/dist/state/exactReplacementRecovery.js.map +1 -0
  309. package/dist/state/experimentArchive.js.map +1 -1
  310. package/dist/state/experimentIdentity.js.map +1 -1
  311. package/dist/state/healthEntities.js +15 -16
  312. package/dist/state/healthEntities.js.map +1 -1
  313. package/dist/state/healthEntityValidation.js.map +1 -1
  314. package/dist/state/healthRepair.js.map +1 -1
  315. package/dist/state/installRoot.js.map +1 -1
  316. package/dist/state/legacyEntityCutoverEvidence.js.map +1 -1
  317. package/dist/state/legacyIdentity.js.map +1 -1
  318. package/dist/state/legacySummaryRecord.js.map +1 -1
  319. package/dist/state/listAccounting.js.map +1 -1
  320. package/dist/state/listClassification.js.map +1 -1
  321. package/dist/state/listCursor.js.map +1 -1
  322. package/dist/state/listRetrieval.js.map +1 -1
  323. package/dist/state/migrationSourceBinding.js.map +1 -1
  324. package/dist/state/objectiveExperimentEntities.js +22 -22
  325. package/dist/state/objectiveExperimentEntities.js.map +1 -1
  326. package/dist/state/planEntities.js +75 -29
  327. package/dist/state/planEntities.js.map +1 -1
  328. package/dist/state/planIdentity.js +4 -1
  329. package/dist/state/planIdentity.js.map +1 -1
  330. package/dist/state/planRetrieval.js.map +1 -1
  331. package/dist/state/planTaskRetrieval.js +41 -31
  332. package/dist/state/planTaskRetrieval.js.map +1 -1
  333. package/dist/state/progressEntities.js +40 -25
  334. package/dist/state/progressEntities.js.map +1 -1
  335. package/dist/state/progressGlossaryCaveat.js.map +1 -1
  336. package/dist/state/progressPublicationOrder.js.map +1 -1
  337. package/dist/state/projectRoot.js.map +1 -1
  338. package/dist/state/projectionPolicy.js +12 -5
  339. package/dist/state/projectionPolicy.js.map +1 -1
  340. package/dist/state/retrievalAuthority.js +86 -272
  341. package/dist/state/retrievalAuthority.js.map +1 -1
  342. package/dist/state/safeProjectFile.js.map +1 -1
  343. package/dist/state/startupAnalysis/benchmark.js.map +1 -1
  344. package/dist/state/startupAnalysis/contract.js.map +1 -1
  345. package/dist/state/startupAnalysis/helpers.js.map +1 -1
  346. package/dist/state/startupAnalysis/index.js.map +1 -1
  347. package/dist/state/startupAnalysis/metrics.js.map +1 -1
  348. package/dist/state/startupAnalysis/records.js.map +1 -1
  349. package/dist/state/startupAnalysis/report.js.map +1 -1
  350. package/dist/state/startupAnalysis/threshold.js.map +1 -1
  351. package/dist/state/startupAnalysis.js.map +1 -1
  352. package/dist/state/startupProjection.js.map +1 -1
  353. package/dist/state/stateMode.js +11 -8
  354. package/dist/state/stateMode.js.map +1 -1
  355. package/dist/state/stateStorageAuthority.js.map +1 -1
  356. package/dist/state/summaryEntityRead.js.map +1 -1
  357. package/dist/state/summaryMigrationProvenance.js.map +1 -1
  358. package/dist/state/summarySourceRowAuthority.js.map +1 -1
  359. package/dist/state/todoDocsEntities.js +811 -91
  360. package/dist/state/todoDocsEntities.js.map +1 -1
  361. package/dist/state/todoDocsEntityValidation.js +127 -2
  362. package/dist/state/todoDocsEntityValidation.js.map +1 -1
  363. package/dist/state/todoReconciliationActivation.js +63 -0
  364. package/dist/state/todoReconciliationActivation.js.map +1 -0
  365. package/dist/state/todoReconciliationTransaction.js +554 -0
  366. package/dist/state/todoReconciliationTransaction.js.map +1 -0
  367. package/dist/state/write/assign.js.map +1 -1
  368. package/dist/state/write/candidateMutation.js +1 -12
  369. package/dist/state/write/candidateMutation.js.map +1 -1
  370. package/dist/state/write/errors.js.map +1 -1
  371. package/dist/state/write/experimentPublication.js.map +1 -1
  372. package/dist/state/write/explain.js +198 -177
  373. package/dist/state/write/explain.js.map +1 -1
  374. package/dist/state/write/fields.js.map +1 -1
  375. package/dist/state/write/glossaryPublication.js.map +1 -1
  376. package/dist/state/write/grammar.js +271 -0
  377. package/dist/state/write/grammar.js.map +1 -0
  378. package/dist/state/write/helpers.js.map +1 -1
  379. package/dist/state/write/index.js.map +1 -1
  380. package/dist/state/write/input.js +351 -5
  381. package/dist/state/write/input.js.map +1 -1
  382. package/dist/state/write/lock.js.map +1 -1
  383. package/dist/state/write/mutation.js.map +1 -1
  384. package/dist/state/write/operations.js +140 -343
  385. package/dist/state/write/operations.js.map +1 -1
  386. package/dist/state/write/planEvaluation.js +4 -0
  387. package/dist/state/write/planEvaluation.js.map +1 -1
  388. package/dist/state/write/planPublication.js +48 -3
  389. package/dist/state/write/planPublication.js.map +1 -1
  390. package/dist/state/write/runtimeOperations.js +65 -0
  391. package/dist/state/write/runtimeOperations.js.map +1 -0
  392. package/dist/state/write/serialize.js.map +1 -1
  393. package/dist/state/write/transaction.js +13 -7
  394. package/dist/state/write/transaction.js.map +1 -1
  395. package/dist/state/write/validate.js.map +1 -1
  396. package/dist/upgrade/appContentRefresh.js.map +1 -1
  397. package/dist/upgrade/appModel.js.map +1 -1
  398. package/dist/upgrade/atomicWriter.js.map +1 -1
  399. package/dist/upgrade/bundleEvidence.js.map +1 -1
  400. package/dist/upgrade/channels.js.map +1 -1
  401. package/dist/upgrade/coexistenceProbe.js.map +1 -1
  402. package/dist/upgrade/compatibility.js.map +1 -1
  403. package/dist/upgrade/doctor.js.map +1 -1
  404. package/dist/upgrade/doctorClassifier.js.map +1 -1
  405. package/dist/upgrade/installedHooksRetirement.js.map +1 -1
  406. package/dist/upgrade/legacyAgentCleanup.js.map +1 -1
  407. package/dist/upgrade/lifecycleUpgrade.js.map +1 -1
  408. package/dist/upgrade/migrateArtifactsV2ToV3.js.map +1 -1
  409. package/dist/upgrade/migrationPublication.js.map +1 -1
  410. package/dist/upgrade/nextMajorDoctor.js.map +1 -1
  411. package/dist/upgrade/npxPlatformStatus.js.map +1 -1
  412. package/dist/upgrade/projectIntegration.js.map +1 -1
  413. package/dist/upgrade/projectIntegrationDecision.js.map +1 -1
  414. package/dist/upgrade/runtimeMigration.js.map +1 -1
  415. package/dist/upgrade/upgradeCommands.js.map +1 -1
  416. package/dist/upgrade/upgradeLock.js.map +1 -1
  417. package/dist/upgrade/upgradeOrchestrator.js.map +1 -1
  418. package/dist/upgrade/v3CapabilitySurface.js.map +1 -1
  419. package/dist/upgrade/versionResolution.js.map +1 -1
  420. package/dist/validate/appHomeContract.js.map +1 -1
  421. package/dist/validate/capability.js.map +1 -1
  422. package/dist/validate/crossCapability.js.map +1 -1
  423. package/dist/validate/selfAudit.js.map +1 -1
  424. package/dist/validate/v1LegacyCruft.js.map +1 -1
  425. package/dist/validate/vocabularyAuthority.js.map +1 -1
  426. package/package.json +2 -2
  427. package/dist/state/experimentRetrieval.js +0 -447
  428. package/dist/state/experimentRetrieval.js.map +0 -1
@@ -1,128 +1,241 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { ExactReplacementConflictError, FileReplacementError } from "./exactReplacementRecovery.js";
4
5
  import { assertValidatedProjectRoot } from "./projectRoot.js";
5
- import { reject } from "./write/errors.js";
6
- const DIRECTORY_FLAGS = fs.constants.O_RDONLY
7
- | (fs.constants.O_DIRECTORY ?? 0)
8
- | (fs.constants.O_NOFOLLOW ?? 0);
9
- const FILE_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
6
+ import { reject, StateWriteInputError } from "./write/errors.js";
10
7
  const RECOVERY_IGNORE_BYTES = "*\n!.gitignore\n";
11
- function samePublished(left, right) {
12
- return left.dev === right.dev
13
- && left.ino === right.ino
14
- && left.type === right.type
15
- && left.size === right.size
16
- && left.sha256 === right.sha256;
17
- }
18
- function fdPath(fd, name) {
19
- return name ? `/proc/self/fd/${fd}/${name}` : `/proc/self/fd/${fd}`;
20
- }
8
+ export const FILE_REPLACEMENT_RECOVERY_VERSION = "agentera.fileReplacementRecovery.v1";
9
+ export const FILE_REPLACEMENT_METADATA_NAME = "replacement.json";
21
10
  function identity(stat) {
22
11
  return {
23
12
  dev: stat.dev,
24
13
  ino: stat.ino,
14
+ type: stat.mode & BigInt(fs.constants.S_IFMT),
25
15
  size: stat.size,
26
16
  mtimeNs: stat.mtimeNs,
27
17
  ctimeNs: stat.ctimeNs,
28
18
  nlink: stat.nlink,
19
+ mode: stat.mode,
29
20
  };
30
21
  }
31
- function sameObject(left, right) {
32
- return left.dev === right.dev && left.ino === right.ino;
33
- }
34
22
  function sameIdentity(left, right) {
35
23
  return left.dev === right.dev
36
24
  && left.ino === right.ino
25
+ && left.type === right.type
37
26
  && left.size === right.size
38
27
  && left.mtimeNs === right.mtimeNs
39
28
  && left.ctimeNs === right.ctimeNs
40
29
  && left.nlink === right.nlink;
41
30
  }
42
- function readDescriptor(fd) {
43
- const stat = fs.fstatSync(fd, { bigint: true });
44
- if (!stat.isFile() || stat.size > BigInt(Number.MAX_SAFE_INTEGER)) {
45
- throw new Error("state mode marker descriptor is not a readable regular file");
46
- }
47
- const bytes = Buffer.alloc(Number(stat.size));
48
- let offset = 0;
49
- while (offset < bytes.length) {
50
- const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
51
- if (count === 0)
52
- break;
53
- offset += count;
54
- }
55
- if (offset !== bytes.length)
56
- throw new Error("state mode marker changed while its exact bytes were read");
57
- return bytes;
31
+ function sameDirectory(left, right) {
32
+ return right.isDirectory()
33
+ && !right.isSymbolicLink()
34
+ && left.dev === right.dev
35
+ && left.ino === right.ino;
58
36
  }
59
- function publishedIdentity(fd) {
60
- const stat = fs.fstatSync(fd, { bigint: true });
61
- const bytes = readDescriptor(fd);
37
+ function sha256(bytes) {
38
+ return createHash("sha256").update(bytes).digest("hex");
39
+ }
40
+ function publishedIdentity(file) {
62
41
  return {
63
- dev: stat.dev,
64
- ino: stat.ino,
65
- type: stat.mode & BigInt(fs.constants.S_IFMT),
66
- size: stat.size,
67
- sha256: createHash("sha256").update(bytes).digest("hex"),
42
+ dev: file.identity.dev,
43
+ ino: file.identity.ino,
44
+ type: file.identity.type,
45
+ size: file.identity.size,
46
+ sha256: sha256(file.bytes),
68
47
  };
69
48
  }
70
- function matchesPublished(fd, expected) {
71
- const stat = fs.fstatSync(fd, { bigint: true });
72
- if (stat.dev !== expected.dev
73
- || stat.ino !== expected.ino
74
- || (stat.mode & BigInt(fs.constants.S_IFMT)) !== expected.type
75
- || stat.size !== expected.size)
76
- return false;
77
- return createHash("sha256").update(readDescriptor(fd)).digest("hex") === expected.sha256;
49
+ function matchesPublished(file, expected) {
50
+ const observed = publishedIdentity(file);
51
+ return observed.dev === expected.dev
52
+ && observed.ino === expected.ino
53
+ && observed.type === expected.type
54
+ && observed.size === expected.size
55
+ && observed.sha256 === expected.sha256;
56
+ }
57
+ function matchesExpected(file, expected) {
58
+ return Buffer.isBuffer(expected) ? file.bytes.equals(expected) : matchesPublished(file, expected);
78
59
  }
79
- function matchesExpected(fd, expected) {
80
- return Buffer.isBuffer(expected) ? readDescriptor(fd).equals(expected) : matchesPublished(fd, expected);
60
+ function sameStableFile(left, right) {
61
+ return sameIdentity(left.identity, right.identity) && left.bytes.equals(right.bytes);
81
62
  }
82
- function openMatches(parentFd, name, expectedFd, flags) {
83
- let observed;
63
+ function readStableFile(absolute, label) {
64
+ let before;
84
65
  try {
85
- observed = fs.openSync(fdPath(parentFd, name), flags);
86
- return sameObject(fs.fstatSync(observed, { bigint: true }), fs.fstatSync(expectedFd, { bigint: true }));
66
+ before = fs.lstatSync(absolute, { bigint: true });
87
67
  }
88
- catch {
89
- return false;
68
+ catch (error) {
69
+ throw new Error(`${label} is unavailable: ${error.message}`, { cause: error });
70
+ }
71
+ if (!before.isFile() || before.isSymbolicLink()) {
72
+ throw new Error(`${label} is not a safe regular file`);
73
+ }
74
+ let descriptor;
75
+ try {
76
+ descriptor = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
77
+ const opened = fs.fstatSync(descriptor, { bigint: true });
78
+ if (!opened.isFile() || opened.isSymbolicLink() || opened.dev !== before.dev || opened.ino !== before.ino) {
79
+ throw new Error(`${label} changed while it was opened`);
80
+ }
81
+ const bytes = fs.readFileSync(descriptor);
82
+ const after = fs.fstatSync(descriptor, { bigint: true });
83
+ const pathname = fs.lstatSync(absolute, { bigint: true });
84
+ const openedIdentity = identity(opened);
85
+ if (!sameIdentity(openedIdentity, identity(after))
86
+ || !sameIdentity(openedIdentity, identity(pathname))
87
+ || BigInt(bytes.length) !== opened.size)
88
+ throw new Error(`${label} changed while its exact bytes were read`);
89
+ return { absolute, identity: identity(after), bytes };
90
90
  }
91
91
  finally {
92
- if (observed !== undefined)
93
- fs.closeSync(observed);
92
+ if (descriptor !== undefined)
93
+ fs.closeSync(descriptor);
94
94
  }
95
95
  }
96
- function entryExists(parentFd, name) {
97
- let fd;
96
+ function readFileIfPresent(absolute, label) {
98
97
  try {
99
- fd = fs.openSync(fdPath(parentFd, name), FILE_FLAGS);
100
- return true;
98
+ fs.lstatSync(absolute);
101
99
  }
102
100
  catch (error) {
103
- return error.code !== "ENOENT";
101
+ if (error.code === "ENOENT")
102
+ return null;
103
+ throw error;
104
+ }
105
+ return readStableFile(absolute, label);
106
+ }
107
+ function readStableDirectory(absolute, created = false) {
108
+ const stat = fs.lstatSync(absolute, { bigint: true });
109
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
110
+ throw new Error(`publication directory '${absolute}' is not a safe real directory`);
111
+ }
112
+ return { absolute, dev: stat.dev, ino: stat.ino, created };
113
+ }
114
+ function projectLocalPath(root, absolute, allowRoot = false) {
115
+ const resolved = path.resolve(absolute);
116
+ const relative = path.relative(root.path, resolved);
117
+ return resolved === absolute
118
+ && (allowRoot || relative.length > 0)
119
+ && relative !== ".."
120
+ && !relative.startsWith(`..${path.sep}`)
121
+ && !path.isAbsolute(relative);
122
+ }
123
+ /** Validate a recovery directory before listing or mutating anything through it. */
124
+ export function validateEntityRecoveryDirectory(root, absolute, targetDirectory, label, expected) {
125
+ assertValidatedProjectRoot(root);
126
+ if (!projectLocalPath(root, absolute) || !projectLocalPath(root, targetDirectory, true)) {
127
+ throw new FileReplacementError(`${label} must be a real project-local directory under the validated project root`);
128
+ }
129
+ let directory;
130
+ let target;
131
+ try {
132
+ directory = readStableDirectory(absolute);
133
+ target = readStableDirectory(targetDirectory);
134
+ if (fs.realpathSync(absolute) !== absolute || fs.realpathSync(targetDirectory) !== targetDirectory) {
135
+ throw new Error("a directory path traverses a symbolic link");
136
+ }
137
+ }
138
+ catch (error) {
139
+ throw new FileReplacementError(`${label} must be a real project-local non-symlink directory: ${error.message}`, error);
140
+ }
141
+ if (typeof process.getuid === "function") {
142
+ const stat = fs.lstatSync(directory.absolute, { bigint: true });
143
+ if ((stat.mode & 18n) !== 0n || stat.uid !== BigInt(process.getuid())) {
144
+ throw new FileReplacementError(`${label} must be owned by the current user and not group/world-writable`);
145
+ }
146
+ }
147
+ if (directory.dev !== target.dev) {
148
+ throw new FileReplacementError(`${label} must share the replacement target filesystem`);
149
+ }
150
+ if (expected && (directory.absolute !== expected.absolute
151
+ || directory.dev !== expected.dev
152
+ || directory.ino !== expected.ino)) {
153
+ throw new FileReplacementError(`${label} changed after its recovery boundary was validated`);
154
+ }
155
+ return { absolute: directory.absolute, dev: directory.dev, ino: directory.ino };
156
+ }
157
+ function syncDirectory(absolute) {
158
+ if (process.platform === "win32")
159
+ return;
160
+ let descriptor;
161
+ try {
162
+ descriptor = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY ?? 0));
163
+ fs.fsyncSync(descriptor);
164
+ }
165
+ catch (error) {
166
+ if (!["EINVAL", "ENOTSUP", "ENOSYS"].includes(error.code ?? ""))
167
+ throw error;
104
168
  }
105
169
  finally {
106
- if (fd !== undefined)
107
- fs.closeSync(fd);
170
+ if (descriptor !== undefined)
171
+ fs.closeSync(descriptor);
108
172
  }
109
173
  }
110
- function pathBytesEqual(parentFd, name, expected) {
111
- let fd;
174
+ function syncDirectoryBestEffort(absolute) {
112
175
  try {
113
- fd = fs.openSync(fdPath(parentFd, name), FILE_FLAGS);
114
- return readDescriptor(fd).equals(expected);
176
+ syncDirectory(absolute);
115
177
  }
116
- catch {
178
+ catch { /* The canonical rename already linearized. */ }
179
+ }
180
+ function safeSegments(relativeTarget, label) {
181
+ const segments = relativeTarget.split(/[\\/]/).filter(Boolean);
182
+ if (segments.length < 1
183
+ || path.posix.isAbsolute(relativeTarget)
184
+ || path.win32.isAbsolute(relativeTarget)
185
+ || relativeTarget.includes("\0")
186
+ || segments.includes("..")
187
+ || segments.includes("."))
188
+ reject({
189
+ class: "unsupported_target",
190
+ message: `${label} '${relativeTarget}' is outside the validated project path grammar`,
191
+ recovery: "Use one project-relative path without symbolic, dot, parent, drive, UNC, or absolute segments; no state was changed.",
192
+ });
193
+ return segments;
194
+ }
195
+ function relativeDisplay(root, absolute) {
196
+ return path.relative(root, absolute).split(path.sep).join("/");
197
+ }
198
+ function removeExactFile(file) {
199
+ const current = readFileIfPresent(file.absolute, `attempt-owned file '${file.absolute}'`);
200
+ if (!current || !sameStableFile(file, current))
117
201
  return false;
202
+ fs.unlinkSync(file.absolute);
203
+ return true;
204
+ }
205
+ function createDurableFile(absolute, bytes, mode = 0o600) {
206
+ let descriptor;
207
+ let opened;
208
+ try {
209
+ descriptor = fs.openSync(absolute, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), mode);
210
+ opened = identity(fs.fstatSync(descriptor, { bigint: true }));
211
+ fs.writeFileSync(descriptor, bytes);
212
+ if (typeof process.getuid === "function")
213
+ fs.fchmodSync(descriptor, mode);
214
+ fs.fsyncSync(descriptor);
215
+ }
216
+ catch (error) {
217
+ if (descriptor !== undefined) {
218
+ try {
219
+ fs.closeSync(descriptor);
220
+ }
221
+ catch { /* Preserve the primary failure. */ }
222
+ descriptor = undefined;
223
+ }
224
+ if (opened) {
225
+ try {
226
+ const current = readFileIfPresent(absolute, `failed stage '${absolute}'`);
227
+ if (current && sameIdentity(opened, current.identity))
228
+ fs.unlinkSync(absolute);
229
+ }
230
+ catch { /* Preserve changed or partially inspectable attempt state. */ }
231
+ }
232
+ throw error;
118
233
  }
119
234
  finally {
120
- if (fd !== undefined)
121
- fs.closeSync(fd);
235
+ if (descriptor !== undefined)
236
+ fs.closeSync(descriptor);
122
237
  }
123
- }
124
- function syncDirectory(fd) {
125
- fs.fsyncSync(fd);
238
+ return readStableFile(absolute, `durable stage '${absolute}'`);
126
239
  }
127
240
  function publicationConflict(markerPath) {
128
241
  reject({
@@ -133,65 +246,28 @@ function publicationConflict(markerPath) {
133
246
  });
134
247
  }
135
248
  /**
136
- * Pins one entity write to the root and exact valid marker selected by mode
137
- * detection. Node cannot combine pathname validation and mutation atomically,
138
- * so publication uses held directory descriptors, validates before and after
139
- * every observable boundary, and rolls back only links and empty directories
140
- * whose descriptor identities still match this attempt. Success linearizes at
141
- * the final root, marker, directory-chain, and target validation.
249
+ * Binds mutations to a validated project and exact entity-mode marker. Every
250
+ * pathname and byte identity is checked at declared boundaries. Publication
251
+ * linearizes through standard complete-file link or rename operations; a
252
+ * non-cooperating writer that races after the final successful check is outside
253
+ * the contract and is not represented as a cross-process compare-and-swap.
142
254
  */
143
255
  export class EntityPublicationContext {
144
256
  projectRoot;
145
257
  validatedRoot;
146
258
  markerPath;
147
- rootFd;
148
- markerParentEntries;
149
- markerFd;
150
- markerIdentity;
151
- markerBytes;
259
+ marker;
152
260
  createdDirectories = new Map();
153
261
  closed = false;
154
262
  constructor(root, markerPath, expectedBytes) {
155
263
  this.projectRoot = root.path;
156
264
  this.validatedRoot = root;
157
265
  this.markerPath = markerPath;
158
- this.markerBytes = Buffer.from(expectedBytes);
159
- this.rootFd = fs.openSync(root.path, DIRECTORY_FLAGS);
160
- this.markerParentEntries = [];
161
- let markerFd;
162
- try {
163
- const rootStat = fs.fstatSync(this.rootFd, { bigint: true });
164
- const rootIdentity = root.identities.at(-1);
165
- if (rootStat.dev !== rootIdentity.dev || rootStat.ino !== rootIdentity.ino) {
166
- throw new Error(`project root '${root.path}' changed while its publication context was opened`);
167
- }
168
- const segments = markerPath.split("/").filter(Boolean);
169
- let parentFd = this.rootFd;
170
- for (const name of segments.slice(0, -1)) {
171
- const fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
172
- this.markerParentEntries.push({ parentFd, name, fd, created: false });
173
- parentFd = fd;
174
- }
175
- markerFd = fs.openSync(fdPath(parentFd, segments.at(-1)), FILE_FLAGS);
176
- const before = fs.fstatSync(markerFd, { bigint: true });
177
- const bytes = readDescriptor(markerFd);
178
- const after = fs.fstatSync(markerFd, { bigint: true });
179
- if (!before.isFile() || !sameIdentity(identity(before), after) || !bytes.equals(expectedBytes)) {
180
- publicationConflict(markerPath);
181
- }
182
- this.markerFd = markerFd;
183
- this.markerIdentity = identity(after);
184
- this.assertValid();
185
- markerFd = undefined;
186
- }
187
- catch (error) {
188
- if (markerFd !== undefined)
189
- fs.closeSync(markerFd);
190
- for (const entry of this.markerParentEntries.reverse())
191
- fs.closeSync(entry.fd);
192
- fs.closeSync(this.rootFd);
193
- throw error;
194
- }
266
+ const markerAbsolute = path.join(root.path, ...safeSegments(markerPath, "state mode marker"));
267
+ this.marker = readStableFile(markerAbsolute, `state mode marker '${markerPath}'`);
268
+ if (!this.marker.bytes.equals(expectedBytes))
269
+ publicationConflict(markerPath);
270
+ this.assertValid();
195
271
  }
196
272
  static open(root, markerPath, expectedBytes) {
197
273
  return new EntityPublicationContext(root, markerPath, expectedBytes);
@@ -199,500 +275,421 @@ export class EntityPublicationContext {
199
275
  pinnedPath(relativePath = "") {
200
276
  if (this.closed)
201
277
  throw new Error("entity publication context is closed");
202
- return relativePath ? path.join(fdPath(this.rootFd), relativePath) : fdPath(this.rootFd);
278
+ if (!relativePath)
279
+ return this.projectRoot;
280
+ return path.join(this.projectRoot, ...safeSegments(relativePath, "entity publication path"));
203
281
  }
204
282
  assertValid() {
205
283
  if (this.closed)
206
284
  throw new Error("entity publication context is closed");
207
285
  assertValidatedProjectRoot(this.validatedRoot);
208
- const currentRoot = fs.fstatSync(this.rootFd, { bigint: true });
209
- const expectedRoot = this.validatedRoot.identities.at(-1);
210
- if (currentRoot.dev !== expectedRoot.dev || currentRoot.ino !== expectedRoot.ino) {
211
- throw new Error(`project root '${this.projectRoot}' changed after validation; restore the exact real directory and retry`);
286
+ let current;
287
+ try {
288
+ current = readStableFile(this.marker.absolute, `state mode marker '${this.markerPath}'`);
212
289
  }
213
- for (const entry of this.markerParentEntries) {
214
- if (!openMatches(entry.parentFd, entry.name, entry.fd, DIRECTORY_FLAGS)) {
215
- publicationConflict(this.markerPath);
216
- }
290
+ catch {
291
+ publicationConflict(this.markerPath);
217
292
  }
218
- const parentFd = this.markerParentEntries.at(-1)?.fd ?? this.rootFd;
219
- const markerName = this.markerPath.split("/").filter(Boolean).at(-1);
220
- if (!openMatches(parentFd, markerName, this.markerFd, FILE_FLAGS)
221
- || !sameIdentity(this.markerIdentity, fs.fstatSync(this.markerFd, { bigint: true }))
222
- || !readDescriptor(this.markerFd).equals(this.markerBytes))
293
+ if (!sameStableFile(this.marker, current))
223
294
  publicationConflict(this.markerPath);
224
295
  }
225
296
  publishImmutable(relativeTarget, bytes) {
226
- const normalized = relativeTarget.split(path.sep).join("/");
227
- const segments = normalized.split("/").filter(Boolean);
228
- if (segments.length < 2
229
- || path.isAbsolute(relativeTarget)
230
- || segments.includes("..")
231
- || segments.includes("."))
232
- throw new Error(`unsafe entity publication target '${relativeTarget}'`);
233
- this.assertValid();
297
+ const segments = safeSegments(relativeTarget, "immutable entity target");
298
+ if (segments.length < 2)
299
+ throw new Error(`unsafe immutable entity target '${relativeTarget}'`);
234
300
  const directories = [];
235
- const createdPaths = [];
236
- let stageFd;
237
- let stageName;
238
- let targetLinked = false;
301
+ const target = path.join(this.projectRoot, ...segments);
302
+ let stage;
303
+ let published;
304
+ let publicationOwned = false;
239
305
  try {
240
- let parentFd = this.rootFd;
241
- for (const name of segments.slice(0, -1)) {
242
- this.assertBoundary(directories);
243
- const entryParentFd = parentFd;
244
- let fd;
245
- let created = false;
246
- try {
247
- fd = fs.openSync(fdPath(entryParentFd, name), DIRECTORY_FLAGS);
248
- }
249
- catch (error) {
250
- if (error.code !== "ENOENT")
251
- throw error;
252
- this.assertBoundary(directories);
253
- fs.mkdirSync(fdPath(entryParentFd, name));
254
- created = true;
255
- fd = fs.openSync(fdPath(entryParentFd, name), DIRECTORY_FLAGS);
256
- }
257
- const entry = { parentFd: entryParentFd, name, fd, created };
258
- directories.push(entry);
259
- if (created)
260
- createdPaths.push({ relativePath: segments.slice(0, directories.length).join("/"), fd });
261
- parentFd = fd;
262
- if (created)
263
- syncDirectory(entryParentFd);
264
- this.assertBoundary(directories);
265
- }
266
- const directoryFd = directories.at(-1).fd;
267
- const targetName = segments.at(-1);
268
- stageName = `.${targetName}.${process.pid}.${randomUUID()}.tmp`;
269
- this.assertBoundary(directories);
270
- stageFd = fs.openSync(fdPath(directoryFd, stageName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
271
- this.assertBoundary(directories, { name: stageName, fd: stageFd });
272
- fs.writeFileSync(stageFd, bytes, "utf8");
273
- fs.fsyncSync(stageFd);
274
- this.assertBoundary(directories, { name: stageName, fd: stageFd });
275
- try {
276
- this.assertBoundary(directories, { name: stageName, fd: stageFd });
277
- fs.linkSync(fdPath(directoryFd, stageName), fdPath(directoryFd, targetName));
278
- targetLinked = true;
306
+ directories.push(...this.openDirectories(segments.slice(0, -1), true));
307
+ const directory = directories.at(-1)?.absolute ?? this.projectRoot;
308
+ const existing = readFileIfPresent(target, `immutable entity target '${relativeTarget}'`);
309
+ if (existing) {
310
+ this.removeCreatedDirectories(directories);
311
+ return null;
279
312
  }
280
- catch (error) {
281
- if (error.code !== "EEXIST")
282
- throw error;
283
- this.removeOwnedFile(directoryFd, stageName, stageFd);
284
- stageName = undefined;
285
- syncDirectory(directoryFd);
286
- this.assertBoundary(directories);
313
+ const stagePath = path.join(directory, `.${segments.at(-1)}.${process.pid}.${randomUUID()}.tmp`);
314
+ stage = createDurableFile(stagePath, bytes);
315
+ this.assertBoundary(directories, undefined, [stage]);
316
+ if (readFileIfPresent(target, `immutable entity target '${relativeTarget}'`)) {
317
+ removeExactFile(stage);
318
+ stage = undefined;
287
319
  this.removeCreatedDirectories(directories);
288
320
  return null;
289
321
  }
290
- syncDirectory(directoryFd);
291
- this.assertBoundary(directories, { name: stageName, fd: stageFd }, { name: targetName, fd: stageFd });
292
- this.removeOwnedFile(directoryFd, stageName, stageFd);
293
- stageName = undefined;
294
- syncDirectory(directoryFd);
295
- this.assertBoundary(directories, undefined, { name: targetName, fd: stageFd });
296
- for (const createdPath of createdPaths)
297
- this.createdDirectories.set(createdPath.relativePath, identity(fs.fstatSync(createdPath.fd, { bigint: true })));
298
- return publishedIdentity(stageFd);
322
+ try {
323
+ fs.linkSync(stage.absolute, target);
324
+ }
325
+ catch (error) {
326
+ if (error.code === "EEXIST") {
327
+ removeExactFile(stage);
328
+ stage = undefined;
329
+ this.removeCreatedDirectories(directories);
330
+ return null;
331
+ }
332
+ throw new FileReplacementError(`immutable publication for '${relativeTarget}' failed before effects`, error);
333
+ }
334
+ syncDirectory(directory);
335
+ stage = readStableFile(stage.absolute, `immutable publication stage '${relativeTarget}'`);
336
+ published = readStableFile(target, `published entity '${relativeTarget}'`);
337
+ if (!published.bytes.equals(Buffer.from(bytes)) || published.identity.ino !== stage.identity.ino || published.identity.dev !== stage.identity.dev) {
338
+ throw new ExactReplacementConflictError(`immutable entity target '${relativeTarget}' changed at its publication boundary`);
339
+ }
340
+ publicationOwned = true;
341
+ this.assertBoundary(directories, published, [stage]);
342
+ removeExactFile(stage);
343
+ stage = undefined;
344
+ syncDirectory(directory);
345
+ published = readStableFile(target, `published entity '${relativeTarget}'`);
346
+ this.assertBoundary(directories, published);
347
+ for (const entry of directories.filter(({ created }) => created)) {
348
+ this.createdDirectories.set(relativeDisplay(this.projectRoot, entry.absolute), entry);
349
+ }
350
+ return publishedIdentity(published);
299
351
  }
300
352
  catch (error) {
301
- const directoryFd = directories.at(-1)?.fd;
302
- const targetName = segments.at(-1);
303
- const cleanupFailures = [];
304
- if (directoryFd !== undefined && stageFd !== undefined && targetLinked) {
305
- try {
306
- this.removeOwnedFile(directoryFd, targetName, stageFd);
307
- }
308
- catch {
309
- cleanupFailures.push("entity");
310
- }
311
- }
312
- if (directoryFd !== undefined && stageFd !== undefined && stageName !== undefined) {
313
- try {
314
- this.removeOwnedFile(directoryFd, stageName, stageFd);
353
+ if (publicationOwned && published) {
354
+ if (stage) {
355
+ try {
356
+ if (removeExactFile(stage))
357
+ stage = undefined;
358
+ }
359
+ catch { /* Preserve changed attempt bytes. */ }
315
360
  }
316
- catch {
317
- cleanupFailures.push("stage");
361
+ if (!stage) {
362
+ try {
363
+ const current = readFileIfPresent(target, `failed immutable publication '${relativeTarget}'`);
364
+ if (current && matchesPublished(current, publishedIdentity(published)))
365
+ removeExactFile(current);
366
+ }
367
+ catch { /* Preserve a changed successor. */ }
318
368
  }
319
369
  }
320
- if (directoryFd !== undefined) {
370
+ if (stage) {
321
371
  try {
322
- syncDirectory(directoryFd);
372
+ removeExactFile(stage);
323
373
  }
324
- catch {
325
- cleanupFailures.push("directory sync");
326
- }
327
- }
328
- try {
329
- this.removeCreatedDirectories(directories);
330
- }
331
- catch {
332
- cleanupFailures.push("directory");
333
- }
334
- if (cleanupFailures.length > 0) {
335
- throw new Error(`entity publication failed and exact rollback could not remove its ${cleanupFailures.join(", ")}; inspect the pinned project state before retrying`, { cause: error });
374
+ catch { /* Preserve changed attempt bytes. */ }
336
375
  }
376
+ this.removeCreatedDirectories(directories);
337
377
  throw error;
338
378
  }
339
- finally {
340
- if (stageFd !== undefined)
341
- fs.closeSync(stageFd);
342
- for (const entry of directories.reverse())
343
- fs.closeSync(entry.fd);
344
- }
345
379
  }
346
380
  publishPreowned(relativeTarget, relativeReceipt, expected) {
347
- const targetSegments = relativeTarget.split(path.sep).filter(Boolean);
348
- const receiptSegments = relativeReceipt.split(path.sep).filter(Boolean);
349
- if ([targetSegments, receiptSegments].some((segments) => segments.length < 2 || segments.includes("..") || segments.includes("."))
350
- || path.isAbsolute(relativeTarget) || path.isAbsolute(relativeReceipt))
381
+ const targetSegments = safeSegments(relativeTarget, "preowned target");
382
+ const receiptSegments = safeSegments(relativeReceipt, "preowned receipt");
383
+ if (targetSegments.length < 2 || receiptSegments.length < 2)
351
384
  throw new Error("unsafe preowned publication path");
352
385
  this.assertValid();
353
- const receiptDirectories = [];
354
- const targetDirectories = [];
355
- const createdPaths = [];
356
- let receiptFd;
357
- let targetFd;
358
- let linked = false;
386
+ const receipt = readStableFile(path.join(this.projectRoot, ...receiptSegments), `preowned receipt '${relativeReceipt}'`);
387
+ if (!matchesPublished(receipt, expected))
388
+ throw new Error(`preowned receipt '${relativeReceipt}' changed before publication`);
389
+ const directories = this.openDirectories(targetSegments.slice(0, -1), true);
390
+ const directory = directories.at(-1)?.absolute ?? this.projectRoot;
391
+ const target = path.join(directory, targetSegments.at(-1));
359
392
  try {
360
- let receiptParent = this.rootFd;
361
- for (const name of receiptSegments.slice(0, -1)) {
362
- const fd = fs.openSync(fdPath(receiptParent, name), DIRECTORY_FLAGS);
363
- receiptDirectories.push({ parentFd: receiptParent, name, fd, created: false });
364
- receiptParent = fd;
365
- }
366
- receiptFd = fs.openSync(fdPath(receiptParent, receiptSegments.at(-1)), FILE_FLAGS);
367
- const receiptIdentity = publishedIdentity(receiptFd);
368
- if (!samePublished(receiptIdentity, expected))
369
- throw new Error(`migration ownership receipt '${relativeReceipt}' changed`);
370
- let targetParent = this.rootFd;
371
- for (const [index, name] of targetSegments.slice(0, -1).entries()) {
372
- this.assertBoundary(targetDirectories);
373
- const parentFd = targetParent;
374
- let fd;
375
- let created = false;
393
+ this.assertBoundary(directories, undefined, [receipt]);
394
+ if (!readFileIfPresent(target, `preowned target '${relativeTarget}'`)) {
376
395
  try {
377
- fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
396
+ fs.linkSync(receipt.absolute, target);
397
+ syncDirectory(directory);
378
398
  }
379
399
  catch (error) {
380
- if (error.code !== "ENOENT")
400
+ if (error.code !== "EEXIST")
381
401
  throw error;
382
- fs.mkdirSync(fdPath(parentFd, name));
383
- created = true;
384
- fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
385
- syncDirectory(parentFd);
386
402
  }
387
- targetDirectories.push({ parentFd, name, fd, created });
388
- if (created)
389
- createdPaths.push({ relativePath: targetSegments.slice(0, index + 1).join("/"), fd });
390
- targetParent = fd;
391
403
  }
392
- const targetName = targetSegments.at(-1);
393
- try {
394
- fs.linkSync(fdPath(receiptParent, receiptSegments.at(-1)), fdPath(targetParent, targetName));
395
- linked = true;
396
- syncDirectory(targetParent);
397
- }
398
- catch (error) {
399
- if (error.code !== "EEXIST")
400
- throw error;
401
- }
402
- targetFd = fs.openSync(fdPath(targetParent, targetName), FILE_FLAGS);
403
- const targetIdentity = publishedIdentity(targetFd);
404
- if (!sameObject(fs.fstatSync(receiptFd, { bigint: true }), fs.fstatSync(targetFd, { bigint: true })) || !samePublished(targetIdentity, expected)) {
404
+ const published = readStableFile(target, `preowned target '${relativeTarget}'`);
405
+ if (!matchesPublished(published, expected) || published.identity.dev !== receipt.identity.dev || published.identity.ino !== receipt.identity.ino) {
405
406
  throw new Error(`canonical target '${relativeTarget}' collides with migration ownership receipt; byte-identical replacement inodes are never adopted`);
406
407
  }
407
- this.assertBoundary(targetDirectories, undefined, { name: targetName, fd: targetFd });
408
- for (const createdPath of createdPaths)
409
- this.createdDirectories.set(createdPath.relativePath, identity(fs.fstatSync(createdPath.fd, { bigint: true })));
410
- return targetIdentity;
408
+ this.assertBoundary(directories, published, [receipt]);
409
+ for (const entry of directories.filter(({ created }) => created)) {
410
+ this.createdDirectories.set(relativeDisplay(this.projectRoot, entry.absolute), entry);
411
+ }
412
+ return publishedIdentity(published);
411
413
  }
412
414
  catch (error) {
413
- const targetParent = targetDirectories.at(-1)?.fd;
414
- if (linked && targetParent !== undefined && receiptFd !== undefined)
415
- this.removeOwnedFile(targetParent, targetSegments.at(-1), receiptFd);
416
- this.removeCreatedDirectories(targetDirectories);
415
+ this.removeCreatedDirectories(directories);
417
416
  throw error;
418
417
  }
419
- finally {
420
- if (targetFd !== undefined)
421
- fs.closeSync(targetFd);
422
- if (receiptFd !== undefined)
423
- fs.closeSync(receiptFd);
424
- for (const entry of targetDirectories.reverse())
425
- fs.closeSync(entry.fd);
426
- for (const entry of receiptDirectories.reverse())
427
- fs.closeSync(entry.fd);
428
- }
429
418
  }
430
- removeExact(relativeTarget, expected) {
431
- const segments = relativeTarget.split(path.sep).filter(Boolean);
432
- if (segments.length < 2 || path.isAbsolute(relativeTarget) || segments.includes("..") || segments.includes("."))
433
- throw new Error(`unsafe entity rollback target '${relativeTarget}'`);
434
- const directories = [];
435
- let targetFd;
419
+ removeExact(relativeTarget, expected, verifyContext = true) {
420
+ const segments = safeSegments(relativeTarget, "exact removal target");
421
+ const directories = this.openDirectories(segments.slice(0, -1), false, true, verifyContext);
422
+ if (!directories)
423
+ return "absent";
424
+ const directory = directories.at(-1)?.absolute ?? this.projectRoot;
425
+ const target = path.join(directory, segments.at(-1));
426
+ const current = readFileIfPresent(target, `exact removal target '${relativeTarget}'`);
427
+ if (!current)
428
+ return "absent";
429
+ if (!matchesPublished(current, expected))
430
+ return "identity_mismatch";
431
+ this.assertBoundary(directories, current, [], verifyContext);
432
+ const confirmed = readStableFile(target, `exact removal target '${relativeTarget}'`);
433
+ if (!sameStableFile(current, confirmed))
434
+ return "identity_mismatch";
435
+ fs.unlinkSync(target);
436
+ syncDirectory(directory);
437
+ this.removeAttemptDirectories();
438
+ return "removed";
439
+ }
440
+ replaceExisting(relativeTarget, expectedBytes, bytes, maxEntityBytes) {
441
+ return this.replaceOwned(relativeTarget, expectedBytes, bytes, maxEntityBytes);
442
+ }
443
+ replaceVisible(relativeTarget, expectedBytes, bytes, maxEntityBytes) {
444
+ return this.replaceOwned(relativeTarget, expectedBytes, bytes, maxEntityBytes);
445
+ }
446
+ restoreExact(relativeTarget, expected, bytes, maxEntityBytes) {
447
+ return this.replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, false);
448
+ }
449
+ restoreVisible(relativeTarget, expected, bytes, maxEntityBytes) {
450
+ return this.replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, false);
451
+ }
452
+ close() {
453
+ this.closed = true;
454
+ }
455
+ replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, verifyContext = true) {
456
+ const segments = safeSegments(relativeTarget, "recoverable replacement target");
457
+ const directories = this.openDirectories(segments.slice(0, -1), false, false, verifyContext);
458
+ if (!directories)
459
+ throw new FileReplacementError(`replacement target '${relativeTarget}' is absent before publication`);
460
+ const directory = directories.at(-1)?.absolute ?? this.projectRoot;
461
+ const target = path.join(directory, segments.at(-1));
462
+ const baseline = readStableFile(target, `replacement target '${relativeTarget}'`);
463
+ if (!matchesExpected(baseline, expected)) {
464
+ throw new ExactReplacementConflictError(`target '${relativeTarget}' ownership or bytes changed before replacement; the detected change was preserved`);
465
+ }
466
+ if (!Number.isSafeInteger(maxEntityBytes) || maxEntityBytes < 1 || baseline.bytes.length > maxEntityBytes) {
467
+ throw new FileReplacementError(`target '${relativeTarget}' exact ${baseline.bytes.length}-byte baseline exceeds the authority-owned ${maxEntityBytes}-byte recovery limit`);
468
+ }
469
+ const after = Buffer.from(bytes);
470
+ let recovery;
471
+ let backup;
472
+ let stage;
473
+ let metadata;
474
+ let committed;
475
+ let renameCompleted = false;
436
476
  try {
437
- let parentFd = this.rootFd;
438
- for (const name of segments.slice(0, -1)) {
439
- let fd;
440
- try {
441
- fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
442
- }
443
- catch (error) {
444
- if (error.code === "ENOENT")
445
- return "absent";
446
- throw error;
447
- }
448
- directories.push({ parentFd, name, fd, created: false });
449
- parentFd = fd;
477
+ recovery = this.createRecoveryAttempt(directory);
478
+ backup = createDurableFile(path.join(recovery.attempt.absolute, "original.previous"), baseline.bytes);
479
+ stage = createDurableFile(path.join(recovery.attempt.absolute, "replacement.tmp"), after, Number(baseline.identity.mode & 4095n));
480
+ metadata = createDurableFile(path.join(recovery.attempt.absolute, FILE_REPLACEMENT_METADATA_NAME), `${JSON.stringify({
481
+ schema_version: FILE_REPLACEMENT_RECOVERY_VERSION,
482
+ target_path: segments.join("/"),
483
+ before_sha256: sha256(baseline.bytes),
484
+ after_sha256: sha256(after),
485
+ })}\n`);
486
+ syncDirectory(recovery.attempt.absolute);
487
+ this.assertBoundary(directories, baseline, [backup, stage, metadata], verifyContext);
488
+ const boundaryTarget = readStableFile(target, `replacement target '${relativeTarget}'`);
489
+ if (!sameStableFile(boundaryTarget, baseline)) {
490
+ throw new ExactReplacementConflictError(`target '${relativeTarget}' changed at the final validation boundary; the detected change was preserved`);
450
491
  }
451
- const directoryFd = directories.at(-1).fd;
452
- const targetName = segments.at(-1);
453
492
  try {
454
- targetFd = fs.openSync(fdPath(directoryFd, targetName), FILE_FLAGS);
493
+ fs.renameSync(stage.absolute, target);
494
+ renameCompleted = true;
455
495
  }
456
496
  catch (error) {
457
- if (error.code === "ENOENT")
458
- return "absent";
497
+ throw new FileReplacementError(`complete-file replacement for '${relativeTarget}' failed before publication${error.code ? ` (${error.code})` : ""}`, error);
498
+ }
499
+ syncDirectory(directory);
500
+ syncDirectory(recovery.attempt.absolute);
501
+ const published = readStableFile(target, `published replacement '${relativeTarget}'`);
502
+ if (!published.bytes.equals(after)) {
503
+ throw new ExactReplacementConflictError(`target '${relativeTarget}' changed through the publication boundary`);
504
+ }
505
+ this.assertBoundary(directories, published, [backup, metadata], verifyContext);
506
+ committed = { previousBytes: baseline.bytes.toString("utf8"), publishedIdentity: publishedIdentity(published) };
507
+ this.cleanupCommittedReplacement(recovery.attempt.absolute, [backup, metadata]);
508
+ backup = undefined;
509
+ metadata = undefined;
510
+ stage = undefined;
511
+ return committed;
512
+ }
513
+ catch (error) {
514
+ if (committed)
515
+ return committed;
516
+ let current = null;
517
+ try {
518
+ current = readFileIfPresent(target, `replacement target '${relativeTarget}'`);
519
+ }
520
+ catch { /* Preserve all recovery evidence. */ }
521
+ if (!renameCompleted && error instanceof ExactReplacementConflictError) {
522
+ const retained = this.cleanupAttemptFiles([stage, backup, metadata]);
523
+ stage = undefined;
524
+ backup = undefined;
525
+ metadata = undefined;
526
+ if (retained.length) {
527
+ throw new ExactReplacementConflictError(`${error.message}; cleanup retained changed recovery roles`, retained);
528
+ }
459
529
  throw error;
460
530
  }
461
- if (!matchesPublished(targetFd, expected))
462
- return "identity_mismatch";
463
- const rollbackName = `.${targetName}.${process.pid}.${randomUUID()}.rollback`;
464
- fs.renameSync(fdPath(directoryFd, targetName), fdPath(directoryFd, rollbackName));
465
- if (openMatches(directoryFd, rollbackName, targetFd, FILE_FLAGS)) {
466
- this.removeOwnedFile(directoryFd, rollbackName, targetFd);
467
- }
468
- else {
531
+ if (renameCompleted && current?.bytes.equals(after) && backup) {
469
532
  try {
470
- fs.linkSync(fdPath(directoryFd, rollbackName), fdPath(directoryFd, targetName));
471
- fs.unlinkSync(fdPath(directoryFd, rollbackName));
533
+ this.assertBoundary(directories, current, [backup], false);
534
+ fs.renameSync(backup.absolute, target);
535
+ backup = undefined;
536
+ syncDirectory(directory);
537
+ const restored = readStableFile(target, `restored replacement target '${relativeTarget}'`);
538
+ if (!restored.bytes.equals(baseline.bytes))
539
+ throw new Error("restored bytes do not match the exact baseline");
540
+ if (stage) {
541
+ try {
542
+ removeExactFile(stage);
543
+ }
544
+ catch { /* Preserve changed stage. */ }
545
+ stage = undefined;
546
+ }
547
+ if (metadata) {
548
+ try {
549
+ removeExactFile(metadata);
550
+ }
551
+ catch { /* Preserve changed metadata. */ }
552
+ metadata = undefined;
553
+ }
554
+ syncDirectoryBestEffort(recovery?.attempt.absolute ?? directory);
555
+ throw new FileReplacementError(`${error.message}; restored exact prior bytes at '${relativeTarget}'`, error);
472
556
  }
473
- catch (error) {
474
- if (error.code !== "EEXIST")
475
- throw error;
557
+ catch (restoreError) {
558
+ if (restoreError instanceof FileReplacementError && restoreError.cause === error)
559
+ throw restoreError;
476
560
  }
477
- syncDirectory(directoryFd);
478
- return "identity_mismatch";
479
561
  }
480
- syncDirectory(directoryFd);
481
- return "removed";
562
+ if (!renameCompleted && current && sameStableFile(current, baseline)) {
563
+ const retained = this.cleanupAttemptFiles([stage, backup, metadata]);
564
+ stage = undefined;
565
+ backup = undefined;
566
+ metadata = undefined;
567
+ if (retained.length) {
568
+ throw new ExactReplacementConflictError(`${error.message}; cleanup retained changed recovery roles`, retained);
569
+ }
570
+ if (error instanceof ExactReplacementConflictError || error instanceof FileReplacementError || error instanceof StateWriteInputError)
571
+ throw error;
572
+ throw new FileReplacementError(`${error.message}; target bytes remained unchanged`, error);
573
+ }
574
+ const retained = [backup, stage, metadata]
575
+ .filter((file) => Boolean(file && fs.existsSync(file.absolute)))
576
+ .map((file) => relativeDisplay(this.projectRoot, file.absolute));
577
+ throw new ExactReplacementConflictError(`${error.message}; recoverable publication did not report success and preserved the canonical target plus ${retained.length} bounded recovery role${retained.length === 1 ? "" : "s"}`, retained);
482
578
  }
483
579
  finally {
484
- if (targetFd !== undefined)
485
- fs.closeSync(targetFd);
486
- for (const entry of directories.reverse())
487
- fs.closeSync(entry.fd);
488
- this.removeAttemptDirectories();
580
+ if (recovery)
581
+ this.closeRecoveryAttempt(recovery);
489
582
  }
490
583
  }
491
- removeAttemptDirectories() {
492
- const paths = [...this.createdDirectories.keys()].sort((left, right) => right.split("/").length - left.split("/").length);
493
- for (const relativePath of paths) {
494
- const segments = relativePath.split("/");
495
- const descriptors = [];
496
- let targetFd;
497
- let parentFd = this.rootFd;
584
+ openDirectories(segments, create, missingIsAbsent = false, verifyContext = true) {
585
+ const directories = [];
586
+ let parent = this.projectRoot;
587
+ for (const segment of segments) {
588
+ if (verifyContext)
589
+ this.assertValid();
590
+ else
591
+ assertValidatedProjectRoot(this.validatedRoot);
592
+ const absolute = path.join(parent, segment);
593
+ let entry;
498
594
  try {
499
- for (const name of segments.slice(0, -1)) {
500
- const fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
501
- descriptors.push(fd);
502
- parentFd = fd;
503
- }
504
- targetFd = fs.openSync(fdPath(parentFd, segments.at(-1)), DIRECTORY_FLAGS);
505
- const expected = this.createdDirectories.get(relativePath);
506
- const observed = fs.fstatSync(targetFd, { bigint: true });
507
- if (observed.dev !== expected.dev || observed.ino !== expected.ino) {
508
- this.createdDirectories.delete(relativePath);
509
- continue;
510
- }
511
- fs.rmdirSync(fdPath(parentFd, segments.at(-1)));
512
- syncDirectory(parentFd);
513
- this.createdDirectories.delete(relativePath);
595
+ entry = readStableDirectory(absolute);
514
596
  }
515
597
  catch (error) {
516
- const code = error.code ?? "";
517
- if (code === "ENOENT")
518
- this.createdDirectories.delete(relativePath);
519
- else if (!["ENOTEMPTY", "EEXIST"].includes(code))
598
+ if (error.code === "ENOENT" || error.message.includes("ENOENT")) {
599
+ if (missingIsAbsent)
600
+ return null;
601
+ if (!create)
602
+ throw error;
603
+ this.assertBoundary(directories, undefined, [], verifyContext);
604
+ fs.mkdirSync(absolute, 0o700);
605
+ syncDirectory(parent);
606
+ entry = readStableDirectory(absolute, true);
607
+ }
608
+ else
520
609
  throw error;
521
610
  }
522
- finally {
523
- if (targetFd !== undefined)
524
- fs.closeSync(targetFd);
525
- for (const fd of descriptors.reverse())
526
- fs.closeSync(fd);
527
- }
611
+ directories.push(entry);
612
+ parent = absolute;
528
613
  }
614
+ return directories;
529
615
  }
530
- replaceExisting(relativeTarget, expectedBytes, bytes, maxEntityBytes) {
531
- return this.replaceOwned(relativeTarget, expectedBytes, bytes, maxEntityBytes);
532
- }
533
- restoreExact(relativeTarget, expected, bytes, maxEntityBytes) {
534
- // Recovery stays bound to the pinned root and exact archived target even when context invalidation caused the primary failure.
535
- return this.replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, false);
536
- }
537
- replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, verifyContext = true) {
538
- const segments = relativeTarget.split(path.sep).filter(Boolean);
539
- if (segments.length < 2 || path.isAbsolute(relativeTarget) || segments.includes("..") || segments.includes("."))
540
- throw new Error(`unsafe entity replacement target '${relativeTarget}'`);
541
- const directories = [];
542
- let targetFd;
543
- let stageFd;
544
- let backupFd;
545
- let stageName;
546
- let backupName;
547
- let displacedName;
548
- let displacedFd;
549
- let displacementActive = false;
550
- let committedResult;
551
- let recovery;
552
- let baselineBytes;
553
- try {
554
- let parentFd = this.rootFd;
555
- for (const name of segments.slice(0, -1)) {
556
- const fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
557
- directories.push({ parentFd, name, fd, created: false });
558
- parentFd = fd;
559
- }
560
- const directoryFd = directories.at(-1).fd;
561
- const targetName = segments.at(-1);
562
- targetFd = fs.openSync(fdPath(directoryFd, targetName), FILE_FLAGS);
563
- this.assertBoundary(directories, undefined, { name: targetName, fd: targetFd }, verifyContext);
564
- const previousBytes = readDescriptor(targetFd);
565
- baselineBytes = previousBytes;
566
- if (!matchesExpected(targetFd, expected))
567
- throw new Error(`entity '${relativeTarget}' ownership changed before replacement; preserve both changes and retry explicitly`);
568
- if (!Number.isSafeInteger(maxEntityBytes) || maxEntityBytes < 1 || previousBytes.length > maxEntityBytes)
569
- throw new Error(`entity '${relativeTarget}' exact ${previousBytes.length}-byte baseline exceeds the authority-owned ${maxEntityBytes}-byte recovery limit`);
570
- recovery = this.createRecoveryAttempt(directoryFd);
571
- backupName = "original.previous";
572
- backupFd = fs.openSync(fdPath(recovery.attemptFd, backupName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
573
- fs.writeFileSync(backupFd, previousBytes);
574
- fs.fsyncSync(backupFd);
575
- const backupStat = fs.fstatSync(backupFd, { bigint: true });
576
- const attemptStat = fs.fstatSync(recovery.attemptFd, { bigint: true });
577
- if (!backupStat.isFile() || backupStat.dev !== attemptStat.dev || backupStat.uid !== attemptStat.uid || backupStat.nlink !== 1n || (backupStat.mode & 63n) !== 0n || !openMatches(recovery.attemptFd, backupName, backupFd, FILE_FLAGS) || !readDescriptor(backupFd).equals(previousBytes))
578
- throw new Error(`entity '${relativeTarget}' exact prior-byte snapshot could not be verified before replacement`);
579
- stageName = "replacement.tmp";
580
- stageFd = fs.openSync(fdPath(recovery.attemptFd, stageName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
581
- fs.writeFileSync(stageFd, bytes, "utf8");
582
- fs.fsyncSync(stageFd);
583
- this.assertBoundary(directories, undefined, { name: targetName, fd: targetFd }, verifyContext);
584
- if (!openMatches(recovery.attemptFd, stageName, stageFd, FILE_FLAGS) || !openMatches(recovery.attemptFd, backupName, backupFd, FILE_FLAGS) || !readDescriptor(backupFd).equals(previousBytes))
585
- throw new Error(`entity '${relativeTarget}' private recovery attempt changed before displacement`);
586
- displacedName = "original.displaced";
587
- fs.renameSync(fdPath(directoryFd, targetName), fdPath(recovery.attemptFd, displacedName));
588
- displacementActive = true;
589
- displacedFd = fs.openSync(fdPath(recovery.attemptFd, displacedName), FILE_FLAGS);
590
- if (!openMatches(recovery.attemptFd, displacedName, targetFd, FILE_FLAGS) || !matchesExpected(displacedFd, expected))
591
- throw new Error(`entity '${relativeTarget}' ownership changed after staging; competing bytes were preserved at the target`);
616
+ assertBoundary(directories, target, roles = [], verifyContext = true) {
617
+ if (verifyContext)
618
+ this.assertValid();
619
+ else
620
+ assertValidatedProjectRoot(this.validatedRoot);
621
+ for (const directory of directories) {
622
+ let current;
592
623
  try {
593
- fs.linkSync(fdPath(recovery.attemptFd, stageName), fdPath(directoryFd, targetName));
594
- }
595
- catch (publishError) {
596
- if (publishError.code === "EEXIST")
597
- throw new Error(`entity '${relativeTarget}' ownership changed during no-clobber publication; competing target bytes were preserved`, { cause: publishError });
598
- throw publishError;
599
- }
600
- if (!this.removeOwnedFile(recovery.attemptFd, stageName, stageFd))
601
- throw new Error(`entity '${relativeTarget}' replacement stage changed before cleanup`);
602
- stageName = undefined;
603
- syncDirectory(directoryFd);
604
- syncDirectory(recovery.attemptFd);
605
- this.assertBoundary(directories, undefined, { name: targetName, fd: stageFd }, verifyContext);
606
- if (!openMatches(recovery.attemptFd, displacedName, targetFd, FILE_FLAGS) || !matchesExpected(displacedFd, expected))
607
- throw new Error(`entity '${relativeTarget}' ownership changed through the publication boundary; competing bytes remain recoverable`);
608
- committedResult = { previousBytes: previousBytes.toString("utf8"), publishedIdentity: publishedIdentity(stageFd) };
609
- this.cleanupCommittedReplacement(recovery.attemptFd, [[displacedName, targetFd], [backupName, backupFd]]);
610
- displacementActive = false;
611
- return committedResult;
624
+ current = fs.lstatSync(directory.absolute, { bigint: true });
625
+ }
626
+ catch {
627
+ throw new ExactReplacementConflictError(`publication directory '${relativeDisplay(this.projectRoot, directory.absolute)}' changed at a validation boundary`);
628
+ }
629
+ if (!sameDirectory(directory, current)) {
630
+ throw new ExactReplacementConflictError(`publication directory '${relativeDisplay(this.projectRoot, directory.absolute)}' changed at a validation boundary`);
631
+ }
612
632
  }
613
- catch (error) {
614
- if (committedResult)
615
- return committedResult;
616
- const directoryFd = directories.at(-1)?.fd;
617
- if (displacementActive && directoryFd !== undefined && targetFd !== undefined && recovery) {
618
- throw this.recoverDisplacedReplacement({ directoryFd, recoveryFd: recovery.attemptFd, recoveryPath: recovery.relativePath, relativeTarget, baselineBytes: baselineBytes, backupFd, stageFd, stageName, backupName, displacedFd, displacedName, retainStage: !verifyContext, primary: error });
619
- }
620
- const cleanupFailures = [];
621
- for (const [name, fd, label] of [[stageName, stageFd, "stage"], [backupName, backupFd, "backup"]])
622
- if (name && fd !== undefined) {
623
- try {
624
- if (!recovery || !this.removeOwnedFile(recovery.attemptFd, name, fd))
625
- cleanupFailures.push(`${label} '${recovery ? path.join(recovery.relativePath, name) : name}' changed`);
626
- }
627
- catch (cleanupError) {
628
- cleanupFailures.push(`${label} '${recovery ? path.join(recovery.relativePath, name) : name}': ${cleanupError.message}`);
629
- }
630
- }
631
- if (cleanupFailures.length)
632
- throw new Error(`${error.message}; cleanup retained attempt state: ${cleanupFailures.join("; ")}`, { cause: error });
633
- throw error;
633
+ if (target) {
634
+ const current = readFileIfPresent(target.absolute, `publication target '${relativeDisplay(this.projectRoot, target.absolute)}'`);
635
+ if (!current || !sameStableFile(target, current)) {
636
+ throw new ExactReplacementConflictError(`publication target '${relativeDisplay(this.projectRoot, target.absolute)}' changed at a validation boundary`);
637
+ }
634
638
  }
635
- finally {
636
- if (displacedFd !== undefined)
637
- fs.closeSync(displacedFd);
638
- if (backupFd !== undefined)
639
- fs.closeSync(backupFd);
640
- if (stageFd !== undefined)
641
- fs.closeSync(stageFd);
642
- if (targetFd !== undefined)
643
- fs.closeSync(targetFd);
644
- for (const entry of directories.reverse())
645
- fs.closeSync(entry.fd);
646
- if (recovery)
647
- this.closeRecoveryAttempt(recovery);
639
+ for (const role of roles) {
640
+ const current = readFileIfPresent(role.absolute, `publication role '${relativeDisplay(this.projectRoot, role.absolute)}'`);
641
+ if (!current || !sameStableFile(role, current)) {
642
+ throw new ExactReplacementConflictError(`publication recovery role '${relativeDisplay(this.projectRoot, role.absolute)}' changed at a validation boundary`);
643
+ }
648
644
  }
649
645
  }
650
- createRecoveryAttempt(targetDirectoryFd) {
651
- const rootName = ".entity-recovery";
652
- const agenteraFd = fs.openSync(fdPath(this.rootFd, ".agentera"), DIRECTORY_FLAGS);
653
- let rootFd;
654
- let ignoreFd;
655
- let ignoreCreated = false;
656
- let attemptFd;
657
- let attemptName;
646
+ createRecoveryAttempt(targetDirectory) {
647
+ const agentera = readStableDirectory(path.join(this.projectRoot, ".agentera"));
648
+ const rootPath = path.join(agentera.absolute, ".entity-recovery");
658
649
  let rootCreated = false;
650
+ let ignoreCreated = false;
651
+ let root;
652
+ let ignore;
653
+ let attempt;
659
654
  try {
660
655
  try {
661
- rootFd = fs.openSync(fdPath(agenteraFd, rootName), DIRECTORY_FLAGS);
656
+ root = readStableDirectory(rootPath);
662
657
  }
663
658
  catch (error) {
664
- if (error.code !== "ENOENT")
665
- throw new Error(`private entity recovery root '.agentera/${rootName}' cannot be opened safely: ${error.message}`, { cause: error });
666
- fs.mkdirSync(fdPath(agenteraFd, rootName), 0o700);
659
+ if (!error.message.includes("ENOENT")) {
660
+ throw new FileReplacementError(`private entity recovery root '.agentera/.entity-recovery' cannot be opened safely: ${error.message}`, error);
661
+ }
662
+ fs.mkdirSync(rootPath, 0o700);
667
663
  rootCreated = true;
668
- syncDirectory(agenteraFd);
669
- rootFd = fs.openSync(fdPath(agenteraFd, rootName), DIRECTORY_FLAGS);
670
- }
671
- const rootStat = fs.fstatSync(rootFd, { bigint: true });
672
- if ((rootStat.mode & 18n) !== 0n || (typeof process.getuid === "function" && rootStat.uid !== BigInt(process.getuid())))
673
- throw new Error(`private entity recovery root '.agentera/${rootName}' must be owned by the current user and not group/world-writable`);
674
- if (rootStat.dev !== fs.fstatSync(targetDirectoryFd, { bigint: true }).dev)
675
- throw new Error("private entity recovery root must share the canonical entity target filesystem");
676
- try {
677
- ignoreFd = fs.openSync(fdPath(rootFd, ".gitignore"), FILE_FLAGS);
678
- }
679
- catch (error) {
680
- if (error.code !== "ENOENT")
681
- throw new Error(`private entity recovery ignore marker cannot be opened safely: ${error.message}`, { cause: error });
682
- ignoreFd = fs.openSync(fdPath(rootFd, ".gitignore"), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
664
+ syncDirectory(agentera.absolute);
665
+ root = readStableDirectory(rootPath, true);
666
+ }
667
+ const trustedRoot = validateEntityRecoveryDirectory(this.validatedRoot, root.absolute, targetDirectory, "private entity recovery root '.agentera/.entity-recovery'", root);
668
+ root = { ...trustedRoot, created: rootCreated };
669
+ const ignorePath = path.join(root.absolute, ".gitignore");
670
+ const existingIgnore = readFileIfPresent(ignorePath, "private entity recovery ignore marker");
671
+ if (existingIgnore)
672
+ ignore = existingIgnore;
673
+ else {
674
+ ignore = createDurableFile(ignorePath, RECOVERY_IGNORE_BYTES);
683
675
  ignoreCreated = true;
684
- fs.writeFileSync(ignoreFd, RECOVERY_IGNORE_BYTES, "utf8");
685
- fs.fsyncSync(ignoreFd);
686
- }
687
- const ignoreStat = fs.fstatSync(ignoreFd, { bigint: true });
688
- if (!ignoreStat.isFile() || (ignoreStat.mode & 18n) !== 0n || (typeof process.getuid === "function" && ignoreStat.uid !== BigInt(process.getuid())) || !readDescriptor(ignoreFd).equals(Buffer.from(RECOVERY_IGNORE_BYTES)))
689
- throw new Error("private entity recovery ignore marker must be an owner-controlled regular file with the authoritative rules");
690
- syncDirectory(rootFd);
691
- for (let attempt = 0; attempt < 8; attempt += 1) {
692
- const candidate = `entity-${process.pid}-${randomUUID()}`;
676
+ }
677
+ if (!ignore.bytes.equals(Buffer.from(RECOVERY_IGNORE_BYTES))) {
678
+ throw new FileReplacementError("private entity recovery ignore marker must contain the authoritative rules");
679
+ }
680
+ const ignoreStat = fs.lstatSync(ignore.absolute, { bigint: true });
681
+ if (typeof process.getuid === "function" && ((ignoreStat.mode & 18n) !== 0n || ignoreStat.uid !== BigInt(process.getuid()))) {
682
+ throw new FileReplacementError("private entity recovery ignore marker must be owner-controlled and not group/world-writable");
683
+ }
684
+ syncDirectory(root.absolute);
685
+ for (let index = 0; index < 8; index += 1) {
686
+ const attemptPath = path.join(root.absolute, `entity-${process.pid}-${randomUUID()}`);
693
687
  try {
694
- fs.mkdirSync(fdPath(rootFd, candidate), 0o700);
695
- attemptName = candidate;
688
+ fs.mkdirSync(attemptPath, 0o700);
689
+ const opened = readStableDirectory(attemptPath, true);
690
+ attempt = opened;
691
+ const trusted = validateEntityRecoveryDirectory(this.validatedRoot, attemptPath, targetDirectory, `private entity recovery attempt '${relativeDisplay(this.projectRoot, attemptPath)}'`, opened);
692
+ attempt = { ...trusted, created: true };
696
693
  break;
697
694
  }
698
695
  catch (error) {
@@ -700,263 +697,108 @@ export class EntityPublicationContext {
700
697
  throw error;
701
698
  }
702
699
  }
703
- if (!attemptName)
704
- throw new Error("could not allocate a unique private entity recovery attempt after 8 tries");
705
- attemptFd = fs.openSync(fdPath(rootFd, attemptName), DIRECTORY_FLAGS);
706
- const attemptStat = fs.fstatSync(attemptFd, { bigint: true });
707
- if ((attemptStat.mode & 63n) !== 0n || attemptStat.uid !== rootStat.uid || attemptStat.dev !== rootStat.dev)
708
- throw new Error("private entity recovery attempt must be owner-only, owner-controlled, and on the recovery-root filesystem");
709
- syncDirectory(rootFd);
710
- return { agenteraFd, rootFd, attemptFd, ignoreFd, attemptName, relativePath: `.agentera/${rootName}/${attemptName}` };
700
+ if (!attempt)
701
+ throw new FileReplacementError("could not allocate a unique private entity recovery attempt after 8 tries");
702
+ syncDirectory(root.absolute);
703
+ return { root, attempt, ignore, relativePath: relativeDisplay(this.projectRoot, attempt.absolute) };
711
704
  }
712
705
  catch (error) {
713
- if (attemptFd !== undefined)
714
- fs.closeSync(attemptFd);
715
- if (rootFd !== undefined && attemptName) {
706
+ if (attempt) {
716
707
  try {
717
- fs.rmdirSync(fdPath(rootFd, attemptName));
708
+ fs.rmdirSync(attempt.absolute);
718
709
  }
719
- catch { /* preserve unexpected contents */ }
710
+ catch { /* Preserve unexpected contents. */ }
720
711
  }
721
- if (rootFd !== undefined && ignoreFd !== undefined && ignoreCreated) {
712
+ if (ignore && ignoreCreated) {
722
713
  try {
723
- this.removeOwnedFile(rootFd, ".gitignore", ignoreFd);
714
+ removeExactFile(ignore);
724
715
  }
725
- catch { /* preserve changed marker */ }
716
+ catch { /* Preserve changed marker. */ }
726
717
  }
727
- if (ignoreFd !== undefined)
728
- fs.closeSync(ignoreFd);
729
- if (rootFd !== undefined)
730
- fs.closeSync(rootFd);
731
- if (rootCreated) {
718
+ if (root && rootCreated) {
732
719
  try {
733
- fs.rmdirSync(fdPath(agenteraFd, rootName));
720
+ fs.rmdirSync(root.absolute);
734
721
  }
735
- catch { /* preserve non-empty or changed root */ }
722
+ catch { /* Preserve non-empty root. */ }
736
723
  }
737
- fs.closeSync(agenteraFd);
738
724
  throw error;
739
725
  }
740
726
  }
741
- closeRecoveryAttempt(recovery) {
742
- try {
743
- if (openMatches(recovery.rootFd, recovery.attemptName, recovery.attemptFd, DIRECTORY_FLAGS)) {
744
- try {
745
- fs.rmdirSync(fdPath(recovery.rootFd, recovery.attemptName));
746
- syncDirectory(recovery.rootFd);
747
- }
748
- catch { /* best-effort private cleanup retains non-empty or changed attempt */ }
749
- }
750
- const remaining = fs.readdirSync(fdPath(recovery.rootFd));
751
- if (remaining.length === 1 && remaining[0] === ".gitignore" && openMatches(recovery.rootFd, ".gitignore", recovery.ignoreFd, FILE_FLAGS)) {
752
- try {
753
- this.removeOwnedFile(recovery.rootFd, ".gitignore", recovery.ignoreFd);
754
- syncDirectory(recovery.rootFd);
755
- }
756
- catch { /* preserve marker on cleanup failure */ }
757
- }
758
- if (openMatches(recovery.agenteraFd, ".entity-recovery", recovery.rootFd, DIRECTORY_FLAGS)) {
759
- try {
760
- fs.rmdirSync(fdPath(recovery.agenteraFd, ".entity-recovery"));
761
- syncDirectory(recovery.agenteraFd);
762
- }
763
- catch { /* best-effort private cleanup retains non-empty or changed root */ }
727
+ cleanupAttemptFiles(files) {
728
+ const retained = [];
729
+ for (const file of files) {
730
+ if (!file)
731
+ continue;
732
+ try {
733
+ if (!removeExactFile(file) && fs.existsSync(file.absolute))
734
+ retained.push(relativeDisplay(this.projectRoot, file.absolute));
764
735
  }
765
- }
766
- catch {
767
- /* committed or already-recovered state must not be reclassified by private cleanup */
768
- }
769
- finally {
770
- for (const fd of [recovery.attemptFd, recovery.ignoreFd, recovery.rootFd, recovery.agenteraFd]) {
771
- try {
772
- fs.closeSync(fd);
773
- }
774
- catch { /* best-effort descriptor close */ }
736
+ catch {
737
+ retained.push(relativeDisplay(this.projectRoot, file.absolute));
775
738
  }
776
739
  }
740
+ return retained;
777
741
  }
778
- cleanupCommittedReplacement(directoryFd, links) {
779
- for (const [name, fd] of links) {
780
- for (let attempt = 0; attempt < 2; attempt += 1) {
781
- try {
782
- this.removeOwnedFile(directoryFd, name, fd);
783
- break;
784
- }
785
- catch { /* committed publication remains authoritative; retry once and otherwise retain recovery link */ }
786
- }
787
- }
788
- for (let attempt = 0; attempt < 2; attempt += 1) {
789
- try {
790
- syncDirectory(directoryFd);
791
- break;
792
- }
793
- catch { /* cleanup durability is best-effort after the durable commit boundary */ }
794
- }
742
+ cleanupCommittedReplacement(directory, files) {
743
+ this.cleanupAttemptFiles(files);
744
+ syncDirectoryBestEffort(directory);
795
745
  }
796
- recoverDisplacedReplacement(state) {
797
- const { directoryFd, recoveryFd, recoveryPath, relativeTarget, baselineBytes, backupFd, stageFd, displacedFd, retainStage, primary } = state;
798
- const targetName = path.basename(relativeTarget);
799
- const failures = [];
800
- const retained = new Set();
801
- let { stageName, backupName, displacedName } = state;
802
- const publicPath = (name) => path.join(recoveryPath, name).split(path.sep).join("/");
803
- const exactBaseline = () => Boolean(backupName && backupFd !== undefined && openMatches(recoveryFd, backupName, backupFd, FILE_FLAGS) && readDescriptor(backupFd).equals(baselineBytes));
804
- const exactDisplaced = () => Boolean(displacedName && displacedFd !== undefined && openMatches(recoveryFd, displacedName, displacedFd, FILE_FLAGS));
805
- const exactStage = () => Boolean(stageName && stageFd !== undefined && openMatches(recoveryFd, stageName, stageFd, FILE_FLAGS));
806
- const remove = (name, fd, label) => {
807
- try {
808
- if (this.removeOwnedFile(recoveryFd, name, fd))
809
- return true;
810
- failures.push(`${label} '${publicPath(name)}' changed before cleanup`);
811
- }
812
- catch (error) {
813
- failures.push(`${label} '${publicPath(name)}' cleanup failed: ${error.message}`);
814
- }
815
- retained.add(publicPath(name));
816
- return false;
817
- };
818
- const sync = (label) => { try {
819
- syncDirectory(directoryFd);
820
- syncDirectory(recoveryFd);
821
- return true;
746
+ closeRecoveryAttempt(recovery) {
747
+ try {
748
+ fs.rmdirSync(recovery.attempt.absolute);
749
+ syncDirectoryBestEffort(recovery.root.absolute);
822
750
  }
823
- catch (error) {
824
- failures.push(`${label} directory fsync failed: ${error.message}`);
825
- return false;
826
- } };
827
- const displacedBytes = exactDisplaced() ? readDescriptor(displacedFd) : undefined;
828
- const displacedCompetitor = Boolean(displacedBytes && !displacedBytes.equals(baselineBytes));
829
- const source = displacedCompetitor ? displacedName : exactBaseline() ? backupName : exactDisplaced() && displacedBytes?.equals(baselineBytes) ? displacedName : undefined;
830
- const sourceFd = source === backupName ? backupFd : source === displacedName ? displacedFd : undefined;
831
- if (stageFd !== undefined && openMatches(directoryFd, targetName, stageFd, FILE_FLAGS) && source && sourceFd !== undefined) {
832
- try {
833
- if (this.removeOwnedFile(directoryFd, targetName, stageFd))
834
- sync("replacement removal");
835
- else
836
- failures.push(`published replacement '${relativeTarget}' changed before cleanup`);
751
+ catch {
752
+ return;
753
+ }
754
+ try {
755
+ const names = fs.readdirSync(recovery.root.absolute);
756
+ if (names.length === 1 && names[0] === ".gitignore") {
757
+ removeExactFile(recovery.ignore);
758
+ syncDirectoryBestEffort(recovery.root.absolute);
837
759
  }
838
- catch (error) {
839
- failures.push(`published replacement '${relativeTarget}' cleanup failed: ${error.message}`);
760
+ if (fs.readdirSync(recovery.root.absolute).length === 0) {
761
+ fs.rmdirSync(recovery.root.absolute);
762
+ syncDirectoryBestEffort(path.dirname(recovery.root.absolute));
840
763
  }
841
764
  }
842
- let restoredBaseline = pathBytesEqual(directoryFd, targetName, baselineBytes);
843
- let restoredCompetitor = Boolean(displacedCompetitor && displacedFd !== undefined && openMatches(directoryFd, targetName, displacedFd, FILE_FLAGS));
844
- let occupied = !restoredBaseline && !restoredCompetitor && entryExists(directoryFd, targetName);
845
- if (!restoredBaseline && !restoredCompetitor && !occupied && source && sourceFd !== undefined) {
765
+ catch { /* Preserve non-empty or changed recovery authority. */ }
766
+ }
767
+ removeCreatedDirectories(directories) {
768
+ for (const directory of [...directories].reverse()) {
769
+ if (!directory.created)
770
+ continue;
846
771
  try {
847
- fs.linkSync(fdPath(recoveryFd, source), fdPath(directoryFd, targetName));
772
+ const current = fs.lstatSync(directory.absolute, { bigint: true });
773
+ if (!sameDirectory(directory, current))
774
+ continue;
775
+ fs.rmdirSync(directory.absolute);
776
+ syncDirectoryBestEffort(path.dirname(directory.absolute));
848
777
  }
849
778
  catch (error) {
850
- if (error.code !== "EEXIST")
851
- failures.push(`restoration from '${publicPath(source)}' failed: ${error.message}`);
852
- }
853
- restoredBaseline = pathBytesEqual(directoryFd, targetName, baselineBytes);
854
- restoredCompetitor = Boolean(displacedCompetitor && displacedFd !== undefined && openMatches(directoryFd, targetName, displacedFd, FILE_FLAGS));
855
- occupied = !restoredBaseline && !restoredCompetitor && entryExists(directoryFd, targetName);
856
- }
857
- if (restoredBaseline || restoredCompetitor) {
858
- const durable = sync(restoredBaseline ? "exact baseline restoration" : "competitor restoration");
859
- if (durable) {
860
- if (retainStage && exactStage())
861
- retained.add(publicPath(stageName));
862
- else if (exactStage() && remove(stageName, stageFd, "replacement stage"))
863
- stageName = undefined;
864
- if (restoredCompetitor) {
865
- if (exactBaseline())
866
- retained.add(publicPath(backupName));
867
- else
868
- failures.push("exact baseline snapshot is unavailable");
869
- if (exactDisplaced() && remove(displacedName, displacedFd, "canonicalized competitor duplicate"))
870
- displacedName = undefined;
871
- }
872
- else {
873
- if (displacedCompetitor && exactDisplaced())
874
- retained.add(publicPath(displacedName));
875
- else if (exactDisplaced() && remove(displacedName, displacedFd, "displaced baseline duplicate"))
876
- displacedName = undefined;
877
- if (exactBaseline() && remove(backupName, backupFd, "baseline snapshot duplicate"))
878
- backupName = undefined;
879
- }
880
- sync("restoration cleanup");
881
- }
882
- for (const name of [backupName, displacedName, stageName])
883
- if (name && entryExists(recoveryFd, name))
884
- retained.add(publicPath(name));
885
- const outcome = restoredCompetitor
886
- ? `recovery restored concurrent competitor bytes at '${relativeTarget}' and retained the exact discovered baseline snapshot at ${exactBaseline() ? `'${publicPath(backupName)}'` : "an unavailable path"}`
887
- : `recovery restored byte-verified exact prior bytes at '${relativeTarget}'`;
888
- return new Error(`${primary.message}; ${outcome}${retained.size ? `; retained recovery files: ${[...retained].join(", ")}` : ""}${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
889
- }
890
- if (occupied) {
891
- if (exactBaseline())
892
- retained.add(publicPath(backupName));
893
- else
894
- failures.push("exact baseline snapshot is unavailable");
895
- if (displacedCompetitor && exactDisplaced())
896
- retained.add(publicPath(displacedName));
897
- if (sync("competing-target retention")) {
898
- if (retainStage && exactStage())
899
- retained.add(publicPath(stageName));
900
- else if (exactStage() && remove(stageName, stageFd, "replacement stage"))
901
- stageName = undefined;
902
- if (!displacedCompetitor && exactDisplaced() && remove(displacedName, displacedFd, "displaced baseline duplicate"))
903
- displacedName = undefined;
904
- sync("retention cleanup");
905
- }
906
- for (const name of [backupName, displacedName, stageName])
907
- if (name && entryExists(recoveryFd, name))
908
- retained.add(publicPath(name));
909
- return new Error(`${primary.message}; recovery preserved the competing canonical target and retained the exact discovered baseline snapshot and any displaced competitor at ${[...retained].join(", ") || "unavailable paths"}; preserve each role and resolve ownership explicitly${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
910
- }
911
- if (exactBaseline())
912
- retained.add(publicPath(backupName));
913
- if (exactDisplaced())
914
- retained.add(publicPath(displacedName));
915
- if (exactStage())
916
- retained.add(publicPath(stageName));
917
- return new Error(`${primary.message}; recovery could not restore the absent canonical target; retained role-labeled recovery files: ${[...retained].join(", ") || "none"}; restore the exact baseline or competitor explicitly before retrying${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
918
- }
919
- close() {
920
- if (this.closed)
921
- return;
922
- this.closed = true;
923
- fs.closeSync(this.markerFd);
924
- for (const entry of this.markerParentEntries.reverse())
925
- fs.closeSync(entry.fd);
926
- fs.closeSync(this.rootFd);
927
- }
928
- assertBoundary(directories, stage, target, verifyContext = true) {
929
- if (verifyContext)
930
- this.assertValid();
931
- for (const entry of directories) {
932
- if (!openMatches(entry.parentFd, entry.name, entry.fd, DIRECTORY_FLAGS)) {
933
- throw new Error(`entity publication directory '${entry.name}' changed during publication`);
779
+ if (!["ENOTEMPTY", "EEXIST", "ENOENT"].includes(error.code ?? ""))
780
+ throw error;
934
781
  }
935
782
  }
936
- const directoryFd = directories.at(-1)?.fd;
937
- if (directoryFd !== undefined && stage && !openMatches(directoryFd, stage.name, stage.fd, FILE_FLAGS)) {
938
- throw new Error("entity publication stage changed during publication");
939
- }
940
- if (directoryFd !== undefined && target && !openMatches(directoryFd, target.name, target.fd, FILE_FLAGS)) {
941
- throw new Error("entity publication target changed during publication");
942
- }
943
- }
944
- removeOwnedFile(parentFd, name, expectedFd) {
945
- if (!openMatches(parentFd, name, expectedFd, FILE_FLAGS))
946
- return false;
947
- fs.unlinkSync(fdPath(parentFd, name));
948
- return true;
949
783
  }
950
- removeCreatedDirectories(directories) {
951
- for (const entry of [...directories].reverse()) {
952
- if (!entry.created || !openMatches(entry.parentFd, entry.name, entry.fd, DIRECTORY_FLAGS))
953
- continue;
784
+ removeAttemptDirectories() {
785
+ const paths = [...this.createdDirectories].sort((left, right) => right[0].split("/").length - left[0].split("/").length);
786
+ for (const [relativePath, expected] of paths) {
954
787
  try {
955
- fs.rmdirSync(fdPath(entry.parentFd, entry.name));
956
- syncDirectory(entry.parentFd);
788
+ const current = fs.lstatSync(expected.absolute, { bigint: true });
789
+ if (!sameDirectory(expected, current)) {
790
+ this.createdDirectories.delete(relativePath);
791
+ continue;
792
+ }
793
+ fs.rmdirSync(expected.absolute);
794
+ syncDirectoryBestEffort(path.dirname(expected.absolute));
795
+ this.createdDirectories.delete(relativePath);
957
796
  }
958
797
  catch (error) {
959
- if (!["ENOTEMPTY", "EEXIST", "ENOENT"].includes(error.code ?? ""))
798
+ const code = error.code ?? "";
799
+ if (code === "ENOENT")
800
+ this.createdDirectories.delete(relativePath);
801
+ else if (!["ENOTEMPTY", "EEXIST"].includes(code))
960
802
  throw error;
961
803
  }
962
804
  }