naive-framework 0.1.0 → 0.3.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 (281) hide show
  1. package/AGENTS.md +2 -1
  2. package/docs/decisions/decisions.json +42 -0
  3. package/docs/reference/ROUTES.md +36 -37
  4. package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
  5. package/governance/layer.json +2 -1
  6. package/governance/venture-template/roles/venture-roles.json +62 -0
  7. package/interface/acting.mjs +83 -0
  8. package/interface/adapters/claude-code.json +16 -0
  9. package/interface/adapters/codex.json +14 -0
  10. package/interface/adapters/cursor.json +16 -0
  11. package/interface/adapters.mjs +183 -0
  12. package/interface/api/server.mjs +4 -2
  13. package/interface/boundaries.mjs +219 -0
  14. package/interface/brand.mjs +215 -0
  15. package/interface/cli/freshness.mjs +59 -0
  16. package/interface/cli/guard.mjs +6 -0
  17. package/interface/cli/mentura.mjs +82 -11
  18. package/interface/cli/naive.mjs +44 -14
  19. package/interface/context.mjs +525 -0
  20. package/interface/demo/board.mjs +9 -8
  21. package/interface/demo/decision-harness.mjs +4 -3
  22. package/interface/dev/setup.mjs +84 -0
  23. package/interface/gate-hooks.mjs +37 -0
  24. package/interface/harness/evals.mjs +10 -9
  25. package/interface/harness/run.mjs +16 -15
  26. package/interface/instrumentation.mjs +87 -0
  27. package/interface/kit-boundary.mjs +73 -0
  28. package/interface/layer.json +8 -2
  29. package/interface/mcp/server.mjs +47 -7
  30. package/interface/package-routes.mjs +197 -0
  31. package/interface/provenance.mjs +28 -4
  32. package/interface/reads.mjs +174 -0
  33. package/interface/release-card.mjs +43 -0
  34. package/interface/router.mjs +110 -36
  35. package/interface/skills/naive-setup/SKILL.md +71 -0
  36. package/interface/skills/naive-setup/artifact.html +83 -0
  37. package/interface/vitals.mjs +116 -0
  38. package/interface/writes.mjs +190 -0
  39. package/knowledge/connectors/catalog.mjs +3 -2
  40. package/knowledge/connectors/filesystem.mjs +10 -5
  41. package/knowledge/connectors/index.mjs +3 -2
  42. package/knowledge/freshness.json +85 -0
  43. package/knowledge/layer.json +11 -3
  44. package/knowledge/templates/glossary.md +23 -0
  45. package/operations/layer.json +10 -3
  46. package/operations/marketing/check-public.mjs +7 -6
  47. package/operations/marketing/site/plan.mjs +4 -3
  48. package/operations/marketing/site/presence.mjs +4 -3
  49. package/operations/templates/marketing-brand.md +46 -0
  50. package/operations/templates/public-assets.md +39 -0
  51. package/package.json +3 -1
  52. package/packages/catalog.json +8 -0
  53. package/packages/customers/package.json +21 -1
  54. package/packages/deals/package.json +27 -0
  55. package/packages/decisions/package.json +12 -1
  56. package/packages/finance/frames/fund-without-ceding-control.frame.json +177 -0
  57. package/packages/foundation/package.json +8 -1
  58. package/packages/fund/package.json +48 -0
  59. package/packages/marketing/package.json +10 -4
  60. package/packages/money/package.json +24 -3
  61. package/packages/validation/package.json +13 -0
  62. package/platform/README.md +6 -0
  63. package/platform/adapter.json +15 -0
  64. package/platform/integrations/remote-image.mjs +79 -0
  65. package/platform/integrations/rho.mjs +9 -7
  66. package/platform/kits/README.md +8 -1
  67. package/platform/kits/agent-upgrade/kit.json +15 -0
  68. package/platform/kits/agent-upgrade/run.mjs +82 -0
  69. package/platform/kits/agent-upgrade/stand-in-agent.mjs +32 -0
  70. package/platform/kits/validation-kickoff/kit.json +29 -0
  71. package/platform/webhooks/server.mjs +70 -0
  72. package/purpose/frameworks/idea-map/ideamap.mjs +4 -2
  73. package/purpose/frameworks/idea-map/route.mjs +5 -4
  74. package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
  75. package/schema/README.md +1 -0
  76. package/schema/access.mjs +58 -0
  77. package/schema/adapter.schema.json +33 -0
  78. package/schema/add-department.mjs +3 -2
  79. package/schema/assess-problem.mjs +3 -2
  80. package/schema/assets.mjs +213 -0
  81. package/schema/brand.mjs +374 -0
  82. package/schema/build-release.mjs +61 -0
  83. package/schema/caching.mjs +506 -0
  84. package/schema/check-assets.mjs +24 -0
  85. package/schema/check-brand.mjs +24 -0
  86. package/schema/check-config.mjs +111 -0
  87. package/schema/check-data-model.mjs +2 -1
  88. package/schema/check-data-packages.mjs +5 -4
  89. package/schema/check-execution.mjs +4 -3
  90. package/schema/check-nextjs-status.mjs +3 -1
  91. package/schema/check-people.mjs +84 -0
  92. package/schema/check-private.mjs +2 -1
  93. package/schema/check-ready.mjs +6 -5
  94. package/schema/check-sources.mjs +11 -2
  95. package/schema/check-where-used.mjs +4 -3
  96. package/schema/data-model.json +334 -8
  97. package/schema/deal.schema.json +80 -0
  98. package/schema/decision-analytics.mjs +2 -1
  99. package/schema/decisions.mjs +21 -12
  100. package/schema/examples/frames/judge.mjs +10 -0
  101. package/schema/examples/venture-type/jev.mjs +12 -0
  102. package/schema/examples/workspace/ventures/acme-labs/AGENTS.md +17 -0
  103. package/schema/examples/workspace/ventures/acme-labs/accounts/.gitkeep +0 -0
  104. package/schema/examples/workspace/ventures/acme-labs/decisions/discovery-goal.decision.json +23 -0
  105. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-icp.decision.json +22 -0
  106. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-problem.decision.json +22 -0
  107. package/schema/examples/workspace/ventures/acme-labs/decisions/venture.team.decision.json +37 -0
  108. package/schema/examples/workspace/ventures/acme-labs/knowledge/README.md +3 -0
  109. package/schema/examples/workspace/ventures/acme-labs/knowledge/sources.json +46 -0
  110. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000001.json +12 -0
  111. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000002.json +12 -0
  112. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000003.json +12 -0
  113. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000004.json +12 -0
  114. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000005.json +12 -0
  115. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000006.json +12 -0
  116. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000007.json +12 -0
  117. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000008.json +12 -0
  118. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000009.json +12 -0
  119. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000010.json +12 -0
  120. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000011.json +12 -0
  121. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000012.json +12 -0
  122. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000013.json +12 -0
  123. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000014.json +12 -0
  124. package/schema/examples/workspace/ventures/acme-labs/money/_streams/transactions/stream.json +5 -0
  125. package/schema/examples/workspace/ventures/acme-labs/money/_summary.json +30 -0
  126. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/README.md +5 -0
  127. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/department.json +5 -0
  128. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/README.md +5 -0
  129. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/department.json +5 -0
  130. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/README.md +5 -0
  131. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/department.json +5 -0
  132. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/README.md +5 -0
  133. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/department.json +5 -0
  134. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/README.md +5 -0
  135. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/department.json +5 -0
  136. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/README.md +5 -0
  137. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/department.json +5 -0
  138. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/README.md +5 -0
  139. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/department.json +5 -0
  140. package/schema/examples/workspace/ventures/acme-labs/people/founders/maya-chen.json +15 -0
  141. package/schema/examples/workspace/ventures/acme-labs/people/people.json +22 -0
  142. package/schema/examples/workspace/ventures/acme-labs/pillars/brand.md +5 -0
  143. package/schema/examples/workspace/ventures/acme-labs/pillars/business.md +5 -0
  144. package/schema/examples/workspace/ventures/acme-labs/pillars/product.md +5 -0
  145. package/schema/examples/workspace/ventures/acme-labs/products/.gitkeep +0 -0
  146. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.core-product.decision.json +28 -0
  147. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.exists.decision.json +38 -0
  148. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.flagship.decision.json +27 -0
  149. package/schema/examples/workspace/ventures/acme-labs/profile.md +12 -0
  150. package/schema/examples/workspace/ventures/acme-labs/provenance.jsonl +12 -0
  151. package/schema/examples/workspace/ventures/acme-labs/raw/dump.md +6 -0
  152. package/schema/examples/workspace/ventures/acme-labs/venture.json +27 -0
  153. package/schema/examples/workspace/ventures/northstar/AGENTS.md +17 -0
  154. package/schema/examples/workspace/ventures/northstar/accounts/.gitkeep +0 -0
  155. package/schema/examples/workspace/ventures/northstar/deals/ductline.json +8 -0
  156. package/schema/examples/workspace/ventures/northstar/deals/kilnworks.json +14 -0
  157. package/schema/examples/workspace/ventures/northstar/decisions/venture.team.decision.json +31 -0
  158. package/schema/examples/workspace/ventures/northstar/evidence/README.md +3 -0
  159. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.decision.json +27 -0
  160. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.first-close.decision.json +27 -0
  161. package/schema/examples/workspace/ventures/northstar/fund/decisions/thesis.decision.json +26 -0
  162. package/schema/examples/workspace/ventures/northstar/fund/terms.json +22 -0
  163. package/schema/examples/workspace/ventures/northstar/fund/thesis.json +22 -0
  164. package/schema/examples/workspace/ventures/northstar/governance/README.md +3 -0
  165. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/charter.md +5 -0
  166. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/department.json +5 -0
  167. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/charter.md +5 -0
  168. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/department.json +5 -0
  169. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/charter.md +5 -0
  170. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/department.json +5 -0
  171. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/charter.md +5 -0
  172. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/department.json +5 -0
  173. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/charter.md +5 -0
  174. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/department.json +5 -0
  175. package/schema/examples/workspace/ventures/northstar/knowledge/README.md +3 -0
  176. package/schema/examples/workspace/ventures/northstar/operations/README.md +3 -0
  177. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/README.md +5 -0
  178. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/department.json +5 -0
  179. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/README.md +5 -0
  180. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/department.json +5 -0
  181. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/README.md +5 -0
  182. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/department.json +5 -0
  183. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/README.md +5 -0
  184. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/department.json +5 -0
  185. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/README.md +5 -0
  186. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/department.json +5 -0
  187. package/schema/examples/workspace/ventures/northstar/people/founders/ana-ruiz.json +13 -0
  188. package/schema/examples/workspace/ventures/northstar/people/founders/ben-okafor.json +7 -0
  189. package/schema/examples/workspace/ventures/northstar/people/people.json +27 -0
  190. package/schema/examples/workspace/ventures/northstar/pillars/brand.md +5 -0
  191. package/schema/examples/workspace/ventures/northstar/pillars/business.md +5 -0
  192. package/schema/examples/workspace/ventures/northstar/pillars/product.md +5 -0
  193. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/AGENTS.md +17 -0
  194. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/accounts/.gitkeep +0 -0
  195. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/decisions/venture.team.decision.json +31 -0
  196. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/knowledge/README.md +3 -0
  197. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/README.md +5 -0
  198. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/department.json +5 -0
  199. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/README.md +5 -0
  200. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/department.json +5 -0
  201. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/README.md +5 -0
  202. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/department.json +5 -0
  203. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/README.md +5 -0
  204. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/department.json +5 -0
  205. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/README.md +5 -0
  206. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/department.json +5 -0
  207. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/README.md +5 -0
  208. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/department.json +5 -0
  209. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/README.md +5 -0
  210. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/department.json +5 -0
  211. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/brand.md +5 -0
  212. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/business.md +5 -0
  213. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/product.md +5 -0
  214. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/.gitkeep +0 -0
  215. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.core-product.decision.json +28 -0
  216. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.exists.decision.json +32 -0
  217. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.flagship.decision.json +27 -0
  218. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/profile.md +5 -0
  219. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/venture.json +27 -0
  220. package/schema/examples/workspace/ventures/northstar/products/.gitkeep +0 -0
  221. package/schema/examples/workspace/ventures/northstar/profile.md +5 -0
  222. package/schema/examples/workspace/ventures/northstar/provenance.jsonl +3 -0
  223. package/schema/examples/workspace/ventures/northstar/purpose/README.md +3 -0
  224. package/schema/examples/workspace/ventures/northstar/venture.json +49 -0
  225. package/schema/foundation.mjs +8 -7
  226. package/schema/founder.schema.json +72 -0
  227. package/schema/frame.schema.json +240 -0
  228. package/schema/frames.mjs +229 -0
  229. package/schema/freshness-log.schema.json +40 -0
  230. package/schema/freshness-profiles.schema.json +44 -0
  231. package/schema/freshness.mjs +474 -0
  232. package/schema/fund-terms.schema.json +107 -0
  233. package/schema/fund-thesis.schema.json +87 -0
  234. package/schema/gate-drafts.mjs +131 -0
  235. package/schema/gate.mjs +105 -11
  236. package/schema/glossary.schema.json +25 -0
  237. package/schema/init-venture.mjs +69 -21
  238. package/schema/judgment.schema.json +38 -0
  239. package/schema/kit.schema.json +47 -0
  240. package/schema/kits.mjs +24 -0
  241. package/schema/knowledge-sources.schema.json +145 -15
  242. package/schema/layer.json +16 -3
  243. package/schema/marketing-brand.schema.json +116 -0
  244. package/schema/migrate-venture.mjs +8 -7
  245. package/schema/migrations/kit-to-package.mjs +95 -0
  246. package/schema/migrations/package-layout.mjs +90 -0
  247. package/schema/model-steps.mjs +718 -0
  248. package/schema/money-summary.schema.json +138 -0
  249. package/schema/naive-config.mjs +166 -0
  250. package/schema/naive-config.schema.json +268 -0
  251. package/schema/need-now.json +171 -0
  252. package/schema/need-now.mjs +8 -6
  253. package/schema/packages.mjs +21 -15
  254. package/schema/payouts.mjs +3 -2
  255. package/schema/people.schema.json +85 -0
  256. package/schema/port-venture.mjs +206 -132
  257. package/schema/public-assets.schema.json +76 -0
  258. package/schema/release-read.mjs +112 -0
  259. package/schema/releases.mjs +498 -0
  260. package/schema/run-handoff.mjs +5 -4
  261. package/schema/setup.mjs +165 -0
  262. package/schema/stale.mjs +112 -0
  263. package/schema/start-release.mjs +41 -0
  264. package/schema/structure.mjs +187 -0
  265. package/schema/systems-catalog.json +126 -0
  266. package/schema/systems-catalog.schema.json +78 -0
  267. package/schema/time-to-ready.mjs +7 -6
  268. package/schema/trigger.schema.json +44 -0
  269. package/schema/triggers.mjs +238 -0
  270. package/schema/upgrade-registry.schema.json +60 -0
  271. package/schema/upgrade-venture.mjs +96 -118
  272. package/schema/upgrades.json +69 -0
  273. package/schema/upgrades.mjs +386 -0
  274. package/schema/validate.py +19 -10
  275. package/schema/venture-paths.mjs +85 -0
  276. package/schema/venture-role.schema.json +64 -0
  277. package/schema/venture-type.mjs +62 -0
  278. package/schema/venture-types.json +346 -0
  279. package/schema/venture-types.schema.json +129 -0
  280. package/schema/venture.schema.json +61 -0
  281. package/schema/workspace.schema.json +5 -1
