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,212 @@
1
+ #!/usr/bin/env node
2
+ // Assets in public/ (I-23, J-2, J-3, J-4): the files a venture ships as they are, each with a source,
3
+ // a license and a credit, the way raw sources answer where they came from. The register is
4
+ // public/assets.json (schema/public-assets.schema.json). Rendering work (sizes, lazy loading) is the
5
+ // interface's; the framework's part is rights and facts.
6
+ //
7
+ // J-2 every file in public/ has a register entry naming its source and license
8
+ // J-3 a remote image is a link with a note in raw/sources/<id>/; a full copy in public/ needs a
9
+ // license that allows copying. remote_hosts is the list of hosts an agent may fetch from
10
+ // (the platform fetcher, platform/integrations/remote-image.mjs, enforces it)
11
+ // J-4 facts (type, width, height, bytes, sha256) are read from the file by code, never guessed:
12
+ // the check fails when recorded facts differ from the file. Alt text an agent wrote carries
13
+ // agent provenance until a person confirms it (a confirm entry in provenance.jsonl, bound
14
+ // to the exact text); a missing credit fails the check
15
+ //
16
+ // No model is called and nothing here reaches the network.
17
+ //
18
+ // node schema/assets.mjs <venture dir> facts [--json] the facts code reads from each file in public/
19
+ // node schema/assets.mjs <venture dir> check [--json] the register check (also schema/check-assets.mjs)
20
+
21
+ import { createHash } from "node:crypto";
22
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
23
+ import { extname, join } from "node:path";
24
+ import { entries } from "../interface/provenance.mjs";
25
+
26
+ export const REGISTER = "public/assets.json";
27
+ const load = (p) => JSON.parse(readFileSync(p, "utf8"));
28
+
29
+ // ---------------------------------------------------------------------------------------------
30
+ // Facts read from the bytes. The type comes from the file's signature, not its name.
31
+
32
+ const EXT = { png: ["png"], gif: ["gif"], jpeg: ["jpg", "jpeg"], webp: ["webp"], ico: ["ico"], svg: ["svg"], pdf: ["pdf"], woff2: ["woff2"], woff: ["woff"], ttf: ["ttf"], otf: ["otf"] };
33
+ export const IMAGE = new Set(["png", "gif", "jpeg", "webp", "ico", "svg"]);
34
+ export const FONT = new Set(["woff2", "woff", "ttf", "otf"]);
35
+ export const MIME = { png: "image/png", gif: "image/gif", jpeg: "image/jpeg", webp: "image/webp", ico: "image/x-icon", svg: "image/svg+xml", pdf: "application/pdf", woff2: "font/woff2", woff: "font/woff", ttf: "font/ttf", otf: "font/otf" };
36
+
37
+ const ascii = (b, s, n) => b.subarray(s, s + n).toString("latin1");
38
+
39
+ function jpegSize(b) {
40
+ let i = 2;
41
+ while (i + 9 < b.length) {
42
+ if (b[i] !== 0xff) { i++; continue; }
43
+ const m = b[i + 1];
44
+ if (m === 0xff) { i++; continue; }
45
+ if ((m >= 0xc0 && m <= 0xc3) || (m >= 0xc5 && m <= 0xc7) || (m >= 0xc9 && m <= 0xcb) || (m >= 0xcd && m <= 0xcf)) return { height: b.readUInt16BE(i + 5), width: b.readUInt16BE(i + 7) };
46
+ if (m === 0xd8 || m === 0x01 || (m >= 0xd0 && m <= 0xd7)) { i += 2; continue; }
47
+ i += 2 + b.readUInt16BE(i + 2);
48
+ }
49
+ return {};
50
+ }
51
+
52
+ function webpSize(b) {
53
+ const chunk = ascii(b, 12, 4);
54
+ if (chunk === "VP8 " && b.length >= 30) return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff };
55
+ if (chunk === "VP8L" && b.length >= 25) {
56
+ const [b0, b1, b2, b3] = [b[21], b[22], b[23], b[24]];
57
+ return { width: 1 + (((b1 & 0x3f) << 8) | b0), height: 1 + (((b3 & 0x0f) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6)) };
58
+ }
59
+ if (chunk === "VP8X" && b.length >= 30) return { width: 1 + b.readUIntLE(24, 3), height: 1 + b.readUIntLE(27, 3) };
60
+ return {};
61
+ }
62
+
63
+ function svgSize(text) {
64
+ const tag = text.match(/<svg\b[^>]*>/i)?.[0] ?? "";
65
+ const attr = (n) => tag.match(new RegExp(`\\s${n}\\s*=\\s*["']\\s*([0-9.]+)(px)?\\s*["']`, "i"));
66
+ const w = attr("width"), h = attr("height");
67
+ if (w && h) return { width: Math.round(Number(w[1])), height: Math.round(Number(h[1])) };
68
+ const vb = tag.match(/viewBox\s*=\s*["']\s*[-0-9.]+[\s,]+[-0-9.]+[\s,]+([0-9.]+)[\s,]+([0-9.]+)\s*["']/i);
69
+ return vb ? { width: Math.round(Number(vb[1])), height: Math.round(Number(vb[2])) } : {};
70
+ }
71
+
72
+ /** What the bytes say: type, width and height for images, the sizes an icon holds, bytes and sha256. */
73
+ export function factsOf(buf) {
74
+ const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
75
+ let type = "unknown", dims = {}, sizes;
76
+ if (b.length >= 24 && b[0] === 0x89 && ascii(b, 1, 3) === "PNG") { type = "png"; dims = { width: b.readUInt32BE(16), height: b.readUInt32BE(20) }; }
77
+ else if (b.length >= 10 && /^GIF8[79]a$/.test(ascii(b, 0, 6))) { type = "gif"; dims = { width: b.readUInt16LE(6), height: b.readUInt16LE(8) }; }
78
+ else if (b.length >= 4 && b[0] === 0xff && b[1] === 0xd8) { type = "jpeg"; dims = jpegSize(b); }
79
+ else if (b.length >= 16 && ascii(b, 0, 4) === "RIFF" && ascii(b, 8, 4) === "WEBP") { type = "webp"; dims = webpSize(b); }
80
+ else if (b.length >= 6 && b.readUInt16LE(0) === 0 && b.readUInt16LE(2) === 1 && b.readUInt16LE(4) > 0) {
81
+ type = "ico";
82
+ const n = b.readUInt16LE(4);
83
+ sizes = [];
84
+ for (let k = 0; k < n && 6 + 16 * k + 1 < b.length; k++) { const w = b[6 + 16 * k] || 256, h = b[7 + 16 * k] || 256; sizes.push(`${w}x${h}`); }
85
+ const big = sizes.map((s) => s.split("x").map(Number)).sort((x, y) => y[0] * y[1] - x[0] * x[1])[0];
86
+ if (big) dims = { width: big[0], height: big[1] };
87
+ }
88
+ else if (ascii(b, 0, 5) === "%PDF-") type = "pdf";
89
+ else if (ascii(b, 0, 4) === "wOF2") type = "woff2";
90
+ else if (ascii(b, 0, 4) === "wOFF") type = "woff";
91
+ else if (ascii(b, 0, 4) === "OTTO") type = "otf";
92
+ else if (b.length >= 4 && b.readUInt32BE(0) === 0x00010000) type = "ttf";
93
+ else {
94
+ const head = b.subarray(0, 4096).toString("utf8");
95
+ if (/<svg\b/i.test(head)) { type = "svg"; dims = svgSize(b.toString("utf8")); }
96
+ }
97
+ return { type, ...(dims.width ? { width: dims.width, height: dims.height } : {}), ...(sizes ? { sizes } : {}), bytes: b.length, sha256: createHash("sha256").update(b).digest("hex") };
98
+ }
99
+
100
+ /** The file's extension agrees with what its bytes are. */
101
+ export const extensionFits = (path, type) => (EXT[type] ?? []).includes(extname(path).slice(1).toLowerCase());
102
+
103
+ // ---------------------------------------------------------------------------------------------
104
+ // Licenses. Copying a work into public/ (so a release ships it) needs a license that allows it.
105
+
106
+ const COPYABLE = ["cc0-1.0", "cc-by-4.0", "cc-by-3.0", "cc-by-sa-4.0", "cc-by-sa-3.0", "mit", "apache-2.0", "ofl-1.1", "bsd-2-clause", "bsd-3-clause", "isc", "unlicense", "public-domain", "owned", "licensed"];
107
+ /** Whether a license lets the venture keep and ship a full copy. Unknown, all-rights-reserved and editorial-only do not. */
108
+ export const allowsCopy = (license) => COPYABLE.includes(String(license ?? "").trim().toLowerCase());
109
+ export const LICENSES_THAT_COPY = COPYABLE;
110
+
111
+ /** A host allowlist entry matches a host exactly, or "*.example.com" matches any subdomain of it. */
112
+ export const hostAllowed = (host, hosts = []) => hosts.some((h) => h === host || (h.startsWith("*.") && host.endsWith(h.slice(1))));
113
+
114
+ /** The hash a person's confirmation of one alt text is bound to: the statement id in provenance. */
115
+ export const altStatement = (path, text) => `alt-${createHash("sha256").update(`${path}\n${text}`).digest("hex").slice(0, 16)}`;
116
+
117
+ // ---------------------------------------------------------------------------------------------
118
+ // The register and its check.
119
+
120
+ /** The files in public/, relative to the venture folder, without the register itself. */
121
+ export function publicFiles(dir) {
122
+ const root = join(dir, "public");
123
+ const walk = (d) => existsSync(d) ? readdirSync(d, { withFileTypes: true }).flatMap((e) => e.name.startsWith(".") ? [] : e.isDirectory() ? walk(join(d, e.name)) : [join(d, e.name)]) : [];
124
+ return walk(root).map((f) => f.slice(dir.length + 1)).filter((f) => f !== REGISTER).sort();
125
+ }
126
+
127
+ export const registerOf = (dir) => existsSync(join(dir, REGISTER)) ? load(join(dir, REGISTER)) : null;
128
+
129
+ /** The facts code reads from every file in public/. */
130
+ export function facts(dir) {
131
+ return publicFiles(dir).map((path) => ({ path, ...factsOf(readFileSync(join(dir, path))) }));
132
+ }
133
+
134
+ /** Which alt texts a person has confirmed, from the provenance log. */
135
+ export function confirmedAlts(dir) {
136
+ return new Set(entries(dir).filter((e) => e.activity === "confirm" && e.agent?.kind === "person" && e.statement?.startsWith("alt-")).map((e) => e.statement));
137
+ }
138
+
139
+ /**
140
+ * The register check. errors fail it; pending lists alt text an agent wrote that no person has
141
+ * confirmed yet (not an error: it ships with agent provenance until then).
142
+ */
143
+ export function checkAssets(dir) {
144
+ const errors = [], warnings = [], pending = [];
145
+ const reg = registerOf(dir);
146
+ const files = publicFiles(dir);
147
+ if (!reg) {
148
+ for (const f of files) errors.push(`${f}: no entry in ${REGISTER}, so nothing says where it came from or whether the venture may ship it (J-2)`);
149
+ return { ok: !errors.length, errors, warnings, pending, assets: 0, remote: 0 };
150
+ }
151
+ const listed = new Map((reg.assets ?? []).map((a) => [a.path, a]));
152
+ const confirmed = confirmedAlts(dir);
153
+ const remoteByUrl = new Map((reg.remote ?? []).map((r) => [r.url, r]));
154
+ for (const f of files) if (!listed.has(f)) errors.push(`${f}: no entry in ${REGISTER}, so nothing says where it came from or whether the venture may ship it (J-2)`);
155
+ const seen = new Set();
156
+ for (const a of reg.assets ?? []) {
157
+ if (seen.has(a.path)) errors.push(`${a.path}: listed twice`);
158
+ seen.add(a.path);
159
+ if (!existsSync(join(dir, a.path))) { errors.push(`${a.path}: listed in ${REGISTER} but the file is not there`); continue; }
160
+ const real = factsOf(readFileSync(join(dir, a.path)));
161
+ if (String(a.license).trim().toLowerCase() === "unknown") errors.push(`${a.path}: the license is unknown; find it before the venture ships the file (J-2)`);
162
+ if (!a.credit || !String(a.credit).trim()) errors.push(`${a.path}: no credit; say who made it (J-4)`);
163
+ if (real.type !== "unknown" && !extensionFits(a.path, real.type)) errors.push(`${a.path}: the bytes are ${real.type}, the name says otherwise`);
164
+ // Facts are code's: recorded facts must equal what the file says now.
165
+ if (!a.facts) errors.push(`${a.path}: no facts yet; run mentura assets fill so code records them (J-4)`);
166
+ else for (const k of ["type", "width", "height", "bytes", "sha256"]) if ((a.facts[k] ?? null) !== (real[k] ?? null)) { errors.push(`${a.path}: recorded ${k} ${a.facts[k] ?? "none"} is not the file's ${real[k] ?? "none"}; facts are filled by code, run mentura assets fill (J-4)`); break; }
167
+ // Alt text: an image needs it unless it is decorative; an agent's stays pending until a person confirms it.
168
+ if (IMAGE.has(real.type)) {
169
+ if (!a.alt?.text && !a.decorative) errors.push(`${a.path}: an image with no alt text; write it, or mark it decorative (J-4)`);
170
+ if (a.alt?.text) {
171
+ const ok = confirmed.has(altStatement(a.path, a.alt.text));
172
+ if (a.alt.by === "person" && !ok) errors.push(`${a.path}: alt text says a person wrote it, but provenance has no person's confirmation of this text (J-4)`);
173
+ if (a.alt.by !== "person" && !ok) pending.push({ path: a.path, alt: a.alt.text, by: a.alt.by ?? "agent", confirm: `mentura assets confirm ${a.path} --by <name> --confirm ${a.path}` });
174
+ }
175
+ }
176
+ // A copy of a remote work (J-3): its link must be in the register, and its license must allow a copy.
177
+ if (a.origin_url) {
178
+ const r = remoteByUrl.get(a.origin_url);
179
+ if (!r) errors.push(`${a.path}: copied from ${a.origin_url}, which has no remote entry with a note in raw/ (J-3)`);
180
+ if (!allowsCopy(a.license)) errors.push(`${a.path}: a full copy of ${a.origin_url} under license ${a.license}; keep it private (raw/sources/*/original.*) unless the license allows a copy (J-3)`);
181
+ }
182
+ }
183
+ for (const r of reg.remote ?? []) {
184
+ const src = join(dir, "raw/sources", r.id, "source.json");
185
+ if (!existsSync(src)) errors.push(`remote ${r.url}: no note in raw/sources/${r.id}/ (J-3)`);
186
+ else if (load(src).origin !== r.url) errors.push(`remote ${r.url}: raw/sources/${r.id}/source.json names another origin`);
187
+ let host = null;
188
+ try { host = new URL(r.url).host; } catch { errors.push(`remote ${r.url}: not a URL`); }
189
+ if (r.fetched && host && !hostAllowed(host, reg.remote_hosts ?? [])) errors.push(`remote ${r.url}: fetched from ${host}, which is not in remote_hosts, the hosts an agent may fetch from (J-3)`);
190
+ if (!r.credit) errors.push(`remote ${r.url}: no credit; say who made it (J-4)`);
191
+ if (r.copy && !allowsCopy(r.license)) errors.push(`remote ${r.url}: copied to ${r.copy} under license ${r.license}, which does not allow a copy (J-3)`);
192
+ if (r.copy && !listed.has(r.copy)) errors.push(`remote ${r.url}: copied to ${r.copy}, which is not in the register`);
193
+ }
194
+ return { ok: !errors.length, errors, warnings, pending, assets: (reg.assets ?? []).length, remote: (reg.remote ?? []).length };
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------------------------
198
+
199
+ if (import.meta.url === `file://${process.argv[1]}`) {
200
+ const [dir, mode = "check"] = process.argv.slice(2).filter((a) => !a.startsWith("--"));
201
+ const json = process.argv.includes("--json");
202
+ if (!dir || !existsSync(join(dir, "venture.json")) || !statSync(dir).isDirectory()) { console.error("usage: node schema/assets.mjs <venture dir> facts|check [--json]"); process.exit(2); }
203
+ if (mode === "facts") { const f = facts(dir); console.log(json ? JSON.stringify({ facts: f }, null, 2) : f.map((x) => `${x.path} ${x.type}${x.width ? ` ${x.width}x${x.height}` : ""} ${x.bytes} bytes`).join("\n") || "public/ is empty"); process.exit(0); }
204
+ const r = checkAssets(dir);
205
+ if (json) console.log(JSON.stringify(r, null, 2));
206
+ else {
207
+ for (const e of r.errors) console.log(`error ${e}`);
208
+ for (const p of r.pending) console.log(`pending ${p.path}: alt text by ${p.by}, not confirmed by a person`);
209
+ console.log(r.ok ? `assets: ${r.assets} files and ${r.remote} remote links have a source, license, credit and facts` : `assets: ${r.errors.length} errors`);
210
+ }
211
+ process.exit(r.ok ? 0 : 1);
212
+ }
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ // The venture's brand (Next.js CSS, Fonts and file-based metadata, translated): brand tokens in the
3
+ // marketing package, read by every view and every agent. Styling itself stays with the interface;
4
+ // what converts is the brand.
5
+ //
6
+ // Y-3 brand tokens (colors, typefaces, voice rules) live in marketing/brand.json, the global
7
+ // stylesheet. Voice rules are the global stylesheet for words: every agent's layout carries
8
+ // them, and the voice check holds marketing content to them
9
+ // Y-5 nearer context wins. brand.json scopes override the venture brand for a route prefix,
10
+ // T-2 a package's scope under an item's; a typeface is defined once and assigned to a role at
11
+ // the root (everywhere) or in a scope (only there), like next/font
12
+ // T-1 typefaces are brand tokens recorded with their license, self-hosted from public/ so a
13
+ // browser makes no outside request
14
+ // Y-4 an outside design system is a reference: linked, with its version and license, not copied
15
+ // Y-2 a package's own terms stay local: glossary/<package>.json terms reach other packages
16
+ // only when the glossary exports them
17
+ // P-24 the venture's logo and marks (logo, icon, apple-icon, favicon) are brand marks pointing at
18
+ // registered assets in public/, checked for type and size
19
+ // P-26 the sitemap is a marketing record (marketing/sitemap.json); sitemap.xml is rendered from
20
+ // it for a release, never from the framework's routes
21
+ //
22
+ // No model is called and nothing here reaches the network.
23
+ //
24
+ // node schema/brand.mjs <venture dir> tokens [--json] DTCG tokens, CSS and head links for the venture
25
+ // node schema/brand.mjs <venture dir> --for <route> [--json] the brand resolved at a route (nearer wins)
26
+ // node schema/brand.mjs <venture dir> voice [--json] marketing content against the voice rules
27
+ // node schema/brand.mjs <venture dir> --package <id> [--json] the terms a package sees (its own and exported ones)
28
+ // node schema/brand.mjs <venture dir> --base <url> [--json] sitemap.xml from the marketing sitemap
29
+ // node schema/brand.mjs <venture dir> check [--json] the brand check (also schema/check-brand.mjs)
30
+
31
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
32
+ import { join } from "node:path";
33
+ import { allowsCopy, factsOf, registerOf, REGISTER } from "./assets.mjs";
34
+
35
+ export const BRAND = "marketing/brand.json";
36
+ export const SITEMAPS = ["marketing/sitemap.json", "operations/marketing/sitemap.json"];
37
+ const load = (p) => JSON.parse(readFileSync(p, "utf8"));
38
+ export const brandOf = (dir) => dir && existsSync(join(dir, BRAND)) ? load(join(dir, BRAND)) : null;
39
+
40
+ // ---------------------------------------------------------------------------------------------
41
+ // Router hooks. interface/router.mjs spreads these into its tables; this module never imports the router.
42
+
43
+ /** Where the new record types route (package folder layout, EE-070). */
44
+ export const BRAND_ROUTES = { "marketing-brand": "marketing/brand", "public-assets": "public/assets", glossary: "glossary" };
45
+
46
+ /** The command routes this module adds, built with the router's own cmd(). */
47
+ export const brandCommands = (cmd) => [
48
+ cmd("marketing/brand/tokens", "schema/brand.mjs", "The venture's brand tokens (Y-3): colors and typefaces as W3C design tokens, the CSS custom properties and @font-face rules an interface loads, each scope as its own selector, and the head links for the venture's marks (P-24)", { args: ["tokens"] }),
49
+ cmd("marketing/brand/resolve", "schema/brand.mjs", "The brand at one route (Y-5, T-2): the venture brand with every scope that covers the route applied, nearer last, and which scope set each value; input value is the route", { value_flag: "--for" }),
50
+ cmd("marketing/voice", "schema/brand.mjs", "Marketing content held to the voice rules, the global stylesheet for words (Y-3): each phrase to avoid, where it is and what to use instead; and terms another package keeps local (Y-2)", { args: ["voice"] }),
51
+ cmd("glossary/terms", "schema/brand.mjs", "The terms one package sees (Y-2): its own glossary and the terms other packages export; their local terms stay with them. Input value is the package", { value_flag: "--package" }),
52
+ cmd("marketing/sitemap/xml", "schema/brand.mjs", "sitemap.xml rendered from the marketing sitemap record (P-26), for a release; input value is the site's base URL", { value_flag: "--base" }),
53
+ cmd("public/assets/facts", "schema/assets.mjs", "What code reads from each file in public/ (J-4): type from the bytes, width and height, icon sizes, bytes and sha256. Agents copy these, never guess them", { args: ["facts"] }),
54
+ ];
55
+
56
+ /** What a layout adds for an agent: the brand and voice at this route, and the terms its package sees. */
57
+ export function brandLayout(dir, route) {
58
+ if (!dir) return null;
59
+ const brand = brandOf(dir);
60
+ const pkg = route.split("/")[0];
61
+ const terms = glossaries(dir).size ? termsFor(dir, pkg) : null;
62
+ if (!brand && !terms) return null;
63
+ const at = brand ? resolveBrand(brand, route) : null;
64
+ return {
65
+ ...(at ? { name: at.name, tone: at.voice.tone, voice: at.voice.rules.map((r) => ({ id: r.id, avoid: r.avoid, use: r.use ?? null, why: r.why ?? null })), fonts: Object.fromEntries(Object.entries(at.fonts).map(([role, t]) => [role, t.family])), colors: at.colors, scopes: at.scopes } : {}),
66
+ ...(terms ? { terms: terms.terms.map((t) => ({ term: t.term, meaning: t.meaning, from: t.from })) } : {}),
67
+ };
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------------------------
71
+ // Resolution: nearer context wins (Y-5, T-2).
72
+
73
+ const covers = (scope, route) => route === scope || route.startsWith(`${scope}/`);
74
+ const depth = (s) => s.split("/").filter(Boolean).length;
75
+
76
+ /**
77
+ * The brand at a route: the venture's colors, font roles and voice rules, then each scope whose
78
+ * `for` covers the route, shallowest first, so the nearest scope wins. set_by says which scope set
79
+ * each value (the venture brand is "venture").
80
+ */
81
+ export function resolveBrand(brand, route = "") {
82
+ const faces = new Map((brand.typefaces ?? []).map((t) => [t.id, t]));
83
+ const colors = { ...(brand.colors ?? {}) };
84
+ const roles = { ...(brand.fonts ?? {}) };
85
+ const rules = new Map((brand.voice?.rules ?? []).map((r) => [r.id, r]));
86
+ let tone = brand.voice?.tone ?? [];
87
+ const set_by = {};
88
+ for (const k of Object.keys(colors)) set_by[`color.${k}`] = "venture";
89
+ for (const k of Object.keys(roles)) set_by[`font.${k}`] = "venture";
90
+ for (const k of rules.keys()) set_by[`voice.${k}`] = "venture";
91
+ const scopes = (brand.scopes ?? []).filter((s) => covers(s.for, route)).sort((a, b) => depth(a.for) - depth(b.for));
92
+ for (const s of scopes) {
93
+ for (const [k, v] of Object.entries(s.colors ?? {})) { colors[k] = v; set_by[`color.${k}`] = s.for; }
94
+ for (const [k, v] of Object.entries(s.fonts ?? {})) { roles[k] = v; set_by[`font.${k}`] = s.for; }
95
+ for (const r of s.voice?.rules ?? []) { rules.set(r.id, r); set_by[`voice.${r.id}`] = s.for; }
96
+ if (s.voice?.tone) tone = s.voice.tone;
97
+ }
98
+ const fonts = Object.fromEntries(Object.entries(roles).filter(([, id]) => faces.has(id)).map(([role, id]) => [role, faces.get(id)]));
99
+ return { route, name: brand.name ?? null, colors, fonts, voice: { tone, rules: [...rules.values()] }, scopes: scopes.map((s) => s.for), set_by };
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------------------------
103
+ // Tokens: W3C design tokens (DTCG), CSS custom properties and head links.
104
+
105
+ const cssName = (s) => String(s).toLowerCase().replace(/[^a-z0-9-]+/g, "-");
106
+ const familyList = (t) => [JSON.stringify(t.family), ...(t.fallback ?? [])].join(", ");
107
+ const FORMAT = { woff2: "woff2", woff: "woff", ttf: "truetype", otf: "opentype" };
108
+ const url = (path) => `/${path.replace(/^public\//, "")}`;
109
+
110
+ /** The marks as head links: the file-based icons Next.js turns into <link> tags. */
111
+ export function headLinks(dir, brand) {
112
+ const reg = registerOf(dir);
113
+ const factsFor = (p) => reg?.assets?.find((a) => a.path === p)?.facts ?? (existsSync(join(dir, p)) ? factsOf(readFileSync(join(dir, p))) : null);
114
+ const out = [];
115
+ for (const m of brand.marks ?? []) {
116
+ if (m.kind === "logo") continue;
117
+ const f = factsFor(m.asset);
118
+ const sizes = m.kind === "favicon" ? "any" : f?.width ? `${f.width}x${f.height}` : undefined;
119
+ const type = { png: "image/png", svg: "image/svg+xml", ico: "image/x-icon" }[f?.type];
120
+ out.push(Object.fromEntries(Object.entries({ rel: m.kind === "apple-icon" ? "apple-touch-icon" : "icon", href: url(m.asset), type, sizes }).filter(([, v]) => v)));
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /** DTCG tokens, CSS (custom properties, @font-face, one selector per scope) and head links. */
126
+ export function tokens(dir, brand = brandOf(dir)) {
127
+ if (!brand) return null;
128
+ const at = resolveBrand(brand, "");
129
+ const dtcg = { color: {}, font: {} };
130
+ for (const [k, v] of Object.entries(at.colors)) dtcg.color[k] = { $type: "color", $value: v };
131
+ for (const [role, t] of Object.entries(at.fonts)) dtcg.font[role] = { $type: "fontFamily", $value: [t.family, ...(t.fallback ?? [])], $description: `${t.family}, ${t.license}` };
132
+ const faces = (brand.typefaces ?? []).filter((t) => t.source === "self-hosted").flatMap((t) => (t.files ?? []).map((f) => {
133
+ const ext = f.split(".").pop().toLowerCase();
134
+ return `@font-face {\n font-family: ${JSON.stringify(t.family)};\n src: url("${url(f)}") format("${FORMAT[ext] ?? ext}");\n font-display: swap;\n}`;
135
+ }));
136
+ const decl = (colors, fonts) => [...Object.entries(colors).map(([k, v]) => ` --color-${cssName(k)}: ${v};`), ...Object.entries(fonts).map(([role, t]) => ` --font-${cssName(role)}: ${familyList(t)};`)].join("\n");
137
+ const blocks = [...faces, `:root {\n${decl(at.colors, at.fonts)}\n}`];
138
+ const faceById = new Map((brand.typefaces ?? []).map((t) => [t.id, t]));
139
+ for (const s of [...(brand.scopes ?? [])].sort((a, b) => depth(a.for) - depth(b.for))) {
140
+ const f = Object.fromEntries(Object.entries(s.fonts ?? {}).filter(([, id]) => faceById.has(id)).map(([role, id]) => [role, faceById.get(id)]));
141
+ const d = decl(s.colors ?? {}, f);
142
+ if (d) blocks.push(`[data-naive-scope="${s.for}"] {\n${d}\n}`);
143
+ }
144
+ return { name: brand.name ?? null, tokens: dtcg, css: blocks.join("\n\n") + "\n", head: headLinks(dir, brand), voice: brand.voice ?? null, design_systems: brand.design_systems ?? [] };
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------------------------
148
+ // Glossaries (Y-2): a package's terms stay local unless it exports them.
149
+
150
+ /** Every package glossary in the venture (glossary/<package>.json): package -> record. */
151
+ export function glossaries(dir) {
152
+ const out = new Map();
153
+ const g = dir ? join(dir, "glossary") : null;
154
+ if (!g || !existsSync(g)) return out;
155
+ for (const f of readdirSync(g).filter((n) => n.endsWith(".json")).sort()) out.set(f.slice(0, -5), load(join(g, f)));
156
+ return out;
157
+ }
158
+
159
+ /** The terms a package sees: its own (all of them), then other packages' exported ones. Its own win. */
160
+ export function termsFor(dir, pkg) {
161
+ const all = glossaries(dir);
162
+ const own = (all.get(pkg)?.terms ?? []).map((t) => ({ ...t, from: pkg }));
163
+ const names = new Set(own.map((t) => t.term.toLowerCase()));
164
+ const imported = [], shadows = [];
165
+ let hidden = 0;
166
+ for (const [p, g] of all) {
167
+ if (p === pkg) continue;
168
+ for (const t of g.terms ?? []) {
169
+ if (!t.export) { hidden++; continue; }
170
+ if (names.has(t.term.toLowerCase())) { shadows.push({ term: t.term, from: p, kept: pkg }); continue; }
171
+ imported.push({ ...t, from: p });
172
+ }
173
+ }
174
+ return { package: pkg, terms: [...own, ...imported], shadows, hidden };
175
+ }
176
+
177
+ function checkGlossaries(dir) {
178
+ const errors = [], warnings = [];
179
+ const exported = new Map();
180
+ for (const [p, g] of glossaries(dir)) {
181
+ if (g.package !== p) errors.push(`glossary/${p}.json: names package ${g.package}; the file for ${g.package} is glossary/${g.package}.json (Y-2)`);
182
+ const seen = new Set();
183
+ for (const t of g.terms ?? []) {
184
+ const k = t.term.toLowerCase();
185
+ if (seen.has(k)) errors.push(`glossary/${p}.json: ${t.term} is defined twice`);
186
+ seen.add(k);
187
+ if (!t.export) continue;
188
+ const prior = exported.get(k);
189
+ if (prior && prior.meaning !== t.meaning) errors.push(`${t.term}: exported by ${prior.from} and ${p} with different meanings; one of them keeps it local or they agree (Y-2)`);
190
+ else exported.set(k, { ...t, from: p });
191
+ }
192
+ }
193
+ return { errors, warnings };
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------------------------
197
+ // Voice (Y-3): the global stylesheet for words.
198
+
199
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
+ const phrase = (p) => new RegExp(`(^|[^\\p{L}\\p{N}])(${escape(p)})(?=$|[^\\p{L}\\p{N}])`, "giu");
201
+
202
+ /** Text held to voice rules: every phrase to avoid, by line, with what to use instead. Frontmatter and code are skipped. */
203
+ export function voiceCheck(text, rules) {
204
+ const out = [];
205
+ let fence = false, front = false;
206
+ text.split("\n").forEach((line, i) => {
207
+ if (i === 0 && line.trim() === "---") { front = true; return; }
208
+ if (front) { if (line.trim() === "---") front = false; return; }
209
+ if (/^\s*(```|~~~)/.test(line)) { fence = !fence; return; }
210
+ if (fence) return;
211
+ const plain = line.replace(/`[^`]*`/g, "");
212
+ for (const r of rules) for (const a of [].concat(r.avoid ?? [])) for (const m of plain.matchAll(phrase(a))) out.push({ rule: r.id, line: i + 1, found: m[2], use: r.use ?? null, why: r.why ?? null });
213
+ });
214
+ return out;
215
+ }
216
+
217
+ /** The venture's marketing content files, venture-relative. */
218
+ export function contentFiles(dir) {
219
+ const walk = (d) => existsSync(d) ? readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(join(d, e.name)) : /\.(mdx?|txt)$/.test(e.name) ? [join(d, e.name)] : []) : [];
220
+ return ["marketing/content", "operations/marketing/content"].flatMap((p) => walk(join(dir, p))).map((f) => f.slice(dir.length + 1)).sort();
221
+ }
222
+
223
+ /** Every marketing content file held to the voice rules in force at its path, and to other packages' local terms (warnings). */
224
+ export function voice(dir, brand = brandOf(dir)) {
225
+ const violations = [], leaks = [];
226
+ const local = [...glossaries(dir)].filter(([p]) => p !== "marketing").flatMap(([p, g]) => (g.terms ?? []).filter((t) => !t.export).map((t) => ({ ...t, from: p })));
227
+ for (const f of contentFiles(dir)) {
228
+ const text = readFileSync(join(dir, f), "utf8");
229
+ const route = f.replace(/^operations\//, "").replace(/\.(mdx?|txt)$/, "");
230
+ if (brand) for (const v of voiceCheck(text, resolveBrand(brand, route).voice.rules)) violations.push({ file: f, ...v });
231
+ for (const t of local) for (const v of voiceCheck(text, [{ id: t.term, avoid: t.term }])) leaks.push({ file: f, line: v.line, term: t.term, local_to: t.from });
232
+ }
233
+ return { ok: !violations.length, files: contentFiles(dir).length, violations, local_terms: leaks };
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------------------------
237
+ // The brand check.
238
+
239
+ const HEX = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
240
+
241
+ export function checkBrand(dir) {
242
+ const errors = [], warnings = [];
243
+ const brand = brandOf(dir);
244
+ const g = checkGlossaries(dir);
245
+ errors.push(...g.errors); warnings.push(...g.warnings);
246
+ if (!brand) return { ok: !errors.length, errors, warnings, brand: false };
247
+ const reg = registerOf(dir);
248
+ const asset = (p) => reg?.assets?.find((a) => a.path === p);
249
+ const faces = new Map((brand.typefaces ?? []).map((t) => [t.id, t]));
250
+ // Colors (Y-3).
251
+ const colorsOk = (cs, where) => { for (const [k, v] of Object.entries(cs ?? {})) if (!HEX.test(v)) errors.push(`${where}: color ${k} is ${v}, not a hex color`); };
252
+ colorsOk(brand.colors, BRAND);
253
+ // Typefaces (T-1): licensed, self-hosted from registered files, never fetched from another host by the browser.
254
+ for (const t of brand.typefaces ?? []) {
255
+ if (t.source !== "system" && !String(t.license ?? "").trim()) errors.push(`typeface ${t.id}: no license (T-1)`);
256
+ if (t.source === "remote") errors.push(`typeface ${t.id}: served from ${t.url ?? "another host"}, so every reader's browser makes an outside request; self-host its files in public/ (T-1)`);
257
+ if (t.source === "self-hosted") {
258
+ if (!(t.files ?? []).length) errors.push(`typeface ${t.id}: self-hosted but names no files in public/ (T-1)`);
259
+ for (const f of t.files ?? []) {
260
+ if (!f.startsWith("public/")) { errors.push(`typeface ${t.id}: ${f} is not in public/ (T-1)`); continue; }
261
+ if (!existsSync(join(dir, f))) { errors.push(`typeface ${t.id}: ${f} is not there (T-1)`); continue; }
262
+ const a = asset(f);
263
+ if (!a) errors.push(`typeface ${t.id}: ${f} has no entry in ${REGISTER} (T-1, J-2)`);
264
+ else if (String(a.license).toLowerCase() !== String(t.license).toLowerCase()) errors.push(`typeface ${t.id}: licensed ${t.license}, but ${REGISTER} says ${f} is ${a.license} (T-1)`);
265
+ const type = factsOf(readFileSync(join(dir, f))).type;
266
+ if (!["woff2", "woff", "ttf", "otf"].includes(type)) errors.push(`typeface ${t.id}: ${f} is ${type}, not a font file (T-1)`);
267
+ }
268
+ }
269
+ }
270
+ // Font roles, at the root and in scopes (T-2).
271
+ for (const [role, id] of Object.entries(brand.fonts ?? {})) if (!faces.has(id)) errors.push(`fonts.${role}: no typeface ${id} (T-2)`);
272
+ for (const s of brand.scopes ?? []) {
273
+ colorsOk(s.colors, `scope ${s.for}`);
274
+ for (const [role, id] of Object.entries(s.fonts ?? {})) if (!faces.has(id)) errors.push(`scope ${s.for}: fonts.${role} names no typeface ${id} (T-2)`);
275
+ }
276
+ const fors = (brand.scopes ?? []).map((s) => s.for);
277
+ for (const f of new Set(fors.filter((x, i) => fors.indexOf(x) !== i))) errors.push(`scope ${f} is declared twice; merge them (Y-5)`);
278
+ // Voice rules (Y-3).
279
+ const ids = (brand.voice?.rules ?? []).map((r) => r.id);
280
+ for (const id of new Set(ids.filter((x, i) => ids.indexOf(x) !== i))) errors.push(`voice rule ${id} is declared twice`);
281
+ // Marks (P-24): registered assets of the right type and size.
282
+ const KINDS = { favicon: ["ico"], icon: ["png", "svg", "ico"], "apple-icon": ["png"], logo: ["png", "svg", "jpeg", "webp"] };
283
+ for (const m of brand.marks ?? []) {
284
+ if (!existsSync(join(dir, m.asset))) { errors.push(`mark ${m.kind}: ${m.asset} is not there (P-24)`); continue; }
285
+ if (!asset(m.asset)) errors.push(`mark ${m.kind}: ${m.asset} has no entry in ${REGISTER}, so its source and license are unknown (P-24, J-2)`);
286
+ const f = factsOf(readFileSync(join(dir, m.asset)));
287
+ if (!KINDS[m.kind].includes(f.type)) errors.push(`mark ${m.kind}: ${m.asset} is ${f.type}; a ${m.kind} is ${KINDS[m.kind].join(" or ")} (P-24)`);
288
+ if (m.kind === "apple-icon" && f.type === "png" && (f.width !== 180 || f.height !== 180)) errors.push(`mark apple-icon: ${m.asset} is ${f.width}x${f.height}; an apple-icon is 180x180 (P-24)`);
289
+ if (m.kind === "icon" && f.width && f.width !== f.height) errors.push(`mark icon: ${m.asset} is ${f.width}x${f.height}; an icon is square (P-24)`);
290
+ }
291
+ const kinds = (brand.marks ?? []).map((m) => m.kind).filter((k) => k !== "icon");
292
+ for (const k of new Set(kinds.filter((x, i) => kinds.indexOf(x) !== i))) errors.push(`mark ${k} is declared twice (P-24)`);
293
+ // Design systems (Y-4): linked, not copied.
294
+ const systems = new Map((brand.design_systems ?? []).map((d) => [d.id, d]));
295
+ for (const d of brand.design_systems ?? []) if (!/^https?:\/\//.test(d.url ?? "")) errors.push(`design system ${d.id}: ${d.url ?? "no url"} is not a link; a design system is referenced, not copied (Y-4)`);
296
+ for (const a of reg?.assets ?? []) if (a.design_system) {
297
+ const d = systems.get(a.design_system);
298
+ if (!d) errors.push(`${a.path}: from design system ${a.design_system}, which the brand does not reference (Y-4)`);
299
+ else if (!allowsCopy(d.license)) errors.push(`${a.path}: a copy from design system ${d.id} (license ${d.license}); link to it at ${d.url} instead (Y-4)`);
300
+ }
301
+ // Content held to the voice.
302
+ const v = voice(dir, brand);
303
+ for (const x of v.violations) errors.push(`${x.file}:${x.line}: "${x.found}" breaks voice rule ${x.rule}${x.use ? `; use "${x.use}"` : ""} (Y-3)`);
304
+ for (const x of v.local_terms) warnings.push(`${x.file}:${x.line}: "${x.term}" is a term local to ${x.local_to}; export it from glossary/${x.local_to}.json or say it plainly (Y-2)`);
305
+ return { ok: !errors.length, errors, warnings, brand: true };
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------------------------
309
+ // The sitemap (P-26): rendered from the marketing record, for a release.
310
+
311
+ const xmlEscape = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
312
+
313
+ export function sitemapOf(dir) {
314
+ const f = SITEMAPS.find((p) => existsSync(join(dir, p)));
315
+ return f ? { file: f, record: load(join(dir, f)) } : null;
316
+ }
317
+
318
+ /** sitemaps.org XML from a marketing sitemap record: one <url> per planned page, home first. */
319
+ export function sitemapXml(record, base) {
320
+ let root;
321
+ try { root = new URL(base); } catch { throw new Error(`${base} is not a URL`); }
322
+ if (!/^https?:$/.test(root.protocol)) throw new Error(`${base} is not an http or https URL`);
323
+ const origin = base.replace(/\/+$/, "");
324
+ const lastmod = /^\d{4}-\d{2}-\d{2}/.test(record.planned_at ?? "") ? record.planned_at.slice(0, 10) : null;
325
+ const pages = [...record.pages].sort((a, b) => (a.level === "home" ? -1 : b.level === "home" ? 1 : 0));
326
+ const urls = pages.map((p) => {
327
+ const path = p.path.startsWith("/") ? p.path : `/${p.path}`;
328
+ return ` <url>\n <loc>${xmlEscape(origin + (path === "/" ? "/" : path))}</loc>${lastmod ? `\n <lastmod>${lastmod}</lastmod>` : ""}\n </url>`;
329
+ });
330
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join("\n")}\n</urlset>\n`;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------------------------
334
+
335
+ if (import.meta.url === `file://${process.argv[1]}`) {
336
+ const argv = process.argv.slice(2);
337
+ const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
338
+ const json = argv.includes("--json");
339
+ const dir = argv[0];
340
+ const mode = argv[1] && !argv[1].startsWith("--") ? argv[1] : null;
341
+ if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node schema/brand.mjs <venture dir> tokens|voice|check | --for <route> | --package <id> | --base <url> [--json]"); process.exit(2); }
342
+ const print = (x, text) => console.log(json ? JSON.stringify(x, null, 2) : text ?? JSON.stringify(x, null, 2));
343
+ if (opt("for") !== undefined) {
344
+ const brand = brandOf(dir);
345
+ if (!brand) { console.error(`no brand yet: write ${BRAND} (mentura marketing/brand template)`); process.exit(1); }
346
+ print(resolveBrand(brand, opt("for")));
347
+ } else if (opt("package") !== undefined) {
348
+ const t = termsFor(dir, opt("package"));
349
+ print(t, t.terms.map((x) => `${x.term.padEnd(24)} ${x.from.padEnd(12)} ${x.meaning}`).join("\n") || `no terms reach ${opt("package")}`);
350
+ } else if (opt("base") !== undefined) {
351
+ const s = sitemapOf(dir);
352
+ if (!s) { console.error("no marketing sitemap yet: plan it (mentura operations/marketing/sitemap run) and write marketing/sitemap.json"); process.exit(1); }
353
+ let xml;
354
+ try { xml = sitemapXml(s.record, opt("base")); } catch (e) { console.error(e.message); process.exit(1); }
355
+ print({ from: s.file, pages: s.record.pages.length, xml }, xml);
356
+ } else if (mode === "tokens") {
357
+ const t = tokens(dir);
358
+ if (!t) { console.error(`no brand yet: write ${BRAND}`); process.exit(1); }
359
+ print(t, t.css);
360
+ } else if (mode === "voice") {
361
+ const v = voice(dir);
362
+ print(v, v.violations.map((x) => `${x.file}:${x.line} "${x.found}" (${x.rule})${x.use ? ` -> ${x.use}` : ""}`).join("\n") || `voice: ${v.files} files keep to the voice rules`);
363
+ } else {
364
+ const r = checkBrand(dir);
365
+ if (json) console.log(JSON.stringify(r, null, 2));
366
+ else {
367
+ for (const e of r.errors) console.log(`error ${e}`);
368
+ for (const w of r.warnings) console.log(`warn ${w}`);
369
+ console.log(r.ok ? `brand: ${r.brand ? "tokens, typefaces, marks, design systems and voice check out" : "no brand record yet; glossaries check out"}` : `brand: ${r.errors.length} errors`);
370
+ }
371
+ process.exit(r.ok ? 0 : 1);
372
+ }
373
+ }