naive-framework 0.2.0 → 0.4.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 (78) hide show
  1. package/AGENTS.md +1 -1
  2. package/docs/decisions/decisions.json +15 -0
  3. package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
  4. package/interface/adapters.mjs +7 -6
  5. package/interface/boundaries.mjs +5 -4
  6. package/interface/brand.mjs +6 -5
  7. package/interface/cli/mentura.mjs +4 -3
  8. package/interface/cli/naive.mjs +11 -6
  9. package/interface/context.mjs +7 -6
  10. package/interface/demo/board.mjs +9 -8
  11. package/interface/demo/decision-harness.mjs +4 -3
  12. package/interface/gate-hooks.mjs +2 -1
  13. package/interface/harness/evals.mjs +8 -7
  14. package/interface/harness/run.mjs +14 -13
  15. package/interface/instrumentation.mjs +4 -3
  16. package/interface/layer.json +4 -0
  17. package/interface/package-routes.mjs +4 -3
  18. package/interface/provenance.mjs +5 -4
  19. package/interface/reads.mjs +4 -3
  20. package/interface/release-card.mjs +2 -1
  21. package/interface/router.mjs +10 -9
  22. package/interface/skills/choosing-artifacts/SKILL.md +113 -0
  23. package/interface/vitals.mjs +3 -2
  24. package/knowledge/connectors/catalog.mjs +3 -2
  25. package/knowledge/connectors/filesystem.mjs +10 -5
  26. package/knowledge/connectors/index.mjs +3 -2
  27. package/operations/marketing/check-public.mjs +7 -6
  28. package/operations/marketing/site/plan.mjs +4 -3
  29. package/operations/marketing/site/presence.mjs +4 -3
  30. package/package.json +1 -1
  31. package/platform/integrations/rho.mjs +2 -1
  32. package/purpose/frameworks/idea-map/route.mjs +5 -4
  33. package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
  34. package/schema/access.mjs +2 -1
  35. package/schema/add-department.mjs +3 -2
  36. package/schema/assess-problem.mjs +3 -2
  37. package/schema/assets.mjs +6 -5
  38. package/schema/brand.mjs +11 -10
  39. package/schema/caching.mjs +5 -4
  40. package/schema/check-config.mjs +4 -3
  41. package/schema/check-data-model.mjs +2 -1
  42. package/schema/check-data-packages.mjs +6 -5
  43. package/schema/check-execution.mjs +4 -3
  44. package/schema/check-people.mjs +22 -8
  45. package/schema/check-private.mjs +2 -1
  46. package/schema/check-ready.mjs +6 -5
  47. package/schema/check-sources.mjs +2 -1
  48. package/schema/check-where-used.mjs +4 -3
  49. package/schema/decision-analytics.mjs +2 -1
  50. package/schema/decisions.mjs +14 -11
  51. package/schema/foundation.mjs +8 -7
  52. package/schema/founder.schema.json +2 -2
  53. package/schema/frames.mjs +4 -3
  54. package/schema/freshness.mjs +13 -12
  55. package/schema/gate-drafts.mjs +8 -7
  56. package/schema/init-venture.mjs +48 -20
  57. package/schema/migrate-venture.mjs +8 -7
  58. package/schema/migrations/kit-to-package.mjs +4 -3
  59. package/schema/migrations/package-layout.mjs +9 -5
  60. package/schema/model-steps.mjs +8 -7
  61. package/schema/naive-config.mjs +4 -3
  62. package/schema/need-now.mjs +5 -4
  63. package/schema/packages.mjs +11 -10
  64. package/schema/payouts.mjs +3 -2
  65. package/schema/people.schema.json +2 -3
  66. package/schema/port-venture.mjs +206 -132
  67. package/schema/release-read.mjs +5 -4
  68. package/schema/releases.mjs +21 -20
  69. package/schema/run-handoff.mjs +5 -4
  70. package/schema/setup.mjs +5 -4
  71. package/schema/stale.mjs +6 -5
  72. package/schema/structure.mjs +5 -4
  73. package/schema/time-to-ready.mjs +7 -6
  74. package/schema/triggers.mjs +7 -6
  75. package/schema/upgrades.mjs +31 -30
  76. package/schema/venture-paths.mjs +97 -0
  77. package/schema/venture.schema.json +25 -17
  78. package/schema/workspace.schema.json +5 -1
@@ -19,6 +19,7 @@
19
19
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
20
20
  import { dirname, join } from "node:path";
21
21
  import { customerNeeds, factsOf, MATRIX, needs, typesOf, weights } from "./presence.mjs";
22
+ import { ventureFile } from "../../../schema/venture-paths.mjs";
22
23
 
23
24
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
24
25
  export const SITEMAP = "operations/marketing/sitemap.json";
