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
@@ -5,8 +5,10 @@
5
5
  // written outside the router.
6
6
 
7
7
  import { createHash } from "node:crypto";
8
- import { appendFileSync, existsSync, readFileSync } from "node:fs";
8
+ import { spawnSync } from "node:child_process";
9
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
9
10
  import { join } from "node:path";
11
+ import { emit } from "./instrumentation.mjs";
10
12
 
11
13
  export const LOG = "provenance.jsonl";
12
14
  const sha = (s) => createHash("sha256").update(s).digest("hex");
@@ -38,5 +40,26 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
38
40
  derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(join(dir, entity)), prev }).filter(([, v]) => v !== undefined));
39
41
  entry.hash = lineHash(prev, entry);
40
42
  appendFileSync(join(dir, LOG), JSON.stringify(entry) + "\n");
43
+ // Context hook: every provenance entry (write, draft, accept, confirm, decision) is an instrumentation event (P-4).
44
+ emit(dir, { kind: "provenance", activity: entry.activity, entity: entry.entity, route: entry.route, decision: entry.decision, by: entry.agent?.kind, via: entry.agent?.via });
45
+ fireTriggers(dir);
41
46
  return entry;
42
47
  }
48
+
49
+ // Triggers hook (M-12, E-7): every entry is an event. A venture whose naive.config.json has triggers or
50
+ // kits runs them (schema/triggers.mjs) in a separate process after the write; the runs themselves do
51
+ // not fire this again. A crash of that process is kept for the next run to raise, never lost.
52
+ function fireTriggers(dir) {
53
+ if (process.env.NAIVE_TRIGGERS_ACTIVE) return;
54
+ let cfg;
55
+ try { cfg = JSON.parse(readFileSync(join(dir, "naive.config.json"), "utf8")); } catch { return; }
56
+ if (!cfg?.triggers?.length && !cfg?.kits?.length) return;
57
+ // The write has landed; nothing here may undo it or throw past it.
58
+ try {
59
+ const p = spawnSync(process.execPath, [new URL("../schema/triggers.mjs", import.meta.url).pathname, dir, "--fire", "--quiet"], { encoding: "utf8", env: { ...process.env, NAIVE_TRIGGERS_ACTIVE: "hook" } });
60
+ if (p.status !== 0) {
61
+ mkdirSync(join(dir, ".naive"), { recursive: true });
62
+ appendFileSync(join(dir, ".naive/trigger-errors.jsonl"), JSON.stringify({ at: new Date().toISOString(), status: p.status, error: (p.stderr || p.stdout || String(p.error ?? "")).trim().slice(-2000) }) + "\n");
63
+ }
64
+ } catch (e) { console.error(`triggers did not run after this write: ${e.message}`); }
65
+ }
@@ -0,0 +1,173 @@
1
+ // What a read answers besides the record (Next.js translation L-9, L-10, O-1, O-2, O-3, P-28). The
2
+ // router's wrapper (interface/writes.mjs) adds these to every read and layout. Code computes all of
3
+ // it from the files and the provenance log; no model writes any of it.
4
+ //
5
+ // params L-9 the dynamic segment's value ({ id: "kettle" } for product/kettle), given to the
6
+ // read and to every layout below it: the item's own folder AGENTS.md joins the
7
+ // chain, and "[id]" in an instruction becomes the id, so a product's layout knows
8
+ // which product
9
+ // where L-10 route inputs that change what a read returns: input.where { status, phase,
10
+ // function, state, or any field by dot path }. A list keeps the items that match;
11
+ // a single record that does not match comes back null with filtered_out
12
+ // metadata O-1 the static metadata: the venture's identity from venture.json and the record's
13
+ // own frontmatter (Markdown) or title
14
+ // O-2 derived fields, computed by code from the record: counts, the phase, the
15
+ // function and its owner, and its state (confirmed, agent, draft, unlogged)
16
+ // O-3 the fields the framework adds to every record without being asked: id,
17
+ // created, updated, and who wrote it last (person or agent), from provenance
18
+ // hierarchy P-28 the order the router resolves a read: layout (context), template (fresh
19
+ // context), error (failed checks), loading (pending drafts), not-found (missing
20
+ // record), then page (the record). resolved_by names the level that answered;
21
+ // input.hierarchy returns every level
22
+
23
+ import { existsSync, readFileSync, statSync } from "node:fs";
24
+ import { dirname, join } from "node:path";
25
+ import { entries } from "./provenance.mjs";
26
+
27
+ const load = (f) => JSON.parse(readFileSync(f, "utf8"));
28
+ const get = (o, p) => p.split(".").reduce((x, k) => (x == null ? undefined : x[k]), o);
29
+
30
+ /** Simple YAML frontmatter: key: value lines between --- fences. Lists in [a, b] form become arrays. */
31
+ export function frontmatter(text) {
32
+ const m = typeof text === "string" ? text.match(/^---\n([\s\S]*?)\n---\n?/) : null;
33
+ if (!m) return null;
34
+ const out = {};
35
+ for (const line of m[1].split("\n")) {
36
+ const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
37
+ if (!kv) continue;
38
+ let v = kv[2].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
39
+ if (/^\[.*\]$/.test(v)) v = v.slice(1, -1).split(",").map((x) => x.trim()).filter(Boolean);
40
+ out[kv[1]] = v;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /** A record's state from the log and the drafts folder (O-2). */
46
+ export async function stateOf(dir, file) {
47
+ const { confirmedIn } = await import("../schema/foundation.mjs");
48
+ if (existsSync(join(dir, "_drafts", `${file.replace(/\//g, "__")}.draft`))) return "draft";
49
+ const es = entries(dir).filter((e) => e.entity === file);
50
+ if (!es.length) return "unlogged";
51
+ return confirmedIn(dir, file, null) ? "confirmed" : "agent";
52
+ }
53
+
54
+ /** The fields every record gets (O-3): id, created, updated and who wrote it last, from the provenance log, else the file. */
55
+ export function defaults(dir, file, id) {
56
+ const es = entries(dir).filter((e) => e.entity === file);
57
+ if (es.length) {
58
+ const last = es.at(-1);
59
+ return { id, created: es[0].at, updated: last.at, by: { kind: last.agent?.kind ?? null, ...(last.agent?.id ? { id: last.agent.id } : {}), via: last.agent?.via ?? null }, activity: last.activity, writes: es.length, from: "provenance" };
60
+ }
61
+ const st = existsSync(join(dir, file)) ? statSync(join(dir, file)) : null;
62
+ return { id, created: st ? new Date(st.birthtimeMs || st.mtimeMs).toISOString() : null, updated: st ? st.mtime.toISOString() : null, by: null, writes: 0, from: "file", note: "written outside the router: mentura baseline adopts it into the log" };
63
+ }
64
+
65
+ /** Counts derived from a record (O-2): the length of each top-level list. */
66
+ const countsOf = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => Array.isArray(x)).map(([k, x]) => [k, x.length])) : {};
67
+
68
+ /** The function owning a record and its owner (O-2), from the package that holds it and venture.json's functions. */
69
+ export async function ownerOf(dir, type) {
70
+ const { packageOfRecord, packageDef } = await import("./package-routes.mjs");
71
+ const pkg = packageOfRecord(type);
72
+ const def = pkg ? packageDef(pkg) : null;
73
+ const fn = def?.function ?? def?.department ?? null;
74
+ const v = load(join(dir, "venture.json"));
75
+ const owner = fn ? v.functions?.[fn]?.owner ?? (!(v.members?.length) ? "the founder" : null) : null;
76
+ return { package: pkg, function: fn, owner };
77
+ }
78
+
79
+ /** The metadata a read answers with (O-1, O-2, O-3). items maps each id to its venture-relative file. */
80
+ export async function metadataFor({ dir, r, route, params, result, items }) {
81
+ const v = load(join(dir, "venture.json"));
82
+ const fn = await ownerOf(dir, r.record);
83
+ const one = async (id, file, value) => ({
84
+ ...defaults(dir, file, id),
85
+ ...(file.endsWith(".md") || file.endsWith(".mdx") ? { frontmatter: frontmatter(value) } : value && typeof value === "object" && (value.title || value.name) ? { title: value.title ?? value.name } : {}),
86
+ derived: { state: await stateOf(dir, file), counts: countsOf(value) },
87
+ });
88
+ const base = {
89
+ venture: { id: v.id, name: v.name, phase: v.phase, status: v.status },
90
+ record: { type: r.record, route, schema: r.schema ?? null, form: r.form },
91
+ derived: { phase: v.phase, function: fn.function, owner: fn.owner, package: fn.package, by: "code" },
92
+ };
93
+ if (r.collection === "list") {
94
+ const per = {};
95
+ for (const [id, file] of Object.entries(items)) per[id] = await one(id, file, result?.[id]);
96
+ return { ...base, derived: { ...base.derived, count: Object.keys(per).length }, items: per };
97
+ }
98
+ const [id, file] = Object.entries(items)[0] ?? [];
99
+ if (!file) return base;
100
+ const own = await one(params.id ?? r.record, file, result);
101
+ return { ...base, ...own, derived: { ...base.derived, ...own.derived } };
102
+ }
103
+
104
+ // One filter test: a field by dot path on the record, else a derived field.
105
+ function matches(value, meta, where) {
106
+ return Object.entries(where).every(([k, want]) => {
107
+ let have = get(value ?? {}, k);
108
+ if (have === undefined) have = { state: meta?.derived?.state, function: meta?.function, owner: meta?.owner, phase: meta?.phase }[k];
109
+ const wants = Array.isArray(want) ? want.map(String) : [String(want)];
110
+ return Array.isArray(have) ? have.some((x) => wants.includes(String(typeof x === "object" ? x?.id ?? x?.name : x))) : have !== undefined && have !== null && wants.includes(String(have));
111
+ });
112
+ }
113
+
114
+ /** Applies input.where to a read answer (L-10): a list keeps matching items; a record that does not match comes back null. */
115
+ export function filterRead(answer, where, r) {
116
+ if (!where || typeof where !== "object" || !Object.keys(where).length || answer.result == null) return answer;
117
+ const md = answer.metadata ?? {};
118
+ const shared = { function: md.derived?.function, owner: md.derived?.owner, phase: md.venture?.phase };
119
+ if (r.collection === "list") {
120
+ const keep = Object.keys(answer.result).filter((id) => matches(answer.result[id], { ...shared, derived: md.items?.[id]?.derived }, where));
121
+ const pick = (o) => o ? Object.fromEntries(Object.entries(o).filter(([id]) => keep.includes(id))) : o;
122
+ return { ...answer, result: pick(answer.result), sources: pick(answer.sources), ...(md.items ? { metadata: { ...md, items: pick(md.items), derived: { ...md.derived, count: keep.length } } } : {}), filtered: { where, matched: keep.length, of: Object.keys(answer.result).length } };
123
+ }
124
+ if (matches(answer.result, { ...shared, derived: md.derived }, where)) return { ...answer, filtered: { where, matched: 1, of: 1 } };
125
+ return { ...answer, result: null, filtered_out: { where, note: "the record exists but does not match the filter" } };
126
+ }
127
+
128
+ const parseInstructions = (chain) => {
129
+ const out = {};
130
+ for (const f of chain) for (const line of f.text.split("\n")) { const m = line.match(/^([a-z][a-z0-9_-]*)\s*:\s*(.+)$/); if (m) out[m[1]] = m[2].trim(); }
131
+ return out;
132
+ };
133
+
134
+ /**
135
+ * The layout with params (L-9) and its group (P-36): the group's AGENTS.md after the venture root's,
136
+ * then the item's own folders' AGENTS.md, nearest last; "[id]" in an instruction becomes the id.
137
+ */
138
+ export function withParams(layout, { dir, params, file, group }) {
139
+ const chain = [...(layout.chain ?? [])];
140
+ const have = new Set(chain.map((c) => c.path));
141
+ const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(join(dir, rel))) { chain.splice(at, 0, { path: rel, text: readFileSync(join(dir, rel), "utf8") }); have.add(rel); } };
142
+ if (group?.agents) add(group.agents, chain.findIndex((c) => c.path === "AGENTS.md") + 1);
143
+ if (file) {
144
+ const parts = dirname(file).split("/").filter((p) => p && p !== ".");
145
+ let acc = "";
146
+ for (const p of parts) { acc = acc ? `${acc}/${p}` : p; add(`${acc}/AGENTS.md`); }
147
+ }
148
+ const instructions = parseInstructions(chain);
149
+ if (params?.id) for (const k of Object.keys(instructions)) instructions[k] = instructions[k].replace(/\[id\]/g, params.id);
150
+ return { ...layout, chain, instructions, params: params ?? {}, ...(group ? { group } : {}) };
151
+ }
152
+
153
+ /** The read's hierarchy (P-28), outermost first, and the level that answered. */
154
+ export function hierarchy({ answer, layout }) {
155
+ const levels = [
156
+ { level: "layout", holds: layout ? { chain: layout.chain.map((c) => c.path), stack: layout.stack?.map((s) => s.name) } : null },
157
+ { level: "template", holds: layout ? { need_now: layout.need_now ?? null, gate: layout.gate ?? null, note: "recomputed on every action" } : null },
158
+ { level: "error", caught: !answer.ok || !!answer.errors, holds: !answer.ok ? { error: answer.error, fix: answer.fix ?? null } : answer.errors ?? null },
159
+ { level: "loading", caught: !!(answer.pending_draft || answer.pending_pull_requests), holds: answer.pending_draft ?? answer.pending_pull_requests ?? null },
160
+ { level: "not-found", caught: answer.ok && answer.result === null && !!answer.not_found, holds: answer.not_found ?? null },
161
+ { level: "page", caught: answer.ok && answer.result !== null && answer.result !== undefined, holds: answer.ok && answer.result != null ? "the record" : null },
162
+ ];
163
+ return { levels, resolved_by: resolvedBy(answer) };
164
+ }
165
+
166
+ /** The level that answered a read (P-28): error, then loading, then not-found, then page. */
167
+ export function resolvedBy(answer) {
168
+ if (!answer.ok) return "error";
169
+ if (answer.pending_draft || answer.pending_pull_requests) return "loading";
170
+ if (answer.result === null && answer.not_found) return "not-found";
171
+ if (answer.result === null && answer.filtered_out) return "page";
172
+ return "page";
173
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // Draws a release card (P-25, O-7). The card's data is assembled by code from the release's records
3
+ // (schema/releases.mjs cardOf, written to <venture>/.release/card.json); drawing it is the interface's
4
+ // job, so the picture lives here and never in the release. It is a 1200 by 630 SVG, the size link
5
+ // previews use, with the venture's name, phase, headline numbers with their dates, and who confirmed them.
6
+ //
7
+ // node interface/release-card.mjs <venture dir> prints the SVG
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ const esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13
+ const day = (t) => (t ? String(t).slice(0, 10) : "");
14
+
15
+ /** The card as SVG text. */
16
+ export function svg(card) {
17
+ const nums = card.headline.filter((h) => h.label !== "Live holes").slice(0, 4);
18
+ const cell = (h, i) => {
19
+ const x = 60 + i * 275;
20
+ return `<text x="${x}" y="330" font-size="64" font-weight="700" fill="#111">${esc(h.value)}${h.unit ? `<tspan font-size="24"> ${esc(h.unit)}</tspan>` : ""}</text>
21
+ <text x="${x}" y="370" font-size="22" fill="#333">${esc(h.label)}</text>
22
+ <text x="${x}" y="400" font-size="18" fill="#666">as of ${esc(day(h.as_of))}</text>`;
23
+ };
24
+ const by = card.confirmed_by.length ? `Confirmed by ${card.confirmed_by.join(", ")}` : "Nothing confirmed yet";
25
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" font-family="system-ui, sans-serif" role="img" aria-label="${esc(card.name)} release card">
26
+ <rect width="1200" height="630" fill="#fafaf7"/>
27
+ <rect x="0" y="0" width="1200" height="12" fill="#1f6f5c"/>
28
+ <text x="60" y="120" font-size="64" font-weight="700" fill="#111">${esc(card.name)}</text>
29
+ <text x="60" y="175" font-size="28" fill="#333">Phase: ${esc(card.phase)}</text>
30
+ ${nums.map(cell).join("\n ")}
31
+ <text x="60" y="520" font-size="22" fill="#333">${esc(by)}</text>
32
+ <text x="60" y="560" font-size="18" fill="#666">Release ${esc(card.release)}, built ${esc(day(card.built_at))}</text>
33
+ </svg>
34
+ `;
35
+ }
36
+
37
+ if (import.meta.url === `file://${process.argv[1]}`) {
38
+ const dir = process.argv[2];
39
+ const f = dir && join(dir, ".release/card.json");
40
+ if (!f || !existsSync(f)) { console.error("usage: node interface/release-card.mjs <venture dir> (after node schema/build-release.mjs)"); process.exit(2); }
41
+ process.stdout.write(svg(JSON.parse(readFileSync(f, "utf8"))));
42
+ }
@@ -24,6 +24,11 @@
24
24
  // record (escalate -> an agent's write is a draft), and the role's allowed routes
25
25
  // robots a team-plan role may list the routes it may read and write; system/robots shows them
26
26
  // aliases interface/route-aliases.json maps a renamed route to its new one, with a note
27
+ // hierarchy a read resolves layout, template, error, loading, not-found, then page (P-28);
28
+ // resolved_by names the level that answered (interface/reads.mjs)
29
+ // writes a write answers with what it changed and the next route to act on; a layout lists
30
+ // the actions open to the actor; package, group and venture routes; global errors
31
+ // (interface/writes.mjs wraps call)
27
32
  // storage a record is read and written through the connector for the source its record
28
33
  // type lives in (knowledge/connectors, EE-037): the venture's own folder unless its
29
34
  // knowledge/sources.json names another, such as a GitHub repository, where a write
@@ -40,7 +45,26 @@ import { agentFromEnv, entries as provEntries, record as prov } from "./provenan
40
45
  import { address } from "../knowledge/connectors/contract.mjs";
41
46
  import { holding, locked } from "../schema/decisions.mjs";
42
47
  import { configOf, hold } from "../schema/gate.mjs";
48
+ // Freshness (Revalidating, V-1 to V-8): stale flags on reads, and the freshness and revalidate commands.
49
+ import { readFlag as freshnessFlag } from "../schema/freshness.mjs";
50
+ // Gate group hooks (M-18, F-5, X-2, X-4 to X-6, P-2, P-7, U-4): who is acting, access at the record,
51
+ // the gate's verdict, where held drafts wait, and check settings. The logic lives in these modules.
52
+ import { actingCall, itemId, mayReadItem, mayReadSome } from "./acting.mjs";
53
+ import { checkPlan, gateWrite, heldDraft } from "./gate-hooks.mjs";
43
54
  import { LOCAL, attributeSources, connect, sourceFor } from "../knowledge/connectors/index.mjs";
55
+ // Caching (schema/caching.mjs): per-action memo, the record-level cache for command routes, and system/cache, system/judge, system/answer.
56
+ import { actionScope, cacheRoutes, command as cachedCommand, forget, internal as cacheInternal, memo } from "../schema/caching.mjs";
57
+ // Context hook: instrumentation of every call and judgment (interface/instrumentation.mjs, P-4, F-4).
58
+ import { instrumented, judgment } from "./instrumentation.mjs";
59
+ // Hooks for writes, reads and boundaries (wf/writes): the wrapper around call, per-record containment and private folders.
60
+ import { around } from "./writes.mjs";
61
+ import { contain } from "./boundaries.mjs";
62
+ import { packageSlots } from "./package-routes.mjs";
63
+ import { routable } from "../schema/structure.mjs";
64
+ // Brand hook (Y-2..Y-4, J-2..J-4, T-1, T-2, P-24, P-26): routes, commands and layout context from schema/brand.mjs.
65
+ import { BRAND_ROUTES, brandCommands, brandLayout } from "../schema/brand.mjs";
66
+ // Upgrades hook (V-9): a read says when what the record reads changed in its system of record.
67
+ import { staleOf } from "../schema/stale.mjs";
44
68
 
45
69
  export const repo = new URL("..", import.meta.url).pathname;
46
70
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -70,7 +94,9 @@ export function kits() {
70
94
  const needNowPriors = () => load(join(repo, "schema/need-now.json"));
71
95
 
72
96
  /** The route table, generated from schema/data-model.json, the kit manifests and schema/need-now.json. */
73
- export function routes() {
97
+ // Caching hook (F-12): built once per action and shared.
98
+ export function routes() { return memo("routes", buildRoutes); }
99
+ function buildRoutes() {
74
100
  const model = load(join(repo, "schema/data-model.json"));
75
101
  const ks = kits();
76
102
  const owner = new Map(ks.flatMap((k) => k.records.map((r) => [r, k])));
@@ -110,6 +136,8 @@ export function routes() {
110
136
  cmd("system/package", "schema/packages.mjs", "The venture package (EE-067, EE-069): the core packages open now and the handoffs, each record confirmed, to review or to write, what moves on its own and what waits, and what completion unlocks; --write renders the view and records a version. Assumes a solo founder or small founding team in Discovery and Validation; agents are a package enhancement"),
111
137
  cmd("system/need-now", "schema/need-now.mjs", "What the venture needs now, soon, or should hold back (EE-028)"),
112
138
  cmd("system/time-to-ready", "schema/time-to-ready.mjs", "Time to running, and to ready per layer and MAD tier"),
139
+ // Model steps hook (use model, EE-073): the logic is schema/model-steps.mjs; this route only shows the payload.
140
+ cmd("system/steps", "schema/model-steps.mjs", "The model steps' payload (use model, EE-073): what code computed, a placeholder for each judgment a model has yet to give, and the only fields each one receives; asks no model. mentura steps run asks one", { args: ["payload"] }),
113
141
  cmd("purpose/validation/assess", "schema/assess-problem.mjs", "What the problem's evidence is strong enough for: each assumption labelled demonstrated, inferred, assumed, unknown or contradicted, the commitment it supports and the next test (EE-050). Asks first when the record is incomplete; input as_is runs it anyway", { as_is: true }),
114
142
  cmd("governance/analytics", "schema/decision-analytics.mjs", "Decision analytics (EE-072): how many decisions of each type each function has made, open against decided, in whose words, in which phase, and their outcomes. Data only; a view is the platform's, or a visualization skill's", { actions: ["run", "layout"] }),
115
143
  cmd("governance/decisions", "schema/decisions.mjs", "The decision matrix: every decision the venture owes by layer and phase, with its parent and boundary, and whether it renders now, waits, comes later, is stale or deferred (EE-054, EE-057)"),
@@ -124,9 +152,17 @@ export function routes() {
124
152
  cmd("governance/payouts", "schema/payouts.mjs", "Who gets what at a sale value, from the cap table (EE-022); input value, else the scenarios in exit.json", { needs: ["governance/ownership.json"], venture_arg: "id" }),
125
153
  cmd("system/overview", null, "The venture at a glance: telos, validation, need-now, time to ready, team and recent provenance, rendered as parallel slots", { internal: "overview" }),
126
154
  cmd("system/robots", null, "Which roles may read and write which routes, from the team plan", { internal: "robots" }),
155
+ // Context hook (interface/vitals.mjs, interface/context.mjs): agent vitals and the context budget (N-6, C-15, N-13).
156
+ cmd("system/vitals", "interface/vitals.mjs", "Agent vitals (N-6): calls per task, tokens of context per call, time to a useful answer and time from a decision opening to a person deciding it, at the 75th percentile, each checked against its threshold (C-15). Reads the events log that instrumentation writes"),
157
+ cmd("system/context", "interface/context.mjs", "The context budget (N-13): what every route sends an agent, in tokens, against its budget, with the largest part and where to move it (S-14, F-6)", { args: ["budget"] }),
127
158
  cmd("knowledge/catalog", "knowledge/connectors/catalog.mjs", "Build or refresh the catalog: every item in every source the venture keeps records in, with its address and version, no content (EE-037)"),
128
159
  cmd("knowledge/search", "knowledge/connectors/catalog.mjs", "Find items across every source through the catalog; input value is the query (EE-037)", { value_flag: "--search" }),
160
+ // Freshness (Revalidating, V-1 to V-8): schema/freshness.mjs.
161
+ cmd("system/freshness", "schema/freshness.mjs", "What is fresh, stale or expired, and which records are live holes: by time (freshness profiles, per phase too) and on demand (a dependency changed, a where-used parent drifted, a tag, path or package was revalidated)"),
162
+ cmd("system/revalidate", "schema/freshness.mjs", "Mark records stale on demand: input value tag:<route> (the record and what depends on it), path:<folder>, package:<name>, or pivot (the validation package); a plain folder is refused in favour of exact tags", { value_flag: "--revalidate" }),
129
163
  );
164
+ out.push(...cacheRoutes(cmd)); // Caching hook: system/cache, system/judge, system/answer (schema/caching.mjs).
165
+ out.push(...brandCommands(cmd)); // Brand hook: brand tokens, voice, glossary terms, sitemap.xml, asset facts (schema/brand.mjs).
130
166
  return out.sort((a, b) => a.route.localeCompare(b.route));
131
167
  }
132
168
 
@@ -172,14 +208,15 @@ export function ventureDir(id, workspace = defaultWorkspace()) {
172
208
  }
173
209
  return null;
174
210
  }
175
- for (const d of walkVentures(workspace)) if (load(join(d, "venture.json")).id === id) return d;
211
+ // A broken venture.json is found by its folder name, so its global error can answer (E-8, interface/boundaries.mjs).
212
+ for (const d of walkVentures(workspace)) { let vid; try { vid = load(join(d, "venture.json")).id; } catch { vid = basename(d); } if (vid === id) return d; }
176
213
  return null;
177
214
  }
178
215
 
179
216
  /** The ventures under a workspace: id, name, phase, path (the [...venture] segment) and folder. */
180
217
  export function ventures(workspace = defaultWorkspace()) {
181
218
  const root = existsSync(join(workspace, "ventures")) ? join(workspace, "ventures") : workspace;
182
- return walkVentures(workspace).map((d) => { const v = load(join(d, "venture.json")); return { id: v.id, name: v.name, phase: v.phase, path: relative(root, d), dir: d }; });
219
+ return walkVentures(workspace).map((d) => { let v; try { v = load(join(d, "venture.json")); } catch { return { id: basename(d), name: null, phase: null, path: relative(root, d), dir: d, broken: true }; } return { id: v.id, name: v.name, phase: v.phase, path: relative(root, d), dir: d }; });
183
220
  }
184
221
 
185
222
  // The venture's decision rights: the template, tightened by the venture's own file.
@@ -200,7 +237,7 @@ export function gateOf(r, dir) {
200
237
  }
201
238
 
202
239
  // Robots: a team-plan role may list the routes it may read and write (globs over routes).
203
- function allowed(dir, actorId, action, route) {
240
+ export function allowed(dir, actorId, action, route) {
204
241
  if (!actorId || !dir) return true;
205
242
  const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
206
243
  if (!plan) return true;
@@ -219,6 +256,8 @@ const PACKAGE_ROUTE = {
219
256
  "anatomy-answers": "product/anatomy-answers", "anatomy-input": "product/anatomy", "venture-package": ".release/venture-package",
220
257
  "decision-record": "decisions",
221
258
  customer: "customers", investor: "investors", "money-account": "money", "raw-source": "raw/sources",
259
+ people: "people", founder: "people/founders", "fund-thesis": "fund/thesis", "fund-terms": "fund/terms", deal: "deals",
260
+ ...BRAND_ROUTES, // Brand hook: marketing/brand, public/assets, glossary (schema/brand.mjs).
222
261
  };
223
262
 
224
263
  // Prefer a file that is already there, so an unmigrated venture keeps its old path. A new write uses the first path, which is the package layout.
@@ -243,6 +282,7 @@ export function draftOf(route, venture, workspace) {
243
282
  const dir = ventureDir(venture, workspace);
244
283
  if (!r || r.kind !== "record" || r.collection === "list" || !dir) return null;
245
284
  const file = fileFor(r, params, dir);
285
+ if (!file) return null; // a route with no concrete file (a pattern-only slot) has no draft
246
286
  return { dir, file, draft: draftPath(file), gate: gateOf(r, dir), route: r.collection === "item" ? r.route.replace("[id]", params.id) : r.route };
247
287
  }
248
288
 
@@ -266,9 +306,11 @@ function instructionsOf(chain) {
266
306
  }
267
307
 
268
308
  /** A score below its threshold is uncertain: not an error and not an answer. It routes to the function owner. */
269
- export function judge(score, threshold, owner) {
270
- if (typeof score !== "number" || score < threshold) return { outcome: "uncertain", owner, result: null };
271
- return { outcome: "answer", result: score };
309
+ export function judge(score, threshold, owner, at) {
310
+ const out = typeof score !== "number" || score < threshold ? { outcome: "uncertain", owner, result: null } : { outcome: "answer", result: score };
311
+ // Context hook: a judgment made for a venture (at.dir) is an instrumentation event (F-4).
312
+ if (at?.dir) judgment(at.dir, { question: at.question, route: at.route, model: at.model, score, threshold, outcome: out.outcome, owner });
313
+ return out;
272
314
  }
273
315
 
274
316
  /** Runs fn. A throw is the uncaught outcome. */
@@ -292,9 +334,11 @@ export function layout(r, dir) {
292
334
  const pkg = owned ? { skill: skillsOf(owned).map((s) => s.path), template: r.template ?? null, gates: owned.gates ?? [] } : null;
293
335
  const out = { route: r.route, stack, chain, instructions: instructionsOf(chain), package: pkg, note: r.note, groups: r.groups ?? [] };
294
336
  if (r.kind === "record") Object.assign(out, { schema: r.schema, template: r.template, checks: r.checks, gate: gateOf(r, dir) });
337
+ { const brand = brandLayout(dir, r.route); if (brand) out.brand = brand; } // Brand hook: brand, voice and package terms at this route (Y-2, Y-3, T-2).
295
338
  if (dir && r.kind === "record") {
296
339
  try {
297
- const nn = JSON.parse(spawnSync("node", [join(repo, "schema/need-now.mjs"), dir, "--json"], { encoding: "utf8" }).stdout);
340
+ // Caching hook (O-5): shared with the system/need-now command in the same action.
341
+ const nn = JSON.parse(memo(`cmd:schema/need-now.mjs ${dir} --json`, () => spawnSync("node", [join(repo, "schema/need-now.mjs"), dir, "--json"], { encoding: "utf8" }), { dir }).stdout);
298
342
  const row = nn.rows.find((x) => x.type === r.record);
299
343
  if (row) out.need_now = { phase: nn.phase, status: row.status, now: row.now, next: row.next, why: row.why };
300
344
  } catch {}
@@ -311,10 +355,18 @@ const fix = (r) => ({ skills: r.kit_path ? skillsOf(kits().find((k) => k.path ==
311
355
  * routes; via: the interface (cli, mcp, api; default router); provenance: { derived_from,
312
356
  * decision, note, agent } for the log.
313
357
  */
314
- export async function call({ venture, route, action, input, actor = "agent", actor_id = process.env.MENTURA_ACTOR_ID, workspace, via = "router", provenance = {} }) {
358
+ // Caching hook (F-2): one top-level call is one action; the calls it makes join it and share its memo.
359
+ // Context hook: every call is timed and measured for agent vitals (interface/instrumentation.mjs).
360
+ // Gate group hook (M-18): who is acting is checked on every call and carried on the answer.
361
+ // Writes hook (interface/writes.mjs): adds what reads, layouts and writes answer.
362
+ export const call = (args) => actionScope(() => instrumented(args, (a) => actingCall(a, (b) => around(callInAction, b), ventureDir), ventureDir));
363
+ async function callInAction({ venture, route, action, input, actor = "agent", actor_id = process.env.MENTURA_ACTOR_ID, workspace, via = "router", provenance = {} }) {
315
364
  const requested = route;
316
365
  const { r, params, alias } = resolve(route);
317
366
  if (!r) return { ok: false, outcome: "error", error: `no route ${route}` };
367
+ // Context hook (interface/context.mjs): navigate reads a route header first, with its layout once per
368
+ // session, its template and prefetched refs; head stops after the header (N-3, L-2, O-4).
369
+ if (action === "navigate" || action === "head") return (await import("./context.mjs")).navigate({ ...(input ?? {}), venture, route: requested, workspace, actor, actor_id, via, header_only: action === "head" || !!input?.header_only });
318
370
  const deprecated = alias ? { deprecated: `${alias.from} is now ${alias.to}${alias.note ? `: ${alias.note}` : ""}` } : {};
319
371
  route = r.collection === "item" ? r.route.replace("[id]", params.id) : r.route;
320
372
  if (!r.actions.includes(action)) return { ok: false, error: `${route} has no action ${action} (it has ${r.actions.join(", ")})` };
@@ -323,7 +375,8 @@ export async function call({ venture, route, action, input, actor = "agent", act
323
375
  if (action === "layout") { if (venture && !dir) return { ok: false, error: `no venture ${venture}` }; return { ok: true, result: layout(r, dir), ...deprecated }; }
324
376
  if (!dir) return { ok: false, error: `no venture ${venture}` };
325
377
  const act = action === "write" ? "write" : "read";
326
- if (!allowed(dir, actor_id, act, requested) && !allowed(dir, actor_id, act, route)) return { ok: false, error: `${actor_id} may not ${act} ${route} (team plan routes)` };
378
+ // Gate group hook (F-5): a role that may read some items of a collection may read its list, filtered below.
379
+ if (!allowed(dir, actor_id, act, requested) && !allowed(dir, actor_id, act, route) && !(act === "read" && r.collection === "list" && mayReadSome(dir, actor_id, route))) return { ok: false, error: `${actor_id} may not ${act} ${route} (team plan routes)` };
327
380
  // An activity a decision unlocks stays closed to agents until a person makes that decision (EE-058).
328
381
  const lock = actor !== "person" && (action === "write" || action === "run") ? (locked(dir, requested, actor_id) || locked(dir, route, actor_id)) : null;
329
382
  if (lock) return { ok: false, error: `${route} is locked until decision ${lock.id} is made: ${lock.question}`, locked_by: lock.id,
@@ -337,15 +390,19 @@ export async function call({ venture, route, action, input, actor = "agent", act
337
390
  if (r.kind === "command") {
338
391
  if (r.internal === "overview") return { ok: true, result: await overview(venture, workspace), ...deprecated };
339
392
  if (r.internal === "robots") return { ok: true, result: robots(dir), ...deprecated };
393
+ // Caching hook: system/cache, system/judge and system/answer are handled in schema/caching.mjs.
394
+ if (["cache", "judge", "answer"].includes(r.internal)) return { ...(await cacheInternal(r.internal, { dir, venture, workspace, input, actor, actor_id, via, provenance }, { call, resolve, draftOf })), ...deprecated };
340
395
  const lacking = (r.needs ?? []).filter((f) => !existsSync(join(dir, f)));
341
396
  if (lacking.length) return { ok: false, error: `${route} needs ${lacking.join(", ")} in ${venture}`, fix: { missing: lacking } };
342
397
  if (r.value_flag && !input?.value) return { ok: false, error: `${route} needs a value (${r.note})` };
343
398
  const args = r.venture_arg === "id" ? [dir, ...(input?.value ? [String(input.value)] : []), "--json"] : r.value_flag ? [dir, r.value_flag, String(input.value), "--json"] : [dir, ...(r.args ?? []), ...(r.as_is && input?.as_is ? ["--as-is"] : []), "--json"];
344
- const p = spawnSync("node", [join(repo, r.script), ...args], { encoding: "utf8" });
399
+ // Caching hook (C-3, H-3): a declared command's result comes from the record-level cache; the spawn is memoized per action (O-5).
400
+ const p = await cachedCommand({ dir, route, script: r.script, args, input, actor, actor_id }, () => memo(`cmd:${r.script} ${args.join(" ")}`, () => spawnSync("node", [join(repo, r.script), ...args], { encoding: "utf8" }), { dir }));
401
+ const cache = p.cache ? { cache: p.cache } : {};
345
402
  if (p.status !== 0) return { ok: false, error: (p.stderr || p.stdout).trim() };
346
403
  // A command that needs input first (EE-050) answers needs_input: the caller asks the person
347
404
  // whether to fill the record in or run as is, and runs again with input as_is to go on.
348
- try { const out = JSON.parse(p.stdout); return { ok: true, result: out.needs_input ? null : out, ...(out.needs_input ? { needs_input: out.needs_input } : {}), ...deprecated }; } catch { return { ok: true, result: p.stdout, ...deprecated }; }
405
+ try { const out = JSON.parse(p.stdout); return { ok: true, result: out.needs_input ? null : out, ...(out.needs_input ? { needs_input: out.needs_input } : {}), ...cache, ...deprecated }; } catch { return { ok: true, result: p.stdout, ...cache, ...deprecated }; }
349
406
  }
350
407
 
351
408
  // Storage: the connector for the source this record type lives in.
@@ -365,11 +422,15 @@ export async function call({ venture, route, action, input, actor = "agent", act
365
422
  const patterns = r.collection === "item" ? [fileFor(r, params, dir)] : r.files;
366
423
  let items;
367
424
  try {
368
- const paths = patterns.some((g) => g.includes("*")) ? (await store.list()).map((x) => x.path).filter((f) => patterns.some((g) => glob(g).test(f))) : patterns;
369
- items = (await Promise.all(paths.map((f) => store.read(f)))).filter(Boolean);
425
+ // a file named _* is never an item; routable: private _folders (P-22)
426
+ const paths = patterns.some((g) => g.includes("*")) ? (await store.list()).map((x) => x.path).filter((f) => !/(^|\/)_[^/]*$/.test(f) && patterns.some((g) => glob(g).test(f) && routable(f, g))) : patterns;
427
+ // Caching hook (F-2): each record is read once per action and shared.
428
+ items = (await Promise.all(paths.map((f) => memo(`read:${src.id}:${f}`, () => store.read(f), { dir })))).filter(Boolean);
429
+ // Gate group hook (F-5): access is checked again at the record; a list holds only the items the role may read.
430
+ if (r.collection === "list" && actor_id) items = items.filter((x) => mayReadItem(dir, actor_id, route, itemId(r.files, x.path)));
370
431
  } catch (e) { return { ok: false, error: `${src.id}: ${String(e.message ?? e)}` }; }
371
432
  const found = items.map((x) => x.path);
372
- const draft = !remote && r.collection !== "list" && existsSync(join(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
433
+ const draft = !remote && r.collection !== "list" && fileFor(r, params, dir) && existsSync(join(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
373
434
  const loading = draft ? { pending_draft: { file: draft, since: provEntries(dir).filter((e) => e.entity === draft).at(-1)?.at ?? null, accept: `mentura accept ${route} --venture ${venture}` } } : {};
374
435
  if (remote) {
375
436
  const prs = provEntries(dir).filter((e) => e.source?.pull_request && found.concat(patterns).some((f) => e.entity === address(src.id, f)));
@@ -394,6 +455,9 @@ export async function call({ venture, route, action, input, actor = "agent", act
394
455
  }
395
456
  if (attrs.length) x.attributes = cites;
396
457
  }
458
+ // Freshness (V-5): a stale record is still read, flagged stale until it is redone (stale-while-revalidate).
459
+ const fresh = remote ? {} : await freshnessFlag(dir, found, r.collection === "list");
460
+ Object.assign(loading, fresh);
397
461
  const parseOne = parse;
398
462
  const parseWith = (x) => { const v = parseOne(x); return overlays.has(x.path) ? { ...v, ...overlays.get(x.path) } : v; };
399
463
  if (r.collection === "list") {
@@ -406,19 +470,26 @@ export async function call({ venture, route, action, input, actor = "agent", act
406
470
  }
407
471
  return path;
408
472
  };
409
- return { ok: true, result: Object.fromEntries(items.map((x) => [idOf(x.path), parseWith(x)])), sources: Object.fromEntries(items.map((x) => [idOf(x.path), cite(x)])), ...deprecated };
473
+ // Per-record containment (E-6): an item that does not parse is named in errors; the rest come back.
474
+ const { values, errors } = contain(items, parseWith, idOf);
475
+ return { ok: true, result: Object.fromEntries(values.map(([x, v]) => [idOf(x.path), v])), sources: Object.fromEntries(values.map(([x]) => [idOf(x.path), cite(x)])), ...(Object.keys(errors).length ? { errors } : {}), ...fresh, ...deprecated };
410
476
  }
411
477
  const data = Object.fromEntries(items.map((x) => [x.path, parseWith(x)]));
412
- return { ok: true, result: items.length === 1 ? data[found[0]] : data, ...(items.length === 1 ? { source: cite(items[0]) } : { sources: Object.fromEntries(items.map((x) => [x.path, cite(x)])) }), ...loading, ...deprecated };
478
+ // V-9 hook: a summary whose source changed since it was written is marked stale (schema/stale.mjs).
479
+ const stale = !remote && items.length === 1 ? staleOf(dir, found[0]) : null;
480
+ return { ok: true, result: items.length === 1 ? data[found[0]] : data, ...(items.length === 1 ? { source: cite(items[0]) } : { sources: Object.fromEntries(items.map((x) => [x.path, cite(x)])) }), ...loading, ...(stale ? { stale } : {}), ...deprecated };
413
481
  }
414
482
 
415
483
  if (action === "check") {
416
- const outs = r.checks.map((c) => ({ check: c, ...spawnSync("node", [join(repo, c), dir], { encoding: "utf8" }) }));
417
- const ok = outs.every((o) => o.status === 0);
418
- return { ok, result: outs.map((o) => ({ check: o.check, passed: o.status === 0, output: (o.stdout + o.stderr).trim() })), ...(ok ? {} : { fix: fix(r) }), ...deprecated };
484
+ // Gate group hook (P-7): the checks settings in naive.config.json turn a check off or make it warn only.
485
+ const plan = checkPlan(dir, r.checks);
486
+ const outs = plan.filter((p) => p.mode !== "off").map((p) => ({ check: p.check, mode: p.mode, ...spawnSync("node", [join(repo, p.check), dir], { encoding: "utf8" }) }));
487
+ const ok = outs.every((o) => o.status === 0 || o.mode === "warn");
488
+ return { ok, result: [...outs.map((o) => ({ check: o.check, passed: o.status === 0, ...(o.mode === "warn" ? { warn_only: true } : {}), output: (o.stdout + o.stderr).trim() })), ...plan.filter((p) => p.mode === "off").map((p) => ({ check: p.check, passed: null, off: true }))], ...(ok ? {} : { fix: fix(r) }), ...deprecated };
419
489
  }
420
490
 
421
491
  if (action === "write") {
492
+ forget(dir); // Caching hook (H-3): a write never uses the cache, and what this action memoized for the venture is read again after it.
422
493
  if (!input || typeof input !== "object") return { ok: false, error: "write needs the record as input" };
423
494
  if (r.collection === "item" && !/^[a-z0-9][a-z0-9-]*$/.test(params.id)) return { ok: false, error: `${params.id} is not a valid id (lowercase letters, digits and hyphens)` };
424
495
  const file = fileFor(r, params, dir);
@@ -444,7 +515,9 @@ export async function call({ venture, route, action, input, actor = "agent", act
444
515
  // (EE-054): an agent's write becomes a draft, as for a gated record.
445
516
  const held = !remote && actor !== "person" ? holding(dir, file) : null;
446
517
  // The venture's own fail-closed routes (naive.config.json, the gate, EE-073) hold an agent's write too.
447
- const configured = !remote ? hold(configOf(dir), r.route, actor) : null;
518
+ // Gate group hook (X-1 to X-6, U-4): the venture's gate decides; a refusal stops the write here.
519
+ const configured = !remote ? gateWrite({ dir, r, route, requested, file, actor, actor_id, provenance, gate }) : null;
520
+ if (configured?.then === "refuse") return { ok: false, error: configured.why, gate_rule: configured.rule, refused_by: configured.source };
448
521
  const gatedDraft = !remote && (!!gate?.drafts || !!held || !!configured) && actor !== "person";
449
522
  const attributeWrites = [];
450
523
  if (split.length && !gatedDraft) {
@@ -482,13 +555,12 @@ export async function call({ venture, route, action, input, actor = "agent", act
482
555
  }
483
556
 
484
557
  const draft = gatedDraft;
485
- if (draft) {
486
- const dest = join(dir, draftPath(file));
487
- mkdirSync(dirname(dest), { recursive: true });
488
- writeFileSync(dest, text);
489
- } else await store.write(file, recordText);
490
- const entry = log(draft ? draftPath(file) : file, draft ? "draft" : provenance.activity ?? "write");
491
- return { ok: true, result: { written: draft ? draftPath(file) : file, draft, gate: gate?.class ?? null, provenance: entry.hash, note: draft ? (gate?.drafts ? `${r.record} is a ${gate.class} decision (${gate.level}, owner ${gate.owner}): written as a draft for a person to accept` : held ? `decision ${held.id} ${held.why}: written as a draft for a person to accept` : `${configured.rule} fails closed in naive.config.json: written as a draft for a person to accept`) : "written", ...(held ? { held_by: held.id } : {}), ...(configured ? { gate_rule: configured.rule } : {}), ...attributesOut }, ...deprecated };
558
+ // Gate group hook (X-2): the held draft, the owner it was sent to and any variants kept side by side.
559
+ let held2 = null;
560
+ if (draft) held2 = heldDraft(dir, { draft: draftPath(file), file, text, verdict: configured, route, agent });
561
+ else await store.write(file, recordText);
562
+ const entry = log(draft ? held2.written : file, draft ? "draft" : provenance.activity ?? "write");
563
+ return { ok: true, result: { written: draft ? held2.written : file, draft, gate: gate?.class ?? null, provenance: entry.hash, note: draft ? (gate?.drafts ? `${r.record} is a ${gate.class} decision (${gate.level}, owner ${gate.owner}): written as a draft for a person to accept` : held ? `decision ${held.id} ${held.why}: written as a draft for a person to accept` : configured.why) : "written", ...(held ? { held_by: held.id } : {}), ...(configured ? { gate_rule: configured.rule, ...(configured.owner ? { owner: configured.owner } : {}) } : {}), ...(held2?.variants ? { variant: held2.variant, variants: held2.variants, decision: held2.decision } : {}), ...attributesOut }, ...deprecated };
492
564
  }
493
565
  return { ok: false, error: `unhandled action ${action}` };
494
566
  }
@@ -539,6 +611,7 @@ async function overview(venture, workspace) {
539
611
  slot("@need-now", async () => (await run("system/need-now")).rows.filter((x) => x.status === "suggest now").map((x) => ({ record: x.type, now: x.now, why: x.why }))),
540
612
  slot("@ready", async () => { const t = await run("system/time-to-ready"); return { running: t.running.on ?? `not yet: ${t.running.missing.join(", ")}`, layers: t.layers.map((l) => ({ layer: l.layer, ready: l.ready_on ?? `missing ${l.missing.join(", ")}` })) }; }),
541
613
  slot("@team", async () => { const p = await read("people/team-plan"); if (typeof p === "string") return p; return p.roles.map((x) => ({ role: x.id, occupant: x.occupant, status: x.status, needed_by: x.needed_by })); }),
614
+ slot("@packages", () => packageSlots(dir, venture)), // P-17: a package with nothing in it shows its default
542
615
  slot("@provenance", () => provEntries(dir).slice(-5).reverse().map((e) => ({ at: e.at, activity: e.activity, entity: e.entity, by: `${e.agent.kind}${e.agent.id ? ` ${e.agent.id}` : ""} via ${e.agent.via}` }))),
543
616
  ]),
544
617
  };