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
@@ -13,19 +13,20 @@
13
13
  import { execSync } from "node:child_process";
14
14
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
+ import { ventureFile } from "./venture-paths.mjs";
16
17
 
17
18
  const [dir, id] = process.argv.slice(2);
18
19
  if (!dir || !id) { console.error("usage: node schema/check-ready.mjs <venture dir> <handoff id> [--record --by <who>]"); process.exit(2); }
19
20
  const opt = (k) => process.argv.includes(k) ? process.argv[process.argv.indexOf(k) + 1] : undefined;
20
21
  const root = new URL("..", import.meta.url).pathname;
21
22
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
22
- const hPath = join(dir, "operations/handoffs", `${id}.json`);
23
+ const hPath = join(ventureFile(dir, "operations/handoffs"), `${id}.json`);
23
24
  const h = load(hPath);
24
25
  const items = [];
25
26
  const item = (name, ok, why = "") => items.push({ name, ok, why });
26
27
 
27
28
  // 1. The brief says what done looks like and what to do, with no placeholders left.
28
- const bPath = join(dir, "operations/handoffs", h.brief);
29
+ const bPath = join(ventureFile(dir, "operations/handoffs"), h.brief);
29
30
  const brief = existsSync(bPath) ? readFileSync(bPath, "utf8") : "";
30
31
  item("brief exists", !!brief, bPath);
31
32
  const section = (name) => { const m = brief.match(new RegExp(`^##\\s+(?:\\d+\\.\\s*)?${name}\\b[^\\n]*\\n([\\s\\S]*?)(?=^##\\s|$(?![\\s\\S]))`, "mi")); return m ? m[1].trim() : null; };
@@ -34,10 +35,10 @@ const holes = brief.match(/\b(TBD|TODO|FIXME)\b|\?\?\?|<(?:title|role|entity|out
34
35
  item("brief has no placeholders left", !holes.length, holes.slice(0, 3).join(", "));
35
36
 
36
37
  // 2. Everything it names exists.
37
- const products = new Set(["products", "product"].flatMap((folder) => existsSync(join(dir, folder)) ? readdirSync(join(dir, folder)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)) : []));
38
+ const products = new Set(["products", "product"].flatMap((folder) => existsSync(ventureFile(dir, folder)) ? readdirSync(ventureFile(dir, folder)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)) : []));
38
39
  const missingProducts = (h.products ?? []).filter((p) => !products.has(p));
39
40
  item("every product it serves exists", !missingProducts.length, missingProducts.join(", "));
40
- const logs = [join(root, "docs/decisions/decisions.json"), join(dir, "knowledge/decisions.json")].filter(existsSync).flatMap((f) => load(f).decisions);
41
+ const logs = [join(root, "docs/decisions/decisions.json"), join(ventureFile(dir, "knowledge/decisions.json"))].filter(existsSync).flatMap((f) => load(f).decisions);
41
42
  const decisions = new Map(logs.map((d) => [d.id, d]));
42
43
  const badDecisions = (h.governed_by ?? []).filter((d) => !decisions.has(d) || ["withdrawn", "superseded"].includes(decisions.get(d).status));
43
44
  item("every governing decision exists and is in force or proposed", !badDecisions.length, badDecisions.join(", "));
