@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14

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 (531) hide show
  1. package/CHANGELOG.md +894 -0
  2. package/README.md +159 -6
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  21. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  22. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  23. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  24. package/contracts/atelier-trackable.v1.schema.json +781 -0
  25. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  26. package/docs/architecture.md +89 -0
  27. package/docs/assurance-controls.md +2 -2
  28. package/docs/blocks/capability-stewardship.md +17 -0
  29. package/docs/blocks/claims.md +16 -3
  30. package/docs/blocks/inquiry-harnesses.md +19 -0
  31. package/docs/blocks/learning-harnesses.md +17 -0
  32. package/docs/capability-stewardship.md +267 -0
  33. package/docs/coordination.md +51 -0
  34. package/docs/decision-practices.md +111 -0
  35. package/docs/decisions.md +29 -0
  36. package/docs/evidence-navigation.md +237 -0
  37. package/docs/exact-upgrades.md +3 -2
  38. package/docs/ingestion.md +181 -0
  39. package/docs/inquiry-harnesses.md +210 -0
  40. package/docs/install.md +80 -3
  41. package/docs/intake-and-guides.md +5 -0
  42. package/docs/integration-contract-decisions.md +24 -2
  43. package/docs/interaction-and-reflection.md +72 -0
  44. package/docs/knowledge-graph.md +20 -0
  45. package/docs/knowledge-health/api.md +30 -0
  46. package/docs/knowledge-health/assessment/api.md +96 -0
  47. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  48. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  49. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  50. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  51. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  52. package/docs/knowledge-health/devday/facilitator.md +16 -0
  53. package/docs/knowledge-health/devday/participant-response.md +22 -0
  54. package/docs/knowledge-health/devday/public-default.md +107 -0
  55. package/docs/knowledge-health/devday/quickstart.md +18 -0
  56. package/docs/knowledge-health/devday/support.md +22 -0
  57. package/docs/knowledge-health/exercise.md +17 -0
  58. package/docs/knowledge-health/facilitator.md +20 -0
  59. package/docs/knowledge-health/quickstart.md +94 -0
  60. package/docs/knowledge-health/readme.md +15 -0
  61. package/docs/knowledge-health/support.md +34 -0
  62. package/docs/knowledge-setup.md +248 -0
  63. package/docs/knowledge-workspace.md +264 -0
  64. package/docs/learning-harnesses.md +237 -0
  65. package/docs/learning.md +215 -0
  66. package/docs/local-services.md +223 -19
  67. package/docs/obsidian-contract.md +246 -15
  68. package/docs/obsidian-plugin.md +252 -7
  69. package/docs/obsidian.md +874 -52
  70. package/docs/optional-ingestion.md +83 -0
  71. package/docs/personal-workspace.md +266 -0
  72. package/docs/practical-judgment.md +92 -0
  73. package/docs/progressive-ingestion-acceptance.md +89 -0
  74. package/docs/progressive-ingestion.md +235 -0
  75. package/docs/project-command-inventory.md +1 -0
  76. package/docs/release-engineering.md +50 -0
  77. package/docs/repo-boundary-guard.md +120 -3
  78. package/docs/research-starter-quickstart.md +66 -0
  79. package/docs/research-starter.md +114 -0
  80. package/docs/semantic-ingestion.md +198 -0
  81. package/docs/semantic-operation.md +318 -0
  82. package/docs/skill-steward.md +6 -0
  83. package/docs/template-profile.md +164 -0
  84. package/docs/template-upgrades.md +114 -0
  85. package/docs/trackables.md +111 -0
  86. package/docs/upgrade.md +219 -2
  87. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  88. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  89. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  90. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  91. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  92. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  93. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  94. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  95. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  96. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  97. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  98. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  99. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  100. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  101. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  102. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  103. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  104. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  105. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  106. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  107. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  108. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  109. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  110. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  111. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  112. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  113. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  114. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  115. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  116. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  117. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  118. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  119. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  120. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  121. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  122. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  123. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  124. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  125. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  126. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  127. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  128. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  129. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  130. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  131. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  132. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  133. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  134. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  135. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  136. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  137. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  138. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  139. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  140. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  141. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  142. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  143. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  144. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  145. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  146. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  147. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  148. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  149. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  150. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  151. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  152. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  153. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  154. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  155. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  156. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  157. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  158. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  159. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  160. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  161. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  162. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  163. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  164. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  165. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  166. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  167. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  168. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  169. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  170. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  171. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  172. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  173. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  174. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  175. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  176. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  177. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  178. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  179. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  180. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  181. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  182. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  183. package/fixtures/atelier-judgment/native-decision.md +3 -0
  184. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  185. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  186. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  187. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  188. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  189. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  190. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  191. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  192. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  193. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  194. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  195. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  196. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  197. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  198. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  199. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  200. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  201. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  202. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  203. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  204. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  205. package/fixtures/atelier-learning/valid/capture.json +13 -0
  206. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  207. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  210. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  211. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  213. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  214. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  215. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  216. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  217. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  218. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  219. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  220. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  221. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  222. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  223. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  224. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  225. package/fixtures/capability-packages/adoption.example.json +70 -0
  226. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  227. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  228. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  229. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  230. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  231. package/fixtures/capability-packages/research/capability-package.json +88 -0
  232. package/fixtures/capability-packages/research/capability-release.json +134 -0
  233. package/fixtures/capability-packages/research/evaluation.md +15 -0
  234. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  235. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  236. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  237. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  238. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  239. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  240. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  241. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  242. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  243. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  245. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  246. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  247. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  248. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  249. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  250. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  251. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  252. package/fixtures/evidence-navigation/valid/host.json +16 -0
  253. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  254. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  255. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  256. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  257. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  258. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  259. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  260. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  261. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  262. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  263. package/fixtures/harnesses/learning-cycle.json +780 -0
  264. package/fixtures/inquiry/workshop.json +675 -0
  265. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  266. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  267. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  268. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  269. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  270. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  271. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  272. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  273. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  274. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  275. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  276. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  277. package/fixtures/knowledge-health/expected-summary.json +27 -0
  278. package/fixtures/knowledge-health/workshop/README.md +20 -0
  279. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  280. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  281. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  282. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  283. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  284. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  285. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  286. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  287. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  288. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  289. package/fixtures/personal-workspace/overlay.json +24 -0
  290. package/fixtures/templates/local-library.v1.json +76 -0
  291. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  292. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  293. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  294. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  295. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  296. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  297. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  298. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  299. package/package.json +60 -5
  300. package/plugins/obsidian/main.js +57 -8
  301. package/plugins/obsidian/manifest.json +1 -1
  302. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  303. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  304. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  305. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  306. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  307. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  308. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  309. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  310. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  311. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  312. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  313. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  314. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  315. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  316. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  317. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  318. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  319. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  320. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  321. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  322. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  323. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  324. package/src/architecture/catalog.mjs +355 -0
  325. package/src/architecture/index.mjs +93 -0
  326. package/src/boundary/content-rules.mjs +536 -69
  327. package/src/boundary/delegation-command.mjs +158 -0
  328. package/src/boundary/delegation.mjs +396 -0
  329. package/src/boundary/policy.mjs +185 -64
  330. package/src/build/git.mjs +18 -0
  331. package/src/build/index.mjs +3 -0
  332. package/src/build/ledger.mjs +88 -0
  333. package/src/capabilities/adoption.mjs +381 -0
  334. package/src/capabilities/evidence.mjs +140 -0
  335. package/src/capabilities/files.mjs +124 -0
  336. package/src/capabilities/index.mjs +4 -0
  337. package/src/capabilities/instructions.mjs +250 -0
  338. package/src/capabilities/package.mjs +136 -0
  339. package/src/cli/command-failure.mjs +36 -0
  340. package/src/cli/execute-command.mjs +16 -2
  341. package/src/cli/run.mjs +66 -9
  342. package/src/coauthor/store.mjs +43 -2
  343. package/src/collaboration/proposals.mjs +189 -0
  344. package/src/commands/architecture.mjs +27 -0
  345. package/src/commands/boundary.mjs +3 -0
  346. package/src/commands/capability.mjs +73 -0
  347. package/src/commands/enroll.mjs +3 -0
  348. package/src/commands/harness.mjs +79 -0
  349. package/src/commands/ingest.mjs +39 -0
  350. package/src/commands/init.mjs +4 -1
  351. package/src/commands/inquiry.mjs +66 -0
  352. package/src/commands/knowledge-semantic.mjs +128 -0
  353. package/src/commands/knowledge.mjs +119 -0
  354. package/src/commands/learn.mjs +61 -0
  355. package/src/commands/obsidian.mjs +772 -65
  356. package/src/commands/practice.mjs +24 -0
  357. package/src/commands/setup.mjs +22 -2
  358. package/src/commands/trackable.mjs +19 -0
  359. package/src/contracts/corpus.mjs +30 -0
  360. package/src/coordination/index.mjs +110 -0
  361. package/src/evidence-navigation/contracts.mjs +60 -0
  362. package/src/evidence-navigation/evaluate.mjs +69 -0
  363. package/src/evidence-navigation/generation.mjs +22 -0
  364. package/src/evidence-navigation/index.mjs +2 -0
  365. package/src/evidence-navigation/local.mjs +167 -0
  366. package/src/evidence-navigation/schema.generated.mjs +604 -0
  367. package/src/evidence-navigation/types.mjs +12 -0
  368. package/src/export/atelier-export-contract.mjs +35 -8
  369. package/src/graph/enroll.mjs +248 -0
  370. package/src/graph/graph.mjs +30 -6
  371. package/src/graph/knowledge-graph.mjs +77 -9
  372. package/src/harnesses/contracts.mjs +41 -0
  373. package/src/harnesses/exchange.mjs +154 -0
  374. package/src/harnesses/feedback.mjs +16 -0
  375. package/src/harnesses/history.mjs +37 -0
  376. package/src/harnesses/index.mjs +4 -0
  377. package/src/harnesses/store.mjs +29 -0
  378. package/src/ingestion/contracts.mjs +41 -0
  379. package/src/ingestion/evaluation.mjs +119 -0
  380. package/src/ingestion/optional/composition/index.mjs +171 -0
  381. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  382. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  383. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  384. package/src/ingestion/optional/graphify/graphify_supplier.py +938 -0
  385. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  386. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  387. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  388. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  389. package/src/ingestion/optional/index.mjs +9 -0
  390. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  391. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  392. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  393. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  394. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  395. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  396. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  397. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  398. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  399. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  400. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  401. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  402. package/src/ingestion/processors.mjs +218 -0
  403. package/src/ingestion/semantic.mjs +203 -0
  404. package/src/ingestion/store.mjs +372 -0
  405. package/src/inquiry/beliefs.mjs +32 -0
  406. package/src/inquiry/contracts.mjs +23 -0
  407. package/src/inquiry/index.mjs +5 -0
  408. package/src/inquiry/ledger.mjs +135 -0
  409. package/src/inquiry/lenses.mjs +15 -0
  410. package/src/inquiry/projection.mjs +77 -0
  411. package/src/inquiry/store.mjs +35 -0
  412. package/src/intake/read-scope.mjs +12 -0
  413. package/src/intake/store.mjs +69 -6
  414. package/src/interaction/index.mjs +91 -0
  415. package/src/judgment/index.mjs +82 -0
  416. package/src/judgment/practice-evaluation.mjs +70 -0
  417. package/src/judgment/practice.mjs +144 -0
  418. package/src/knowledge/context.mjs +217 -0
  419. package/src/knowledge/index.mjs +5 -0
  420. package/src/knowledge/ingestion.mjs +79 -0
  421. package/src/knowledge/intake.mjs +33 -0
  422. package/src/knowledge/ledger.mjs +110 -0
  423. package/src/knowledge/participatory/guidance.mjs +36 -0
  424. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  425. package/src/knowledge/participatory/index.mjs +2 -0
  426. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  427. package/src/knowledge/plan.mjs +67 -0
  428. package/src/knowledge/projection.mjs +43 -0
  429. package/src/knowledge/semantic-operation.mjs +567 -0
  430. package/src/knowledge/sessions.mjs +467 -0
  431. package/src/knowledge/workspace.mjs +295 -0
  432. package/src/knowledge-health/assessment.mjs +165 -0
  433. package/src/knowledge-health/capabilities.mjs +350 -0
  434. package/src/knowledge-health/context.mjs +62 -0
  435. package/src/knowledge-health/example.mjs +47 -0
  436. package/src/knowledge-health/index.mjs +10 -0
  437. package/src/knowledge-health/owner-join.mjs +88 -0
  438. package/src/knowledge-health/proof/common.mjs +24 -0
  439. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  440. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  441. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  442. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  443. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  444. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  445. package/src/knowledge-health/rule-identity.mjs +49 -0
  446. package/src/knowledge-health/workshop-composition.mjs +19 -0
  447. package/src/knowledge-health/workshop-profile.mjs +176 -0
  448. package/src/knowledge-health/workshop.mjs +99 -0
  449. package/src/learning/adapters.mjs +68 -0
  450. package/src/learning/assistance.mjs +127 -0
  451. package/src/learning/contracts.mjs +65 -0
  452. package/src/learning/store.mjs +272 -0
  453. package/src/personal-workspace/index.mjs +391 -0
  454. package/src/personal-workspace/selection.mjs +401 -0
  455. package/src/project/config.mjs +8 -0
  456. package/src/project/durable-state.mjs +3 -1
  457. package/src/project/file-class.mjs +1 -1
  458. package/src/project/path-match.mjs +2 -1
  459. package/src/projection/obsidian/edits/apply.mjs +55 -16
  460. package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
  461. package/src/projection/obsidian/personal-workspace.mjs +519 -0
  462. package/src/projection/obsidian/proposals/adapter.mjs +79 -17
  463. package/src/projection/obsidian/proposals/observation.mjs +50 -4
  464. package/src/projection/obsidian/publication/publisher.mjs +214 -18
  465. package/src/projection/obsidian/publication/vault-list.mjs +4 -3
  466. package/src/projection/obsidian/recovery/index.mjs +4 -1
  467. package/src/projection/obsidian/recovery/store.mjs +160 -7
  468. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  469. package/src/projection/project.mjs +3 -2
  470. package/src/readiness/readiness.mjs +10 -3
  471. package/src/reflection/index.mjs +56 -0
  472. package/src/runtime/obsidian/app-production-seams.mjs +48 -16
  473. package/src/runtime/obsidian/app-registration.mjs +175 -28
  474. package/src/runtime/obsidian/app-restart.mjs +154 -0
  475. package/src/runtime/obsidian/engine.mjs +437 -56
  476. package/src/runtime/obsidian/extension-points.mjs +1 -1
  477. package/src/runtime/obsidian/index.mjs +10 -7
  478. package/src/runtime/obsidian/launch-plan.mjs +7 -4
  479. package/src/runtime/obsidian/lifecycle.mjs +108 -23
  480. package/src/runtime/obsidian/login-item.mjs +335 -0
  481. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  482. package/src/runtime/obsidian/observation.mjs +36 -4
  483. package/src/runtime/obsidian/opening.mjs +173 -38
  484. package/src/runtime/obsidian/personal-selection.mjs +125 -0
  485. package/src/runtime/obsidian/pipeline.mjs +115 -6
  486. package/src/runtime/obsidian/plugin-channel.mjs +11 -3
  487. package/src/runtime/obsidian/plugin-choice.mjs +16 -4
  488. package/src/runtime/obsidian/plugin-drift.mjs +59 -11
  489. package/src/runtime/obsidian/plugin-presence.mjs +14 -1
  490. package/src/runtime/obsidian/private-lock.mjs +39 -4
  491. package/src/runtime/obsidian/process-identity.mjs +20 -10
  492. package/src/runtime/obsidian/project-views.mjs +282 -0
  493. package/src/runtime/obsidian/questions.mjs +48 -0
  494. package/src/runtime/obsidian/release-watch.mjs +56 -0
  495. package/src/runtime/obsidian/service-main.mjs +148 -19
  496. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  497. package/src/runtime/obsidian/service-managers.mjs +275 -0
  498. package/src/runtime/obsidian/service-record.mjs +87 -23
  499. package/src/runtime/obsidian/service.mjs +102 -9
  500. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  501. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  502. package/src/runtime/obsidian/vault-location.mjs +300 -0
  503. package/src/runtime/obsidian/view-counts.mjs +34 -0
  504. package/src/runtime/obsidian/view-permission.mjs +334 -0
  505. package/src/server/local-sidecar.mjs +169 -2
  506. package/src/server/server.mjs +28 -2
  507. package/src/template-bindings/project.mjs +92 -0
  508. package/src/templates/profile.mjs +225 -0
  509. package/src/trackables/domain.mjs +175 -0
  510. package/src/trackables/store.mjs +114 -0
  511. package/src/ui/control-system.mjs +201 -0
  512. package/src/ui/html-primitives.mjs +3 -0
  513. package/src/ui/knowledge-page.mjs +1388 -0
  514. package/src/upgrade/closure-diagnostics.mjs +135 -0
  515. package/src/upgrade/closure.mjs +649 -0
  516. package/src/upgrade/explanation.mjs +1 -1
  517. package/src/upgrade/template-participant.mjs +171 -0
  518. package/src/upgrade/transaction.mjs +72 -25
  519. package/src/upgrade/upgrade.mjs +20 -7
  520. package/templates/external-project-workspace/pack-lifecycle.json +7 -1
  521. package/templates/knowledge-workspace/.gitattributes +2 -0
  522. package/templates/knowledge-workspace/AGENTS.md +27 -0
  523. package/templates/knowledge-workspace/README.md +44 -0
  524. package/templates/knowledge-workspace/atelier.project.json +22 -0
  525. package/templates/knowledge-workspace/gitignore +6 -0
  526. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  527. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  528. package/templates/knowledge-workspace/records/forecast.md +22 -0
  529. package/templates/knowledge-workspace/records/inspection.md +25 -0
  530. package/templates/knowledge-workspace/records/telescope.md +24 -0
  531. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -1,7 +1,10 @@
