naive-framework 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/AGENTS.md +1 -1
  2. package/docs/decisions/decisions.json +15 -0
  3. package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
  4. package/interface/adapters.mjs +7 -6
  5. package/interface/boundaries.mjs +5 -4
  6. package/interface/brand.mjs +6 -5
  7. package/interface/cli/mentura.mjs +4 -3
  8. package/interface/cli/naive.mjs +11 -6
  9. package/interface/context.mjs +7 -6
  10. package/interface/demo/board.mjs +9 -8
  11. package/interface/demo/decision-harness.mjs +4 -3
  12. package/interface/gate-hooks.mjs +2 -1
  13. package/interface/harness/evals.mjs +8 -7
  14. package/interface/harness/run.mjs +14 -13
  15. package/interface/instrumentation.mjs +4 -3
  16. package/interface/layer.json +4 -0
  17. package/interface/package-routes.mjs +4 -3
  18. package/interface/provenance.mjs +5 -4
  19. package/interface/reads.mjs +4 -3
  20. package/interface/release-card.mjs +2 -1
  21. package/interface/router.mjs +10 -9
  22. package/interface/skills/choosing-artifacts/SKILL.md +113 -0
  23. package/interface/vitals.mjs +3 -2
  24. package/knowledge/connectors/catalog.mjs +3 -2
  25. package/knowledge/connectors/filesystem.mjs +10 -5
  26. package/knowledge/connectors/index.mjs +3 -2
  27. package/operations/marketing/check-public.mjs +7 -6
  28. package/operations/marketing/site/plan.mjs +4 -3
  29. package/operations/marketing/site/presence.mjs +4 -3
  30. package/package.json +1 -1
  31. package/platform/integrations/rho.mjs +2 -1
  32. package/purpose/frameworks/idea-map/route.mjs +5 -4
  33. package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
  34. package/schema/access.mjs +2 -1
  35. package/schema/add-department.mjs +3 -2
  36. package/schema/assess-problem.mjs +3 -2
  37. package/schema/assets.mjs +6 -5
  38. package/schema/brand.mjs +11 -10
  39. package/schema/caching.mjs +5 -4
  40. package/schema/check-config.mjs +4 -3
  41. package/schema/check-data-model.mjs +2 -1
  42. package/schema/check-data-packages.mjs +6 -5
  43. package/schema/check-execution.mjs +4 -3
  44. package/schema/check-people.mjs +22 -8
  45. package/schema/check-private.mjs +2 -1
  46. package/schema/check-ready.mjs +6 -5
  47. package/schema/check-sources.mjs +2 -1
  48. package/schema/check-where-used.mjs +4 -3
  49. package/schema/decision-analytics.mjs +2 -1
  50. package/schema/decisions.mjs +14 -11
  51. package/schema/foundation.mjs +8 -7
  52. package/schema/founder.schema.json +2 -2
  53. package/schema/frames.mjs +4 -3
  54. package/schema/freshness.mjs +13 -12
  55. package/schema/gate-drafts.mjs +8 -7
  56. package/schema/init-venture.mjs +48 -20
  57. package/schema/migrate-venture.mjs +8 -7
  58. package/schema/migrations/kit-to-package.mjs +4 -3
  59. package/schema/migrations/package-layout.mjs +9 -5
  60. package/schema/model-steps.mjs +8 -7
  61. package/schema/naive-config.mjs +4 -3
  62. package/schema/need-now.mjs +5 -4
  63. package/schema/packages.mjs +11 -10
  64. package/schema/payouts.mjs +3 -2
  65. package/schema/people.schema.json +2 -3
  66. package/schema/port-venture.mjs +206 -132
  67. package/schema/release-read.mjs +5 -4
  68. package/schema/releases.mjs +21 -20
  69. package/schema/run-handoff.mjs +5 -4
  70. package/schema/setup.mjs +5 -4
  71. package/schema/stale.mjs +6 -5
  72. package/schema/structure.mjs +5 -4
  73. package/schema/time-to-ready.mjs +7 -6
  74. package/schema/triggers.mjs +7 -6
  75. package/schema/upgrades.mjs +31 -30
  76. package/schema/venture-paths.mjs +97 -0
  77. package/schema/venture.schema.json +25 -17
  78. package/schema/workspace.schema.json +5 -1
