naive-framework 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) 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/package-routes.mjs +4 -3
  17. package/interface/provenance.mjs +5 -4
  18. package/interface/reads.mjs +4 -3
  19. package/interface/release-card.mjs +2 -1
  20. package/interface/router.mjs +10 -9
  21. package/interface/vitals.mjs +3 -2
  22. package/knowledge/connectors/catalog.mjs +3 -2
  23. package/knowledge/connectors/filesystem.mjs +10 -5
  24. package/knowledge/connectors/index.mjs +3 -2
  25. package/operations/marketing/check-public.mjs +7 -6
  26. package/operations/marketing/site/plan.mjs +4 -3
  27. package/operations/marketing/site/presence.mjs +4 -3
  28. package/package.json +1 -1
  29. package/platform/integrations/rho.mjs +2 -1
  30. package/purpose/frameworks/idea-map/route.mjs +5 -4
  31. package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
  32. package/schema/access.mjs +2 -1
  33. package/schema/add-department.mjs +3 -2
  34. package/schema/assess-problem.mjs +3 -2
  35. package/schema/assets.mjs +6 -5
  36. package/schema/brand.mjs +11 -10
  37. package/schema/caching.mjs +5 -4
  38. package/schema/check-config.mjs +4 -3
  39. package/schema/check-data-model.mjs +2 -1
  40. package/schema/check-data-packages.mjs +5 -4
  41. package/schema/check-execution.mjs +4 -3
  42. package/schema/check-people.mjs +22 -8
  43. package/schema/check-private.mjs +2 -1
  44. package/schema/check-ready.mjs +6 -5
  45. package/schema/check-sources.mjs +2 -1
  46. package/schema/check-where-used.mjs +4 -3
  47. package/schema/decision-analytics.mjs +2 -1
  48. package/schema/decisions.mjs +14 -11
  49. package/schema/foundation.mjs +8 -7
  50. package/schema/founder.schema.json +2 -2
  51. package/schema/frames.mjs +4 -3
  52. package/schema/freshness.mjs +13 -12
  53. package/schema/gate-drafts.mjs +8 -7
  54. package/schema/init-venture.mjs +38 -20
  55. package/schema/migrate-venture.mjs +8 -7
  56. package/schema/migrations/kit-to-package.mjs +4 -3
  57. package/schema/migrations/package-layout.mjs +9 -5
  58. package/schema/model-steps.mjs +8 -7
  59. package/schema/naive-config.mjs +4 -3
  60. package/schema/need-now.mjs +5 -4
  61. package/schema/packages.mjs +11 -10
  62. package/schema/payouts.mjs +3 -2
  63. package/schema/people.schema.json +2 -3
  64. package/schema/port-venture.mjs +206 -132
  65. package/schema/release-read.mjs +5 -4
  66. package/schema/releases.mjs +21 -20
  67. package/schema/run-handoff.mjs +5 -4
  68. package/schema/setup.mjs +5 -4
  69. package/schema/stale.mjs +6 -5
  70. package/schema/structure.mjs +5 -4
  71. package/schema/time-to-ready.mjs +7 -6
  72. package/schema/triggers.mjs +7 -6
  73. package/schema/upgrades.mjs +31 -30
  74. package/schema/venture-paths.mjs +85 -0
  75. package/schema/venture.schema.json +25 -17
  76. package/schema/workspace.schema.json +5 -1
@@ -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 { 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,37 @@ 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 knowledge/ 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: "knowledge/" } : false;
58
+ writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
59
+ for (const sub of ["knowledge", "meta"]) mkdirSync(ventureFile(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 });
57
72
  }
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`);
73
+ if (!structured) mkdirSync(join(ventureFile(dir, "pillars")), { recursive: true });
74
+ for (const [name, what] of Object.entries(structured ? {} : pillars.stubs)) {
75
+ const guide = relative(join(ventureFile(dir, "pillars")), join(repo, pillars.guides, `${name}.md`));
76
+ writeFileSync(join(ventureFile(dir, "pillars"), `${name}.md`), `# ${name[0].toUpperCase() + name.slice(1)} pillar\n\n${what}\n\nGuide: [${name} pillar](${guide})\n`);
62
77
  }
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");
78
+ 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`);
79
+ if (!structured) writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
65
80
  // The venture's root layout (EE-072, I-4): every route's layout chain starts here. Lines shaped
66
81
  // "key: value" are instructions the router hands an agent with the layout.
67
82
  writeFileSync(join(dir, "AGENTS.md"), `# AGENTS.md: ${id}