1
+ import { createCooperativeBudget, prepareViewCooperatively } from '../../projection/obsidian/materialize/prepare-view.mjs'
1
2
  import { createHash } from 'node:crypto'
2
3
  import path from 'node:path'
3
- import { buildCanonicalGraph, createGraphFileCache } from '../../graph/graph.mjs'
4
- import { EMITTER_VERSION, createPreparationCache, prepareView, withEligibility } from '../../projection/obsidian/materialize/index.mjs'
4
+ import { buildCanonicalGraph, buildCanonicalGraphSteps, createGraphFileCache } from '../../graph/graph.mjs'
5
+ import { markdownMetadata } from '../../graph/knowledge-graph.mjs'
6
+ import { EMITTER_VERSION, createPreparationCache, prepareView, readMarkdownLens, withEligibility } from '../../projection/obsidian/materialize/index.mjs'
7
+ import { personalWorkspaceGraph } from '../../projection/obsidian/personal-workspace.mjs'
5
8
  import { publishView } from '../../projection/obsidian/publication/publisher.mjs'
6
9
  import { recheckDisplacedFiles } from '../../projection/obsidian/recovery/late-writer.mjs'
7
10
  import { createRecoveryStore, readFileBytes } from '../../projection/obsidian/recovery/store.mjs'