@@ -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
  }
@@ -10,6 +10,10 @@
10
10
  {
11
11
  "id": "naive-setup",
12
12
  "path": "interface/skills/naive-setup/SKILL.md"
13
+ },
14
+ {
15
+ "id": "choosing-artifacts",
16
+ "path": "interface/skills/choosing-artifacts/SKILL.md"
13
17
  }
14
18
  ],
15
19
  "records": [
@@ -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
  }
@@ -0,0 +1,113 @@
1
+ ---
2
+ type: knowledge
3
+ tags: [everything-ecosystem, naive, skills, interface, artifacts, records, views]
4
+ ai-model: claude-opus-5.5
5
+ created: 2026-10-29T20:12:08Z
6
+ source: hybrid
7
+ name: choosing-artifacts
8
+ description: >
9
+ Decide whether a venture action should build an artifact (a rendered view a
10
+ person reads, presents, shares or fills in) and which kind, and keep records,
11
+ the source of truth in k-base/, separate from artifacts, which are views built
12
+ from records and cite them. General for any agent, with a section for Claude's
13
+ own artifacts in Claude Code and claude.ai.
14
+ MANDATORY TRIGGERS: make an artifact, build a page, make a deck, make a
15
+ dashboard, pitch deck, investor update, show me, visualize, one-pager, which
16
+ artifact, should this be a doc, render this record.
17
+ ---
18
+
19
+ # Choosing artifacts
20
+
21
+ Interface layer. An artifact is a rendered view; a record is the source of truth. Build an artifact from records when a person needs to look at, present, share or fill in something. Never let the artifact become the record.
22
+
23
+ ## The core rule: records are not artifacts
24
+
25
+ - A **record** is a file in the venture's `k-base/` with a schema. It is what agents read, what decisions cite and what `naive check` tests.
26
+ - An **artifact** is a page, deck, dashboard or file made from one or more records for a reader. A marketing site describes the company, but the company's facts live in its records, not on the site.
27
+ - Every artifact names the records it was built from (route and revision or date), so a reader can trace each claim.
28
+ - When a record changes, rebuild the artifact. Never hand-edit an artifact to change a fact.
29
+ - What people type into an artifact (a form, a docket, comments) is input. Write it back through the router as a proposal or a draft in `_drafts/`; a person settles it. The artifact's own stored state is never the source of truth.
30
+ - Outside a Naive venture the same rule holds: name what the view was made from, and keep the facts where they already live.
31
+
32
+ ## Should this be an artifact at all?
33
+
34
+ Answer in the conversation when the result is short, read once and acted on now. Build an artifact when any of these is true:
35
+
36
+ 1. Someone other than the requester will read or use it.
37
+ 2. It will be presented, printed, exported or sent.
38
+ 3. A person needs to fill it in, decide on it or come back to it.
39
+ 4. It is longer than about fifteen lines, or a picture explains it better than prose.
40
+
41
+ If it belongs in the venture's code (a README, a spec that ships, code docs), write a file in `codebase/` instead.
42
+
43
+ ## Kinds of artifact
44
+
45
+ | Kind | Use it for |
46
+ |---|---|
47
+ | Document | Writing people keep editing or pass around: memo, plan, spec, brief, report, notes |
48
+ | Deck | Something to present or page through: pitch, investor update, demo-day talk |
49
+ | Dashboard | Numbers people read: KPIs, runway, scorecards. Every number shows its source record |
50
+ | Design | Visual work: landing pages, screens, mockups, ads, social posts |
51
+ | Design system | The brand's tokens, type, components and assets, built from the brand record |
52
+ | Page | Explainers, diagrams, interactive tools, trackers, forms |
53
+ | File | Only when a format is named (.pptx, .docx, .xlsx, PDF) or an existing file is to be changed |
54
+
55
+ ## Decide in order
56
+
57
+ 1. Is it an artifact at all? (above)
58
+ 2. Did the person name a format? Use it.
59
+ 3. Writing to edit: document. To present: deck. Data to read: dashboard. A visual: design. A brand: design system.
60
+ 4. Otherwise a page. Add a diagram, charts or interactivity only when each earns its place.
61
+ 5. List the records it reads, and put that source list in the artifact.
62
+ 6. Read the records through Naive (`mentura_read`, or `naive` routes), never from memory. A record that is missing, a draft or stale is shown as such, never filled in.
63
+
64
+ ## Actions and their artifacts
65
+
66
+ Each function's skills say which of its actions build which artifact. This is the starting map; it is a proposal until the founder confirms it in `naive.config.json` or the function's own skill.
67
+
68
+ | Function | Action | Artifact | Built from |
69
+ |---|---|---|---|
70
+ | Foundation | Review the strategic foundation | Document (brief) | foundation/problem, foundation/icp, telos, the phase in venture.json |
71
+ | Foundation | Decide pending decisions | Page that collects answers (the docket) | decisions/; answers go back as decisions a person settles |
72
+ | Validation | Plan or report an experiment | Document | validation records and evidence/ |
73
+ | Validation | Track evidence over time | Dashboard | evidence/ and metric records |
74
+ | Validation | Run interviews | Page that collects answers (intake form) | the interview schema; answers become records |
75
+ | Marketing | Draft a landing page or ad | Design | positioning and marketing/brand |
76
+ | Marketing | Set up the brand | Design system | marketing/brand |
77
+ | Marketing | Plan content | Document | positioning and calendar records |
78
+ | Product | Write a spec or roadmap | Document | product/ and decisions/ |
79
+ | Product | Show a flow or architecture | Page with a diagram | product/ |
80
+ | Product | Prototype a screen | Design | product specs |
81
+ | Money | Show runway, burn, revenue | Dashboard | money/ (from the bank or accounting source in sources.json) |
82
+ | Money | Build a model or budget | File (.xlsx) when asked, else dashboard | money/ and finance/ |
83
+ | Investors | Pitch or investor update | Deck | foundation/, evidence/ (traction) and money/ |
84
+ | Any | Release notes for a release | Document | the release in meta/releases/ |
85
+
86
+ ## Checks before you publish
87
+
88
+ - [ ] The artifact names its source records.
89
+ - [ ] No fact appears that is missing from a record. If one is needed, write the record first (as a draft) or mark the claim as a proposal.
90
+ - [ ] Agent-drafted content is marked as a proposal, never as the founder's words.
91
+ - [ ] Input the artifact collects has a path back to records.
92
+ - [ ] Nothing from `_private/`, no secrets, keys or `.env` contents.
93
+ - [ ] Nothing imitates a real person or organization.
94
+
95
+ ## In Claude (Claude Code and claude.ai)
96
+
97
+ Claude publishes artifacts with its `Artifact` tool. The kinds above map onto the artifact types on the person's account, which change, so always start a new artifact with `Artifact` `action: "quickstart"` and the fitting `intent`; it names the type to start from and the default design system.
98
+
99
+ | Kind | Claude artifact type | Intent |
100
+ |---|---|---|
101
+ | Document | Docs (through the Claude Docs connector) | `document` |
102
+ | Deck | Slides | `slides` |
103
+ | Dashboard | Dashboard | `other` |
104
+ | Design | Design | `design` |
105
+ | Design system | Design System | `other` |
106
+ | Page | plain HTML page, after loading `artifact-design` | `other` |
107
+ | File | the matching file skill (docx, pptx, xlsx, pdf) | none |
108
+
109
+ - **Diagram:** load `artifact-diagramming` when a picture shows the real mechanism.
110
+ - **Charts:** load `dataviz` before drawing any chart; prefer the Dashboard type when the page is mostly data.
111
+ - **Collecting input:** load `artifact-capabilities` and declare the smallest set: `db` and `user` for answers from several people (the docket, an intake form), `artifact` for a page that saves its own versions. Sync what it holds back to records through Naive; never treat the page's store as the record.
112
+ - **Updating:** republish to the same URL when the records change, so the link people hold stays current.
113
+ - Publish privately and give the link. Sharing it further is the founder's call.
@@ -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));