naive-framework 0.1.0 → 0.2.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 (254) hide show
  1. package/AGENTS.md +2 -1
  2. package/docs/decisions/decisions.json +27 -0
  3. package/docs/reference/ROUTES.md +36 -37
  4. package/governance/layer.json +2 -1
  5. package/governance/venture-template/roles/venture-roles.json +62 -0
  6. package/interface/acting.mjs +83 -0
  7. package/interface/adapters/claude-code.json +16 -0
  8. package/interface/adapters/codex.json +14 -0
  9. package/interface/adapters/cursor.json +16 -0
  10. package/interface/adapters.mjs +182 -0
  11. package/interface/api/server.mjs +4 -2
  12. package/interface/boundaries.mjs +218 -0
  13. package/interface/brand.mjs +214 -0
  14. package/interface/cli/freshness.mjs +59 -0
  15. package/interface/cli/guard.mjs +6 -0
  16. package/interface/cli/mentura.mjs +78 -8
  17. package/interface/cli/naive.mjs +33 -8
  18. package/interface/context.mjs +524 -0
  19. package/interface/demo/board.mjs +1 -1
  20. package/interface/dev/setup.mjs +84 -0
  21. package/interface/gate-hooks.mjs +36 -0
  22. package/interface/harness/evals.mjs +3 -3
  23. package/interface/harness/run.mjs +4 -4
  24. package/interface/instrumentation.mjs +86 -0
  25. package/interface/kit-boundary.mjs +73 -0
  26. package/interface/layer.json +8 -2
  27. package/interface/mcp/server.mjs +47 -7
  28. package/interface/package-routes.mjs +196 -0
  29. package/interface/provenance.mjs +24 -1
  30. package/interface/reads.mjs +173 -0
  31. package/interface/release-card.mjs +42 -0
  32. package/interface/router.mjs +101 -28
  33. package/interface/skills/naive-setup/SKILL.md +71 -0
  34. package/interface/skills/naive-setup/artifact.html +83 -0
  35. package/interface/vitals.mjs +115 -0
  36. package/interface/writes.mjs +190 -0
  37. package/knowledge/freshness.json +85 -0
  38. package/knowledge/layer.json +11 -3
  39. package/knowledge/templates/glossary.md +23 -0
  40. package/operations/layer.json +10 -3
  41. package/operations/templates/marketing-brand.md +46 -0
  42. package/operations/templates/public-assets.md +39 -0
  43. package/package.json +3 -1
  44. package/packages/catalog.json +8 -0
  45. package/packages/customers/package.json +21 -1
  46. package/packages/deals/package.json +27 -0
  47. package/packages/decisions/package.json +12 -1
  48. package/packages/finance/frames/fund-without-ceding-control.frame.json +177 -0
  49. package/packages/foundation/package.json +8 -1
  50. package/packages/fund/package.json +48 -0
  51. package/packages/marketing/package.json +10 -4
  52. package/packages/money/package.json +24 -3
  53. package/packages/validation/package.json +13 -0
  54. package/platform/README.md +6 -0
  55. package/platform/adapter.json +15 -0
  56. package/platform/integrations/remote-image.mjs +79 -0
  57. package/platform/integrations/rho.mjs +7 -6
  58. package/platform/kits/README.md +8 -1
  59. package/platform/kits/agent-upgrade/kit.json +15 -0
  60. package/platform/kits/agent-upgrade/run.mjs +82 -0
  61. package/platform/kits/agent-upgrade/stand-in-agent.mjs +32 -0
  62. package/platform/kits/validation-kickoff/kit.json +29 -0
  63. package/platform/webhooks/server.mjs +70 -0
  64. package/purpose/frameworks/idea-map/ideamap.mjs +4 -2
  65. package/schema/README.md +1 -0
  66. package/schema/access.mjs +57 -0
  67. package/schema/adapter.schema.json +33 -0
  68. package/schema/assets.mjs +212 -0
  69. package/schema/brand.mjs +373 -0
  70. package/schema/build-release.mjs +61 -0
  71. package/schema/caching.mjs +505 -0
  72. package/schema/check-assets.mjs +24 -0
  73. package/schema/check-brand.mjs +24 -0
  74. package/schema/check-config.mjs +110 -0
  75. package/schema/check-nextjs-status.mjs +3 -1
  76. package/schema/check-people.mjs +70 -0
  77. package/schema/check-sources.mjs +9 -1
  78. package/schema/data-model.json +334 -8
  79. package/schema/deal.schema.json +80 -0
  80. package/schema/decisions.mjs +7 -1
  81. package/schema/examples/frames/judge.mjs +10 -0
  82. package/schema/examples/venture-type/jev.mjs +12 -0
  83. package/schema/examples/workspace/ventures/acme-labs/AGENTS.md +17 -0
  84. package/schema/examples/workspace/ventures/acme-labs/accounts/.gitkeep +0 -0
  85. package/schema/examples/workspace/ventures/acme-labs/decisions/discovery-goal.decision.json +23 -0
  86. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-icp.decision.json +22 -0
  87. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-problem.decision.json +22 -0
  88. package/schema/examples/workspace/ventures/acme-labs/decisions/venture.team.decision.json +37 -0
  89. package/schema/examples/workspace/ventures/acme-labs/knowledge/README.md +3 -0
  90. package/schema/examples/workspace/ventures/acme-labs/knowledge/sources.json +46 -0
  91. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000001.json +12 -0
  92. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000002.json +12 -0
  93. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000003.json +12 -0
  94. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000004.json +12 -0
  95. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000005.json +12 -0
  96. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000006.json +12 -0
  97. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000007.json +12 -0
  98. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000008.json +12 -0
  99. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000009.json +12 -0
  100. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000010.json +12 -0
  101. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000011.json +12 -0
  102. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000012.json +12 -0
  103. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000013.json +12 -0
  104. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000014.json +12 -0
  105. package/schema/examples/workspace/ventures/acme-labs/money/_streams/transactions/stream.json +5 -0
  106. package/schema/examples/workspace/ventures/acme-labs/money/_summary.json +30 -0
  107. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/README.md +5 -0
  108. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/department.json +5 -0
  109. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/README.md +5 -0
  110. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/department.json +5 -0
  111. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/README.md +5 -0
  112. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/department.json +5 -0
  113. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/README.md +5 -0
  114. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/department.json +5 -0
  115. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/README.md +5 -0
  116. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/department.json +5 -0
  117. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/README.md +5 -0
  118. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/department.json +5 -0
  119. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/README.md +5 -0
  120. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/department.json +5 -0
  121. package/schema/examples/workspace/ventures/acme-labs/people/founders/maya-chen.json +15 -0
  122. package/schema/examples/workspace/ventures/acme-labs/people/people.json +22 -0
  123. package/schema/examples/workspace/ventures/acme-labs/pillars/brand.md +5 -0
  124. package/schema/examples/workspace/ventures/acme-labs/pillars/business.md +5 -0
  125. package/schema/examples/workspace/ventures/acme-labs/pillars/product.md +5 -0
  126. package/schema/examples/workspace/ventures/acme-labs/products/.gitkeep +0 -0
  127. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.core-product.decision.json +28 -0
  128. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.exists.decision.json +38 -0
  129. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.flagship.decision.json +27 -0
  130. package/schema/examples/workspace/ventures/acme-labs/profile.md +12 -0
  131. package/schema/examples/workspace/ventures/acme-labs/provenance.jsonl +12 -0
  132. package/schema/examples/workspace/ventures/acme-labs/raw/dump.md +6 -0
  133. package/schema/examples/workspace/ventures/acme-labs/venture.json +27 -0
  134. package/schema/examples/workspace/ventures/northstar/AGENTS.md +17 -0
  135. package/schema/examples/workspace/ventures/northstar/accounts/.gitkeep +0 -0
  136. package/schema/examples/workspace/ventures/northstar/deals/ductline.json +8 -0
  137. package/schema/examples/workspace/ventures/northstar/deals/kilnworks.json +14 -0
  138. package/schema/examples/workspace/ventures/northstar/decisions/venture.team.decision.json +31 -0
  139. package/schema/examples/workspace/ventures/northstar/evidence/README.md +3 -0
  140. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.decision.json +27 -0
  141. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.first-close.decision.json +27 -0
  142. package/schema/examples/workspace/ventures/northstar/fund/decisions/thesis.decision.json +26 -0
  143. package/schema/examples/workspace/ventures/northstar/fund/terms.json +22 -0
  144. package/schema/examples/workspace/ventures/northstar/fund/thesis.json +22 -0
  145. package/schema/examples/workspace/ventures/northstar/governance/README.md +3 -0
  146. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/charter.md +5 -0
  147. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/department.json +5 -0
  148. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/charter.md +5 -0
  149. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/department.json +5 -0
  150. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/charter.md +5 -0
  151. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/department.json +5 -0
  152. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/charter.md +5 -0
  153. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/department.json +5 -0
  154. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/charter.md +5 -0
  155. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/department.json +5 -0
  156. package/schema/examples/workspace/ventures/northstar/knowledge/README.md +3 -0
  157. package/schema/examples/workspace/ventures/northstar/operations/README.md +3 -0
  158. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/README.md +5 -0
  159. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/department.json +5 -0
  160. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/README.md +5 -0
  161. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/department.json +5 -0
  162. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/README.md +5 -0
  163. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/department.json +5 -0
  164. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/README.md +5 -0
  165. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/department.json +5 -0
  166. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/README.md +5 -0
  167. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/department.json +5 -0
  168. package/schema/examples/workspace/ventures/northstar/people/founders/ana-ruiz.json +13 -0
  169. package/schema/examples/workspace/ventures/northstar/people/founders/ben-okafor.json +7 -0
  170. package/schema/examples/workspace/ventures/northstar/people/people.json +27 -0
  171. package/schema/examples/workspace/ventures/northstar/pillars/brand.md +5 -0
  172. package/schema/examples/workspace/ventures/northstar/pillars/business.md +5 -0
  173. package/schema/examples/workspace/ventures/northstar/pillars/product.md +5 -0
  174. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/AGENTS.md +17 -0
  175. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/accounts/.gitkeep +0 -0
  176. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/decisions/venture.team.decision.json +31 -0
  177. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/knowledge/README.md +3 -0
  178. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/README.md +5 -0
  179. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/department.json +5 -0
  180. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/README.md +5 -0
  181. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/department.json +5 -0
  182. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/README.md +5 -0
  183. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/department.json +5 -0
  184. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/README.md +5 -0
  185. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/department.json +5 -0
  186. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/README.md +5 -0
  187. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/department.json +5 -0
  188. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/README.md +5 -0
  189. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/department.json +5 -0
  190. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/README.md +5 -0
  191. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/department.json +5 -0
  192. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/brand.md +5 -0
  193. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/business.md +5 -0
  194. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/product.md +5 -0
  195. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/.gitkeep +0 -0
  196. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.core-product.decision.json +28 -0
  197. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.exists.decision.json +32 -0
  198. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.flagship.decision.json +27 -0
  199. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/profile.md +5 -0
  200. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/venture.json +27 -0
  201. package/schema/examples/workspace/ventures/northstar/products/.gitkeep +0 -0
  202. package/schema/examples/workspace/ventures/northstar/profile.md +5 -0
  203. package/schema/examples/workspace/ventures/northstar/provenance.jsonl +3 -0
  204. package/schema/examples/workspace/ventures/northstar/purpose/README.md +3 -0
  205. package/schema/examples/workspace/ventures/northstar/venture.json +49 -0
  206. package/schema/founder.schema.json +72 -0
  207. package/schema/frame.schema.json +240 -0
  208. package/schema/frames.mjs +228 -0
  209. package/schema/freshness-log.schema.json +40 -0
  210. package/schema/freshness-profiles.schema.json +44 -0
  211. package/schema/freshness.mjs +473 -0
  212. package/schema/fund-terms.schema.json +107 -0
  213. package/schema/fund-thesis.schema.json +87 -0
  214. package/schema/gate-drafts.mjs +130 -0
  215. package/schema/gate.mjs +105 -11
  216. package/schema/glossary.schema.json +25 -0
  217. package/schema/init-venture.mjs +38 -8
  218. package/schema/judgment.schema.json +38 -0
  219. package/schema/kit.schema.json +47 -0
  220. package/schema/kits.mjs +24 -0
  221. package/schema/knowledge-sources.schema.json +145 -15
  222. package/schema/layer.json +16 -3
  223. package/schema/marketing-brand.schema.json +116 -0
  224. package/schema/migrations/kit-to-package.mjs +94 -0
  225. package/schema/migrations/package-layout.mjs +86 -0
  226. package/schema/model-steps.mjs +717 -0
  227. package/schema/money-summary.schema.json +138 -0
  228. package/schema/naive-config.mjs +165 -0
  229. package/schema/naive-config.schema.json +268 -0
  230. package/schema/need-now.json +171 -0
  231. package/schema/need-now.mjs +3 -2
  232. package/schema/packages.mjs +10 -5
  233. package/schema/people.schema.json +86 -0
  234. package/schema/public-assets.schema.json +76 -0
  235. package/schema/release-read.mjs +111 -0
  236. package/schema/releases.mjs +497 -0
  237. package/schema/setup.mjs +164 -0
  238. package/schema/stale.mjs +111 -0
  239. package/schema/start-release.mjs +41 -0
  240. package/schema/structure.mjs +186 -0
  241. package/schema/systems-catalog.json +126 -0
  242. package/schema/systems-catalog.schema.json +78 -0
  243. package/schema/trigger.schema.json +44 -0
  244. package/schema/triggers.mjs +237 -0
  245. package/schema/upgrade-registry.schema.json +60 -0
  246. package/schema/upgrade-venture.mjs +96 -118
  247. package/schema/upgrades.json +69 -0
  248. package/schema/upgrades.mjs +385 -0
  249. package/schema/validate.py +19 -10
  250. package/schema/venture-role.schema.json +64 -0
  251. package/schema/venture-type.mjs +62 -0
  252. package/schema/venture-types.json +346 -0
  253. package/schema/venture-types.schema.json +129 -0
  254. package/schema/venture.schema.json +53 -0
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // naive build (I-17, C-13): checks every open package can be released, then writes the release to
3
+ // <venture>/.release/: release.json (the manifest: confirmed records frozen in, named live holes, the
4
+ // route table as the release holds it), records/ (the frozen copies) and card.json (the release card).
5
+ // It refuses, exit 1, when a record in an open package is unconfirmed or a check fails, and names each
6
+ // one with the command that settles it. The rules are in schema/releases.mjs.
7
+ //
8
+ // node schema/build-release.mjs <venture dir> [--now <iso time>] [--json] [--dry-run]
9
+ // node schema/build-release.mjs <venture dir> --add <route> add one new item without rebuilding (C-18)
10
+ // node schema/build-release.mjs <venture dir> --export <out dir> the release as plain files, no holes (Z-3)
11
+ //
12
+ // --port is accepted and ignored, so naive build and naive start can pass the same arguments.
13
+
14
+ import { existsSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { add, build, exportRelease } from "./releases.mjs";
17
+
18
+ const args = process.argv.slice(2);
19
+ const opt = (n) => { const i = args.indexOf(`--${n}`); return i >= 0 ? args[i + 1] : undefined; };
20
+ const has = (n) => args.includes(`--${n}`);
21
+ const values = new Set(["now", "add", "export", "port"].flatMap((n) => (opt(n) !== undefined ? [opt(n)] : [])));
22
+ const dir = args.find((a) => !a.startsWith("--") && !values.has(a));
23
+ if (!dir || !existsSync(join(dir, "venture.json"))) {
24
+ console.error("usage: node schema/build-release.mjs <venture dir> [--now <iso>] [--json] [--dry-run] | --add <route> | --export <out dir>");
25
+ process.exit(2);
26
+ }
27
+ const actor = process.env.MENTURA_ACTOR === "agent" ? "agent" : "person";
28
+ const now = opt("now") ?? new Date().toISOString();
29
+ if (opt("now") && Number.isNaN(Date.parse(opt("now")))) { console.error(`--now ${opt("now")} is not a time`); process.exit(2); }
30
+
31
+ if (opt("export")) {
32
+ const r = exportRelease(dir, opt("export"));
33
+ if (!r.ok) { console.error(r.error); process.exit(1); }
34
+ console.log(has("json") ? JSON.stringify(r, null, 2) : `exported ${r.records} records to ${r.out}; ${r.left_out_live} live items left out`);
35
+ process.exit(0);
36
+ }
37
+
38
+ if (opt("add")) {
39
+ const r = await add(dir, opt("add"), { now, actor });
40
+ if (!r.ok) { console.error(r.error + (r.fix ? `\n fix: ${r.fix}` : "")); process.exit(1); }
41
+ console.log(has("json") ? JSON.stringify(r.manifest, null, 2) : `added ${r.added.route} to release ${r.added.from}: now release ${r.manifest.release}; nothing else was rebuilt`);
42
+ process.exit(0);
43
+ }
44
+
45
+ const r = await build(dir, { now, actor, dryRun: has("dry-run") });
46
+ if (!r.ok) {
47
+ if (r.error) { console.error(r.error); process.exit(1); }
48
+ if (has("json")) console.log(JSON.stringify(r, null, 2));
49
+ console.error("naive build: no release written.");
50
+ for (const p of r.packages.filter((x) => !x.releasable)) console.error(` package ${p.id} cannot be released:`);
51
+ for (const b of r.blockers) console.error(` ${b.file}: ${b.why}\n fix: ${b.fix}`);
52
+ for (const c of r.failed_checks) console.error(` check ${c.check} failed:\n ${c.output.replace(/\n/g, "\n ")}`);
53
+ process.exit(1);
54
+ }
55
+ const m = r.manifest;
56
+ if (has("json")) { console.log(JSON.stringify(m, null, 2)); process.exit(0); }
57
+ console.log(`${r.dry_run ? "would write" : "wrote"} release ${m.release} for ${m.venture} (${m.phase}) as of ${m.now}`);
58
+ console.log(` ${m.records.length} records frozen, ${m.holes.length} live holes, ${m.left_out.length} files left out`);
59
+ for (const p of m.packages) console.log(` package ${p.id.padEnd(12)} ${p.releasable ? "released" : "blocked"} shell ${p.shell ?? "empty"}${p.gaps.length ? ` not written yet: ${p.gaps.join(", ")}` : ""}`);
60
+ for (const h of m.holes) console.log(` hole ${h.kind.padEnd(9)} ${h.name}: ${h.why}`);
61
+ console.log(` cache: ${m.cache.hits} hits, ${m.cache.misses} misses${m.cache.fresh ? " (started fresh: new framework, packages or model)" : ""}`);
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env node
2
+ // Caching (the Next.js Caching chapter, translated; ledger rows C-1 to C-8, S-8, F-2, F-12, O-5 and
3
+ // H-3). Naive stores two kinds of work so it is not redone:
4
+ //
5
+ // record level a derived record, such as a command route's result (need-now, the decision
6
+ // list, the foundation report). Keyed by the venture's files, the framework's
7
+ // code, the arguments and anything the declaration says it varies on.
8
+ // judgment level one System 1 answer (a Choice, Noul or Score question). Keyed by the exact state
9
+ // it was given, the question, the model and the package versions. Change any of
10
+ // them and it is asked again; otherwise the stored answer is reused (S-8, C-5).
11
+ //
12
+ // Every cached value has a life (C-4), named like Next.js cacheLife profiles. Caching is on in the
13
+ // recommended defaults (C-2); naive.config.json { "cache": { "enabled": false } } or NAIVE_CACHE=off
14
+ // turns it off. A route is cached only when a declaration names it (C-3, H-3): a package's
15
+ // use_cache in packages/<name>/package.json, the framework's own list below, or the venture's
16
+ // naive.config.json cache.routes. Reads (run, and view over run) may use the cache; writes never do.
17
+ // A result that depends on who is acting is private: stored per person, never returned to anyone
18
+ // else, and not stored at all when the actor is unnamed (C-7).
19
+ //
20
+ // Separately, within one action (one router call and every call it makes), each record read and
21
+ // each computed value (the route table, the decision list, a command's output) is computed once and
22
+ // shared, like React.cache and fetch memoization (F-2, F-12, O-5). A write in the action forgets
23
+ // what was memoized for that venture.
24
+ //
25
+ // The store is <venture>/.naive/cache/ (C-17), which git ignores and the router never routes.
26
+ // No model and no network here: a judgment is asked by NAIVE_JUDGE_CMD (for example
27
+ // node platform/integrations/jev.mjs), which reads a request on stdin and prints its answers.
28
+ //
29
+ // node schema/caching.mjs stats <venture dir> [--json]
30
+ // node schema/caching.mjs list <venture dir> [--level record|judgment]
31
+ // node schema/caching.mjs prune <venture dir> remove expired entries
32
+ // node schema/caching.mjs clear <venture dir> [--level record|judgment]
33
+ // node schema/caching.mjs judge <venture dir> --request <file|-> [--life <profile>] [--private --as <person>]
34
+
35
+ import { AsyncLocalStorage } from "node:async_hooks";
36
+ import { spawnSync } from "node:child_process";
37
+ import { createHash } from "node:crypto";
38
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
39
+ import { dirname, join } from "node:path";
40
+ import { entries as provEntries } from "../interface/provenance.mjs";
41
+
42
+ const repo = new URL("..", import.meta.url).pathname;
43
+ const load = (f) => JSON.parse(readFileSync(f, "utf8"));
44
+ const sha = (s) => createHash("sha256").update(s).digest("hex");
45
+ const canon = (x) => Array.isArray(x) ? x.map(canon)
46
+ : x && typeof x === "object" ? Object.fromEntries(Object.keys(x).sort().filter((k) => x[k] !== undefined).map((k) => [k, canon(x[k])])) : x;
47
+ /** The hash of a key: canonical JSON (keys sorted), so the same parts always give the same key. */
48
+ export const keyOf = (parts) => sha(JSON.stringify(canon(parts)));
49
+
50
+ // ---------------------------------------------------------------------------------------------
51
+ // Per-action memoization (F-2, F-12, O-5)
52
+
53
+ const als = new AsyncLocalStorage();
54
+
55
+ /** Runs fn as one action. Inside an action already, fn joins it, so nested router calls share. */
56
+ export function actionScope(fn) {
57
+ if (als.getStore()) return fn();
58
+ return als.run({ memo: new Map(), computed: {}, shared: {} }, fn);
59
+ }
60
+ export const inAction = () => !!als.getStore();
61
+
62
+ /**
63
+ * The value of fn, computed once per action and shared by every caller in it. Outside an action
64
+ * fn runs every time. A throw or a rejected promise is not kept. dir scopes the value to a venture,
65
+ * so forget(dir) drops it.
66
+ */
67
+ export function memo(key, fn, { dir } = {}) {
68
+ const s = als.getStore();
69
+ if (!s) return fn();
70
+ const k = `${dir ?? ""}\u0000${key}`;
71
+ if (s.memo.has(k)) { s.shared[key] = (s.shared[key] ?? 0) + 1; return s.memo.get(k); }
72
+ const v = fn();
73
+ s.memo.set(k, v);
74
+ s.computed[key] = (s.computed[key] ?? 0) + 1;
75
+ if (v && typeof v.then === "function") v.then(null, () => s.memo.delete(k));
76
+ return v;
77
+ }
78
+
79
+ /** Drops everything memoized for one venture in this action (after a write). */
80
+ export function forget(dir) {
81
+ const s = als.getStore();
82
+ if (!s) return;
83
+ for (const k of [...s.memo.keys()]) if (k.startsWith(`${dir}\u0000`)) s.memo.delete(k);
84
+ }
85
+
86
+ /** What this action computed and how often each value was shared instead of computed again. */
87
+ export function actionStats() {
88
+ const s = als.getStore();
89
+ return s ? { computed: { ...s.computed }, shared: { ...s.shared } } : null;
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------------------------
93
+ // Lifetimes (C-4): Next.js cacheLife profiles, in seconds. Fresh while younger than revalidate;
94
+ // between revalidate and expire it is stale and computed again unless the caller accepts stale;
95
+ // past expire it is gone. expire null is never.
96
+
97
+ export const LIFE = {
98
+ seconds: { stale: 30, revalidate: 1, expire: 60 },
99
+ minutes: { stale: 300, revalidate: 60, expire: 3600 },
100
+ hours: { stale: 300, revalidate: 3600, expire: 86400 },
101
+ days: { stale: 300, revalidate: 86400, expire: 604800 },
102
+ weeks: { stale: 300, revalidate: 604800, expire: 2592000 },
103
+ max: { stale: 300, revalidate: 2592000, expire: 31536000 },
104
+ default: { stale: 300, revalidate: 900, expire: null },
105
+ };
106
+
107
+ /** A life by profile name, or { revalidate, expire } in seconds. A cache without one is refused (C-4). */
108
+ export function lifeOf(life) {
109
+ if (life === undefined || life === null || life === "") throw new Error(`every cached value needs a life: one of ${Object.keys(LIFE).join(", ")}, or { revalidate, expire } in seconds`);
110
+ if (typeof life === "string") {
111
+ if (!LIFE[life]) throw new Error(`unknown cache life ${life}: one of ${Object.keys(LIFE).join(", ")}`);
112
+ return { name: life, ...LIFE[life] };
113
+ }
114
+ const n = (x) => x === null || (typeof x === "number" && x >= 0);
115
+ if (typeof life === "object" && n(life.expire ?? null) && (life.revalidate === undefined || n(life.revalidate))) {
116
+ const expire = life.expire ?? null;
117
+ return { name: "custom", stale: life.stale ?? 300, revalidate: life.revalidate ?? expire ?? LIFE.default.revalidate, expire };
118
+ }
119
+ throw new Error(`a cache life is a profile name or { revalidate, expire } in seconds, not ${JSON.stringify(life)}`);
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------------------------
123
+ // Configuration (C-2) and declarations (C-3)
124
+
125
+ export const CACHE_DIR = ".naive/cache";
126
+ export const DEFAULTS = { enabled: true, life: "default", judgment_life: "max", routes: {} };
127
+
128
+ /** The venture's cache settings: the recommended defaults, then naive.config.json's cache, then NAIVE_CACHE. */
129
+ export function configOf(dir) {
130
+ const f = dir && join(dir, "naive.config.json");
131
+ const own = f && existsSync(f) ? load(f).cache ?? {} : {};
132
+ const off = /^(0|off|false|no)$/i.test(process.env.NAIVE_CACHE ?? "");
133
+ return { ...DEFAULTS, ...own, routes: { ...(own.routes ?? {}) }, enabled: !off && own.enabled !== false };
134
+ }
135
+
136
+ // The framework's own derived records, which no package owns. Each is a pure function of the
137
+ // venture's files and the framework's code; vary names any other input it reads (C-9: the hour or
138
+ // the day, an environment variable, who is acting).
139
+ export const FRAMEWORK_USE_CACHE = [
140
+ { route: "system/need-now", level: "record", life: "hours" },
141
+ { route: "system/time-to-ready", level: "record", life: "hours" },
142
+ { route: "system/package", level: "record", life: "hours", vary: ["day"] },
143
+ ];
144
+
145
+ /** Every use_cache declaration: the framework's, then each package's (packages/<name>/package.json). */
146
+ export function declarations() {
147
+ const out = FRAMEWORK_USE_CACHE.map((d) => ({ ...d, by: "framework" }));
148
+ const pk = join(repo, "packages");
149
+ if (existsSync(pk)) for (const name of readdirSync(pk).sort()) {
150
+ const f = join(pk, name, "package.json");
151
+ if (!existsSync(f)) continue;
152
+ for (const d of load(f).use_cache ?? []) out.push({ level: "record", ...d, by: `packages/${name}` });
153
+ }
154
+ return out;
155
+ }
156
+
157
+ /** The declaration that caches one route for a venture, or null. naive.config.json cache.routes wins: a life, an object, or false. */
158
+ export function declarationFor(dir, route) {
159
+ const own = configOf(dir).routes[route];
160
+ if (own === false) return null;
161
+ const base = declarations().find((d) => d.route === route && d.level === "record") ?? null;
162
+ if (own === undefined) return base;
163
+ return typeof own === "string" ? { ...(base ?? { route, level: "record" }), life: own, by: "naive.config.json" } : { ...(base ?? { route, level: "record" }), ...own, by: "naive.config.json" };
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------------------------
167
+ // Keys (C-5)
168
+
169
+ const SKIP = new Set([".naive", ".git", "node_modules"]);
170
+ function hashTree(root, sub = "", h = createHash("sha256")) {
171
+ const here = join(root, sub);
172
+ if (!existsSync(here)) return h;
173
+ for (const e of readdirSync(here, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
174
+ if (SKIP.has(e.name)) continue;
175
+ const rel = sub ? `${sub}/${e.name}` : e.name;
176
+ if (e.isDirectory()) hashTree(root, rel, h);
177
+ else if (e.isFile()) h.update(`${rel}\u0000${sha(readFileSync(join(root, rel)))}\n`);
178
+ }
179
+ return h;
180
+ }
181
+
182
+ /** The state of a venture: one hash over every file in it except the cache. Once per action. */
183
+ export function stateOf(dir) {
184
+ return memo("state", () => hashTree(dir).digest("hex"), { dir });
185
+ }
186
+
187
+ let code;
188
+ const CODE_DIRS = ["schema", "interface", "knowledge", "purpose", "governance", "operations", "evidence", "packages", "platform"];
189
+ /** The framework's code and definitions, hashed: a change to any of them starts record-level caches fresh (C-17). */
190
+ export function codeVersion() {
191
+ if (code) return code;
192
+ const h = createHash("sha256");
193
+ for (const d of CODE_DIRS) hashTree(repo, d, h);
194
+ return (code = h.digest("hex"));
195
+ }
196
+
197
+ /** Naive's version and each package definition's hash: the versions a judgment's key holds (C-5). */
198
+ export function versionsOf(packages) {
199
+ const naive = readFileSync(join(repo, "schema/VERSION"), "utf8").trim();
200
+ const pk = join(repo, "packages");
201
+ const names = packages?.length ? packages : existsSync(pk) ? readdirSync(pk).filter((n) => existsSync(join(pk, n, "package.json"))).sort() : [];
202
+ return { naive, packages: Object.fromEntries(names.map((n) => {
203
+ const f = join(pk, n, "package.json");
204
+ return [n, existsSync(f) ? sha(readFileSync(f)).slice(0, 16) : null];
205
+ })) };
206
+ }
207
+
208
+ /** Who a private result belongs to: the person (MENTURA_PERSON, else the actor id) or the agent's role id. */
209
+ export function identityOf({ actor, actor_id } = {}) {
210
+ const id = actor === "person" ? process.env.MENTURA_PERSON ?? actor_id : actor_id;
211
+ return id ? String(id) : null;
212
+ }
213
+
214
+ /** The values a declaration varies on (C-9: "now" is an input, read on purpose). */
215
+ export function varyValues(vary = [], { now = Date.now(), identity = null } = {}) {
216
+ const iso = new Date(now).toISOString();
217
+ return Object.fromEntries(vary.map((v) => [v, v === "day" ? iso.slice(0, 10) : v === "hour" ? iso.slice(0, 13) : v === "actor" ? identity
218
+ : v.startsWith("env:") ? process.env[v.slice(4)] ?? null : (() => { throw new Error(`unknown vary ${v}: day, hour, actor or env:<NAME>`); })()]));
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------------------------
222
+ // The store (C-1, C-7, C-17)
223
+
224
+ const scopeOf = (priv, identity) => priv ? `private/${sha(`naive-person:${identity}`).slice(0, 16)}` : "shared";
225
+ const slug = (s) => String(s).replace(/[^A-Za-z0-9._-]+/g, "_");
226
+ const fileOf = (dir, { level, name, hash, priv, identity }) => join(dir, CACHE_DIR, scopeOf(priv, identity), level, slug(name), `${hash}.json`);
227
+
228
+ function spec(s) {
229
+ if (!["record", "judgment"].includes(s.level)) throw new Error(`a cache level is record or judgment, not ${s.level}`);
230
+ if (!s.name) throw new Error("a cached value needs a name (its route or question)");
231
+ const priv = !!s.private;
232
+ const identity = priv ? s.identity ?? null : null;
233
+ const life = lifeOf(s.life);
234
+ return { level: s.level, name: s.name, key: s.key ?? {}, priv, identity, life, hash: keyOf({ level: s.level, name: s.name, key: s.key ?? {}, ...(priv ? { identity } : {}) }) };
235
+ }
236
+
237
+ /**
238
+ * Looks a value up. status: hit (fresh), stale (served because allowStale), revalidate (stale, to be
239
+ * computed again), expired, miss, or skip (private with no named actor: never stored or shared).
240
+ */
241
+ export function lookup(dir, s, { now = Date.now(), allowStale = false } = {}) {
242
+ const p = spec(s);
243
+ if (p.priv && !p.identity) return { status: "skip", why: "a private result with no named actor is never cached", spec: p };
244
+ const f = fileOf(dir, p);
245
+ if (!existsSync(f)) return { status: "miss", spec: p };
246
+ let e;
247
+ try { e = load(f); } catch { return { status: "miss", spec: p }; }
248
+ const age = (now - Date.parse(e.stored_at)) / 1000;
249
+ const life = e.life ?? p.life;
250
+ if (life.expire !== null && age >= life.expire) return { status: "expired", spec: p, age };
251
+ if (age < life.revalidate) return { status: "hit", spec: p, entry: e, age };
252
+ return allowStale ? { status: "stale", spec: p, entry: e, age } : { status: "revalidate", spec: p, age };
253
+ }
254
+
255
+ /** Stores a value for the looked-up spec. Written whole (temp file, then rename), so a reader never sees half of it. */
256
+ export function store(dir, p, value, { now = Date.now() } = {}) {
257
+ if (p.priv && !p.identity) return null;
258
+ const f = fileOf(dir, p);
259
+ mkdirSync(dirname(f), { recursive: true });
260
+ const stored_at = new Date(now).toISOString();
261
+ const entry = { level: p.level, name: p.name, key: p.hash, parts: p.key, private: p.priv, stored_at, life: p.life,
262
+ expires_at: p.life.expire === null ? null : new Date(now + p.life.expire * 1000).toISOString(), value };
263
+ const tmp = `${f}.${process.pid}.tmp`;
264
+ writeFileSync(tmp, JSON.stringify(entry, null, 2) + "\n");
265
+ renameSync(tmp, f);
266
+ return entry;
267
+ }
268
+
269
+ const info = (l, entry) => ({ status: l.status, level: l.spec.level, name: l.spec.name, key: l.spec.hash.slice(0, 16), life: l.spec.life.name,
270
+ ...(l.spec.priv ? { private: true } : {}), ...(entry ? { stored_at: entry.stored_at, expires_at: entry.expires_at } : {}), ...(l.age !== undefined ? { age_s: Math.round(l.age) } : {}), ...(l.why ? { why: l.why } : {}) });
271
+
272
+ /**
273
+ * The cached value, or compute() stored with its life. compute returning undefined is not stored
274
+ * (a failure is never cached). Returns { value, cache }, cache saying hit, stale, miss, revalidate,
275
+ * expired, skip or off.
276
+ */
277
+ export async function cached(dir, s, compute, { now = Date.now(), allowStale = false, enabled = configOf(dir).enabled } = {}) {
278
+ const p = spec(s);
279
+ if (!enabled) return { value: await compute(), cache: { status: "off", level: p.level, name: p.name } };
280
+ const l = lookup(dir, s, { now, allowStale });
281
+ if (l.status === "hit" || l.status === "stale") return { value: l.entry.value, cache: info(l, l.entry) };
282
+ const value = await compute();
283
+ const entry = value === undefined ? null : store(dir, l.spec, value, { now });
284
+ return { value, cache: info(l, entry) };
285
+ }
286
+
287
+ // ---------------------------------------------------------------------------------------------
288
+ // Record level: command routes (C-3, H-3, C-7)
289
+
290
+ /**
291
+ * Runs a command route through the record-level cache when a declaration names it. run() is the
292
+ * spawn; its { status, stdout, stderr } comes back, from the store on a hit, plus cache. A failing
293
+ * run is returned as it is and never stored. Only the run action uses this; a write never does.
294
+ */
295
+ export async function command({ dir, route, script, args = [], input, actor, actor_id, now = Date.now() }, run) {
296
+ const d = declarationFor(dir, route);
297
+ if (!d) return run();
298
+ const cfg = configOf(dir);
299
+ if (!cfg.enabled) return { ...run(), cache: { status: "off", level: "record", name: route } };
300
+ if (input?.cache === "no-store") return { ...run(), cache: { status: "bypass", level: "record", name: route } };
301
+ const identity = identityOf({ actor, actor_id });
302
+ const priv = !!d.private || (d.vary ?? []).includes("actor");
303
+ const key = { route, script, args: args.map((a) => (a === dir ? "<venture>" : a)), state: stateOf(dir), code: codeVersion(), vary: varyValues(d.vary, { now, identity }) };
304
+ let failed = null;
305
+ const r = await cached(dir, { level: "record", name: route, key, life: d.life ?? cfg.life, private: priv, identity }, () => {
306
+ const p = run();
307
+ if (p.status !== 0) { failed = p; return undefined; }
308
+ return { stdout: p.stdout };
309
+ }, { now, enabled: true });
310
+ if (failed) return failed;
311
+ return { status: 0, stdout: r.value.stdout, stderr: "", cache: { ...r.cache, by: d.by } };
312
+ }
313
+
314
+ // ---------------------------------------------------------------------------------------------
315
+ // Judgment level (S-8, C-5)
316
+
317
+ function judgeCommand(cmd) {
318
+ if (!cmd) return null;
319
+ if (Array.isArray(cmd)) return cmd;
320
+ const t = String(cmd).trim();
321
+ return t.startsWith("[") ? JSON.parse(t) : t.split(/\s+/);
322
+ }
323
+
324
+ /**
325
+ * Answers a typed request ({ state, model, questions }, the shape platform/integrations/jev.mjs
326
+ * reads) with every answer it can reuse, and asks the judge only the rest, in one request. Each
327
+ * question is its own entry, keyed by the request's state, the question, the model and the package
328
+ * versions. Returns { model, answers, cache: { reused, asked, unanswered } }.
329
+ */
330
+ export async function judge(dir, request, { cmd = process.env.NAIVE_JUDGE_CMD, life, actor, actor_id, private: priv = false, packages, now = Date.now(), allowStale = false } = {}) {
331
+ if (!request || typeof request !== "object" || !request.questions || typeof request.questions !== "object") throw new Error("a judgment request has questions: { state, model, questions }");
332
+ const cfg = configOf(dir);
333
+ const identity = identityOf({ actor, actor_id });
334
+ const versions = versionsOf(request.packages ?? packages);
335
+ const specOf = (q) => ({ level: "judgment", name: request.model ?? "model", key: { state: request.state ?? null, question: q, model: request.model ?? null, versions }, life: life ?? cfg.judgment_life, private: priv, identity });
336
+ const answers = {}, reused = [], ask = [], looked = {};
337
+ for (const [k, q] of Object.entries(request.questions)) {
338
+ if (!cfg.enabled) { ask.push(k); continue; }
339
+ const l = lookup(dir, specOf(q), { now, allowStale });
340
+ looked[k] = l;
341
+ if (l.status === "hit" || l.status === "stale") { answers[k] = l.entry.value; reused.push(k); } else ask.push(k);
342
+ }
343
+ let model = request.model ?? null;
344
+ const unanswered = [];
345
+ if (ask.length) {
346
+ const argv = judgeCommand(cmd);
347
+ if (!argv) throw new Error(`${ask.length} question(s) are not cached and there is no judge: set NAIVE_JUDGE_CMD (for example node platform/integrations/jev.mjs)`);
348
+ const sub = { ...request, questions: Object.fromEntries(ask.map((k) => [k, request.questions[k]])) };
349
+ const p = spawnSync(argv[0], argv.slice(1), { input: JSON.stringify(sub), encoding: "utf8", maxBuffer: 1 << 26 });
350
+ if (p.error) throw new Error(`could not run the judge (${argv[0]}): ${p.error.message}`);
351
+ if (p.status !== 0) throw new Error(`the judge stopped with status ${p.status}: ${(p.stderr || p.stdout).trim().slice(0, 500)}`);
352
+ let out;
353
+ try { out = JSON.parse(p.stdout); } catch { throw new Error(`the judge did not answer with JSON: ${p.stdout.trim().slice(0, 300)}`); }
354
+ model = out.model ?? model;
355
+ for (const k of ask) {
356
+ const a = out.answers?.[k];
357
+ if (a === undefined) { unanswered.push(k); continue; }
358
+ answers[k] = a;
359
+ if (cfg.enabled) store(dir, (looked[k] ?? lookup(dir, specOf(request.questions[k]))).spec, a, { now });
360
+ }
361
+ }
362
+ return { model, answers, cache: { status: cfg.enabled ? (ask.length ? (reused.length ? "partial" : "miss") : "hit") : "off", reused, asked: ask.filter((k) => !unanswered.includes(k)), unanswered, ...(priv ? { private: true } : {}) } };
363
+ }
364
+
365
+ // ---------------------------------------------------------------------------------------------
366
+ // Looking after the store
367
+
368
+ function walkEntries(root) {
369
+ if (!existsSync(root)) return [];
370
+ return readdirSync(root, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walkEntries(join(root, e.name)) : e.name.endsWith(".json") ? [join(root, e.name)] : []);
371
+ }
372
+
373
+ /** The entries one actor may see: the shared ones and their own private ones, never another person's (C-7). */
374
+ export function entries(dir, { identity = null, level } = {}) {
375
+ const base = join(dir, CACHE_DIR);
376
+ const roots = [join(base, "shared"), ...(identity ? [join(base, scopeOf(true, identity))] : [])];
377
+ return roots.flatMap(walkEntries).flatMap((f) => { try { const e = load(f); return [{ file: f.slice(dir.length + 1), bytes: statSync(f).size, ...e }]; } catch { return []; } })
378
+ .filter((e) => !level || e.level === level);
379
+ }
380
+
381
+ /** Counts by level and scope, what has expired, and the size. */
382
+ export function stats(dir, { identity = null, now = Date.now() } = {}) {
383
+ const es = entries(dir, { identity });
384
+ const expired = (e) => e.expires_at && Date.parse(e.expires_at) <= now;
385
+ const by = (k) => es.reduce((m, e) => ({ ...m, [k(e)]: (m[k(e)] ?? 0) + 1 }), {});
386
+ const cfg = configOf(dir);
387
+ return { store: CACHE_DIR, enabled: cfg.enabled, entries: es.length, by_level: by((e) => e.level), private: es.filter((e) => e.private).length,
388
+ expired: es.filter(expired).length, bytes: es.reduce((n, e) => n + e.bytes, 0),
389
+ routes: Object.fromEntries(declarations().map((d) => [d.route, declarationFor(dir, d.route)?.life ?? "off"]).concat(Object.keys(cfg.routes).map((r) => [r, declarationFor(dir, r)?.life ?? "off"]))) };
390
+ }
391
+
392
+ /** Removes expired entries in every scope. Returns how many. */
393
+ export function prune(dir, { now = Date.now() } = {}) {
394
+ let n = 0;
395
+ for (const f of walkEntries(join(dir, CACHE_DIR))) {
396
+ try { const e = load(f); if (e.expires_at && Date.parse(e.expires_at) <= now) { rmSync(f); n++; } } catch { rmSync(f, { force: true }); n++; }
397
+ }
398
+ return n;
399
+ }
400
+
401
+ /** Removes entries, all or one level, in every scope. Returns how many. */
402
+ export function clear(dir, { level } = {}) {
403
+ let n = 0;
404
+ for (const f of walkEntries(join(dir, CACHE_DIR))) {
405
+ let e = null;
406
+ try { e = load(f); } catch {}
407
+ if (!level || e?.level === level) { rmSync(f, { force: true }); n++; }
408
+ }
409
+ return n;
410
+ }
411
+
412
+ // ---------------------------------------------------------------------------------------------
413
+ // The router's routes (system/cache, system/judge, system/answer) and their handlers
414
+
415
+ /** The command routes this module adds to the route table. cmd is the router's command constructor. */
416
+ export function cacheRoutes(cmd) {
417
+ return [
418
+ cmd("system/cache", null, "The venture's cache (C-1, C-17): entries by level, what is private to you, what has expired, and which routes are cached for how long; input value list, prune or clear", { internal: "cache" }),
419
+ cmd("system/judge", null, "Ask System 1 a typed request ({ state, model, questions }) through the judgment cache (S-8, C-5): reused answers are not asked again; the rest go to NAIVE_JUDGE_CMD in one request", { internal: "judge" }),
420
+ cmd("system/answer", null, "One answer from several routes (C-8): each part says whether it is a confirmed record, an agent's record, a draft, a cached result or judgment, or fresh computation; input routes (a list, or value a,b) and judge (a request)", { internal: "answer" }),
421
+ ];
422
+ }
423
+
424
+ // Whether a record is confirmed: the last provenance entry for its file was a person's (a write, an
425
+ // accept or a confirm), or an agent wrote it and nobody has confirmed it since.
426
+ function confirmation(dir, file) {
427
+ const es = provEntries(dir).filter((e) => e.entity === file);
428
+ const last = es.at(-1);
429
+ if (!last) return { kind: "unlogged", note: "written outside the router; its history is in git" };
430
+ if (last.agent?.kind === "person" || ["accept", "confirm"].includes(last.activity)) return { kind: "confirmed", by: last.agent?.id ?? last.agent?.kind, at: last.at };
431
+ return { kind: "agent", by: last.agent?.id ?? "agent", at: last.at };
432
+ }
433
+
434
+ /**
435
+ * Handles system/cache, system/judge and system/answer for the router. helpers carries the
436
+ * router's call, resolve and draftOf, so this module never imports the router.
437
+ */
438
+ export async function internal(kind, { dir, venture, workspace, input, actor, actor_id, via, provenance }, { call, resolve, draftOf }) {
439
+ const identity = identityOf({ actor, actor_id });
440
+ try {
441
+ if (kind === "cache") {
442
+ const v = input?.value;
443
+ if (v === "list") return { ok: true, result: entries(dir, { identity }).map((e) => ({ level: e.level, name: e.name, key: e.key.slice(0, 16), private: e.private, stored_at: e.stored_at, expires_at: e.expires_at, life: e.life?.name })) };
444
+ if (v === "prune") return { ok: true, result: { pruned: prune(dir) } };
445
+ if (v === "clear") return { ok: true, result: { cleared: clear(dir, { level: input?.level }) } };
446
+ if (v !== undefined) return { ok: false, error: `system/cache takes value list, prune or clear, not ${v}` };
447
+ return { ok: true, result: stats(dir, { identity }) };
448
+ }
449
+ if (kind === "judge") {
450
+ const { life, private: priv, packages, ...request } = input ?? {};
451
+ return { ok: true, result: await judge(dir, request, { life, private: !!priv, packages, actor, actor_id }) };
452
+ }
453
+ if (kind === "answer") {
454
+ const list = Array.isArray(input?.routes) ? input.routes : typeof input?.value === "string" ? input.value.split(",").map((s) => s.trim()).filter(Boolean) : [];
455
+ if (!list.length && !input?.judge) return { ok: false, error: "system/answer needs routes (input routes [...], or value a,b) or a judge request" };
456
+ const parts = await Promise.all(list.map(async (route) => {
457
+ const { r } = resolve(route);
458
+ if (!r) return { route, kind: "error", error: `no route ${route}` };
459
+ if (r.kind === "command") {
460
+ if (["answer"].includes(r.internal)) return { route, kind: "error", error: "system/answer cannot hold itself" };
461
+ const res = await call({ venture, route, action: "run", workspace, actor, actor_id, via, provenance });
462
+ if (!res.ok) return { route, kind: "error", error: res.error };
463
+ const hit = res.cache && ["hit", "stale"].includes(res.cache.status);
464
+ return { route, kind: hit ? "cached" : "fresh", ...(res.cache ? { cache: res.cache } : {}), data: res.result };
465
+ }
466
+ const res = await call({ venture, route, action: "read", workspace, actor, actor_id, via, provenance });
467
+ if (!res.ok) return { route, kind: "error", error: res.error };
468
+ if (res.result === null) return { route, kind: "missing", note: res.not_found?.note ?? null, ...(res.pending_draft ? { pending_draft: res.pending_draft } : {}) };
469
+ const d = draftOf(route, venture, workspace);
470
+ const c = d && r.collection !== "list" ? confirmation(dir, d.file) : { kind: "records" };
471
+ return { route, ...c, ...(res.pending_draft ? { pending_draft: res.pending_draft } : {}), ...(res.source ? { source: res.source } : {}), data: res.result };
472
+ }));
473
+ if (input?.judge) {
474
+ const j = await judge(dir, input.judge, { actor, actor_id, private: !!input.judge.private });
475
+ parts.push({ route: "system/judge", kind: j.cache.status === "hit" ? "cached" : j.cache.status === "partial" ? "partly cached" : "fresh", level: "judgment", cache: j.cache, data: j.answers });
476
+ }
477
+ const mix = parts.reduce((m, p) => ({ ...m, [p.kind]: (m[p.kind] ?? 0) + 1 }), {});
478
+ return { ok: true, result: { venture, mix, parts } };
479
+ }
480
+ } catch (e) { return { ok: false, error: String(e.message ?? e) }; }
481
+ return { ok: false, error: `unknown cache route ${kind}` };
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------------------------
485
+
486
+ if (import.meta.url === `file://${process.argv[1]}`) {
487
+ const argv = process.argv.slice(2);
488
+ const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
489
+ const [cmd, dir] = argv;
490
+ const usage = () => { console.error(readFileSync(new URL(import.meta.url), "utf8").split("\n").filter((l) => l.startsWith("// node schema/caching.mjs")).map((l) => l.slice(5)).join("\n")); process.exit(2); };
491
+ if (!cmd || !dir || !existsSync(join(dir, "venture.json"))) usage();
492
+ const identity = opt("as") ?? null;
493
+ const out = (x) => console.log(typeof x === "string" ? x : JSON.stringify(x, null, 2));
494
+ if (cmd === "stats") out(stats(dir, { identity }));
495
+ else if (cmd === "list") out(entries(dir, { identity, level: opt("level") }).map(({ value, parts, ...e }) => e));
496
+ else if (cmd === "prune") out({ pruned: prune(dir) });
497
+ else if (cmd === "clear") out({ cleared: clear(dir, { level: opt("level") }) });
498
+ else if (cmd === "judge") {
499
+ const src = opt("request");
500
+ if (!src) usage();
501
+ const req = JSON.parse(src === "-" ? readFileSync(0, "utf8") : readFileSync(src, "utf8"));
502
+ try { out(await judge(dir, req, { life: opt("life"), private: argv.includes("--private"), actor: identity ? "agent" : undefined, actor_id: identity })); }
503
+ catch (e) { console.error(String(e.message ?? e)); process.exit(1); }
504
+ } else usage();
505
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Checks each venture's public/ assets (schema/assets.mjs): every file has a source, a license and a
3
+ // credit (J-2), facts read by code match the file (J-4), images have alt text and an agent's alt text
4
+ // is listed until a person confirms it (J-4), and remote images are links with a note in raw/ whose
5
+ // full copies need a license that allows them (J-3).
6
+ //
7
+ // node schema/check-assets.mjs <workspace or venture dir>
8
+
9
+ import { existsSync, readdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { checkAssets } from "./assets.mjs";
12
+
13
+ const dir = process.argv[2];
14
+ if (!dir || !existsSync(dir)) { console.error("usage: node schema/check-assets.mjs <workspace or venture dir>"); process.exit(2); }
15
+ const walk = (d) => existsSync(join(d, "venture.json")) ? [d] : readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" ? walk(join(d, e.name)) : []);
16
+ let failed = 0;
17
+ for (const v of walk(dir)) {
18
+ const r = checkAssets(v);
19
+ for (const e of r.errors) console.log(`error ${v}: ${e}`);
20
+ for (const p of r.pending) console.log(`pending ${v}: ${p.path}: alt text by ${p.by}, not yet confirmed by a person`);
21
+ if (!r.ok) failed++;
22
+ }
23
+ console.log(failed ? `assets: ${failed} ventures fail` : "assets: ok");
24
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Checks each venture's brand (schema/brand.mjs): colors, licensed and self-hosted typefaces (T-1),
3
+ // font roles and scopes (T-2, Y-5), marks of the right type and size (P-24), design systems linked
4
+ // and not copied (Y-4), package glossaries (Y-2), and marketing content held to the voice rules (Y-3).
5
+ // A venture with no brand record passes.
6
+ //
7
+ // node schema/check-brand.mjs <workspace or venture dir>
8
+
9
+ import { existsSync, readdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { checkBrand } from "./brand.mjs";
12
+
13
+ const dir = process.argv[2];
14
+ if (!dir || !existsSync(dir)) { console.error("usage: node schema/check-brand.mjs <workspace or venture dir>"); process.exit(2); }
15
+ const walk = (d) => existsSync(join(d, "venture.json")) ? [d] : readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" ? walk(join(d, e.name)) : []);
16
+ let failed = 0;
17
+ for (const v of walk(dir)) {
18
+ const r = checkBrand(v);
19
+ for (const e of r.errors) console.log(`error ${v}: ${e}`);
20
+ for (const w of r.warnings) console.log(`warn ${v}: ${w}`);
21
+ if (!r.ok) failed++;
22
+ }
23
+ console.log(failed ? `brand: ${failed} ventures fail` : "brand: ok");
24
+ process.exit(failed ? 1 : 0);