@@ -33,10 +34,10 @@ const RECORD_FILE = { venture: "venture.json", icp: "purpose/icp.json", "validat
33
34
 
34
35
  /** The offerings: each product, what kind of offering it is, whom it is offered to and whom it serves through them. */
35
36
  export function offeringsOf(dir) {
36
- const icp = existsSync(join(dir, "purpose/icp.json")) ? load(join(dir, "purpose/icp.json")) : { profiles: [] };
37
+ const icp = existsSync(join(ventureFile(dir, "purpose/icp.json"))) ? load(join(ventureFile(dir, "purpose/icp.json"))) : { profiles: [] };
37
38
  const profiles = new Map(icp.profiles.map((p) => [p.id, p]));
38
39
  const primary = icp.profiles.find((p) => p.primary) ?? icp.profiles[0];
39
- const pdir = join(dir, "products");
40
+ const pdir = join(ventureFile(dir, "products"));
40
41
  const products = existsSync(pdir) ? readdirSync(pdir).filter((f) => f.endsWith(".json")).map((f) => load(join(pdir, f))).filter((p) => !["retired", "archived", "idea"].includes(p.status)) : [];
41
42
  return { profiles, products: products.map((p) => {
42
43
  // Who it is offered to: the product says so; otherwise the profiles others are reached through
@@ -104,7 +105,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
104
105
  if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node plan.mjs <venture> [--raising] [--hiring] [--contributors] [--write] [--json]"); process.exit(2); }
105
106
  const has = (n) => process.argv.includes(`--${n}`);
106
107
  const r = plan(dir, { flags: Object.fromEntries(["raising", "hiring", "contributors"].filter(has).map((k) => [k, true])) });
107
- if (has("write")) { mkdirSync(join(dir, dirname(SITEMAP)), { recursive: true }); writeFileSync(join(dir, SITEMAP), JSON.stringify(r, null, 2) + "\n"); }
108
+ if (has("write")) { mkdirSync(join(dir, dirname(SITEMAP)), { recursive: true }); writeFileSync(ventureFile(dir, SITEMAP), JSON.stringify(r, null, 2) + "\n"); }
108
109
  if (has("json")) console.log(JSON.stringify(r, null, 2));
109
110
  else {
110
111
  console.log(`${r.name}: site planned for ${r.audience.offered_to.map((p) => p.id).join(", ") || "no ICP yet"} (${r.audience.types.map((t) => t.label).join(", ") || "customers in general"})${r.audience.partner_led ? "; partner-led" : ""}`);
@@ -22,6 +22,7 @@
22
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { dirname, join } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { ventureFile } from "../../../schema/venture-paths.mjs";
25
26
 
26
27
  const here = dirname(fileURLToPath(import.meta.url));
27
28
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -34,7 +35,7 @@ const r2 = (x) => Math.round(x * 100) / 100;
34
35
  /** The venture's facts: flags the founder gave, else what its ICP record says about who it courts. */
35
36
  export function factsOf(dir, flags = {}) {
36
37
  const f = { raising: false, hiring: false, contributors: false };
37
- const icpFile = dir && join(dir, "purpose/icp.json");
38
+ const icpFile = dir && join(ventureFile(dir, "purpose/icp.json"));
38
39
  if (icpFile && existsSync(icpFile)) {
39
40
  const said = JSON.stringify(load(icpFile)).toLowerCase();
40
41
  if (/investor|angel|\bvc\b/.test(said)) f.raising = true;
@@ -57,7 +58,7 @@ export function icpOf(dir, site) {
57
58
  const who = ok.filter((p) => /customer|solution|who|industr|use-case|about|for-/i.test(p.path));
58
59
  const says = [...new Set([home, ...who].filter(Boolean).flatMap((p) => (p.text ?? "").match(SAYS) ?? []).map((x) => x.trim()))].slice(0, 8);
59
60
  const opening = (home?.text ?? "").slice(0, 500);
60
- const icpFile = dir && join(dir, "purpose/icp.json");
61
+ const icpFile = dir && join(ventureFile(dir, "purpose/icp.json"));
61
62
  const stated = icpFile && existsSync(icpFile) ? (load(icpFile).profiles ?? []).map((p) => p.who ?? p.description ?? p.name).filter(Boolean) : null;
62
63
  return { text: [says.join(" | "), opening].filter(Boolean).join(" || "), source: says.length ? "site" : "inferred", says,
63
64
  cite: says.length ? `what the site says about who it is for (${says.length} phrase${says.length > 1 ? "s" : ""}), with the home page's opening` : "the home page's opening text (inferred: the site does not say outright who it is for)",
@@ -237,7 +238,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
237
238
  const answers = opt("answers") ? (load(opt("answers")).answers ?? load(opt("answers"))) : null;
238
239
  const facts = factsOf(dir, Object.fromEntries(["raising", "hiring", "contributors"].filter(has).map((k) => [k, true])));
239
240
  const r = score(site, { answers, facts, icp: icpOf(dir, site), venture: dir ? load(join(dir, "venture.json")).id : null });
240
- if (dir) { mkdirSync(join(dir, dirname(PRESENCE)), { recursive: true }); writeFileSync(join(dir, PRESENCE), JSON.stringify(r, null, 2) + "\n"); }
241
+ if (dir) { mkdirSync(join(dir, dirname(PRESENCE)), { recursive: true }); writeFileSync(ventureFile(dir, PRESENCE), JSON.stringify(r, null, 2) + "\n"); }
241
242
  console.log(JSON.stringify(r, null, 2));
242
243
  } else if (a && existsSync(join(a, "venture.json"))) {
243
244
  if (!existsSync(join(a, PRESENCE))) { console.error(`${PRESENCE} does not exist yet: read the site first (node interface/harness/site.mjs <url> --venture <dir>)`); process.exit(1); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naive-framework",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Naive, a venture development framework: conventions, routes, records, checks and an agent interface for building a venture.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { join } from "node:path";
24
+ import { ventureFile } from "../../schema/venture-paths.mjs";
24
25
 
25
26
  export const SANDBOX = "https://rhoapi-sandbox.rho.co/mcp/v1";
26
27
  export const LIVE = "https://rhoapi.rho.co/mcp/v1";
@@ -107,7 +108,7 @@ export function summarize(data, syncedAt) {
107
108
  const json = (f, v) => { mkdirSync(join(f, ".."), { recursive: true }); writeFileSync(f, `${JSON.stringify(v, null, 2)}\n`); };
108
109
 
109
110
  export function write(venture, data, syncedAt = new Date().toISOString()) {
110
- const money = join(venture, "money");
111
+ const money = join(ventureFile(venture, "money"));
111
112
  const pointer = (kind, id) => `${data.source}:${kind}:${id}`;
112
113
  for (const a of data.accounts)
113
114
  json(join(money, `${a.id}.json`), { id: a.id, name: a.account_name, type: a.account_type, last_4: a.account_number_last_4 ?? null, balance: a.balance, source: pointer("account", a.id), synced_at: syncedAt });
@@ -25,6 +25,7 @@ import { CONVENTIONS, MAP, parse } from "./ideamap.mjs";
25
25
  import { GRAPH, ORDER } from "./graph.mjs";
26
26
  import { confirmedIn } from "../../../schema/foundation.mjs";
27
27
  import { loadPackages } from "../../../schema/load-packages.mjs";
28
+ import { ventureFile } from "../../../schema/venture-paths.mjs";
28
29
 
29
30
  const here = dirname(fileURLToPath(import.meta.url));
30
31
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -37,7 +38,7 @@ const HOME = { "venture-core": "foundation", brand: "marketing", ...Object.fromE
37
38
  /** Understand is a gate (EE-069): the founder confirms Mentura read the ideas right, after any
38
39
  * corrections, before they are routed. A person's confirm of the map after its last agent write. */
39
40
  export function understood(dir) {
40
- return existsSync(join(dir, MAP)) && confirmedIn(dir, MAP, "ideas");
41
+ return existsSync(ventureFile(dir, MAP)) && confirmedIn(dir, MAP, "ideas");
41
42
  }
42
43
 
43
44
  export const STAGES = [["routed", "Routed"], ["drafted", "Drafted"], ["yours", "Your decision"], ["waiting", "Waiting"], ["done", "Done"]];
@@ -62,13 +63,13 @@ function written(dir, key) {
62
63
  const rel = m?.paths?.[0]?.replace(/^instance\/\*\*\//, "");
63
64
  if (!rel) return null;
64
65
  if (rel.includes("*")) { const d = join(dir, dirname(rel)); return existsSync(d) && readdirSync(d).some((f) => f.endsWith(".json")); }
65
- return existsSync(join(dir, rel));
66
+ return existsSync(ventureFile(dir, rel));
66
67
  }
67
68
 
68
69
  export function route(dir, { now } = {}) {
69
70
  const venture = load(join(dir, "venture.json"));
70
- const g = existsSync(join(dir, GRAPH)) ? load(join(dir, GRAPH)) : null;
71
- const nodes = g?.nodes ?? (existsSync(join(dir, MAP)) ? parse(readFileSync(join(dir, MAP), "utf8")).ideas.map((i) => ({ ...i, signal: null, lane: null })) : []);
71
+ const g = existsSync(ventureFile(dir, GRAPH)) ? load(ventureFile(dir, GRAPH)) : null;
72
+ const nodes = g?.nodes ?? (existsSync(ventureFile(dir, MAP)) ? parse(readFileSync(ventureFile(dir, MAP), "utf8")).ideas.map((i) => ({ ...i, signal: null, lane: null })) : []);
72
73
  if (!nodes.length) return { venture: venture.id, phase: venture.phase, waiting: "no idea map yet: map the idea dump first (purpose/ideas)" };
73
74
  const lanes = new Map((g?.lanes ?? []).map((l) => [l.id, l]));
74
75
  const m = decisions(dir, now);
@@ -20,6 +20,7 @@
20
20
  import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
21
21
  import { dirname, join } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
+ import { ventureFile } from "../../../schema/venture-paths.mjs";
23
24
 
24
25
  const here = dirname(fileURLToPath(import.meta.url));
25
26
  const ENDPOINT = "https://api.typesafe.ai/v1/systemone";
@@ -39,7 +40,7 @@ function readVenture(path) {
39
40
  }
40
41
  const venture = load(join(path, "venture.json"));
41
42
  const dir = join(path, "products");
42
- const cards = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => load(join(dir, f))) : [];
43
+ const cards = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => load(ventureFile(dir, f))) : [];
43
44
  const products = Object.fromEntries(cards.map((c) => [c.id, c]));
44
45
  const flows = cards.flatMap((c) => (c.flows_to ?? []).map((to) => ({ from: c.id, to })));
45
46
  return { venture, core: venture.core, products, flows };
package/schema/access.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
+ import { ventureFile } from "./venture-paths.mjs";
11
12
 
12
13
  export const PLANS = ["people/team-plan.json", "operations/team-plan.json"];
13
14
 
@@ -16,7 +17,7 @@ export const routeGlob = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g,
16
17
 
17
18
  /** The venture's team plan, or null. Read fresh on every call, so a changed plan is seen at once. */
18
19
  export function teamPlan(dir) {
19
- const f = dir && PLANS.map((p) => join(dir, p)).find((p) => existsSync(p));
20
+ const f = dir && PLANS.map((p) => ventureFile(dir, p)).find((p) => existsSync(p));
20
21
  return f ? { path: f, ...JSON.parse(readFileSync(f, "utf8")) } : null;
21
22
  }
22
23
 
@@ -11,6 +11,7 @@
11
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { catalog, foldersOf } from "./departments.mjs";
14
+ import { ventureFile } from "./venture-paths.mjs";
14
15
 
15
16
  const args = process.argv.slice(2);
16
17
  const group = args.includes("--group");
@@ -28,8 +29,8 @@ const tag = { department: d.id, value_chain: d.value_chain, apqc: d.apqc.id };
28
29
  const made = [];
29
30
  for (const layer of layersOn.filter((l) => !held.includes(l))) {
30
31
  const rel = join(layer, "departments", ...(group ? [d.value_chain] : []), d.id);
31
- if (existsSync(join(dir, rel))) { console.error(`${rel} exists without a department.json; add the tag by hand`); process.exit(1); }
32
- mkdirSync(join(dir, rel), { recursive: true });
32
+ if (existsSync(ventureFile(dir, rel))) { console.error(`${rel} exists without a department.json; add the tag by hand`); process.exit(1); }
33
+ mkdirSync(ventureFile(dir, rel), { recursive: true });
33
34
  writeFileSync(join(dir, rel, "department.json"), JSON.stringify(tag, null, 2) + "\n");
34
35
  const [file, title, what] = layer === "governance"
35
36
  ? ["charter.md", "charter", d.governance] : ["README.md", "operations", d.operations];
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { existsSync, readFileSync } from "node:fs";
28
28
  import { join } from "node:path";
29
+ import { ventureFile } from "./venture-paths.mjs";
29
30
 
30
31
  export const ASSUMPTIONS = [
31
32
  { id: "exists", says: "the problem is real", field: "statement", test: "Find people already paying for or building a workaround (search, forums, competitors' customers)" },
@@ -97,7 +98,7 @@ export const ASK = "Some things the assessment reads are missing. Fill them in f
97
98
 
98
99
  /** The whole run: preflight first, then the assessment unless input is missing and as_is is not set. */
99
100
  export function assess(dir, { as_is = false, today = new Date() } = {}) {
100
- const f = join(dir, "purpose/validation/problem.json");
101
+ const f = join(ventureFile(dir, "purpose/validation/problem.json"));
101
102
  const rec = existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null;
102
103
  const missing = preflight(rec);
103
104
  if (missing.length && !as_is) return { needs_input: { ask: ASK, missing, options: [
@@ -117,7 +118,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
117
118
  const dir = process.argv[2];
118
119
  if (!dir) { console.error("usage: node schema/assess-problem.mjs <venture dir> [--json] [--preflight] [--as-is]"); process.exit(2); }
119
120
  const flag = (f) => process.argv.includes(f);
120
- const out = flag("--preflight") ? { missing: preflight(existsSync(join(dir, "purpose/validation/problem.json")) ? JSON.parse(readFileSync(join(dir, "purpose/validation/problem.json"), "utf8")) : null) } : assess(dir, { as_is: flag("--as-is") });
121
+ const out = flag("--preflight") ? { missing: preflight(existsSync(join(ventureFile(dir, "purpose/validation/problem.json"))) ? JSON.parse(readFileSync(join(ventureFile(dir, "purpose/validation/problem.json")), "utf8")) : null) } : assess(dir, { as_is: flag("--as-is") });
121
122
  if (flag("--json")) { console.log(JSON.stringify(out, null, 2)); process.exit(0); }
122
123
  const list = (m) => m.map((x) => ` - ${x.field}${x.items ? ` (${x.items.join(", ")})` : ""}: ${x.why}. As is: ${x.as_is}`).join("\n");
123
124
  if (out.needs_input) { console.log(`${out.needs_input.ask}\n${list(out.needs_input.missing)}\nFill in: ${out.needs_input.options[0].how}\nRun as is: ${out.needs_input.options[1].how}`); process.exit(0); }
package/schema/assets.mjs CHANGED
@@ -22,6 +22,7 @@ import { createHash } from "node:crypto";
22
22
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
23
23
  import { extname, join } from "node:path";
24
24
  import { entries } from "../interface/provenance.mjs";
25
+ import { ventureFile } from "./venture-paths.mjs";
25
26
 
26
27
  export const REGISTER = "public/assets.json";
27
28
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -124,11 +125,11 @@ export function publicFiles(dir) {
124
125
  return walk(root).map((f) => f.slice(dir.length + 1)).filter((f) => f !== REGISTER).sort();
125
126
  }
126
127
 
127
- export const registerOf = (dir) => existsSync(join(dir, REGISTER)) ? load(join(dir, REGISTER)) : null;
128
+ export const registerOf = (dir) => existsSync(ventureFile(dir, REGISTER)) ? load(ventureFile(dir, REGISTER)) : null;
128
129
 
129
130
  /** The facts code reads from every file in public/. */
130
131
  export function facts(dir) {
131
- return publicFiles(dir).map((path) => ({ path, ...factsOf(readFileSync(join(dir, path))) }));
132
+ return publicFiles(dir).map((path) => ({ path, ...factsOf(readFileSync(ventureFile(dir, path))) }));
132
133
  }
133
134
 
134
135
  /** Which alt texts a person has confirmed, from the provenance log. */
@@ -156,8 +157,8 @@ export function checkAssets(dir) {
156
157
  for (const a of reg.assets ?? []) {
157
158
  if (seen.has(a.path)) errors.push(`${a.path}: listed twice`);
158
159
  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)));
160
+ if (!existsSync(ventureFile(dir, a.path))) { errors.push(`${a.path}: listed in ${REGISTER} but the file is not there`); continue; }
161
+ const real = factsOf(readFileSync(ventureFile(dir, a.path)));
161
162
  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
163
  if (!a.credit || !String(a.credit).trim()) errors.push(`${a.path}: no credit; say who made it (J-4)`);
163
164
  if (real.type !== "unknown" && !extensionFits(a.path, real.type)) errors.push(`${a.path}: the bytes are ${real.type}, the name says otherwise`);
@@ -181,7 +182,7 @@ export function checkAssets(dir) {
181
182
  }
182
183
  }
183
184
  for (const r of reg.remote ?? []) {
184
- const src = join(dir, "raw/sources", r.id, "source.json");
185
+ const src = join(ventureFile(dir, "raw/sources"), r.id, "source.json");
185
186
  if (!existsSync(src)) errors.push(`remote ${r.url}: no note in raw/sources/${r.id}/ (J-3)`);
186
187
  else if (load(src).origin !== r.url) errors.push(`remote ${r.url}: raw/sources/${r.id}/source.json names another origin`);
187
188
  let host = null;
package/schema/brand.mjs CHANGED
@@ -31,11 +31,12 @@
31
31
  import { existsSync, readdirSync, readFileSync } from "node:fs";
32
32
  import { join } from "node:path";
33
33
  import { allowsCopy, factsOf, registerOf, REGISTER } from "./assets.mjs";
34
+ import { ventureFile } from "./venture-paths.mjs";
34
35
 
35
36
  export const BRAND = "marketing/brand.json";
36
37
  export const SITEMAPS = ["marketing/sitemap.json", "operations/marketing/sitemap.json"];
37
38
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
38
- export const brandOf = (dir) => dir && existsSync(join(dir, BRAND)) ? load(join(dir, BRAND)) : null;
39
+ export const brandOf = (dir) => dir && existsSync(ventureFile(dir, BRAND)) ? load(ventureFile(dir, BRAND)) : null;
39
40
 
40
41
  // ---------------------------------------------------------------------------------------------
41
42
  // Router hooks. interface/router.mjs spreads these into its tables; this module never imports the router.
@@ -110,7 +111,7 @@ const url = (path) => `/${path.replace(/^public\//, "")}`;
110
111
  /** The marks as head links: the file-based icons Next.js turns into <link> tags. */
111
112
  export function headLinks(dir, brand) {
112
113
  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 factsFor = (p) => reg?.assets?.find((a) => a.path === p)?.facts ?? (existsSync(ventureFile(dir, p)) ? factsOf(readFileSync(ventureFile(dir, p))) : null);
114
115
  const out = [];
115
116
  for (const m of brand.marks ?? []) {
116
117
  if (m.kind === "logo") continue;
@@ -217,7 +218,7 @@ export function voiceCheck(text, rules) {
217
218
  /** The venture's marketing content files, venture-relative. */
218
219
  export function contentFiles(dir) {
219
220
  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
+ return ["marketing/content", "operations/marketing/content"].flatMap((p) => walk(ventureFile(dir, p))).map((f) => f.slice(dir.length + 1)).sort();
221
222
  }
222
223
 
223
224
  /** Every marketing content file held to the voice rules in force at its path, and to other packages' local terms (warnings). */
@@ -225,7 +226,7 @@ export function voice(dir, brand = brandOf(dir)) {
225
226
  const violations = [], leaks = [];
226
227
  const local = [...glossaries(dir)].filter(([p]) => p !== "marketing").flatMap(([p, g]) => (g.terms ?? []).filter((t) => !t.export).map((t) => ({ ...t, from: p })));
227
228
  for (const f of contentFiles(dir)) {
228
- const text = readFileSync(join(dir, f), "utf8");
229
+ const text = readFileSync(ventureFile(dir, f), "utf8");
229
230
  const route = f.replace(/^operations\//, "").replace(/\.(mdx?|txt)$/, "");
230
231
  if (brand) for (const v of voiceCheck(text, resolveBrand(brand, route).voice.rules)) violations.push({ file: f, ...v });
231
232
  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 });
@@ -258,11 +259,11 @@ export function checkBrand(dir) {
258
259
  if (!(t.files ?? []).length) errors.push(`typeface ${t.id}: self-hosted but names no files in public/ (T-1)`);
259
260
  for (const f of t.files ?? []) {
260
261
  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
+ if (!existsSync(ventureFile(dir, f))) { errors.push(`typeface ${t.id}: ${f} is not there (T-1)`); continue; }
262
263
  const a = asset(f);
263
264
  if (!a) errors.push(`typeface ${t.id}: ${f} has no entry in ${REGISTER} (T-1, J-2)`);
264
265
  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
+ const type = factsOf(readFileSync(ventureFile(dir, f))).type;
266
267
  if (!["woff2", "woff", "ttf", "otf"].includes(type)) errors.push(`typeface ${t.id}: ${f} is ${type}, not a font file (T-1)`);
267
268
  }
268
269
  }
@@ -281,9 +282,9 @@ export function checkBrand(dir) {
281
282
  // Marks (P-24): registered assets of the right type and size.
282
283
  const KINDS = { favicon: ["ico"], icon: ["png", "svg", "ico"], "apple-icon": ["png"], logo: ["png", "svg", "jpeg", "webp"] };
283
284
  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 (!existsSync(ventureFile(dir, m.asset))) { errors.push(`mark ${m.kind}: ${m.asset} is not there (P-24)`); continue; }
285
286
  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
+ const f = factsOf(readFileSync(ventureFile(dir, m.asset)));
287
288
  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
289
  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
290
  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)`);
@@ -311,8 +312,8 @@ export function checkBrand(dir) {
311
312
  const xmlEscape = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
312
313
 
313
314
  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;
315
+ const f = SITEMAPS.find((p) => existsSync(ventureFile(dir, p)));
316
+ return f ? { file: f, record: load(ventureFile(dir, f)) } : null;
316
317
  }
317
318
 
318
319
  /** sitemaps.org XML from a marketing sitemap record: one <url> per planned page, home first. */
@@ -38,6 +38,7 @@ import { createHash } from "node:crypto";
38
38
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
39
39
  import { dirname, join } from "node:path";
40
40
  import { entries as provEntries } from "../interface/provenance.mjs";
41
+ import { ventureFile } from "./venture-paths.mjs";
41
42
 
42
43
  const repo = new URL("..", import.meta.url).pathname;
43
44
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
@@ -127,7 +128,7 @@ export const DEFAULTS = { enabled: true, life: "default", judgment_life: "max",
127
128
 
128
129
  /** The venture's cache settings: the recommended defaults, then naive.config.json's cache, then NAIVE_CACHE. */
129
130
  export function configOf(dir) {
130
- const f = dir && join(dir, "naive.config.json");
131
+ const f = dir && join(ventureFile(dir, "naive.config.json"));
131
132
  const own = f && existsSync(f) ? load(f).cache ?? {} : {};
132
133
  const off = /^(0|off|false|no)$/i.test(process.env.NAIVE_CACHE ?? "");
133
134
  return { ...DEFAULTS, ...own, routes: { ...(own.routes ?? {}) }, enabled: !off && own.enabled !== false };
@@ -372,7 +373,7 @@ function walkEntries(root) {
372
373
 
373
374
  /** The entries one actor may see: the shared ones and their own private ones, never another person's (C-7). */
374
375
  export function entries(dir, { identity = null, level } = {}) {
375
- const base = join(dir, CACHE_DIR);
376
+ const base = ventureFile(dir, CACHE_DIR);
376
377
  const roots = [join(base, "shared"), ...(identity ? [join(base, scopeOf(true, identity))] : [])];
377
378
  return roots.flatMap(walkEntries).flatMap((f) => { try { const e = load(f); return [{ file: f.slice(dir.length + 1), bytes: statSync(f).size, ...e }]; } catch { return []; } })
378
379
  .filter((e) => !level || e.level === level);
@@ -392,7 +393,7 @@ export function stats(dir, { identity = null, now = Date.now() } = {}) {
392
393
  /** Removes expired entries in every scope. Returns how many. */
393
394
  export function prune(dir, { now = Date.now() } = {}) {
394
395
  let n = 0;
395
- for (const f of walkEntries(join(dir, CACHE_DIR))) {
396
+ for (const f of walkEntries(ventureFile(dir, CACHE_DIR))) {
396
397
  try { const e = load(f); if (e.expires_at && Date.parse(e.expires_at) <= now) { rmSync(f); n++; } } catch { rmSync(f, { force: true }); n++; }
397
398
  }
398
399
  return n;
@@ -401,7 +402,7 @@ export function prune(dir, { now = Date.now() } = {}) {
401
402
  /** Removes entries, all or one level, in every scope. Returns how many. */
402
403
  export function clear(dir, { level } = {}) {
403
404
  let n = 0;
404
- for (const f of walkEntries(join(dir, CACHE_DIR))) {
405
+ for (const f of walkEntries(ventureFile(dir, CACHE_DIR))) {
405
406
  let e = null;
406
407
  try { e = load(f); } catch {}
407
408
  if (!level || e?.level === level) { rmSync(f, { force: true }); n++; }
@@ -23,13 +23,14 @@ import { evaluate, patternRe, rulesFor } from "./gate.mjs";
23
23
  import { teamPlan } from "./access.mjs";
24
24
  import { gateOf, repo, resolve, routes } from "../interface/router.mjs";
25
25
  import { agentFromEnv, record } from "../interface/provenance.mjs";
26
+ import { ventureFile } from "./venture-paths.mjs";
26
27
 
27
28
  const FRAMEWORKS = ["idea-map"];
28
29
 
29
30
  /** Problems with a venture's naive.config.json: { errors, warnings }. */
30
31
  export function check(dir) {
31
32
  const errors = [], warnings = [];
32
- const f = join(dir, CONFIG);
33
+ const f = ventureFile(dir, CONFIG);
33
34
  if (!existsSync(f)) return { errors, warnings, note: `no ${CONFIG}: every default applies` };
34
35
  let raw;
35
36
  try { raw = rawConfig(dir); } catch (e) { return { errors: [e.message], warnings }; }
@@ -66,7 +67,7 @@ export function explain(dir, route, { actor = "agent", role, confidence } = {})
66
67
  const concrete = r.collection === "item" ? r.route.replace("[id]", params.id) : r.route;
67
68
  const cls = gateOf(r, dir);
68
69
  const files = (r.collection === "item" ? r.files.map((x) => x.replace("*", params.id)) : r.files);
69
- const exists = files.some((x) => existsSync(join(dir, x)));
70
+ const exists = files.some((x) => existsSync(ventureFile(dir, x)));
70
71
  const verdict = evaluate({ config: configFor(dir), dir, route: concrete, table: r.route, record: r.record, actor, role, confidence, exists, decisionOwner: cls?.owner ?? undefined });
71
72
  return { route: concrete, record: r.record, actor, role: role ?? null, confidence: confidence ?? null, decision_class: cls ? { class: cls.class, level: cls.level, drafts: cls.drafts && actor !== "person" } : null, gate: verdict };
72
73
  }
@@ -78,7 +79,7 @@ export function pin(dir, names) {
78
79
  const unknown = chosen.filter((n) => !have[n]);
79
80
  if (unknown.length) throw new Error(`not installed: ${unknown.join(", ")}`);
80
81
  raw.packages = { ...(raw.packages ?? {}), ...Object.fromEntries(chosen.map((n) => [n, have[n]])) };
81
- writeFileSync(join(dir, CONFIG), JSON.stringify(raw, null, 2) + "\n");
82
+ writeFileSync(ventureFile(dir, CONFIG), JSON.stringify(raw, null, 2) + "\n");
82
83
  record(dir, { entity: CONFIG, route: "system/naive-config", activity: "write", agent: agentFromEnv("person", "cli"), note: `pinned ${chosen.join(", ")} at the installed versions` });
83
84
  return raw.packages;
84
85
  }
@@ -17,6 +17,7 @@
17
17
  import { spawnSync } from "node:child_process";
18
18
  import { existsSync, readFileSync, readdirSync } from "node:fs";
19
19
  import { join, relative } from "node:path";
20
+ import { ventureFile } from "./venture-paths.mjs";
20
21
 
21
22
  // EE_ROOT points the check at another copy of the repository (used by schema/test.mjs).
22
23
  const root = process.env.EE_ROOT?.replace(/\/?$/, "/") ?? new URL("..", import.meta.url).pathname;
@@ -26,7 +27,7 @@ const errors = [];
26
27
  const warnings = [];
27
28
 
28
29
  const files = (dir) => readdirSync(join(root, dir), { withFileTypes: true }).flatMap((e) =>
29
- [".git", "node_modules", ".naive"].includes(e.name) ? [] : e.isDirectory() ? files(join(dir, e.name)) : [join(dir, e.name)]);
30
+ [".git", "node_modules", ".naive"].includes(e.name) ? [] : e.isDirectory() ? files(ventureFile(dir, e.name)) : [ventureFile(dir, e.name)]);
30
31
  // The files git would track (so build output and installed packages under .gitignore are left out); a
31
32
  // copy outside git, as the tests use, is walked instead.
32
33
  const tracked = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: root, encoding: "utf8" });
@@ -10,6 +10,7 @@
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
12
  import { join } from "node:path";
13
+ import { ventureFile } from "./venture-paths.mjs";
13
14
 
14
15
  const dir = process.argv[2];
15
16
  if (!dir) { console.error("usage: node schema/check-data-packages.mjs <workspace or venture dir>"); process.exit(2); }
@@ -32,10 +33,10 @@ for (const v of walk(dir)) {
32
33
  const id = load(join(v, "venture.json")).id;
33
34
  const git = inGit(v);
34
35
  for (const pack of PACKS) {
35
- const base = join(v, pack);
36
+ const base = ventureFile(v, pack);
36
37
  if (!existsSync(base)) continue;
37
38
  for (const name of readdirSync(base)) {
38
- if (name.startsWith("_") || name.startsWith(".")) continue;
39
+ if (name.startsWith("_") || name.startsWith(".") || name === "README.md" || name === "AGENTS.md") continue;
39
40
  const p = join(base, name);
40
41
  if (!statSync(p).isFile() || !name.endsWith(".json")) errors.push(`${id}/${pack}/${name}: a master record is <id>.json`);
41
42
  }
@@ -56,7 +57,7 @@ for (const v of walk(dir)) {
56
57
  }
57
58
  }
58
59
  }
59
- const sources = join(v, "raw/sources");
60
+ const sources = join(ventureFile(v, "raw/sources"));
60
61
  if (existsSync(sources)) for (const s of readdirSync(sources)) {
61
62
  const sd = join(sources, s);
62
63
  if (!statSync(sd).isDirectory() || s.startsWith(".")) continue;
@@ -64,9 +65,9 @@ for (const v of walk(dir)) {
64
65
  if (!existsSync(join(sd, "text.md"))) errors.push(`${id}/raw/sources/${s}: missing text.md`);
65
66
  for (const f of readdirSync(sd)) if (f.startsWith("original.") && (!git || tracked(v, join("raw/sources", s, f)))) errors.push(`${id}/raw/sources/${s}/${f}: original.* stays out of git`);
66
67
  }
67
- const map = ["ideas/map.mdx", "purpose/ideas/map.mdx"].map((f) => join(v, f)).find((p) => existsSync(p));
68
+ const map = ["ideas/map.mdx", "purpose/ideas/map.mdx"].map((f) => ventureFile(v, f)).find((p) => existsSync(p));
68
69
  const rawIds = new Set();
69
- const dump = join(v, "raw/dump");
70
+ const dump = join(ventureFile(v, "raw/dump"));
70
71
  if (existsSync(dump)) for (const f of readdirSync(dump)) if (f !== ".gitkeep") rawIds.add(f.replace(/\.[^.]+$/, ""));
71
72
  if (existsSync(sources)) for (const s of readdirSync(sources)) if (!s.startsWith(".")) rawIds.add(s);
72
73
  if (map && rawIds.size) {
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { existsSync, readFileSync, readdirSync } from "node:fs";
15
15
  import { dirname, join, resolve } from "node:path";
16
+ import { ventureFile } from "./venture-paths.mjs";
16
17
 
17
18
  const dir = process.argv[2];
18
19
  if (!dir) { console.error("usage: node schema/check-execution.mjs <venture dir>"); process.exit(2); }
@@ -27,7 +28,7 @@ const template = load(join(root, "governance/venture-template/decision-rights.js
27
28
  const errors = [];
28
29
 
29
30
  const classes = new Map(template.classes.map((c) => [c.id, c]));
30
- const own = join(dir, "governance/decision-rights.json");
31
+ const own = join(ventureFile(dir, "governance/decision-rights.json"));
31
32
  if (existsSync(own)) for (const c of load(own).classes) {
32
33
  const t = classes.get(c.id);
33
34
  if (t && RANK[c.level] < RANK[t.level] && !c.decision) errors.push(`decision-rights/${c.id}: loosens ${t.level} to ${c.level} without naming a decision`);
@@ -36,14 +37,14 @@ if (existsSync(own)) for (const c of load(own).classes) {
36
37
  for (const c of classes.values()) if (!roles.has(c.owner)) errors.push(`decision-rights/${c.id}: owner ${c.owner} is not a role`);
37
38
 
38
39
  const results = new Map();
39
- for (const f of jsonIn(join(dir, "evidence/results"))) {
40
+ for (const f of jsonIn(join(ventureFile(dir, "evidence/results")))) {
40
41
  const e = load(f);
41
42
  if (e.passed !== e.results.every((r) => r.passed)) errors.push(`evidence/${e.id}: passed says ${e.passed} but its results disagree`);
42
43
  results.set(resolve(f), e);
43
44
  }
44
45
 
45
46
  let count = 0;
46
- for (const f of jsonIn(join(dir, "operations/handoffs"))) {
47
+ for (const f of jsonIn(join(ventureFile(dir, "operations/handoffs")))) {
47
48
  const h = load(f), at = `handoff/${h.id}`;
48
49
  count++;
49
50
  if (h.venture !== venture.id) errors.push(`${at}: venture ${h.venture} is not ${venture.id}`);
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readdirSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { roles as ROLES, types as TYPES } from "./venture-type.mjs";
12
+ import { ventureFile } from "./venture-paths.mjs";
12
13
 
13
14
  const ws = process.argv[2];
14
15
  if (!ws || !existsSync(ws)) { console.error("usage: node schema/check-people.mjs <workspace>"); process.exit(2); }
@@ -26,28 +27,41 @@ for (const dir of walk(ws)) {
26
27
  const at = (m) => errors.push(`${v.id}: ${m}`);
27
28
  const type = v.type ? types.get(v.type) : null;
28
29
  if (v.type && !type) at(`type ${v.type} is not in schema/venture-types.json`);
29
- const rosterFile = join(dir, "people/people.json");
30
- const fdir = join(dir, "people/founders");
30
+ const rosterFile = join(ventureFile(dir, "people/people.json"));
31
+ const fdir = join(ventureFile(dir, "people/founders"));
31
32
  const files = existsSync(fdir) ? readdirSync(fdir).filter((f) => f.endsWith(".json")) : [];
32
33
  if (!existsSync(rosterFile)) { if (files.length) at("people/founders/ has files but there is no people/people.json"); continue; }
33
34
  rosters++;
34
35
  const roster = load(rosterFile);
35
- const imported = new Set(v.departments ?? []);
36
+ // In the structured layout (EE-086) the org chart is venture.json's functions, and a person's hats are the
37
+ // functions they own there; in the flat layout, the departments venture.json imports, worn as hats in people/.
38
+ const structured = v.layout === "structured";
39
+ const imported = new Set(structured ? Object.keys(v.functions ?? {}) : v.departments ?? []);
40
+ const ids = new Set(roster.people.map((p) => p.id));
41
+ if (structured) {
42
+ for (const [fn, f] of Object.entries(v.functions ?? {})) {
43
+ if (f.owner && !ids.has(f.owner)) at(`venture.json says ${f.owner} owns ${fn}, but people/people.json has no ${f.owner}`);
44
+ for (const c of f.contributors ?? []) if (!ids.has(c) && !roles.has(c)) at(`${fn} names contributor ${c}, who is neither a person in people/ nor a role`);
45
+ }
46
+ for (const p of roster.people) p.hats = Object.entries(v.functions ?? {}).filter(([, f]) => f.owner === p.id).map(([fn]) => fn);
47
+ }
36
48
  for (const p of roster.people) {
37
49
  const role = roles.get(p.role);
38
50
  if (!role) { at(`${p.id} has role ${p.role}, which is not in governance/venture-template/roles/venture-roles.json`); continue; }
39
51
  if (type && !type.roles.includes(p.role)) at(`${p.id} is a ${p.role}, which a ${type.label.toLowerCase()} does not have (${type.roles.join(", ")})`);
40
- for (const h of p.hats) if (imported.size && !imported.has(h)) at(`${p.id} wears the ${h} hat, but venture.json does not import ${h}`);
52
+ for (const h of p.hats ?? []) if (imported.size && !imported.has(h)) at(`${p.id} wears the ${h} hat, but venture.json does not import ${h}`);
53
+ p.hats ??= [];
41
54
  if (!role.owns_when_solo && p.hats.length) at(`${p.id} is a ${p.role}, who owns no function, but wears ${p.hats.join(", ")}`);
42
55
  if (role.founder) {
43
56
  const f = `people/founders/${p.id}.json`;
44
57
  if (p.founder_file !== f) at(`${p.id} is a founder, so founder_file is ${f}`);
45
- else if (!existsSync(join(dir, f))) at(`${f} is missing`);
58
+ else if (!existsSync(ventureFile(dir, f))) at(`${f} is missing`);
46
59
  else {
47
- const rec = load(join(dir, f));
60
+ const rec = load(ventureFile(dir, f));
48
61
  if (rec.id !== p.id || rec.role !== p.role) at(`${f} says ${rec.id} is a ${rec.role}; the roster says ${p.id} is a ${p.role}`);
49
- if (JSON.stringify([...rec.hats].sort()) !== JSON.stringify([...p.hats].sort())) at(`${f} and the roster disagree on ${p.id}'s hats`);
50
- if (rec.agreement && !existsSync(join(dir, rec.agreement.path))) at(`${f} links a founder agreement at ${rec.agreement.path}, which does not exist`);
62
+ if (!structured && JSON.stringify([...(rec.hats ?? [])].sort()) !== JSON.stringify([...p.hats].sort())) at(`${f} and the roster disagree on ${p.id}'s hats`);
63
+ if (structured && rec.hats) at(`${f} stores hats; in the structured layout they are read from venture.json`);
64
+ if (rec.agreement && !existsSync(ventureFile(dir, rec.agreement.path))) at(`${f} links a founder agreement at ${rec.agreement.path}, which does not exist`);
51
65
  }
52
66
  }
53
67
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
9
  import { join } from "node:path";
10
+ import { ventureFile } from "./venture-paths.mjs";
10
11
 
11
12
  const dir = process.argv[2];
12
13
  if (!dir) { console.error("usage: node schema/check-private.mjs <venture dir>"); process.exit(2); }
@@ -49,7 +50,7 @@ for (const { package: name, input } of inputs) {
49
50
  if (privateTargets.has(input) || privateRecords.has(input.split(".")[0])) errors.push(`${name}: use_model input ${input} is private and must not reach a model`);
50
51
  }
51
52
  if (readsRelease) {
52
- const release = join(dir, ".release");
53
+ const release = join(ventureFile(dir, ".release"));
53
54
  if (existsSync(release) && statSync(release).isDirectory()) {
54
55
  for (const f of walk(release).filter((p) => p.endsWith(".json"))) {
55
56
  const text = readFileSync(f, "utf8");