naive-framework 0.0.0-stage → 0.1.0

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 (338) hide show
  1. package/AGENTS.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -2
  4. package/docs/decisions/decisions.json +861 -0
  5. package/docs/ontology.json +796 -0
  6. package/docs/ontology.md +163 -0
  7. package/docs/reference/ROUTES.md +125 -0
  8. package/evidence/benchmarks/2026-10-27-golden.jev.after-catalog-fix.json +558 -0
  9. package/evidence/benchmarks/2026-10-27-golden.jev.json +501 -0
  10. package/evidence/benchmarks/2026-10-27-golden.reasoning-answers.json +177 -0
  11. package/evidence/benchmarks/JEV_VS_REASONING_MODEL_2026-10-27.md +68 -0
  12. package/evidence/capabilities/agent-capabilities.json +86 -0
  13. package/evidence/design-documents.registry.json +2896 -0
  14. package/evidence/evals/evals.json +25 -0
  15. package/evidence/evals/harness-evals.json +140 -0
  16. package/evidence/layer.json +51 -0
  17. package/evidence/skills/agent-capabilities/SKILL.md +35 -0
  18. package/evidence/skills/design-coverage/SKILL.md +57 -0
  19. package/evidence/skills/design-coverage/check.mjs +192 -0
  20. package/evidence/skills/design-coverage/scan.mjs +45 -0
  21. package/evidence/skills/fidelity-ladder-audit/SKILL.md +121 -0
  22. package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +180 -0
  23. package/evidence/skills/findability/SKILL.md +32 -0
  24. package/evidence/skills/findability/inventory.rb +41 -0
  25. package/evidence/skills/foreshadowing-audit/SKILL.md +183 -0
  26. package/evidence/skills/metrics-review/SKILL.md +177 -0
  27. package/evidence/tooling/integrations.json +153 -0
  28. package/governance/layer.json +101 -0
  29. package/governance/references/references.json +26 -0
  30. package/governance/skills/board-design/SKILL.md +68 -0
  31. package/governance/skills/brand-pillar/SKILL.md +90 -0
  32. package/governance/skills/business-pillar/SKILL.md +96 -0
  33. package/governance/skills/chief-of-staff/SKILL.md +45 -0
  34. package/governance/skills/cofounder-structure/SKILL.md +64 -0
  35. package/governance/skills/cross-pillar-tradeoff/SKILL.md +168 -0
  36. package/governance/skills/equity-and-share-classes/SKILL.md +68 -0
  37. package/governance/skills/executive-roles/SKILL.md +95 -0
  38. package/governance/skills/exit-planning/SKILL.md +66 -0
  39. package/governance/skills/financing-and-investor-rights/SKILL.md +73 -0
  40. package/governance/skills/office-hours-mode/SKILL.md +113 -0
  41. package/governance/skills/product-pillar/SKILL.md +81 -0
  42. package/governance/skills/publication-policy/SKILL.md +63 -0
  43. package/governance/skills/role-and-candidate-authoring/SKILL.md +198 -0
  44. package/governance/skills/stakeholder-update/SKILL.md +177 -0
  45. package/governance/tooling/integrations.json +37 -0
  46. package/governance/venture-template/AGENT_ROLES_AND_DECISION_RIGHTS.md +58 -0
  47. package/governance/venture-template/COMPANY_VAULT_DEFINITION_2026-10-25.md +204 -0
  48. package/governance/venture-template/README.md +74 -0
  49. package/governance/venture-template/decision-rights.json +186 -0
  50. package/governance/venture-template/index.md +151 -0
  51. package/governance/venture-template/pillars/brand.md +209 -0
  52. package/governance/venture-template/pillars/business.md +255 -0
  53. package/governance/venture-template/pillars/product.md +254 -0
  54. package/governance/venture-template/render-roles.py +11 -0
  55. package/governance/venture-template/roles/README.md +85 -0
  56. package/governance/venture-template/roles/agent-roles.json +202 -0
  57. package/governance/venture-template/roles/ceo.md +163 -0
  58. package/governance/venture-template/roles/chief-of-staff.md +153 -0
  59. package/governance/venture-template/roles/cmo.md +165 -0
  60. package/governance/venture-template/roles/coo.md +189 -0
  61. package/governance/venture-template/roles/cto.md +158 -0
  62. package/governance/venture-template/roles/department-head.md +47 -0
  63. package/governance/venture-template/roles/forward-deployed-engineer.md +51 -0
  64. package/governance/venture-template/roles/founder.md +173 -0
  65. package/governance/venture-template/roles/hr-manager.md +49 -0
  66. package/governance/venture-template/roles/talent-acquisition-manager.md +50 -0
  67. package/interface/api/server.mjs +69 -0
  68. package/interface/cli/find-venture.mjs +15 -0
  69. package/interface/cli/mentura.mjs +271 -0
  70. package/interface/cli/naive.mjs +169 -0
  71. package/interface/demo/board.mjs +581 -0
  72. package/interface/demo/decision-harness.mjs +91 -0
  73. package/interface/demo/live-agent.mjs +47 -0
  74. package/interface/harness/evals.mjs +90 -0
  75. package/interface/harness/examples/pt-no-shows.dump.md +19 -0
  76. package/interface/harness/run.mjs +313 -0
  77. package/interface/harness/site.mjs +59 -0
  78. package/interface/layer.json +31 -0
  79. package/interface/mcp/server.mjs +122 -0
  80. package/interface/operator-console/README.md +58 -0
  81. package/interface/provenance.mjs +42 -0
  82. package/interface/references/references.json +28 -0
  83. package/interface/route-aliases.json +32 -0
  84. package/interface/router.mjs +580 -0
  85. package/interface/skills/install-mentura/SKILL.md +88 -0
  86. package/interface/tooling/integrations.json +112 -0
  87. package/knowledge/connectors/README.md +54 -0
  88. package/knowledge/connectors/catalog.mjs +118 -0
  89. package/knowledge/connectors/contract.mjs +56 -0
  90. package/knowledge/connectors/eval.mjs +176 -0
  91. package/knowledge/connectors/filesystem.mjs +79 -0
  92. package/knowledge/connectors/index.mjs +102 -0
  93. package/knowledge/connectors/resolve.mjs +44 -0
  94. package/knowledge/connectors/suite.mjs +80 -0
  95. package/knowledge/layer.json +42 -0
  96. package/knowledge/skills/find-reference/SKILL.md +38 -0
  97. package/knowledge/skills/industry-awareness/SKILL.md +277 -0
  98. package/knowledge/tooling/integrations.json +127 -0
  99. package/knowledge/tooling/questions.md +46 -0
  100. package/operations/FOUNDER_OPERATING_MODEL_CAMPAIGN_2026-10-14.md +16 -0
  101. package/operations/departments.json +162 -0
  102. package/operations/handoff-v2.md +93 -0
  103. package/operations/layer.json +60 -0
  104. package/operations/marketing/check-public.mjs +68 -0
  105. package/operations/marketing/site/README.md +86 -0
  106. package/operations/marketing/site/matrix.json +731 -0
  107. package/operations/marketing/site/plan.mjs +117 -0
  108. package/operations/marketing/site/presence.mjs +252 -0
  109. package/operations/references/references.json +45 -0
  110. package/operations/skills/recruiting/SKILL.md +32 -0
  111. package/operations/skills/team-design/SKILL.md +36 -0
  112. package/operations/skills/tool-selection/SKILL.md +46 -0
  113. package/operations/templates/recruiting-brief.md +35 -0
  114. package/operations/tooling/integrations.json +158 -0
  115. package/package.json +57 -4
  116. package/packages/catalog.json +272 -0
  117. package/packages/cofounder/package.json +42 -0
  118. package/packages/counsel/package.json +47 -0
  119. package/packages/customers/package.json +21 -0
  120. package/packages/decisions/package.json +17 -0
  121. package/packages/engineering/package.json +41 -0
  122. package/packages/evidence/package.json +17 -0
  123. package/packages/finance/package.json +48 -0
  124. package/packages/foundation/package.json +52 -0
  125. package/packages/investor/package.json +57 -0
  126. package/packages/investors/package.json +21 -0
  127. package/packages/legal/package.json +73 -0
  128. package/packages/marketing/package.json +57 -0
  129. package/packages/money/package.json +21 -0
  130. package/packages/operations/package.json +74 -0
  131. package/packages/partner/package.json +38 -0
  132. package/packages/people/package.json +64 -0
  133. package/packages/product/package.json +49 -0
  134. package/packages/raw/package.json +19 -0
  135. package/packages/relationships/package.json +52 -0
  136. package/packages/validation/package.json +99 -0
  137. package/platform/README.md +45 -0
  138. package/platform/connectors/eval.mjs +154 -0
  139. package/platform/connectors/fake-github.mjs +91 -0
  140. package/platform/connectors/github.mjs +105 -0
  141. package/platform/integrations/jev.mjs +30 -0
  142. package/platform/integrations/langsmith-docket.mjs +53 -0
  143. package/platform/integrations/langsmith-setup.mjs +115 -0
  144. package/platform/integrations/langsmith.mjs +47 -0
  145. package/platform/integrations/rho.mjs +145 -0
  146. package/platform/integrations/site.mjs +75 -0
  147. package/platform/kits/README.md +7 -0
  148. package/platform/mcp/research.mjs +64 -0
  149. package/purpose/checklists/vault-and-tooling-consultancy-setup.md +216 -0
  150. package/purpose/frameworks/business-discovery-framework-amendments-2026-05-20.md +181 -0
  151. package/purpose/frameworks/business-discovery-framework.html +938 -0
  152. package/purpose/frameworks/business-discovery-framework.md +326 -0
  153. package/purpose/frameworks/idea-map/README.md +74 -0
  154. package/purpose/frameworks/idea-map/conventions.json +146 -0
  155. package/purpose/frameworks/idea-map/graph.mjs +182 -0
  156. package/purpose/frameworks/idea-map/ideamap.mjs +179 -0
  157. package/purpose/frameworks/idea-map/route.mjs +140 -0
  158. package/purpose/frameworks/onboarding-handoff-framework.md +269 -0
  159. package/purpose/frameworks/post-discovery-deliverable-playbook.md +274 -0
  160. package/purpose/frameworks/product-anatomy/README.md +63 -0
  161. package/purpose/frameworks/product-anatomy/anatomy.mjs +195 -0
  162. package/purpose/frameworks/product-anatomy/examples/polity.answers.reasoned.json +11 -0
  163. package/purpose/frameworks/product-anatomy/examples/polity.json +89 -0
  164. package/purpose/frameworks/product-anatomy/phases.json +107 -0
  165. package/purpose/frameworks/product-anatomy/questions.json +74 -0
  166. package/purpose/kits/validation/README.md +14 -0
  167. package/purpose/kits/validation/references/references.json +122 -0
  168. package/purpose/kits/validation/skills/problem-validation/SKILL.md +43 -0
  169. package/purpose/kits/validation/skills/validation-score/SKILL.md +35 -0
  170. package/purpose/kits/validation/templates/go-to-market.md +19 -0
  171. package/purpose/kits/validation/templates/operations-readiness.md +20 -0
  172. package/purpose/kits/validation/templates/pricing.md +22 -0
  173. package/purpose/kits/validation/templates/problem-interview.md +36 -0
  174. package/purpose/kits/validation/templates/problem.md +45 -0
  175. package/purpose/kits/validation/templates/product-market-fit.md +23 -0
  176. package/purpose/kits/validation/templates/solution.md +18 -0
  177. package/purpose/kits/validation/templates/team.md +19 -0
  178. package/purpose/kits/validation/weights.json +22 -0
  179. package/purpose/layer.json +53 -0
  180. package/purpose/references/references.json +324 -0
  181. package/purpose/skills/ambition-tier/SKILL.md +205 -0
  182. package/purpose/skills/telos/SKILL.md +52 -0
  183. package/purpose/strategic-foundation.json +133 -0
  184. package/purpose/templates/telos-based-smart.md +39 -0
  185. package/purpose/tooling/integrations.json +48 -0
  186. package/schema/README.md +61 -0
  187. package/schema/VERSION +1 -0
  188. package/schema/account.schema.json +79 -0
  189. package/schema/add-department.mjs +41 -0
  190. package/schema/agent-capability.schema.json +94 -0
  191. package/schema/alignment.schema.json +61 -0
  192. package/schema/assess-problem.mjs +131 -0
  193. package/schema/bdd.mjs +56 -0
  194. package/schema/board.schema.json +101 -0
  195. package/schema/check-boundary.mjs +54 -0
  196. package/schema/check-data-model.mjs +170 -0
  197. package/schema/check-data-packages.mjs +83 -0
  198. package/schema/check-departments.mjs +45 -0
  199. package/schema/check-execution.mjs +85 -0
  200. package/schema/check-governance.mjs +98 -0
  201. package/schema/check-integrations.mjs +33 -0
  202. package/schema/check-nextjs-status.mjs +77 -0
  203. package/schema/check-nouns.mjs +49 -0
  204. package/schema/check-private.mjs +61 -0
  205. package/schema/check-provenance.mjs +55 -0
  206. package/schema/check-publication.mjs +96 -0
  207. package/schema/check-ready.mjs +89 -0
  208. package/schema/check-sources.mjs +66 -0
  209. package/schema/check-team.mjs +94 -0
  210. package/schema/check-telos.mjs +59 -0
  211. package/schema/check-validation.mjs +165 -0
  212. package/schema/check-ventures.mjs +81 -0
  213. package/schema/check-where-used.mjs +58 -0
  214. package/schema/data-master.schema.json +14 -0
  215. package/schema/data-model.json +1258 -0
  216. package/schema/data-model.schema.json +152 -0
  217. package/schema/decision-analytics.mjs +77 -0
  218. package/schema/decision-record.schema.json +290 -0
  219. package/schema/decision-rights.schema.json +34 -0
  220. package/schema/decision.schema.json +30 -0
  221. package/schema/decisions.mjs +321 -0
  222. package/schema/department-catalog.schema.json +55 -0
  223. package/schema/department.schema.json +15 -0
  224. package/schema/departments.mjs +32 -0
  225. package/schema/docket-eval.mjs +89 -0
  226. package/schema/docket.mjs +135 -0
  227. package/schema/eval-suite.schema.json +45 -0
  228. package/schema/evidence.schema.json +110 -0
  229. package/schema/examples/migrate/agent.mjs +12 -0
  230. package/schema/examples/workspace/ventures/studio/accounts/bakery/account.json +18 -0
  231. package/schema/examples/workspace/ventures/studio/evidence/.gitkeep +0 -0
  232. package/schema/examples/workspace/ventures/studio/governance/.gitkeep +0 -0
  233. package/schema/examples/workspace/ventures/studio/knowledge/.gitkeep +0 -0
  234. package/schema/examples/workspace/ventures/studio/operations/.gitkeep +0 -0
  235. package/schema/examples/workspace/ventures/studio/print/knowledge/.gitkeep +0 -0
  236. package/schema/examples/workspace/ventures/studio/print/purpose/.gitkeep +0 -0
  237. package/schema/examples/workspace/ventures/studio/print/venture.json +37 -0
  238. package/schema/examples/workspace/ventures/studio/products/brand-care.json +19 -0
  239. package/schema/examples/workspace/ventures/studio/products/brand-kit.json +23 -0
  240. package/schema/examples/workspace/ventures/studio/provenance.jsonl +3 -0
  241. package/schema/examples/workspace/ventures/studio/purpose/.gitkeep +0 -0
  242. package/schema/examples/workspace/ventures/studio/venture.json +43 -0
  243. package/schema/examples/workspace/ventures/tandem/accounts/.gitkeep +0 -0
  244. package/schema/examples/workspace/ventures/tandem/evidence/README.md +3 -0
  245. package/schema/examples/workspace/ventures/tandem/governance/README.md +3 -0
  246. package/schema/examples/workspace/ventures/tandem/governance/agreements/dee-fast.md +3 -0
  247. package/schema/examples/workspace/ventures/tandem/governance/board.json +51 -0
  248. package/schema/examples/workspace/ventures/tandem/governance/departments/finance/charter.md +5 -0
  249. package/schema/examples/workspace/ventures/tandem/governance/departments/finance/department.json +5 -0
  250. package/schema/examples/workspace/ventures/tandem/governance/departments/primary/marketing-and-sales/charter.md +5 -0
  251. package/schema/examples/workspace/ventures/tandem/governance/departments/primary/marketing-and-sales/department.json +5 -0
  252. package/schema/examples/workspace/ventures/tandem/governance/exit.json +33 -0
  253. package/schema/examples/workspace/ventures/tandem/governance/founder-agreement.json +24 -0
  254. package/schema/examples/workspace/ventures/tandem/governance/founder-agreement.md +3 -0
  255. package/schema/examples/workspace/ventures/tandem/governance/ownership.json +131 -0
  256. package/schema/examples/workspace/ventures/tandem/governance/publication-policy.json +171 -0
  257. package/schema/examples/workspace/ventures/tandem/knowledge/README.md +3 -0
  258. package/schema/examples/workspace/ventures/tandem/knowledge/research/friends-feedback-2026-08.md +3 -0
  259. package/schema/examples/workspace/ventures/tandem/knowledge/research/pilot-2026-10.md +3 -0
  260. package/schema/examples/workspace/ventures/tandem/knowledge/research/problem-interviews-2026-09.md +20 -0
  261. package/schema/examples/workspace/ventures/tandem/knowledge/sources.json +8 -0
  262. package/schema/examples/workspace/ventures/tandem/knowledge/where-used.json +13 -0
  263. package/schema/examples/workspace/ventures/tandem/operations/README.md +3 -0
  264. package/schema/examples/workspace/ventures/tandem/operations/departments/finance/README.md +5 -0
  265. package/schema/examples/workspace/ventures/tandem/operations/departments/finance/department.json +5 -0
  266. package/schema/examples/workspace/ventures/tandem/operations/departments/primary/marketing-and-sales/README.md +5 -0
  267. package/schema/examples/workspace/ventures/tandem/operations/departments/primary/marketing-and-sales/department.json +5 -0
  268. package/schema/examples/workspace/ventures/tandem/pillars/brand.md +5 -0
  269. package/schema/examples/workspace/ventures/tandem/pillars/business.md +5 -0
  270. package/schema/examples/workspace/ventures/tandem/pillars/product.md +5 -0
  271. package/schema/examples/workspace/ventures/tandem/products/.gitkeep +0 -0
  272. package/schema/examples/workspace/ventures/tandem/products/shop-app.json +22 -0
  273. package/schema/examples/workspace/ventures/tandem/profile.md +5 -0
  274. package/schema/examples/workspace/ventures/tandem/provenance.jsonl +13 -0
  275. package/schema/examples/workspace/ventures/tandem/purpose/README.md +3 -0
  276. package/schema/examples/workspace/ventures/tandem/purpose/telos.json +78 -0
  277. package/schema/examples/workspace/ventures/tandem/purpose/validation/go-to-market.json +35 -0
  278. package/schema/examples/workspace/ventures/tandem/purpose/validation/pricing.json +37 -0
  279. package/schema/examples/workspace/ventures/tandem/purpose/validation/problem.json +74 -0
  280. package/schema/examples/workspace/ventures/tandem/purpose/validation/product-market-fit.json +66 -0
  281. package/schema/examples/workspace/ventures/tandem/purpose/validation/solution.json +33 -0
  282. package/schema/examples/workspace/ventures/tandem/purpose/validation/team.json +52 -0
  283. package/schema/examples/workspace/ventures/tandem/venture.json +79 -0
  284. package/schema/examples/workspace/ventures/woodworking/knowledge/.gitkeep +0 -0
  285. package/schema/examples/workspace/ventures/woodworking/venture.json +21 -0
  286. package/schema/examples/workspace/workspace.json +8 -0
  287. package/schema/exit.schema.json +74 -0
  288. package/schema/foundation.mjs +118 -0
  289. package/schema/founder-agreement.schema.json +83 -0
  290. package/schema/gate.mjs +39 -0
  291. package/schema/handoff.schema.json +239 -0
  292. package/schema/icp.schema.json +136 -0
  293. package/schema/idea-graph.schema.json +61 -0
  294. package/schema/init-venture.mjs +98 -0
  295. package/schema/integration.schema.json +143 -0
  296. package/schema/knowledge-sources.schema.json +130 -0
  297. package/schema/layer.json +46 -0
  298. package/schema/layer.schema.json +119 -0
  299. package/schema/load-packages.mjs +26 -0
  300. package/schema/marketing-sitemap.schema.json +232 -0
  301. package/schema/migrate-venture.mjs +192 -0
  302. package/schema/need-now.json +601 -0
  303. package/schema/need-now.mjs +107 -0
  304. package/schema/nextjs-status.schema.json +29 -0
  305. package/schema/ownership.schema.json +282 -0
  306. package/schema/packages.json +4 -0
  307. package/schema/packages.mjs +247 -0
  308. package/schema/payouts.mjs +91 -0
  309. package/schema/port-venture.mjs +161 -0
  310. package/schema/product.schema.json +223 -0
  311. package/schema/provenance-entry.schema.json +154 -0
  312. package/schema/publication-policy.schema.json +293 -0
  313. package/schema/raw-source.schema.json +15 -0
  314. package/schema/reference.schema.json +115 -0
  315. package/schema/route-aliases.schema.json +25 -0
  316. package/schema/run-handoff.mjs +51 -0
  317. package/schema/scaffold/defaults.json +54 -0
  318. package/schema/site-presence.schema.json +337 -0
  319. package/schema/strategic-foundation.schema.json +74 -0
  320. package/schema/stream.schema.json +14 -0
  321. package/schema/team-plan.schema.json +157 -0
  322. package/schema/telos.schema.json +197 -0
  323. package/schema/time-to-ready.mjs +93 -0
  324. package/schema/upgrade-venture.mjs +129 -0
  325. package/schema/use-model.mjs +10 -0
  326. package/schema/validate.py +49 -0
  327. package/schema/validation-evidence.schema.json +123 -0
  328. package/schema/validation-go-to-market.schema.json +110 -0
  329. package/schema/validation-operations-readiness.schema.json +91 -0
  330. package/schema/validation-pricing.schema.json +114 -0
  331. package/schema/validation-problem.schema.json +118 -0
  332. package/schema/validation-product-market-fit.schema.json +135 -0
  333. package/schema/validation-solution.schema.json +79 -0
  334. package/schema/validation-team.schema.json +116 -0
  335. package/schema/venture.schema.json +407 -0
  336. package/schema/vocab/business-models.json +298 -0
  337. package/schema/where-used.schema.json +56 -0
  338. package/schema/workspace.schema.json +38 -0
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env node
2
+ // The decision matrix (EE-054, EE-057): every decision a venture has open, by layer and phase,
3
+ // with its parent, its boundary and whether it renders now. Behaviors:
4
+ // purpose/features/decisions.feature.
5
+ //
6
+ // node schema/decisions.mjs <venture or workspace dir> [--json | --md]
7
+ //
8
+ // Sources: decision records (decisions/*.decision.json beside the record they decide), foundation
9
+ // statements no person has confirmed, and agents' drafts of gated records waiting in _drafts/.
10
+ // Need-now suggestions, failing checks and forecasts are not gathered yet.
11
+ //
12
+ // Like React's error boundaries, a boundary holds for everything under it: the venture root and
13
+ // the foundation fail closed (a product-only workspace, venture.json kind "product", fails open),
14
+ // a product fails open, and any other decision inherits its parent's boundary unless it sets one.
15
+ // Under an open fail-closed decision, children wait and agents only draft (Suspense). A decision
16
+ // made before its parent last changed is stale and shown to the owner again (revalidation). Like
17
+ // a component rendered only when needed, a decision needed in a later phase is held back.
18
+ // In Discovery a decided decision is still a working draft (EE-057). A decision is decided only
19
+ // when a person approved its wording; provenance says who, when and in which phase (EE-058).
20
+ //
21
+ // render: overdue (open and past its due time), now (open, needed in this phase or earlier,
22
+ // nothing above it open and fail-closed),
23
+ // later (needed in a later phase; checked before waiting), waiting, stale, deferred, done, withdrawn.
24
+ // Exit 1 on a duplicate id, an unknown parent, a cycle, or an id that takes a reserved root name.
25
+
26
+ import { spawnSync } from "node:child_process";
27
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
28
+ import { tmpdir } from "node:os";
29
+ import { dirname, join, relative } from "node:path";
30
+ import { FRAME, foundation } from "./foundation.mjs";
31
+ import { agentFromEnv, record as prov } from "../interface/provenance.mjs";
32
+ import { glob as routeFileGlob, routes } from "../interface/router.mjs";
33
+
34
+ const repo = new URL("..", import.meta.url).pathname;
35
+ const load = (f) => JSON.parse(readFileSync(f, "utf8"));
36
+ const MODEL = load(join(repo, "schema/data-model.json"));
37
+ export const PHASES = ["discovery", "validation", "beachhead", "scale", "platform"];
38
+ const glob = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, "(?:[^/]+/)*") + "$");
39
+ const FOUNDATION_RECORDS = new Set(FRAME.questions.flatMap((q) => q.statements).map((s) => s.record).filter(Boolean));
40
+
41
+ /** The record type a venture-relative file belongs to. */
42
+ export function recordOf(file) {
43
+ return MODEL.records.find((r) => r.level === "venture" && r.paths.some((p) => p.startsWith("instance/**/") && glob(p.slice("instance/**/".length)).test(file)))?.id ?? null;
44
+ }
45
+
46
+ // Every *.decision.json in a venture, outside drafts, hidden folders and nested ventures.
47
+ function decisionFiles(dir, sub = "") {
48
+ return readdirSync(join(dir, sub), { withFileTypes: true }).flatMap((e) => {
49
+ const rel = sub ? `${sub}/${e.name}` : e.name;
50
+ if (e.isDirectory()) return e.name.startsWith(".") || e.name.startsWith("_") || existsSync(join(dir, rel, "venture.json")) ? [] : decisionFiles(dir, rel);
51
+ return e.name.endsWith(".decision.json") && sub.split("/").at(-1) === "decisions" ? [rel] : [];
52
+ });
53
+ }
54
+
55
+ /** Every decision of a venture, resolved: boundary, render state and what holds it. */
56
+ export function decisions(dir, now = Date.parse(process.env.DECISIONS_NOW ?? "") || Date.now()) {
57
+ const venture = load(join(dir, "venture.json"));
58
+ const phase = PHASES.indexOf(venture.phase);
59
+ const productOnly = venture.kind === "product";
60
+ const report = foundation(dir);
61
+ const statements = report.flatMap((q) => q.statements);
62
+ const settled = statements.filter((s) => s.status !== "planned").every((s) => s.status === "confirmed");
63
+ const errors = [];
64
+
65
+ const list = [
66
+ { id: "venture", question: `Build ${venture.name} as a venture`, source: "root", layer: "governance", parent: null, boundary: productOnly ? "fail-open" : "fail-closed", status: "decided", decided_at: null },
67
+ { id: "foundation", question: `Settle the strategic foundation (${statements.filter((s) => !["confirmed", "planned"].includes(s.status)).length} statements open)`, source: "root", layer: "purpose", parent: "venture", boundary: productOnly ? "fail-open" : "fail-closed",
68
+ status: settled ? "decided" : "open", decided_at: null, needed_in: venture.phase },
69
+ ];
70
+ for (const f of decisionFiles(dir)) {
71
+ const d = load(join(dir, f));
72
+ // decisions/<record>[.<topic>].decision.json decides <record>.json in the folder that holds decisions/.
73
+ const at = f.split("/").slice(0, -2).join("/"), base = f.split("/").at(-1).split(".")[0];
74
+ const path = d.decides?.path ?? (at ? `${at}/${base}.json` : `${base}.json`);
75
+ const record = d.decides?.record ?? recordOf(path);
76
+ const layer = d.layer ?? MODEL.records.find((r) => r.id === record)?.layer ?? f.split("/")[0];
77
+ if (["venture", "foundation"].includes(d.id)) errors.push(`${f}: ${d.id} is a reserved root`);
78
+ // Provenance (EE-058): a decision is decided only when a person approved its wording, at a
79
+ // time and in a phase; an agent's wording waits for that approval.
80
+ const pv = d.provenance;
81
+ if (d.status === "decided" && !pv?.approved_by) errors.push(`${f}: decided, but names no one who decided`);
82
+ else if (d.status === "decided" && (!pv.approved_at || !pv.phase)) errors.push(`${f}: decided, but its provenance has no ${!pv.approved_at ? "approved_at" : "phase"}`);
83
+ if (pv?.approved_by && d.decided_by && pv.approved_by !== d.decided_by) errors.push(`${f}: decided_by ${d.decided_by} differs from provenance.approved_by ${pv.approved_by}`);
84
+ if (pv?.phase && PHASES.indexOf(pv.phase) > phase) errors.push(`${f}: approved in ${pv.phase}, a phase the venture has not reached`);
85
+ for (const o of d.outcomes ?? []) if (["met", "partly", "missed"].includes(o.result) && !o.actual) errors.push(`${f}: outcome ${o.id} is ${o.result} but records no actual outcome`);
86
+ const boundary = d.boundary && d.boundary !== "inherit" ? d.boundary : record === "product" ? "fail-open" : "inherit";
87
+ // A decision that settles a foundation statement is part of the foundation, not under it. One
88
+ // about a foundation record waits on that record's open statement, set below, not the whole foundation.
89
+ const parent = d.parent ?? (d.statement ? "venture" : null);
90
+ list.push({ ...d, file: f, source: "file", decides: { record, path }, layer, boundary, parent, ...(d.statement ? { part_of: "foundation" } : {}) });
91
+ }
92
+ const covered = new Set(list.map((d) => d.statement).filter(Boolean));
93
+ // The roll-up counts only the statements still open after decision records stand in for some.
94
+ list[1].question = `Settle the strategic foundation (${statements.filter((s) => !["confirmed", "planned"].includes(s.status) && !s.derived_from && !list.some((d) => d.statement === s.id && d.status === "decided")).length} statements open)`;
95
+ // A foundation statement no person has confirmed is a decision the founder still owes. It is
96
+ // part of the foundation rather than under it, so the foundation does not hold it.
97
+ for (const s of statements.filter((x) => !["confirmed", "planned"].includes(x.status) && !x.derived_from && !covered.has(x.id))) {
98
+ const question = { missing: `Write the ${s.id} statement`, draft: `Accept or change the agent's draft of the ${s.id} statement`, agent: `Confirm the ${s.id} statement an agent wrote` }[s.status] ?? `Confirm the ${s.id} statement (${s.status})`;
99
+ list.push({ id: `foundation-${s.id}`, question, source: "foundation", statement_status: s.status, layer: MODEL.records.find((r) => r.id === s.record)?.layer ?? "purpose",
100
+ decides: { record: s.record }, parent: "venture", part_of: "foundation", boundary: "inherit", status: "open", needed_in: venture.phase });
101
+ }
102
+ const statementFor = (record) => list.find((x) => x.part_of === "foundation" && x.status === "open" && x.decides?.record === record);
103
+ // An agent's draft of a gated record waits for a person to accept it.
104
+ const drafts = existsSync(join(dir, "_drafts")) ? readdirSync(join(dir, "_drafts")).filter((f) => f.endsWith(".draft")) : [];
105
+ for (const d of drafts) {
106
+ const path = d.replace(/\.draft$/, "").replace(/__/g, "/");
107
+ const record = recordOf(path);
108
+ // A draft of a foundation record is settled through its statement, which says to accept it.
109
+ const s = statementFor(record);
110
+ if (s?.statement_status === "draft") { s.decides = { record, path }; continue; }
111
+ list.push({ id: `draft-${path.replace(/[^a-z0-9]+/g, "-").replace(/-json$/, "")}`, question: `Accept or reject the agent's draft of ${path}`, source: "draft",
112
+ layer: MODEL.records.find((r) => r.id === record)?.layer ?? path.split("/")[0], decides: { record, path }, parent: FOUNDATION_RECORDS.has(record) ? (statementFor(record)?.id ?? "venture") : "venture",
113
+ boundary: "inherit", status: "open", needed_in: venture.phase });
114
+ }
115
+ // A decision about a foundation record waits on that record's open statement only, so one open
116
+ // statement does not hold every decision about the foundation.
117
+ for (const d of list) if (d.parent === null && d.source === "file") d.parent = (FOUNDATION_RECORDS.has(d.decides?.record) ? statementFor(d.decides.record)?.id : null) ?? "venture";
118
+
119
+ const byId = new Map();
120
+ for (const d of list) { if (byId.has(d.id)) errors.push(`decision ${d.id}: duplicate id`); else byId.set(d.id, d); }
121
+ for (const d of list) if (d.parent && !byId.has(d.parent)) { errors.push(`decision ${d.id}: unknown parent ${d.parent}`); d.parent = "venture"; }
122
+ const ancestors = (d) => {
123
+ const out = [];
124
+ for (let p = byId.get(d.parent); p; p = byId.get(p.parent)) {
125
+ if (p === d || out.includes(p)) { errors.push(`decision ${d.id}: its parents form a cycle`); break; }
126
+ out.push(p);
127
+ }
128
+ return out;
129
+ };
130
+ const chains = new Map(list.map((d) => [d.id, ancestors(d)]));
131
+ const boundaryOf = (d) => d.boundary !== "inherit" ? d.boundary : (chains.get(d.id).find((a) => a.boundary !== "inherit")?.boundary ?? "fail-closed");
132
+
133
+ const resolved = list.map((d) => {
134
+ const boundary = boundaryOf(d);
135
+ // An open decision above that fails closed holds this one. As with React's error boundaries the
136
+ // nearest boundary governs: a decision that fails open (a product) shields everything under it
137
+ // from the fail-closed decisions above it.
138
+ const holder = (() => {
139
+ if (d.boundary === "fail-open") return null;
140
+ for (const a of chains.get(d.id)) {
141
+ if (a.boundary === "fail-open") return null;
142
+ if (a.status !== "decided" && boundaryOf(a) === "fail-closed") return a;
143
+ }
144
+ return null;
145
+ })();
146
+ const parent = byId.get(d.parent);
147
+ const at = (x) => x?.provenance?.approved_at ?? x?.decided_at;
148
+ const stale = d.status === "decided" && at(d) && at(parent) && at(parent) > at(d);
149
+ // A due time is a real deadline: it overrides needed_in, and an open decision past it is overdue.
150
+ const later = !d.due && d.needed_in && PHASES.indexOf(d.needed_in) > phase;
151
+ const overdue = d.status === "open" && d.due && Date.parse(d.due) < now;
152
+ const render = d.status === "withdrawn" ? "withdrawn" : d.status === "deferred" ? "deferred" : stale ? "stale" : overdue ? "overdue"
153
+ : d.status === "decided" ? "done" : later ? "later" : holder ? "waiting" : "now";
154
+ const firmness = d.status !== "decided" ? "open" : venture.phase === "discovery" ? "working" : "firm";
155
+ // Lane (EE-060): a fail-closed decision is on the venture's ordered path, so what is under it waits;
156
+ // a fail-open one can move on its own, beside the rest, at whatever priority it ranks.
157
+ const lane = boundary === "fail-open" ? "async" : "sync";
158
+ return { id: d.id, question: d.question, source: d.source, layer: d.layer, parent: d.parent, boundary, lane, needed_in: d.needed_in ?? null, status: d.status, render, firmness,
159
+ ...(render === "waiting" || (overdue && holder) ? { held_by: holder.id } : {}),
160
+ ...(d.due ? { due: d.due, due_in_hours: Math.round((Date.parse(d.due) - now) / 36e5) } : {}), ...(d.part_of ? { part_of: d.part_of } : {}), ...(d.decides ? { decides: d.decides } : {}), ...(d.file ? { file: d.file } : {}), ...(d.answer ? { answer: d.answer } : {}), ...(d.ideas?.length ? { ideas: d.ideas } : {}),
161
+ ...(d.status === "open" && d.answer && d.provenance?.drafted_by?.kind === "agent" ? { awaiting: "approval" } : {}),
162
+ ...(d.provenance ? { provenance: d.provenance } : {}),
163
+ ...(d.outcomes?.length ? { outcomes: { total: d.outcomes.length, pending: d.outcomes.filter((o) => (o.result ?? "pending") === "pending").length,
164
+ due: d.outcomes.filter((o) => (o.result ?? "pending") === "pending" && /^\d{4}-\d{2}-\d{2}/.test(o.by ?? "") && Date.parse(o.by) < now).map((o) => o.id) } } : {}),
165
+ ...(d.unlocks?.length ? { unlocks: d.unlocks.map((u) => u.activity) } : {}),
166
+ ...(["open", "deferred"].includes(d.status) ? { action: actionFor(d, venture.id) } : {}) };
167
+ });
168
+ return { venture: venture.id, phase: venture.phase, kind: productOnly ? "product" : "venture", errors, decisions: rank(resolved, venture.phase, now) };
169
+ }
170
+
171
+ // The route that writes a venture-relative file, for the commands a founder runs.
172
+ function routeOf(path, record) {
173
+ const r = routes().find((x) => x.kind === "record" && (path ? x.files.some((g) => routeFileGlob(g).test(path)) : x.record === record && x.collection !== "item"));
174
+ if (!r) return null;
175
+ return r.collection === "item" && path ? r.route.replace("[id]", path.split("/").at(-1).replace(/\.json$/, "")) : r.route;
176
+ }
177
+
178
+ /** How a person settles an open decision: the command to run, and the word to type to confirm it. */
179
+ export function actionFor(d, venture) {
180
+ const v = `--venture ${venture}`;
181
+ if (d.source === "file") return { kind: "decide", confirm: d.id, command: `mentura decide ${d.id} ${v} --by <your name> [--answer "<your words>"]` };
182
+ const route = routeOf(d.decides?.path, d.decides?.record);
183
+ if (d.source === "draft" || d.statement_status === "draft") return { kind: "accept", route, confirm: route, command: `mentura accept ${route} ${v}` };
184
+ if (d.source === "foundation" && d.statement_status === "agent") { const id = d.id.replace(/^foundation-/, ""); return { kind: "confirm", statement: id, confirm: id, command: `mentura confirm ${id} ${v}` }; }
185
+ if (d.source === "foundation") return { kind: "write", route, command: `write ${route}${route ? "" : ` (${d.decides?.record})`} yourself, or ask an agent to draft it` };
186
+ return { kind: "settle", command: d.question };
187
+ }
188
+
189
+ /** A decision's own weight, before anything that waits on it (EE-057): a deadline, a stale parent, what
190
+ * it unlocks, whether it settles the foundation, a draft ready to approve, outcomes to test. Scored for
191
+ * every open decision, so a gate can carry the weight of what it holds (schema/docket.mjs). */
192
+ export function ownScore(d, phase, now = Date.now()) {
193
+ const why = [];
194
+ let s = 0;
195
+ if (d.render === "overdue") { s += 100; why.push("past its due time"); }
196
+ // Cost of delay grows as the deadline nears: 40 at three days, 60 at two, 120 at one, up to 300.
197
+ else if (d.due) { const h = (Date.parse(d.due) - now) / 36e5; if (h <= 72) { s += Math.min(300, Math.round((60 * 48) / Math.max(h, 1))); why.push(`due in ${Math.max(0, Math.round(h))}h`); } }
198
+ if (d.render === "stale") { s += 30; why.push("its parent changed after it was decided"); }
199
+ if (d.unlocks?.length) { s += 10 * d.unlocks.length; why.push(`unlocks ${d.unlocks.join("; ")}`); }
200
+ if (d.part_of === "foundation") { s += phase === "discovery" ? 20 : 10; why.push("settles the strategic foundation"); }
201
+ if (d.awaiting) { s += 8; why.push("an agent has drafted it: approving takes a minute"); }
202
+ if (d.outcomes?.total) { s += 3 * d.outcomes.total; why.push(`${d.outcomes.total} expected outcome${d.outcomes.total > 1 ? "s" : ""} to test`); }
203
+ return { score: s, why };
204
+ }
205
+
206
+ /** Which open decisions matter most now, and why (EE-057): a decision's own weight plus 12 for each
207
+ * decision that waits on it. Ranks the decisions that render now, overdue or stale; the others keep
208
+ * rank null. Higher first; ties keep their order. */
209
+ export function rank(ds, phase, now = Date.now()) {
210
+ const below = (id) => ds.filter((x) => x.held_by === id || x.parent === id).length;
211
+ const scored = ds.map((d) => {
212
+ if (!["now", "overdue", "stale"].includes(d.render) || d.source === "root") return { ...d, rank: null };
213
+ const { score, why } = ownScore(d, phase, now);
214
+ const n = below(d.id);
215
+ // Kept in the order the reasons were always given: deadline and staleness first, then what waits.
216
+ if (n) why.splice(why.findIndex((w) => !/due|parent changed/.test(w)) === -1 ? why.length : why.findIndex((w) => !/due|parent changed/.test(w)), 0, `${n} decision${n > 1 ? "s" : ""} wait on it`);
217
+ return { ...d, rank: score + 12 * n, why };
218
+ });
219
+ const order = scored.map((d, i) => [d, i]).sort(([a, i], [b, j]) => (b.rank ?? -1) - (a.rank ?? -1) || i - j).map(([d]) => d);
220
+ return order;
221
+ }
222
+
223
+ export function holding(dir, file) {
224
+ if (!existsSync(join(dir, "venture.json"))) return null;
225
+ const record = recordOf(file);
226
+ const m = decisions(dir).decisions.find((d) => d.source === "file" && (d.decides?.path === file || (!d.decides?.path && d.decides?.record === record))
227
+ && (d.render === "waiting" || d.render === "stale" || (d.status === "open" && d.boundary === "fail-closed")));
228
+ return m ? { id: m.id, why: m.render === "waiting" ? `waits on ${m.held_by}` : m.render === "stale" ? "is stale" : "is open and fails closed" } : null;
229
+ }
230
+
231
+ // Routes an unlock names are globs over routes, as in the team plan's robots.
232
+ const routeGlob = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*") + "$");
233
+
234
+ /** The decision that keeps a route closed to an actor (EE-058): an undecided decision that unlocks it. */
235
+ export function locked(dir, route, actorId) {
236
+ if (!existsSync(join(dir, "venture.json"))) return null;
237
+ const files = decisionFiles(dir).map((f) => load(join(dir, f)));
238
+ const d = files.find((x) => x.status !== "decided" && x.status !== "withdrawn" && (x.unlocks ?? []).some((u) =>
239
+ (u.routes ?? []).some((g) => routeGlob(g).test(route)) && (!u.for?.length || !actorId || u.for.includes(actorId))));
240
+ return d ? { id: d.id, question: d.question } : null;
241
+ }
242
+
243
+ const validate = (d) => {
244
+ const scratch = mkdtempSync(join(tmpdir(), "decision-"));
245
+ const f = join(scratch, "d.json");
246
+ writeFileSync(f, JSON.stringify(d));
247
+ const v = spawnSync("python3", [join(repo, "schema/validate.py"), "--schema", "decision-record.schema.json", "--file", f], { encoding: "utf8" });
248
+ rmSync(scratch, { recursive: true, force: true });
249
+ return v.status === 0 ? null : (v.stdout + v.stderr).trim();
250
+ };
251
+ const fileOf = (dir, id) => decisionFiles(dir).find((f) => load(join(dir, f)).id === id);
252
+
253
+ /** An agent proposes a decision, or new wording for an open one. It stays open until a person
254
+ * approves it: an agent never decides (EE-058). at: the folder beside the record it decides. */
255
+ export function propose(dir, input, { agentId, via = "router", harness, model } = {}) {
256
+ const { at = "", name, ...rest } = input ?? {};
257
+ if (!rest.id || !rest.question) return { ok: false, error: "a decision needs an id and a question" };
258
+ if (rest.status && rest.status !== "open") return { ok: false, error: "an agent may only propose an open decision; a person approves it" };
259
+ const existing = fileOf(dir, rest.id);
260
+ if (existing && load(join(dir, existing)).status === "decided") return { ok: false, error: `${rest.id} is decided; only a person can change it` };
261
+ const file = existing ?? `${at ? `${at}/` : ""}decisions/${name ?? rest.id}.decision.json`;
262
+ if (!existing && !/^([a-z0-9-]+\/)*decisions\/[a-z0-9.-]+\.decision\.json$/.test(file)) return { ok: false, error: `${file} is not a decisions/ path` };
263
+ // New wording for an open decision keeps what the agent did not restate: its parent, boundary, unlocks.
264
+ const before = existing ? load(join(dir, existing)) : {};
265
+ const d = { ...before, ...rest, status: "open", provenance: { drafted_by: { kind: "agent", ...(agentId ? { id: agentId } : {}) } } };
266
+ const bad = validate(d);
267
+ if (bad) return { ok: false, error: `does not match schema/decision-record.schema.json:\n${bad}` };
268
+ mkdirSync(dirname(join(dir, file)), { recursive: true });
269
+ writeFileSync(join(dir, file), JSON.stringify(d, null, 2) + "\n");
270
+ const e = prov(dir, { entity: file, route: "governance/decisions", activity: "draft", agent: agentFromEnv("agent", via, { ...(agentId ? { id: agentId } : {}), ...(harness ? { harness } : {}), ...(model ? { model } : {}) }), note: `proposed decision ${d.id}` });
271
+ return { ok: true, result: { proposed: d.id, file, awaiting: "approval", provenance: e.hash } };
272
+ }
273
+
274
+ /** A person approves a decision: as worded, or with their own answer. */
275
+ export function approve(dir, id, { by, answer, via = "cli", note } = {}) {
276
+ const file = fileOf(dir, id);
277
+ if (!file) return { ok: false, error: `no decision ${id}` };
278
+ if (!by) return { ok: false, error: "approving needs the person's name (--by)" };
279
+ const d = load(join(dir, file));
280
+ if (d.status === "decided") return { ok: false, error: `${id} is already decided` };
281
+ if (!answer && !d.answer) return { ok: false, error: `${id} has no wording to approve; give an answer` };
282
+ const drafted = d.provenance?.drafted_by ?? { kind: "person", id: by };
283
+ const wording = !d.answer || drafted.kind === "framework" ? "own" : answer && answer !== d.answer ? (drafted.kind === "person" && drafted.id === by ? "own" : "changed") : drafted.kind === "person" ? "own" : "approved";
284
+ const phase = load(join(dir, "venture.json")).phase;
285
+ const out = { ...d, status: "decided", answer: answer ?? d.answer, provenance: { ...d.provenance, drafted_by: drafted, approved_by: by, approved_at: new Date().toISOString().replace(/\.\d+Z$/, "Z"), phase, wording, via } };
286
+ const bad = validate(out);
287
+ if (bad) return { ok: false, error: bad };
288
+ writeFileSync(join(dir, file), JSON.stringify(out, null, 2) + "\n");
289
+ const e = prov(dir, { entity: file, route: "governance/decisions", activity: "accept", agent: agentFromEnv("person", via, { id: by }), note: note ?? `approved decision ${id} (${wording})` });
290
+ return { ok: true, result: { decided: id, wording, phase, provenance: e.hash, unlocks: (out.unlocks ?? []).map((u) => u.activity) } };
291
+ }
292
+
293
+ /** The matrix: for each layer, the decisions that render now, wait, or come later, by phase. */
294
+ export function matrix(r) {
295
+ const live = r.decisions.filter((d) => !["done", "withdrawn"].includes(d.render) && d.source !== "root");
296
+ const layers = [...new Set(live.map((d) => d.layer))].sort();
297
+ return layers.map((l) => ({ layer: l, ...Object.fromEntries(PHASES.map((p) => [p, live.filter((d) => d.layer === l && (d.needed_in ?? r.phase) === p).map((d) => `${d.id} (${d.render})`)])) }));
298
+ }
299
+
300
+ if (import.meta.url === `file://${process.argv[1]}`) {
301
+ const arg = process.argv[2];
302
+ if (!arg) { console.error("usage: node schema/decisions.mjs <venture or workspace dir> [--json | --md]"); process.exit(2); }
303
+ const walk = (d) => existsSync(join(d, "venture.json")) ? [d] : readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_")).flatMap((e) => walk(join(d, e.name)));
304
+ const dirs = walk(arg);
305
+ const rs = dirs.map((d) => ({ dir: d, ...decisions(d) }));
306
+ if (process.argv.includes("--json")) console.log(JSON.stringify(rs.length === 1 ? rs[0] : rs, null, 2));
307
+ else for (const r of rs) {
308
+ console.log(`\n${relative(process.cwd(), r.dir) || "."} (${r.phase}, ${r.kind})`);
309
+ const order = ["overdue", "stale", "now", "waiting", "later", "deferred", "done"];
310
+ const byDue = (a, b) => (b.rank ?? -1) - (a.rank ?? -1) || (a.due ?? "~").localeCompare(b.due ?? "~");
311
+ for (const state of order) for (const d of r.decisions.filter((x) => x.render === state && x.source !== "root" || (x.id === "foundation" && x.render === state)).sort(byDue)) console.log(` ${state.padEnd(8)} ${d.id.padEnd(30)} ${d.boundary.padEnd(11)} ${d.layer.padEnd(10)} ${d.question}${d.held_by ? ` [held by ${d.held_by}]` : ""}${d.render === "done" ? ` [${d.firmness}${d.provenance?.approved_by ? `, approved by ${d.provenance.approved_by} in ${d.provenance.phase}` : ""}]` : ""}${d.awaiting ? " [agent's wording, awaiting approval]" : ""}${d.due && d.render !== "done" ? ` [due ${d.due}, ${d.due_in_hours}h]` : ""}${d.outcomes?.due?.length ? ` [outcomes due: ${d.outcomes.due.join(", ")}]` : ""}${d.why?.length ? `\n why now: ${d.why.join("; ")}` : ""}`);
312
+ if (process.argv.includes("--md")) {
313
+ const m = matrix(r);
314
+ console.log(`\n| layer | ${PHASES.join(" | ")} |\n| --- | ${PHASES.map(() => "---").join(" | ")} |`);
315
+ for (const row of m) console.log(`| ${row.layer} | ${PHASES.map((p) => row[p].join("<br>")).join(" | ")} |`);
316
+ }
317
+ }
318
+ const errs = rs.flatMap((r) => r.errors.map((e) => `${r.dir}: ${e}`));
319
+ for (const e of errs) console.error(`FAIL ${e}`);
320
+ process.exit(errs.length ? 1 : 0);
321
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "department-catalog.schema.json",
4
+ "title": "Department catalog",
5
+ "description": "The departments a venture can import (EE-047): APQC Process Classification Framework categories, each tagged with its place in Porter's value chain and given need-now priors (EE-028). The framework's copy is operations/departments.json.",
6
+ "type": "object",
7
+ "required": ["version", "departments"],
8
+ "properties": {
9
+ "version": { "type": "string" },
10
+ "note": { "type": "string" },
11
+ "departments": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["id", "label", "apqc", "value_chain", "porter", "governance", "operations", "base", "why"],
16
+ "properties": {
17
+ "id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
18
+ "label": { "type": "string" },
19
+ "apqc": {
20
+ "type": "object",
21
+ "required": ["id", "name"],
22
+ "properties": { "id": { "type": "string", "pattern": "^\\d+\\.0$" }, "name": { "type": "string" } },
23
+ "additionalProperties": false
24
+ },
25
+ "value_chain": { "enum": ["primary", "support"] },
26
+ "porter": { "type": "string", "description": "The value-chain activity it falls under (Porter 1985)" },
27
+ "governance": { "type": "string", "description": "What its folder in the governance layer holds" },
28
+ "operations": { "type": "string", "description": "What its folder in the operations layer holds" },
29
+ "base": {
30
+ "type": "object",
31
+ "description": "Probability a venture needs it, per phase",
32
+ "additionalProperties": { "type": "number", "minimum": 0, "maximum": 1 }
33
+ },
34
+ "when": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "required": ["if"],
39
+ "properties": {
40
+ "if": { "type": "string" },
41
+ "phase": { "type": "string" },
42
+ "at_least": { "type": "number", "minimum": 0, "maximum": 1 },
43
+ "at_most": { "type": "number", "minimum": 0, "maximum": 1 }
44
+ },
45
+ "additionalProperties": false
46
+ }
47
+ },
48
+ "why": { "type": "string" }
49
+ },
50
+ "additionalProperties": false
51
+ }
52
+ }
53
+ },
54
+ "additionalProperties": false
55
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "department.schema.json",
4
+ "title": "Department",
5
+ "description": "The tag in each of a venture's department folders (EE-047): which department the folder is and where it sits in the value chain. It lives in the folder at <layer>/departments/<id>/department.json, or under an optional grouping folder, <layer>/departments/primary/<id>/ or support/<id>/, which is for looking at only; the tag, not the folder, says what the department is. Its values must match operations/departments.json.",
6
+ "type": "object",
7
+ "required": ["department", "value_chain", "apqc"],
8
+ "properties": {
9
+ "department": { "type": "string", "pattern": "^[a-z0-9-]+$" },
10
+ "value_chain": { "enum": ["primary", "support"] },
11
+ "apqc": { "type": "string", "pattern": "^\\d+\\.0$", "description": "The APQC category id" },
12
+ "owner": { "type": "string", "description": "The role that heads it, from the roles register" }
13
+ },
14
+ "additionalProperties": false
15
+ }
@@ -0,0 +1,32 @@
1
+ // Departments (EE-047): the catalog, and the department folders a venture holds. A department
2
+ // folder is any folder under <layer>/departments/ that holds a department.json; it may sit under
3
+ // an optional grouping folder named for its place in the value chain (primary/ or support/).
4
+
5
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ const root = new URL("..", import.meta.url).pathname;
9
+ export const LAYERS = ["governance", "operations"];
10
+ export const GROUPS = ["primary", "support"];
11
+
12
+ export const catalog = () => JSON.parse(readFileSync(join(root, "operations/departments.json"), "utf8")).departments;
13
+
14
+ /** Every department folder in a venture: { layer, path (relative to the venture), folder, group, tag }. */
15
+ export function foldersOf(dir) {
16
+ const out = [];
17
+ for (const layer of LAYERS) {
18
+ const base = join(dir, layer, "departments");
19
+ if (!existsSync(base)) continue;
20
+ for (const e of readdirSync(base, { withFileTypes: true }).filter((e) => e.isDirectory())) {
21
+ const inside = GROUPS.includes(e.name) && !existsSync(join(base, e.name, "department.json"))
22
+ ? readdirSync(join(base, e.name), { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => [e.name, x.name])
23
+ : [[null, e.name]];
24
+ for (const [group, folder] of inside) {
25
+ const rel = join(layer, "departments", ...(group ? [group] : []), folder);
26
+ const file = join(dir, rel, "department.json");
27
+ out.push({ layer, path: rel, folder, group, tag: existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null });
28
+ }
29
+ }
30
+ }
31
+ return out;
32
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ // Evidence for the docket (EE-075): scores its next step against venture states where the right next
3
+ // step is argued from a cited source (schema/examples/docket-scenarios.json), and builds the same
4
+ // question for an independent judge, so the docket, the judge and the source can be compared.
5
+ //
6
+ // node schema/docket-eval.mjs [--json] the docket against the scenarios
7
+ // node schema/docket-eval.mjs request | <judge> > a.json the judge's request (Jev: platform/integrations/jev.mjs)
8
+ // node schema/docket-eval.mjs --answers a.json [--json] the docket and the judge against the scenarios
9
+ //
10
+ // Per state: does the docket's next decision match the expected one, and its leading function? For the
11
+ // judge, each open decision the founder can act on is one question ("is this the best next step?") and
12
+ // the judge's pick is the one it is most sure of. A day scenario is scored at every step, settling the
13
+ // expected decision before the next, so a miss early does not cascade.
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import { rankDocket } from "./docket.mjs";
17
+
18
+ const FILE = new URL("examples/docket-scenarios.json", import.meta.url).pathname;
19
+ const NOW = Date.parse("2026-10-29T12:00:00Z");
20
+ const TITLE = { foundation: "Foundation", validation: "Validation", product: "Product", marketing: "Marketing and sales", finance: "Finance", people: "People", legal: "Legal and governance", operations: "Operations" };
21
+
22
+ /** The decisions as decisions.mjs would resolve them, after the given ones are settled. */
23
+ function resolve(decisions, settled) {
24
+ const open = decisions.filter((d) => !settled.has(d.id));
25
+ return open.map((d) => {
26
+ const held = d.held_by && !settled.has(d.held_by);
27
+ const render = d.state === "waiting" ? (held ? "waiting" : "now") : d.state;
28
+ return { id: d.id, question: d.question, source: "file", render, ...(held ? { held_by: d.held_by } : {}),
29
+ ...(d.due_in_hours != null ? { due: new Date(NOW + d.due_in_hours * 36e5).toISOString() } : {}),
30
+ ...(d.unlocks?.length ? { unlocks: d.unlocks } : {}), ...(d.foundation ? { part_of: "foundation" } : {}),
31
+ ...(d.drafted ? { awaiting: "approval" } : {}), ...(d.outcomes ? { outcomes: { total: d.outcomes, pending: d.outcomes } } : {}),
32
+ function: { id: d.function, title: TITLE[d.function] ?? d.function } };
33
+ });
34
+ }
35
+
36
+ /** Every moment to score: one per scenario, or one per step of a day. */
37
+ export function states(file = FILE) {
38
+ const { scenarios } = JSON.parse(readFileSync(file, "utf8"));
39
+ return scenarios.flatMap((s) => {
40
+ const steps = s.steps ?? [s.expected];
41
+ const settled = new Set();
42
+ return steps.map((exp, i) => {
43
+ const st = { id: steps.length > 1 ? `${s.id}#${i + 1}` : s.id, scenario: s, phase: s.phase, decisions: resolve(s.decisions, settled), expected: exp };
44
+ settled.add(exp.decision);
45
+ return st;
46
+ });
47
+ });
48
+ }
49
+
50
+ const actionable = (st) => st.decisions.filter((d) => ["now", "overdue", "stale"].includes(d.render));
51
+
52
+ export function request(list = states()) {
53
+ const questions = {};
54
+ for (const st of list) for (const d of actionable(st))
55
+ questions[`next:${st.id}:${d.id}`] = { type: "noul", instructions: `In the venture state \`states.${st.id}\`, is the open decision \`${d.id}\` the single best next step for the founder to settle now, before every other open decision listed there?`,
56
+ criteria: { true: "Yes: this one should come first", false: "No: another listed decision should come first" } };
57
+ const state = Object.fromEntries(list.map((st) => [st.id, { phase: st.phase, situation: st.scenario.situation,
58
+ open: Object.fromEntries(st.decisions.map((d) => [d.id, `${d.question} [${d.function.title}; ${d.render}${d.held_by ? `, waits on ${d.held_by}` : ""}${d.due ? `; due ${d.due}` : ""}]`])) }]));
59
+ return { state: { guidelines: "Each state is a venture at one moment: its phase, its situation, and its open decisions with their function and whether each can be settled now or waits on another. Judge as an experienced startup advisor would.", states: state }, model: "jev-latest", questions };
60
+ }
61
+
62
+ export function score(answers = null, list = states()) {
63
+ const rows = list.map((st) => {
64
+ const k = rankDocket(st.decisions, st.phase, NOW);
65
+ const judge = answers ? actionable(st).map((d) => {
66
+ const a = answers[`next:${st.id}:${d.id}`];
67
+ return { id: d.id, fn: d.function.id, p: typeof a?.noul === "number" ? a.noul : a?.noul === true ? 1 : 0 };
68
+ }).sort((a, b) => b.p - a.p)[0] : null;
69
+ return { state: st.id, expected: st.expected, docket: { decision: k.next?.decision, function: k.next?.function_id, dominant: k.next?.dominant, leading: k.functions.map((f) => `${f.id} ${f.attention}`) },
70
+ ...(judge ? { judge: { decision: judge.id, function: judge.fn, p: Math.round(judge.p * 100) / 100 } } : {}),
71
+ docket_right: k.next?.decision === st.expected.decision, docket_function_right: k.next?.function_id === st.expected.function,
72
+ ...(judge ? { judge_right: judge.id === st.expected.decision, agree: judge.id === k.next?.decision } : {}) };
73
+ });
74
+ const rate = (k) => Math.round((rows.filter((r) => r[k]).length / rows.length) * 100) / 100;
75
+ return { states: rows.length, docket_next: rate("docket_right"), docket_function: rate("docket_function_right"),
76
+ ...(answers ? { judge_next: rate("judge_right"), docket_judge_agree: rate("agree") } : {}), rows };
77
+ }
78
+
79
+ if (import.meta.url === `file://${process.argv[1]}`) {
80
+ if (process.argv[2] === "request") { console.log(JSON.stringify(request())); process.exit(0); }
81
+ const i = process.argv.indexOf("--answers");
82
+ const answers = i >= 0 ? JSON.parse(readFileSync(process.argv[i + 1], "utf8")).answers : null;
83
+ const r = score(answers);
84
+ if (process.argv.includes("--json")) console.log(JSON.stringify(r, null, 2));
85
+ else {
86
+ for (const x of r.rows) console.log(`${x.docket_right ? "right" : "MISS "} ${x.state.padEnd(34)} expected ${x.expected.decision.padEnd(22)} docket ${String(x.docket.decision).padEnd(22)}${x.judge ? ` judge ${x.judge.decision} (${x.judge.p})${x.judge_right ? "" : " MISS"}` : ""}`);
87
+ console.log(`\n${r.states} states: the docket picked the expected next step in ${Math.round(r.docket_next * 100)}% and the expected function in ${Math.round(r.docket_function * 100)}%${answers ? `; the judge picked it in ${Math.round(r.judge_next * 100)}%, and agreed with the docket in ${Math.round(r.docket_judge_agree * 100)}%` : ""}`);
88
+ }
89
+ }