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

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 (236) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +80 -47
  3. package/SECURITY.md +1 -1
  4. package/TRADEMARKS.md +2 -2
  5. package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
  6. package/contracts/atelier-graph-view.v1.schema.json +1085 -0
  7. package/contracts/public-api-baseline.json +298 -19
  8. package/docs/README.md +90 -0
  9. package/docs/blocks/claims.md +2 -2
  10. package/docs/blocks/conformance-admission.md +1 -1
  11. package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
  12. package/docs/guides/co-authoring.md +113 -0
  13. package/docs/{distributions.md → guides/distributions.md} +13 -8
  14. package/docs/guides/extensions.md +189 -0
  15. package/docs/guides/graph-refresh.md +152 -0
  16. package/docs/guides/graph-workspace.md +76 -0
  17. package/docs/{ingestion.md → guides/ingestion.md} +13 -12
  18. package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
  19. package/docs/{learning.md → guides/learning.md} +12 -5
  20. package/docs/{obsidian.md → guides/obsidian.md} +134 -47
  21. package/docs/guides/ontology-guide.md +111 -0
  22. package/docs/{personal-workspace.md → guides/personal-workspace.md} +7 -4
  23. package/docs/{research-starter.md → guides/research-starter.md} +1 -1
  24. package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
  25. package/docs/{upgrade.md → guides/upgrade.md} +40 -9
  26. package/docs/knowledge-health/assessment/capabilities.md +6 -15
  27. package/docs/knowledge-health/facilitator.md +15 -4
  28. package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
  29. package/docs/knowledge-health/quickstart.md +97 -75
  30. package/docs/knowledge-health/readme.md +13 -5
  31. package/docs/maintainers/describing-the-atelier.md +173 -0
  32. package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -5
  33. package/docs/{announcements.md → reference/announcements.md} +1 -1
  34. package/docs/{architecture.md → reference/architecture.md} +31 -5
  35. package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
  36. package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
  37. package/docs/{coordination.md → reference/coordination.md} +1 -1
  38. package/docs/{decisions.md → reference/decisions.md} +3 -3
  39. package/docs/{design.md → reference/design.md} +1 -1
  40. package/docs/reference/edge-policy.md +151 -0
  41. package/docs/reference/graph-view-structured.md +119 -0
  42. package/docs/reference/graph-view.md +139 -0
  43. package/docs/reference/graph-workspace.md +37 -0
  44. package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
  45. package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
  46. package/docs/{local-services.md → reference/local-services.md} +1 -1
  47. package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +6 -4
  48. package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +5 -66
  49. package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
  50. package/docs/reference/portability.md +92 -0
  51. package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
  52. package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
  53. package/docs/{project-options.md → reference/project-options.md} +17 -0
  54. package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +27 -4
  55. package/docs/{review-portability.md → reference/review-portability.md} +4 -0
  56. package/docs/{template-profile.md → reference/template-profile.md} +1 -1
  57. package/docs/{trackables.md → reference/trackables.md} +2 -2
  58. package/docs/start/independent-start.md +89 -0
  59. package/docs/{install.md → start/install.md} +6 -6
  60. package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
  61. package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
  62. package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
  63. package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
  64. package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
  65. package/fixtures/graph-view/synthetic-view.v1.json +1 -0
  66. package/fixtures/knowledge-health/workshop/README.md +1 -1
  67. package/fixtures/templates/local-library.v1.json +3 -1
  68. package/package.json +21 -8
  69. package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
  70. package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
  71. package/skills/claude/atelier-local-service/SKILL.md +1 -1
  72. package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
  73. package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
  74. package/skills/claude/mnstry-readiness/SKILL.md +2 -2
  75. package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
  76. package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
  77. package/skills/codex/atelier-local-service/SKILL.md +1 -1
  78. package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
  79. package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
  80. package/skills/codex/mnstry-readiness/SKILL.md +2 -2
  81. package/src/architecture/catalog.mjs +2 -2
  82. package/src/attestation/sign.mjs +1 -1
  83. package/src/boundary/content-rules.mjs +5 -4
  84. package/src/boundary/policy.mjs +60 -12
  85. package/src/cli/run.mjs +24 -8
  86. package/src/commands/distribution.mjs +1 -1
  87. package/src/commands/extension.mjs +27 -0
  88. package/src/commands/ingest.mjs +1 -1
  89. package/src/commands/learn.mjs +1 -1
  90. package/src/commands/obsidian.mjs +28 -16
  91. package/src/commands/review.mjs +3 -0
  92. package/src/edge-policy/contract.mjs +69 -0
  93. package/src/edge-policy/policy.mjs +142 -0
  94. package/src/edge-policy/source-citation-adapter.mjs +43 -0
  95. package/src/extension-packs/loader.mjs +4 -3
  96. package/src/extensions/admission.mjs +83 -0
  97. package/src/extensions/client.d.mts +23 -0
  98. package/src/extensions/client.mjs +99 -0
  99. package/src/extensions/contracts.d.mts +29 -0
  100. package/src/extensions/contracts.mjs +163 -0
  101. package/src/extensions/convex.d.mts +24 -0
  102. package/src/extensions/convex.mjs +189 -0
  103. package/src/extensions/delegation.mjs +54 -0
  104. package/src/extensions/drizzle.d.mts +11 -0
  105. package/src/extensions/drizzle.mjs +33 -0
  106. package/src/extensions/postgres.d.mts +7 -0
  107. package/src/extensions/postgres.mjs +66 -0
  108. package/src/extensions/prisma.d.mts +13 -0
  109. package/src/extensions/prisma.mjs +63 -0
  110. package/src/extensions/project.mjs +432 -0
  111. package/src/extensions/scaffold-convex.mjs +130 -0
  112. package/src/extensions/scaffold-sql.mjs +144 -0
  113. package/src/extensions/server.d.mts +20 -0
  114. package/src/extensions/server.mjs +102 -0
  115. package/src/extensions/sql-scope.mjs +22 -0
  116. package/src/graph/knowledge-graph.mjs +1 -1
  117. package/src/graph/repo-contribution.mjs +405 -0
  118. package/src/graph/repo-contribution.types.mjs +32 -0
  119. package/src/graph-access/edge-decision-adapter.mjs +41 -0
  120. package/src/graph-access/edge-record-adapter.mjs +73 -0
  121. package/src/graph-access/index.mjs +253 -0
  122. package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
  123. package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
  124. package/src/graph-access/source-adapter.mjs +81 -0
  125. package/src/graph-refresh/compare.mjs +110 -0
  126. package/src/graph-refresh/controller.mjs +324 -0
  127. package/src/graph-refresh/index.mjs +3 -0
  128. package/src/graph-refresh/local.mjs +78 -0
  129. package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
  130. package/src/graph-view/babylon/index.mjs +2 -0
  131. package/src/graph-view/babylon/navigator.mjs +312 -0
  132. package/src/graph-view/babylon/provenance.mjs +36 -0
  133. package/src/graph-view/babylon/scene-controller.mjs +131 -0
  134. package/src/graph-view/babylon/semantic-picking.mjs +35 -0
  135. package/src/graph-view/babylon/web-host.mjs +188 -0
  136. package/src/graph-view/cli.mjs +3 -0
  137. package/src/graph-view/command.mjs +34 -0
  138. package/src/graph-view/contracts.mjs +79 -0
  139. package/src/graph-view/errors.mjs +5 -0
  140. package/src/graph-view/index.mjs +4 -0
  141. package/src/graph-view/intents.mjs +30 -0
  142. package/src/graph-view/query.mjs +213 -0
  143. package/src/graph-view/refs.mjs +2 -0
  144. package/src/graph-view/structured/browser.mjs +262 -0
  145. package/src/graph-view/structured/styles.mjs +8 -0
  146. package/src/graph-workspace/authoring.mjs +185 -0
  147. package/src/graph-workspace/browser/dom.mjs +23 -0
  148. package/src/graph-workspace/browser/index.mjs +6 -0
  149. package/src/graph-workspace/browser/map.mjs +374 -0
  150. package/src/graph-workspace/browser/review.mjs +55 -0
  151. package/src/graph-workspace/browser/styles.mjs +11 -0
  152. package/src/graph-workspace/browser/workbench.mjs +351 -0
  153. package/src/graph-workspace/browser/workspace.mjs +433 -0
  154. package/src/graph-workspace/disclosure.mjs +39 -0
  155. package/src/graph-workspace/geometry.mjs +78 -0
  156. package/src/graph-workspace/index.mjs +11 -0
  157. package/src/graph-workspace/labels.mjs +50 -0
  158. package/src/graph-workspace/model.mjs +109 -0
  159. package/src/graph-workspace/recipes.mjs +100 -0
  160. package/src/graph-workspace/scale.mjs +98 -0
  161. package/src/graph-workspace/theme.mjs +156 -0
  162. package/src/knowledge/ontology-guide.mjs +152 -0
  163. package/src/knowledge-health/proof/installed-check.mjs +2 -2
  164. package/src/knowledge-health/rule-identity.mjs +7 -7
  165. package/src/knowledge-health/workshop.mjs +22 -2
  166. package/src/personal-workspace/selection.mjs +17 -2
  167. package/src/project/config.mjs +1 -1
  168. package/src/project/providers/github.mjs +24 -0
  169. package/src/project/repo-identity.mjs +3 -22
  170. package/src/projection/obsidian/contracts.mjs +1 -1
  171. package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
  172. package/src/projection/obsidian/materialize/prepare-view.mjs +1 -1
  173. package/src/projection/obsidian/materialize/redaction.mjs +1 -1
  174. package/src/projection/obsidian/personal-workspace.mjs +24 -28
  175. package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
  176. package/src/projection/obsidian/publication/publisher.mjs +25 -4
  177. package/src/projection/project.mjs +18 -14
  178. package/src/readiness/readiness.mjs +20 -11
  179. package/src/readiness-protocols/bundled-pack.mjs +14 -766
  180. package/src/readiness-protocols/bundled-selection.mjs +32 -0
  181. package/src/readiness-protocols/evidence.mjs +2 -1
  182. package/src/readiness-protocols/packs/mnstry.mjs +766 -0
  183. package/src/readiness-protocols/runtime.mjs +43 -6
  184. package/src/runtime/obsidian/engine.mjs +99 -26
  185. package/src/runtime/obsidian/lifecycle.mjs +3 -3
  186. package/src/runtime/obsidian/login-item.mjs +2 -2
  187. package/src/runtime/obsidian/machine-settings.mjs +52 -8
  188. package/src/runtime/obsidian/opening.mjs +2 -2
  189. package/src/runtime/obsidian/personal-selection.mjs +88 -26
  190. package/src/runtime/obsidian/pipeline.mjs +75 -7
  191. package/src/runtime/obsidian/private-lock.mjs +11 -2
  192. package/src/runtime/obsidian/selection-continuity.mjs +124 -0
  193. package/src/runtime/obsidian/service-main.mjs +15 -9
  194. package/src/runtime/obsidian/service.mjs +14 -0
  195. package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
  196. package/src/runtime/obsidian/view-permission.mjs +2 -1
  197. package/src/upgrade/upgrade.mjs +23 -4
  198. package/templates/distribution-workspace/README.md +2 -2
  199. package/templates/external-project-workspace/atelier.project.json +3 -0
  200. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
  201. package/templates/knowledge-workspace/README.md +2 -2
  202. package/docs/integration-and-review-plan.md +0 -409
  203. package/docs/integration-contract-decisions.md +0 -44
  204. package/docs/knowledge-health/devday/facilitator.md +0 -16
  205. package/docs/knowledge-health/devday/public-default.md +0 -107
  206. package/docs/knowledge-health/devday/quickstart.md +0 -18
  207. package/docs/knowledge-health/devday/support.md +0 -22
  208. package/docs/knowledge-health/exercise.md +0 -17
  209. package/docs/knowledge-health/support.md +0 -34
  210. package/docs/presentation-review-disposition.md +0 -47
  211. package/docs/progressive-ingestion-acceptance.md +0 -89
  212. package/docs/progressive-ingestion.md +0 -235
  213. /package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +0 -0
  214. /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
  215. /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
  216. /package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +0 -0
  217. /package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +0 -0
  218. /package/docs/{local-review.md → guides/local-review.md} +0 -0
  219. /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
  220. /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
  221. /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
  222. /package/docs/{attestation.md → reference/attestation.md} +0 -0
  223. /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
  224. /package/docs/{continuity.md → reference/continuity.md} +0 -0
  225. /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
  226. /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
  227. /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
  228. /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
  229. /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
  230. /package/docs/{local-state.md → reference/local-state.md} +0 -0
  231. /package/docs/{ontology.md → reference/ontology.md} +0 -0
  232. /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
  233. /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
  234. /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
  235. /package/docs/{semantic-operation.md → reference/semantic-operation.md} +0 -0
  236. /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