@@ -31,6 +34,68 @@ export const DEFAULT_ELIGIBILITY = Object.freeze({
31
34
  isEligible: (node) => node.classification === 'classified',
32
35
  })
33
36
 
37
+ // A vault that is the person's own also shows the notes that carry no classification, when they decided so: the
38
+ // audience decision `only-you` with `unclassified: 'shown'`, which no other audience decision may carry. Those notes
39
+ // are the person's own files, in a vault only they see. One is admitted only when its bytes read as a note (the byte
40
+ // lens the emitter reads it with), so a file the emitter would refuse never stops the vault: it stays withheld, as does
41
+ // one that cannot be read. A note whose labels are unknown is never admitted: front matter Atelier could not read
42
+ // (`malformed-frontmatter`: a block scalar, a wrapped value), or any top-level `kg` key that is not a
43
+ // block, may carry an audience such as `sensitive` that "only you" leaves out. Only a note with no front matter, or
44
+ // front matter of plain top-level keys at column 0 none of which is `kg` (plainTopLevelKeys) and in which the graph
45
+ // reads no `kg` either, is the person's plain note. Assets follow the documents that
46
+ // embed them, as always.
47
+ export function onlyYouEligibility({ project }) {
48
+ const roots = new Map((project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string').map((repo) => [repo.name, repo.path]))
49
+ const readsAsNote = (node) => {
50
+ const root = roots.get(node.repo)
51
+ if (root === undefined || node.extension !== 'md' || typeof node.path !== 'string') return false
52
+ let bytes
53
+ let lens
54
+ try { bytes = readFileBytes(path.join(root, ...node.path.split('/'))); lens = readMarkdownLens(bytes) } catch { return false }
55
+ // Read now, not as the graph saw it: a file that gained front matter since is judged by what it holds now.
56
+ if (lens.frontmatter === null) return node.classificationReason === 'absent-frontmatter'
57
+ if (node.classificationReason !== 'missing-kg-block') return false
58
+ const text = bytes.toString('utf8')
59
+ const block = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
60
+ if (block === null || !plainTopLevelKeys(block[1])) return false
61
+ // A second layer: the graph's own reading of the front matter has no kg key either.
62
+ try { return !('kg' in markdownMetadata(text)) } catch { return false }
63
+ }
64
+ return Object.freeze({
65
+ revision: () => 'classified-documents/v2+unclassified-notes-read-as-notes-for-only-you/v4+assets-embedded-by-eligible-documents/v1',
66
+ isEligible: (node) => node.classification === 'classified' || (node.classification === 'unclassified' && readsAsNote(node)),
67
+ })
68
+ }
69
+
70
+ // Whether front matter is plain `key: value` lines at its top level, none of them `kg`: every line that is not blank,
71
+ // a comment, or indented (a nested value or a continuation) starts with a bare key. A quoted key, a flow or JSON
72
+ // mapping, a complex key (`? `), a top-level list or anything else unusual is not plain, so a label it may hold (a
73
+ // `kg.audience: sensitive` Atelier did not read) keeps the note withheld. An allow-list, never a pattern to find.
74
+ const PLAIN_KEY = /^([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:(?:[ \t]|$)/
75
+ function plainTopLevelKeys(frontmatter) {
76
+ // Line breaks YAML 1.1 readers honour and this reading does not (NEL, LS, PS) could hide a kg from it.
77
+ if (/[\u0085\u2028\u2029]/.test(frontmatter)) return false
78
+ let rooted = false
79
+ for (const line of frontmatter.split(/\r?\n/)) {
80
+ const trimmed = line.trim()
81
+ if (trimmed === '' || trimmed.startsWith('#')) continue
82
+ // An indented line is a nested value or a continuation only below a key at column 0; YAML also lets a whole root
83
+ // mapping be indented, and then its keys (kg among them) are indented too.
84
+ if (/^[ \t]/.test(line)) { if (!rooted) return false; continue }
85
+ const key = PLAIN_KEY.exec(line)
86
+ if (key === null || key[1] === 'kg') return false
87
+ rooted = true
88
+ }
89
+ return true
90
+ }
91
+
92
+ // The eligibility a workspace's machine settings decide: the one above only for "only you" with unclassified notes
93
+ // shown, and the default, which withholds them, for every other decision and for none.
94
+ export function eligibilityFor({ machine, project }) {
95
+ const audience = machine?.decisions?.audience
96
+ return audience?.choice === 'only-you' && audience.unclassified === 'shown' ? onlyYouEligibility({ project }) : DEFAULT_ELIGIBILITY
97
+ }
98
+
34
99
  export function assetEligibilityFor({ graph, eligibility }) {
35
100
  const eligibleSources = new Set(graph.nodes.filter((node) => eligibility.isEligible(node) === true).map((node) => node.id))
36
101
  const embeds = Array.isArray(graph.embeds) ? graph.embeds : []
@@ -59,9 +124,47 @@ export function profileFor({ project, workspaceId, audienceAllow }) {
59
124
  // `index` is the engine's observation index. With both, a source whose observed digest is the cached one is not
60
125
  // opened: observation already decided by digest that it did not change, under the stat-hint bound observation
61
126
  // documents, and a full reconciliation hashes it again. Without an index every source is read and hashed here.
62
- export function buildGraph({ project, eligibility, cache = null, index = null }) {
63
- const observedDigest = cache && index ? (repoId, relative) => index.get(sourceKey(repoId, relative))?.digest ?? null : null
64
- const canonical = buildCanonicalGraph(project, { fileCache: cache, observedDigest })
127
+ // A project bound to a personal workspace (personal-workspace.mjs) is built by that route, for every caller, with the
128
+ // same caches: the composition's graph, served only under a validity key the composition confirmed. With
129
+ // `deferPersonalValidation` (the engine), an unconfirmed key refuses `personal-validation-pending` instead of composing
130
+ // on this thread.
131
+ export function buildGraph({ project, eligibility, cache = null, index = null, deferPersonalValidation = false }) {
132
+ const observedDigest = observedDigestOf(cache, index)
133
+ const canonical = personalWorkspaceGraph(project, { fileCache: cache, observedDigest, defer: deferPersonalValidation }) ?? buildCanonicalGraph(project, { fileCache: cache, observedDigest })
134
+ return eligibleGraph(canonical, eligibility)
135
+ }
136
+
137
+ // The same graph, built with the event loop given a turn between the Markdown sources of the census, under the
138
+ // caller's cooperative budget (`scheduling`: createCooperativeBudget's options, its guard checked at every wait). For
139
+ // the maintenance engine's tick: a first tick reads every source, and a large corpus would hold the service's
140
+ // listener for the whole census. A project bound to a personal workspace is built by its own route, in one stretch,
141
+ // as buildGraph builds it.
142
+ export async function buildGraphCooperatively({ project, eligibility, cache = null, index = null, deferPersonalValidation = false, scheduling = {} }) {
143
+ const observedDigest = observedDigestOf(cache, index)
144
+ // The personal route answers for a bound project, and refuses a project that carries the reserved key unbound.
145
+ const personal = personalWorkspaceGraph(project, { fileCache: cache, observedDigest, defer: deferPersonalValidation })
146
+ if (personal !== null) return eligibleGraph(personal, eligibility)
147
+ const budget = createCooperativeBudget(scheduling)
148
+ budget.check()
149
+ const steps = buildCanonicalGraphSteps(project, { fileCache: cache, observedDigest })
150
+ let canonical
151
+ try {
152
+ for (;;) {
153
+ const step = steps.next()
154
+ if (step.done) { canonical = step.value; break }
155
+ await budget.checkpoint(step.value)
156
+ }
157
+ } catch (error) {
158
+ steps.return?.()
159
+ throw error
160
+ }
161
+ budget.finish('graph-assembled')
162
+ return eligibleGraph(canonical, eligibility)
163
+ }
164
+
165
+ const observedDigestOf = (cache, index) => (cache && index ? (repoId, relative) => index.get(sourceKey(repoId, relative))?.digest ?? null : null)
166
+
167
+ function eligibleGraph(canonical, eligibility) {
65
168
  if (!canonical.ok) refuse('canonical-graph-invalid', 'the canonical graph has errors; no view is prepared from it', { errorCount: canonical.errors.length })
66
169
  return withEligibility(canonical, eligibility.isEligible, assetEligibilityFor({ graph: canonical, eligibility }))
67
170
  }
@@ -73,6 +176,12 @@ export function buildGraph({ project, eligibility, cache = null, index = null })
73
176
  // its pinned digest is unchanged and the cached bytes are the ones that digest
74
177
  // describes, so bytes that drift under an unchanged pin are not consulted
75
178
  // until observation hashes the file again (the same bound as the graph stage).
179
+ // The engine keeps that bound when it prepares cooperatively. Called on its
180
+ // own, prepareViewCooperatively waits between notes and so reads every
181
+ // selected source against its pin once more before it returns. The engine
182
+ // turns that second reading off and checks the selected sources itself, after
183
+ // the preparation and again before the commit, as observation does: by stat
184
+ // hint, and by digest only where the hint moved.
76
185
  export function captureSnapshot({ project, graph, workspaceId, index, configDigest, capturedAt }) {
77
186
  const roots = new Map((project.repos ?? []).filter((repo) => !repo.external).map((repo) => [repo.name, repo.path]))
78
187
  const absolute = (repoId, relative) => {
@@ -118,5 +227,5 @@ export function captureSnapshot({ project, graph, workspaceId, index, configDige
118
227
  // that note and reuses the rest. Both caches are derived, in-memory state: a
119
228
  // test may replace either seam with `() => null` to build or prepare in full.
120
229
  export function createProductionSeams() {
121
- return { buildGraph, captureSnapshot, profileFor, prepareView, publishView, createRecoveryStore, recheckDisplacedFiles, createGraphCache: createGraphFileCache, createPreparationCache }
230
+ return { buildGraph, buildGraphCooperatively, captureSnapshot, profileFor, prepareView, prepareViewCooperatively, publishView, createRecoveryStore, recheckDisplacedFiles, createGraphCache: createGraphFileCache, createPreparationCache }
122
231
  }
@@ -2,6 +2,7 @@ import { createHash, randomBytes as cryptoRandomBytes, timingSafeEqual } from 'n
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow, realPathAsStored } from '../../project/private-state.mjs'
5
+ import { assertAllocatedFolder, vaultRootFor } from '../../projection/obsidian/recovery/store.mjs'
5
6
  import {
6
7
  PLUGIN_BEARER, PLUGIN_CHALLENGE_WINDOW_MS, PLUGIN_CHANNEL_PROTOCOL, PLUGIN_HANDSHAKE_TTL_MS, PLUGIN_LEASE_TTL_MS, PLUGIN_MAX_PENDING_HANDSHAKES_PER_SCOPE, PLUGIN_MAX_SESSION_AGE_MS, PLUGIN_MAX_SESSIONS_PER_SCOPE,
7
8
  PLUGIN_RENEW_INTERVAL_MS, PLUGIN_STATUS_SCHEMA, pluginClientProof, pluginKeyHint, pluginRequestMac, pluginResponseMac, pluginServerProof, pluginSessionKey, pluginVaultProof,
@@ -158,9 +159,16 @@ export function createPluginChannelForOracleTests({
158
159
  onSessionOpened = () => {},
159
160
  }, primitives = PLUGIN_CHANNEL_PRIMITIVES) {
160
161
  const rules = { ...PLUGIN_CHANNEL_PRIMITIVES, ...primitives }
161
- // As the file system stores the path, as the store registers the vault in the app (realPathAsStored): a data root given
162
- // in another letter case names the same vault.
163
- const vaultRootOf = (scopeId) => { try { return realPathAsStored(path.join(workspaceRoot, 'vaults', segment(scopeId))) } catch { return null } }
162
+ // The view's vault wherever it is (vaultRootFor: allocated for the view, else under the data root), and an allocated one
163
+ // only while it is the folder recorded; as the file system stores the path, as the store registers the vault in the app
164
+ // (realPathAsStored): a data root given in another letter case names the same vault.
165
+ const vaultRootOf = (scopeId) => {
166
+ try {
167
+ const found = vaultRootFor({ workspaceRoot, workspaceId, scopeId })
168
+ if (found.allocation !== null) assertAllocatedFolder(found.allocation)
169
+ return realPathAsStored(found.path)
170
+ } catch { return null }
171
+ }
164
172
  const keyDigestOf = (bearer) => createHash('sha256').update(bearer, 'utf8').digest()
165
173
  const handshakes = new Map()
166
174
  const pruneHandshakes = () => { const at = now(); for (const [handshakeId, handshake] of handshakes) if (handshake.expiresAt <= at) handshakes.delete(handshakeId) }
@@ -4,6 +4,7 @@ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularF
4
4
  import { PLUGIN_ID } from '../../projection/obsidian/plugin-bridge/channel.mjs'
5
5
  import { COMMUNITY_PLUGINS_PATH } from '../../projection/obsidian/materialize/settings.mjs'
6
6
  import { sha256Digest } from '../../projection/obsidian/materialize/byte-lens.mjs'
7
+ import { vaultRootFor } from '../../projection/obsidian/recovery/store.mjs'
7
8
  import { canonicalJson, isPlainObject, isoTime } from './documents.mjs'
8
9
 
9
10
  // Whether the person wants Atelier's plugin in one vault, as the vault itself
@@ -42,8 +43,14 @@ const MAX_SETTINGS_BYTES = 256 * 1024
42
43
  const segment = (identifier) => identifier.replaceAll(':', '_')
43
44
 
44
45
  export const pluginChoiceDirectory = (workspaceRoot) => path.join(workspaceRoot, 'state', 'plugin', 'choices')
45
- // Where the recovery store places a view's vault.
46
- export const viewVaultRoot = (workspaceRoot, scopeId) => path.join(workspaceRoot, 'vaults', segment(scopeId))
46
+ // Where a view's vault is, as the recovery store finds it (vaultRootFor): the folder allocated to the view where the
47
+ // workspace decided its vaults live, else its folder under the data root. A record that cannot be read, or was lost,
48
+ // refuses, typed. Pass `workspaceId`; every caller here does. The two-argument form of earlier releases still works:
49
+ // it takes the name of the workspace's private-state folder as the id, which a record of another workspace (or a
50
+ // folder reached through a link) does not match, and such a record refuses, typed, rather than answering another vault.
51
+ export function viewVaultRoot(workspaceRoot, scopeId, workspaceId = path.basename(String(workspaceRoot))) {
52
+ return vaultRootFor({ workspaceRoot, workspaceId, scopeId }).path
53
+ }
47
54
  const choiceFile = (workspaceRoot, scopeId) => path.join(pluginChoiceDirectory(workspaceRoot), `${segment(scopeId)}.json`)
48
55
 
49
56
  function validChoice(document, { workspaceId, scopeId }) {
@@ -142,7 +149,10 @@ export function decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultR
142
149
  // `pending` (the view's next preparation records it).
143
150
  export function currentPluginChoice({ workspaceRoot, workspaceId, scopeId }) {
144
151
  const recorded = readPluginChoice({ workspaceRoot, workspaceId, scopeId })
145
- const change = changeShown(recorded, readCommunityEntry(viewVaultRoot(workspaceRoot, scopeId)).entry)
152
+ // A view whose vault cannot be found (its record cannot be read) shows what was recorded; its view says why.
153
+ let vaultRoot
154
+ try { vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId) } catch (error) { if (typeof error?.code !== 'string') throw error; return recorded }
155
+ const change = changeShown(recorded, readCommunityEntry(vaultRoot).entry)
146
156
  return change === null ? recorded : { ...change, since: null, pending: true }
147
157
  }
148
158
 
@@ -150,7 +160,9 @@ export function currentPluginChoice({ workspaceRoot, workspaceId, scopeId }) {
150
160
  // no app ran, it is confirmed: an app reads the list, entry and all, when it
151
161
  // next opens the vault. Published through a running app, it is only offered:
152
162
  // that app may not have read it, and the plugin running there confirms it
153
- // (confirmPluginSeen).
163
+ // (confirmPluginSeen). So is one published into a vault no Obsidian listed
164
+ // while one ran (`direct-unheld`): the evidence covers only the lists Atelier
165
+ // can read, not another profile's or another machine's.
154
166
  const IN_PLACE = new Set(['policy-satisfied', 'created', 'published', 'published-external-captured', 'already-current'])
155
167
  export function confirmPluginEntry({ workspaceRoot, workspaceId, scopeId, result, clock }) {
156
168
  const unit = (result?.notes ?? []).find((entry) => entry.path === COMMUNITY_PLUGINS_PATH)
@@ -18,6 +18,10 @@ import { viewVaultRoot } from './plugin-choice.mjs'
18
18
  // where a file goes) is asked about once, not at every tick; a change to it is
19
19
  // asked about again. Where the plugin is turned off in the vault, a pinned
20
20
  // file that is gone is no drift: the publisher never makes one again.
21
+ // `settle(scopeIds)` looks again after each tick, and takes as seen what a
22
+ // publication of that tick could not change, so a drift publishing cannot
23
+ // repair is not published a second time at the next look (at a service's
24
+ // start, say).
21
25
 
22
26
  const segment = (identifier) => identifier.replaceAll(':', '_')
23
27
  const TURNED_OFF = 'turned-off-in-this-vault'
@@ -33,7 +37,7 @@ function pinnedPlugin(workspaceRoot, workspaceId, scopeId) {
33
37
  if (sha256Digest(bytes) !== pointer.manifestDigest) return null
34
38
  const owned = JSON.parse(bytes.toString('utf8'))?.ext?.[OBSIDIAN_EXT_KEY]?.settings?.pluginOwned
35
39
  if (!Array.isArray(owned?.files)) return null
36
- return { files: owned.files.filter((file) => typeof file?.path === 'string' && typeof file?.digest === 'string'), off: owned.withheldBecause === TURNED_OFF }
40
+ return { manifest: pointer.manifestDigest, files: owned.files.filter((file) => typeof file?.path === 'string' && typeof file?.digest === 'string'), off: owned.withheldBecause === TURNED_OFF }
37
41
  } catch {
38
42
  return null
39
43
  }
@@ -53,24 +57,68 @@ function onDisk(vaultRoot, relativePath) {
53
57
  try { return readFileDigest(path.join(vaultRoot, relativePath)) ?? 'absent' } catch { return 'unreadable' }
54
58
  }
55
59
 
60
+ // A look, as it is compared: the vault root, and each pinned file with its pin and what the disk holds.
61
+ const signatureOf = (record) => JSON.stringify([record.root, record.files.map((file) => file.digest), record.files.map((file) => file.state)])
62
+ // Never what a look finds on disk: a path recorded so is asked about at the next look whatever it holds then.
63
+ const NOT_SEEN = 'not-seen'
64
+
56
65
  export function createPluginDriftObserver({ workspaceRoot, workspaceId }) {
66
+ // Per view: the last look ({ root, files: [{ path, digest, state }], manifest }).
57
67
  const looked = new Map()
68
+ // The views the last observe asked to be prepared again: the tick that follows publishes them.
69
+ let asked = new Set()
70
+ // What the pinned plugin files of a view look like on disk now; null when its committed generation pins none.
71
+ const look = (scopeId) => {
72
+ const pinned = pinnedPlugin(workspaceRoot, workspaceId, scopeId)
73
+ if (pinned === null || pinned.files.length === 0) return null
74
+ // The view's vault wherever it is; one that cannot be found is refused by the engine, not observed here.
75
+ let vaultRoot
76
+ try { vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId) } catch (error) { if (typeof error?.code !== 'string') throw error; return null }
77
+ let root = 'absent'
78
+ try { const stat = fs.lstatSync(vaultRoot); root = stat.isSymbolicLink() ? 'link' : (stat.mode & 0o777).toString(8) } catch { root = 'absent' }
79
+ const files = pinned.files.map((file) => ({ path: file.path, digest: file.digest, state: onDisk(vaultRoot, file.path) }))
80
+ const drifted = (file) => file.state !== file.digest && !(pinned.off && file.state === 'absent')
81
+ return { root, files, manifest: pinned.manifest, drifted, differs: files.some(drifted) }
82
+ }
58
83
  return {
59
84
  observe(scopeIds) {
60
85
  const drifted = []
61
86
  for (const scopeId of scopeIds) {
62
- const pinned = pinnedPlugin(workspaceRoot, workspaceId, scopeId)
63
- if (pinned === null || pinned.files.length === 0) { looked.delete(scopeId); continue }
64
- const vaultRoot = viewVaultRoot(workspaceRoot, scopeId)
65
- let root = 'absent'
66
- try { const stat = fs.lstatSync(vaultRoot); root = stat.isSymbolicLink() ? 'link' : (stat.mode & 0o777).toString(8) } catch { root = 'absent' }
67
- const disk = pinned.files.map((file) => onDisk(vaultRoot, file.path))
68
- const differs = disk.some((found, index) => found !== pinned.files[index].digest && !(pinned.off && found === 'absent'))
69
- const signature = JSON.stringify([root, pinned.files.map((file) => file.digest), disk])
70
- if (differs && looked.get(scopeId) !== signature) drifted.push(scopeId)
71
- looked.set(scopeId, signature)
87
+ const seen = look(scopeId)
88
+ if (seen === null) { looked.delete(scopeId); continue }
89
+ const last = looked.get(scopeId)
90
+ if (seen.differs && (last === undefined || signatureOf(last) !== signatureOf(seen))) drifted.push(scopeId)
91
+ looked.set(scopeId, { root: seen.root, files: seen.files, manifest: seen.manifest })
72
92
  }
93
+ asked = new Set(drifted)
73
94
  return drifted
74
95
  },
96
+ // After a tick. A view this tick published (one the last observe asked for, one whose committed generation
97
+ // changed since the last look, or one never looked at before) had its plugin files written or left for the person
98
+ // by its publisher. Of what the disk shows now, only what that publication could not change is taken as seen,
99
+ // without asking: a file as it pins it, or one that holds what it held before the tick under the same pin and the
100
+ // same vault root. So a drift publishing cannot repair (the data file of a vault root that is a link, say) is not
101
+ // asked about again once it was asked about under its pin. Anything else is not taken as seen, and the next look
102
+ // asks about it: a file another writer removed or changed after the publication, while the tick went on, or
103
+ // brought back to what it held under a pin the tick replaced; a file the publication left for the person, which
104
+ // the person may have repaired before the tick ended; and any file, when the vault root changed during the tick.
105
+ // Asking about a file that is still left writes no vault file: the publisher does not publish a committed
106
+ // generation again for it (pluginFilesDrifted). Any other view keeps its last look.
107
+ settle(scopeIds) {
108
+ for (const scopeId of scopeIds) {
109
+ const seen = look(scopeId)
110
+ if (seen === null) { looked.delete(scopeId); continue }
111
+ const last = looked.get(scopeId)
112
+ if (!asked.has(scopeId) && last !== undefined && last.manifest === seen.manifest) continue
113
+ // Keyed by path and pin: what a file held under a pin this tick replaced says nothing about the new one.
114
+ const key = (file) => `${file.path}\u0000${file.digest}`
115
+ const before = new Map((last?.files ?? []).map((file) => [key(file), file.state]))
116
+ // And only under the same vault root: a root made private during the tick changes what the publisher leaves.
117
+ const sameRoot = last !== undefined && last.root === seen.root
118
+ const files = seen.files.map((file) => (!seen.drifted(file) || (sameRoot && before.get(key(file)) === file.state) ? file : { ...file, state: NOT_SEEN }))
119
+ looked.set(scopeId, { root: seen.root, files, manifest: seen.manifest })
120
+ }
121
+ asked = new Set()
122
+ },
75
123
  }
76
124
  }
@@ -9,6 +9,18 @@ export const PLUGIN_PRESENCE_REASONS = Object.freeze(['live-lease', 'no-live-lea
9
9
  // The two ways back, for a view whose vault turned the plugin off.
10
10
  export const turnPluginOnNext = (scopeId) => `turn Atelier on in Obsidian (Settings, Community plugins), or run \`atelier obsidian plugin on --scope ${scopeId}\`, which restores it at the view's next publication`
11
11
 
12
+ // A view kept `current` while a plugin file its committed generation pins waits for an app that qualifies: the notes
13
+ // are verified, the plugin file (often `data.json`) is not written yet (engine.mjs). Its freshness reason says so.
14
+ export const PLUGIN_FILES_WAIT_FOR_APP = 'verified-by-read-back-plugin-waits-for-app'
15
+ export const PLUGIN_FILES_WAIT_NEXT = 'the view\'s notes are current, but a file of Atelier\'s plugin in this vault is not written yet, since Obsidian could not be coordinated with (it runs without its command line answering, say): it is written once Obsidian answers its command line or is quit, and it is retried automatically'
16
+
17
+ // The presence, with the plugin files that wait for the app when the view's freshness says so (`files`, and the next
18
+ // step unless the presence has its own).
19
+ export function withPluginFilesPending(presence, freshness) {
20
+ if (freshness?.state !== 'current' || freshness.reason !== PLUGIN_FILES_WAIT_FOR_APP) return presence
21
+ return { ...presence, files: 'waits-for-app', next: presence.next ?? PLUGIN_FILES_WAIT_NEXT }
22
+ }
23
+
12
24
  export function pluginPresenceOf(statusDocument, scopeId) {
13
25
  if (!isPlainObject(statusDocument)) return { present: false, reason: 'service-not-running' }
14
26
  const scopes = isPlainObject(statusDocument.plugins) && Array.isArray(statusDocument.plugins.scopes) ? statusDocument.plugins.scopes : null
@@ -40,7 +52,8 @@ export function withPluginReportedVersion(appProbe, readPresence, { maxAgeMs = 1
40
52
  return {
41
53
  async inspect() {
42
54
  const [observation, presence] = await Promise.all([appProbe.inspect(), presenceNow()])
43
- if (presence?.present !== true || observation?.installed !== true || observation?.cli !== true || observation.running === false || typeof observation.version === 'string') return observation
55
+ // A command line that answered it is turned off stays that answer: a plugin's version never hides it.
56
+ if (presence?.present !== true || observation?.installed !== true || observation?.cli !== true || observation.running === false || typeof observation.version === 'string' || observation.cliOff === true) return observation
44
57
  return { ...observation, running: true, version: presence.appVersion, noVaultOpen: false, versionSource: 'plugin' }
45
58
  },
46
59
  vaultState: (input) => appProbe.vaultState(input),
@@ -89,6 +89,20 @@ function listGenerations(directory) {
89
89
 
90
90
  const ticketName = (generation, kind) => `${String(generation).padStart(12, '0')}.${kind}`
91
91
 
92
+ // A ticket is removed only while it holds the given bytes: one that holds others is another holder's. Answers whether
93
+ // nothing of these bytes is left at the path (removed now, or gone already); throws what reading or removing it threw.
94
+ function removeIfStill(file, body) {
95
+ let current
96
+ try { current = readRegularTextNoFollow(file) } catch (error) { if (error.code === 'ENOENT') return true; throw error }
97
+ if (current !== body) return true
98
+ fs.unlinkSync(file)
99
+ return true
100
+ }
101
+
102
+ // Tickets this process published and could not remove after its acquisition failed, by lock directory. Each is
103
+ // removed at this process's next acquisition in that directory, before the directory is looked at.
104
+ const UNWITHDRAWN = new Map()
105
+
92
106
  // Read-only. { available, reason, generation, ticket }
93
107
  export async function inspectPrivateGenerationLock({ directory, workspaceId, proveAbandoned = createAbandonmentProof(), nowMs = Date.now() }) {
94
108
  const { names, highest, unknown } = listGenerations(directory)
@@ -116,6 +130,11 @@ export async function inspectPrivateGenerationLock({ directory, workspaceId, pro
116
130
  // Never waits and never throws for a lock that is merely held.
117
131
  export async function acquirePrivateGenerationLock({ workspaceRoot, directory, workspaceId, purpose, service = null, clock, proveAbandoned, randomBytes = cryptoRandomBytes }) {
118
132
  const contained = ensureContainedPrivateDirectory({ workspaceRoot, directory, label: 'private lock' })
133
+ const owed = UNWITHDRAWN.get(contained)
134
+ if (owed !== undefined) {
135
+ // Still failing: the ticket stays, and this acquisition finds it as held by this process, as before.
136
+ try { if (removeIfStill(owed.file, owed.body)) UNWITHDRAWN.delete(contained) } catch { /* tried again at the next acquisition */ }
137
+ }
119
138
  const acquiredAt = isoTime(clock)
120
139
  const status = await inspectPrivateGenerationLock({ directory: contained, workspaceId, proveAbandoned, nowMs: Date.parse(acquiredAt) })
121
140
  const holder = status.ticket ? { pid: status.ticket.pid, purpose: status.ticket.purpose, runtimeId: status.ticket.service?.runtimeId ?? null } : null
@@ -125,12 +144,25 @@ export async function acquirePrivateGenerationLock({ workspaceRoot, directory, w
125
144
  const ticket = validateTicket({ schema: LOCK_TICKET_SCHEMA, workspaceId, purpose, pid: process.pid, machine: machineDigest(), nonce: randomBytes(16).toString('hex'), acquiredAt, service }, workspaceId)
126
145
  const body = JSON.stringify(ticket)
127
146
  const file = path.join(contained, ticketName(generation, 'json'))
147
+ // A ticket published and then not handed to its caller, because a later step failed (the directory sync or the
148
+ // read-back of the publication, the listing that confirms it), is removed while it still holds these bytes: nobody
149
+ // would release it, and every later acquisition by this process would find it held by this process. One that does
150
+ // not hold them is another holder's and is left alone. A removal that fails too (the same fault, say) is owed, and
151
+ // made at the next acquisition in this directory.
152
+ const withdraw = () => {
153
+ try { removeIfStill(file, body) } catch { UNWITHDRAWN.set(contained, { file, body }) }
154
+ }
128
155
  try { publishPrivateFile(file, body) } catch (error) {
129
156
  if (error.code === 'EEXIST') return { acquired: false, reason: 'lost-the-race-for-this-generation', holder: null }
157
+ withdraw()
130
158
  throw error
131
159
  }
132
160
  // Confirm: a decision made on an old listing finds the newer ticket here and withdraws.
133
- const after = listGenerations(contained)
161
+ let after
162
+ try { after = listGenerations(contained) } catch (error) {
163
+ withdraw()
164
+ throw error
165
+ }
134
166
  if (after.highest !== generation) {
135
167
  try { fs.unlinkSync(file) } catch { /* a lower generation is ignored and pruned by the next holder */ }
136
168
  return { acquired: false, reason: 'a-newer-holder-exists', holder: null }
@@ -138,18 +170,21 @@ export async function acquirePrivateGenerationLock({ workspaceRoot, directory, w
138
170
  for (const name of after.names) {
139
171
  if (Number(TICKET_NAME.exec(name)[1]) < generation) try { fs.unlinkSync(path.join(contained, name)) } catch { /* pruned by somebody else */ }
140
172
  }
173
+ // Settled once the lock is given back, or found not to be ours any more. A release that throws (a full disk, an
174
+ // I/O error while the released ticket is written) settles nothing: the lock is still held, and calling release()
175
+ // again tries again. Writing the same released ticket a second time is harmless.
141
176
  let released = false
142
177
  return {
143
178
  acquired: true,
144
179
  generation,
145
180
  release() {
146
181
  if (released) return false
147
- released = true
148
182
  let current
149
- try { current = readRegularTextNoFollow(file) } catch (error) { if (error.code === 'ENOENT') return false; throw error }
183
+ try { current = readRegularTextNoFollow(file) } catch (error) { if (error.code === 'ENOENT') { released = true; return false } throw error }
150
184
  // Taken over after a proof of abandonment (this process was thought gone): nothing here is ours to release.
151
- if (current !== body) return false
185
+ if (current !== body) { released = true; return false }
152
186
  publishPrivateFile(path.join(contained, ticketName(generation, 'released')), body)
187
+ released = true
153
188
  return true
154
189
  },
155
190
  }
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process'
2
2
  import fs from 'node:fs'
3
+ import path from 'node:path'
3
4
  import { executableIdentity } from './service-record.mjs'
4
5
 
5
6
  // Whether a live PID provably runs the executable a service record names,
@@ -32,27 +33,36 @@ export function readProcessCommandLine(pid, { platform = process.platform, readF
32
33
  return null
33
34
  }
34
35
 
35
- // Pure. The command line must name the recorded entry module, and a runtime
36
- // identifier it carries must be the recorded one.
37
- export function commandLineNamesRecord(commandLine, record) {
38
- const entry = record?.executable?.path
39
- if (typeof entry !== 'string' || entry === '' || typeof record.runtimeId !== 'string') return false
36
+ // Pure. The command line must name the recorded entry module, by one of
37
+ // `entries` (its recorded path, and the path it was started by when that is
38
+ // another name for it), and a runtime identifier it carries must be the
39
+ // recorded one.
40
+ export function commandLineNamesRecord(commandLine, record, entries = [record?.executable?.path]) {
41
+ const names = entries.filter((entry) => typeof entry === 'string' && entry !== '')
42
+ if (names.length === 0 || typeof record?.runtimeId !== 'string') return false
40
43
  if (Array.isArray(commandLine)) {
41
- if (!commandLine.slice(1).includes(entry)) return false
44
+ if (!commandLine.slice(1).some((word) => names.includes(word))) return false
42
45
  const named = commandLine.filter((word) => word.startsWith(RUNTIME_ARGUMENT)).map((word) => word.slice(RUNTIME_ARGUMENT.length))
43
46
  return named.every((runtimeId) => runtimeId === record.runtimeId)
44
47
  }
45
48
  if (typeof commandLine !== 'string') return false
46
- if (!` ${commandLine} `.includes(` ${entry} `)) return false
49
+ if (!names.some((entry) => ` ${commandLine} `.includes(` ${entry} `))) return false
47
50
  const named = [...commandLine.matchAll(/(?:^|\s)--runtime-id=(\S+)/g)].map((match) => match[1])
48
51
  return named.every((runtimeId) => runtimeId === record.runtimeId)
49
52
  }
50
53
 
51
54
  // The recorded executable is still the bytes that were recorded, and the live
52
- // process was started on it.
53
- export function processRunsRecordedExecutable(record, { commandLineOf = readProcessCommandLine, identityOf = executableIdentity } = {}) {
55
+ // process was started on it: by its recorded path, or by the path the record
56
+ // says it was started by (a login item names the installed entry by its path),
57
+ // when that still leads to the recorded one.
58
+ export function processRunsRecordedExecutable(record, { commandLineOf = readProcessCommandLine, identityOf = executableIdentity, realpathOf = fs.realpathSync } = {}) {
54
59
  try {
55
60
  if (identityOf(record.executable.path).digest !== record.executable.digest) return false
56
61
  } catch { return false }
57
- return commandLineNamesRecord(commandLineOf(record.pid), record)
62
+ const entries = [record.executable.path]
63
+ const invokedAs = record.executable.ext?.invokedAs
64
+ if (typeof invokedAs === 'string' && path.isAbsolute(invokedAs)) {
65
+ try { if (realpathOf(invokedAs) === record.executable.path) entries.push(invokedAs) } catch { /* not another name for it */ }
66
+ }
67
+ return commandLineNamesRecord(commandLineOf(record.pid), record, entries)
58
68
  }