@@ -49,7 +50,7 @@ item("every input it needs exists", !missingInputs.length, missingInputs.join(",
49
50
  // 3. The holder can act inside its rights.
50
51
  const roles = new Set(load(join(root, "governance/venture-template/roles/agent-roles.json")).roles.map((r) => r.id));
51
52
  const classes = new Map(load(join(root, "governance/venture-template/decision-rights.json")).classes.map((c) => [c.id, c]));
52
- const own = join(dir, "governance/decision-rights.json");
53
+ const own = join(ventureFile(dir, "governance/decision-rights.json"));
53
54
  if (existsSync(own)) for (const c of load(own).classes) classes.set(c.id, { ...classes.get(c.id), ...c });
54
55
  item(`role ${h.role} exists`, roles.has(h.role));
55
56
  const foreign = h.decision_classes.filter((c) => classes.get(c)?.owner !== h.role);
@@ -11,6 +11,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { LOCAL, MAP, registry } from "../knowledge/connectors/index.mjs";
13
13
  import { ANY, CONTEXT, specificity } from "../knowledge/connectors/resolve.mjs";
14
+ import { ventureFile } from "./venture-paths.mjs";
14
15
 
15
16
  const dir = process.argv[2];
16
17
  if (!dir) { console.error("usage: node schema/check-sources.mjs <workspace or venture dir>"); process.exit(2); }
@@ -27,7 +28,7 @@ const errors = [];
27
28
  let maps = 0;
28
29
 
29
30
  for (const v of walk(dir)) {
30
- const f = join(v, MAP);
31
+ const f = ventureFile(v, MAP);
31
32
  if (!existsSync(f)) continue;
32
33
  maps++;
33
34
  const id = JSON.parse(readFileSync(join(v, "venture.json"), "utf8")).id;
@@ -10,6 +10,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { connect, sourceFor, sourcesOf } from "../knowledge/connectors/index.mjs";
12
12
  import { resolve } from "../interface/router.mjs";
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-where-used.mjs <workspace or venture dir> [--json]"); process.exit(2); }
@@ -21,18 +22,18 @@ const at = (obj, pointer) => pointer.split("/").slice(1).map((k) => k.replace(/~
21
22
  const errors = [], drift = [], unchecked = [];
22
23
  let lists = 0, checked = 0;
23
24
  for (const v of walk(dir)) {
24
- if (!existsSync(join(v, FILE))) continue;
25
+ if (!existsSync(ventureFile(v, FILE))) continue;
25
26
  lists++;
26
27
  const id = JSON.parse(readFileSync(join(v, "venture.json"), "utf8")).id;
27
28
  const ids = new Map(sourcesOf(v).sources.map((s) => [s.id, s]));
28
- for (const fact of JSON.parse(readFileSync(join(v, FILE), "utf8")).facts) {
29
+ for (const fact of JSON.parse(readFileSync(ventureFile(v, FILE), "utf8")).facts) {
29
30
  const where = `${id}: ${fact.id}`;
30
31
  const { r, params } = resolve(fact.parent.route);
31
32
  if (!r || r.kind !== "record" || r.collection === "list") { errors.push(`${where}: parent ${fact.parent.route} is not a record route`); continue; }
32
33
  const context = r.collection === "item" && (r.record === "product" || r.record === "account") ? { [r.record]: params.id } : {};
33
34
  const src = sourceFor(v, r.record, context);
34
35
  const cands = (r.collection === "item" ? r.files.map((f) => f.replace("*", params.id)) : r.files).filter((f) => !f.includes("*"));
35
- const path = cands.find((f) => existsSync(join(v, f))) ?? cands[0];
36
+ const path = cands.find((f) => existsSync(ventureFile(v, f))) ?? cands[0];
36
37
  if (src.connector !== "filesystem") { unchecked.push(`${where}: parent is in ${src.id}`); continue; }
37
38
  const parent = await (await connect(src, v)).read(path);
38
39
  const value = parent && at(JSON.parse(parent.content), fact.parent.pointer);
@@ -19,6 +19,7 @@ import { readFileSync } from "node:fs";
19
19
  import { join } from "node:path";
20
20
  import { decisions } from "./decisions.mjs";
21
21
  import { HOME, PACKAGES, TITLES } from "./packages.mjs";
22
+ import { ventureFile } from "./venture-paths.mjs";
22
23
 
23
24
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
24
25
  const titleOf = (t) => ({ icp: "ICP", "venture-core": "Core product" })[t] ?? TITLES[t] ?? t.replace(/^validation-/, "").replace(/-/g, " ").replace(/^./, (c) => c.toUpperCase());
@@ -30,7 +31,7 @@ const OPEN = new Set(["now", "overdue", "waiting", "stale", "later"]);
30
31
  export function analytics(dir, now) {
31
32
  const r = decisions(dir, now);
32
33
  const rows = r.decisions.filter((d) => d.source !== "root").map((d) => {
33
- const raw = d.file ? load(join(dir, d.file)) : {};
34
+ const raw = d.file ? load(ventureFile(dir, d.file)) : {};
34
35
  const record = d.decides?.record ?? null;
35
36
  // A foundation statement belongs to the strategic foundation whatever record holds it; so does a
36
37
  // decision about venture.json itself (the core product, the team).
@@ -32,6 +32,7 @@ import { agentFromEnv, record as prov } from "../interface/provenance.mjs";
32
32
  import { glob as routeFileGlob, routes } from "../interface/router.mjs";
33
33
  // Caching hook (F-12): the decision list is computed once per router action (schema/caching.mjs).
34
34
  import { memo } from "./caching.mjs";
35
+ import { layoutOf, logicalFiles, ventureFile } from "./venture-paths.mjs";
35
36
 
36
37
  const repo = new URL("..", import.meta.url).pathname;
37
38
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
@@ -47,7 +48,9 @@ export function recordOf(file) {
47
48
 
48
49
  // Every *.decision.json in a venture, outside drafts, hidden folders and nested ventures.
49
50
  function decisionFiles(dir, sub = "") {
50
- return readdirSync(join(dir, sub), { withFileTypes: true }).flatMap((e) => {
51
+ // A structured venture (EE-086): its decision files by their logical paths.
52
+ if (!sub && layoutOf(dir) === "structured") return logicalFiles(dir).filter((r) => r.endsWith(".decision.json") && r.split("/").at(-2) === "decisions" && !r.split("/").some((s) => s.startsWith(".") || s.startsWith("_")));
53
+ return readdirSync(ventureFile(dir, sub), { withFileTypes: true }).flatMap((e) => {
51
54
  const rel = sub ? `${sub}/${e.name}` : e.name;
52
55
  if (e.isDirectory()) return e.name.startsWith(".") || e.name.startsWith("_") || existsSync(join(dir, rel, "venture.json")) ? [] : decisionFiles(dir, rel);
53
56
  return e.name.endsWith(".decision.json") && sub.split("/").at(-1) === "decisions" ? [rel] : [];
@@ -74,7 +77,7 @@ function decisionsAt(dir, now = Date.parse(process.env.DECISIONS_NOW ?? "") || D
74
77
  status: settled ? "decided" : "open", decided_at: null, needed_in: venture.phase },
75
78
  ];
76
79
  for (const f of decisionFiles(dir)) {
77
- const d = load(join(dir, f));
80
+ const d = load(ventureFile(dir, f));
78
81
  // decisions/<record>[.<topic>].decision.json decides <record>.json in the folder that holds decisions/.
79
82
  const at = f.split("/").slice(0, -2).join("/"), base = f.split("/").at(-1).split(".")[0];
80
83
  const path = d.decides?.path ?? (at ? `${at}/${base}.json` : `${base}.json`);
@@ -107,7 +110,7 @@ function decisionsAt(dir, now = Date.parse(process.env.DECISIONS_NOW ?? "") || D
107
110
  }
108
111
  const statementFor = (record) => list.find((x) => x.part_of === "foundation" && x.status === "open" && x.decides?.record === record);
109
112
  // An agent's draft of a gated record waits for a person to accept it.
110
- const drafts = existsSync(join(dir, "_drafts")) ? readdirSync(join(dir, "_drafts")).filter((f) => f.endsWith(".draft")) : [];
113
+ const drafts = existsSync(join(ventureFile(dir, "_drafts"))) ? readdirSync(join(ventureFile(dir, "_drafts"))).filter((f) => f.endsWith(".draft")) : [];
111
114
  for (const d of drafts) {
112
115
  const path = d.replace(/\.draft$/, "").replace(/__/g, "/");
113
116
  const record = recordOf(path);
@@ -240,7 +243,7 @@ const routeGlob = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g, "\\$&")
240
243
  /** The decision that keeps a route closed to an actor (EE-058): an undecided decision that unlocks it. */
241
244
  export function locked(dir, route, actorId) {
242
245
  if (!existsSync(join(dir, "venture.json"))) return null;
243
- const files = decisionFiles(dir).map((f) => load(join(dir, f)));
246
+ const files = decisionFiles(dir).map((f) => load(ventureFile(dir, f)));
244
247
  const d = files.find((x) => x.status !== "decided" && x.status !== "withdrawn" && (x.unlocks ?? []).some((u) =>
245
248
  (u.routes ?? []).some((g) => routeGlob(g).test(route)) && (!u.for?.length || !actorId || u.for.includes(actorId))));
246
249
  return d ? { id: d.id, question: d.question } : null;
@@ -254,7 +257,7 @@ const validate = (d) => {
254
257
  rmSync(scratch, { recursive: true, force: true });
255
258
  return v.status === 0 ? null : (v.stdout + v.stderr).trim();
256
259
  };
257
- const fileOf = (dir, id) => decisionFiles(dir).find((f) => load(join(dir, f)).id === id);
260
+ const fileOf = (dir, id) => decisionFiles(dir).find((f) => load(ventureFile(dir, f)).id === id);
258
261
 
259
262
  /** An agent proposes a decision, or new wording for an open one. It stays open until a person
260
263
  * approves it: an agent never decides (EE-058). at: the folder beside the record it decides. */
@@ -263,16 +266,16 @@ export function propose(dir, input, { agentId, via = "router", harness, model }
263
266
  if (!rest.id || !rest.question) return { ok: false, error: "a decision needs an id and a question" };
264
267
  if (rest.status && rest.status !== "open") return { ok: false, error: "an agent may only propose an open decision; a person approves it" };
265
268
  const existing = fileOf(dir, rest.id);
266
- if (existing && load(join(dir, existing)).status === "decided") return { ok: false, error: `${rest.id} is decided; only a person can change it` };
269
+ if (existing && load(ventureFile(dir, existing)).status === "decided") return { ok: false, error: `${rest.id} is decided; only a person can change it` };
267
270
  const file = existing ?? `${at ? `${at}/` : ""}decisions/${name ?? rest.id}.decision.json`;
268
271
  if (!existing && !/^([a-z0-9-]+\/)*decisions\/[a-z0-9.-]+\.decision\.json$/.test(file)) return { ok: false, error: `${file} is not a decisions/ path` };
269
272
  // New wording for an open decision keeps what the agent did not restate: its parent, boundary, unlocks.
270
- const before = existing ? load(join(dir, existing)) : {};
273
+ const before = existing ? load(ventureFile(dir, existing)) : {};
271
274
  const d = { ...before, ...rest, status: "open", provenance: { drafted_by: { kind: "agent", ...(agentId ? { id: agentId } : {}) } } };
272
275
  const bad = validate(d);
273
276
  if (bad) return { ok: false, error: `does not match schema/decision-record.schema.json:\n${bad}` };
274
- mkdirSync(dirname(join(dir, file)), { recursive: true });
275
- writeFileSync(join(dir, file), JSON.stringify(d, null, 2) + "\n");
277
+ mkdirSync(dirname(ventureFile(dir, file)), { recursive: true });
278
+ writeFileSync(ventureFile(dir, file), JSON.stringify(d, null, 2) + "\n");
276
279
  const e = prov(dir, { entity: file, route: "governance/decisions", activity: "draft", agent: agentFromEnv("agent", via, { ...(agentId ? { id: agentId } : {}), ...(harness ? { harness } : {}), ...(model ? { model } : {}) }), note: `proposed decision ${d.id}` });
277
280
  return { ok: true, result: { proposed: d.id, file, awaiting: "approval", provenance: e.hash } };
278
281
  }
@@ -282,7 +285,7 @@ export function approve(dir, id, { by, answer, via = "cli", note } = {}) {
282
285
  const file = fileOf(dir, id);
283
286
  if (!file) return { ok: false, error: `no decision ${id}` };
284
287
  if (!by) return { ok: false, error: "approving needs the person's name (--by)" };
285
- const d = load(join(dir, file));
288
+ const d = load(ventureFile(dir, file));
286
289
  if (d.status === "decided") return { ok: false, error: `${id} is already decided` };
287
290
  if (!answer && !d.answer) return { ok: false, error: `${id} has no wording to approve; give an answer` };
288
291
  const drafted = d.provenance?.drafted_by ?? { kind: "person", id: by };
@@ -291,7 +294,7 @@ export function approve(dir, id, { by, answer, via = "cli", note } = {}) {
291
294
  const out = { ...d, status: "decided", answer: answer ?? d.answer, provenance: { ...d.provenance, drafted_by: drafted, approved_by: by, approved_at: new Date().toISOString().replace(/\.\d+Z$/, "Z"), phase, wording, via } };
292
295
  const bad = validate(out);
293
296
  if (bad) return { ok: false, error: bad };
294
- writeFileSync(join(dir, file), JSON.stringify(out, null, 2) + "\n");
297
+ writeFileSync(ventureFile(dir, file), JSON.stringify(out, null, 2) + "\n");
295
298
  const e = prov(dir, { entity: file, route: "governance/decisions", activity: "accept", agent: agentFromEnv("person", via, { id: by }), note: note ?? `approved decision ${id} (${wording})` });
296
299
  return { ok: true, result: { decided: id, wording, phase, provenance: e.hash, unlocks: (out.unlocks ?? []).map((u) => u.activity) } };
297
300
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { existsSync, readdirSync, readFileSync } from "node:fs";
17
17
  import { join } from "node:path";
18
+ import { ventureFile } from "./venture-paths.mjs";
18
19
 
19
20
  const repo = new URL("..", import.meta.url).pathname;
20
21
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
@@ -30,15 +31,15 @@ export function filesOf(dir, type) {
30
31
  const gs = (r?.paths ?? []).filter((p) => p.startsWith("instance/**/")).map((p) => p.slice("instance/**/".length));
31
32
  const found = [];
32
33
  for (const g of gs) {
33
- if (!g.includes("*")) { if (existsSync(join(dir, g))) found.push(g); continue; }
34
+ if (!g.includes("*")) { if (existsSync(ventureFile(dir, g))) found.push(g); continue; }
34
35
  const star = g.indexOf("*");
35
36
  const folder = g.slice(0, star), rest = g.slice(star + 1);
36
37
  if (rest.includes("*")) continue;
37
- const d = join(dir, folder);
38
+ const d = ventureFile(dir, folder);
38
39
  if (existsSync(d)) for (const f of readdirSync(d)) if (f.endsWith(rest)) found.push(folder + f);
39
40
  }
40
41
  if (found.length) return found;
41
- const drafted = gs.filter((g) => !g.includes("*") && existsSync(join(dir, `_drafts/${g.replace(/\//g, "__")}.draft`)));
42
+ const drafted = gs.filter((g) => !g.includes("*") && existsSync(ventureFile(dir, `_drafts/${g.replace(/\//g, "__")}.draft`)));
42
43
  return drafted.length ? drafted : gs.slice(0, 1);
43
44
  }
44
45
 
@@ -47,7 +48,7 @@ export function filesOf(dir, type) {
47
48
  export function confirmedIn(dir, file, id, seen = new Set()) {
48
49
  if (seen.has(file)) return false;
49
50
  seen.add(file);
50
- const log = join(dir, "provenance.jsonl");
51
+ const log = join(ventureFile(dir, "provenance.jsonl"));
51
52
  if (!existsSync(log)) return false;
52
53
  const draft = `_drafts/${file.replace(/\//g, "__")}.draft`;
53
54
  const es = readFileSync(log, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => e.entity === file || e.entity === draft);
@@ -64,8 +65,8 @@ export function foundation(dir) {
64
65
  if (!s.record) return { id: s.id, status: "planned", note: s.note ?? null };
65
66
  const files = filesOf(dir, s.record);
66
67
  // A retired product no longer answers for the venture.
67
- const values = files.filter((f) => existsSync(join(dir, f)) && load(join(dir, f)).status !== "retired").map((f) => ({ f, v: get(load(join(dir, f)), s.attribute) })).filter((x) => filled(x.v));
68
- const drafts = files.filter((f) => existsSync(join(dir, `_drafts/${f.replace(/\//g, "__")}.draft`)));
68
+ const values = files.filter((f) => existsSync(ventureFile(dir, f)) && load(ventureFile(dir, f)).status !== "retired").map((f) => ({ f, v: get(load(ventureFile(dir, f)), s.attribute) })).filter((x) => filled(x.v));
69
+ const drafts = files.filter((f) => existsSync(ventureFile(dir, `_drafts/${f.replace(/\//g, "__")}.draft`)));
69
70
  if (!values.length) return { id: s.id, record: s.record, attribute: s.attribute, status: drafts.length ? "draft" : "missing" };
70
71
  // A record with no log entry existed before the log; a person's baseline or write confirms it.
71
72
  const confirmed = values.every(({ f }) => confirmedIn(dir, f, s.id));
@@ -87,7 +88,7 @@ export function foundation(dir) {
87
88
 
88
89
  export function problems(dir) {
89
90
  const out = FRAME.questions.flatMap((q) => q.statements).filter((s) => s.record && !MODEL.records.some((r) => r.id === s.record)).map((s) => `frame: ${s.id} names record type ${s.record}, which the data model lacks`);
90
- const there = (...fs) => fs.map((f) => join(dir, f)).find((p) => existsSync(p));
91
+ const there = (...fs) => fs.map((f) => ventureFile(dir, f)).find((p) => existsSync(p));
91
92
  const icp = there("foundation/icp.json", "purpose/icp.json"), prob = there("foundation/problem.json", "purpose/validation/problem.json");
92
93
  if (icp) {
93
94
  const forms = new Set(existsSync(prob) ? (load(prob).forms ?? []).map((f) => f.id) : []);
@@ -8,7 +8,6 @@
8
8
  "id",
9
9
  "name",
10
10
  "role",
11
- "hats",
12
11
  "joined"
13
12
  ],
14
13
  "additionalProperties": false,
@@ -46,7 +45,8 @@
46
45
  "capabilities"
47
46
  ]
48
47
  },
49
- "uniqueItems": true
48
+ "uniqueItems": true,
49
+ "description": "The functions this founder owns, in the flat layout. In the structured layout (EE-086) they are read from venture.json"
50
50
  },
51
51
  "joined": {
52
52
  "type": "string",
package/schema/frames.mjs CHANGED
@@ -23,6 +23,7 @@
23
23
  import { existsSync, readdirSync, readFileSync } from "node:fs";
24
24
  import { tmpdir } from "node:os";
25
25
  import { join, resolve } from "node:path";
26
+ import { ventureFile } from "./venture-paths.mjs";
26
27
 
27
28
  const repo = new URL("..", import.meta.url).pathname;
28
29
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -93,9 +94,9 @@ export async function props(dir) {
93
94
  const v = load(join(dir, "venture.json"));
94
95
  const ds = decisions(dir).decisions;
95
96
  const answer = (id) => { const d = ds.find((x) => x.id === id); return d?.status === "decided" ? d.answer : null; };
96
- const sum = existsSync(join(dir, "money/_summary.json")) ? load(join(dir, "money/_summary.json")) : null;
97
- const own = existsSync(join(dir, "governance/ownership.json")) ? load(join(dir, "governance/ownership.json")) : null;
98
- const people = existsSync(join(dir, "people/people.json")) ? load(join(dir, "people/people.json")) : null;
97
+ const sum = existsSync(join(ventureFile(dir, "money/_summary.json"))) ? load(join(ventureFile(dir, "money/_summary.json"))) : null;
98
+ const own = existsSync(join(ventureFile(dir, "governance/ownership.json"))) ? load(join(ventureFile(dir, "governance/ownership.json"))) : null;
99
+ const people = existsSync(join(ventureFile(dir, "people/people.json"))) ? load(join(ventureFile(dir, "people/people.json"))) : null;
99
100
  const out = {
100
101
  phase: v.phase,
101
102
  goal: answer("discovery-goal") ?? NONE,
@@ -39,6 +39,7 @@ import { agentFromEnv, entries as provEntries, record as prov } from "../interfa
39
39
  import { files as ventureFiles, glob, resolve as resolveRoute, routes } from "../interface/router.mjs";
40
40
  import { connect, LOCAL, sourcesOf } from "../knowledge/connectors/index.mjs";
41
41
  import { decisions as decisionMatrix, PHASES } from "./decisions.mjs";
42
+ import { ventureFile } from "./venture-paths.mjs";
42
43
 
43
44
  const repo = new URL("..", import.meta.url).pathname;
44
45
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
@@ -83,7 +84,7 @@ const human = (s) => s % 604800 === 0 && s ? `${s / 604800}w` : s % 86400 === 0
83
84
  */
84
85
  export function profiles(dir) {
85
86
  const base = load(join(repo, DEFAULTS));
86
- const cfg = dir && existsSync(join(dir, "naive.config.json")) ? load(join(dir, "naive.config.json")).freshness ?? {} : {};
87
+ const cfg = dir && existsSync(join(ventureFile(dir, "naive.config.json"))) ? load(join(ventureFile(dir, "naive.config.json"))).freshness ?? {} : {};
87
88
  const out = { default: cfg.default ?? base.default, profiles: { ...base.profiles, ...(cfg.profiles ?? {}) }, records: { ...base.records, ...(cfg.records ?? {}) }, overridden: Object.keys(cfg).length > 0 };
88
89
  const where = (k) => (cfg.profiles?.[k] || cfg.records?.[k] || k === "default") && out.overridden ? "naive.config.json freshness" : DEFAULTS;
89
90
  for (const [name, p] of Object.entries(out.profiles)) {
@@ -124,7 +125,7 @@ export function recordFiles(dir) {
124
125
 
125
126
  /** The freshness log: revalidate, verify and phase events, oldest first. */
126
127
  export function events(dir) {
127
- const f = join(dir, LOG);
128
+ const f = ventureFile(dir, LOG);
128
129
  return existsSync(f) ? readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l)) : [];
129
130
  }
130
131
 
@@ -172,7 +173,7 @@ const at = (obj, pointer) => pointer.split("/").slice(1).map((k) => k.replace(/~
172
173
  // parent record; a child that does not show the parent's value has drifted. Children elsewhere
173
174
  // are read through their connector when it is a file system, else left to the platform.
174
175
  async function whereUsed(dir, byRoute, byPath, unchecked) {
175
- const f = join(dir, "knowledge/where-used.json");
176
+ const f = join(ventureFile(dir, "knowledge/where-used.json"));
176
177
  if (!existsSync(f)) return [];
177
178
  const ids = new Map(sourcesOf(dir).sources.map((s) => [s.id, s]));
178
179
  const out = [];
@@ -181,13 +182,13 @@ async function whereUsed(dir, byRoute, byPath, unchecked) {
181
182
  const route = r ? (r.collection === "item" ? r.route.replace("[id]", params.id) : r.route) : fact.parent.route;
182
183
  const parent = byRoute.get(route);
183
184
  if (!parent || !parent.endsWith(".json")) { unchecked.push(`${fact.id}: parent ${fact.parent.route} is not a JSON record in the venture folder`); continue; }
184
- const value = at(load(join(dir, parent)), fact.parent.pointer);
185
+ const value = at(load(ventureFile(dir, parent)), fact.parent.pointer);
185
186
  for (const child of fact.children) {
186
187
  if (child.status === "frozen") continue;
187
188
  const s = ids.get(child.source);
188
189
  if (!s || s.connector !== "filesystem") { unchecked.push(`${fact.id} -> ${child.source}://${child.path}: needs the platform to check`); continue; }
189
190
  let content;
190
- if (s.id === LOCAL) content = existsSync(join(dir, child.path)) ? readFileSync(join(dir, child.path), "utf8") : null;
191
+ if (s.id === LOCAL) content = existsSync(ventureFile(dir, child.path)) ? readFileSync(ventureFile(dir, child.path), "utf8") : null;
191
192
  else content = (await (await connect(s, dir)).read(child.path))?.content ?? null;
192
193
  const shows = content === null ? false : child.match.kind === "json"
193
194
  ? (() => { try { return JSON.stringify(at(JSON.parse(content), child.match.pointer)) === JSON.stringify(value); } catch { return false; } })()
@@ -217,17 +218,17 @@ export async function freshness(dir, { now, light = false } = {}) {
217
218
  const change = phaseChange(dir, venture, evs, warnings);
218
219
  const lastOf = (path, kinds) => { const e = log.filter((x) => x.entity === path && kinds.has(x.activity)).at(-1); return e ? Date.parse(e.at) : null; };
219
220
  // The last time a file's content changed: its last logged write, else its file time.
220
- const changedAt = (path) => lastOf(path, CONTENT) ?? (existsSync(join(dir, path)) ? statSync(join(dir, path)).mtimeMs : null);
221
+ const changedAt = (path) => lastOf(path, CONTENT) ?? (existsSync(ventureFile(dir, path)) ? statSync(ventureFile(dir, path)).mtimeMs : null);
221
222
 
222
223
  for (const i of items) {
223
224
  const write = lastOf(i.path, CONTENT), confirmed = lastOf(i.path, CHECKED);
224
225
  const verified = evs.filter((e) => e.event === "verify" && e.target && covers(e.target, i, dir)).map((e) => Date.parse(e.at)).at(-1) ?? null;
225
226
  const known = [[write, "provenance log: written"], [confirmed, "provenance log: confirmed"], [verified, "freshness log: verified"]].filter(([x]) => x !== null).sort((a, b) => b[0] - a[0]);
226
- i.checked_at = known.length ? known[0][0] : statSync(join(dir, i.path)).mtimeMs;
227
+ i.checked_at = known.length ? known[0][0] : statSync(ventureFile(dir, i.path)).mtimeMs;
227
228
  i.basis = known.length ? known[0][1] : "file time (not in the provenance log)";
228
- i.changed_at = write ?? statSync(join(dir, i.path)).mtimeMs;
229
+ i.changed_at = write ?? statSync(ventureFile(dir, i.path)).mtimeMs;
229
230
  const latest = log.filter((x) => x.entity === i.path && x.derived_from?.length && CONTENT.has(x.activity)).at(-1);
230
- i.depends_on = normalizeDeps(latest?.derived_from, byRoute, log).filter((d) => d !== i.path && (byPath.has(d) || existsSync(join(dir, d))));
231
+ i.depends_on = normalizeDeps(latest?.derived_from, byRoute, log).filter((d) => d !== i.path && (byPath.has(d) || existsSync(ventureFile(dir, d))));
231
232
  i.profile = profileFor(P, i.record, i.route);
232
233
  i.reasons = [];
233
234
  }
@@ -356,7 +357,7 @@ function append(dir, event, by) {
356
357
  rmSync(scratch, { recursive: true, force: true });
357
358
  if (v.status !== 0) throw new Error(`does not match schema/freshness-log.schema.json:\n${(v.stdout + v.stderr).trim()}`);
358
359
  mkdirSync(join(dir, dirname(LOG)), { recursive: true });
359
- appendFileSync(join(dir, LOG), JSON.stringify(line) + "\n");
360
+ appendFileSync(ventureFile(dir, LOG), JSON.stringify(line) + "\n");
360
361
  const what = event.target ? `${event.target.kind}:${event.target.value}` : `${event.from} -> ${event.to}`;
361
362
  const entry = prov(dir, { entity: LOG, route: "knowledge/freshness-log", activity: "write", agent: by, note: `${event.event} ${what}${event.reason ? `: ${event.reason}` : ""}` });
362
363
  return { line, provenance: entry.hash };
@@ -437,10 +438,10 @@ export async function freeze(dir, out, { now } = {}) {
437
438
  if (!rel.startsWith("..") && !isAbsolute(rel) && !rel.split("/")[0].startsWith(".")) return { ok: false, error: `${out} is inside the venture: freeze into a folder outside it, or under a dot folder such as .release/` };
438
439
  const byPath = new Map(f.items.map((i) => [i.path, i]));
439
440
  mkdirSync(target, { recursive: true });
440
- for (const p of f.shell.frozen) { mkdirSync(dirname(join(target, p)), { recursive: true }); copyFileSync(join(dir, p), join(target, p)); }
441
+ for (const p of f.shell.frozen) { mkdirSync(dirname(join(target, p)), { recursive: true }); copyFileSync(ventureFile(dir, p), join(target, p)); }
441
442
  const manifest = {
442
443
  venture: f.venture, phase: f.phase, frozen_at: f.now,
443
- frozen: f.shell.frozen.map((p) => { const i = byPath.get(p); return { path: p, route: i.route, record: i.record, state: i.state, sha256: createHash("sha256").update(readFileSync(join(dir, p))).digest("hex"), ...(i.state === "stale" ? { reasons: i.reasons.map((r) => r.note) } : {}) }; }),
444
+ frozen: f.shell.frozen.map((p) => { const i = byPath.get(p); return { path: p, route: i.route, record: i.record, state: i.state, sha256: createHash("sha256").update(readFileSync(ventureFile(dir, p))).digest("hex"), ...(i.state === "stale" ? { reasons: i.reasons.map((r) => r.note) } : {}) }; }),
444
445
  holes: f.shell.holes.map((p) => { const i = byPath.get(p); return { path: p, route: i.route, record: i.record, profile: i.profile, read: `mentura ${i.route} read --venture ${f.venture}` }; }),
445
446
  };
446
447
  writeFileSync(join(target, "shell.json"), JSON.stringify(manifest, null, 2) + "\n");
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
14
14
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
15
15
  import { basename, dirname, join } from "node:path";
16
16
  import { approve } from "./decisions.mjs";
17
+ import { ventureFile } from "./venture-paths.mjs";
17
18
 
18
19
  const sha = (s) => createHash("sha256").update(s).digest("hex");
19
20
  const LETTERS = "abcdefghijklmnopqrstuvwxyz";
@@ -42,7 +43,7 @@ export function sidecar(dir, draft) {
42
43
  * Returns { written, variant, variants, decision }.
43
44
  */
44
45
  export function writeHeld(dir, { draft, file, text, verdict, route, agent = {} }) {
45
- const dest = join(dir, draft);
46
+ const dest = ventureFile(dir, draft);
46
47
  mkdirSync(dirname(dest), { recursive: true });
47
48
  const at = new Date().toISOString();
48
49
  const by = agent.id ?? agent.kind ?? "agent";
@@ -64,7 +65,7 @@ export function writeHeld(dir, { draft, file, text, verdict, route, agent = {} }
64
65
  const next = LETTERS[variants.length];
65
66
  if (!next) throw new Error(`${route} already holds ${variants.length} drafts side by side; accept one first`);
66
67
  const vfile = variantPath(draft, next);
67
- writeFileSync(join(dir, vfile), text);
68
+ writeFileSync(ventureFile(dir, vfile), text);
68
69
  variants.push({ variant: next, file: vfile, by, at, sha256: sha(text) });
69
70
  const dec = variantsDecision(file);
70
71
  const record = {
@@ -79,8 +80,8 @@ export function writeHeld(dir, { draft, file, text, verdict, route, agent = {} }
79
80
  note: `The gate kept ${variants.length} differing drafts side by side (rule ${verdict.rule}). Accept one: mentura accept ${route} --pick <letter>`,
80
81
  provenance: { drafted_by: { kind: "framework" } },
81
82
  };
82
- mkdirSync(dirname(join(dir, dec.path)), { recursive: true });
83
- writeFileSync(join(dir, dec.path), JSON.stringify(record, null, 2) + "\n");
83
+ mkdirSync(dirname(ventureFile(dir, dec.path)), { recursive: true });
84
+ writeFileSync(ventureFile(dir, dec.path), JSON.stringify(record, null, 2) + "\n");
84
85
  writeFileSync(join(dir, sidecarOf(draft)), JSON.stringify({ ...(meta ?? { route, file, rule: verdict.rule, source: verdict.source, then: verdict.then, ...(verdict.owner ? { owner: verdict.owner, function: verdict.function ?? null } : {}) }), variants, decision: dec.id }, null, 2) + "\n");
85
86
  return { written: vfile, variant: next, variants: variants.map((v) => v.variant), decision: dec.id };
86
87
  }
@@ -99,8 +100,8 @@ export function pick(dir, draft, letter) {
99
100
  /** After a person accepts a draft: removes every variant and the sidecar, and decides the variants decision. */
100
101
  export function settle(dir, draft, { picked = "a", by } = {}) {
101
102
  const meta = sidecar(dir, draft);
102
- for (const v of meta?.variants ?? []) rmSync(join(dir, v.file), { force: true });
103
- rmSync(join(dir, draft), { force: true });
103
+ for (const v of meta?.variants ?? []) rmSync(ventureFile(dir, v.file), { force: true });
104
+ rmSync(ventureFile(dir, draft), { force: true });
104
105
  rmSync(join(dir, sidecarOf(draft)), { force: true });
105
106
  if (!meta?.decision) return { picked };
106
107
  const d = approve(dir, meta.decision, { by: by ?? process.env.MENTURA_PERSON ?? "founder", answer: picked, note: `accepted draft ${picked} of ${meta.route}` });
@@ -109,7 +110,7 @@ export function settle(dir, draft, { picked = "a", by } = {}) {
109
110
 
110
111
  /** The drafts waiting in a venture: route, rule, owner and variants. A draft with no sidecar was held by its decision class. */
111
112
  export function inbox(dir, owner) {
112
- const d = join(dir, "_drafts");
113
+ const d = join(ventureFile(dir, "_drafts"));
113
114
  if (!existsSync(d)) return [];
114
115
  return readdirSync(d).filter((f) => f.endsWith(".draft")).sort().map((f) => {
115
116
  const draft = `_drafts/${f}`;
@@ -19,6 +19,7 @@ import { spawnSync } from "node:child_process";
19
19
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
20
20
  import { dirname, join, relative } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
+ import { KBASE, ventureFile } from "./venture-paths.mjs";
22
23
 
23
24
  const here = dirname(fileURLToPath(import.meta.url));
24
25
  const repo = join(here, "..");
@@ -26,6 +27,10 @@ const { layers, formalized, pillars, readme } = JSON.parse(readFileSync(join(her
26
27
  const args = process.argv.slice(2);
27
28
  const flag = (name) => { const i = args.indexOf(`--${name}`); return i < 0 ? null : args.splice(i, 2)[1]; };
28
29
  const phase = flag("phase") ?? "discovery", parent = flag("parent"), legalForm = flag("legal-form"), only = flag("layers"), typeId = flag("type");
30
+ // --layout structured (EE-086): knowledge/, meta/ and an org chart in venture.json; without it, the flat layout.
31
+ const layout = flag("layout") ?? "flat";
32
+ if (!["flat", "structured"].includes(layout)) { console.error(`unknown layout ${layout}: flat or structured`); process.exit(2); }
33
+ const structured = layout === "structured";
29
34
  const vtype = typeId ? JSON.parse(readFileSync(join(here, "venture-types.json"), "utf8")).types.find((t) => t.id === typeId) : null;
30
35
  if (typeId && !vtype) { console.error(`unknown venture type ${typeId}: see node schema/venture-type.mjs list`); process.exit(2); }
31
36
  const [dir, id] = args;
@@ -41,27 +46,47 @@ mkdirSync(dir, { recursive: true });
41
46
  const venture = { id, name: id, ...(vtype ? { type: vtype.id } : {}), parent: parent ?? null, phase };
42
47
  if (legalForm || vtype?.funding) venture.form = { ...(legalForm ? { legal_form: legalForm } : {}), ...(vtype?.funding ? { funding: vtype.funding } : {}) };
43
48
  if (phase !== "discovery") venture.core = { promise: "", promised_to: "", must_not: "", competence: "" };
44
- Object.assign(venture, { owner: "self", status: "active", profile: "profile.md", layers: {} });
45
- for (const layer of ALL) {
49
+ Object.assign(venture, { owner: "self", status: "active", profile: structured ? "README.md" : "profile.md", layers: {} });
50
+ // The package folders (EE-070); a typed venture gets its type's packages.
51
+ const packageDirs = vtype ? vtype.packages : ["foundation", "validation", "product", "marketing", "finance", "people", "customers", "investors", "money"];
52
+ if (structured) {
53
+ // venture.json first, so every path below resolves into k-base/ and meta/. The org chart starts flat:
54
+ // one function per package, owned by no one until create-venture or the founder names the owner.
55
+ venture.layout = "structured";
56
+ venture.functions = Object.fromEntries(packageDirs.map((p) => [p, { owner: null }]));
57
+ for (const layer of ALL) venture.layers[layer] = on.includes(layer) ? { location: `${KBASE}/` } : false;
58
+ writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
59
+ for (const sub of [KBASE, "meta"]) mkdirSync(join(dir, sub), { recursive: true });
60
+ }
61
+ for (const layer of structured ? [] : ALL) {
46
62
  if (!on.includes(layer)) { if (only !== null || !["governance", "interface"].includes(layer)) venture.layers[layer] = false; continue; }
47
- mkdirSync(join(dir, layer), { recursive: true });
63
+ mkdirSync(ventureFile(dir, layer), { recursive: true });
48
64
  writeFileSync(join(dir, layer, "README.md"), `# ${layer}\n\n${readme[layer]}\n`);
49
65
  venture.layers[layer] = { location: `${layer}/` };
50
66
  }
51
- for (const sub of ["products", "accounts"]) { mkdirSync(join(dir, sub), { recursive: true }); writeFileSync(join(dir, sub, ".gitkeep"), ""); }
67
+ if (!structured) for (const sub of ["products", "accounts"]) { mkdirSync(ventureFile(dir, sub), { recursive: true }); writeFileSync(join(dir, sub, ".gitkeep"), ""); }
52
68
  // The package folder layout (EE-070). Old layer folders above stay for one release.
53
69
  // A typed venture gets its type's packages; the rest stay out until need-now suggests them.
54
- const packageDirs = vtype ? vtype.packages : ["foundation", "validation", "product", "marketing", "finance", "people", "customers", "investors", "money"];
55
- for (const sub of ["raw/dump", "raw/sources", "ideas", ...packageDirs, "decisions", "evidence", "public", "_private", "_drafts", ".release"]) {
56
- mkdirSync(join(dir, sub), { recursive: true });
70
+ for (const sub of ["raw/dump", "raw/sources", "ideas", ...packageDirs, "decisions", "evidence", ...(structured ? [] : ["public"]), "_private", "_drafts", ".release"]) {
71
+ mkdirSync(ventureFile(dir, sub), { recursive: true });
72
+ }
73
+ // Every package folder explains itself (EE-086): a README.md from its package, for people. GitHub and
74
+ // editors show it when the folder is opened; an AGENTS.md beside it is the founder's, for agents.
75
+ if (structured) for (const pkg of packageDirs) {
76
+ const f = join(repo, "packages", pkg, "package.json");
77
+ if (!existsSync(f)) continue;
78
+ const p = JSON.parse(readFileSync(f, "utf8"));
79
+ const lines = [`# ${p.title ?? pkg}`, "", p.purpose ?? "", ...(p.required?.length ? ["", `What goes in it first: ${p.required.join(", ")}.`] : []), ...(p.optional?.length ? [`Also: ${p.optional.join(", ")}.`] : []),
80
+ ...(p.gates?.length ? ["", `It is ready when ${p.gates.join("; ")}.`] : []), "", `Owner: whoever owns the ${pkg} function in venture.json.`, ""];
81
+ writeFileSync(join(ventureFile(dir, pkg), "README.md"), lines.join("\n"));
57
82
  }
58
- mkdirSync(join(dir, "pillars"), { recursive: true });
59
- for (const [name, what] of Object.entries(pillars.stubs)) {
60
- const guide = relative(join(dir, "pillars"), join(repo, pillars.guides, `${name}.md`));
61
- writeFileSync(join(dir, "pillars", `${name}.md`), `# ${name[0].toUpperCase() + name.slice(1)} pillar\n\n${what}\n\nGuide: [${name} pillar](${guide})\n`);
83
+ if (!structured) mkdirSync(join(ventureFile(dir, "pillars")), { recursive: true });
84
+ for (const [name, what] of Object.entries(structured ? {} : pillars.stubs)) {
85
+ const guide = relative(join(ventureFile(dir, "pillars")), join(repo, pillars.guides, `${name}.md`));
86
+ writeFileSync(join(ventureFile(dir, "pillars"), `${name}.md`), `# ${name[0].toUpperCase() + name.slice(1)} pillar\n\n${what}\n\nGuide: [${name} pillar](${guide})\n`);
62
87
  }
63
- writeFileSync(join(dir, "profile.md"), `# ${id}\n\nWhat this venture is, in a few sentences. Its machine fields are in venture.json (EE-011).\n\n## Changelog\n`);
64
- writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
88
+ writeFileSync(join(ventureFile(dir, "profile.md")), `# ${id}\n\nWhat this venture is, in a few sentences. Its machine fields are in venture.json (EE-011).\n\n## Changelog\n`);
89
+ if (!structured) writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
65
90
  // The venture's root layout (EE-072, I-4): every route's layout chain starts here. Lines shaped
66
91
  // "key: value" are instructions the router hands an agent with the layout.
67
92
  writeFileSync(join(dir, "AGENTS.md"), `# AGENTS.md: ${id}
@@ -97,8 +122,8 @@ const productFoundation = [
97
122
  ];
98
123
  // The team question (EE-060): asked in Discovery, failing open, so validation never waits on it. A
99
124
  // founding team, or a founder looking for one, gets the founder-agreement kit beside validation.
100
- mkdirSync(join(dir, "decisions"), { recursive: true });
101
- writeFileSync(join(dir, "decisions/venture.team.decision.json"), JSON.stringify({ id: "founding-team", question: "Who is founding this venture: you alone, you with cofounders, or you looking for cofounders?",
125
+ mkdirSync(join(ventureFile(dir, "decisions")), { recursive: true });
126
+ writeFileSync(join(ventureFile(dir, "decisions/venture.team.decision.json")), JSON.stringify({ id: "founding-team", question: "Who is founding this venture: you alone, you with cofounders, or you looking for cofounders?",
102
127
  decides: { record: "founder-agreement", path: "governance/founder-agreement.json" }, layer: "governance", boundary: "fail-open", needed_in: phase, status: "open",
103
128
  options: ["Solo: I am founding it alone", "Cofounders: we are founding it together", "Looking: I want cofounders"],
104
129
  unlocks: [{ activity: "Draft the founder agreement with the founding-agreement kit", routes: ["governance/founder-agreement"] }], provenance: framework }, null, 2) + "\n");
@@ -113,15 +138,18 @@ const fundFoundation = [
113
138
  ];
114
139
  const decisionSet = vtype?.decisions ?? "product-foundation";
115
140
  if (decisionSet === "fund-foundation") {
116
- mkdirSync(join(dir, "fund/decisions"), { recursive: true });
117
- for (const d of fundFoundation) writeFileSync(join(dir, "fund/decisions", `${d.id === "first-close" ? "terms.first-close" : d.id.replace(/^fund-/, "")}.decision.json`),
141
+ mkdirSync(join(ventureFile(dir, "fund/decisions")), { recursive: true });
142
+ for (const d of fundFoundation) writeFileSync(join(ventureFile(dir, "fund/decisions"), `${d.id === "first-close" ? "terms.first-close" : d.id.replace(/^fund-/, "")}.decision.json`),
118
143
  JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
119
144
  }
120
- if (decisionSet === "product-foundation") mkdirSync(join(dir, "products/decisions"), { recursive: true });
121
- if (decisionSet === "product-foundation") for (const d of productFoundation) writeFileSync(join(dir, "products/decisions", `product.${d.id.replace(/^product-/, "")}.decision.json`),
145
+ // The product questions sit beside the products: product/ in the structured layout, products/ in the flat one.
146
+ const productDecisions = structured ? "product/decisions" : "products/decisions";
147
+ if (decisionSet === "product-foundation") mkdirSync(join(ventureFile(dir, productDecisions)), { recursive: true });
148
+ if (decisionSet === "product-foundation") for (const d of productFoundation) writeFileSync(join(ventureFile(dir, productDecisions), `product.${d.id.replace(/^product-/, "")}.decision.json`),
122
149
  JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
123
150
  // The type's functions are imported like any department (EE-047): listed in venture.json, with their folders.
124
- if (vtype) for (const d of vtype.departments) {
151
+ // In the structured layout the org chart is venture.json's functions; the flat layout imports departments.
152
+ if (vtype && !structured) for (const d of vtype.departments) {
125
153
  const r = spawnSync("node", [join(here, "add-department.mjs"), dir, d], { encoding: "utf8" });
126
154
  if (r.status !== 0) { console.error((r.stderr || r.stdout).trim()); process.exit(1); }
127
155
  }