@@ -33,7 +33,7 @@ await scenario('public-quickstart',async()=>{
33
33
  return {init,graph:shellBin(['graph'],workspace),project:shellBin(['project'],workspace),scope:'Exact macOS public quickstart command sequence, supported baseline only'};
34
34
  });
35
35
  await scenario('installed-document-references',async()=>{
36
- const references=['README.md','docs/knowledge-graph.md','docs/coauthor-session.md'];
36
+ const references=['README.md','docs/reference/knowledge-graph.md','docs/reference/coauthor-session.md'];
37
37
  const documents=references.map(relativePath=>{const file=fs.realpathSync(path.join(packageRoot,relativePath));assert.ok(file.startsWith(fs.realpathSync(packageRoot)+path.sep));const bytes=fs.readFileSync(file);return {relativePath,bytes:bytes.length,sha256:digest(bytes)};});
38
38
  for(const subpath of ['./graph','./project','./coauthor']){assert.ok(pkg.exports[subpath]);await import(pkg.name+'/'+subpath.slice(2));}
39
39
  return {documents,apiReferences:['@mnstry/atelier/graph','@mnstry/atelier/project','@mnstry/atelier/coauthor'],scope:'Public quickstart references resolve in the actual installed package'};
@@ -72,7 +72,7 @@ await scenario('installed-public-graph-project-baseline',async()=>{
72
72
  assert.ok(Array.isArray(graph.nodes));assert.ok(Array.isArray(graph.edges));assert.ok(Array.isArray(graph.errors));assert.equal(graph.errors.length,0);
73
73
  return {init,graphCommand,projectCommand,graph,agentCIOutput:'Actual machine-readable canonical graph; structural evidence only, no model/provider or semantic acceptance.'};
74
74
  });
75
- const docs=[];for(const candidate of ['README.md','docs/knowledge.md','docs/coauthor.md','docs/coauthor-session.md','docs/upgrade.md','docs/graph.md','docs/knowledge-graph.md','docs/release-engineering.md']){const full=path.join(packageRoot,candidate);if(fs.existsSync(full)){const b=fs.readFileSync(full);docs.push({path:candidate,sha256:digest(b),bytes:b.length});}}
75
+ const docs=[];for(const candidate of ['README.md','docs/guides/knowledge-workspace.md','docs/reference/coauthor-session.md','docs/guides/upgrade.md','docs/reference/knowledge-graph.md','docs/maintainers/release-engineering.md']){const full=path.join(packageRoot,candidate);if(fs.existsSync(full)){const b=fs.readFileSync(full);docs.push({path:candidate,sha256:digest(b),bytes:b.length});}}
76
76
  for(const id of ['assessment-to-guided-authoring','full-native-finding-reference','preview-owning-source-correction-readback-reassessment','keyboard-focus-undo','native-withdrawal-recovery','participant-usefulness'])gaps.push({id,owner:id==='participant-usefulness'?'Facilitator/participant':'Foundation and existing AW Integration/Journey',action:'Receive exact public Dev Day installed entry/bindings and native allocation; inherited native or fixture proof cannot satisfy installed or human evidence.'});
77
77
  const bindings=profile.deliveryBindings;
78
78
  if(bindings){
@@ -1,7 +1,7 @@
1
1
  export const ruleIdentity = Object.freeze({
2
2
  "profile": "source-qualified-required-direction/r1",
3
- "commit": "0a77cdc0f4f949d9aef29e3567303e2a6588a610",
4
- "tree": "4aaadc77690868921c1748e15079e0965e06faae",
3
+ "commit": "43a6d6a9d4718e735576bdd878cbea1f9b7e454a",
4
+ "tree": "306d92f8f3a36fa8938419bf8df32cb3b30f6704",
5
5
  "files": [
6
6
  {
7
7
  "path": "src/knowledge/plan.mjs",
@@ -24,8 +24,8 @@ export const ruleIdentity = Object.freeze({
24
24
  {
25
25
  "path": "src/graph/knowledge-graph.mjs",
26
26
  "bytes": 66377,
27
- "sha256": "fce2815bbc851451fca187082238a286f4796742b917229ec1b55af85d9e8eb0",
28
- "blob": "95cf2bf277c5a6599c0c95f046adea786cfc3f53"
27
+ "sha256": "ee62867895c7a8ab16295365120c2b4a69754066bf2258fc9156708d9b61bc96",
28
+ "blob": "fc6c543d08bef5de449709674122994cb1c51b75"
29
29
  },
30
30
  {
31
31
  "path": "templates/knowledge-workspace/knowledge-plan.schema.json",
@@ -42,8 +42,8 @@ export const ruleIdentity = Object.freeze({
42
42
  "note": "The supplier qualified against this earlier commit. The pinned plan, context and schema files are byte-identical there and at the source authority above; src/graph/graph.mjs and src/graph/knowledge-graph.mjs are not, and that qualification does not cover their current bytes."
43
43
  },
44
44
  "previousSourceAuthority": {
45
- "commit": "fcfdbe5ded672d41c717f49e0cfdf68b6060834d",
46
- "tree": "400e181cd0cb55976f3ef7ab1a1405531dacd521",
47
- "note": "Regenerated by the source owner for a changed candidate: the canonical graph build became a set of steps that a caller may run with waits between them (the census of Markdown sources), and buildKnowledgeGraph and buildCanonicalGraph run them through. Their results and errors are meant to be unchanged; the Knowledge Health tests pass on the current bytes."
45
+ "commit": "0a77cdc0f4f949d9aef29e3567303e2a6588a610",
46
+ "tree": "4aaadc77690868921c1748e15079e0965e06faae",
47
+ "note": "Regenerated by the source owner for a changed candidate: workspace link resolution in src/graph/knowledge-graph.mjs now recognises Markdown sources by the existing isMarkdownPath test, so links from uppercase .MD files resolve. One line changes; the file keeps its size. The other pinned files are byte-identical; the Knowledge Health tests pass on the current bytes."
48
48
  }
49
49
  })
@@ -28,12 +28,32 @@ function read(file) {
28
28
  } finally { fs.closeSync(fd) }
29
29
  }
30
30
 
31
+ // Every directory is created by this copy and every file is created exclusively, so an entry that arrived meanwhile
32
+ // refuses instead of being merged. Nothing is overwritten, and nothing unknown is cleaned up on failure.
33
+ function copyExclusive(source, target) {
34
+ const stat = fs.lstatSync(source)
35
+ if (stat.isDirectory()) {
36
+ fs.mkdirSync(target, { recursive: false })
37
+ for (const name of fs.readdirSync(source)) copyExclusive(path.join(source, name), path.join(target, name))
38
+ } else if (stat.isFile()) fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL)
39
+ else throw Error('The workshop fixture holds an entry that is neither a file nor a directory')
40
+ }
41
+
42
+ // Entries created inside these new directories after they were made are not ours either; refuse before Git sees them.
43
+ function assertExactCopy(source, target) {
44
+ const expected = fs.readdirSync(source).sort(), actual = fs.readdirSync(target).sort()
45
+ if (expected.length !== actual.length || expected.some((name, index) => name !== actual[index]))
46
+ throw Object.assign(Error('The new workshop holds entries this initialization did not create'), { code: 'EEXIST' })
47
+ for (const name of expected) if (fs.lstatSync(path.join(source, name)).isDirectory()) assertExactCopy(path.join(source, name), path.join(target, name))
48
+ }
49
+
31
50
  /** Copy the actual shipped public fixture into a NEW disposable workspace. */
32
51
  export function initializeKnowledgeHealthWorkshop(destination) {
33
52
  const root = path.resolve(destination)
34
53
  fs.mkdirSync(root, { recursive: false })
35
- fs.cpSync(fileURLToPath(new URL('../../fixtures/knowledge-health/workshop/', import.meta.url)), root,
36
- { recursive: true, force: false, errorOnExist: true })
54
+ const fixture = fileURLToPath(new URL('../../fixtures/knowledge-health/workshop/', import.meta.url))
55
+ for (const name of fs.readdirSync(fixture)) copyExclusive(path.join(fixture, name), path.join(root, name))
56
+ assertExactCopy(fixture, root)
37
57
  fs.writeFileSync(path.join(root, '.gitignore'), '.atelier-local/\noutput/\n', { flag: 'wx' })
38
58
  const git = spawnSync('git', ['init', '--quiet'], { cwd: root, encoding: 'utf8', timeout: 15000,
39
59
  env: { PATH: process.env.PATH, GIT_CONFIG_NOSYSTEM: '1',
@@ -19,6 +19,7 @@ const MAX_RECORD = 1024 * 1024
19
19
  const MAX_GENERATION_FILE = 8 * 1024 * 1024
20
20
  const MAX_RESTORE_RECORD = 4 * MAX_RECORD
21
21
  const ID = /^[0-9a-f]{64}$/
22
+ const CONFIRMATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
22
23
  // Every temporary name this module writes, including restore aside copies
23
24
  // (.restore-<uuid>-aside-<key>.json).
24
25
  const TEMPORARY = /^\.(selection|restore|validate)-[0-9a-f-]{36}(-[a-z]+)*\.json$/
@@ -136,7 +137,7 @@ function readHistory(personalHome) {
136
137
  let record
137
138
  try { record = JSON.parse(bytes) } catch { refuse('selection-history-corrupt') }
138
139
  if (record?.schema !== SELECTION_SCHEMA || record.sequence !== index + 1 || record.previous !== head || !ID.test(record.generationId ?? '') ||
139
- canonical(record) !== bytes.toString('utf8')) refuse('selection-history-corrupt')
140
+ (record.confirmationId !== undefined && !CONFIRMATION_ID.test(record.confirmationId)) || canonical(record) !== bytes.toString('utf8')) refuse('selection-history-corrupt')
140
141
  records.push(record)
141
142
  head = hash(bytes)
142
143
  files.push({ name, bytes: bytes.length, digest: head })
@@ -163,7 +164,7 @@ export function selectPersonalGeneration(options) {
163
164
  const { eligible, reason } = eligibility(personalHome, generationId)
164
165
  if (!eligible) refuse(reason)
165
166
  const dir = privateDir(personalHome, 'selections', true)
166
- const record = { schema: SELECTION_SCHEMA, sequence: records.length + 1, previous: head, generationId }
167
+ const record = { schema: SELECTION_SCHEMA, sequence: records.length + 1, previous: head, generationId, confirmationId: randomUUID() }
167
168
  const content = canonical(record)
168
169
  try { publishExclusive(dir, `${String(record.sequence).padStart(6, '0')}.json`, content, 'selection') }
169
170
  catch (error) { if (error.code === 'EEXIST') refuse('selection-concurrent'); throw error }
@@ -198,6 +199,20 @@ export function readPersonalSelectionHead(options) {
198
199
  }, 'personal-home-unavailable')
199
200
  }
200
201
 
202
+ // Package-internal snapshot for a host that keeps continuity across starts. The public head reader keeps its exact
203
+ // result. These identities come from the same verified history read, including every previously confirmed prefix.
204
+ export function readPersonalSelectionSnapshot(options) {
205
+ return safe(() => {
206
+ const { personalHome } = options ?? {}
207
+ privateHome(personalHome)
208
+ const { records, files, head } = readHistory(personalHome)
209
+ const current = records.at(-1) ?? null
210
+ return freeze({ selected: current?.generationId ?? null, sequence: current?.sequence ?? 0, head,
211
+ confirmationId: current?.confirmationId ?? null, recordDigests: files.map((file) => file.digest),
212
+ confirmationIds: records.flatMap((record) => record.confirmationId === undefined ? [] : [record.confirmationId]) })
213
+ }, 'personal-home-unavailable')
214
+ }
215
+
201
216
  // Lists entries without failing the whole inventory: a concurrently removed entry
202
217
  // is skipped; an unreadable, foreign or oversized one is reported without a digest.
203
218
  function listFiles(dir, keep) {
@@ -557,7 +557,7 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
557
557
  errors.push('setup.profile is invalid')
558
558
  }
559
559
  // The census reads these as path patterns relative to the project config's
560
- // folder (docs/knowledge-graph.md), under the contract's pathString rules.
560
+ // folder (docs/reference/knowledge-graph.md), under the contract's pathString rules.
561
561
  for (const key of ['include', 'exclude']) {
562
562
  errors.push(...stringPathErrors(setup[key], `setup.${key}`))
563
563
  if (typeof setup[key] === 'string' && (setup[key].includes('\u0000') || /^[a-z][a-z0-9+.-]*:/.test(setup[key]) || path.isAbsolute(setup[key]))) {
@@ -0,0 +1,24 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ export const PROVIDER_HOSTS = Object.freeze({ 'github.com': 'github' })
4
+
5
+ const lower = (value) => String(value ?? '').trim().toLowerCase()
6
+
7
+ // Ask the provider to resolve the recorded ref. GitHub follows the rename redirect
8
+ // and answers with the stable numeric id plus the current full name, which is the
9
+ // only signal that survives a rename.
10
+ export function githubLookup({ owner, name }) {
11
+ try {
12
+ const out = execFileSync('gh', ['api', `repos/${owner}/${name}`, '--jq', '{id: .id, fullName: .full_name}'], {
13
+ encoding: 'utf8',
14
+ stdio: ['ignore', 'pipe', 'ignore'],
15
+ })
16
+ const parsed = JSON.parse(out)
17
+ if (!parsed?.id) return null
18
+ return { id: String(parsed.id), fullName: lower(parsed.fullName), currentName: lower(String(parsed.fullName).split('/').pop()) }
19
+ } catch {
20
+ return null
21
+ }
22
+ }
23
+
24
+ export const defaultLookups = { github: githubLookup }
@@ -1,9 +1,9 @@
1
- import { execFileSync } from 'node:child_process'
2
1
  import { SUPPORTED_IDENTITY_PROVIDERS, gitRemoteUrl, remoteHost } from './config.mjs'
2
+ import { PROVIDER_HOSTS, defaultLookups } from './providers/github.mjs'
3
3
 
4
- export const SUPPORTED_PROVIDERS = Object.freeze([...SUPPORTED_IDENTITY_PROVIDERS])
4
+ export { githubLookup } from './providers/github.mjs'
5
5
 
6
- const PROVIDER_HOSTS = Object.freeze({ 'github.com': 'github' })
6
+ export const SUPPORTED_PROVIDERS = Object.freeze([...SUPPORTED_IDENTITY_PROVIDERS])
7
7
 
8
8
  const lower = (value) => String(value ?? '').trim().toLowerCase()
9
9
 
@@ -23,25 +23,6 @@ export function parseRemoteRef(url) {
23
23
  return { host, provider: PROVIDER_HOSTS[host] ?? null, owner: lower(parts[0]), name: lower(parts.slice(1).join('/')) }
24
24
  }
25
25
 
26
- // Ask the provider to resolve the recorded ref. GitHub follows the rename redirect
27
- // and answers with the stable numeric id plus the current full name, which is the
28
- // only signal that survives a rename.
29
- export function githubLookup({ owner, name }) {
30
- try {
31
- const out = execFileSync('gh', ['api', `repos/${owner}/${name}`, '--jq', '{id: .id, fullName: .full_name}'], {
32
- encoding: 'utf8',
33
- stdio: ['ignore', 'pipe', 'ignore'],
34
- })
35
- const parsed = JSON.parse(out)
36
- if (!parsed?.id) return null
37
- return { id: String(parsed.id), fullName: lower(parsed.fullName), currentName: lower(String(parsed.fullName).split('/').pop()) }
38
- } catch {
39
- return null
40
- }
41
- }
42
-
43
- const defaultLookups = { github: githubLookup }
44
-
45
26
  /**
46
27
  * Resolve a clone's stable identity.
47
28
  *
@@ -404,7 +404,7 @@ export function cutName(value, maxBytes) {
404
404
 
405
405
  // One file or folder name made safe for macOS, Linux, Windows and Obsidian
406
406
  // links, or '' when nothing is left of it. The author's casing and words are
407
- // kept; see "File names" in docs/obsidian-contract.md for each rule.
407
+ // kept; see "File names" in docs/reference/obsidian-contract.md for each rule.
408
408
  export function vaultName(value, maxBytes = NAME_BYTES) {
409
409
  const cleaned = trimName(String(value ?? '')
410
410
  .toWellFormed()
@@ -7,7 +7,7 @@ import { refuse } from './byte-lens.mjs'
7
7
 
8
8
  export { collisionKey }
9
9
 
10
- // Vault paths of layout 2 (see "Vault layout" in docs/obsidian-contract.md):
10
+ // Vault paths of layout 2 (see "Vault layout" in docs/reference/obsidian-contract.md):
11
11
  //
12
12
  // <repository folder>/<source directory>/<title>.md a Markdown note
13
13
  // <repository folder>/<source directory>/<file name> a wrapped file
@@ -75,7 +75,7 @@ import { isUserOwnedSettingsPath, prepareSettings } from './settings.mjs'
75
75
  //
76
76
  // A view is laid out in vault layout 2: folders mirror the repository, the
77
77
  // file name is the note's title and each note names its identity in generated
78
- // front-matter properties (see "Vault layout" in docs/obsidian-contract.md).
78
+ // front-matter properties (see "Vault layout" in docs/reference/obsidian-contract.md).
79
79
  // Layout 1, the earlier release's, is kept for a view whose prior generation
80
80
  // is in layout 1 and holds a note with an open edit, and for recovering a
81
81
  // layout 1 generation as it was published.
@@ -5,7 +5,7 @@ import { refuse } from './byte-lens.mjs'
5
5
  // judges the assembled result, every note included, whether the note was
6
6
  // emitted on this call or reused from the preparation cache, and refuses the
7
7
  // view with `redaction-failure`, naming the in-view note (or file) and the
8
- // rule, never the value. See "The redaction guard" in docs/obsidian-contract.md.
8
+ // rule, never the value. See "The redaction guard" in docs/reference/obsidian-contract.md.
9
9
  //
10
10
  // allow-list every path the emitter wrote (note and attachment paths,
11
11
  // rewritten link and embed targets, link targets in generated
@@ -2,11 +2,12 @@ import { createHash } from 'node:crypto'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
5
- import { PersonalWorkspaceRefusal, composePersonalWorkspace, readPersonalSelectionHead } from '@mnstry/atelier/personal-workspace'
5
+ import { PersonalWorkspaceRefusal, composePersonalWorkspace } from '@mnstry/atelier/personal-workspace'
6
6
  import { ensureLocalState, resolveProjectConfig, validateProjectConfigDoc } from '@mnstry/atelier/project'
7
7
  import { buildCanonicalGraph } from '../../graph/graph.mjs'
8
8
  import { ObsidianMaintenanceRefusal, refuse } from '../../runtime/obsidian/errors.mjs'
9
9
  import { OBSIDIAN_EXT_KEY } from './contracts.mjs'
10
+ import { readPersonalSelectionSnapshot } from '../../personal-workspace/selection.mjs'
10
11
 
11
12
  // The one personal-workspace route of the Obsidian projection.
12
13
  //
@@ -411,22 +412,17 @@ export async function validatePersonalWorkspace(project) {
411
412
 
412
413
  const selectionRecord = ({ personalHome }, sequence) => path.join(personalHome, 'selections', `${String(sequence).padStart(6, '0')}.json`)
413
414
 
414
- // The highest record number this process has read each home's selection history ending at, at any read of it: a
415
- // loader's first read, its read-again, the recheck of a pinned project, and the read the engine's guard asks for when
416
- // it stops a view (notePersonalSelectionNow). Kept in the main thread: no composition worker reads the history's end,
417
- // and one that did would have a map of its own and escape the rule.
415
+ // The most recently accepted head observed in this process. Durable continuity is checked by the loader's note
416
+ // callback before this index changes. The index only tells an engine when another reader requires it to reload.
418
417
  const READ_UP_TO = new Map()
419
418
 
420
- // Notes where a home's selection history ends, as just read, and answers that head. Selections are only ever appended,
421
- // so a history that ends at an earlier record than this process has read it ending at was cut short by something other
422
- // than a confirmation. It refuses `selection-history-shortened` at every read, the read-again and the recheck of a
423
- // project already loaded included, while it is shorter. Only the number is kept, and only in this process: a record in
424
- // a place already read, with other content, is read as a confirmation, and a process started after the cut reads the
425
- // history as it then stands. `refusesShortened: false` is a test seam only, the rule's mutation control.
426
- export function notePersonalSelectionHead(personalHome, head, { refusesShortened = true } = {}) {
427
- const before = READ_UP_TO.get(personalHome) ?? 0
428
- if (head.sequence > before) READ_UP_TO.set(personalHome, head.sequence)
429
- else if (refusesShortened && head.sequence < before) refuse('selection-history-shortened', 'the selection history of the personal workspace ends at an earlier record than this process has read it ending at; nothing is used from it while it is shorter', { source: 'personal-workspace' })
419
+ export function notePersonalSelectionHead(personalHome, head, { refusesShortened = true, note = null } = {}) {
420
+ if (note !== null) note(head)
421
+ else {
422
+ const before = READ_UP_TO.get(personalHome)?.sequence ?? 0
423
+ if (refusesShortened && head.sequence < before) refuse('selection-history-shortened', 'the selection history of the personal workspace ends at an earlier record than this process has read; nothing is used from it', { source: 'personal-workspace' })
424
+ }
425
+ if (refusesShortened) READ_UP_TO.set(personalHome, { sequence: head.sequence, head: head.head })
430
426
  return head
431
427
  }
432
428
 
@@ -435,8 +431,8 @@ export function notePersonalSelectionHead(personalHome, head, { refusesShortened
435
431
  // appended to the history, or a history cut short, is a change. And every time the composition is asked again
436
432
  // (validatePersonalWorkspace) the history is read again and must still end with that record. `observe: false`,
437
433
  // `recheck: false` and `refusesShortened: false` are test seams only, the mutation controls of each.
438
- export function pinPersonalSelection(project, { sequence, head }, { observe = true, recheck = true, refusesShortened = true } = {}) {
439
- SELECTED.set(personalWorkspaceBindingOf(project), Object.freeze({ sequence, head, observe, recheck, refusesShortened }))
434
+ export function pinPersonalSelection(project, { sequence, head }, { observe = true, recheck = true, refusesShortened = true, note = null } = {}) {
435
+ SELECTED.set(personalWorkspaceBindingOf(project), Object.freeze({ sequence, head, observe, recheck, refusesShortened, note }))
440
436
  return project
441
437
  }
442
438
 
@@ -449,25 +445,23 @@ export function personalSelectionOf(project) {
449
445
 
450
446
  // Refuses `personal-selection-changed` unless the history still ends with the record this project is pinned to; a
451
447
  // history that cannot be read refuses with the module's own code. What is read is noted first
452
- // (notePersonalSelectionHead): a later record seen here counts from then on, and a history shorter than this process
453
- // has read refuses `selection-history-shortened`, also when it ends with the pinned record. Nothing for a project that
454
- // is not pinned.
448
+ // (notePersonalSelectionHead): the same durable continuity rule applies to this read, before its head is compared
449
+ // with the pin. Nothing for a project that is not pinned.
455
450
  export function assertPersonalSelection(project) {
456
451
  const pinned = personalSelectionOf(project)
457
452
  if (pinned === null) return
458
453
  let now
459
- try { now = readPersonalSelectionHead({ personalHome: pinned.personalHome }) } catch (error) { asMaintenanceRefusal(error, 'the selection history of the personal workspace could not be read; nothing is used from it') }
454
+ try { now = readPersonalSelectionSnapshot({ personalHome: pinned.personalHome }) } catch (error) { asMaintenanceRefusal(error, 'the selection history of the personal workspace could not be read; nothing is used from it') }
460
455
  notePersonalSelectionHead(pinned.personalHome, now, SELECTED.get(personalWorkspaceBindingOf(project)))
461
456
  if (now.sequence !== pinned.sequence || now.head !== pinned.head) refuse('personal-selection-changed', 'the confirmed selection of the personal workspace is no longer the one this project was loaded under; nothing is used from it', { source: 'personal-workspace' })
462
457
  }
463
458
 
464
- // Whether this process has read a pinned project's selection history ending at a later record than the one the
465
- // project is pinned to: a confirmation since, or a history that was longer and is cut short now. Either way the project
466
- // is not the one to publish under any more, and is loaded again: the load follows the confirmation, or refuses the
467
- // shortened history. In memory, with no read. False for a project that is not pinned.
459
+ // Whether another reader in this process accepted a head different from the pin: a later confirmation or a newly
460
+ // confirmed shorter history. In memory, with no read. False for a project that is not pinned.
468
461
  export function personalSelectionBehind(project) {
469
462
  const pinned = personalSelectionOf(project)
470
- return pinned !== null && (READ_UP_TO.get(pinned.personalHome) ?? 0) > pinned.sequence
463
+ const latest = pinned === null ? null : READ_UP_TO.get(pinned.personalHome)
464
+ return latest !== undefined && latest !== null && (latest.sequence !== pinned.sequence || latest.head !== pinned.head)
471
465
  }
472
466
 
473
467
  // Reads where the history of a pinned project's home ends now, for the note alone: what the read refuses is not this
@@ -498,10 +492,12 @@ export function personalOverlayRepoOf(project) {
498
492
  }
499
493
 
500
494
  // Settings that a personal binding takes from the overlay or does not have yet. A saved view is authored in the
501
- // overlay, so `view add` would write a module-owned generation file; who may see and where vaults live wait for a
502
- // stable per-person projection configuration. Refused before anything is resolved or written.
495
+ // overlay, so `view add` would write a module-owned generation file. The machine records an owner-only audience or
496
+ // an explicit clear. Custom audiences refuse; vault location remains unavailable for this binding.
503
497
  export function refuseOnPersonalBinding(project, operation) {
504
498
  if (personalWorkspaceBindingOf(project) === null) return
499
+ if (operation === 'audience set me' || operation === 'audience clear') return
500
+ if (operation.startsWith('audience ')) refuse('personal-binding-owner-only', 'a personal workspace vault is owner only; use `audience set me` or `audience clear`', { operation })
505
501
  if (operation === 'view add') refuse('views-from-personal-overlay', 'the views of a personal workspace come from its overlay; nothing was written', { operation })
506
502
  refuse('personal-binding-decision-unavailable', `${operation} is not available for a personal workspace yet; nothing was written`, { operation })
507
503
  }
@@ -38,7 +38,7 @@ import { createHmac } from 'node:crypto'
38
38
  // and the exact bytes of the answer. A handshake is used once and lapses after
39
39
  // five seconds, a session after fifteen minutes. Nothing here takes a path to
40
40
  // act on, a command or code, and every command maps to state the service
41
- // already keeps (see docs/obsidian-plugin.md).
41
+ // already keeps (see docs/reference/obsidian-plugin.md).
42
42
  //
43
43
  // The plugin cannot import this module: it runs inside the app, from the
44
44
  // vault. It carries the same constants and the same computations, and the test
@@ -16,6 +16,10 @@ import { probeExchange } from './exchange.mjs'
16
16
  import { CRASH_INJECTION_TEST_SEAM } from './test-seam.mjs'
17
17
  import { createDirectAdapter } from './transport.mjs'
18
18
 
19
+ // The caller's private-state location refusals: once the state root may lie in a declared repository, no further
20
+ // entry is written there.
21
+ const isLocationRefusal = (error) => /^managed-root-|^repository-inside-managed-root$/.test(String(error?.code ?? ''))
22
+
19
23
  // publishView: conditional publication of one prepared view into an editable
20
24
  // vault. The order is the contract's:
21
25
  //
@@ -380,6 +384,9 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
380
384
 
381
385
  const budget = createCooperativeBudget(options.scheduling)
382
386
  budget.check()
387
+ // The caller's guard alone, not the cancellation signal: a cancelled publication still records its failure, and so
388
+ // does one the guard stops for another reason (a changed selection or settings). Only a location refusal suppresses it.
389
+ const locationStillProven = () => { try { options.scheduling?.guard?.(); return true } catch (error) { return !isLocationRefusal(error) } }
383
390
  const releases = []
384
391
  let journal = null
385
392
  try {
@@ -426,9 +433,11 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
426
433
  // Path selection. An app with this vault open in several windows is refused as such: publishing through one of
427
434
  // them would leave the others uncoordinated.
428
435
  const probe = await adapter.probe({ vaultRoot: store.vaultRoot })
436
+ budget.check()
429
437
  store.checkAllocatedVault?.()
430
438
  const unheld = probe.state !== 'coordinated' && probe.state !== 'absent' && await unheldPathApplies({ probe, pointer, store, unheldEvidence, platform })
431
- // The evidence took time to read: the folder is checked again.
439
+ // The evidence took time to read: the location and the folder are checked again.
440
+ budget.check()
432
441
  if (unheld) store.checkAllocatedVault?.()
433
442
  if (probe.state !== 'coordinated' && probe.state !== 'absent' && !unheld) refuse(probe.code === 'vault-open-in-several-windows' ? probe.code : 'editor-uncoordinated', `an Obsidian process may have this vault open and cannot be coordinated with: ${probe.reason}`)
434
443
  const mode = probe.state === 'coordinated' ? 'in-app' : unheld ? 'direct-unheld' : 'direct'
@@ -517,10 +526,14 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
517
526
  store.checkAllocatedVault?.()
518
527
  crash('after-staging')
519
528
 
520
- const context = { store, journal, journalId, channel, clock, crash, mode, rules }
529
+ // `check` runs the caller's guard: every wait for the app resumes through it before the next recovery, journal or
530
+ // vault write.
531
+ const context = { store, journal, journalId, channel, clock, crash, mode, rules, check: () => budget.check() }
521
532
  const results = []
522
533
  // Kept notes are compared first, several at a time; one that holds exactly its candidate bytes is unchanged.
523
534
  const unchanged = await unchangedKeepUnits(units, store)
535
+ // The comparison read the disk: the location is proven again before any unit's recovery, journal or vault write.
536
+ budget.check()
524
537
  // The direct path has no editor coordination, so it is only right while no
525
538
  // Obsidian runs. Staging takes time and an app may have started since path
526
539
  // selection: the process table is read again immediately before the first
@@ -537,11 +550,14 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
537
550
  if (mode === 'direct' && (lastProbe === null || Date.now() - lastProbe > 2000)) {
538
551
  lastProbe = Date.now()
539
552
  const again = await adapter.probe({ vaultRoot: store.vaultRoot })
553
+ budget.check()
540
554
  if (again.state !== 'absent') context.uncoordinated = again.reason
541
555
  }
542
556
  if (mode === 'direct-unheld' && !context.uncoordinated && (lastProbe === null || Date.now() - lastProbe > 2000)) {
543
557
  lastProbe = Date.now()
544
- if (!(await readsUnlisted(unheldEvidence, store))) context.uncoordinated = 'the vault list of the app no longer shows, positively, that no Obsidian lists this vault'
558
+ const listed = await readsUnlisted(unheldEvidence, store)
559
+ budget.check()
560
+ if (!listed) context.uncoordinated = 'the vault list of the app no longer shows, positively, that no Obsidian lists this vault'
545
561
  }
546
562
  store.checkAllocatedVault?.()
547
563
  if (unit.op === 'keep' && unchanged.has(unit.path)) {
@@ -569,6 +585,7 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
569
585
 
570
586
  // Quiet period, then look at the displaced files again.
571
587
  if (quietPeriodMs > 0 && results.some((result) => result.recoveryRef)) await sleep(quietPeriodMs)
588
+ budget.check()
572
589
  const lateWriters = recheckDisplacedFiles({ store, journalIds: [journalId], clock })
573
590
  for (const finding of lateWriters.filter((item) => item.code === 'late-writer-captured')) {
574
591
  journal.append({ step: 'verify', outcome: 'conflict', state: 'verifying', notePath: finding.notePath, beforeDigest: finding.digestAtMove, afterDigest: finding.observedDigest, recoveryRef: finding.displacedRef,
@@ -595,7 +612,9 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
595
612
  return { ...base, state: 'committed', lateWriters }
596
613
  } catch (error) {
597
614
  if (!(error instanceof PublicationRefusal)) {
598
- if (journal) try { journal.append({ step: 'verify', outcome: 'failed', state: 'failed', detail: { code: rules.journalFailureCode(error), message: String(error.code ?? error.message).slice(0, 200) } }) } catch { /* the journal itself cannot be written */ }
615
+ // A private-state location refusal records nothing more there, a failure entry included, and neither does a
616
+ // failure that leaves the location unproven now (a rejected wait for the app). Locks are still released.
617
+ if (journal && !isLocationRefusal(error) && locationStillProven()) try { journal.append({ step: 'verify', outcome: 'failed', state: 'failed', detail: { code: rules.journalFailureCode(error), message: String(error.code ?? error.message).slice(0, 200) } }) } catch { /* the journal itself cannot be written */ }
599
618
  throw error
600
619
  }
601
620
  return { state: 'refused', refusal: { code: error.code, message: error.message, detail: error.detail }, notes: [], retainedEdits: [], lateWriters: [] }
@@ -772,6 +791,7 @@ async function publishOneUnit(unit, context) {
772
791
  }
773
792
  const baseObject = observe(current)
774
793
  const inspected = await context.channel.inspect({ vaultRoot: store.vaultRoot, path: unit.path })
794
+ context.check?.()
775
795
  if (inspected.status !== 'inspected') {
776
796
  capture('failed', inspected.status, { beforeDigest: currentDigest })
777
797
  if (plan.stagedPath) retire(plan, context)
@@ -790,6 +810,7 @@ async function publishOneUnit(unit, context) {
790
810
  ? { op: 'publish', mode: 'remove', vaultRoot: store.vaultRoot, path: unit.path, operationId, baseSha256: hex(plan.baseDigest), recoveryPath: displaced }
791
811
  : { op: 'publish', mode: 'replace', vaultRoot: store.vaultRoot, path: unit.path, operationId, baseSha256: hex(plan.baseDigest), candidateSha256: hex(plan.candidateDigest), stagedPath: plan.stagedPath, recoveryPath: displaced })
792
812
  context.crash('after-publish')
813
+ context.check?.()
793
814
 
794
815
  if (reply.status === 'published' || reply.status === 'published-external-captured' || reply.status === 'removed' || reply.status === 'removed-external-captured') {
795
816
  syncPrivateDirectory(path.dirname(displaced))
@@ -3,6 +3,7 @@ import { isDeepStrictEqual } from 'node:util'
3
3
  import path from 'node:path'
4
4
  import { commandProject, readJson, writeJson } from '../project/config.mjs'
5
5
  import { summarizeReadinessJourney } from '../readiness-protocols/runtime.mjs'
6
+ import { bundledReadinessEnabled } from '../readiness-protocols/bundled-selection.mjs'
6
7
  import { atelierControlStyles } from '../ui/control-system.mjs'
7
8
 
8
9
  const esc = (value) => String(value ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
@@ -45,11 +46,12 @@ function resolveDistributionBranding(project) {
45
46
  }
46
47
 
47
48
  export function buildProjectProjection(project) {
49
+ const includeBundled = bundledReadinessEnabled(project)
48
50
  const graph = readJson(project.graphPath)
49
51
  const branding = resolveDistributionBranding(project)
50
- const readinessJourney = summarizeReadinessJourney(project)
52
+ const readinessJourney = includeBundled ? summarizeReadinessJourney(project) : null
51
53
  const output = path.join(project.outputRoot, 'index.html')
52
- const dimensionCards = readinessJourney.dimensions.map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
54
+ const dimensionCards = (readinessJourney?.dimensions ?? []).map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
53
55
  <p class="eyebrow">${esc(item.label)}</p>
54
56
  <h3>${esc(item.title)}</h3>
55
57
  <p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100 answers complete</p>
@@ -90,7 +92,7 @@ ${branding.themeCss}</style>
90
92
  <p class="eyebrow">${esc(branding.name)} · ${esc(branding.eyebrow)}</p>
91
93
  <h1>${esc(project.config.name || 'Project Workspace')}</h1>
92
94
  <p>${graph.counts.nodes} graph nodes · ${graph.counts.edges} graph edges · ${graph.counts.diagnostics} diagnostics</p>
93
- <section class="readiness" aria-labelledby="tenant-readiness-title">
95
+ ${readinessJourney ? `<section class="readiness" aria-labelledby="tenant-readiness-title">
94
96
  <div class="readiness-head">
95
97
  <div>
96
98
  <p class="eyebrow">MNSTRY Tenant Readiness</p>
@@ -101,7 +103,7 @@ ${branding.themeCss}</style>
101
103
  <div class="readiness-score">${esc(readinessJourney.score)}%</div>
102
104
  </div>
103
105
  <div class="readiness-grid">${dimensionCards}</div>
104
- </section>
106
+ </section>` : ''}
105
107
  <section class="grid">${cards}</section>
106
108
  <script>
107
109
  document.addEventListener('click', async (event) => {
@@ -126,16 +128,18 @@ export function buildProjectManifest(project, projection) {
126
128
  generatedAt: 'deterministic',
127
129
  graphPath: path.relative(project.outputRoot, project.graphPath).split(path.sep).join('/'),
128
130
  entry: 'index.html',
129
- tenantReadiness: {
130
- score: projection.readinessJourney.score,
131
- ready: projection.readinessJourney.ready,
132
- dimensions: projection.readinessJourney.dimensions.map((item) => ({
133
- key: item.key,
134
- protocolId: item.protocolId,
135
- status: item.status,
136
- score: item.score,
137
- })),
138
- },
131
+ ...(projection.readinessJourney ? {
132
+ tenantReadiness: {
133
+ score: projection.readinessJourney.score,
134
+ ready: projection.readinessJourney.ready,
135
+ dimensions: projection.readinessJourney.dimensions.map((item) => ({
136
+ key: item.key,
137
+ protocolId: item.protocolId,
138
+ status: item.status,
139
+ score: item.score,
140
+ })),
141
+ },
142
+ } : {}),
139
143
  nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
140
144
  }
141
145
  }