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
@@ -42,6 +42,7 @@ import { understood } from "../../purpose/frameworks/idea-map/route.mjs";
42
42
  import { createHash } from "node:crypto";
43
43
  import { agentFromEnv, record as prov } from "../provenance.mjs";
44
44
  import { evaluate } from "./evals.mjs";
45
+ import { ventureFile } from "../../schema/venture-paths.mjs";
45
46
 
46
47
  const repo = new URL("../../", import.meta.url).pathname;
47
48
  const argv = process.argv.slice(2);
@@ -71,8 +72,8 @@ if (!existsSync(join(dir, "venture.json"))) {
71
72
  if (p.status !== 0) { console.error(p.stderr || p.stdout); process.exit(1); }
72
73
  if (opt("name")) { const v = JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")); v.name = opt("name"); writeFileSync(join(dir, "venture.json"), JSON.stringify(v, null, 2) + "\n"); }
73
74
  }
74
- mkdirSync(join(dir, "purpose/ideas"), { recursive: true });
75
- copyFileSync(dumpFile, join(dir, DUMP));
75
+ mkdirSync(join(ventureFile(dir, "purpose/ideas")), { recursive: true });
76
+ copyFileSync(dumpFile, ventureFile(dir, DUMP));
76
77
  prov(dir, { entity: DUMP, route: "purpose/idea-dump", activity: "create", agent: agentFromEnv("person", "harness", { id: founder }), note: "the founder's idea dump, verbatim" });
77
78
  lap(`venture ${id} scaffolded in ${ws}; idea dump saved (${readFileSync(dumpFile, "utf8").length} characters)`);
78
79
 
@@ -83,8 +84,8 @@ if (opt("money")) {
83
84
  const m = opt("money");
84
85
  const src = m === "live" ? ["--live"] : m === "sandbox" ? [] : ["--fixture", resolve(m)];
85
86
  const p = spawnSync("node", [join(repo, "platform/integrations/rho.mjs"), "sync", "--venture", dir, ...src], { encoding: "utf8" });
86
- if (p.status === 0 && existsSync(join(dir, "money/_summary.json"))) {
87
- money = JSON.parse(readFileSync(join(dir, "money/_summary.json"), "utf8"));
87
+ if (p.status === 0 && existsSync(join(ventureFile(dir, "money/_summary.json")))) {
88
+ money = JSON.parse(readFileSync(join(ventureFile(dir, "money/_summary.json")), "utf8"));
88
89
  prov(dir, { entity: "money/_summary.json", route: "money/_summary", activity: "create", agent: agentFromEnv("agent", "harness", { id: "naive-money" }), note: `synced from ${money.source}; ${money.transactions} transactions kept as pointers in a sensitive stream` });
89
90
  lap(`money synced from ${money.source}${money.business ? ` (${money.business})` : ""}: cash ${(money.cash.amount / 100).toFixed(2)} ${money.cash.currency}, monthly net ${(money.monthly_net.amount / 100).toFixed(2)}, runway ${money.runway_months ?? "not limited by burn"}, as of ${money.as_of}`);
90
91
  } else console.log(` money sync failed, so the run goes on without it: ${(p.stderr || p.stdout).trim().slice(0, 300)}`);
@@ -100,7 +101,7 @@ if (flag("board")) { process.env.MENTURA_WORKSPACE = ws; board = (await import("
100
101
 
101
102
  // The trace (EE-058): every step and every tool call the agent made, with timings, for LangSmith.
102
103
  const iso = () => new Date().toISOString();
103
- const trace = { name: `naive discovery: ${id}`, run_type: "chain", start: iso(), inputs: { dump: DUMP, ideas: readFileSync(join(dir, DUMP), "utf8"), founder, phase }, children: [], metadata: {
104
+ const trace = { name: `naive discovery: ${id}`, run_type: "chain", start: iso(), inputs: { dump: DUMP, ideas: readFileSync(ventureFile(dir, DUMP), "utf8"), founder, phase }, children: [], metadata: {
104
105
  venture: id, venture_id: id, pipeline_step: phase, money: money?.source ?? null,
105
106
  // LangSmith groups a founder's runs into one thread by thread_id: one session per venture per day.
106
107
  thread_id: process.env.NAIVE_SESSION ?? `${id}-${new Date().toISOString().slice(0, 10)}`,
@@ -155,11 +156,11 @@ async function agent(prompt, label, { tools = "mcp__naive__*,mcp__research__*",
155
156
  // The run's evals and trace: written beside the workspace, and sent to LangSmith when a key is set.
156
157
  function finish(code = 0) {
157
158
  trace.end = iso();
158
- const evals = existsSync(join(dir, DUMP)) ? evaluate(dir, { researched, settled }) : [];
159
+ const evals = existsSync(ventureFile(dir, DUMP)) ? evaluate(dir, { researched, settled }) : [];
159
160
  trace.feedback = evals;
160
161
  // What the run drafted, so a judge reading only the root run (LangSmith's online evaluators, Jev)
161
162
  // sees the problem record and the agent's reports beside the founder's dump in the inputs.
162
- const problem = join(dir, "purpose/validation/problem.json");
163
+ const problem = join(ventureFile(dir, "purpose/validation/problem.json"));
163
164
  const drafted = { problem: existsSync(problem) ? JSON.parse(readFileSync(problem, "utf8")) : null, reports: trace.children.filter((c) => c.outputs?.report).map((c) => ({ pass: c.name, report: c.outputs.report })) };
164
165
  trace.outputs = { settled, docket: { steps: docketSteps, now: compact(docket(dir)) }, drafted, evals: Object.fromEntries(evals.map((e) => [e.key, e.score])), laps };
165
166
  writeFileSync(join(ws, "trace.json"), JSON.stringify(trace, null, 2) + "\n");
@@ -180,7 +181,7 @@ const compact = (k) => ({ leading: k.functions[0]?.function ?? null, functions:
180
181
 
181
182
  // 1. The idea map: Jev tags each idea with Naive's conventions; without Jev, the agent does, with the
182
183
  // same vocabulary. Either way the ideas stay verbatim and unknown tags are dropped.
183
- const ideas = split(readFileSync(join(dir, DUMP), "utf8"));
184
+ const ideas = split(readFileSync(ventureFile(dir, DUMP), "utf8"));
184
185
  const venture = JSON.parse(readFileSync(join(dir, "venture.json"), "utf8"));
185
186
  let tags, tagger;
186
187
  const mapNode = { name: "idea map", run_type: "tool", start: iso(), inputs: { ideas: ideas.length }, children: [] };
@@ -200,24 +201,24 @@ Ideas: ${JSON.stringify(Object.fromEntries(ideas.map((i) => [i.id, i.section ? `
200
201
  tags = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
201
202
  tagger = "claude-code";
202
203
  }
203
- writeFileSync(join(dir, MAP), render(ideas, clean(tags), { venture: id, tagger }));
204
+ writeFileSync(ventureFile(dir, MAP), render(ideas, clean(tags), { venture: id, tagger }));
204
205
  prov(dir, { entity: MAP, route: "purpose/idea-map", activity: "create", derived_from: [DUMP], agent: agentFromEnv("agent", "harness", { id: tagger }), note: `${ideas.length} ideas tagged with Naive's conventions` });
205
- const sum = summary(parse(readFileSync(join(dir, MAP), "utf8")), phase);
206
+ const sum = summary(parse(readFileSync(ventureFile(dir, MAP), "utf8")), phase);
206
207
  trace.children.push(Object.assign(mapNode, { end: iso(), outputs: { tagger, summary: sum }, metadata: { tagger } }));
207
208
  lap(`idea map: ${sum.ideas} ideas; ${Object.entries(sum.by_pillar).map(([k, v]) => `${k} ${v}`).join(", ")}; clusters ${sum.strengths.map((s) => s.department).join(", ")}; ${sum.product.reading}`);
208
209
 
209
210
  // 1b. The idea graph: the map's structured form. Jev judges which ideas build on which; without Jev,
210
211
  // Naive's record order does. Signal, lanes and layout follow in code.
211
212
  const graphNode = { name: "idea graph", run_type: "tool", start: iso(), inputs: { ideas: ideas.length }, children: [] };
212
- const mapIdeas = parse(readFileSync(join(dir, MAP), "utf8")).ideas;
213
+ const mapIdeas = parse(readFileSync(ventureFile(dir, MAP), "utf8")).ideas;
213
214
  let graphAnswers = null;
214
215
  if (jev) {
215
216
  const p = spawnSync("node", [join(repo, "platform/integrations/jev.mjs")], { input: JSON.stringify(graphRequest(mapIdeas, venture)), encoding: "utf8", maxBuffer: 1 << 26 });
216
217
  if (p.status === 0) { const out = JSON.parse(p.stdout); graphAnswers = out.answers; console.log(` Jev answered ${Object.keys(out.answers).length} dependency and concreteness questions in ${out.ms} ms`); }
217
218
  else console.log(` Jev could not judge the dependencies (${p.stderr.trim().slice(0, 200)}); using Naive's record order`);
218
219
  }
219
- const g = buildGraph(mapIdeas, { answers: graphAnswers, phase, venture: id, source: `${MAP}@${createHash("sha256").update(readFileSync(join(dir, MAP))).digest("hex").slice(0, 16)}` });
220
- writeFileSync(join(dir, GRAPH), JSON.stringify(g, null, 2) + "\n");
220
+ const g = buildGraph(mapIdeas, { answers: graphAnswers, phase, venture: id, source: `${MAP}@${createHash("sha256").update(readFileSync(ventureFile(dir, MAP))).digest("hex").slice(0, 16)}` });
221
+ writeFileSync(ventureFile(dir, GRAPH), JSON.stringify(g, null, 2) + "\n");
221
222
  prov(dir, { entity: GRAPH, route: "purpose/idea-graph", activity: "create", derived_from: [MAP], agent: agentFromEnv("agent", "harness", { id: g.judged_by }), note: `Merkle root ${g.root}` });
222
223
  trace.children.push(Object.assign(graphNode, { end: iso(), outputs: { root: g.root, edges: g.edges.length, lanes: g.lanes.map((l) => ({ mode: l.mode, ideas: l.ideas.length })), next: g.next } }));
223
224
  lap(`idea graph: ${g.edges.length} dependencies (${g.judged_by}); ${g.lanes.filter((l) => l.mode === "sync").length} lane(s) in order, ${g.lanes.filter((l) => l.mode === "async").length} on their own; develop next: ${g.next.join(", ")}; root ${g.root}`);
@@ -17,6 +17,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
17
17
  import { randomUUID } from "node:crypto";
18
18
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
19
19
  import { join } from "node:path";
20
+ import { ventureFile } from "../schema/venture-paths.mjs";
20
21
 
21
22
  export const EVENTS = ".naive/events.jsonl";
22
23
  const listeners = new Set();
@@ -29,7 +30,7 @@ export const tokens = (x) => Math.ceil((x === undefined ? 0 : typeof x === "stri
29
30
  export function register(fn) { listeners.add(fn); return () => listeners.delete(fn); }
30
31
 
31
32
  const configOf = (dir) => {
32
- const f = dir && join(dir, "naive.config.json");
33
+ const f = dir && join(ventureFile(dir, "naive.config.json"));
33
34
  if (!f || !existsSync(f)) return {};
34
35
  try { return JSON.parse(readFileSync(f, "utf8")); } catch { return {}; }
35
36
  };
@@ -48,14 +49,14 @@ export function emit(dir, event) {
48
49
  for (const k of Object.keys(e)) if (e[k] === undefined) delete e[k];
49
50
  for (const fn of listeners) { try { fn(e, dir); } catch {} }
50
51
  if (dir && enabled(dir)) {
51
- try { mkdirSync(join(dir, ".naive"), { recursive: true }); appendFileSync(join(dir, EVENTS), JSON.stringify(e) + "\n"); } catch {}
52
+ try { mkdirSync(join(dir, ".naive"), { recursive: true }); appendFileSync(ventureFile(dir, EVENTS), JSON.stringify(e) + "\n"); } catch {}
52
53
  }
53
54
  return e;
54
55
  }
55
56
 
56
57
  /** The venture's events, oldest first. */
57
58
  export function events(dir) {
58
- const f = join(dir, EVENTS);
59
+ const f = ventureFile(dir, EVENTS);
59
60
  if (!existsSync(f)) return [];
60
61
  return readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).flatMap((l) => { try { return [JSON.parse(l)]; } catch { return []; } });
61
62
  }
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { existsSync, readFileSync, readdirSync } from "node:fs";
21
21
  import { join } from "node:path";
22
+ import { ventureFile } from "../schema/venture-paths.mjs";
22
23
 
23
24
  const repo = new URL("..", import.meta.url).pathname;
24
25
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
@@ -33,7 +34,7 @@ export const packageDef = (n) => load(join(repo, "packages", n, "package.json"))
33
34
 
34
35
  /** The venture's route groups: the framework's, then its own from naive.config.json. */
35
36
  export function groupsOf(dir) {
36
- const f = dir && join(dir, "naive.config.json");
37
+ const f = dir && join(ventureFile(dir, "naive.config.json"));
37
38
  const own = f && existsSync(f) ? load(f).groups ?? {} : {};
38
39
  return { ...GROUPS, ...own };
39
40
  }
@@ -78,7 +79,7 @@ export const routeOfType = async (type) => {
78
79
  async function pendingIn(dir, types, venture) {
79
80
  const { recordOf } = await import("../schema/decisions.mjs");
80
81
  const { entries } = await import("./provenance.mjs");
81
- const d = join(dir, "_drafts");
82
+ const d = join(ventureFile(dir, "_drafts"));
82
83
  if (!existsSync(d)) return [];
83
84
  const log = entries(dir);
84
85
  const out = [];
@@ -179,7 +180,7 @@ export async function specialCall({ venture, route, action, workspace }) {
179
180
  const g = gs[sp.id];
180
181
  if (!g) return { ok: false, error: `no route group (${sp.id}): the groups are ${Object.keys(gs).map((x) => `(${x})`).join(", ")}` };
181
182
  const lay = groupLayout(g.packages[0], dir);
182
- if (action === "layout" || !dir) return { ok: true, result: { ...lay, chain: lay.agents && dir ? [{ path: lay.agents, text: readFileSync(join(dir, lay.agents), "utf8") }] : [] } };
183
+ if (action === "layout" || !dir) return { ok: true, result: { ...lay, chain: lay.agents && dir ? [{ path: lay.agents, text: readFileSync(ventureFile(dir, lay.agents), "utf8") }] : [] } };
183
184
  const states = [];
184
185
  for (const n of g.packages.filter((x) => packageNames().includes(x))) states.push(await packageState(dir, n, venture));
185
186
  const res = { group: `(${sp.id})`, context: g.context ?? null, packages: states.map((s) => ({ package: s.package, state: s.state, progress: s.progress ?? null, pending: s.loading?.drafts.length ?? 0 })) };
@@ -9,6 +9,7 @@ import { spawnSync } from "node:child_process";
9
9
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { emit } from "./instrumentation.mjs";
12
+ import { ventureFile } from "../schema/venture-paths.mjs";
12
13
 
13
14
  export const LOG = "provenance.jsonl";
14
15
  const sha = (s) => createHash("sha256").update(s).digest("hex");
@@ -17,7 +18,7 @@ export const lineHash = (prev, entry) => { const { hash, ...rest } = entry; retu
17
18
  export const contentHash = (file) => sha(readFileSync(file));
18
19
 
19
20
  export function entries(dir) {
20
- const f = join(dir, LOG);
21
+ const f = ventureFile(dir, LOG);
21
22
  return existsSync(f) ? readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l)) : [];
22
23
  }
23
24
 
@@ -37,9 +38,9 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
37
38
  const prior = entries(dir);
38
39
  const prev = prior.length ? prior.at(-1).hash : "genesis";
39
40
  const entry = Object.fromEntries(Object.entries({ at: new Date().toISOString(), entity, route, activity, agent,
40
- derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(join(dir, entity)), prev }).filter(([, v]) => v !== undefined));
41
+ derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(ventureFile(dir, entity)), prev }).filter(([, v]) => v !== undefined));
41
42
  entry.hash = lineHash(prev, entry);
42
- appendFileSync(join(dir, LOG), JSON.stringify(entry) + "\n");
43
+ appendFileSync(ventureFile(dir, LOG), JSON.stringify(entry) + "\n");
43
44
  // Context hook: every provenance entry (write, draft, accept, confirm, decision) is an instrumentation event (P-4).
44
45
  emit(dir, { kind: "provenance", activity: entry.activity, entity: entry.entity, route: entry.route, decision: entry.decision, by: entry.agent?.kind, via: entry.agent?.via });
45
46
  fireTriggers(dir);
@@ -52,7 +53,7 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
52
53
  function fireTriggers(dir) {
53
54
  if (process.env.NAIVE_TRIGGERS_ACTIVE) return;
54
55
  let cfg;
55
- try { cfg = JSON.parse(readFileSync(join(dir, "naive.config.json"), "utf8")); } catch { return; }
56
+ try { cfg = JSON.parse(readFileSync(join(ventureFile(dir, "naive.config.json")), "utf8")); } catch { return; }
56
57
  if (!cfg?.triggers?.length && !cfg?.kits?.length) return;
57
58
  // The write has landed; nothing here may undo it or throw past it.
58
59
  try {
@@ -23,6 +23,7 @@
23
23
  import { existsSync, readFileSync, statSync } from "node:fs";
24
24
  import { dirname, join } from "node:path";
25
25
  import { entries } from "./provenance.mjs";
26
+ import { ventureFile } from "../schema/venture-paths.mjs";
26
27
 
27
28
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
28
29
  const get = (o, p) => p.split(".").reduce((x, k) => (x == null ? undefined : x[k]), o);
@@ -45,7 +46,7 @@ export function frontmatter(text) {
45
46
  /** A record's state from the log and the drafts folder (O-2). */
46
47
  export async function stateOf(dir, file) {
47
48
  const { confirmedIn } = await import("../schema/foundation.mjs");
48
- if (existsSync(join(dir, "_drafts", `${file.replace(/\//g, "__")}.draft`))) return "draft";
49
+ if (existsSync(join(ventureFile(dir, "_drafts"), `${file.replace(/\//g, "__")}.draft`))) return "draft";
49
50
  const es = entries(dir).filter((e) => e.entity === file);
50
51
  if (!es.length) return "unlogged";
51
52
  return confirmedIn(dir, file, null) ? "confirmed" : "agent";
@@ -58,7 +59,7 @@ export function defaults(dir, file, id) {
58
59
  const last = es.at(-1);
59
60
  return { id, created: es[0].at, updated: last.at, by: { kind: last.agent?.kind ?? null, ...(last.agent?.id ? { id: last.agent.id } : {}), via: last.agent?.via ?? null }, activity: last.activity, writes: es.length, from: "provenance" };
60
61
  }
61
- const st = existsSync(join(dir, file)) ? statSync(join(dir, file)) : null;
62
+ const st = existsSync(ventureFile(dir, file)) ? statSync(ventureFile(dir, file)) : null;
62
63
  return { id, created: st ? new Date(st.birthtimeMs || st.mtimeMs).toISOString() : null, updated: st ? st.mtime.toISOString() : null, by: null, writes: 0, from: "file", note: "written outside the router: mentura baseline adopts it into the log" };
63
64
  }
64
65
 
@@ -138,7 +139,7 @@ const parseInstructions = (chain) => {
138
139
  export function withParams(layout, { dir, params, file, group }) {
139
140
  const chain = [...(layout.chain ?? [])];
140
141
  const have = new Set(chain.map((c) => c.path));
141
- const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(join(dir, rel))) { chain.splice(at, 0, { path: rel, text: readFileSync(join(dir, rel), "utf8") }); have.add(rel); } };
142
+ const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(ventureFile(dir, rel))) { chain.splice(at, 0, { path: rel, text: readFileSync(ventureFile(dir, rel), "utf8") }); have.add(rel); } };
142
143
  if (group?.agents) add(group.agents, chain.findIndex((c) => c.path === "AGENTS.md") + 1);
143
144
  if (file) {
144
145
  const parts = dirname(file).split("/").filter((p) => p && p !== ".");
@@ -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 "../schema/venture-paths.mjs";
11
12
 
12
13
  const esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13
14
  const day = (t) => (t ? String(t).slice(0, 10) : "");
@@ -36,7 +37,7 @@ export function svg(card) {
36
37
 
37
38
  if (import.meta.url === `file://${process.argv[1]}`) {
38
39
  const dir = process.argv[2];
39
- const f = dir && join(dir, ".release/card.json");
40
+ const f = dir && join(ventureFile(dir, ".release/card.json"));
40
41
  if (!f || !existsSync(f)) { console.error("usage: node interface/release-card.mjs <venture dir> (after node schema/build-release.mjs)"); process.exit(2); }
41
42
  process.stdout.write(svg(JSON.parse(readFileSync(f, "utf8"))));
42
43
  }
@@ -65,6 +65,7 @@ import { routable } from "../schema/structure.mjs";
65
65
  import { BRAND_ROUTES, brandCommands, brandLayout } from "../schema/brand.mjs";
66
66
  // Upgrades hook (V-9): a read says when what the record reads changed in its system of record.
67
67
  import { staleOf } from "../schema/stale.mjs";
68
+ import { ventureFile } from "../schema/venture-paths.mjs";
68
69
 
69
70
  export const repo = new URL("..", import.meta.url).pathname;
70
71
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -223,7 +224,7 @@ export function ventures(workspace = defaultWorkspace()) {
223
224
  function decisionRights(dir) {
224
225
  const t = load(join(repo, "governance/venture-template/decision-rights.json"));
225
226
  const classes = new Map(t.classes.map((c) => [c.id, c]));
226
- const own = join(dir, "governance/decision-rights.json");
227
+ const own = join(ventureFile(dir, "governance/decision-rights.json"));
227
228
  if (existsSync(own)) for (const c of load(own).classes) classes.set(c.id, { ...classes.get(c.id), ...c, level: classes.get(c.id)?.level === "escalate" ? "escalate" : c.level });
228
229
  return classes;
229
230
  }
@@ -239,7 +240,7 @@ export function gateOf(r, dir) {
239
240
  // Robots: a team-plan role may list the routes it may read and write (globs over routes).
240
241
  export function allowed(dir, actorId, action, route) {
241
242
  if (!actorId || !dir) return true;
242
- const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
243
+ const plan = [join(ventureFile(dir, "people/team-plan.json")), join(ventureFile(dir, "operations/team-plan.json"))].find((p) => existsSync(p));
243
244
  if (!plan) return true;
244
245
  const role = load(plan).roles.find((x) => x.id === actorId);
245
246
  if (!role?.routes) return true;
@@ -264,11 +265,11 @@ const PACKAGE_ROUTE = {
264
265
  const fileFor = (r, params, dir) => {
265
266
  const cands = (r.collection === "item" ? r.files.map((f) => f.replace("*", params.id)) : r.files).filter((f) => !f.includes("*"));
266
267
  if (dir) {
267
- const hit = cands.find((f) => existsSync(join(dir, f)));
268
+ const hit = cands.find((f) => existsSync(ventureFile(dir, f)));
268
269
  if (hit) return hit;
269
270
  // A new collection item joins the folder the venture already keeps that collection in.
270
271
  if (r.collection === "item") {
271
- const lived = cands.find((f) => { const d = join(dir, dirname(f)); return existsSync(d) && readdirSync(d).some((n) => n.endsWith(".json") || n.endsWith(".md") || n.endsWith(".mdx")); });
272
+ const lived = cands.find((f) => { const d = ventureFile(dir, dirname(f)); return existsSync(d) && readdirSync(d).some((n) => n.endsWith(".json") || n.endsWith(".md") || n.endsWith(".mdx")); });
272
273
  if (lived) return lived;
273
274
  }
274
275
  }
@@ -289,7 +290,7 @@ export function draftOf(route, venture, workspace) {
289
290
  function agentsChain(dir, route) {
290
291
  if (!dir) return [];
291
292
  const chain = [];
292
- const add = (rel) => { const f = join(dir, rel); if (existsSync(f)) chain.push({ path: rel, text: readFileSync(f, "utf8") }); };
293
+ const add = (rel) => { const f = ventureFile(dir, rel); if (existsSync(f)) chain.push({ path: rel, text: readFileSync(f, "utf8") }); };
293
294
  add("AGENTS.md");
294
295
  const parts = route.split("/").filter(Boolean);
295
296
  let acc = "";
@@ -392,7 +393,7 @@ async function callInAction({ venture, route, action, input, actor = "agent", ac
392
393
  if (r.internal === "robots") return { ok: true, result: robots(dir), ...deprecated };
393
394
  // Caching hook: system/cache, system/judge and system/answer are handled in schema/caching.mjs.
394
395
  if (["cache", "judge", "answer"].includes(r.internal)) return { ...(await cacheInternal(r.internal, { dir, venture, workspace, input, actor, actor_id, via, provenance }, { call, resolve, draftOf })), ...deprecated };
395
- const lacking = (r.needs ?? []).filter((f) => !existsSync(join(dir, f)));
396
+ const lacking = (r.needs ?? []).filter((f) => !existsSync(ventureFile(dir, f)));
396
397
  if (lacking.length) return { ok: false, error: `${route} needs ${lacking.join(", ")} in ${venture}`, fix: { missing: lacking } };
397
398
  if (r.value_flag && !input?.value) return { ok: false, error: `${route} needs a value (${r.note})` };
398
399
  const args = r.venture_arg === "id" ? [dir, ...(input?.value ? [String(input.value)] : []), "--json"] : r.value_flag ? [dir, r.value_flag, String(input.value), "--json"] : [dir, ...(r.args ?? []), ...(r.as_is && input?.as_is ? ["--as-is"] : []), "--json"];
@@ -430,7 +431,7 @@ async function callInAction({ venture, route, action, input, actor = "agent", ac
430
431
  if (r.collection === "list" && actor_id) items = items.filter((x) => mayReadItem(dir, actor_id, route, itemId(r.files, x.path)));
431
432
  } catch (e) { return { ok: false, error: `${src.id}: ${String(e.message ?? e)}` }; }
432
433
  const found = items.map((x) => x.path);
433
- const draft = !remote && r.collection !== "list" && fileFor(r, params, dir) && existsSync(join(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
434
+ const draft = !remote && r.collection !== "list" && fileFor(r, params, dir) && existsSync(ventureFile(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
434
435
  const loading = draft ? { pending_draft: { file: draft, since: provEntries(dir).filter((e) => e.entity === draft).at(-1)?.at ?? null, accept: `mentura accept ${route} --venture ${venture}` } } : {};
435
436
  if (remote) {
436
437
  const prs = provEntries(dir).filter((e) => e.source?.pull_request && found.concat(patterns).some((f) => e.entity === address(src.id, f)));
@@ -493,7 +494,7 @@ async function callInAction({ venture, route, action, input, actor = "agent", ac
493
494
  if (!input || typeof input !== "object") return { ok: false, error: "write needs the record as input" };
494
495
  if (r.collection === "item" && !/^[a-z0-9][a-z0-9-]*$/.test(params.id)) return { ok: false, error: `${params.id} is not a valid id (lowercase letters, digits and hyphens)` };
495
496
  const file = fileFor(r, params, dir);
496
- if (["idea-dump", "raw-source", "raw-text"].includes(r.record) && existsSync(join(dir, file))) return { ok: false, error: `${route} is raw and immutable: add a new capture, do not edit this one` };
497
+ if (["idea-dump", "raw-source", "raw-text"].includes(r.record) && existsSync(ventureFile(dir, file))) return { ok: false, error: `${route} is raw and immutable: add a new capture, do not edit this one` };
497
498
  const gate = gateOf(r, dir);
498
499
  const text = JSON.stringify(input, null, 2) + "\n";
499
500
  // The schema first, on a scratch copy, so nothing reaches any source unless it matches.
@@ -619,7 +620,7 @@ async function overview(venture, workspace) {
619
620
 
620
621
  // Robots: per role, the routes it may read and write.
621
622
  function robots(dir) {
622
- const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
623
+ const plan = [join(ventureFile(dir, "people/team-plan.json")), join(ventureFile(dir, "operations/team-plan.json"))].find((p) => existsSync(p));
623
624
  if (!plan) return { note: "no team plan: every actor may reach every route; gates still apply", roles: [] };
624
625
  return { note: "a role without routes may reach every route; gates still apply", roles: load(plan).roles.map((r) => ({ role: r.id, occupant: r.occupant, read: (r.routes?.read ?? ["**"]).join(", "), write: (r.routes?.write ?? (r.routes ? [] : ["**"])).join(", ") || "none" })) };
625
626
  }
@@ -21,6 +21,7 @@ import { existsSync, readFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
22
  import { events } from "./instrumentation.mjs";
23
23
  import { entries } from "./provenance.mjs";
24
+ import { ventureFile } from "../schema/venture-paths.mjs";
24
25
 
25
26
  export const THRESHOLDS = { calls_per_task: 25, tokens_per_call: 6000, time_to_useful_ms: 10000, decision_hours: 72 };
26
27
  const DECISION = /(^|\/)decisions\/[^/]+\.decision\.json$/;
@@ -28,7 +29,7 @@ const DECISION = /(^|\/)decisions\/[^/]+\.decision\.json$/;
28
29
  /** The 75th percentile, nearest rank. */
29
30
  export const p75 = (xs) => { if (!xs.length) return null; const s = [...xs].sort((a, b) => a - b); return s[Math.ceil(0.75 * s.length) - 1]; };
30
31
  const startOf = (e) => Date.parse(e.at) - (e.ms ?? 0);
31
- const config = (dir) => { const f = join(dir, "naive.config.json"); try { return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : {}; } catch { return {}; } };
32
+ const config = (dir) => { const f = join(ventureFile(dir, "naive.config.json")); try { return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : {}; } catch { return {}; } };
32
33
 
33
34
  /** Top-level calls grouped by session: one task per session. */
34
35
  export function tasks(dir) {
@@ -55,7 +56,7 @@ export function decisionLatency(dir, now = Date.now()) {
55
56
  }
56
57
  return [...by.values()].map((d) => {
57
58
  let id = d.file.split("/").at(-1).replace(/\.decision\.json$/, "");
58
- try { id = JSON.parse(readFileSync(join(dir, d.file), "utf8")).id ?? id; } catch {}
59
+ try { id = JSON.parse(readFileSync(ventureFile(dir, d.file), "utf8")).id ?? id; } catch {}
59
60
  const end = d.decided ? Date.parse(d.decided) : now;
60
61
  return { id, ...d, open: !d.decided, hours: Math.round(((end - Date.parse(d.opened)) / 36e5) * 100) / 100 };
61
62
  });
@@ -15,6 +15,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { address, isText, words } from "./contract.mjs";
17
17
  import { connectAll } from "./index.mjs";
18
+ import { ventureFile } from "../../schema/venture-paths.mjs";
18
19
 
19
20
  export const FILE = "_catalog/catalog.json";
20
21
  const MAX_BYTES = 1_000_000;
@@ -50,7 +51,7 @@ async function routeOf(path) {
50
51
  return { route: null, record: null };
51
52
  }
52
53
 
53
- export const load = (dir) => existsSync(join(dir, FILE)) ? JSON.parse(readFileSync(join(dir, FILE), "utf8")) : null;
54
+ export const load = (dir) => existsSync(ventureFile(dir, FILE)) ? JSON.parse(readFileSync(ventureFile(dir, FILE), "utf8")) : null;
54
55
 
55
56
  /** Builds or refreshes the catalog. Returns it with stats: items, read (items fetched), reused. */
56
57
  export async function build(dir, { write = true, sources } = {}) {
@@ -75,7 +76,7 @@ export async function build(dir, { write = true, sources } = {}) {
75
76
  }
76
77
  stats.items = items.length;
77
78
  const catalog = { built_at: new Date().toISOString(), sources: conns.map(({ source }) => ({ id: source.id, connector: source.connector })), items };
78
- if (write) { mkdirSync(join(dir, "_catalog"), { recursive: true }); writeFileSync(join(dir, FILE), JSON.stringify(catalog) + "\n"); }
79
+ if (write) { mkdirSync(join(dir, "_catalog"), { recursive: true }); writeFileSync(ventureFile(dir, FILE), JSON.stringify(catalog) + "\n"); }
79
80
  return { catalog, stats };
80
81
  }
81
82
 
@@ -13,6 +13,7 @@ import { spawnSync } from "node:child_process";
13
13
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
15
  import { blobSha, isText, matches, safePath } from "./contract.mjs";
16
+ import { layoutOf, logicalFiles, ventureFile } from "../../schema/venture-paths.mjs";
16
17
 
17
18
  const SKIP = new Set(["node_modules"]);
18
19
 
@@ -29,7 +30,11 @@ export function connect({ id = "venture", root, writes = "direct", venture = fal
29
30
  }
30
31
  return e.isFile() ? [r] : [];
31
32
  });
32
- const file = (p) => join(root, safePath(p));
33
+ // A venture in the structured layout (EE-086) keeps its records in knowledge/ and meta/: read and write them
34
+ // by their logical paths, and list them that way, so routes never see the layout.
35
+ const structured = venture && layoutOf(root) === "structured";
36
+ const file = (p) => (structured ? ventureFile(root, safePath(p)) : join(root, safePath(p)));
37
+ const listed = () => structured ? logicalFiles(root).filter((r) => !r.split("/").some((s) => s.startsWith(".")) && !r.startsWith("_") && !r.startsWith("node_modules/")) : walk(root);
33
38
  const git = (...args) => spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
34
39
  const lastCommit = (p, content) => {
35
40
  const tracked = git("ls-files", "-s", "--", p);
@@ -42,8 +47,8 @@ export function connect({ id = "venture", root, writes = "direct", venture = fal
42
47
  id, kind: "filesystem", writes, root,
43
48
  async list(prefix = "") {
44
49
  const pre = prefix ? safePath(prefix) : "";
45
- return walk(root).filter((p) => !pre || p === pre || p.startsWith(`${pre}/`)).sort()
46
- .map((path) => ({ path, version: blobSha(readFileSync(join(root, path))) }));
50
+ return listed().filter((p) => !pre || p === pre || p.startsWith(`${pre}/`)).sort()
51
+ .map((path) => ({ path, version: blobSha(readFileSync(file(path))) }));
47
52
  },
48
53
  async read(p) {
49
54
  const f = file(p);
@@ -58,9 +63,9 @@ export function connect({ id = "venture", root, writes = "direct", venture = fal
58
63
  },
59
64
  async search(query, { limit = 50 } = {}) {
60
65
  const out = [];
61
- for (const path of walk(root).sort()) {
66
+ for (const path of listed().sort()) {
62
67
  if (out.length >= limit) break;
63
- const content = readFileSync(join(root, path), "utf8");
68
+ const content = readFileSync(file(path), "utf8");
64
69
  if (isText(path, content) && matches(`${path}\n${content}`, query)) out.push({ path, version: blobSha(content) });
65
70
  }
66
71
  return out;
@@ -17,6 +17,7 @@ import { existsSync, readFileSync } from "node:fs";
17
17
  import { isAbsolute, join, resolve } from "node:path";
18
18
  import { conformance } from "./contract.mjs";
19
19
  import { ANY, resolve as resolveRule } from "./resolve.mjs";
20
+ import { ventureFile } from "../../schema/venture-paths.mjs";
20
21
 
21
22
  const repo = new URL("../..", import.meta.url).pathname;
22
23
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -34,8 +35,8 @@ export function registry() {
34
35
 
35
36
  /** A venture's source map, with the implicit venture source first. */
36
37
  export function sourcesOf(dir) {
37
- const own = existsSync(join(dir, MAP)) ? load(join(dir, MAP)) : { sources: [], records: {} };
38
- return { sources: [{ id: LOCAL, connector: "filesystem", root: ".", writes: "direct", implicit: true }, ...(own.sources ?? [])], records: own.records ?? {}, rules: own.rules ?? [], mapped: existsSync(join(dir, MAP)) };
38
+ const own = existsSync(ventureFile(dir, MAP)) ? load(ventureFile(dir, MAP)) : { sources: [], records: {} };
39
+ return { sources: [{ id: LOCAL, connector: "filesystem", root: ".", writes: "direct", implicit: true }, ...(own.sources ?? [])], records: own.records ?? {}, rules: own.rules ?? [], mapped: existsSync(ventureFile(dir, MAP)) };
39
40
  }
40
41
 
41
42
  /**
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
12
  import { join, relative } from "node:path";
13
+ import { ventureFile } from "../../schema/venture-paths.mjs";
13
14
 
14
15
  export const CONTENT = "operations/marketing/content";
15
16
  const RUN = 10;
@@ -27,24 +28,24 @@ export function frontmatter(text) {
27
28
 
28
29
  function files(dir) {
29
30
  if (!existsSync(dir)) return [];
30
- return readdirSync(dir).flatMap((f) => statSync(join(dir, f)).isDirectory() ? files(join(dir, f)) : f.endsWith(".mdx") ? [join(dir, f)] : []);
31
+ return readdirSync(dir).flatMap((f) => statSync(ventureFile(dir, f)).isDirectory() ? files(ventureFile(dir, f)) : f.endsWith(".mdx") ? [ventureFile(dir, f)] : []);
31
32
  }
32
33
 
33
34
  export function check(dir) {
34
35
  const venture = existsSync(join(dir, "venture.json")) ? JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")) : {};
35
36
  const publicText = [venture.name, venture.brand?.tagline].filter(Boolean);
36
- const pdir = join(dir, "products");
37
+ const pdir = join(ventureFile(dir, "products"));
37
38
  if (existsSync(pdir)) for (const f of readdirSync(pdir).filter((f) => f.endsWith(".json"))) publicText.push(JSON.parse(readFileSync(join(pdir, f), "utf8")).name ?? "");
38
39
  const allowed = new Set(publicText.flatMap((t) => [...runs(t)]));
39
- const results = files(join(dir, CONTENT)).map((f) => {
40
+ const results = files(ventureFile(dir, CONTENT)).map((f) => {
40
41
  const rel = relative(dir, f), { meta, body } = frontmatter(readFileSync(f, "utf8")), errors = [];
41
42
  if (!meta.variant_of) errors.push("names no internal record (variant_of): marketing content is a variant of an internal record");
42
- else if (!existsSync(join(dir, meta.variant_of))) errors.push(`variant_of ${meta.variant_of} does not exist`);
43
+ else if (!existsSync(ventureFile(dir, meta.variant_of))) errors.push(`variant_of ${meta.variant_of} does not exist`);
43
44
  if (!meta.audience) errors.push("names no audience");
44
45
  if (meta.publish === undefined) errors.push("does not say whether it is published (publish: true or false)");
45
46
  if (meta.publish === true && !meta.approved_by) errors.push("is published without a person's approval (approved_by, EE-025)");
46
- if (meta.variant_of && existsSync(join(dir, meta.variant_of))) {
47
- const src = join(dir, meta.variant_of);
47
+ if (meta.variant_of && existsSync(ventureFile(dir, meta.variant_of))) {
48
+ const src = ventureFile(dir, meta.variant_of);
48
49
  const raw = readFileSync(src, "utf8");
49
50
  const internal = new Set(strings(src.endsWith(".json") ? JSON.parse(raw) : raw).flatMap((s) => [...runs(s)]));
50
51
  const leaks = [...runs(body)].filter((r) => internal.has(r) && !allowed.has(r));
@@ -19,6 +19,7 @@
19
19
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
20
20
  import { dirname, join } from "node:path";
21
21
  import { customerNeeds, factsOf, MATRIX, needs, typesOf, weights } from "./presence.mjs";
22
+ import { ventureFile } from "../../../schema/venture-paths.mjs";
22
23
 
23
24
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
24
25
  export const SITEMAP = "operations/marketing/sitemap.json";
@@ -33,10 +34,10 @@ const RECORD_FILE = { venture: "venture.json", icp: "purpose/icp.json", "validat
33
34
 
34
35
  /** The offerings: each product, what kind of offering it is, whom it is offered to and whom it serves through them. */
35
36
  export function offeringsOf(dir) {
36
- const icp = existsSync(join(dir, "purpose/icp.json")) ? load(join(dir, "purpose/icp.json")) : { profiles: [] };
37
+ const icp = existsSync(join(ventureFile(dir, "purpose/icp.json"))) ? load(join(ventureFile(dir, "purpose/icp.json"))) : { profiles: [] };
37
38
  const profiles = new Map(icp.profiles.map((p) => [p.id, p]));
38
39
  const primary = icp.profiles.find((p) => p.primary) ?? icp.profiles[0];
39
- const pdir = join(dir, "products");
40
+ const pdir = join(ventureFile(dir, "products"));
40
41
  const products = existsSync(pdir) ? readdirSync(pdir).filter((f) => f.endsWith(".json")).map((f) => load(join(pdir, f))).filter((p) => !["retired", "archived", "idea"].includes(p.status)) : [];
41
42
  return { profiles, products: products.map((p) => {
42
43
  // Who it is offered to: the product says so; otherwise the profiles others are reached through
@@ -104,7 +105,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
104
105
  if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node plan.mjs <venture> [--raising] [--hiring] [--contributors] [--write] [--json]"); process.exit(2); }
105
106
  const has = (n) => process.argv.includes(`--${n}`);
106
107
  const r = plan(dir, { flags: Object.fromEntries(["raising", "hiring", "contributors"].filter(has).map((k) => [k, true])) });
107
- if (has("write")) { mkdirSync(join(dir, dirname(SITEMAP)), { recursive: true }); writeFileSync(join(dir, SITEMAP), JSON.stringify(r, null, 2) + "\n"); }
108
+ if (has("write")) { mkdirSync(join(dir, dirname(SITEMAP)), { recursive: true }); writeFileSync(ventureFile(dir, SITEMAP), JSON.stringify(r, null, 2) + "\n"); }
108
109
  if (has("json")) console.log(JSON.stringify(r, null, 2));
109
110
  else {
110
111
  console.log(`${r.name}: site planned for ${r.audience.offered_to.map((p) => p.id).join(", ") || "no ICP yet"} (${r.audience.types.map((t) => t.label).join(", ") || "customers in general"})${r.audience.partner_led ? "; partner-led" : ""}`);
@@ -22,6 +22,7 @@
22
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { dirname, join } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { ventureFile } from "../../../schema/venture-paths.mjs";
25
26
 
26
27
  const here = dirname(fileURLToPath(import.meta.url));
27
28
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -34,7 +35,7 @@ const r2 = (x) => Math.round(x * 100) / 100;
34
35
  /** The venture's facts: flags the founder gave, else what its ICP record says about who it courts. */
35
36
  export function factsOf(dir, flags = {}) {
36
37
  const f = { raising: false, hiring: false, contributors: false };
37
- const icpFile = dir && join(dir, "purpose/icp.json");
38
+ const icpFile = dir && join(ventureFile(dir, "purpose/icp.json"));
38
39
  if (icpFile && existsSync(icpFile)) {
39
40
  const said = JSON.stringify(load(icpFile)).toLowerCase();
40
41
  if (/investor|angel|\bvc\b/.test(said)) f.raising = true;
@@ -57,7 +58,7 @@ export function icpOf(dir, site) {
57
58
  const who = ok.filter((p) => /customer|solution|who|industr|use-case|about|for-/i.test(p.path));
58
59
  const says = [...new Set([home, ...who].filter(Boolean).flatMap((p) => (p.text ?? "").match(SAYS) ?? []).map((x) => x.trim()))].slice(0, 8);
59
60
  const opening = (home?.text ?? "").slice(0, 500);
60
- const icpFile = dir && join(dir, "purpose/icp.json");
61
+ const icpFile = dir && join(ventureFile(dir, "purpose/icp.json"));
61
62
  const stated = icpFile && existsSync(icpFile) ? (load(icpFile).profiles ?? []).map((p) => p.who ?? p.description ?? p.name).filter(Boolean) : null;
62
63
  return { text: [says.join(" | "), opening].filter(Boolean).join(" || "), source: says.length ? "site" : "inferred", says,
63
64
  cite: says.length ? `what the site says about who it is for (${says.length} phrase${says.length > 1 ? "s" : ""}), with the home page's opening` : "the home page's opening text (inferred: the site does not say outright who it is for)",
@@ -237,7 +238,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
237
238
  const answers = opt("answers") ? (load(opt("answers")).answers ?? load(opt("answers"))) : null;
238
239
  const facts = factsOf(dir, Object.fromEntries(["raising", "hiring", "contributors"].filter(has).map((k) => [k, true])));
239
240
  const r = score(site, { answers, facts, icp: icpOf(dir, site), venture: dir ? load(join(dir, "venture.json")).id : null });
240
- if (dir) { mkdirSync(join(dir, dirname(PRESENCE)), { recursive: true }); writeFileSync(join(dir, PRESENCE), JSON.stringify(r, null, 2) + "\n"); }
241
+ if (dir) { mkdirSync(join(dir, dirname(PRESENCE)), { recursive: true }); writeFileSync(ventureFile(dir, PRESENCE), JSON.stringify(r, null, 2) + "\n"); }
241
242
  console.log(JSON.stringify(r, null, 2));
242
243
  } else if (a && existsSync(join(a, "venture.json"))) {
243
244
  if (!existsSync(join(a, PRESENCE))) { console.error(`${PRESENCE} does not exist yet: read the site first (node interface/harness/site.mjs <url> --venture <dir>)`); process.exit(1); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naive-framework",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Naive, a venture development framework: conventions, routes, records, checks and an agent interface for building a venture.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { join } from "node:path";
24
+ import { ventureFile } from "../../schema/venture-paths.mjs";
24
25
 
25
26
  export const SANDBOX = "https://rhoapi-sandbox.rho.co/mcp/v1";
26
27
  export const LIVE = "https://rhoapi.rho.co/mcp/v1";
@@ -107,7 +108,7 @@ export function summarize(data, syncedAt) {
107
108
  const json = (f, v) => { mkdirSync(join(f, ".."), { recursive: true }); writeFileSync(f, `${JSON.stringify(v, null, 2)}\n`); };
108
109
 
109
110
  export function write(venture, data, syncedAt = new Date().toISOString()) {
110
- const money = join(venture, "money");
111
+ const money = join(ventureFile(venture, "money"));
111
112
  const pointer = (kind, id) => `${data.source}:${kind}:${id}`;
112
113
  for (const a of data.accounts)
113
114
  json(join(money, `${a.id}.json`), { id: a.id, name: a.account_name, type: a.account_type, last_4: a.account_number_last_4 ?? null, balance: a.balance, source: pointer("account", a.id), synced_at: syncedAt });