@@ -1,161 +1,235 @@
1
1
  #!/usr/bin/env node
2
- // naive port: brings an existing project into a new venture by keeping its files as they are (EE-076).
3
- // Each file is placed, unchanged, in the package directory its name or folder points to; a code
4
- // repository or anything that should stay where it is becomes a pointer in knowledge/sources.json;
5
- // what fits nowhere is left behind, by name, so nothing disappears silently. Migrate (rewriting a
6
- // project into the Naive convention) is the other way in, and is not this command.
2
+ // naive port: moves an existing folder into a venture, as it is (EE-076, revised by Torian on
3
+ // 2026-10-29: port moves, it no longer copies files into packages). The folder is moved whole, with a
4
+ // rename, so not one file in it is opened or changed; the venture names it in sources.json as a source
5
+ // of truth (a read-only filesystem source) and logs the move in provenance.jsonl with the path it came
6
+ // from. Migrate (rewriting a project into the Naive convention) is the other way in.
7
7
  //
8
- // node schema/port-venture.mjs --from <project> --venture <new venture dir>
9
- // writes the plan to <venture>/.release/port-plan.json and prints it; changes nothing else
10
- // node schema/port-venture.mjs --venture <dir> --apply --confirm port
11
- // carries out the plan as it now reads; the founder may first set any item's action to
12
- // port, point or leave
8
+ // node schema/port-venture.mjs <folder> --venture <dir> [--to <path>] [--links <dir> ...]
9
+ // adds the move to the plan at <venture>/.release/port-plan.json and prints it; moves nothing.
10
+ // The folder goes to <venture>/repo/<its name> unless --to says where (relative to the venture
11
+ // or absolute). Run once per folder: each run adds or replaces that folder's item.
12
+ // node schema/port-venture.mjs --venture <dir> --apply --confirm port [--even-if-busy]
13
+ // carries out every planned move the founder left as move; any item can be set to leave first
13
14
  //