@@ -97,8 +112,8 @@ const productFoundation = [
97
112
  ];
98
113
  // The team question (EE-060): asked in Discovery, failing open, so validation never waits on it. A
99
114
  // 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?",
115
+ mkdirSync(join(ventureFile(dir, "decisions")), { recursive: true });
116
+ 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
117
  decides: { record: "founder-agreement", path: "governance/founder-agreement.json" }, layer: "governance", boundary: "fail-open", needed_in: phase, status: "open",
103
118
  options: ["Solo: I am founding it alone", "Cofounders: we are founding it together", "Looking: I want cofounders"],
104
119
  unlocks: [{ activity: "Draft the founder agreement with the founding-agreement kit", routes: ["governance/founder-agreement"] }], provenance: framework }, null, 2) + "\n");
@@ -113,15 +128,18 @@ const fundFoundation = [
113
128
  ];
114
129
  const decisionSet = vtype?.decisions ?? "product-foundation";
115
130
  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`),
131
+ mkdirSync(join(ventureFile(dir, "fund/decisions")), { recursive: true });
132
+ 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
133
  JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
119
134
  }
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`),
135
+ // The product questions sit beside the products: product/ in the structured layout, products/ in the flat one.
136
+ const productDecisions = structured ? "product/decisions" : "products/decisions";
137
+ if (decisionSet === "product-foundation") mkdirSync(join(ventureFile(dir, productDecisions)), { recursive: true });
138
+ if (decisionSet === "product-foundation") for (const d of productFoundation) writeFileSync(join(ventureFile(dir, productDecisions), `product.${d.id.replace(/^product-/, "")}.decision.json`),
122
139
  JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
123
140
  // 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) {
141
+ // In the structured layout the org chart is venture.json's functions; the flat layout imports departments.
142
+ if (vtype && !structured) for (const d of vtype.departments) {
125
143
  const r = spawnSync("node", [join(here, "add-department.mjs"), dir, d], { encoding: "utf8" });
126
144
  if (r.status !== 0) { console.error((r.stderr || r.stdout).trim()); process.exit(1); }
127
145
  }
@@ -21,6 +21,7 @@ import { fileURLToPath } from "node:url";
21
21
  import { call, routes } from "../interface/router.mjs";
22
22
  import { agentFromEnv, record as prov } from "../interface/provenance.mjs";
23
23
  import { propose } from "./decisions.mjs";
24
+ import { ventureFile } from "./venture-paths.mjs";
24
25
 
25
26
  export const PLAN = ".release/migrate-plan.json";
26
27
  const MAX_FILE = 512 * 1024;
@@ -117,7 +118,7 @@ Files: ${JSON.stringify(Object.fromEntries(given.map((f) => [f.path, f.text])))}
117
118
  /** Writes the plan into the venture and proposes one open decision per function it touches. */
118
119
  export function proposeDecisions(dir, p) {
119
120
  mkdirSync(join(dir, dirname(PLAN)), { recursive: true });
120
- writeFileSync(join(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
121
+ writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
121
122
  prov(dir, { entity: PLAN, activity: "draft", agent: agentFromEnv("agent", "migrate", { id: "naive-migrate" }), note: `migrate plan from ${p.from}: ${p.items.length} records, ${p.left_behind.length} left behind` });
122
123
  const groups = new Map();
123
124
  for (const it of p.items) groups.set(functionOf(it.route), [...(groups.get(functionOf(it.route)) ?? []), it]);
@@ -139,8 +140,8 @@ const answerOf = (d) => { const a = String(d?.answer ?? "").toLowerCase(); retur
139
140
 
140
141
  /** Writes what a person decided, through the router. Open decisions wait. */
141
142
  export async function apply(dir) {
142
- const p = load(join(dir, PLAN));
143
- const decisionOf = (fn) => { const f = join(dir, "decisions", `migrate-${fn}.decision.json`); return existsSync(f) ? load(f) : null; };
143
+ const p = load(ventureFile(dir, PLAN));
144
+ const decisionOf = (fn) => { const f = join(ventureFile(dir, "decisions"), `migrate-${fn}.decision.json`); return existsSync(f) ? load(f) : null; };
144
145
  const done = { brought: 0, drafted: 0, pointed: 0, left: 0, waiting: 0, failed: 0 };
145
146
  let pointed = false;
146
147
  for (const it of p.items.filter((x) => x.status === "planned")) {
@@ -157,7 +158,7 @@ export async function apply(dir) {
157
158
  done[it.status]++;
158
159
  }
159
160
  if (pointed) {
160
- const f = join(dir, "knowledge/sources.json");
161
+ const f = join(ventureFile(dir, "knowledge/sources.json"));
161
162
  const s = existsSync(f) ? load(f) : { sources: [], records: {} };
162
163
  const id = slug(basename(p.from));
163
164
  if (!s.sources.some((x) => x.id === id)) s.sources.push({ id, connector: "filesystem", writes: "none", root: p.from, note: "migrate: a person chose to point to these files rather than rewrite them" });
@@ -165,7 +166,7 @@ export async function apply(dir) {
165
166
  writeFileSync(f, JSON.stringify(s, null, 2) + "\n");
166
167
  prov(dir, { entity: "knowledge/sources.json", activity: "write", agent: agentFromEnv("agent", "migrate", { id: "naive-migrate" }), note: `points to ${p.from}, read-only` });
167
168
  }
168
- writeFileSync(join(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
169
+ writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
169
170
  return done;
170
171
  }
171
172
 
@@ -176,7 +177,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
176
177
  if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node schema/migrate-venture.mjs --from <project> --venture <dir> | --venture <dir> --apply"); process.exit(2); }
177
178
  try {
178
179
  if (args.includes("--apply")) {
179
- if (!existsSync(join(dir, PLAN))) { console.error(`no plan at ${join(dir, PLAN)}: run with --from first`); process.exit(1); }
180
+ if (!existsSync(ventureFile(dir, PLAN))) { console.error(`no plan at ${ventureFile(dir, PLAN)}: run with --from first`); process.exit(1); }
180
181
  const d = await apply(dir);
181
182
  console.log(Object.entries(d).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "nothing to do");
182
183
  } else {
@@ -186,7 +187,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
186
187
  for (const i of p.items) console.log(`${i.id} ${i.from.join(", ")} -> ${i.route} (${i.reshaping})`);
187
188
  for (const l of p.left_behind) console.log(`left ${l.from} (${l.why})`);
188
189
  console.log(`\n${p.items.length} records planned, ${p.left_behind.length} left behind. Nothing is written yet.`);
189
- if (decisions.length) console.log(`Read ${relative(process.cwd(), join(dir, PLAN))}, then decide ${decisions.join(", ")} with mentura decide (answer "bring in", "point to them" or "leave them"), then run with --apply.`);
190
+ if (decisions.length) console.log(`Read ${relative(process.cwd(), ventureFile(dir, PLAN))}, then decide ${decisions.join(", ")} with mentura decide (answer "bring in", "point to them" or "leave them"), then run with --apply.`);
190
191
  }
191
192
  } catch (e) { console.error(e.message); process.exit(1); }
192
193
  }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { existsSync, readdirSync, readFileSync } from "node:fs";
16
16
  import { join } from "node:path";
17
+ import { ventureFile } from "../venture-paths.mjs";
17
18
 
18
19
  export const id = "kit-to-package";
19
20
  export const describe = "Moves Naive packages out of naive.config.json kits into packages, and rewrites layer and kit routes inside records (decision unlocks, team-plan access, where-used parents) to package routes";
@@ -67,15 +68,15 @@ export function apply(dir, ctx) {
67
68
  return r.to;
68
69
  };
69
70
  const rewriteFile = (file, fn) => {
70
- if (!existsSync(join(dir, file))) return;
71
- const before = readFileSync(join(dir, file), "utf8");
71
+ if (!existsSync(ventureFile(dir, file))) return;
72
+ const before = readFileSync(ventureFile(dir, file), "utf8");
72
73
  const rec = JSON.parse(before);
73
74
  fn(rec);
74
75
  const after = JSON.stringify(rec, null, 2) + "\n";
75
76
  if (after.trim() !== before.trim()) ctx.write(file, after, { note: "routes renamed from layer and kit routes to package routes (EE-071)" });
76
77
  };
77
78
 
78
- const decisionFiles = (sub = "") => readdirSync(join(dir, sub), { withFileTypes: true }).flatMap((e) => {
79
+ const decisionFiles = (sub = "") => readdirSync(ventureFile(dir, sub), { withFileTypes: true }).flatMap((e) => {
79
80
  const rel = sub ? `${sub}/${e.name}` : e.name;
80
81
  if (e.isDirectory()) return e.name.startsWith(".") || e.name.startsWith("_") || existsSync(join(dir, rel, "venture.json")) ? [] : decisionFiles(rel);
81
82
  return e.name.endsWith(".decision.json") ? [rel] : [];
@@ -8,16 +8,20 @@
8
8
 
9
9
  import { existsSync, readdirSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
+ import { ventureFile } from "../venture-paths.mjs";
12
+ import { layoutOf } from "../venture-paths.mjs";
11
13
 
12
14
  export const id = "package-layout";
13
15
  export const describe = "Moves records from the layer folders (purpose/, pillars/, products/, knowledge/) into package folders (foundation/, validation/, product/, marketing/, people/, decisions/)";
14
16
 
15
17
  export function apply(dir, ctx) {
18
+ // A venture in the structured layout (EE-086) is already past this layout: nothing to move.
19
+ if (layoutOf(dir) === "structured") return;
16
20
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
17
21
  // A decision file infers its target from the folder it sits in. Stamp that path before the file
18
22
  // moves, so the decision still points at the same record.
19
23
  const stampDecisions = (folder) => {
20
- const d = join(dir, folder);
24
+ const d = ventureFile(dir, folder);
21
25
  if (!existsSync(d)) return;
22
26
  for (const f of readdirSync(d).filter((x) => x.endsWith(".decision.json"))) {
23
27
  const rec = load(join(d, f));
@@ -45,7 +49,7 @@ export function apply(dir, ctx) {
45
49
  ];
46
50
  for (const [a, b] of pairs) ctx.mv(a, b);
47
51
 
48
- const list = (rel, ext) => existsSync(join(dir, rel)) ? readdirSync(join(dir, rel)).filter((f) => f.endsWith(ext)) : [];
52
+ const list = (rel, ext) => existsSync(ventureFile(dir, rel)) ? readdirSync(ventureFile(dir, rel)).filter((f) => f.endsWith(ext)) : [];
49
53
  for (const f of list("products", ".json")) ctx.mv(`products/${f}`, `product/${f}`);
50
54
  for (const f of list("purpose/anatomy", ".json")) ctx.mv(`purpose/anatomy/${f}`, `product/anatomy/${f}`);
51
55
 
@@ -55,7 +59,7 @@ export function apply(dir, ctx) {
55
59
  }
56
60
 
57
61
  for (const f of ["product.md", "business.md"]) {
58
- if (existsSync(join(dir, "pillars", f))) {
62
+ if (existsSync(join(ventureFile(dir, "pillars"), f))) {
59
63
  ctx.mv(`pillars/${f}`, `_private/pillars/${f}`);
60
64
  ctx.escalate(`${f.replace(".md", "")} pillar: the brief names a home only for brand (marketing/). Archived at _private/pillars/${f} rather than guessed.`);
61
65
  }
@@ -68,8 +72,8 @@ export function apply(dir, ctx) {
68
72
  for (const gone of ["purpose", "pillars", "knowledge", "products"]) ctx.removeEmpty(gone);
69
73
 
70
74
  const policy = "governance/publication-policy.json";
71
- if (existsSync(join(dir, policy))) {
72
- const text = readFileSync(join(dir, policy), "utf8");
75
+ if (existsSync(ventureFile(dir, policy))) {
76
+ const text = readFileSync(ventureFile(dir, policy), "utf8");
73
77
  if (text.includes("pillars/brand.md")) ctx.write(policy, text.replaceAll("pillars/brand.md", "marketing/brand.md"), { note: "the brand pillar moved to marketing/brand.md (EE-070)" });
74
78
  }
75
79