@smartmemory/compose 0.3.6-beta → 0.3.8

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 (269) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/.compose-deps.json +1 -13
  3. package/README.md +72 -5
  4. package/bin/compose.js +754 -347
  5. package/bin/git-hooks/pre-push.template +29 -0
  6. package/bin/judgment-import.js +7 -0
  7. package/bin/judgment-migrate.js +387 -0
  8. package/contracts/comp-obs-contract.schema.json +9 -3
  9. package/contracts/feature-json.schema.json +5 -0
  10. package/contracts/fluid-record.schema.json +209 -0
  11. package/contracts/judgment-record.schema.json +425 -4
  12. package/contracts/lifecycle-backfill.schema.json +322 -0
  13. package/dist/assets/App-Z4MU-H_F.js +916 -0
  14. package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
  15. package/dist/assets/arc-DY26UIVo.js +1 -0
  16. package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
  17. package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
  18. package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
  19. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  20. package/dist/assets/channel-SnZzzh7k.js +1 -0
  21. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  22. package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
  23. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  24. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  26. package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  28. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  29. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  30. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  31. package/dist/assets/clone-DgklGjHm.js +1 -0
  32. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  33. package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
  34. package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
  35. package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
  36. package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
  37. package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
  38. package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
  39. package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
  40. package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
  41. package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
  42. package/dist/assets/graph-BNzKGvoy.js +1 -0
  43. package/dist/assets/graph-CI_1htl0.js +331 -0
  44. package/dist/assets/index-BEfrNBp8.js +123 -0
  45. package/dist/assets/index-yyrA5OZd.css +1 -0
  46. package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
  47. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
  48. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  49. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
  50. package/dist/assets/katex-DkKDou_j.js +257 -0
  51. package/dist/assets/layout-BI8cXFPI.js +1 -0
  52. package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
  53. package/dist/assets/min-vPHfnXcC.js +1 -0
  54. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
  55. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  56. package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
  57. package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
  58. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
  59. package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
  60. package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
  61. package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
  62. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  63. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
  64. package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
  65. package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
  66. package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
  67. package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +7 -5
  70. package/lib/append-integrity.js +81 -0
  71. package/lib/backfill-evidence.js +109 -0
  72. package/lib/bug-escalation.js +39 -4
  73. package/lib/build-stream-schema.js +3 -1
  74. package/lib/build-stream-writer.js +25 -0
  75. package/lib/build.js +1624 -195
  76. package/lib/canon-guard.js +245 -0
  77. package/lib/canon-override.js +196 -0
  78. package/lib/canon-registry.js +291 -0
  79. package/lib/cli-commands.js +144 -0
  80. package/lib/codex-preflight.js +50 -15
  81. package/lib/colleague/context.js +215 -0
  82. package/lib/colleague/writeback.js +95 -0
  83. package/lib/completion-gate.js +1421 -0
  84. package/lib/completion-writer.js +47 -47
  85. package/lib/consumer-fanout.js +105 -11
  86. package/lib/coverage-gate.js +200 -0
  87. package/lib/dir-lock.js +170 -0
  88. package/lib/dispatch-ledger.js +301 -0
  89. package/lib/dispatch-metrics.js +236 -0
  90. package/lib/experiment-judge.js +6 -1
  91. package/lib/feature-json.js +1 -1
  92. package/lib/feature-reconciler.js +8 -0
  93. package/lib/feature-validator.js +64 -1
  94. package/lib/feature-writer.js +66 -2
  95. package/lib/fluid/factory.js +167 -0
  96. package/lib/fluid/ideabox-dates.js +73 -0
  97. package/lib/fluid/ideabox-migrate.js +154 -0
  98. package/lib/fluid/ideabox-ops.js +585 -0
  99. package/lib/fluid/ideabox-view.js +146 -0
  100. package/lib/fluid/import-ideabox.js +186 -0
  101. package/lib/fluid/local-provider.js +606 -0
  102. package/lib/fluid/provider.js +684 -0
  103. package/lib/fluid/record-shape.js +214 -0
  104. package/lib/fluid/record-store.js +328 -0
  105. package/lib/fluid/render-ideabox.js +261 -0
  106. package/lib/fluid/schema.js +40 -0
  107. package/lib/fluid/smartmemory-provider.js +1695 -0
  108. package/lib/gsd.js +63 -14
  109. package/lib/guard-cli.js +175 -0
  110. package/lib/guard-custody.js +141 -0
  111. package/lib/guard-descriptors.js +530 -0
  112. package/lib/guard-enrol.js +254 -0
  113. package/lib/health-score.js +1 -1
  114. package/lib/hooks-status.js +32 -3
  115. package/lib/ideabox-cli.js +315 -0
  116. package/lib/ideabox.js +121 -21
  117. package/lib/judgment/store/index.js +166 -0
  118. package/lib/judgment/store/records.js +184 -25
  119. package/lib/judgment/trace.js +380 -0
  120. package/lib/judgment-attest.js +259 -0
  121. package/lib/judgment-decision-write.js +277 -0
  122. package/lib/judgment-decisions.js +466 -0
  123. package/lib/judgment-gen.js +375 -22
  124. package/lib/judgment-verify.js +153 -0
  125. package/lib/judgment-writer.js +2842 -262
  126. package/lib/lane-gate.js +2 -0
  127. package/lib/lifecycle-modes.js +4 -4
  128. package/lib/lineage.js +400 -0
  129. package/lib/local-claude-connector.js +250 -54
  130. package/lib/maya-client.js +302 -0
  131. package/lib/maya-config.js +53 -0
  132. package/lib/maya-identity.js +283 -0
  133. package/lib/mcp-enforcement.js +21 -35
  134. package/lib/migrate-anon.js +5 -0
  135. package/lib/migrate-roadmap.js +15 -0
  136. package/lib/new.js +13 -1
  137. package/lib/pipeline-compat.js +104 -0
  138. package/lib/policy-catalog.js +295 -0
  139. package/lib/policy-check.js +0 -0
  140. package/lib/process-termination.js +98 -0
  141. package/lib/resolve-workspace.js +5 -1
  142. package/lib/result-normalizer.js +428 -153
  143. package/lib/review-normalize.js +4 -0
  144. package/lib/roadmap-errors.js +65 -0
  145. package/lib/roadmap-preservers.js +24 -4
  146. package/lib/roadmap-residue.js +299 -0
  147. package/lib/smartmemory-client.js +614 -78
  148. package/lib/smartmemory-config.js +54 -0
  149. package/lib/smartmemory-ingest.js +19 -2
  150. package/lib/step-prompt.js +7 -6
  151. package/lib/stratum-engine.js +53 -4
  152. package/lib/stratum-mcp-client.js +391 -31
  153. package/lib/test-bootstrap.js +31 -0
  154. package/lib/tool-inventory.js +122 -0
  155. package/lib/version-check.js +91 -19
  156. package/lib/vision-writer.js +88 -1
  157. package/package.json +7 -6
  158. package/pipelines/bug-fix.stratum.yaml +205 -211
  159. package/pipelines/build-quick.profiles.json +12 -0
  160. package/pipelines/build-quick.stratum.yaml +263 -350
  161. package/pipelines/content.stratum.yaml +81 -77
  162. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  163. package/pipelines/plan.stratum.yaml +76 -86
  164. package/pipelines/refactor.stratum.yaml +125 -125
  165. package/pipelines/research.stratum.yaml +56 -58
  166. package/pipelines/review-fix.profiles.json +6 -0
  167. package/pipelines/review-fix.stratum.yaml +110 -83
  168. package/presets/team-feature.profiles.json +6 -0
  169. package/presets/team-feature.stratum.yaml +93 -66
  170. package/presets/team-research.profiles.json +6 -0
  171. package/presets/team-research.stratum.yaml +89 -80
  172. package/presets/team-review.profiles.json +8 -0
  173. package/presets/team-review.stratum.yaml +98 -80
  174. package/scripts/cost-census.mjs +70 -0
  175. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  176. package/server/agent-health.js +22 -0
  177. package/server/agent-hooks.js +14 -1
  178. package/server/agent-server.js +5 -248
  179. package/server/agent-spawn.js +3 -4
  180. package/server/agent-workspace.js +294 -0
  181. package/server/build-routes.js +6 -5
  182. package/server/build-stream-bridge.js +53 -0
  183. package/server/cc-session-watcher.js +4 -1
  184. package/server/coalescing-buffer.js +7 -1
  185. package/server/completion-projection.js +228 -0
  186. package/server/compose-mcp-tools.js +124 -24
  187. package/server/compose-mcp.js +91 -790
  188. package/server/decision-event-emit.js +41 -2
  189. package/server/decision-event-id.js +17 -0
  190. package/server/decision-events-snapshot.js +3 -0
  191. package/server/design-routes.js +14 -8
  192. package/server/feature-scan.js +76 -2
  193. package/server/file-watcher.js +170 -21
  194. package/server/ideabox-routes.js +166 -224
  195. package/server/index.js +70 -100
  196. package/server/lifecycle-guard.js +240 -10
  197. package/server/lifecycle-phase-history.js +276 -0
  198. package/server/maya-routes.js +507 -0
  199. package/server/mcp-tool-defs.js +940 -0
  200. package/server/mcp-tool-policy.js +35 -3
  201. package/server/model-tiers.js +22 -5
  202. package/server/pipeline-routes.js +21 -11
  203. package/server/project-root.js +58 -19
  204. package/server/remote-utils.js +3 -1
  205. package/server/schema-validator.js +7 -1
  206. package/server/session-manager.js +5 -6
  207. package/server/session-routes.js +3 -1
  208. package/server/stratum-client.js +57 -10
  209. package/server/stratum-sync.js +6 -3
  210. package/server/summarizer.js +3 -4
  211. package/server/supervisor.js +0 -1
  212. package/server/vision-routes.js +208 -98
  213. package/server/vision-server.js +86 -23
  214. package/server/vision-store.js +60 -6
  215. package/server/vision-utils.js +3 -4
  216. package/server/workspace-activity.js +18 -0
  217. package/server/workspace-middleware.js +2 -2
  218. package/server/workspace-runtime.js +243 -0
  219. package/server/worktree-gc.js +1 -0
  220. package/dist/assets/App-BG3ngu8H.js +0 -896
  221. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  222. package/dist/assets/arc-_v4hR_uD.js +0 -1
  223. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  224. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  225. package/dist/assets/channel-B3U5wFAT.js +0 -1
  226. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  227. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  228. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  229. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  230. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  231. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  232. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  233. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  234. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  235. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  236. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  237. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  238. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  239. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  240. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  241. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  242. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  243. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  244. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  245. package/dist/assets/graph-BXPQrYYB.js +0 -331
  246. package/dist/assets/graph-C9eacEi8.js +0 -1
  247. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  248. package/dist/assets/index-LIwREYgH.css +0 -1
  249. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  250. package/dist/assets/katex-C5jXJg4s.js +0 -257
  251. package/dist/assets/layout-DEXfKzaS.js +0 -1
  252. package/dist/assets/map-Czzmt4hB.js +0 -1
  253. package/dist/assets/mobile-CaoXUwAr.js +0 -17
  254. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  255. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  256. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  257. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  258. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  259. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  260. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  261. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  262. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  263. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  264. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  265. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  266. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  267. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
  268. package/lib/staleness.js +0 -87
  269. package/server/ideabox-cache.js +0 -77
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * lib/judgment-gen.js — judgment projections (S04).
3
3
  *