14
- // The project is only ever read. A port never overwrites a file in the venture, refuses a plan
15
- // whose source files changed since it was written, and records every copy in provenance.jsonl
16
- // with the source path it was derived from.
15
+ // The plan says what a move would break: worktrees of a git repository (they point at its old path
16
+ // until `git worktree repair` runs from the new one), symbolic links that point into the folder, and
17
+ // processes working inside it, which refuse the apply unless --even-if-busy. A port never overwrites,
18
+ // never copies (a move across disks is refused, not copied), and refuses an item whose folder changed
19
+ // identity since the plan. Every run writes a trace to .release/port-trace.json, in the shape
20
+ // platform/integrations/langsmith.mjs uploads.
17
21
 
22
+ import { spawnSync } from "node:child_process";
18
23
  import { createHash } from "node:crypto";
19
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
20
- import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
21
- import { record } from "../interface/provenance.mjs";
22
- import { SECRET } from "./migrate-venture.mjs";
24
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
25
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
26
+ import { agentFromEnv, record } from "../interface/provenance.mjs";
27
+ import { ventureFile } from "./venture-paths.mjs";
23
28
 
24
29
  export const PLAN = ".release/port-plan.json";
25
- const SKIP = new Set([".git", "node_modules", ".next", ".turbo", "dist", "build", ".venv", "__pycache__", ".DS_Store"]);
26
- const REPO_MARKERS = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", "Gemfile", "pom.xml"];
27
- const CODE = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift", ".c", ".h", ".cpp", ".cs", ".php", ".sh", ".sql", ".css", ".scss", ".html", ".lock", ".yml", ".yaml", ".toml"]);
28
- const MAX_BYTES = 5 * 1024 * 1024;
30
+ export const TRACE = ".release/port-trace.json";
31
+ const MAP = "knowledge/sources.json";
32
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+|-$/g, "") || "folder";
33
+ const inside = (a, b) => a === b || a.startsWith(b + sep);
34
+ const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
29
35
 
30
- // The first rule whose pattern matches wins: the file name is tried before its folders, so a file
31
- // says more about itself than the folder it sits in. Patterns match words in lowercase paths.
32
- const RULES = [
33
- [/\b(problem|pain[- ]?points?)\b/, "foundation", "states the problem"],
34
- [/\b(icp|personas?|ideal[- ]customer|customer[- ]profile|segments?)\b/, "foundation", "describes the customer"],
35
- [/\b(vision|mission|tagline|values|manifesto|one[- ]pager|strategy)\b/, "foundation", "states what the venture is for"],
36
- [/\b(interviews?|surveys?|user[- ]research|customer[- ]research|discovery|experiments?|hypothes[ie]s|validation)\b/, "validation", "is evidence about the problem or the solution"],
37
- [/\b(investors?|pitch|deck|fundrais\w*|safe|term[- ]sheet|cap[- ]table|data[- ]room)\b/, "investors", "is for or about investors"],
38
- [/\b(pricing|prices?|revenue|budget|forecast|runway|financ\w*|p&l|burn|unit[- ]economics|model)\b/, "finance", "is about money the venture plans or earns"],
39
- [/\b(brand\w*|marketing|copy|landing|seo|social|launch|blog|newsletter|campaigns?|content|go[- ]to[- ]market|gtm)\b/, "marketing", "is marketing or sales"],
40
- [/\b(roadmap|specs?|prd|requirements|features?|product|designs?|ux|ui|wireframes?|mockups?|backlog)\b/, "product", "describes the product"],
41
- [/\b(team|hiring|roles?|org[- ]chart|founders?|equity|vesting|people|onboarding|handbook|culture)\b/, "people", "is about the people"],
42
- [/\b(customers?|clients?|accounts|crm|leads|pipeline)\b/, "customers", "is about customers"],
43
- [/\b(decisions?|adrs?|decision[- ]log)\b/, "decisions", "records decisions"],
44
- [/\b(legal|contracts?|agreements?|incorporation|bylaws|operating[- ]agreement|nda|terms|privacy|trademark|ip)\b/, "_private/legal", "is a legal document, kept private so it never reaches a model"],
45
- ];
46
- const BANK = /\b(bank|transactions?|ledger|payroll|tax(es)?)\b/;
36
+ // Files and bytes under a folder, without following links, to show the size of what moves, and a
37
+ // manifest hash (each path with its size, never its contents) for the provenance entry.
38
+ function measure(root) {
39
+ let files = 0, bytes = 0;
40
+ const lines = [];
41
+ const stack = [root];
42
+ while (stack.length) {
43
+ const d = stack.pop();
44
+ let list = [];
45
+ try { list = readdirSync(d, { withFileTypes: true }); } catch { continue; }
46
+ for (const e of list) {
47
+ const p = join(d, e.name);
48
+ if (e.isDirectory()) stack.push(p);
49
+ else { files++; let n = 0; try { n = lstatSync(p).size; } catch {} bytes += n; lines.push(`${relative(root, p)} ${n}`); }
50
+ }
51
+ }
52
+ return { files, bytes, manifest: createHash("sha256").update(lines.sort().join("\n")).digest("hex") };
53
+ }
47
54
 
