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
package/schema/setup.mjs CHANGED
@@ -13,6 +13,7 @@
13
13
  import { spawnSync } from "node:child_process";
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { basename, dirname, join, resolve } from "node:path";
16
+ import { ventureFile } from "./venture-paths.mjs";
16
17
 
17
18
  const repo = new URL("..", import.meta.url).pathname;
18
19
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
@@ -23,8 +24,8 @@ const GOAL = "discovery-goal";
23
24
  export async function state(dir) {
24
25
  const { decisions } = await import("./decisions.mjs");
25
26
  const v = load(join(dir, "venture.json"));
26
- const people = existsSync(join(dir, "people/people.json")) ? load(join(dir, "people/people.json")) : null;
27
- const sources = existsSync(join(dir, "knowledge/sources.json")) ? load(join(dir, "knowledge/sources.json")) : { sources: [], records: {} };
27
+ const people = existsSync(join(ventureFile(dir, "people/people.json"))) ? load(join(ventureFile(dir, "people/people.json"))) : null;
28
+ const sources = existsSync(join(ventureFile(dir, "knowledge/sources.json"))) ? load(join(ventureFile(dir, "knowledge/sources.json"))) : { sources: [], records: {} };
28
29
  const catalog = load(join(repo, "schema/systems-catalog.json"));
29
30
  const types = load(join(repo, "schema/venture-types.json")).types;
30
31
  const all = decisions(dir).decisions;
@@ -51,7 +52,7 @@ export async function state(dir) {
51
52
  // An open decision whose wording an agent wrote: in setup, the founder's words the model relayed.
52
53
  const relayed = (d) => d.status !== "decided" && !!d.answer && d.provenance?.drafted_by?.kind === "agent";
53
54
 
54
- const founderName = (dir) => existsSync(join(dir, "people/people.json")) ? load(join(dir, "people/people.json")).people[0]?.name ?? "Founder" : "Founder";
55
+ const founderName = (dir) => existsSync(join(ventureFile(dir, "people/people.json"))) ? load(join(ventureFile(dir, "people/people.json"))).people[0]?.name ?? "Founder" : "Founder";
55
56
 
56
57
  // One answer: a decision on file is approved; a foundation statement (or the goal) becomes a decision a person approves.
57
58
  export async function decide(dir, { id, answer, via = "api", where = "/setup" }) {
@@ -79,7 +80,7 @@ export async function decide(dir, { id, answer, via = "api", where = "/setup" })
79
80
  export async function point(dir, { fn, system, address, via = "api", actor = "person", agentId }) {
80
81
  const catalog = load(join(repo, "schema/systems-catalog.json"));
81
82
  if (!catalog.functions[fn]?.includes(system)) return { ok: false, error: `${system} is not offered for ${fn}` };
82
- const file = join(dir, "knowledge/sources.json");
83
+ const file = join(ventureFile(dir, "knowledge/sources.json"));
83
84
  const map = existsSync(file) ? load(file) : { sources: [], records: {}, truth: "local" };
84
85
  const f = (map.functions ??= {})[fn] ??= { systems: [] };
85
86
  const entry = { system, ...(address ? { address } : {}), status: "pointed" };
package/schema/stale.mjs CHANGED
@@ -23,13 +23,14 @@ import { entries } from "../interface/provenance.mjs";
23
23
  import { resolve as resolveRoute } from "../interface/router.mjs";
24
24
  import { address } from "../knowledge/connectors/contract.mjs";
25
25
  import { LOCAL, sourcesOf } from "../knowledge/connectors/index.mjs";
26
+ import { ventureFile } from "./venture-paths.mjs";
26
27
 
27
28
  export const MARKS = ".naive/stale.json";
28
29
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
29
30
  const now = () => new Date().toISOString();
30
31
  const WRITES = new Set(["write", "accept", "baseline"]);
31
32
 
32
- const readMarks = (dir) => (existsSync(join(dir, MARKS)) ? load(join(dir, MARKS)) : { marks: [] });
33
+ const readMarks = (dir) => (existsSync(ventureFile(dir, MARKS)) ? load(ventureFile(dir, MARKS)) : { marks: [] });
33
34
 
34
35
  // The latest write of each file in the log.
35
36
  function lastWrites(dir) {
@@ -52,14 +53,14 @@ export function notify(dir, { source, paths, version, at = now(), via = "framewo
52
53
  if (!paths.length) throw new Error("a notification names at least one changed path");
53
54
  // The changed items as the venture refers to them: the address always, and the bare path for its own folder
54
55
  // or for a source whose records the venture keeps a local copy of at the same path.
55
- const items = paths.flatMap((p) => [address(source, p), ...(source === LOCAL || existsSync(join(dir, p)) ? [p] : [])]);
56
+ const items = paths.flatMap((p) => [address(source, p), ...(source === LOCAL || existsSync(ventureFile(dir, p)) ? [p] : [])]);
56
57
  const readers = new Map();
57
58
  for (const [file, e] of lastWrites(dir)) {
58
59
  if (items.includes(file)) continue;
59
60
  const hit = (e.derived_from ?? []).find((d) => items.some((i) => covers(d, i)));
60
61
  if (hit) readers.set(file, { via: "provenance", reads: hit });
61
62
  }
62
- const wu = join(dir, "knowledge/where-used.json");
63
+ const wu = join(ventureFile(dir, "knowledge/where-used.json"));
63
64
  if (existsSync(wu)) {
64
65
  for (const fact of load(wu).facts ?? []) {
65
66
  const { r, params } = resolveRoute(fact.parent.route);
@@ -74,13 +75,13 @@ export function notify(dir, { source, paths, version, at = now(), via = "framewo
74
75
  for (const [file, how] of readers) state.marks.push({ file, ...how, because, marked_at: now() });
75
76
  state.notifications = [...(state.notifications ?? []), { ...because, marked: readers.size, received_at: now() }].slice(-100);
76
77
  mkdirSync(join(dir, ".naive"), { recursive: true });
77
- writeFileSync(join(dir, MARKS), JSON.stringify(state, null, 2) + "\n");
78
+ writeFileSync(ventureFile(dir, MARKS), JSON.stringify(state, null, 2) + "\n");
78
79
  return { source, paths, stale: [...readers.keys()].sort() };
79
80
  }
80
81
 
81
82
  /** Whether one file is stale: the marks made after its latest write, or null when it is fresh. */
82
83
  export function staleOf(dir, file) {
83
- if (!existsSync(join(dir, MARKS))) return null;
84
+ if (!existsSync(ventureFile(dir, MARKS))) return null;
84
85
  const written = lastWrites(dir).get(file)?.at ?? "";
85
86
  const live = readMarks(dir).marks.filter((m) => m.file === file && m.marked_at > written);
86
87
  return live.length ? { since: live[0].marked_at, because: live.map((m) => ({ ...m.because, reads: m.reads, via: m.via })), fix: `write ${file} again from the changed source; it is fresh once written` } : null;
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
22
22
  import { join } from "node:path";
23
+ import { ventureFile } from "./venture-paths.mjs";
23
24
 
24
25
  const repo = new URL("..", import.meta.url).pathname;
25
26
  const load = (f) => JSON.parse(readFileSync(f, "utf8"));
@@ -74,7 +75,7 @@ export function routable(file, pattern) {
74
75
 
75
76
  // Every entry under a folder, as venture-relative paths, stopping at nested ventures and dot folders other than reserved ones.
76
77
  function walk(dir, sub = "") {
77
- const d = join(dir, sub);
78
+ const d = ventureFile(dir, sub);
78
79
  return readdirSync(d, { withFileTypes: true }).flatMap((e) => {
79
80
  const rel = sub ? `${sub}/${e.name}` : e.name;
80
81
  if (e.isDirectory()) {
@@ -105,7 +106,7 @@ export function structure(dir) {
105
106
  if (!s.re.test(name)) continue;
106
107
  if (s.name === "(group)") {
107
108
  if (parent) errors.push(`${e.path}/: a route group folder sits at the venture root (P-36)`);
108
- const inside = readdirSync(join(dir, e.path)).filter((f) => f !== "AGENTS.md" && f !== ".gitkeep");
109
+ const inside = readdirSync(ventureFile(dir, e.path)).filter((f) => f !== "AGENTS.md" && f !== ".gitkeep");
109
110
  if (inside.length) errors.push(`${e.path}/: ${s.rule}; move ${inside.join(", ")} into its package`);
110
111
  } else warnings.push(`${e.path}/: ${s.name} is ${s.rule} (P-32)`);
111
112
  }
@@ -129,7 +130,7 @@ export async function manifest(dir) {
129
130
  // The packages that fit the phase now, plus every package that already holds a record.
130
131
  const { packages } = await import("./packages.mjs");
131
132
  const p = packages(dir);
132
- const holding = names.filter((n) => { const k = pkgOf(n); return (k.records ?? []).length && existsSync(join(dir, n)) && readdirSync(join(dir, n)).some((f) => /\.(json|md|mdx)$/.test(f)); });
133
+ const holding = names.filter((n) => { const k = pkgOf(n); return (k.records ?? []).length && existsSync(ventureFile(dir, n)) && readdirSync(ventureFile(dir, n)).some((f) => /\.(json|md|mdx)$/.test(f)); });
133
134
  derived = [...new Set([...p.core.filter((x) => x.in === "now").map((x) => x.id), ...holding])];
134
135
  }
135
136
  const used = v.packages ?? derived;
@@ -141,7 +142,7 @@ export async function manifest(dir) {
141
142
  return {
142
143
  identity: { id: v.id, name: v.name, kind: v.kind ?? "venture", parent: v.parent, owner: v.owner, status: v.status },
143
144
  phase: v.phase,
144
- governance: { governed_by: v.governed_by ?? null, legal_form: v.form?.legal_form ?? "none", members: v.members ?? [], decision_rights: existsSync(join(dir, "governance/decision-rights.json")) ? "governance/decision-rights.json" : "the framework's template" },
145
+ governance: { governed_by: v.governed_by ?? null, legal_form: v.form?.legal_form ?? "none", members: v.members ?? [], decision_rights: existsSync(join(ventureFile(dir, "governance/decision-rights.json"))) ? "governance/decision-rights.json" : "the framework's template" },
145
146
  functions,
146
147
  packages: { declared: !!v.packages, list: used.map((n) => ({ name: n, kind: names.includes(n) ? pkgOf(n).kind : null, known: names.includes(n) })) },
147
148
  kits: (v.kits ?? []).map((k) => ({ ...k, note: "a kit is Mentura's; the framework checks only that it names a package the venture uses" })),
@@ -15,6 +15,7 @@ import { execFileSync } from "node:child_process";
15
15
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
16
16
  import { join, dirname } from "node:path";
17
17
  import { needNow } from "./need-now.mjs";
18
+ import { ventureFile } from "./venture-paths.mjs";
18
19
 
19
20
  const load = (p) => JSON.parse(readFileSync(p, "utf8"));
20
21
  const day = (d) => d ? d.slice(0, 10) : null;
@@ -43,19 +44,19 @@ export function timeToReady(dir) {
43
44
  const rels = (model.records.find((r) => r.id === type)?.paths ?? []).filter((p) => p.startsWith("instance/**/")).map((p) => p.slice("instance/**/".length));
44
45
  return here.filter((x) => rels.some((g) => glob(g).test(x)));
45
46
  };
46
- const seen = (type) => { const fs = where(type); return fs.length ? fs.map((f) => firstSeen(join(dir, f))).sort()[0] : null; };
47
+ const seen = (type) => { const fs = where(type); return fs.length ? fs.map((f) => firstSeen(ventureFile(dir, f))).sort()[0] : null; };
47
48
 
48
49
  // Running.
49
50
  const nn = needNow(dir);
50
- const products = where("product").map((f) => ({ f, p: load(join(dir, f)) }));
51
+ const products = where("product").map((f) => ({ f, p: load(ventureFile(dir, f)) }));
51
52
  const offered = products.filter(({ p }) => p.status === "live");
52
53
  const telosFile = where("telos")[0];
53
- const telos = telosFile ? load(join(dir, telosFile)) : null;
54
+ const telos = telosFile ? load(ventureFile(dir, telosFile)) : null;
54
55
  const conditions = {
55
56
  promise: v.core?.promise ? firstSeen(join(dir, "venture.json")) : null,
56
- product_offered: offered.length ? offered.map(({ f }) => firstSeen(join(dir, f))).sort()[0] : null,
57
+ product_offered: offered.length ? offered.map(({ f }) => firstSeen(ventureFile(dir, f))).sort()[0] : null,
57
58
  first_account: seen("account"),
58
- next_exit_criteria: nn.next && telos?.phases?.some((p) => p.phase === nn.next) ? firstSeen(join(dir, telosFile)) : null,
59
+ next_exit_criteria: nn.next && telos?.phases?.some((p) => p.phase === nn.next) ? firstSeen(ventureFile(dir, telosFile)) : null,
59
60
  };
60
61
  const missing = Object.entries(conditions).filter(([, d]) => !d).map(([k]) => k);
61
62
  const runningOn = missing.length ? null : Object.values(conditions).sort().at(-1);
@@ -75,7 +76,7 @@ export function timeToReady(dir) {
75
76
 
76
77
  // MAD tiers.
77
78
  const opsFile = where("validation-operations-readiness")[0];
78
- const tiers = opsFile ? load(join(dir, opsFile)).tiers.map((t) => ({ tier: t.tier, ready_on: t.ready_on ?? null, days: days(start, t.ready_on) })) : [];
79
+ const tiers = opsFile ? load(ventureFile(dir, opsFile)).tiers.map((t) => ({ tier: t.tier, ready_on: t.ready_on ?? null, days: days(start, t.ready_on) })) : [];
79
80
 
80
81
  return { venture: v.id, phase: nn.phase, started: start, running: { on: runningOn, days: days(start, runningOn), missing, conditions }, layers: layerRows, tiers };
81
82
  }
@@ -26,6 +26,7 @@ import { decisions, propose } from "./decisions.mjs";
26
26
  import { kit } from "./kits.mjs";
27
27
  import { CONFIG, readConfig, writeConfig } from "./naive-config.mjs";
28
28
  import { packages } from "./packages.mjs";
29
+ import { ventureFile } from "./venture-paths.mjs";
29
30
 
30
31
  export const STATE = ".naive/triggers.json";
31
32
  export const ERRORS = ".naive/trigger-errors.jsonl";
@@ -63,19 +64,19 @@ export function matches(t, e) {
63
64
  }
64
65
 
65
66
  function readState(dir) {
66
- const f = join(dir, STATE);
67
+ const f = ventureFile(dir, STATE);
67
68
  if (existsSync(f)) {
68
69
  try { const s = load(f); if (Array.isArray(s.runs) && Array.isArray(s.waiting)) return s; } catch {}
69
70
  // A state file that cannot be read is kept beside it, and the failure is raised; runs start again from now.
70
71
  const kept = `.naive/triggers.unreadable-${Date.now()}.json`;
71
- renameSync(f, join(dir, kept));
72
+ renameSync(f, ventureFile(dir, kept));
72
73
  return { since: now(), cursor: null, runs: [], waiting: [], unreadable: kept };
73
74
  }
74
75
  // Triggers start with the events after they were configured, not the venture's whole history.
75
- const cfg = join(dir, CONFIG);
76
+ const cfg = ventureFile(dir, CONFIG);
76
77
  return { since: existsSync(cfg) ? statSync(cfg).mtime.toISOString() : now(), cursor: null, runs: [], waiting: [] };
77
78
  }
78
- const saveState = (dir, s) => { mkdirSync(join(dir, ".naive"), { recursive: true }); writeFileSync(join(dir, STATE), JSON.stringify(s, null, 2) + "\n"); };
79
+ const saveState = (dir, s) => { mkdirSync(join(dir, ".naive"), { recursive: true }); writeFileSync(ventureFile(dir, STATE), JSON.stringify(s, null, 2) + "\n"); };
79
80
 
80
81
  const complete = (dir, name) => { try { return packages(dir).complete.includes(name); } catch { return false; } };
81
82
  const brief = (x) => (x == null ? null : typeof x === "string" ? x.slice(0, 300) : JSON.stringify(x).slice(0, 300));
@@ -155,7 +156,7 @@ export async function fire(dir, { rounds = 20 } = {}) {
155
156
  delete state.unreadable; state.runs.push(run); made.push(run); saveState(dir, state);
156
157
  }
157
158
  // A crash the provenance hook kept is a failure too.
158
- const ef = join(dir, ERRORS);
159
+ const ef = ventureFile(dir, ERRORS);
159
160
  if (existsSync(ef)) {
160
161
  for (const l of readFileSync(ef, "utf8").split("\n").filter(Boolean)) {
161
162
  const x = JSON.parse(l);
@@ -211,7 +212,7 @@ export async function fire(dir, { rounds = 20 } = {}) {
211
212
  export function report(dir) {
212
213
  const { triggers, problems } = triggersOf(dir);
213
214
  // Read only: a listing never moves an unreadable state file aside (the next run does, and raises it).
214
- const s = (() => { try { return load(join(dir, STATE)); } catch { return { runs: [], waiting: [] }; } })();
215
+ const s = (() => { try { return load(ventureFile(dir, STATE)); } catch { return { runs: [], waiting: [] }; } })();
215
216
  return { triggers: triggers.map((t) => ({ id: t.id, from: t.from, on: t.on, run: t.run.propose ? { propose: t.run.propose.id } : { route: t.run.route, action: t.run.action }, needs: t.needs ?? null, needs_complete: t.needs ? complete(dir, t.needs) : null, enabled: t.enabled, once: !!t.once })),
216
217
  problems, runs: s.runs.slice(-10), waiting: s.waiting, open: s.runs.filter((r) => r.status === "raised").map((r) => ({ run: r.n, trigger: r.trigger, error: r.error, decision: r.decision, decide: `mentura decide ${r.decision} --venture ${load(join(dir, "venture.json")).id} --by <your name> --answer "Retry" (or "Skip this event", "Turn the trigger off")` })) };
217
218
  }
@@ -25,6 +25,7 @@ import { agentFromEnv, record } from "../interface/provenance.mjs";
25
25
  import { routes, glob } from "../interface/router.mjs";
26
26
  import { CONFIG, pins, readConfig, writeConfig } from "./naive-config.mjs";
27
27
  import { packages as packageFit } from "./packages.mjs";
28
+ import { ventureFile } from "./venture-paths.mjs";
28
29
 
29
30
  export const repo = new URL("..", import.meta.url).pathname;
30
31
  export const STAGE = ".naive/upgrade";
@@ -106,26 +107,26 @@ function context(dir, { dry, git, cfg, at, onConfig }) {
106
107
  return {
107
108
  at, config: cfg, moved, writes, escalated, notes, kept,
108
109
  mv(a, b) {
109
- if (!existsSync(join(dir, a))) return;
110
- if (existsSync(join(dir, b)) && !moved.some(([, t]) => t === b)) throw new Error(`${b} already exists, so ${a} cannot move there; move one of them yourself and upgrade again`);
110
+ if (!existsSync(ventureFile(dir, a))) return;
111
+ if (existsSync(ventureFile(dir, b)) && !moved.some(([, t]) => t === b)) throw new Error(`${b} already exists, so ${a} cannot move there; move one of them yourself and upgrade again`);
111
112
  if (!dry) {
112
- mkdirSync(dirname(join(dir, b)), { recursive: true });
113
+ mkdirSync(dirname(ventureFile(dir, b)), { recursive: true });
113
114
  if (tracked(a)) execFileSync("git", ["mv", a, b], { cwd: dir, stdio: "pipe" });
114
- else renameSync(join(dir, a), join(dir, b));
115
+ else renameSync(ventureFile(dir, a), ventureFile(dir, b));
115
116
  }
116
117
  moved.push([a, b]);
117
118
  },
118
119
  write(rel, text, { activity = "write", note, derived_from, log = true } = {}) {
119
- if (!dry) { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), text); }
120
+ if (!dry) { mkdirSync(dirname(ventureFile(dir, rel)), { recursive: true }); writeFileSync(ventureFile(dir, rel), text); }
120
121
  writes.push({ path: rel, activity, note, derived_from, log });
121
122
  },
122
123
  escalate: (msg) => escalated.push(msg),
123
124
  note: (msg) => notes.push(msg),
124
125
  configChanged: () => onConfig(),
125
- removeEmpty: (rel) => { if (!dry) removeEmpty(join(dir, rel)); },
126
+ removeEmpty: (rel) => { if (!dry) removeEmpty(ventureFile(dir, rel)); },
126
127
  keep(sub) {
127
- if (existsSync(join(dir, sub))) return;
128
- if (!dry) { mkdirSync(join(dir, sub), { recursive: true }); writeFileSync(join(dir, sub, ".gitkeep"), ""); }
128
+ if (existsSync(ventureFile(dir, sub))) return;
129
+ if (!dry) { mkdirSync(ventureFile(dir, sub), { recursive: true }); writeFileSync(join(ventureFile(dir, sub), ".gitkeep"), ""); }
129
130
  kept.push(sub);
130
131
  },
131
132
  };
@@ -177,8 +178,8 @@ export async function upgrade(dir, { reg = registry(), dry = false, git = true }
177
178
  if (!dry && (p.pending || configChanged)) {
178
179
  if (configChanged) { writeConfig(dir, cfg); record(dir, { entity: CONFIG, activity: "write", agent, note: `pinned Naive ${p.to} and ${Object.keys(cfg.packages).length} packages (U-1)` }); }
179
180
  const rel = `.release/upgrade-${p.to}.json`;
180
- mkdirSync(join(dir, ".release"), { recursive: true });
181
- writeFileSync(join(dir, rel), JSON.stringify(release, null, 2) + "\n");
181
+ mkdirSync(join(ventureFile(dir, ".release")), { recursive: true });
182
+ writeFileSync(ventureFile(dir, rel), JSON.stringify(release, null, 2) + "\n");
182
183
  record(dir, { entity: rel, activity: "release", agent, derived_from: [...new Set([...ran.flatMap((r) => [...r.moves.map((m) => m.to), ...r.rewrote]), ...(configChanged ? [CONFIG] : [])])], note: `upgraded from Naive ${p.from} to ${p.to}` });
183
184
  // Adapters installed in the venture (Z-4) are refreshed, so their copies of package skills follow the upgrade.
184
185
  release.adapters = await refreshAdapters(dir);
@@ -264,18 +265,18 @@ export function updateFramework(root = repo) {
264
265
  export function snapshot(dir, { skipPrivate = true } = {}) {
265
266
  const out = {};
266
267
  const walk = (rel) => {
267
- for (const e of readdirSync(join(dir, rel), { withFileTypes: true })) {
268
+ for (const e of readdirSync(ventureFile(dir, rel), { withFileTypes: true })) {
268
269
  const r = rel ? `${rel}/${e.name}` : e.name;
269
270
  if (!rel && (r === ".naive" || r === ".git" || (skipPrivate && r === "_private"))) continue;
270
271
  if (e.isDirectory()) { if (!existsSync(join(dir, r, "venture.json"))) walk(r); }
271
- else if (e.isFile()) out[r] = sha(readFileSync(join(dir, r)));
272
+ else if (e.isFile()) out[r] = sha(readFileSync(ventureFile(dir, r)));
272
273
  }
273
274
  };
274
275
  walk("");
275
276
  return out;
276
277
  }
277
278
 
278
- const writeDraft = (dir, d) => { mkdirSync(join(dir, STAGE), { recursive: true }); writeFileSync(join(dir, DRAFT), JSON.stringify(d, null, 2) + "\n"); return d; };
279
+ const writeDraft = (dir, d) => { mkdirSync(ventureFile(dir, STAGE), { recursive: true }); writeFileSync(ventureFile(dir, DRAFT), JSON.stringify(d, null, 2) + "\n"); return d; };
279
280
  /** Adds to the staged upgrade's draft record (what an agent did, say). */
280
281
  export function annotate(dir, extra) {
281
282
  const d = draft(dir);
@@ -283,16 +284,16 @@ export function annotate(dir, extra) {
283
284
  return writeDraft(dir, { ...d, ...extra });
284
285
  }
285
286
  /** The staged upgrade's draft record, or null. */
286
- export const draft = (dir) => (existsSync(join(dir, DRAFT)) ? load(join(dir, DRAFT)) : null);
287
+ export const draft = (dir) => (existsSync(ventureFile(dir, DRAFT)) ? load(ventureFile(dir, DRAFT)) : null);
287
288
 
288
289
  /** Copies the venture to .naive/upgrade/venture and upgrades the copy. Nothing in the venture changes. */
289
290
  export async function stage(dir, { reg = registry(), by = "naive" } = {}) {
290
- if (existsSync(join(dir, STAGE))) throw new Error(`an upgrade is already staged in ${STAGE}: accept it (--accept --confirm ${CONFIRM}) or reject it (--reject)`);
291
+ if (existsSync(ventureFile(dir, STAGE))) throw new Error(`an upgrade is already staged in ${STAGE}: accept it (--accept --confirm ${CONFIRM}) or reject it (--reject)`);
291
292
  const p = plan(dir, { reg });
292
293
  if (!p.pending) throw new Error(`${p.venture} is already on Naive ${p.to} with every package pinned: nothing to stage`);
293
294
  const base = snapshot(dir);
294
- const out = join(dir, STAGED);
295
- for (const f of Object.keys(base)) { mkdirSync(dirname(join(out, f)), { recursive: true }); copyFileSync(join(dir, f), join(out, f)); }
295
+ const out = ventureFile(dir, STAGED);
296
+ for (const f of Object.keys(base)) { mkdirSync(dirname(join(out, f)), { recursive: true }); copyFileSync(ventureFile(dir, f), join(out, f)); }
296
297
  const result = await upgrade(out, { reg, git: false });
297
298
  return writeDraft(dir, { status: "staged", from: p.from, to: p.to, staged_at: now(), by, base, migrations: result.release.migrations.map((m) => ({ id: m.id, moves: m.moves.length, rewrote: m.rewrote.length })), escalated: result.release.escalated, guides: result.release.guides });
298
299
  }
@@ -310,7 +311,7 @@ export function runChecks(vdir, { reg = registry() } = {}) {
310
311
  for (const r of routes().filter((x) => x.kind === "record" && x.schema && (x.form ?? "json") === "json")) {
311
312
  for (const f of files.filter((f) => r.files.some((g) => glob(g).test(f)))) {
312
313
  n++;
313
- const v = spawnSync("python3", [join(repo, "schema/validate.py"), "--schema", r.schema.replace(/^schema\//, ""), "--file", join(vdir, f)], { encoding: "utf8" });
314
+ const v = spawnSync("python3", [join(repo, "schema/validate.py"), "--schema", r.schema.replace(/^schema\//, ""), "--file", ventureFile(vdir, f)], { encoding: "utf8" });
314
315
  if (v.status !== 0) bad.push(`${f} does not match ${r.schema}: ${(v.stdout + v.stderr).trim().split("\n")[0]}`);
315
316
  }
316
317
  }
@@ -323,10 +324,10 @@ export function runChecks(vdir, { reg = registry() } = {}) {
323
324
 
324
325
  /** What the staged copy changes: files added, changed and removed (the venture's _private/ is never removed). */
325
326
  export function changes(dir) {
326
- const live = snapshot(dir), staged = snapshot(join(dir, STAGED), { skipPrivate: false });
327
+ const live = snapshot(dir), staged = snapshot(ventureFile(dir, STAGED), { skipPrivate: false });
327
328
  return {
328
- added: Object.keys(staged).filter((f) => !(f in live) && !existsSync(join(dir, f))).sort(),
329
- changed: Object.keys(staged).filter((f) => (f in live && live[f] !== staged[f]) || (!(f in live) && existsSync(join(dir, f)) && sha(readFileSync(join(dir, f))) !== staged[f])).sort(),
329
+ added: Object.keys(staged).filter((f) => !(f in live) && !existsSync(ventureFile(dir, f))).sort(),
330
+ changed: Object.keys(staged).filter((f) => (f in live && live[f] !== staged[f]) || (!(f in live) && existsSync(ventureFile(dir, f)) && sha(readFileSync(ventureFile(dir, f))) !== staged[f])).sort(),
330
331
  removed: Object.keys(live).filter((f) => !(f in staged)).sort(),
331
332
  };
332
333
  }
@@ -335,7 +336,7 @@ export function changes(dir) {
335
336
  export function check(dir, { reg = registry() } = {}) {
336
337
  const d = draft(dir);
337
338
  if (!d) throw new Error("no upgrade is staged: stage one with --stage or --agent");
338
- const checks = runChecks(join(dir, STAGED), { reg });
339
+ const checks = runChecks(ventureFile(dir, STAGED), { reg });
339
340
  const ok = checks.every((c) => c.passed);
340
341
  return writeDraft(dir, { ...d, status: ok ? "ready" : "checks-failed", checked_at: now(), checks, changes: changes(dir) });
341
342
  }
@@ -357,13 +358,13 @@ export async function accept(dir, { confirm, by, reg = registry() } = {}) {
357
358
  const ch = checked.changes;
358
359
  const clash = ch.changed.filter((f) => f.startsWith("_private/"));
359
360
  if (clash.length) throw new Error(`not accepted: the upgrade would overwrite private files that differ (${clash.join(", ")}); move them aside, reject and stage again`);
360
- const src = join(dir, STAGED);
361
- for (const f of [...ch.added, ...ch.changed]) { mkdirSync(dirname(join(dir, f)), { recursive: true }); copyFileSync(join(src, f), join(dir, f)); }
362
- for (const f of ch.removed) rmSync(join(dir, f));
361
+ const src = ventureFile(dir, STAGED);
362
+ for (const f of [...ch.added, ...ch.changed]) { mkdirSync(dirname(ventureFile(dir, f)), { recursive: true }); copyFileSync(join(src, f), ventureFile(dir, f)); }
363
+ for (const f of ch.removed) rmSync(ventureFile(dir, f));
363
364
  // Folders the upgrade emptied go too, unless the copy still has them.
364
365
  const dirs = [...new Set(ch.removed.flatMap((f) => f.split("/").slice(0, -1).map((_, i, a) => a.slice(0, i + 1).join("/"))))].sort((a, b) => b.length - a.length);
365
- for (const x of dirs) if (existsSync(join(dir, x)) && !existsSync(join(src, x)) && readdirSync(join(dir, x)).length === 0) rmSync(join(dir, x), { recursive: true });
366
- rmSync(join(dir, STAGE), { recursive: true, force: true });
366
+ for (const x of dirs) if (existsSync(ventureFile(dir, x)) && !existsSync(join(src, x)) && readdirSync(ventureFile(dir, x)).length === 0) rmSync(ventureFile(dir, x), { recursive: true });
367
+ rmSync(ventureFile(dir, STAGE), { recursive: true, force: true });
367
368
  return landed(dir, d, ch, by);
368
369
  }
369
370
 
@@ -373,13 +374,13 @@ async function refreshAdapters(dir) {
373
374
 
374
375
  async function landed(dir, d, ch, by) {
375
376
  const rel = `.release/upgrade-${d.to}.json`;
376
- const e = existsSync(join(dir, rel)) ? record(dir, { entity: rel, activity: "accept", agent: agentFromEnv("person", "cli", by ? { id: by } : {}), note: `accepted the staged upgrade to Naive ${d.to}${d.by && d.by !== "naive" ? ` (worked by ${d.by})` : ""}` }) : null;
377
+ const e = existsSync(ventureFile(dir, rel)) ? record(dir, { entity: rel, activity: "accept", agent: agentFromEnv("person", "cli", by ? { id: by } : {}), note: `accepted the staged upgrade to Naive ${d.to}${d.by && d.by !== "naive" ? ` (worked by ${d.by})` : ""}` }) : null;
377
378
  return { accepted: d.to, from: d.from, added: ch.added.length, changed: ch.changed.length, removed: ch.removed.length, provenance: e?.hash ?? null, adapters: await refreshAdapters(dir) };
378
379
  }
379
380
 
380
381
  /** Discards a staged upgrade. The venture never changed, so nothing is logged. */
381
382
  export function reject(dir) {
382
- if (!existsSync(join(dir, STAGE))) throw new Error("no upgrade is staged");
383
- rmSync(join(dir, STAGE), { recursive: true, force: true });
383
+ if (!existsSync(ventureFile(dir, STAGE))) throw new Error("no upgrade is staged");
384
+ rmSync(ventureFile(dir, STAGE), { recursive: true, force: true });
384
385
  return { rejected: true };
385
386
  }
@@ -0,0 +1,85 @@
1
+ // Where a venture's files physically are (EE-086). Code names a file by its logical path, the path a
2
+ // record has always had (foundation/problem.json, provenance.jsonl, knowledge/sources.json), and `at`
3
+ // says where it sits on disk under the venture's layout:
4
+ //
5
+ // flat (no layout in venture.json, every venture made before EE-086): the logical path, unchanged
6
+ // structured (venture.json layout: "structured"):
7
+ // venture.json, AGENTS.md, README.md, package.json at the top (profile.md is README.md)
8
+ // provenance.jsonl, sources.json, naive.config.json in meta/; .release/ is meta/releases/
9
+ // every package (foundation/, people/, _drafts/ ...) in knowledge/, or in knowledge/(group)/ when the
10
+ // founder mirrors a function as a folder
11
+ //
12
+ // `logical` goes the other way, for code that walks the disk and matches files against the data model.
13
+
14
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
+ import { join, relative, sep } from "node:path";
16
+
17
+ const TOP = new Set(["venture.json", "AGENTS.md", "README.md", "package.json", "package-lock.json", ".gitignore"]);
18
+ const META = { "provenance.jsonl": "meta/provenance.jsonl", "knowledge/sources.json": "meta/sources.json", "sources.json": "meta/sources.json", "naive.config.json": "meta/naive.config.json" };
19
+ const cache = new Map();
20
+
21
+ /** The venture's layout: "structured" (EE-086) or "flat". Read once per venture.json change. */
22
+ export function layoutOf(dir) {
23
+ const f = join(dir, "venture.json");
24
+ let m = 0;
25
+ try { m = statSync(f).mtimeMs; } catch { return "flat"; }
26
+ const hit = cache.get(dir);
27
+ if (hit && hit.m === m) return hit.layout;
28
+ let layout = "flat";
29
+ try { layout = JSON.parse(readFileSync(f, "utf8")).layout === "structured" ? "structured" : "flat"; } catch {}
30
+ cache.set(dir, { m, layout });
31
+ return layout;
32
+ }
33
+
34
+ // The (group)/ folder a package sits in, when the founder mirrors functions on disk.
35
+ function groupOf(dir, pkg) {
36
+ const kb = join(dir, "knowledge");
37
+ if (!pkg || existsSync(join(kb, pkg)) || !existsSync(kb)) return null;
38
+ for (const g of readdirSync(kb)) if (/^\(.+\)$/.test(g) && existsSync(join(kb, g, pkg))) return g;
39
+ return null;
40
+ }
41
+
42
+ /** The venture-relative physical path of a logical path. */
43
+ export function physical(dir, rel) {
44
+ const r = String(rel).split(sep).join("/").replace(/^\.\//, "");
45
+ if (layoutOf(dir) !== "structured") return r;
46
+ if (TOP.has(r)) return r;
47
+ if (r === "profile.md") return "README.md";
48
+ if (META[r]) return META[r];
49
+ if (r === ".release" || r.startsWith(".release/")) return "meta/releases" + r.slice(".release".length);
50
+ if (r.startsWith(".naive/") || r === ".naive" || r.startsWith("node_modules") || r.startsWith(".git")) return r;
51
+ if (r.startsWith("knowledge/") || r.startsWith("meta/") || r.startsWith("repo/")) return r;
52
+ const pkg = r.split("/")[0];
53
+ const g = groupOf(dir, pkg);
54
+ return `knowledge/${g ? `${g}/` : ""}${r}`;
55
+ }
56
+
57
+ /** The absolute path of a logical path in a venture. */
58
+ export const ventureFile = (dir, rel) => join(dir, physical(dir, rel));
59
+
60
+ /** The logical path of a venture-relative physical path: the inverse of `physical`. */
61
+ export function logical(dir, rel) {
62
+ const r = String(rel).split(sep).join("/");
63
+ if (layoutOf(dir) !== "structured") return r;
64
+ if (r === "README.md") return "profile.md";
65
+ for (const [l, p] of Object.entries(META)) if (r === p) return l === "sources.json" ? "knowledge/sources.json" : l;
66
+ if (r === "meta/releases" || r.startsWith("meta/releases/")) return ".release" + r.slice("meta/releases".length);
67
+ if (r.startsWith("knowledge/")) return r.slice("knowledge/".length).replace(/^\([^/]+\)\//, "");
68
+ return r;
69
+ }
70
+
71
+ /** Every file of a venture by its logical path, skipping what is never a record. */
72
+ export function logicalFiles(dir) {
73
+ const out = [];
74
+ const walk = (d) => {
75
+ if (!existsSync(d)) return;
76
+ for (const e of readdirSync(d, { withFileTypes: true })) {
77
+ if (e.name === "node_modules" || e.name === ".git") continue;
78
+ const p = join(d, e.name);
79
+ if (e.isDirectory()) { if (!existsSync(join(p, "venture.json"))) walk(p); }
80
+ else out.push(logical(dir, relative(dir, p)));
81
+ }
82
+ };
83
+ walk(dir);
84
+ return out;
85
+ }
@@ -34,6 +34,31 @@
34
34
  "pattern": "^[a-z0-9-]+$",
35
35
  "description": "The venture type, an id from schema/venture-types.json: what kind of venture this is (a software startup, a VC fund), which decides its starting packages, first decisions, functions and roles (EE-082)"
36
36
  },
37
+ "layout": {
38
+ "enum": [
39
+ "flat",
40
+ "structured"
41
+ ],
42
+ "default": "flat",
43
+ "description": "Where the venture's files sit (EE-086). structured: venture.json, README.md and AGENTS.md at the top, the knowledge base in knowledge/, sources.json, naive.config.json, the provenance log and releases in meta/, and code in an optional repo/. flat: every venture made before EE-086, with packages at the top."
44
+ },
45
+ "functions": {
46
+ "type": "object",
47
+ "description": "The venture's functions and their one accountable owner each (EE-071), keyed by function id from operations/departments.json. A solo founder owns them all, which is what the manifest assumes when this is left out.",
48
+ "additionalProperties": {
49
+ "type": "object",
50
+ "required": [
51
+ "owner"
52
+ ],
53
+ "properties": {
54
+ "owner": {
55
+ "type": "string",
56
+ "description": "A role or a person's name; no personal details"
57
+ }
58
+ },
59
+ "additionalProperties": false
60
+ }
61
+ },
37
62
  "parent": {
38
63
  "type": [
39
64
  "string",
@@ -181,23 +206,6 @@
181
206
  "additionalProperties": false
182
207
  }
183
208
  },
184
- "functions": {
185
- "type": "object",
186
- "description": "The venture's functions and their one accountable owner each (EE-071), keyed by function id from operations/departments.json. A solo founder owns them all, which is what the manifest assumes when this is left out.",
187
- "additionalProperties": {
188
- "type": "object",
189
- "required": [
190
- "owner"
191
- ],
192
- "properties": {
193
- "owner": {
194
- "type": "string",
195
- "description": "A role or a person's name; no personal details"
196
- }
197
- },
198
- "additionalProperties": false
199
- }
200
- },
201
209
  "brand": {
202
210
  "type": "object",
203
211
  "description": "The venture's verbal brand elements, owned by the brand pillar. Visual identity (tokens, logos) is a separate record.",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "workspace.schema.json",
4
4
  "title": "Workspace",
5
- "description": "A builder's workspace: one instance of the system. A workspace.json at its top says which schema version it follows, where its vault is and where its repositories are (EE-002, EE-011, EE-020). A workspace always has a vault; it has a code folder of repositories once any venture builds software.",
5
+ "description": "A builder's workspace: one instance of the system. A naive-workspace.json (earlier workspace.json) at its top says which schema version it follows, where its vault is and where its repositories are (EE-002, EE-011, EE-020). A workspace always has a vault; it has a code folder of repositories once any venture builds software.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "schema",
@@ -18,6 +18,10 @@
18
18
  "pattern": "^\\d+\\.\\d+\\.\\d+$",
19
19
  "description": "The version in schema/VERSION that this workspace follows"
20
20
  },
21
+ "name": {
22
+ "type": "string",
23
+ "description": "The workspace's name, shown to the founder. create-venture names a new workspace \"Naive workspace\"; the founder can rename it"
24
+ },
21
25
  "ventures": {
22
26
  "type": "array",
23
27
  "items": {