4
- * records → REGISTER.md, LEDGER.md, OBJECTIVE.md, positions/<slug>.md, and
5
- * the OKF bundle root index.md. Projections are PURE OUTPUT: generation
4
+ * records → REGISTER.md, LEDGER.md, OBJECTIVE.md, SITUATION.md,
5
+ * people/<slug>.md, positions/<slug>.md, and the OKF bundle root index.md.
6
+ * Projections are PURE OUTPUT: generation
6
7
  * reads only records — never an existing projection — so any hand-edit is
7
8
  * overwritten by the next regeneration and there are no preserved sections
8
9
  * (curated prose is `note` records rendered at their anchors).
@@ -22,7 +23,12 @@ import {
22
23
  readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, readdirSync,
23
24
  } from 'node:fs';
24
25
  import { join } from 'node:path';
25
- import { createJudgmentStore } from './judgment/store/index.js';
26
+ import {
27
+ createJudgmentStore,
28
+ effectiveStore,
29
+ goalCutoverComplete,
30
+ } from './judgment/store/index.js';
31
+ import { buildSupersessionIndex } from './judgment/trace.js';
26
32
 
27
33
  function atomicWrite(path, content) {
28
34
  const tmp = `${path}.tmp.${process.pid}`;
@@ -50,17 +56,26 @@ function smartmemoryTeamId(cwd) {
50
56
  * Load everything projection-relevant from the store into a plain snapshot,
51
57
  * so the generator core stays pure.
52
58
  */
53
- function loadSnapshot(cwd) {
54
- const store = createJudgmentStore(cwd);
59
+ export function loadSnapshot(cwd) {
60
+ const rawStore = createJudgmentStore(cwd);
61
+ const store = effectiveStore(rawStore);
62
+ // Prebuilt index: derivePositionStatus otherwise rescans every other slug's
63
+ // chain per call, making this whole-store pass O(n^2) (COMP-JUDGMENT-PRECEDENT).
64
+ const supersession = buildSupersessionIndex(store);
55
65
  const positions = store.listPositionSlugs().map((slug) => ({
56
66
  slug,
57
67
  chain: store.readPositionChain(slug),
58
- status: store.derivePositionStatus(slug),
68
+ status: store.derivePositionStatus(slug, supersession),
59
69
  }));
60
70
  return {
61
71
  positions,
62
72
  joints: store.listJoints(),
63
73
  ledger: store.readLedgerEvents(),
74
+ people: store.listPeople(),
75
+ situationEntities: store.listSituationEntities(),
76
+ goalChain: store.readGoalChain(),
77
+ goalState: store.readGoalState(),
78
+ goalCutoverComplete: goalCutoverComplete(store),
64
79
  teamId: smartmemoryTeamId(cwd),
65
80
  };
66
81
  }
@@ -75,11 +90,22 @@ function renderNotes(notes) {
75
90
  return notes.map((n) => `> **${n.title}** — ${n.body ?? ''}`.trimEnd()).join('\n\n');
76
91
  }
77
92
 
93
+ // Plain YAML scalars that need no quoting: no leading/trailing space, none
94
+ // of the indicator/structure characters that would change the parse. Risky
95
+ // titles (e.g. "Jane: CEO") are emitted JSON-quoted, which is valid YAML
96
+ // double-quote style.
97
+ const YAML_PLAIN_SAFE_RE = /^[A-Za-z0-9](?:[A-Za-z0-9 _./()-]*[A-Za-z0-9_./()-])?$/;
98
+
99
+ function yamlScalar(value) {
100
+ const s = String(value);
101
+ return YAML_PLAIN_SAFE_RE.test(s) ? s : JSON.stringify(s);
102
+ }
103
+
78
104
  function okfFrontmatter({ type, title, timestamp, resource }) {
79
105
  const lines = ['---'];
80
106
  if (type) lines.push(`type: ${type}`);
81
107
  else lines.push('okf_version: "0.1"');
82
- lines.push(`title: ${title}`);
108
+ lines.push(`title: ${yamlScalar(title)}`);
83
109
  if (timestamp) lines.push(`timestamp: "${timestamp}"`);
84
110
  if (resource) lines.push(`resource: "${resource}"`);
85
111
  lines.push('smartmemory:');
@@ -94,6 +120,194 @@ function buildResource(teamId, providerId) {
94
120
  return `smartmemory://${encodeURIComponent(teamId)}/${encodeURIComponent(providerId)}`;
95
121
  }
96
122
 
123
+ function auditValue(value) {
124
+ const encoded = JSON.stringify(value);
125
+ return encoded === undefined ? String(value) : encoded;
126
+ }
127
+
128
+ function renderTraceLines(trace = [], { prefix = ' - ' } = {}) {
129
+ return trace.map((entry) => {
130
+ const prior = Object.entries(entry.prior ?? {})
131
+ .map(([key, value]) => `${key}=${auditValue(value)}`)
132
+ .join(', ');
133
+ return `${prefix}corrected from ${prior} at ${entry.corrected_at ?? 'unknown'}`;
134
+ });
135
+ }
136
+
137
+ function renderRecordedAndRemoval(entry, { prefix = ' - ' } = {}) {
138
+ const lines = [];
139
+ lines.push(`${prefix}recorded at ${entry.provenance?.written_at ?? 'unknown'}`);
140
+ if (entry.removed) {
141
+ lines.push(`${prefix}removed at ${entry.removed.at ?? 'unknown'} — ${entry.removed.reason}`);
142
+ }
143
+ return lines;
144
+ }
145
+
146
+ function renderAuditedFact(fact) {
147
+ const lines = [`- **${fact.id}** ${fact.text}`];
148
+ lines.push(` - channel: \`${fact.channel}\``);
149
+ if (fact.via !== undefined) lines.push(` - via: ${fact.via}`);
150
+ lines.push(` - at: ${fact.at}`);
151
+ lines.push(...renderTraceLines(fact.trace));
152
+ return lines.join('\n');
153
+ }
154
+
155
+ const PERSON_SECTIONS = ['role', 'life', 'stated', 'revealed'];
156
+
157
+ function sectionTitle(section) {
158
+ return section[0].toUpperCase() + section.slice(1);
159
+ }
160
+
161
+ function orderedPairLabel(left, right) {
162
+ const suffix = (id) => Number.parseInt(String(id).replace(/^[^0-9]+/, ''), 10);
163
+ const ids = [left, right].sort((a, b) => suffix(a) - suffix(b) || a.localeCompare(b));
164
+ return `${ids[0]} ↔ ${ids[1]}`;
165
+ }
166
+
167
+ function renderPerson(person) {
168
+ const lifecycle = person.facts.some((fact) => fact.channel === 'said') ? 'spoken' : 'stub';
169
+ const parts = [okfFrontmatter({
170
+ type: 'person',
171
+ title: person.display_name,
172
+ timestamp: person.provenance?.written_at,
173
+ })];
174
+ parts.push('');
175
+ parts.push(`# ${person.display_name}`);
176
+ parts.push('');
177
+ parts.push(`**Lifecycle:** ${lifecycle}`);
178
+ parts.push(`**Record:** \`${person.slug}\``);
179
+
180
+ const factsById = new Map(person.facts.map((fact) => [fact.id, fact]));
181
+ const rendered = new Set();
182
+ for (const section of PERSON_SECTIONS) {
183
+ parts.push('');
184
+ parts.push(`## ${sectionTitle(section)}`);
185
+ parts.push('');
186
+ const sectionLines = [];
187
+ for (const fact of person.facts.filter((entry) => entry.section === section)) {
188
+ if (rendered.has(fact.id)) continue;
189
+ const paired = fact.diverges_with ? factsById.get(fact.diverges_with) : null;
190
+ const isStatedRevealedPair = (
191
+ section === 'stated'
192
+ && paired?.section === 'revealed'
193
+ && paired.diverges_with === fact.id
194
+ );
195
+ sectionLines.push(renderAuditedFact(fact));
196
+ rendered.add(fact.id);
197
+ if (isStatedRevealedPair) {
198
+ sectionLines.push(renderAuditedFact(paired));
199
+ sectionLines.push(` - divergence pair: ${orderedPairLabel(fact.id, paired.id)}`);
200
+ rendered.add(paired.id);
201
+ }
202
+ }
203
+ parts.push(sectionLines.length > 0 ? sectionLines.join('\n') : 'No facts recorded.');
204
+ }
205
+
206
+ parts.push('');
207
+ parts.push('## Edges');
208
+ parts.push('');
209
+ if (person.edges.length === 0) {
210
+ parts.push('None.');
211
+ } else {
212
+ for (const edge of person.edges) {
213
+ parts.push(`- **${edge.id}** \`${edge.removed ? 'removed' : 'active'}\` — ${edge.kind} → [${edge.to}](${edge.to}.md)`);
214
+ parts.push(...renderRecordedAndRemoval(edge));
215
+ }
216
+ }
217
+
218
+ parts.push('');
219
+ parts.push('## Open fields');
220
+ parts.push('');
221
+ if (person.open_fields.length === 0) {
222
+ parts.push('None.');
223
+ } else {
224
+ for (const field of person.open_fields) {
225
+ parts.push(`- **${field.id}** \`${field.status}\` — ${field.name}`);
226
+ parts.push(` - filled_by: ${field.filled_by ?? '—'}`);
227
+ parts.push(` - recorded at ${field.provenance?.written_at ?? 'unknown'}`);
228
+ parts.push(...renderTraceLines(field.trace));
229
+ }
230
+ }
231
+
232
+ parts.push('');
233
+ parts.push('## Load links');
234
+ parts.push('');
235
+ if (person.load_links.length === 0) {
236
+ parts.push('None.');
237
+ } else {
238
+ for (const link of person.load_links) {
239
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — fact ${link.fact} carries ${link.carries}`);
240
+ parts.push(...renderRecordedAndRemoval(link));
241
+ }
242
+ }
243
+ return parts.join('\n') + '\n';
244
+ }
245
+
246
+ function latestWrittenAt(records) {
247
+ return records
248
+ .map((record) => record.provenance?.written_at)
249
+ .filter(Boolean)
250
+ .sort()
251
+ .at(-1);
252
+ }
253
+
254
+ function renderSituation(snapshot) {
255
+ const entities = [...(snapshot.situationEntities ?? [])].sort((a, b) => (
256
+ a.display_name.localeCompare(b.display_name) || a.slug.localeCompare(b.slug)
257
+ ));
258
+ const parts = [okfFrontmatter({
259
+ type: 'situation',
260
+ title: 'Situation',
261
+ timestamp: latestWrittenAt(entities),
262
+ })];
263
+ parts.push('');
264
+ parts.push('# Situation');
265
+ if (entities.length === 0) {
266
+ parts.push('');
267
+ parts.push('No situation entities recorded.');
268
+ }
269
+ for (const entity of entities) {
270
+ parts.push('');
271
+ parts.push(`## ${entity.display_name} (\`${entity.slug}\`)`);
272
+ parts.push('');
273
+ parts.push('### Facts');
274
+ parts.push('');
275
+ if (entity.facts.length === 0) {
276
+ parts.push('None.');
277
+ } else {
278
+ for (const fact of entity.facts) parts.push(renderAuditedFact(fact));
279
+ }
280
+
281
+ parts.push('');
282
+ parts.push('### Owed');
283
+ parts.push('');
284
+ if (entity.owed.length === 0) {
285
+ parts.push('None.');
286
+ } else {
287
+ for (const owed of entity.owed) {
288
+ parts.push(`> **OWED ${owed.id} · ${owed.status}** ${owed.name}`);
289
+ parts.push(`> - why load-bearing: ${owed.why_load_bearing}`);
290
+ parts.push(`> - filled_by: ${owed.filled_by ?? '—'}`);
291
+ parts.push(`> - recorded at ${owed.provenance?.written_at ?? 'unknown'}`);
292
+ parts.push(...renderTraceLines(owed.trace, { prefix: '> - ' }));
293
+ }
294
+ }
295
+
296
+ parts.push('');
297
+ parts.push('### Load links');
298
+ parts.push('');
299
+ if (entity.load_links.length === 0) {
300
+ parts.push('None.');
301
+ } else {
302
+ for (const link of entity.load_links) {
303
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — fact ${link.fact} carries ${link.carries}`);
304
+ parts.push(...renderRecordedAndRemoval(link));
305
+ }
306
+ }
307
+ }
308
+ return parts.join('\n') + '\n';
309
+ }
310
+
97
311
  function renderClaim(claim) {
98
312
  const lines = [`- **${claim.id}** \`[${claim.grounding}]\`${claim.owner_locked ? ' `[owner-locked]`' : ''} ${claim.text}`];
99
313
  if (Array.isArray(claim.supports) && claim.supports.length > 0) {
@@ -218,6 +432,7 @@ const EVENT_DETAIL_KEYS = [
218
432
  'refs', 'rejected', 'conviction', 'trigger', 'open_joints', 'prediction',
219
433
  'disposition', 'recall_verdict', 'attribution', 'prediction_ref',
220
434
  'prediction_grade', 'reason', 'rank_change', 'elicitation',
435
+ 'intent_id', 'tool', 'op', 'rests_on',
221
436
  ];
222
437
 
223
438
  function renderLedger(snapshot) {
@@ -248,8 +463,118 @@ function renderLedger(snapshot) {
248
463
  return parts.join('\n') + '\n';
249
464
  }
250
465
 
466
+ function renderGoalClause(clause) {
467
+ const lines = [`- **${clause.id}** ${clause.text}`];
468
+ lines.push(` - channel: \`${clause.channel}\``);
469
+ if (clause.via !== undefined) lines.push(` - via: ${clause.via}`);
470
+ const citation = clause.elicitation;
471
+ lines.push(
472
+ ` - elicitation: asked "${citation.asked}" (answered ${citation.answered_at}, ref ${citation.answer_ref})`,
473
+ );
474
+ lines.push(...renderTraceLines(clause.trace));
475
+ return lines.join('\n');
476
+ }
477
+
478
+ function tableCell(value) {
479
+ return String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
480
+ }
481
+
482
+ function renderGoalObjective(snapshot) {
483
+ const chain = snapshot.goalChain ?? [];
484
+ const current = chain.at(-1);
485
+ if (!current) {
486
+ return [
487
+ okfFrontmatter({ type: 'goal', title: 'Objective' }),
488
+ '',
489
+ '# Objective',
490
+ '',
491
+ 'No goal recorded.',
492
+ ].join('\n') + '\n';
493
+ }
494
+
495
+ const state = snapshot.goalState ?? { joints: [], load_links: [] };
496
+ const parts = [okfFrontmatter({
497
+ type: 'goal',
498
+ title: 'Objective',
499
+ timestamp: current.provenance?.written_at,
500
+ })];
501
+ parts.push('');
502
+ parts.push('# Objective');
503
+ parts.push('');
504
+ parts.push(`**Current version:** v${current.version}`);
505
+ const draft = (
506
+ ['import', 'migration'].includes(current.provenance?.via)
507
+ && !current.ratification
508
+ );
509
+ if (draft) {
510
+ parts.push('');
511
+ parts.push('**DRAFT HEALTH WARNING:** This imported/migrated goal is not owner-ratified.');
512
+ }
513
+
514
+ parts.push('');
515
+ parts.push(`## Current clauses (v${current.version})`);
516
+ parts.push('');
517
+ for (const clause of current.clauses) parts.push(renderGoalClause(clause));
518
+
519
+ parts.push('');
520
+ parts.push('## Ratification citation');
521
+ parts.push('');
522
+ if (current.ratification) {
523
+ parts.push(`- asked "${current.ratification.asked}"`);
524
+ parts.push(`- quote: "${current.ratification.quote}"`);
525
+ parts.push(
526
+ `- answered ${current.ratification.answered_at}, ref ${current.ratification.answer_ref}`,
527
+ );
528
+ } else {
529
+ parts.push('None — imported/migrated draft.');
530
+ }
531
+
532
+ parts.push('');
533
+ parts.push('## Goal joints');
534
+ parts.push('');
535
+ if ((state.joints ?? []).length === 0) {
536
+ parts.push('None.');
537
+ } else {
538
+ for (const link of state.joints) {
539
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — [${link.joint}](REGISTER.md#${link.joint})`);
540
+ parts.push(...renderRecordedAndRemoval(link));
541
+ }
542
+ }
543
+
544
+ parts.push('');
545
+ parts.push('## Load-link bill');
546
+ parts.push('');
547
+ if ((state.load_links ?? []).length === 0) {
548
+ parts.push('None.');
549
+ } else {
550
+ for (const link of state.load_links) {
551
+ const version = Number(/^v([1-9][0-9]*)#/.exec(link.clause)?.[1]);
552
+ const versionMark = version === current.version ? 'current version' : 'superseded version';
553
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — ${link.clause} \`${versionMark}\` carries ${link.carries}`);
554
+ parts.push(...renderRecordedAndRemoval(link));
555
+ }
556
+ }
557
+
558
+ parts.push('');
559
+ parts.push('## Trajectory');
560
+ parts.push('');
561
+ parts.push('| Version | Date | Provocation | Diff note | Wording fix |');
562
+ parts.push('|---|---|---|---|---|');
563
+ for (const version of chain) {
564
+ const provocation = version.provocation?.quote ?? 'unknown (migrated)';
565
+ const wordingFix = version.clauses.some((clause) => (clause.trace?.length ?? 0) > 0)
566
+ ? 'yes'
567
+ : 'no';
568
+ parts.push(
569
+ `| v${version.version} | ${tableCell(version.provenance?.written_at ?? 'unknown')} | ${tableCell(provocation)} | ${tableCell(version.diff_note)} | ${wordingFix} |`,
570
+ );
571
+ }
572
+ return parts.join('\n') + '\n';
573
+ }
574
+
251
575
  function renderObjective(snapshot) {
252
- const objective = snapshot.positions.find((p) => p.slug === 'objective');
576
+ if (snapshot.goalCutoverComplete) return renderGoalObjective(snapshot);
577
+ const objective = (snapshot.positions ?? []).find((p) => p.slug === 'objective');
253
578
  if (objective) return renderPosition(objective, snapshot);
254
579
  return [
255
580
  okfFrontmatter({ type: 'position', title: 'objective' }),
@@ -261,6 +586,9 @@ function renderObjective(snapshot) {
261
586
  }
262
587
 
263
588
  function renderIndex(snapshot) {
589
+ const positions = (snapshot.positions ?? []).filter(
590
+ (position) => !(snapshot.goalCutoverComplete && position.slug === 'objective'),
591
+ );
264
592
  const parts = [okfFrontmatter({ type: null, title: 'Judgment canon' })];
265
593
  parts.push('');
266
594
  parts.push('# Judgment canon (generated)');
@@ -270,11 +598,11 @@ function renderIndex(snapshot) {
270
598
  parts.push('- [Register](REGISTER.md)');
271
599
  parts.push('- [Ledger](LEDGER.md)');
272
600
  parts.push('- [Objective](OBJECTIVE.md)');
273
- if (snapshot.positions.length) {
601
+ if (positions.length) {
274
602
  parts.push('');
275
603
  parts.push('## Positions');
276
604
  parts.push('');
277
- for (const p of snapshot.positions) {
605
+ for (const p of positions) {
278
606
  parts.push(`- [${p.slug}](positions/${p.slug}.md) — ${p.status}`);
279
607
  }
280
608
  }
@@ -293,14 +621,40 @@ export function generateFromRecords(snapshot, { now } = {}) { // eslint-disable-
293
621
  'docs/judgment/REGISTER.md': renderRegister(snapshot),
294
622
  'docs/judgment/LEDGER.md': renderLedger(snapshot),
295
623
  'docs/judgment/OBJECTIVE.md': renderObjective(snapshot),
624
+ 'docs/judgment/SITUATION.md': renderSituation(snapshot),
296
625
  'docs/judgment/index.md': renderIndex(snapshot),
297
626
  };
298
- for (const position of snapshot.positions) {
627
+ for (const person of [...(snapshot.people ?? [])].sort((a, b) => a.slug.localeCompare(b.slug))) {
628
+ files[`docs/judgment/people/${person.slug}.md`] = renderPerson(person);
629
+ }
630
+ for (const position of (snapshot.positions ?? [])) {
631
+ if (snapshot.goalCutoverComplete && position.slug === 'objective') continue;
299
632
  files[`docs/judgment/positions/${position.slug}.md`] = renderPosition(position, snapshot);
300
633
  }
301
634
  return files;
302
635
  }
303
636
 
637
+ const MANAGED_MARKDOWN_DIRS = [
638
+ 'docs/judgment/people',
639
+ 'docs/judgment/positions',
640
+ ];
641
+
642
+ function managedStaleProjections(cwd, files) {
643
+ const stale = [];
644
+ for (const relDir of MANAGED_MARKDOWN_DIRS) {
645
+ const dir = join(cwd, relDir);
646
+ if (!existsSync(dir)) continue;
647
+ const entries = readdirSync(dir, { withFileTypes: true })
648
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
649
+ .sort((a, b) => a.name.localeCompare(b.name));
650
+ for (const entry of entries) {
651
+ const rel = `${relDir}/${entry.name}`;
652
+ if (!(rel in files)) stale.push(rel);
653
+ }
654
+ }
655
+ return stale;
656
+ }
657
+
304
658
  /**
305
659
  * Regenerate every projection from records on disk. Atomic per file.
306
660
  * @returns {{ files: string[] }} relative paths written
@@ -312,6 +666,9 @@ export function regenerateProjections(cwd) {
312
666
  mkdirSync(join(path, '..'), { recursive: true });
313
667
  atomicWrite(path, content);
314
668
  }
669
+ for (const rel of managedStaleProjections(cwd, files)) {
670
+ unlinkSync(join(cwd, rel));
671
+ }
315
672
  return { files: Object.keys(files) };
316
673
  }
317
674
 
@@ -325,18 +682,14 @@ export function checkProjectionRoundtrip(cwd) {
325
682
  const diffs = [];
326
683
  for (const [rel, content] of Object.entries(files)) {
327
684
  const path = join(cwd, rel);
328
- const onDisk = existsSync(path) ? readFileSync(path, 'utf8') : null;
329
- if (onDisk !== content) diffs.push(rel);
330
- }
331
- // Orphan detection: a projection file the generator no longer emits (stale
332
- // position projection, leftover from an older generator) is drift too.
333
- const positionsDir = join(cwd, 'docs', 'judgment', 'positions');
334
- if (existsSync(positionsDir)) {
335
- for (const f of readdirSync(positionsDir)) {
336
- if (!f.endsWith('.md')) continue;
337
- const rel = `docs/judgment/positions/${f}`;
338
- if (!(rel in files)) diffs.push(`${rel} (orphan — not emitted by the generator)`);
685
+ if (!existsSync(path)) {
686
+ diffs.push(`${rel} (missing)`);
687
+ continue;
339
688
  }
689
+ if (readFileSync(path, 'utf8') !== content) diffs.push(`${rel} (drift)`);
690
+ }
691
+ for (const rel of managedStaleProjections(cwd, files)) {
692
+ diffs.push(`${rel} (orphan — not emitted by the generator)`);
340
693
  }
341
694
  return { fixedPoint: diffs.length === 0, diffs };
342
695
  }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Combined drift verification for the judgment canon.
3
+ *
4
+ * The inventory, projection roundtrip, and record attestation are evaluated
5
+ * under the writer's advisory lock so a legitimate write cannot expose a
6
+ * partially-published canon to the verifier.
7
+ */
8
+ import {
9
+ existsSync,
10
+ lstatSync,
11
+ readdirSync,
12
+ } from 'node:fs';
13
+ import { join, posix } from 'node:path';
14
+
15
+ import { readManifest, recordFileSet, verifyRecords } from './judgment-attest.js';
16
+ import {
17
+ checkProjectionRoundtrip,
18
+ generateFromRecords,
19
+ loadSnapshot,
20
+ } from './judgment-gen.js';
21
+ import { withJudgmentLock } from './judgment-writer.js';
22
+
23
+ const JUDGMENT_REL = 'docs/judgment';
24
+ const MANIFEST_REL = `${JUDGMENT_REL}/.attest.json`;
25
+ const RECORDS_REL = `${JUDGMENT_REL}/records`;
26
+ const STRUCTURAL_DIRS = [
27
+ RECORDS_REL,
28
+ `${JUDGMENT_REL}/people`,
29
+ `${JUDGMENT_REL}/positions`,
30
+ ];
31
+ // The legitimate depth-1 dirs under records/. Anything else there (e.g. a
32
+ // Bash-created `records/garbage/`) is drift. Deeper legitimate dirs — such as
33
+ // `records/positions/<slug>/` — are admitted by expectedTree's parent-walk over
34
+ // the real record files, so this list stays exact rather than blanket-exempting
35
+ // every nested path.
36
+ const RECORD_STRUCTURAL_DIRS = [
37
+ 'goal', 'intents', 'joints', 'people', 'positions', 'predictions', 'situation',
38
+ ].map((name) => `${RECORDS_REL}/${name}`);
39
+
40
+ function expectedTree(cwd) {
41
+ // Derive the expected projection set from the SAME snapshot builder that
42
+ // checkProjectionRoundtrip uses (loadSnapshot, exported from judgment-gen).
43
+ // Reconstructing it here would be a mirror that silently diverges into false
44
+ // tree drift the moment loadSnapshot changes.
45
+ const projections = Object.keys(generateFromRecords(loadSnapshot(cwd)));
46
+ const records = recordFileSet(cwd);
47
+ // The manifest is NOT in this set: it lives at .compose/judgment-attest.json,
48
+ // outside the tree being inventoried.
49
+ const files = new Set([...projections, ...records]);
50
+ const directories = new Set([JUDGMENT_REL, ...STRUCTURAL_DIRS, ...RECORD_STRUCTURAL_DIRS]);
51
+
52
+ for (const relPath of files) {
53
+ let parent = posix.dirname(relPath);
54
+ while (parent.startsWith(`${JUDGMENT_REL}/`)) {
55
+ directories.add(parent);
56
+ parent = posix.dirname(parent);
57
+ }
58
+ }
59
+
60
+ return { files, directories };
61
+ }
62
+
63
+ function inventoryTree(cwd, expected) {
64
+ const root = join(cwd, ...JUDGMENT_REL.split('/'));
65
+ if (!existsSync(root)) return [];
66
+
67
+ const rootStat = lstatSync(root);
68
+ if (rootStat.isSymbolicLink()) {
69
+ return [{ path: JUDGMENT_REL, kind: 'symlink' }];
70
+ }
71
+ if (!rootStat.isDirectory()) {
72
+ return [{ path: JUDGMENT_REL, kind: 'unexpected' }];
73
+ }
74
+
75
+ const drift = [];
76
+
77
+ function walk(absDir, relDir) {
78
+ const entries = readdirSync(absDir)
79
+ .sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
80
+
81
+ for (const name of entries) {
82
+ const absPath = join(absDir, name);
83
+ const relPath = `${relDir}/${name}`;
84
+ const stat = lstatSync(absPath);
85
+
86
+ if (stat.isSymbolicLink()) {
87
+ drift.push({ path: relPath, kind: 'symlink' });
88
+ continue;
89
+ }
90
+
91
+ if (stat.isDirectory()) {
92
+ // An EMPTY unexpected dir is not reported: it holds no canon, and
93
+ // UndoLog.restore unlinks files without removing their directories, so a
94
+ // rolled-back op legitimately leaves e.g. an empty positions/<slug>/.
95
+ // Flagging those would false-RED on correct behaviour. A non-empty stray
96
+ // dir is still caught — every file inside it fails the file rule below.
97
+ if (!expected.directories.has(relPath) && readdirSync(absPath).length > 0) {
98
+ drift.push({ path: relPath, kind: 'unexpected' });
99
+ }
100
+ walk(absPath, relPath);
101
+ continue;
102
+ }
103
+
104
+ if (!stat.isFile() || !expected.files.has(relPath)) {
105
+ drift.push({ path: relPath, kind: 'unexpected' });
106
+ }
107
+ }
108
+ }
109
+
110
+ walk(root, JUDGMENT_REL);
111
+ drift.sort((a, b) => {
112
+ if (a.path !== b.path) return a.path < b.path ? -1 : 1;
113
+ return a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;
114
+ });
115
+ return drift;
116
+ }
117
+
118
+ /**
119
+ * True when this project has no judgment canon at all — no `docs/judgment/`
120
+ * tree and no attestation baseline. There is nothing to verify, so verification
121
+ * must pass: most Compose projects never adopt the judgment layer, and a
122
+ * gate that fails them would break every pre-push and ship in those repos.
123
+ *
124
+ * This is NOT the same as a deleted canon. The baseline lives outside
125
+ * `docs/judgment/**` (at `.compose/judgment-attest.json`), so wiping the canon
126
+ * leaves the manifest behind — `hasNoCanon` is false and every record it
127
+ * remembers surfaces as `removed` drift.
128
+ */
129
+ function hasNoCanon(cwd) {
130
+ return !existsSync(join(cwd, ...JUDGMENT_REL.split('/')))
131
+ && readManifest(cwd) === null;
132
+ }
133
+
134
+ export async function verifyJudgmentCanon(cwd) {
135
+ return withJudgmentLock(cwd, async () => {
136
+ if (hasNoCanon(cwd)) {
137
+ return { ok: true, treeDrift: [], projectionDrift: [], recordDrift: [] };
138
+ }
139
+
140
+ const treeDrift = inventoryTree(cwd, expectedTree(cwd));
141
+ const projection = checkProjectionRoundtrip(cwd);
142
+ const records = verifyRecords(cwd);
143
+ const projectionDrift = projection.diffs;
144
+ const recordDrift = records.drift;
145
+
146
+ return {
147
+ ok: treeDrift.length === 0 && projection.fixedPoint && records.ok,
148
+ treeDrift,
149
+ projectionDrift,
150
+ recordDrift,
151
+ };
152
+ });
153
+ }