48
- const words = (s) => s.toLowerCase().replace(/[_.]+/g, " ");
49
- const sha = (f) => createHash("sha256").update(readFileSync(f)).digest("hex");
50
- const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "project";
55
+ // Symbolic links under `roots` (to `depth` levels, skipping node_modules) whose target is inside `src`.
56
+ // A link inside `src` that points inside it with a relative path keeps working, so only absolute ones count.
57
+ export function linksInto(src, roots, depth = 4) {
58
+ const out = [];
59
+ const seen = new Set();
60
+ const visit = (d, n) => {
61
+ if (n > depth || seen.has(d)) return;
62
+ seen.add(d);
63
+ let list = [];
64
+ try { list = readdirSync(d, { withFileTypes: true }); } catch { return; }
65
+ for (const e of list) {
66
+ if (e.name === "node_modules" || e.name === ".git") continue;
67
+ const p = join(d, e.name);
68
+ if (e.isSymbolicLink()) {
69
+ const t = readlinkSync(p);
70
+ const target = resolve(dirname(p), t);
71
+ if (inside(target, src) && !(inside(p, src) && !isAbsolute(t))) out.push({ link: p, target: t });
72
+ } else if (e.isDirectory()) visit(p, n + 1);
73
+ }
74
+ };
75
+ for (const r of roots) visit(resolve(r), 0);
76
+ return out;
77
+ }
51
78
 
