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