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.
- package/AGENTS.md +1 -1
- package/docs/decisions/decisions.json +15 -0
- package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
- package/interface/adapters.mjs +7 -6
- package/interface/boundaries.mjs +5 -4
- package/interface/brand.mjs +6 -5
- package/interface/cli/mentura.mjs +4 -3
- package/interface/cli/naive.mjs +11 -6
- package/interface/context.mjs +7 -6
- package/interface/demo/board.mjs +9 -8
- package/interface/demo/decision-harness.mjs +4 -3
- package/interface/gate-hooks.mjs +2 -1
- package/interface/harness/evals.mjs +8 -7
- package/interface/harness/run.mjs +14 -13
- package/interface/instrumentation.mjs +4 -3
- package/interface/layer.json +4 -0
- package/interface/package-routes.mjs +4 -3
- package/interface/provenance.mjs +5 -4
- package/interface/reads.mjs +4 -3
- package/interface/release-card.mjs +2 -1
- package/interface/router.mjs +10 -9
- package/interface/skills/choosing-artifacts/SKILL.md +113 -0
- package/interface/vitals.mjs +3 -2
- package/knowledge/connectors/catalog.mjs +3 -2
- package/knowledge/connectors/filesystem.mjs +10 -5
- package/knowledge/connectors/index.mjs +3 -2
- package/operations/marketing/check-public.mjs +7 -6
- package/operations/marketing/site/plan.mjs +4 -3
- package/operations/marketing/site/presence.mjs +4 -3
- package/package.json +1 -1
- package/platform/integrations/rho.mjs +2 -1
- package/purpose/frameworks/idea-map/route.mjs +5 -4
- package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
- package/schema/access.mjs +2 -1
- package/schema/add-department.mjs +3 -2
- package/schema/assess-problem.mjs +3 -2
- package/schema/assets.mjs +6 -5
- package/schema/brand.mjs +11 -10
- package/schema/caching.mjs +5 -4
- package/schema/check-config.mjs +4 -3
- package/schema/check-data-model.mjs +2 -1
- package/schema/check-data-packages.mjs +6 -5
- package/schema/check-execution.mjs +4 -3
- package/schema/check-people.mjs +22 -8
- package/schema/check-private.mjs +2 -1
- package/schema/check-ready.mjs +6 -5
- package/schema/check-sources.mjs +2 -1
- package/schema/check-where-used.mjs +4 -3
- package/schema/decision-analytics.mjs +2 -1
- package/schema/decisions.mjs +14 -11
- package/schema/foundation.mjs +8 -7
- package/schema/founder.schema.json +2 -2
- package/schema/frames.mjs +4 -3
- package/schema/freshness.mjs +13 -12
- package/schema/gate-drafts.mjs +8 -7
- package/schema/init-venture.mjs +48 -20
- package/schema/migrate-venture.mjs +8 -7
- package/schema/migrations/kit-to-package.mjs +4 -3
- package/schema/migrations/package-layout.mjs +9 -5
- package/schema/model-steps.mjs +8 -7
- package/schema/naive-config.mjs +4 -3
- package/schema/need-now.mjs +5 -4
- package/schema/packages.mjs +11 -10
- package/schema/payouts.mjs +3 -2
- package/schema/people.schema.json +2 -3
- package/schema/port-venture.mjs +206 -132
- package/schema/release-read.mjs +5 -4
- package/schema/releases.mjs +21 -20
- package/schema/run-handoff.mjs +5 -4
- package/schema/setup.mjs +5 -4
- package/schema/stale.mjs +6 -5
- package/schema/structure.mjs +5 -4
- package/schema/time-to-ready.mjs +7 -6
- package/schema/triggers.mjs +7 -6
- package/schema/upgrades.mjs +31 -30
- package/schema/venture-paths.mjs +97 -0
- package/schema/venture.schema.json +25 -17
- package/schema/workspace.schema.json +5 -1
package/schema/check-ready.mjs
CHANGED
|
@@ -13,19 +13,20 @@
|
|
|
13
13
|
import { execSync } from "node:child_process";
|
|
14
14
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
16
17
|
|
|
17
18
|
const [dir, id] = process.argv.slice(2);
|
|
18
19
|
if (!dir || !id) { console.error("usage: node schema/check-ready.mjs <venture dir> <handoff id> [--record --by <who>]"); process.exit(2); }
|
|
19
20
|
const opt = (k) => process.argv.includes(k) ? process.argv[process.argv.indexOf(k) + 1] : undefined;
|
|
20
21
|
const root = new URL("..", import.meta.url).pathname;
|
|
21
22
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
22
|
-
const hPath = join(dir, "operations/handoffs", `${id}.json`);
|
|
23
|
+
const hPath = join(ventureFile(dir, "operations/handoffs"), `${id}.json`);
|
|
23
24
|
const h = load(hPath);
|
|
24
25
|
const items = [];
|
|
25
26
|
const item = (name, ok, why = "") => items.push({ name, ok, why });
|
|
26
27
|
|
|
27
28
|
// 1. The brief says what done looks like and what to do, with no placeholders left.
|
|
28
|
-
const bPath = join(dir, "operations/handoffs", h.brief);
|
|
29
|
+
const bPath = join(ventureFile(dir, "operations/handoffs"), h.brief);
|
|
29
30
|
const brief = existsSync(bPath) ? readFileSync(bPath, "utf8") : "";
|
|
30
31
|
item("brief exists", !!brief, bPath);
|
|
31
32
|
const section = (name) => { const m = brief.match(new RegExp(`^##\\s+(?:\\d+\\.\\s*)?${name}\\b[^\\n]*\\n([\\s\\S]*?)(?=^##\\s|$(?![\\s\\S]))`, "mi")); return m ? m[1].trim() : null; };
|
|
@@ -34,10 +35,10 @@ const holes = brief.match(/\b(TBD|TODO|FIXME)\b|\?\?\?|<(?:title|role|entity|out
|
|
|
34
35
|
item("brief has no placeholders left", !holes.length, holes.slice(0, 3).join(", "));
|
|
35
36
|
|
|
36
37
|
// 2. Everything it names exists.
|
|
37
|
-
const products = new Set(["products", "product"].flatMap((folder) => existsSync(
|
|
38
|
+
const products = new Set(["products", "product"].flatMap((folder) => existsSync(ventureFile(dir, folder)) ? readdirSync(ventureFile(dir, folder)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)) : []));
|
|
38
39
|
const missingProducts = (h.products ?? []).filter((p) => !products.has(p));
|
|
39
40
|
item("every product it serves exists", !missingProducts.length, missingProducts.join(", "));
|
|
40
|
-
const logs = [join(root, "docs/decisions/decisions.json"), join(dir, "knowledge/decisions.json")].filter(existsSync).flatMap((f) => load(f).decisions);
|
|
41
|
+
const logs = [join(root, "docs/decisions/decisions.json"), join(ventureFile(dir, "knowledge/decisions.json"))].filter(existsSync).flatMap((f) => load(f).decisions);
|
|
41
42
|
const decisions = new Map(logs.map((d) => [d.id, d]));
|
|
42
43
|
const badDecisions = (h.governed_by ?? []).filter((d) => !decisions.has(d) || ["withdrawn", "superseded"].includes(decisions.get(d).status));
|
|
43
44
|
item("every governing decision exists and is in force or proposed", !badDecisions.length, badDecisions.join(", "));
|
|
@@ -49,7 +50,7 @@ item("every input it needs exists", !missingInputs.length, missingInputs.join(",
|
|
|
49
50
|
// 3. The holder can act inside its rights.
|
|
50
51
|
const roles = new Set(load(join(root, "governance/venture-template/roles/agent-roles.json")).roles.map((r) => r.id));
|
|
51
52
|
const classes = new Map(load(join(root, "governance/venture-template/decision-rights.json")).classes.map((c) => [c.id, c]));
|
|
52
|
-
const own = join(dir, "governance/decision-rights.json");
|
|
53
|
+
const own = join(ventureFile(dir, "governance/decision-rights.json"));
|
|
53
54
|
if (existsSync(own)) for (const c of load(own).classes) classes.set(c.id, { ...classes.get(c.id), ...c });
|
|
54
55
|
item(`role ${h.role} exists`, roles.has(h.role));
|
|
55
56
|
const foreign = h.decision_classes.filter((c) => classes.get(c)?.owner !== h.role);
|
package/schema/check-sources.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { LOCAL, MAP, registry } from "../knowledge/connectors/index.mjs";
|
|
13
13
|
import { ANY, CONTEXT, specificity } from "../knowledge/connectors/resolve.mjs";
|
|
14
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
14
15
|
|
|
15
16
|
const dir = process.argv[2];
|
|
16
17
|
if (!dir) { console.error("usage: node schema/check-sources.mjs <workspace or venture dir>"); process.exit(2); }
|
|
@@ -27,7 +28,7 @@ const errors = [];
|
|
|
27
28
|
let maps = 0;
|
|
28
29
|
|
|
29
30
|
for (const v of walk(dir)) {
|
|
30
|
-
const f =
|
|
31
|
+
const f = ventureFile(v, MAP);
|
|
31
32
|
if (!existsSync(f)) continue;
|
|
32
33
|
maps++;
|
|
33
34
|
const id = JSON.parse(readFileSync(join(v, "venture.json"), "utf8")).id;
|
|
@@ -10,6 +10,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { connect, sourceFor, sourcesOf } from "../knowledge/connectors/index.mjs";
|
|
12
12
|
import { resolve } from "../interface/router.mjs";
|
|
13
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
13
14
|
|
|
14
15
|
const dir = process.argv[2];
|
|
15
16
|
if (!dir) { console.error("usage: node schema/check-where-used.mjs <workspace or venture dir> [--json]"); process.exit(2); }
|
|
@@ -21,18 +22,18 @@ const at = (obj, pointer) => pointer.split("/").slice(1).map((k) => k.replace(/~
|
|
|
21
22
|
const errors = [], drift = [], unchecked = [];
|
|
22
23
|
let lists = 0, checked = 0;
|
|
23
24
|
for (const v of walk(dir)) {
|
|
24
|
-
if (!existsSync(
|
|
25
|
+
if (!existsSync(ventureFile(v, FILE))) continue;
|
|
25
26
|
lists++;
|
|
26
27
|
const id = JSON.parse(readFileSync(join(v, "venture.json"), "utf8")).id;
|
|
27
28
|
const ids = new Map(sourcesOf(v).sources.map((s) => [s.id, s]));
|
|
28
|
-
for (const fact of JSON.parse(readFileSync(
|
|
29
|
+
for (const fact of JSON.parse(readFileSync(ventureFile(v, FILE), "utf8")).facts) {
|
|
29
30
|
const where = `${id}: ${fact.id}`;
|
|
30
31
|
const { r, params } = resolve(fact.parent.route);
|
|
31
32
|
if (!r || r.kind !== "record" || r.collection === "list") { errors.push(`${where}: parent ${fact.parent.route} is not a record route`); continue; }
|
|
32
33
|
const context = r.collection === "item" && (r.record === "product" || r.record === "account") ? { [r.record]: params.id } : {};
|
|
33
34
|
const src = sourceFor(v, r.record, context);
|
|
34
35
|
const cands = (r.collection === "item" ? r.files.map((f) => f.replace("*", params.id)) : r.files).filter((f) => !f.includes("*"));
|
|
35
|
-
const path = cands.find((f) => existsSync(
|
|
36
|
+
const path = cands.find((f) => existsSync(ventureFile(v, f))) ?? cands[0];
|
|
36
37
|
if (src.connector !== "filesystem") { unchecked.push(`${where}: parent is in ${src.id}`); continue; }
|
|
37
38
|
const parent = await (await connect(src, v)).read(path);
|
|
38
39
|
const value = parent && at(JSON.parse(parent.content), fact.parent.pointer);
|
|
@@ -19,6 +19,7 @@ import { readFileSync } from "node:fs";
|
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { decisions } from "./decisions.mjs";
|
|
21
21
|
import { HOME, PACKAGES, TITLES } from "./packages.mjs";
|
|
22
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
22
23
|
|
|
23
24
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
24
25
|
const titleOf = (t) => ({ icp: "ICP", "venture-core": "Core product" })[t] ?? TITLES[t] ?? t.replace(/^validation-/, "").replace(/-/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
@@ -30,7 +31,7 @@ const OPEN = new Set(["now", "overdue", "waiting", "stale", "later"]);
|
|
|
30
31
|
export function analytics(dir, now) {
|
|
31
32
|
const r = decisions(dir, now);
|
|
32
33
|
const rows = r.decisions.filter((d) => d.source !== "root").map((d) => {
|
|
33
|
-
const raw = d.file ? load(
|
|
34
|
+
const raw = d.file ? load(ventureFile(dir, d.file)) : {};
|
|
34
35
|
const record = d.decides?.record ?? null;
|
|
35
36
|
// A foundation statement belongs to the strategic foundation whatever record holds it; so does a
|
|
36
37
|
// decision about venture.json itself (the core product, the team).
|
package/schema/decisions.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { agentFromEnv, record as prov } from "../interface/provenance.mjs";
|
|
|
32
32
|
import { glob as routeFileGlob, routes } from "../interface/router.mjs";
|
|
33
33
|
// Caching hook (F-12): the decision list is computed once per router action (schema/caching.mjs).
|
|
34
34
|
import { memo } from "./caching.mjs";
|
|
35
|
+
import { layoutOf, logicalFiles, ventureFile } from "./venture-paths.mjs";
|
|
35
36
|
|
|
36
37
|
const repo = new URL("..", import.meta.url).pathname;
|
|
37
38
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
@@ -47,7 +48,9 @@ export function recordOf(file) {
|
|
|
47
48
|
|
|
48
49
|
// Every *.decision.json in a venture, outside drafts, hidden folders and nested ventures.
|
|
49
50
|
function decisionFiles(dir, sub = "") {
|
|
50
|
-
|
|
51
|
+
// A structured venture (EE-086): its decision files by their logical paths.
|
|
52
|
+
if (!sub && layoutOf(dir) === "structured") return logicalFiles(dir).filter((r) => r.endsWith(".decision.json") && r.split("/").at(-2) === "decisions" && !r.split("/").some((s) => s.startsWith(".") || s.startsWith("_")));
|
|
53
|
+
return readdirSync(ventureFile(dir, sub), { withFileTypes: true }).flatMap((e) => {
|
|
51
54
|
const rel = sub ? `${sub}/${e.name}` : e.name;
|
|
52
55
|
if (e.isDirectory()) return e.name.startsWith(".") || e.name.startsWith("_") || existsSync(join(dir, rel, "venture.json")) ? [] : decisionFiles(dir, rel);
|
|
53
56
|
return e.name.endsWith(".decision.json") && sub.split("/").at(-1) === "decisions" ? [rel] : [];
|
|
@@ -74,7 +77,7 @@ function decisionsAt(dir, now = Date.parse(process.env.DECISIONS_NOW ?? "") || D
|
|
|
74
77
|
status: settled ? "decided" : "open", decided_at: null, needed_in: venture.phase },
|
|
75
78
|
];
|
|
76
79
|
for (const f of decisionFiles(dir)) {
|
|
77
|
-
const d = load(
|
|
80
|
+
const d = load(ventureFile(dir, f));
|
|
78
81
|
// decisions/<record>[.<topic>].decision.json decides <record>.json in the folder that holds decisions/.
|
|
79
82
|
const at = f.split("/").slice(0, -2).join("/"), base = f.split("/").at(-1).split(".")[0];
|
|
80
83
|
const path = d.decides?.path ?? (at ? `${at}/${base}.json` : `${base}.json`);
|
|
@@ -107,7 +110,7 @@ function decisionsAt(dir, now = Date.parse(process.env.DECISIONS_NOW ?? "") || D
|
|
|
107
110
|
}
|
|
108
111
|
const statementFor = (record) => list.find((x) => x.part_of === "foundation" && x.status === "open" && x.decides?.record === record);
|
|
109
112
|
// An agent's draft of a gated record waits for a person to accept it.
|
|
110
|
-
const drafts = existsSync(join(dir, "_drafts")) ? readdirSync(join(dir, "_drafts")).filter((f) => f.endsWith(".draft")) : [];
|
|
113
|
+
const drafts = existsSync(join(ventureFile(dir, "_drafts"))) ? readdirSync(join(ventureFile(dir, "_drafts"))).filter((f) => f.endsWith(".draft")) : [];
|
|
111
114
|
for (const d of drafts) {
|
|
112
115
|
const path = d.replace(/\.draft$/, "").replace(/__/g, "/");
|
|
113
116
|
const record = recordOf(path);
|
|
@@ -240,7 +243,7 @@ const routeGlob = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
|
240
243
|
/** The decision that keeps a route closed to an actor (EE-058): an undecided decision that unlocks it. */
|
|
241
244
|
export function locked(dir, route, actorId) {
|
|
242
245
|
if (!existsSync(join(dir, "venture.json"))) return null;
|
|
243
|
-
const files = decisionFiles(dir).map((f) => load(
|
|
246
|
+
const files = decisionFiles(dir).map((f) => load(ventureFile(dir, f)));
|
|
244
247
|
const d = files.find((x) => x.status !== "decided" && x.status !== "withdrawn" && (x.unlocks ?? []).some((u) =>
|
|
245
248
|
(u.routes ?? []).some((g) => routeGlob(g).test(route)) && (!u.for?.length || !actorId || u.for.includes(actorId))));
|
|
246
249
|
return d ? { id: d.id, question: d.question } : null;
|
|
@@ -254,7 +257,7 @@ const validate = (d) => {
|
|
|
254
257
|
rmSync(scratch, { recursive: true, force: true });
|
|
255
258
|
return v.status === 0 ? null : (v.stdout + v.stderr).trim();
|
|
256
259
|
};
|
|
257
|
-
const fileOf = (dir, id) => decisionFiles(dir).find((f) => load(
|
|
260
|
+
const fileOf = (dir, id) => decisionFiles(dir).find((f) => load(ventureFile(dir, f)).id === id);
|
|
258
261
|
|
|
259
262
|
/** An agent proposes a decision, or new wording for an open one. It stays open until a person
|
|
260
263
|
* approves it: an agent never decides (EE-058). at: the folder beside the record it decides. */
|
|
@@ -263,16 +266,16 @@ export function propose(dir, input, { agentId, via = "router", harness, model }
|
|
|
263
266
|
if (!rest.id || !rest.question) return { ok: false, error: "a decision needs an id and a question" };
|
|
264
267
|
if (rest.status && rest.status !== "open") return { ok: false, error: "an agent may only propose an open decision; a person approves it" };
|
|
265
268
|
const existing = fileOf(dir, rest.id);
|
|
266
|
-
if (existing && load(
|
|
269
|
+
if (existing && load(ventureFile(dir, existing)).status === "decided") return { ok: false, error: `${rest.id} is decided; only a person can change it` };
|
|
267
270
|
const file = existing ?? `${at ? `${at}/` : ""}decisions/${name ?? rest.id}.decision.json`;
|
|
268
271
|
if (!existing && !/^([a-z0-9-]+\/)*decisions\/[a-z0-9.-]+\.decision\.json$/.test(file)) return { ok: false, error: `${file} is not a decisions/ path` };
|
|
269
272
|
// New wording for an open decision keeps what the agent did not restate: its parent, boundary, unlocks.
|
|
270
|
-
const before = existing ? load(
|
|
273
|
+
const before = existing ? load(ventureFile(dir, existing)) : {};
|
|
271
274
|
const d = { ...before, ...rest, status: "open", provenance: { drafted_by: { kind: "agent", ...(agentId ? { id: agentId } : {}) } } };
|
|
272
275
|
const bad = validate(d);
|
|
273
276
|
if (bad) return { ok: false, error: `does not match schema/decision-record.schema.json:\n${bad}` };
|
|
274
|
-
mkdirSync(dirname(
|
|
275
|
-
writeFileSync(
|
|
277
|
+
mkdirSync(dirname(ventureFile(dir, file)), { recursive: true });
|
|
278
|
+
writeFileSync(ventureFile(dir, file), JSON.stringify(d, null, 2) + "\n");
|
|
276
279
|
const e = prov(dir, { entity: file, route: "governance/decisions", activity: "draft", agent: agentFromEnv("agent", via, { ...(agentId ? { id: agentId } : {}), ...(harness ? { harness } : {}), ...(model ? { model } : {}) }), note: `proposed decision ${d.id}` });
|
|
277
280
|
return { ok: true, result: { proposed: d.id, file, awaiting: "approval", provenance: e.hash } };
|
|
278
281
|
}
|
|
@@ -282,7 +285,7 @@ export function approve(dir, id, { by, answer, via = "cli", note } = {}) {
|
|
|
282
285
|
const file = fileOf(dir, id);
|
|
283
286
|
if (!file) return { ok: false, error: `no decision ${id}` };
|
|
284
287
|
if (!by) return { ok: false, error: "approving needs the person's name (--by)" };
|
|
285
|
-
const d = load(
|
|
288
|
+
const d = load(ventureFile(dir, file));
|
|
286
289
|
if (d.status === "decided") return { ok: false, error: `${id} is already decided` };
|
|
287
290
|
if (!answer && !d.answer) return { ok: false, error: `${id} has no wording to approve; give an answer` };
|
|
288
291
|
const drafted = d.provenance?.drafted_by ?? { kind: "person", id: by };
|
|
@@ -291,7 +294,7 @@ export function approve(dir, id, { by, answer, via = "cli", note } = {}) {
|
|
|
291
294
|
const out = { ...d, status: "decided", answer: answer ?? d.answer, provenance: { ...d.provenance, drafted_by: drafted, approved_by: by, approved_at: new Date().toISOString().replace(/\.\d+Z$/, "Z"), phase, wording, via } };
|
|
292
295
|
const bad = validate(out);
|
|
293
296
|
if (bad) return { ok: false, error: bad };
|
|
294
|
-
writeFileSync(
|
|
297
|
+
writeFileSync(ventureFile(dir, file), JSON.stringify(out, null, 2) + "\n");
|
|
295
298
|
const e = prov(dir, { entity: file, route: "governance/decisions", activity: "accept", agent: agentFromEnv("person", via, { id: by }), note: note ?? `approved decision ${id} (${wording})` });
|
|
296
299
|
return { ok: true, result: { decided: id, wording, phase, provenance: e.hash, unlocks: (out.unlocks ?? []).map((u) => u.activity) } };
|
|
297
300
|
}
|
package/schema/foundation.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
18
19
|
|
|
19
20
|
const repo = new URL("..", import.meta.url).pathname;
|
|
20
21
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
@@ -30,15 +31,15 @@ export function filesOf(dir, type) {
|
|
|
30
31
|
const gs = (r?.paths ?? []).filter((p) => p.startsWith("instance/**/")).map((p) => p.slice("instance/**/".length));
|
|
31
32
|
const found = [];
|
|
32
33
|
for (const g of gs) {
|
|
33
|
-
if (!g.includes("*")) { if (existsSync(
|
|
34
|
+
if (!g.includes("*")) { if (existsSync(ventureFile(dir, g))) found.push(g); continue; }
|
|
34
35
|
const star = g.indexOf("*");
|
|
35
36
|
const folder = g.slice(0, star), rest = g.slice(star + 1);
|
|
36
37
|
if (rest.includes("*")) continue;
|
|
37
|
-
const d =
|
|
38
|
+
const d = ventureFile(dir, folder);
|
|
38
39
|
if (existsSync(d)) for (const f of readdirSync(d)) if (f.endsWith(rest)) found.push(folder + f);
|
|
39
40
|
}
|
|
40
41
|
if (found.length) return found;
|
|
41
|
-
const drafted = gs.filter((g) => !g.includes("*") && existsSync(
|
|
42
|
+
const drafted = gs.filter((g) => !g.includes("*") && existsSync(ventureFile(dir, `_drafts/${g.replace(/\//g, "__")}.draft`)));
|
|
42
43
|
return drafted.length ? drafted : gs.slice(0, 1);
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -47,7 +48,7 @@ export function filesOf(dir, type) {
|
|
|
47
48
|
export function confirmedIn(dir, file, id, seen = new Set()) {
|
|
48
49
|
if (seen.has(file)) return false;
|
|
49
50
|
seen.add(file);
|
|
50
|
-
const log = join(dir, "provenance.jsonl");
|
|
51
|
+
const log = join(ventureFile(dir, "provenance.jsonl"));
|
|
51
52
|
if (!existsSync(log)) return false;
|
|
52
53
|
const draft = `_drafts/${file.replace(/\//g, "__")}.draft`;
|
|
53
54
|
const es = readFileSync(log, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => e.entity === file || e.entity === draft);
|
|
@@ -64,8 +65,8 @@ export function foundation(dir) {
|
|
|
64
65
|
if (!s.record) return { id: s.id, status: "planned", note: s.note ?? null };
|
|
65
66
|
const files = filesOf(dir, s.record);
|
|
66
67
|
// A retired product no longer answers for the venture.
|
|
67
|
-
const values = files.filter((f) => existsSync(
|
|
68
|
-
const drafts = files.filter((f) => existsSync(
|
|
68
|
+
const values = files.filter((f) => existsSync(ventureFile(dir, f)) && load(ventureFile(dir, f)).status !== "retired").map((f) => ({ f, v: get(load(ventureFile(dir, f)), s.attribute) })).filter((x) => filled(x.v));
|
|
69
|
+
const drafts = files.filter((f) => existsSync(ventureFile(dir, `_drafts/${f.replace(/\//g, "__")}.draft`)));
|
|
69
70
|
if (!values.length) return { id: s.id, record: s.record, attribute: s.attribute, status: drafts.length ? "draft" : "missing" };
|
|
70
71
|
// A record with no log entry existed before the log; a person's baseline or write confirms it.
|
|
71
72
|
const confirmed = values.every(({ f }) => confirmedIn(dir, f, s.id));
|
|
@@ -87,7 +88,7 @@ export function foundation(dir) {
|
|
|
87
88
|
|
|
88
89
|
export function problems(dir) {
|
|
89
90
|
const out = FRAME.questions.flatMap((q) => q.statements).filter((s) => s.record && !MODEL.records.some((r) => r.id === s.record)).map((s) => `frame: ${s.id} names record type ${s.record}, which the data model lacks`);
|
|
90
|
-
const there = (...fs) => fs.map((f) =>
|
|
91
|
+
const there = (...fs) => fs.map((f) => ventureFile(dir, f)).find((p) => existsSync(p));
|
|
91
92
|
const icp = there("foundation/icp.json", "purpose/icp.json"), prob = there("foundation/problem.json", "purpose/validation/problem.json");
|
|
92
93
|
if (icp) {
|
|
93
94
|
const forms = new Set(existsSync(prob) ? (load(prob).forms ?? []).map((f) => f.id) : []);
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"id",
|
|
9
9
|
"name",
|
|
10
10
|
"role",
|
|
11
|
-
"hats",
|
|
12
11
|
"joined"
|
|
13
12
|
],
|
|
14
13
|
"additionalProperties": false,
|
|
@@ -46,7 +45,8 @@
|
|
|
46
45
|
"capabilities"
|
|
47
46
|
]
|
|
48
47
|
},
|
|
49
|
-
"uniqueItems": true
|
|
48
|
+
"uniqueItems": true,
|
|
49
|
+
"description": "The functions this founder owns, in the flat layout. In the structured layout (EE-086) they are read from venture.json"
|
|
50
50
|
},
|
|
51
51
|
"joined": {
|
|
52
52
|
"type": "string",
|
package/schema/frames.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
24
24
|
import { tmpdir } from "node:os";
|
|
25
25
|
import { join, resolve } from "node:path";
|
|
26
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
26
27
|
|
|
27
28
|
const repo = new URL("..", import.meta.url).pathname;
|
|
28
29
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -93,9 +94,9 @@ export async function props(dir) {
|
|
|
93
94
|
const v = load(join(dir, "venture.json"));
|
|
94
95
|
const ds = decisions(dir).decisions;
|
|
95
96
|
const answer = (id) => { const d = ds.find((x) => x.id === id); return d?.status === "decided" ? d.answer : null; };
|
|
96
|
-
const sum = existsSync(join(dir, "money/_summary.json")) ? load(join(dir, "money/_summary.json")) : null;
|
|
97
|
-
const own = existsSync(join(dir, "governance/ownership.json")) ? load(join(dir, "governance/ownership.json")) : null;
|
|
98
|
-
const people = existsSync(join(dir, "people/people.json")) ? load(join(dir, "people/people.json")) : null;
|
|
97
|
+
const sum = existsSync(join(ventureFile(dir, "money/_summary.json"))) ? load(join(ventureFile(dir, "money/_summary.json"))) : null;
|
|
98
|
+
const own = existsSync(join(ventureFile(dir, "governance/ownership.json"))) ? load(join(ventureFile(dir, "governance/ownership.json"))) : null;
|
|
99
|
+
const people = existsSync(join(ventureFile(dir, "people/people.json"))) ? load(join(ventureFile(dir, "people/people.json"))) : null;
|
|
99
100
|
const out = {
|
|
100
101
|
phase: v.phase,
|
|
101
102
|
goal: answer("discovery-goal") ?? NONE,
|
package/schema/freshness.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import { agentFromEnv, entries as provEntries, record as prov } from "../interfa
|
|
|
39
39
|
import { files as ventureFiles, glob, resolve as resolveRoute, routes } from "../interface/router.mjs";
|
|
40
40
|
import { connect, LOCAL, sourcesOf } from "../knowledge/connectors/index.mjs";
|
|
41
41
|
import { decisions as decisionMatrix, PHASES } from "./decisions.mjs";
|
|
42
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
42
43
|
|
|
43
44
|
const repo = new URL("..", import.meta.url).pathname;
|
|
44
45
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
@@ -83,7 +84,7 @@ const human = (s) => s % 604800 === 0 && s ? `${s / 604800}w` : s % 86400 === 0
|
|
|
83
84
|
*/
|
|
84
85
|
export function profiles(dir) {
|
|
85
86
|
const base = load(join(repo, DEFAULTS));
|
|
86
|
-
const cfg = dir && existsSync(join(dir, "naive.config.json")) ? load(join(dir, "naive.config.json")).freshness ?? {} : {};
|
|
87
|
+
const cfg = dir && existsSync(join(ventureFile(dir, "naive.config.json"))) ? load(join(ventureFile(dir, "naive.config.json"))).freshness ?? {} : {};
|
|
87
88
|
const out = { default: cfg.default ?? base.default, profiles: { ...base.profiles, ...(cfg.profiles ?? {}) }, records: { ...base.records, ...(cfg.records ?? {}) }, overridden: Object.keys(cfg).length > 0 };
|
|
88
89
|
const where = (k) => (cfg.profiles?.[k] || cfg.records?.[k] || k === "default") && out.overridden ? "naive.config.json freshness" : DEFAULTS;
|
|
89
90
|
for (const [name, p] of Object.entries(out.profiles)) {
|
|
@@ -124,7 +125,7 @@ export function recordFiles(dir) {
|
|
|
124
125
|
|
|
125
126
|
/** The freshness log: revalidate, verify and phase events, oldest first. */
|
|
126
127
|
export function events(dir) {
|
|
127
|
-
const f =
|
|
128
|
+
const f = ventureFile(dir, LOG);
|
|
128
129
|
return existsSync(f) ? readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l)) : [];
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -172,7 +173,7 @@ const at = (obj, pointer) => pointer.split("/").slice(1).map((k) => k.replace(/~
|
|
|
172
173
|
// parent record; a child that does not show the parent's value has drifted. Children elsewhere
|
|
173
174
|
// are read through their connector when it is a file system, else left to the platform.
|
|
174
175
|
async function whereUsed(dir, byRoute, byPath, unchecked) {
|
|
175
|
-
const f = join(dir, "knowledge/where-used.json");
|
|
176
|
+
const f = join(ventureFile(dir, "knowledge/where-used.json"));
|
|
176
177
|
if (!existsSync(f)) return [];
|
|
177
178
|
const ids = new Map(sourcesOf(dir).sources.map((s) => [s.id, s]));
|
|
178
179
|
const out = [];
|
|
@@ -181,13 +182,13 @@ async function whereUsed(dir, byRoute, byPath, unchecked) {
|
|
|
181
182
|
const route = r ? (r.collection === "item" ? r.route.replace("[id]", params.id) : r.route) : fact.parent.route;
|
|
182
183
|
const parent = byRoute.get(route);
|
|
183
184
|
if (!parent || !parent.endsWith(".json")) { unchecked.push(`${fact.id}: parent ${fact.parent.route} is not a JSON record in the venture folder`); continue; }
|
|
184
|
-
const value = at(load(
|
|
185
|
+
const value = at(load(ventureFile(dir, parent)), fact.parent.pointer);
|
|
185
186
|
for (const child of fact.children) {
|
|
186
187
|
if (child.status === "frozen") continue;
|
|
187
188
|
const s = ids.get(child.source);
|
|
188
189
|
if (!s || s.connector !== "filesystem") { unchecked.push(`${fact.id} -> ${child.source}://${child.path}: needs the platform to check`); continue; }
|
|
189
190
|
let content;
|
|
190
|
-
if (s.id === LOCAL) content = existsSync(
|
|
191
|
+
if (s.id === LOCAL) content = existsSync(ventureFile(dir, child.path)) ? readFileSync(ventureFile(dir, child.path), "utf8") : null;
|
|
191
192
|
else content = (await (await connect(s, dir)).read(child.path))?.content ?? null;
|
|
192
193
|
const shows = content === null ? false : child.match.kind === "json"
|
|
193
194
|
? (() => { try { return JSON.stringify(at(JSON.parse(content), child.match.pointer)) === JSON.stringify(value); } catch { return false; } })()
|
|
@@ -217,17 +218,17 @@ export async function freshness(dir, { now, light = false } = {}) {
|
|
|
217
218
|
const change = phaseChange(dir, venture, evs, warnings);
|
|
218
219
|
const lastOf = (path, kinds) => { const e = log.filter((x) => x.entity === path && kinds.has(x.activity)).at(-1); return e ? Date.parse(e.at) : null; };
|
|
219
220
|
// The last time a file's content changed: its last logged write, else its file time.
|
|
220
|
-
const changedAt = (path) => lastOf(path, CONTENT) ?? (existsSync(
|
|
221
|
+
const changedAt = (path) => lastOf(path, CONTENT) ?? (existsSync(ventureFile(dir, path)) ? statSync(ventureFile(dir, path)).mtimeMs : null);
|
|
221
222
|
|
|
222
223
|
for (const i of items) {
|
|
223
224
|
const write = lastOf(i.path, CONTENT), confirmed = lastOf(i.path, CHECKED);
|
|
224
225
|
const verified = evs.filter((e) => e.event === "verify" && e.target && covers(e.target, i, dir)).map((e) => Date.parse(e.at)).at(-1) ?? null;
|
|
225
226
|
const known = [[write, "provenance log: written"], [confirmed, "provenance log: confirmed"], [verified, "freshness log: verified"]].filter(([x]) => x !== null).sort((a, b) => b[0] - a[0]);
|
|
226
|
-
i.checked_at = known.length ? known[0][0] : statSync(
|
|
227
|
+
i.checked_at = known.length ? known[0][0] : statSync(ventureFile(dir, i.path)).mtimeMs;
|
|
227
228
|
i.basis = known.length ? known[0][1] : "file time (not in the provenance log)";
|
|
228
|
-
i.changed_at = write ?? statSync(
|
|
229
|
+
i.changed_at = write ?? statSync(ventureFile(dir, i.path)).mtimeMs;
|
|
229
230
|
const latest = log.filter((x) => x.entity === i.path && x.derived_from?.length && CONTENT.has(x.activity)).at(-1);
|
|
230
|
-
i.depends_on = normalizeDeps(latest?.derived_from, byRoute, log).filter((d) => d !== i.path && (byPath.has(d) || existsSync(
|
|
231
|
+
i.depends_on = normalizeDeps(latest?.derived_from, byRoute, log).filter((d) => d !== i.path && (byPath.has(d) || existsSync(ventureFile(dir, d))));
|
|
231
232
|
i.profile = profileFor(P, i.record, i.route);
|
|
232
233
|
i.reasons = [];
|
|
233
234
|
}
|
|
@@ -356,7 +357,7 @@ function append(dir, event, by) {
|
|
|
356
357
|
rmSync(scratch, { recursive: true, force: true });
|
|
357
358
|
if (v.status !== 0) throw new Error(`does not match schema/freshness-log.schema.json:\n${(v.stdout + v.stderr).trim()}`);
|
|
358
359
|
mkdirSync(join(dir, dirname(LOG)), { recursive: true });
|
|
359
|
-
appendFileSync(
|
|
360
|
+
appendFileSync(ventureFile(dir, LOG), JSON.stringify(line) + "\n");
|
|
360
361
|
const what = event.target ? `${event.target.kind}:${event.target.value}` : `${event.from} -> ${event.to}`;
|
|
361
362
|
const entry = prov(dir, { entity: LOG, route: "knowledge/freshness-log", activity: "write", agent: by, note: `${event.event} ${what}${event.reason ? `: ${event.reason}` : ""}` });
|
|
362
363
|
return { line, provenance: entry.hash };
|
|
@@ -437,10 +438,10 @@ export async function freeze(dir, out, { now } = {}) {
|
|
|
437
438
|
if (!rel.startsWith("..") && !isAbsolute(rel) && !rel.split("/")[0].startsWith(".")) return { ok: false, error: `${out} is inside the venture: freeze into a folder outside it, or under a dot folder such as .release/` };
|
|
438
439
|
const byPath = new Map(f.items.map((i) => [i.path, i]));
|
|
439
440
|
mkdirSync(target, { recursive: true });
|
|
440
|
-
for (const p of f.shell.frozen) { mkdirSync(dirname(join(target, p)), { recursive: true }); copyFileSync(
|
|
441
|
+
for (const p of f.shell.frozen) { mkdirSync(dirname(join(target, p)), { recursive: true }); copyFileSync(ventureFile(dir, p), join(target, p)); }
|
|
441
442
|
const manifest = {
|
|
442
443
|
venture: f.venture, phase: f.phase, frozen_at: f.now,
|
|
443
|
-
frozen: f.shell.frozen.map((p) => { const i = byPath.get(p); return { path: p, route: i.route, record: i.record, state: i.state, sha256: createHash("sha256").update(readFileSync(
|
|
444
|
+
frozen: f.shell.frozen.map((p) => { const i = byPath.get(p); return { path: p, route: i.route, record: i.record, state: i.state, sha256: createHash("sha256").update(readFileSync(ventureFile(dir, p))).digest("hex"), ...(i.state === "stale" ? { reasons: i.reasons.map((r) => r.note) } : {}) }; }),
|
|
444
445
|
holes: f.shell.holes.map((p) => { const i = byPath.get(p); return { path: p, route: i.route, record: i.record, profile: i.profile, read: `mentura ${i.route} read --venture ${f.venture}` }; }),
|
|
445
446
|
};
|
|
446
447
|
writeFileSync(join(target, "shell.json"), JSON.stringify(manifest, null, 2) + "\n");
|
package/schema/gate-drafts.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { basename, dirname, join } from "node:path";
|
|
16
16
|
import { approve } from "./decisions.mjs";
|
|
17
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
17
18
|
|
|
18
19
|
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
19
20
|
const LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
|
@@ -42,7 +43,7 @@ export function sidecar(dir, draft) {
|
|
|
42
43
|
* Returns { written, variant, variants, decision }.
|
|
43
44
|
*/
|
|
44
45
|
export function writeHeld(dir, { draft, file, text, verdict, route, agent = {} }) {
|
|
45
|
-
const dest =
|
|
46
|
+
const dest = ventureFile(dir, draft);
|
|
46
47
|
mkdirSync(dirname(dest), { recursive: true });
|
|
47
48
|
const at = new Date().toISOString();
|
|
48
49
|
const by = agent.id ?? agent.kind ?? "agent";
|
|
@@ -64,7 +65,7 @@ export function writeHeld(dir, { draft, file, text, verdict, route, agent = {} }
|
|
|
64
65
|
const next = LETTERS[variants.length];
|
|
65
66
|
if (!next) throw new Error(`${route} already holds ${variants.length} drafts side by side; accept one first`);
|
|
66
67
|
const vfile = variantPath(draft, next);
|
|
67
|
-
writeFileSync(
|
|
68
|
+
writeFileSync(ventureFile(dir, vfile), text);
|
|
68
69
|
variants.push({ variant: next, file: vfile, by, at, sha256: sha(text) });
|
|
69
70
|
const dec = variantsDecision(file);
|
|
70
71
|
const record = {
|
|
@@ -79,8 +80,8 @@ export function writeHeld(dir, { draft, file, text, verdict, route, agent = {} }
|
|
|
79
80
|
note: `The gate kept ${variants.length} differing drafts side by side (rule ${verdict.rule}). Accept one: mentura accept ${route} --pick <letter>`,
|
|
80
81
|
provenance: { drafted_by: { kind: "framework" } },
|
|
81
82
|
};
|
|
82
|
-
mkdirSync(dirname(
|
|
83
|
-
writeFileSync(
|
|
83
|
+
mkdirSync(dirname(ventureFile(dir, dec.path)), { recursive: true });
|
|
84
|
+
writeFileSync(ventureFile(dir, dec.path), JSON.stringify(record, null, 2) + "\n");
|
|
84
85
|
writeFileSync(join(dir, sidecarOf(draft)), JSON.stringify({ ...(meta ?? { route, file, rule: verdict.rule, source: verdict.source, then: verdict.then, ...(verdict.owner ? { owner: verdict.owner, function: verdict.function ?? null } : {}) }), variants, decision: dec.id }, null, 2) + "\n");
|
|
85
86
|
return { written: vfile, variant: next, variants: variants.map((v) => v.variant), decision: dec.id };
|
|
86
87
|
}
|
|
@@ -99,8 +100,8 @@ export function pick(dir, draft, letter) {
|
|
|
99
100
|
/** After a person accepts a draft: removes every variant and the sidecar, and decides the variants decision. */
|
|
100
101
|
export function settle(dir, draft, { picked = "a", by } = {}) {
|
|
101
102
|
const meta = sidecar(dir, draft);
|
|
102
|
-
for (const v of meta?.variants ?? []) rmSync(
|
|
103
|
-
rmSync(
|
|
103
|
+
for (const v of meta?.variants ?? []) rmSync(ventureFile(dir, v.file), { force: true });
|
|
104
|
+
rmSync(ventureFile(dir, draft), { force: true });
|
|
104
105
|
rmSync(join(dir, sidecarOf(draft)), { force: true });
|
|
105
106
|
if (!meta?.decision) return { picked };
|
|
106
107
|
const d = approve(dir, meta.decision, { by: by ?? process.env.MENTURA_PERSON ?? "founder", answer: picked, note: `accepted draft ${picked} of ${meta.route}` });
|
|
@@ -109,7 +110,7 @@ export function settle(dir, draft, { picked = "a", by } = {}) {
|
|
|
109
110
|
|
|
110
111
|
/** The drafts waiting in a venture: route, rule, owner and variants. A draft with no sidecar was held by its decision class. */
|
|
111
112
|
export function inbox(dir, owner) {
|
|
112
|
-
const d = join(dir, "_drafts");
|
|
113
|
+
const d = join(ventureFile(dir, "_drafts"));
|
|
113
114
|
if (!existsSync(d)) return [];
|
|
114
115
|
return readdirSync(d).filter((f) => f.endsWith(".draft")).sort().map((f) => {
|
|
115
116
|
const draft = `_drafts/${f}`;
|
package/schema/init-venture.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { spawnSync } from "node:child_process";
|
|
|
19
19
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { dirname, join, relative } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { KBASE, ventureFile } from "./venture-paths.mjs";
|
|
22
23
|
|
|
23
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const repo = join(here, "..");
|
|
@@ -26,6 +27,10 @@ const { layers, formalized, pillars, readme } = JSON.parse(readFileSync(join(her
|
|
|
26
27
|
const args = process.argv.slice(2);
|
|
27
28
|
const flag = (name) => { const i = args.indexOf(`--${name}`); return i < 0 ? null : args.splice(i, 2)[1]; };
|
|
28
29
|
const phase = flag("phase") ?? "discovery", parent = flag("parent"), legalForm = flag("legal-form"), only = flag("layers"), typeId = flag("type");
|
|
30
|
+
// --layout structured (EE-086): knowledge/, meta/ and an org chart in venture.json; without it, the flat layout.
|
|
31
|
+
const layout = flag("layout") ?? "flat";
|
|
32
|
+
if (!["flat", "structured"].includes(layout)) { console.error(`unknown layout ${layout}: flat or structured`); process.exit(2); }
|
|
33
|
+
const structured = layout === "structured";
|
|
29
34
|
const vtype = typeId ? JSON.parse(readFileSync(join(here, "venture-types.json"), "utf8")).types.find((t) => t.id === typeId) : null;
|
|
30
35
|
if (typeId && !vtype) { console.error(`unknown venture type ${typeId}: see node schema/venture-type.mjs list`); process.exit(2); }
|
|
31
36
|
const [dir, id] = args;
|
|
@@ -41,27 +46,47 @@ mkdirSync(dir, { recursive: true });
|
|
|
41
46
|
const venture = { id, name: id, ...(vtype ? { type: vtype.id } : {}), parent: parent ?? null, phase };
|
|
42
47
|
if (legalForm || vtype?.funding) venture.form = { ...(legalForm ? { legal_form: legalForm } : {}), ...(vtype?.funding ? { funding: vtype.funding } : {}) };
|
|
43
48
|
if (phase !== "discovery") venture.core = { promise: "", promised_to: "", must_not: "", competence: "" };
|
|
44
|
-
Object.assign(venture, { owner: "self", status: "active", profile: "profile.md", layers: {} });
|
|
45
|
-
|
|
49
|
+
Object.assign(venture, { owner: "self", status: "active", profile: structured ? "README.md" : "profile.md", layers: {} });
|
|
50
|
+
// The package folders (EE-070); a typed venture gets its type's packages.
|
|
51
|
+
const packageDirs = vtype ? vtype.packages : ["foundation", "validation", "product", "marketing", "finance", "people", "customers", "investors", "money"];
|
|
52
|
+
if (structured) {
|
|
53
|
+
// venture.json first, so every path below resolves into k-base/ and meta/. The org chart starts flat:
|
|
54
|
+
// one function per package, owned by no one until create-venture or the founder names the owner.
|
|
55
|
+
venture.layout = "structured";
|
|
56
|
+
venture.functions = Object.fromEntries(packageDirs.map((p) => [p, { owner: null }]));
|
|
57
|
+
for (const layer of ALL) venture.layers[layer] = on.includes(layer) ? { location: `${KBASE}/` } : false;
|
|
58
|
+
writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
|
|
59
|
+
for (const sub of [KBASE, "meta"]) mkdirSync(join(dir, sub), { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
for (const layer of structured ? [] : ALL) {
|
|
46
62
|
if (!on.includes(layer)) { if (only !== null || !["governance", "interface"].includes(layer)) venture.layers[layer] = false; continue; }
|
|
47
|
-
mkdirSync(
|
|
63
|
+
mkdirSync(ventureFile(dir, layer), { recursive: true });
|
|
48
64
|
writeFileSync(join(dir, layer, "README.md"), `# ${layer}\n\n${readme[layer]}\n`);
|
|
49
65
|
venture.layers[layer] = { location: `${layer}/` };
|
|
50
66
|
}
|
|
51
|
-
for (const sub of ["products", "accounts"]) { mkdirSync(
|
|
67
|
+
if (!structured) for (const sub of ["products", "accounts"]) { mkdirSync(ventureFile(dir, sub), { recursive: true }); writeFileSync(join(dir, sub, ".gitkeep"), ""); }
|
|
52
68
|
// The package folder layout (EE-070). Old layer folders above stay for one release.
|
|
53
69
|
// A typed venture gets its type's packages; the rest stay out until need-now suggests them.
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
for (const sub of ["raw/dump", "raw/sources", "ideas", ...packageDirs, "decisions", "evidence", ...(structured ? [] : ["public"]), "_private", "_drafts", ".release"]) {
|
|
71
|
+
mkdirSync(ventureFile(dir, sub), { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
// Every package folder explains itself (EE-086): a README.md from its package, for people. GitHub and
|
|
74
|
+
// editors show it when the folder is opened; an AGENTS.md beside it is the founder's, for agents.
|
|
75
|
+
if (structured) for (const pkg of packageDirs) {
|
|
76
|
+
const f = join(repo, "packages", pkg, "package.json");
|
|
77
|
+
if (!existsSync(f)) continue;
|
|
78
|
+
const p = JSON.parse(readFileSync(f, "utf8"));
|
|
79
|
+
const lines = [`# ${p.title ?? pkg}`, "", p.purpose ?? "", ...(p.required?.length ? ["", `What goes in it first: ${p.required.join(", ")}.`] : []), ...(p.optional?.length ? [`Also: ${p.optional.join(", ")}.`] : []),
|
|
80
|
+
...(p.gates?.length ? ["", `It is ready when ${p.gates.join("; ")}.`] : []), "", `Owner: whoever owns the ${pkg} function in venture.json.`, ""];
|
|
81
|
+
writeFileSync(join(ventureFile(dir, pkg), "README.md"), lines.join("\n"));
|
|
57
82
|
}
|
|
58
|
-
mkdirSync(join(dir, "pillars"), { recursive: true });
|
|
59
|
-
for (const [name, what] of Object.entries(pillars.stubs)) {
|
|
60
|
-
const guide = relative(join(dir, "pillars"), join(repo, pillars.guides, `${name}.md`));
|
|
61
|
-
writeFileSync(join(dir, "pillars", `${name}.md`), `# ${name[0].toUpperCase() + name.slice(1)} pillar\n\n${what}\n\nGuide: [${name} pillar](${guide})\n`);
|
|
83
|
+
if (!structured) mkdirSync(join(ventureFile(dir, "pillars")), { recursive: true });
|
|
84
|
+
for (const [name, what] of Object.entries(structured ? {} : pillars.stubs)) {
|
|
85
|
+
const guide = relative(join(ventureFile(dir, "pillars")), join(repo, pillars.guides, `${name}.md`));
|
|
86
|
+
writeFileSync(join(ventureFile(dir, "pillars"), `${name}.md`), `# ${name[0].toUpperCase() + name.slice(1)} pillar\n\n${what}\n\nGuide: [${name} pillar](${guide})\n`);
|
|
62
87
|
}
|
|
63
|
-
writeFileSync(join(dir, "profile.md"), `# ${id}\n\nWhat this venture is, in a few sentences. Its machine fields are in venture.json (EE-011).\n\n## Changelog\n`);
|
|
64
|
-
writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
|
|
88
|
+
writeFileSync(join(ventureFile(dir, "profile.md")), `# ${id}\n\nWhat this venture is, in a few sentences. Its machine fields are in venture.json (EE-011).\n\n## Changelog\n`);
|
|
89
|
+
if (!structured) writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
|
|
65
90
|
// The venture's root layout (EE-072, I-4): every route's layout chain starts here. Lines shaped
|
|
66
91
|
// "key: value" are instructions the router hands an agent with the layout.
|
|
67
92
|
writeFileSync(join(dir, "AGENTS.md"), `# AGENTS.md: ${id}
|
|
@@ -97,8 +122,8 @@ const productFoundation = [
|
|
|
97
122
|
];
|
|
98
123
|
// The team question (EE-060): asked in Discovery, failing open, so validation never waits on it. A
|
|
99
124
|
// founding team, or a founder looking for one, gets the founder-agreement kit beside validation.
|
|
100
|
-
mkdirSync(join(dir, "decisions"), { recursive: true });
|
|
101
|
-
writeFileSync(join(dir, "decisions/venture.team.decision.json"), JSON.stringify({ id: "founding-team", question: "Who is founding this venture: you alone, you with cofounders, or you looking for cofounders?",
|
|
125
|
+
mkdirSync(join(ventureFile(dir, "decisions")), { recursive: true });
|
|
126
|
+
writeFileSync(join(ventureFile(dir, "decisions/venture.team.decision.json")), JSON.stringify({ id: "founding-team", question: "Who is founding this venture: you alone, you with cofounders, or you looking for cofounders?",
|
|
102
127
|
decides: { record: "founder-agreement", path: "governance/founder-agreement.json" }, layer: "governance", boundary: "fail-open", needed_in: phase, status: "open",
|
|
103
128
|
options: ["Solo: I am founding it alone", "Cofounders: we are founding it together", "Looking: I want cofounders"],
|
|
104
129
|
unlocks: [{ activity: "Draft the founder agreement with the founding-agreement kit", routes: ["governance/founder-agreement"] }], provenance: framework }, null, 2) + "\n");
|
|
@@ -113,15 +138,18 @@ const fundFoundation = [
|
|
|
113
138
|
];
|
|
114
139
|
const decisionSet = vtype?.decisions ?? "product-foundation";
|
|
115
140
|
if (decisionSet === "fund-foundation") {
|
|
116
|
-
mkdirSync(join(dir, "fund/decisions"), { recursive: true });
|
|
117
|
-
for (const d of fundFoundation) writeFileSync(join(dir, "fund/decisions", `${d.id === "first-close" ? "terms.first-close" : d.id.replace(/^fund-/, "")}.decision.json`),
|
|
141
|
+
mkdirSync(join(ventureFile(dir, "fund/decisions")), { recursive: true });
|
|
142
|
+
for (const d of fundFoundation) writeFileSync(join(ventureFile(dir, "fund/decisions"), `${d.id === "first-close" ? "terms.first-close" : d.id.replace(/^fund-/, "")}.decision.json`),
|
|
118
143
|
JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
|
|
119
144
|
}
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
// The product questions sit beside the products: product/ in the structured layout, products/ in the flat one.
|
|
146
|
+
const productDecisions = structured ? "product/decisions" : "products/decisions";
|
|
147
|
+
if (decisionSet === "product-foundation") mkdirSync(join(ventureFile(dir, productDecisions)), { recursive: true });
|
|
148
|
+
if (decisionSet === "product-foundation") for (const d of productFoundation) writeFileSync(join(ventureFile(dir, productDecisions), `product.${d.id.replace(/^product-/, "")}.decision.json`),
|
|
122
149
|
JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
|
|
123
150
|
// The type's functions are imported like any department (EE-047): listed in venture.json, with their folders.
|
|
124
|
-
|
|
151
|
+
// In the structured layout the org chart is venture.json's functions; the flat layout imports departments.
|
|
152
|
+
if (vtype && !structured) for (const d of vtype.departments) {
|
|
125
153
|
const r = spawnSync("node", [join(here, "add-department.mjs"), dir, d], { encoding: "utf8" });
|
|
126
154
|
if (r.status !== 0) { console.error((r.stderr || r.stdout).trim()); process.exit(1); }
|
|
127
155
|
}
|