52
- /** Where one file goes, or why it stays behind. `rel` is relative to the project root. */
53
- export function place(rel, bytes) {
54
- const name = basename(rel, extname(rel));
55
- const ext = extname(rel).toLowerCase();
56
- if (bytes > MAX_BYTES) return { action: "leave", reason: `larger than ${MAX_BYTES / 1024 / 1024} MB; point to it instead` };
57
- if (CODE.has(ext)) return { action: "leave", reason: "code or configuration: it stays in its repository" };
58
- const folders = dirname(rel) === "." ? [] : dirname(rel).split(sep).reverse();
59
- for (const [where, confidence] of [[name, "high"], ...folders.map((f) => [f, "low"])]) {
60
- if (BANK.test(words(where))) return { action: "leave", reason: "bank and payroll data come from the bank connection, not from files" };
61
- const hit = RULES.find(([re]) => re.test(words(where)));
62
- if (hit) return { action: "port", to: `${hit[1]}/${basename(rel)}`, function: hit[1].split("/").at(-1), confidence, reason: `${confidence === "high" ? "its name" : `its folder ${where}`} ${hit[2]}` };
63
- }
64
- if (/^readme$/i.test(name)) return { action: "leave", reason: "the venture has its own AGENTS.md and profile.md; read this to write profile.md" };
65
- return { action: "leave", reason: "nothing in its name or folders matches a package; place it by hand or migrate it" };
79
+ // The git facts a move touches: whether the folder is a repository, its other worktrees, and how
80
+ // many changes are uncommitted (the move keeps them; it is only said).
81
+ function git(src) {
82
+ const g = join(src, ".git");
83
+ if (!existsSync(g)) return null;
84
+ if (statSync(g).isFile()) return { linked: true, worktrees: [], uncommitted: null };
85
+ const run = (...a) => spawnSync("git", ["-C", src, ...a], { encoding: "utf8" });
86
+ const wt = (run("worktree", "list", "--porcelain").stdout ?? "").split("\n").filter((l) => l.startsWith("worktree ")).map((l) => l.slice(9)).filter((p) => real(p) !== real(src));
87
+ const st = run("status", "--porcelain");
88
+ return { linked: false, worktrees: wt, uncommitted: st.status === 0 ? st.stdout.split("\n").filter(Boolean).length : null };
66
89
  }
67
90
 
68
- function walk(root, rel = "", out = { files: [], repos: [], secrets: [] }) {
69
- for (const e of readdirSync(join(root, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
70
- if (SKIP.has(e.name) || e.isSymbolicLink()) continue;
71
- const r = rel ? join(rel, e.name) : e.name;
72
- if (e.isDirectory()) {
73
- if (REPO_MARKERS.some((m) => existsSync(join(root, r, m)))) out.repos.push(r);
74
- else walk(root, r, out);
75
- } else if (e.isFile() && SECRET.test(r)) out.secrets.push(r);
76
- else if (e.isFile() && !e.name.startsWith(".")) out.files.push(r);
91
+ // Processes whose working directory is inside the folder (macOS and Linux lsof).
92
+ export function busy(src) {
93
+ const r = spawnSync("lsof", ["-d", "cwd", "-Fpcn"], { encoding: "utf8" });
94
+ if (r.error || !r.stdout) return [];
95
+ const out = [];
96
+ let p = null, c = null;
97
+ for (const l of r.stdout.split("\n")) {
98
+ if (l[0] === "p") p = l.slice(1);
99
+ else if (l[0] === "c") c = l.slice(1);
100
+ else if (l[0] === "n" && inside(l.slice(1), real(src)) && p !== String(process.pid)) out.push({ pid: Number(p), command: c, cwd: l.slice(1) });
77
101
  }
78
102
  return out;
79
103
  }
80
104
 
81
- /** The plan for porting `from` into the venture at `dir`. Reads both, writes neither. */
82
- export function plan(from, dir) {
83
- const src = resolve(from);
84
- if (!existsSync(src) || !statSync(src).isDirectory()) throw new Error(`${from} is not a directory`);
105
+ const readPlan = (dir) => existsSync(ventureFile(dir, PLAN)) ? JSON.parse(readFileSync(ventureFile(dir, PLAN), "utf8")) : null;
106
+ const readMap = (dir) => existsSync(ventureFile(dir, MAP)) ? JSON.parse(readFileSync(ventureFile(dir, MAP), "utf8")) : { sources: [], records: {} };
107
+
108
+ /** The planned move of `folder` into the venture at `dir`. Reads both, writes neither. */
109
+ export function planOne(folder, dir, { to, links = [] } = {}) {
110
+ if (!existsSync(folder) || !lstatSync(folder).isDirectory()) throw new Error(`${folder} is not a directory`);
85
111
  if (!existsSync(join(dir, "venture.json"))) throw new Error(`${dir} is not a venture: create one first with naive init`);
86
- if (resolve(dir).startsWith(src + sep) || resolve(dir) === src) throw new Error("the venture cannot sit inside the project it ports from");
87
- const { files, repos, secrets } = walk(src);
88
- const items = secrets.map((f) => ({ from: f, action: "leave", reason: "looks like a secret, so it was never opened" }));
89
- // The project itself is a repository when it carries a marker at its root: point to it, and still
90
- // port its documents.
91
- for (const r of REPO_MARKERS.some((m) => existsSync(join(src, m))) ? ["", ...repos] : repos)
92
- items.push({ from: r || ".", action: "point", source: slug(r ? basename(r) : basename(src)), reason: "a code repository stays where it is; the venture points to it" });
93
- const taken = new Set();
94
- for (const f of files) {
95
- const p = place(f, statSync(join(src, f)).size);
96
- if (p.action === "port") {
97
- // Two files with one name in one package: the second keeps its folder name in front.
98
- if (taken.has(p.to)) p.to = `${dirname(p.to)}/${slug(basename(dirname(f)))}-${basename(f)}`;
99
- if (taken.has(p.to) || existsSync(join(dir, p.to))) Object.assign(p, { action: "leave", reason: `${p.to} already exists in the venture; a port never overwrites` });
100
- else taken.add(p.to);
101
- }
102
- items.push({ from: f, sha256: sha(join(src, f)), ...p });
103
- }
104
- const count = (a) => items.filter((i) => i.action === a).length;
105
- return { from: src, venture: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).id, at: new Date().toISOString(),
106
- summary: { port: count("port"), point: count("point"), leave: count("leave") }, items };
112
+ const src = real(folder);
113
+ const v = real(dir);
114
+ if (inside(v, src)) throw new Error("the venture cannot sit inside the folder it ports");
115
+ const dest = resolve(v, to ?? join("repo", basename(src)));
116
+ const s = statSync(src);
117
+ const item = { action: "move", from: src, to: dest, source: slug(basename(src)), identity: { dev: s.dev, ino: s.ino }, ...measure(src) };
118
+ const warnings = [];
119
+ const leave = (why) => Object.assign(item, { action: "leave", reason: why });
120
+ if (existsSync(dest)) leave(`${dest} already exists; a port never overwrites`);
121
+ else if (inside(dest, src)) leave("the destination is inside the folder itself");
122
+ else if (!inside(dest, v)) warnings.push("the destination is outside the venture folder; sources.json will name it by absolute path");
123
+ // A rename only works on one disk: the nearest existing parent of the destination must share it.
124
+ let up = dirname(dest);
125
+ while (!existsSync(up)) up = dirname(up);
126
+ if (item.action === "move" && statSync(up).dev !== s.dev) leave("the destination is on another disk; a port moves, it never copies");
127
+ if (item.action === "move" && readMap(dir).sources.some((x) => x.id === item.source)) leave(`sources.json already has a source ${item.source}`);
128
+ const g = git(src);
129
+ item.git = g;
130
+ if (g?.linked) warnings.push("this folder is a linked git worktree; its main repository will point at the old path until `git worktree repair` runs there");
131
+ if (g?.worktrees.length) warnings.push(`${g.worktrees.length} worktree${g.worktrees.length === 1 ? "" : "s"} of this repository point at its old path until \`git -C ${dest} worktree repair\` runs: ${g.worktrees.join(", ")}`);
132
+ if (g?.uncommitted) warnings.push(`${g.uncommitted} uncommitted change${g.uncommitted === 1 ? "" : "s"} move with it, unchanged`);
133
+ item.links = linksInto(src, [dirname(src), src, ...links]);
134
+ if (item.links.length) warnings.push(`${item.links.length} symbolic link${item.links.length === 1 ? "" : "s"} point into it and will break: ${item.links.map((x) => x.link).join(", ")}`);
135
+ item.busy = busy(src);
136
+ if (item.busy.length) warnings.push(`${item.busy.length} process${item.busy.length === 1 ? " is" : "es are"} working inside it (${[...new Set(item.busy.map((x) => x.command))].join(", ")}); apply refuses unless --even-if-busy`);
137
+ item.warnings = warnings;
138
+ return item;
107
139
  }
108
140
 
109
- /** Carries out a plan as the founder left it. Returns what was done. */
110
- export function apply(dir, p, agent = { kind: "person", via: "cli" }) {
111
- const src = p.from;
112
- for (const i of p.items.filter((x) => x.action === "port")) {
113
- if (!existsSync(join(src, i.from)) || sha(join(src, i.from)) !== i.sha256) throw new Error(`${i.from} changed since the plan was written; write the plan again`);
114
- if (existsSync(join(dir, i.to))) throw new Error(`${i.to} already exists in the venture; a port never overwrites`);
141
+ /** Adds (or replaces) one folder's item in the venture's plan and writes it. */
142
+ export function plan(folder, dir, opts = {}) {
143
+ const item = planOne(folder, dir, opts);
144
+ const p = readPlan(dir) ?? { venture: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).id, items: [] };
145
+ const others = p.items.filter((i) => i.from !== item.from && i.action === "move" && !i.done);
146
+ if (others.some((i) => i.source === item.source)) Object.assign(item, { action: "leave", reason: `another planned move already uses the source id ${item.source}` });
147
+ if (others.some((i) => i.to === item.to)) Object.assign(item, { action: "leave", reason: `another planned move already goes to ${item.to}` });
148
+ p.items = [...p.items.filter((i) => i.from !== item.from || i.done), item];
149
+ p.at = new Date().toISOString();
150
+ mkdirSync(dirname(ventureFile(dir, PLAN)), { recursive: true });
151
+ writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
152
+ return { plan: p, item };
153
+ }
154
+
155
+ /** Carries out the plan as the founder left it. Checks every item before moving any. */
156
+ export function apply(dir, p, { agent = agentFromEnv("person", "cli"), evenIfBusy = false } = {}) {
157
+ const moves = p.items.filter((i) => i.action === "move" && !i.done);
158
+ const ids = new Set(readMap(dir).sources.map((s) => s.id));
159
+ for (const i of moves) {
160
+ if (!existsSync(i.from)) throw new Error(`${i.from} is gone; write the plan again`);
161
+ const s = statSync(i.from);
162
+ if (s.dev !== i.identity.dev || s.ino !== i.identity.ino) throw new Error(`${i.from} is not the folder the plan was written for; write the plan again`);
163
+ if (existsSync(i.to)) throw new Error(`${i.to} already exists; a port never overwrites`);
164
+ if (ids.has(i.source)) throw new Error(`sources.json already has a source ${i.source}`);
165
+ const b = busy(i.from);
166
+ if (b.length && !evenIfBusy) throw new Error(`${b.length} process${b.length === 1 ? " is" : "es are"} working inside ${i.from} (${b.map((x) => `${x.command} ${x.pid}`).join(", ")}); stop them or add --even-if-busy`);
115
167
  }
116
- const done = { ported: [], pointed: [] };
117
- for (const i of p.items.filter((x) => x.action === "port")) {
118
- mkdirSync(dirname(join(dir, i.to)), { recursive: true });
119
- copyFileSync(join(src, i.from), join(dir, i.to));
120
- record(dir, { entity: i.to, activity: "port", agent, derived_from: [`file://${join(src, i.from)}`], note: `ported unchanged: ${i.reason}` });
121
- done.ported.push(i.to);
122
- }
123
- const points = p.items.filter((x) => x.action === "point");
124
- if (points.length) {
125
- const f = join(dir, "knowledge/sources.json");
126
- const map = existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : { sources: [], records: {} };
127
- for (const i of points) {
128
- if (map.sources.some((s) => s.id === i.source)) continue;
129
- map.sources.push({ id: i.source, connector: "filesystem", writes: "none", root: join(src, i.from === "." ? "" : i.from), note: `ported as a pointer: ${i.reason}` });
130
- done.pointed.push(i.source);
131
- }
132
- mkdirSync(dirname(f), { recursive: true });
133
- writeFileSync(f, JSON.stringify(map, null, 2) + "\n");
134
- if (done.pointed.length) record(dir, { entity: "knowledge/sources.json", activity: "port", agent, note: `pointers to ${done.pointed.join(", ")}` });
168
+ const done = [];
169
+ for (const i of moves) {
170
+ mkdirSync(dirname(i.to), { recursive: true });
171
+ renameSync(i.from, i.to);
172
+ const map = readMap(dir);
173
+ const root = inside(i.to, real(dir)) ? relative(real(dir), i.to) : i.to;
174
+ map.sources.push({ id: i.source, connector: "filesystem", writes: "none", root, note: `source of truth, moved here unchanged from ${i.from} by naive port` });
175
+ mkdirSync(dirname(ventureFile(dir, MAP)), { recursive: true });
176
+ writeFileSync(ventureFile(dir, MAP), JSON.stringify(map, null, 2) + "\n");
177
+ record(dir, { entity: root, activity: "port", agent, derived_from: [`file://${i.from}`], sha256: measure(i.to).manifest, note: `moved unchanged (${i.files} files); named in sources.json as source ${i.source}` });
178
+ i.done = new Date().toISOString();
179
+ done.push(i);
180
+ writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
135
181
  }
136
182
  return done;
137
183
  }
138
184
 
185
+ // One run as a trace for platform/integrations/langsmith.mjs: the run, and a child per item.
186
+ function trace(dir, name, inputs, start, items, error) {
187
+ const end = new Date().toISOString();
188
+ const t = { name, run_type: "chain", start, end, inputs, outputs: { items: items.map(({ from, to, action, done }) => ({ from, to, action, done })) }, ...(error ? { error } : {}),
189
+ tags: ["naive", "port"], children: items.map((i) => ({ name: `${i.action} ${basename(i.from)}`, run_type: "tool", start, end, inputs: { from: i.from, to: i.to },
190
+ outputs: { action: i.action, reason: i.reason, files: i.files, bytes: i.bytes, warnings: i.warnings, done: i.done } })) };
191
+ try { mkdirSync(dirname(ventureFile(dir, TRACE)), { recursive: true }); writeFileSync(ventureFile(dir, TRACE), JSON.stringify(t, null, 2) + "\n"); } catch { return; }
192
+ // The harness's way (interface/harness/run.mjs): with a key set, the platform exporter ships the trace.
193
+ if (process.env.LANGSMITH_API_KEY) {
194
+ const p = spawnSync("node", [join(dirname(new URL(import.meta.url).pathname), "../platform/integrations/langsmith.mjs"), ventureFile(dir, TRACE), "--project", process.env.LANGSMITH_PROJECT ?? "naive-port"], { encoding: "utf8" });
195
+ console.log(p.status === 0 ? `LangSmith: ${p.stdout.trim()}` : `LangSmith upload failed: ${(p.stderr || p.stdout).trim()}`);
196
+ }
197
+ }
198
+
199
+ const mb = (b) => `${(b / 1024 / 1024).toFixed(1)} MB`;
200
+
139
201
  if (import.meta.url === `file://${process.argv[1]}`) {
140
202
  const args = process.argv.slice(2);
141
203
  const opt = (k) => { const i = args.indexOf(`--${k}`); return i < 0 ? undefined : args[i + 1]; };
204
+ const all = (k) => args.flatMap((a, i) => a === `--${k}` ? [args[i + 1]] : []);
205
+ const VALUED = new Set(["--venture", "--to", "--links", "--confirm", "--from"]);
206
+ const folder = opt("from") ?? args.find((a, i) => !a.startsWith("--") && !VALUED.has(args[i - 1]));
142
207
  const dir = opt("venture");
143
- if (!dir) { console.error("usage: node schema/port-venture.mjs --from <project> --venture <dir> | --venture <dir> --apply --confirm port"); process.exit(2); }
144
- try {
145
- if (args.includes("--apply")) {
146
- if (opt("confirm") !== "port") { console.error("type --confirm port to carry out the plan"); process.exit(1); }
147
- if (!existsSync(join(dir, PLAN))) { console.error(`no plan at ${join(dir, PLAN)}: run with --from first`); process.exit(1); }
148
- const d = apply(dir, JSON.parse(readFileSync(join(dir, PLAN), "utf8")));
149
- console.log(`ported ${d.ported.length} files unchanged; pointed to ${d.pointed.length} source${d.pointed.length === 1 ? "" : "s"}`);
150
- } else {
151
- const from = opt("from");
152
- if (!from) { console.error("--from <project> is needed to write a plan"); process.exit(2); }
153
- const p = plan(from, dir);
154
- mkdirSync(join(dir, ".release"), { recursive: true });
155
- writeFileSync(join(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
156
- for (const i of p.items) console.log(`${i.action.padEnd(5)} ${i.from}${i.to ? ` -> ${i.to}` : i.source ? ` -> source ${i.source}` : ""} (${i.reason})`);
157
- console.log(`\n${p.summary.port} to port, ${p.summary.point} to point to, ${p.summary.leave} left behind. Nothing has changed yet.`);
158
- console.log(`Read ${relative(process.cwd(), join(dir, PLAN))}, set any item's action to port, point or leave, then run with --apply --confirm port.`);
159
- }
160
- } catch (e) { console.error(e.message); process.exit(1); }
208
+ if (!dir) { console.error("usage: node schema/port-venture.mjs <folder> --venture <dir> [--to <path>] | --venture <dir> --apply --confirm port"); process.exit(2); }
209
+ const start = new Date().toISOString();
210
+ if (args.includes("--apply")) {
211
+ if (opt("confirm") !== "port") { console.error("type --confirm port to carry out the plan"); process.exit(1); }
212
+ const p = readPlan(dir);
213
+ if (!p) { console.error(`no plan at ${ventureFile(dir, PLAN)}: run naive port <folder> first`); process.exit(1); }
214
+ try {
215
+ const d = apply(dir, p, { evenIfBusy: args.includes("--even-if-busy") });
216
+ trace(dir, "naive port apply", { venture: dir }, start, d);
217
+ for (const i of d) console.log(`moved ${i.from} -> ${i.to} (${i.files} files, unchanged; source ${i.source})`);
218
+ for (const i of d.filter((x) => x.git?.worktrees.length)) console.log(`\nrepair its worktrees: git -C ${i.to} worktree repair`);
219
+ console.log(`\nmoved ${d.length} folder${d.length === 1 ? "" : "s"}. Trace: ${relative(process.cwd(), ventureFile(dir, TRACE))}`);
220
+ } catch (e) { trace(dir, "naive port apply", { venture: dir }, start, p.items, e.message); console.error(e.message); process.exit(1); }
221
+ } else {
222
+ if (!folder) { console.error("name the folder to port: naive port <folder>"); process.exit(2); }
223
+ try {
224
+ const { plan: p, item } = plan(folder, dir, { to: opt("to"), links: all("links") });
225
+ trace(dir, "naive port plan", { folder, venture: dir, to: opt("to") }, start, [item]);
226
+ for (const i of p.items) {
227
+ console.log(`${(i.done ? "moved" : i.action).padEnd(5)} ${i.from} -> ${i.to} (${i.files} files, ${mb(i.bytes)}${i.reason ? `; ${i.reason}` : ""})`);
228
+ if (!i.done) for (const w of i.warnings ?? []) console.log(` ! ${w}`);
229
+ }
230
+ const n = p.items.filter((i) => i.action === "move" && !i.done).length;
231
+ console.log(`\n${n} folder${n === 1 ? "" : "s"} to move, unchanged. Nothing has moved yet.`);
232
+ console.log(`Read ${relative(process.cwd(), ventureFile(dir, PLAN))}, set any item's action to leave, then run with --apply --confirm port.`);
233
+ } catch (e) { trace(dir, "naive port plan", { folder, venture: dir }, start, [], e.message); console.error(e.message); process.exit(1); }
234
+ }
161
235
  }
@@ -0,0 +1,76 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "public-assets.schema.json",
4
+ "title": "Public assets",
5
+ "description": "The register of what the venture ships as it is, at public/assets.json (I-23, J-2, J-3, J-4): each file in public/ with its source, license and credit, facts read by code from its bytes, and alt text with who wrote it; remote images as links with a note in raw/sources/<id>/; and remote_hosts, the hosts an agent may fetch from. Checked by schema/check-assets.mjs. Version 0.x, unevaluated.",
6
+ "type": "object",
7
+ "required": ["version", "assets"],
8
+ "additionalProperties": false,
9
+ "$defs": {
10
+ "alt": {
11
+ "type": "object",
12
+ "required": ["text", "by"],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "text": { "type": "string", "minLength": 1 },
16
+ "by": { "enum": ["agent", "person"], "description": "Who wrote it. An agent's alt text stays pending until a person confirms it in provenance" }
17
+ }
18
+ },
19
+ "facts": {
20
+ "type": "object",
21
+ "required": ["type", "bytes", "sha256"],
22
+ "additionalProperties": false,
23
+ "description": "Read from the bytes by code (schema/assets.mjs), never written by hand",
24
+ "properties": {
25
+ "type": { "type": "string" },
26
+ "width": { "type": "integer" },
27
+ "height": { "type": "integer" },
28
+ "sizes": { "type": "array", "items": { "type": "string" } },
29
+ "bytes": { "type": "integer" },
30
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
31
+ "filled_at": { "type": "string" }
32
+ }
33
+ }
34
+ },
35
+ "properties": {
36
+ "version": { "type": "string" },
37
+ "remote_hosts": { "type": "array", "items": { "type": "string" }, "description": "Hosts an agent may fetch images from (exact, or *.example.com); the platform fetcher refuses any other" },
38
+ "assets": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "required": ["path", "source", "license"],
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "path": { "type": "string", "pattern": "^public/(?!.*\\.\\./).+" },
46
+ "source": { "type": "string", "description": "Where it came from: who made it, or a URL" },
47
+ "license": { "type": "string", "description": "An SPDX id (CC-BY-4.0, OFL-1.1), owned (made by or for the venture), licensed (bought), or the restriction (all-rights-reserved)" },
48
+ "credit": { "type": "string", "description": "Who to credit; a missing credit fails the check" },
49
+ "alt": { "$ref": "#/$defs/alt" },
50
+ "decorative": { "type": "boolean", "description": "An image that says nothing, so it needs no alt text" },
51
+ "origin_url": { "type": "string", "description": "The remote work this is a copy of" },
52
+ "design_system": { "type": "string", "description": "The design system (brand design_systems id) this came from" },
53
+ "facts": { "$ref": "#/$defs/facts" }
54
+ }
55
+ }
56
+ },
57
+ "remote": {
58
+ "type": "array",
59
+ "description": "Remote images the venture links to rather than ships",
60
+ "items": {
61
+ "type": "object",
62
+ "required": ["id", "url", "license"],
63
+ "additionalProperties": false,
64
+ "properties": {
65
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "description": "The raw source holding the note: raw/sources/<id>/" },
66
+ "url": { "type": "string" },
67
+ "license": { "type": "string" },
68
+ "credit": { "type": "string" },
69
+ "alt": { "$ref": "#/$defs/alt" },
70
+ "fetched": { "$ref": "#/$defs/facts", "description": "Facts from a fetch by the platform; the original stays private in raw/sources/<id>/original.*" },
71
+ "copy": { "type": "string", "pattern": "^public/", "description": "Where a full copy was kept, only when the license allows it" }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,112 @@
1
+ // Reading a release without the router (C-12, I-17). A release is the venture's static shell
2
+ // (C-11): the confirmed records frozen in, and named holes for what stays live. Everything here
3
+ // reads <venture>/.release/ and nothing else, so a release can be read after the venture's records
4
+ // change, on a machine with no router, or handed to an investor as files. It never imports
5
+ // interface/router.mjs; schema/releases-test.mjs checks that.
6
+ //
7
+ // The answer to a path is { status, type, body }:
8
+ // / the release index: venture, phase, release id, routes in the release, holes
9
+ // /release.json the manifest; /card.json the release card's data (P-25, O-7)
10
+ // /records/<file> one frozen record, as it was when it was confirmed
11
+ // /<route> a route as the release holds it: a frozen record, a collection's frozen items,
12
+ // a frozen command answer (H-4), or a hole that names where to read it live.
13
+ // An item the release does not hold answers 404 with the items it does and the
14
+ // command that adds it without rebuilding the rest (C-18, N-10).
15
+ // Any method other than GET or HEAD answers 405: a release is read-only.
16
+
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join, normalize } from "node:path";
19
+ import { ventureFile } from "./venture-paths.mjs";
20
+
21
+ export const RELEASE_DIR = ".release";
22
+ export const MANIFEST = ".release/release.json";
23
+ export const CARD = ".release/card.json";
24
+ export const RECORDS = ".release/records";
25
+
26
+ /** The release manifest of a venture, or null when it has none. */
27
+ export function readRelease(dir) {
28
+ const f = ventureFile(dir, MANIFEST);
29
+ return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null;
30
+ }
31
+
32
+ /** The frozen copy of one record in a release, as text, or null. */
33
+ export function frozenText(dir, file) {
34
+ const rel = normalize(file);
35
+ if (rel.startsWith("..") || rel.startsWith("/")) return null;
36
+ const f = join(ventureFile(dir, RECORDS), rel);
37
+ return existsSync(f) ? readFileSync(f, "utf8") : null;
38
+ }
39
+
40
+ const parse = (file, text) => file.endsWith(".json") ? JSON.parse(text) : text;
41
+ const json = (status, body) => ({ status, type: "application/json; charset=utf-8", body: JSON.stringify(body, null, 2) + "\n" });
42
+
43
+ /** The index a reader starts from. */
44
+ export function index(m) {
45
+ return { release: m.release, venture: m.venture, name: m.name, phase: m.phase, built_at: m.built_at, now: m.now,
46
+ routes: m.routes.filter((r) => r.in === "release").map((r) => r.route),
47
+ holes: m.holes.map((h) => ({ kind: h.kind, route: h.route, name: h.name, why: h.why })),
48
+ card: "/card.json", manifest: "/release.json",
49
+ note: "A release is read-only. Holes stay live: read them in the venture through the router." };
50
+ }
51
+
52
+ /** What a route holds in a release: { status, body }. Matches a concrete route (product/kettle) to its entry. */
53
+ export function resolveRoute(dir, m, route) {
54
+ route = route.replace(/^\/+|\/+$/g, "");
55
+ const exact = m.routes.find((r) => r.route === route);
56
+ const entry = exact ?? m.routes.find((r) => r.route.endsWith("/[id]") && route.startsWith(r.route.slice(0, -4)) && !route.slice(r.route.length - 4).includes("/"));
57
+ if (!entry) return { status: 404, body: { error: `no route ${route} in release ${m.release}` } };
58
+ const id = exact ? null : route.slice(entry.route.length - 4);
59
+ const holes = m.holes.filter((h) => h.route === route || h.route_template === entry.route);
60
+ // A decision is also found by its id (decisions/founding-team), whatever its file is called.
61
+ const own = holes.find((h) => (h.route === route || (h.kind === "decision" && `decisions/${h.name}` === route)) && h.kind !== "attribute");
62
+ if (entry.in === "hole") return { status: 200, body: { route, hole: true, kind: own?.kind ?? entry.kind, why: own?.why ?? entry.why, live: own?.live ?? entry.live ?? null, ...(own ? {} : { holes }) } };
63
+ if (entry.in === "left out") return { status: 404, body: { route, left_out: true, why: entry.why } };
64
+ if (entry.kind === "command") {
65
+ const out = m.commands?.[entry.route];
66
+ return out === undefined ? { status: 404, body: { route, empty: true, why: entry.why ?? "not in this release" } } : { status: 200, body: { route, frozen: true, built_from: out.built_from, result: out.result } };
67
+ }
68
+ const recs = m.records.filter((r) => r.route_template === entry.route);
69
+ const fieldHoles = (r) => m.holes.filter((h) => h.kind === "attribute" && h.file === r.file).map((h) => h.field);
70
+ const one = (r) => ({ file: r.file, as_of: r.as_of, confirmed_by: r.confirmed_by, ...(fieldHoles(r).length ? { live_fields: fieldHoles(r) } : {}), record: parse(r.file, frozenText(dir, r.file)) });
71
+ if (entry.collection === "list") {
72
+ const items = m.records.filter((r) => r.route_template === `${entry.route}/[id]`);
73
+ return { status: 200, body: { route, items: Object.fromEntries(items.map((r) => [r.id, one(r)])), holes } };
74
+ }
75
+ if (entry.collection === "item") {
76
+ const hit = recs.find((r) => r.id === id);
77
+ if (own) return { status: 200, body: { route, hole: true, kind: own.kind, why: own.why, live: own.live ?? null } };
78
+ if (!hit) return { status: 404, body: { route, not_in_release: true, release: m.release, items: recs.map((r) => r.id), ...(holes.length ? { live_items: holes.filter((h) => h.kind !== "attribute").map((h) => h.name) } : {}),
79
+ note: `${route} was not known when release ${m.release} was built`, add: `node schema/build-release.mjs <venture dir> --add ${route}` } };
80
+ return { status: 200, body: { route, ...one(hit) } };
81
+ }
82
+ if (!recs.length) return { status: 404, body: { route, empty: true, why: entry.why ?? "no confirmed record when the release was built" } };
83
+ return { status: 200, body: { route, ...(recs.length === 1 ? one(recs[0]) : { records: recs.map(one) }) } };
84
+ }
85
+
86
+ /**
87
+ * Answers one request against a venture's release. draw, when given, turns the card's data into an
88
+ * image (the interface's job, O-7): draw(card) -> SVG text.
89
+ */
90
+ export function answer(dir, method, path, { draw } = {}) {
91
+ if (method !== "GET" && method !== "HEAD") return { ...json(405, { error: "a release is read-only: only GET and HEAD" }), allow: "GET, HEAD" };
92
+ const m = readRelease(dir);
93
+ if (!m) return json(503, { error: `no release in ${dir}: run node schema/build-release.mjs ${dir} first` });
94
+ const p = decodeURIComponent(path.split("?")[0]);
95
+ if (p.split("/").includes("..")) return json(400, { error: "no .. in a release path" });
96
+ if (p === "/" || p === "") return json(200, index(m));
97
+ if (p === "/release.json") return json(200, m);
98
+ if (p === "/card.json") return existsSync(ventureFile(dir, CARD)) ? json(200, JSON.parse(readFileSync(ventureFile(dir, CARD), "utf8"))) : json(404, { error: "this release has no card" });
99
+ if (p === "/card.svg") {
100
+ if (!draw) return json(404, { error: "no card drawing here" });
101
+ return { status: 200, type: "image/svg+xml; charset=utf-8", body: draw(JSON.parse(readFileSync(ventureFile(dir, CARD), "utf8"))) };
102
+ }
103
+ if (p.startsWith("/records/")) {
104
+ const file = p.slice("/records/".length);
105
+ const r = m.records.find((x) => x.file === file);
106
+ const text = r ? frozenText(dir, file) : null;
107
+ if (text === null) return json(404, { error: `${file} is not in release ${m.release}` });
108
+ return { status: 200, type: file.endsWith(".json") ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8", body: text };
109
+ }
110
+ const r = resolveRoute(dir, m, p);
111
+ return json(r.status, r.body);
112
+ }