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.
- 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/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/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 +5 -4
- 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 +38 -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 +85 -0
- package/schema/venture.schema.json +25 -17
- package/schema/workspace.schema.json +5 -1
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 { 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,37 @@ 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 knowledge/ 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: "knowledge/" } : false;
|
|
58
|
+
writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
|
|
59
|
+
for (const sub of ["knowledge", "meta"]) mkdirSync(ventureFile(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
|
-
mkdirSync(join(dir, sub), { recursive: true });
|
|
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 });
|
|
57
72
|
}
|
|
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`);
|
|
73
|
+
if (!structured) mkdirSync(join(ventureFile(dir, "pillars")), { recursive: true });
|
|
74
|
+
for (const [name, what] of Object.entries(structured ? {} : pillars.stubs)) {
|
|
75
|
+
const guide = relative(join(ventureFile(dir, "pillars")), join(repo, pillars.guides, `${name}.md`));
|
|
76
|
+
writeFileSync(join(ventureFile(dir, "pillars"), `${name}.md`), `# ${name[0].toUpperCase() + name.slice(1)} pillar\n\n${what}\n\nGuide: [${name} pillar](${guide})\n`);
|
|
62
77
|
}
|
|
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");
|
|
78
|
+
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`);
|
|
79
|
+
if (!structured) writeFileSync(join(dir, "venture.json"), JSON.stringify(venture, null, 2) + "\n");
|
|
65
80
|
// The venture's root layout (EE-072, I-4): every route's layout chain starts here. Lines shaped
|
|
66
81
|
// "key: value" are instructions the router hands an agent with the layout.
|
|
67
82
|
writeFileSync(join(dir, "AGENTS.md"), `# AGENTS.md: ${id}
|
|
@@ -97,8 +112,8 @@ const productFoundation = [
|
|
|
97
112
|
];
|
|
98
113
|
// The team question (EE-060): asked in Discovery, failing open, so validation never waits on it. A
|
|
99
114
|
// 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?",
|
|
115
|
+
mkdirSync(join(ventureFile(dir, "decisions")), { recursive: true });
|
|
116
|
+
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
117
|
decides: { record: "founder-agreement", path: "governance/founder-agreement.json" }, layer: "governance", boundary: "fail-open", needed_in: phase, status: "open",
|
|
103
118
|
options: ["Solo: I am founding it alone", "Cofounders: we are founding it together", "Looking: I want cofounders"],
|
|
104
119
|
unlocks: [{ activity: "Draft the founder agreement with the founding-agreement kit", routes: ["governance/founder-agreement"] }], provenance: framework }, null, 2) + "\n");
|
|
@@ -113,15 +128,18 @@ const fundFoundation = [
|
|
|
113
128
|
];
|
|
114
129
|
const decisionSet = vtype?.decisions ?? "product-foundation";
|
|
115
130
|
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`),
|
|
131
|
+
mkdirSync(join(ventureFile(dir, "fund/decisions")), { recursive: true });
|
|
132
|
+
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
133
|
JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
|
|
119
134
|
}
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
// The product questions sit beside the products: product/ in the structured layout, products/ in the flat one.
|
|
136
|
+
const productDecisions = structured ? "product/decisions" : "products/decisions";
|
|
137
|
+
if (decisionSet === "product-foundation") mkdirSync(join(ventureFile(dir, productDecisions)), { recursive: true });
|
|
138
|
+
if (decisionSet === "product-foundation") for (const d of productFoundation) writeFileSync(join(ventureFile(dir, productDecisions), `product.${d.id.replace(/^product-/, "")}.decision.json`),
|
|
122
139
|
JSON.stringify({ ...d, layer: "purpose", boundary: "fail-closed", needed_in: phase, status: "open", provenance: framework }, null, 2) + "\n");
|
|
123
140
|
// The type's functions are imported like any department (EE-047): listed in venture.json, with their folders.
|
|
124
|
-
|
|
141
|
+
// In the structured layout the org chart is venture.json's functions; the flat layout imports departments.
|
|
142
|
+
if (vtype && !structured) for (const d of vtype.departments) {
|
|
125
143
|
const r = spawnSync("node", [join(here, "add-department.mjs"), dir, d], { encoding: "utf8" });
|
|
126
144
|
if (r.status !== 0) { console.error((r.stderr || r.stdout).trim()); process.exit(1); }
|
|
127
145
|
}
|
|
@@ -21,6 +21,7 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import { call, routes } from "../interface/router.mjs";
|
|
22
22
|
import { agentFromEnv, record as prov } from "../interface/provenance.mjs";
|
|
23
23
|
import { propose } from "./decisions.mjs";
|
|
24
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
24
25
|
|
|
25
26
|
export const PLAN = ".release/migrate-plan.json";
|
|
26
27
|
const MAX_FILE = 512 * 1024;
|
|
@@ -117,7 +118,7 @@ Files: ${JSON.stringify(Object.fromEntries(given.map((f) => [f.path, f.text])))}
|
|
|
117
118
|
/** Writes the plan into the venture and proposes one open decision per function it touches. */
|
|
118
119
|
export function proposeDecisions(dir, p) {
|
|
119
120
|
mkdirSync(join(dir, dirname(PLAN)), { recursive: true });
|
|
120
|
-
writeFileSync(
|
|
121
|
+
writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
|
|
121
122
|
prov(dir, { entity: PLAN, activity: "draft", agent: agentFromEnv("agent", "migrate", { id: "naive-migrate" }), note: `migrate plan from ${p.from}: ${p.items.length} records, ${p.left_behind.length} left behind` });
|
|
122
123
|
const groups = new Map();
|
|
123
124
|
for (const it of p.items) groups.set(functionOf(it.route), [...(groups.get(functionOf(it.route)) ?? []), it]);
|
|
@@ -139,8 +140,8 @@ const answerOf = (d) => { const a = String(d?.answer ?? "").toLowerCase(); retur
|
|
|
139
140
|
|
|
140
141
|
/** Writes what a person decided, through the router. Open decisions wait. */
|
|
141
142
|
export async function apply(dir) {
|
|
142
|
-
const p = load(
|
|
143
|
-
const decisionOf = (fn) => { const f = join(dir, "decisions", `migrate-${fn}.decision.json`); return existsSync(f) ? load(f) : null; };
|
|
143
|
+
const p = load(ventureFile(dir, PLAN));
|
|
144
|
+
const decisionOf = (fn) => { const f = join(ventureFile(dir, "decisions"), `migrate-${fn}.decision.json`); return existsSync(f) ? load(f) : null; };
|
|
144
145
|
const done = { brought: 0, drafted: 0, pointed: 0, left: 0, waiting: 0, failed: 0 };
|
|
145
146
|
let pointed = false;
|
|
146
147
|
for (const it of p.items.filter((x) => x.status === "planned")) {
|
|
@@ -157,7 +158,7 @@ export async function apply(dir) {
|
|
|
157
158
|
done[it.status]++;
|
|
158
159
|
}
|
|
159
160
|
if (pointed) {
|
|
160
|
-
const f = join(dir, "knowledge/sources.json");
|
|
161
|
+
const f = join(ventureFile(dir, "knowledge/sources.json"));
|
|
161
162
|
const s = existsSync(f) ? load(f) : { sources: [], records: {} };
|
|
162
163
|
const id = slug(basename(p.from));
|
|
163
164
|
if (!s.sources.some((x) => x.id === id)) s.sources.push({ id, connector: "filesystem", writes: "none", root: p.from, note: "migrate: a person chose to point to these files rather than rewrite them" });
|
|
@@ -165,7 +166,7 @@ export async function apply(dir) {
|
|
|
165
166
|
writeFileSync(f, JSON.stringify(s, null, 2) + "\n");
|
|
166
167
|
prov(dir, { entity: "knowledge/sources.json", activity: "write", agent: agentFromEnv("agent", "migrate", { id: "naive-migrate" }), note: `points to ${p.from}, read-only` });
|
|
167
168
|
}
|
|
168
|
-
writeFileSync(
|
|
169
|
+
writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
|
|
169
170
|
return done;
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -176,7 +177,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
176
177
|
if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node schema/migrate-venture.mjs --from <project> --venture <dir> | --venture <dir> --apply"); process.exit(2); }
|
|
177
178
|
try {
|
|
178
179
|
if (args.includes("--apply")) {
|
|
179
|
-
if (!existsSync(
|
|
180
|
+
if (!existsSync(ventureFile(dir, PLAN))) { console.error(`no plan at ${ventureFile(dir, PLAN)}: run with --from first`); process.exit(1); }
|
|
180
181
|
const d = await apply(dir);
|
|
181
182
|
console.log(Object.entries(d).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "nothing to do");
|
|
182
183
|
} else {
|
|
@@ -186,7 +187,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
186
187
|
for (const i of p.items) console.log(`${i.id} ${i.from.join(", ")} -> ${i.route} (${i.reshaping})`);
|
|
187
188
|
for (const l of p.left_behind) console.log(`left ${l.from} (${l.why})`);
|
|
188
189
|
console.log(`\n${p.items.length} records planned, ${p.left_behind.length} left behind. Nothing is written yet.`);
|
|
189
|
-
if (decisions.length) console.log(`Read ${relative(process.cwd(),
|
|
190
|
+
if (decisions.length) console.log(`Read ${relative(process.cwd(), ventureFile(dir, PLAN))}, then decide ${decisions.join(", ")} with mentura decide (answer "bring in", "point to them" or "leave them"), then run with --apply.`);
|
|
190
191
|
}
|
|
191
192
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
192
193
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { ventureFile } from "../venture-paths.mjs";
|
|
17
18
|
|
|
18
19
|
export const id = "kit-to-package";
|
|
19
20
|
export const describe = "Moves Naive packages out of naive.config.json kits into packages, and rewrites layer and kit routes inside records (decision unlocks, team-plan access, where-used parents) to package routes";
|
|
@@ -67,15 +68,15 @@ export function apply(dir, ctx) {
|
|
|
67
68
|
return r.to;
|
|
68
69
|
};
|
|
69
70
|
const rewriteFile = (file, fn) => {
|
|
70
|
-
if (!existsSync(
|
|
71
|
-
const before = readFileSync(
|
|
71
|
+
if (!existsSync(ventureFile(dir, file))) return;
|
|
72
|
+
const before = readFileSync(ventureFile(dir, file), "utf8");
|
|
72
73
|
const rec = JSON.parse(before);
|
|
73
74
|
fn(rec);
|
|
74
75
|
const after = JSON.stringify(rec, null, 2) + "\n";
|
|
75
76
|
if (after.trim() !== before.trim()) ctx.write(file, after, { note: "routes renamed from layer and kit routes to package routes (EE-071)" });
|
|
76
77
|
};
|
|
77
78
|
|
|
78
|
-
const decisionFiles = (sub = "") => readdirSync(
|
|
79
|
+
const decisionFiles = (sub = "") => readdirSync(ventureFile(dir, sub), { withFileTypes: true }).flatMap((e) => {
|
|
79
80
|
const rel = sub ? `${sub}/${e.name}` : e.name;
|
|
80
81
|
if (e.isDirectory()) return e.name.startsWith(".") || e.name.startsWith("_") || existsSync(join(dir, rel, "venture.json")) ? [] : decisionFiles(rel);
|
|
81
82
|
return e.name.endsWith(".decision.json") ? [rel] : [];
|
|
@@ -8,16 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { ventureFile } from "../venture-paths.mjs";
|
|
12
|
+
import { layoutOf } from "../venture-paths.mjs";
|
|
11
13
|
|
|
12
14
|
export const id = "package-layout";
|
|
13
15
|
export const describe = "Moves records from the layer folders (purpose/, pillars/, products/, knowledge/) into package folders (foundation/, validation/, product/, marketing/, people/, decisions/)";
|
|
14
16
|
|
|
15
17
|
export function apply(dir, ctx) {
|
|
18
|
+
// A venture in the structured layout (EE-086) is already past this layout: nothing to move.
|
|
19
|
+
if (layoutOf(dir) === "structured") return;
|
|
16
20
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
17
21
|
// A decision file infers its target from the folder it sits in. Stamp that path before the file
|
|
18
22
|
// moves, so the decision still points at the same record.
|
|
19
23
|
const stampDecisions = (folder) => {
|
|
20
|
-
const d =
|
|
24
|
+
const d = ventureFile(dir, folder);
|
|
21
25
|
if (!existsSync(d)) return;
|
|
22
26
|
for (const f of readdirSync(d).filter((x) => x.endsWith(".decision.json"))) {
|
|
23
27
|
const rec = load(join(d, f));
|
|
@@ -45,7 +49,7 @@ export function apply(dir, ctx) {
|
|
|
45
49
|
];
|
|
46
50
|
for (const [a, b] of pairs) ctx.mv(a, b);
|
|
47
51
|
|
|
48
|
-
const list = (rel, ext) => existsSync(
|
|
52
|
+
const list = (rel, ext) => existsSync(ventureFile(dir, rel)) ? readdirSync(ventureFile(dir, rel)).filter((f) => f.endsWith(ext)) : [];
|
|
49
53
|
for (const f of list("products", ".json")) ctx.mv(`products/${f}`, `product/${f}`);
|
|
50
54
|
for (const f of list("purpose/anatomy", ".json")) ctx.mv(`purpose/anatomy/${f}`, `product/anatomy/${f}`);
|
|
51
55
|
|
|
@@ -55,7 +59,7 @@ export function apply(dir, ctx) {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
for (const f of ["product.md", "business.md"]) {
|
|
58
|
-
if (existsSync(join(dir, "pillars", f))) {
|
|
62
|
+
if (existsSync(join(ventureFile(dir, "pillars"), f))) {
|
|
59
63
|
ctx.mv(`pillars/${f}`, `_private/pillars/${f}`);
|
|
60
64
|
ctx.escalate(`${f.replace(".md", "")} pillar: the brief names a home only for brand (marketing/). Archived at _private/pillars/${f} rather than guessed.`);
|
|
61
65
|
}
|
|
@@ -68,8 +72,8 @@ export function apply(dir, ctx) {
|
|
|
68
72
|
for (const gone of ["purpose", "pillars", "knowledge", "products"]) ctx.removeEmpty(gone);
|
|
69
73
|
|
|
70
74
|
const policy = "governance/publication-policy.json";
|
|
71
|
-
if (existsSync(
|
|
72
|
-
const text = readFileSync(
|
|
75
|
+
if (existsSync(ventureFile(dir, policy))) {
|
|
76
|
+
const text = readFileSync(ventureFile(dir, policy), "utf8");
|
|
73
77
|
if (text.includes("pillars/brand.md")) ctx.write(policy, text.replaceAll("pillars/brand.md", "marketing/brand.md"), { note: "the brand pillar moved to marketing/brand.md (EE-070)" });
|
|
74
78
|
}
|
|
75
79
|
|