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
|
@@ -25,6 +25,7 @@ import { CONVENTIONS, MAP, parse } from "./ideamap.mjs";
|
|
|
25
25
|
import { GRAPH, ORDER } from "./graph.mjs";
|
|
26
26
|
import { confirmedIn } from "../../../schema/foundation.mjs";
|
|
27
27
|
import { loadPackages } from "../../../schema/load-packages.mjs";
|
|
28
|
+
import { ventureFile } from "../../../schema/venture-paths.mjs";
|
|
28
29
|
|
|
29
30
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -37,7 +38,7 @@ const HOME = { "venture-core": "foundation", brand: "marketing", ...Object.fromE
|
|
|
37
38
|
/** Understand is a gate (EE-069): the founder confirms Mentura read the ideas right, after any
|
|
38
39
|
* corrections, before they are routed. A person's confirm of the map after its last agent write. */
|
|
39
40
|
export function understood(dir) {
|
|
40
|
-
return existsSync(
|
|
41
|
+
return existsSync(ventureFile(dir, MAP)) && confirmedIn(dir, MAP, "ideas");
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const STAGES = [["routed", "Routed"], ["drafted", "Drafted"], ["yours", "Your decision"], ["waiting", "Waiting"], ["done", "Done"]];
|
|
@@ -62,13 +63,13 @@ function written(dir, key) {
|
|
|
62
63
|
const rel = m?.paths?.[0]?.replace(/^instance\/\*\*\//, "");
|
|
63
64
|
if (!rel) return null;
|
|
64
65
|
if (rel.includes("*")) { const d = join(dir, dirname(rel)); return existsSync(d) && readdirSync(d).some((f) => f.endsWith(".json")); }
|
|
65
|
-
return existsSync(
|
|
66
|
+
return existsSync(ventureFile(dir, rel));
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export function route(dir, { now } = {}) {
|
|
69
70
|
const venture = load(join(dir, "venture.json"));
|
|
70
|
-
const g = existsSync(
|
|
71
|
-
const nodes = g?.nodes ?? (existsSync(
|
|
71
|
+
const g = existsSync(ventureFile(dir, GRAPH)) ? load(ventureFile(dir, GRAPH)) : null;
|
|
72
|
+
const nodes = g?.nodes ?? (existsSync(ventureFile(dir, MAP)) ? parse(readFileSync(ventureFile(dir, MAP), "utf8")).ideas.map((i) => ({ ...i, signal: null, lane: null })) : []);
|
|
72
73
|
if (!nodes.length) return { venture: venture.id, phase: venture.phase, waiting: "no idea map yet: map the idea dump first (purpose/ideas)" };
|
|
73
74
|
const lanes = new Map((g?.lanes ?? []).map((l) => [l.id, l]));
|
|
74
75
|
const m = decisions(dir, now);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { ventureFile } from "../../../schema/venture-paths.mjs";
|
|
23
24
|
|
|
24
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const ENDPOINT = "https://api.typesafe.ai/v1/systemone";
|
|
@@ -39,7 +40,7 @@ function readVenture(path) {
|
|
|
39
40
|
}
|
|
40
41
|
const venture = load(join(path, "venture.json"));
|
|
41
42
|
const dir = join(path, "products");
|
|
42
|
-
const cards = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => load(
|
|
43
|
+
const cards = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => load(ventureFile(dir, f))) : [];
|
|
43
44
|
const products = Object.fromEntries(cards.map((c) => [c.id, c]));
|
|
44
45
|
const flows = cards.flatMap((c) => (c.flows_to ?? []).map((to) => ({ from: c.id, to })));
|
|
45
46
|
return { venture, core: venture.core, products, flows };
|
package/schema/access.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
11
12
|
|
|
12
13
|
export const PLANS = ["people/team-plan.json", "operations/team-plan.json"];
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@ export const routeGlob = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g,
|
|
|
16
17
|
|
|
17
18
|
/** The venture's team plan, or null. Read fresh on every call, so a changed plan is seen at once. */
|
|
18
19
|
export function teamPlan(dir) {
|
|
19
|
-
const f = dir && PLANS.map((p) =>
|
|
20
|
+
const f = dir && PLANS.map((p) => ventureFile(dir, p)).find((p) => existsSync(p));
|
|
20
21
|
return f ? { path: f, ...JSON.parse(readFileSync(f, "utf8")) } : null;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { catalog, foldersOf } from "./departments.mjs";
|
|
14
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
14
15
|
|
|
15
16
|
const args = process.argv.slice(2);
|
|
16
17
|
const group = args.includes("--group");
|
|
@@ -28,8 +29,8 @@ const tag = { department: d.id, value_chain: d.value_chain, apqc: d.apqc.id };
|
|
|
28
29
|
const made = [];
|
|
29
30
|
for (const layer of layersOn.filter((l) => !held.includes(l))) {
|
|
30
31
|
const rel = join(layer, "departments", ...(group ? [d.value_chain] : []), d.id);
|
|
31
|
-
if (existsSync(
|
|
32
|
-
mkdirSync(
|
|
32
|
+
if (existsSync(ventureFile(dir, rel))) { console.error(`${rel} exists without a department.json; add the tag by hand`); process.exit(1); }
|
|
33
|
+
mkdirSync(ventureFile(dir, rel), { recursive: true });
|
|
33
34
|
writeFileSync(join(dir, rel, "department.json"), JSON.stringify(tag, null, 2) + "\n");
|
|
34
35
|
const [file, title, what] = layer === "governance"
|
|
35
36
|
? ["charter.md", "charter", d.governance] : ["README.md", "operations", d.operations];
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { existsSync, readFileSync } from "node:fs";
|
|
28
28
|
import { join } from "node:path";
|
|
29
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
29
30
|
|
|
30
31
|
export const ASSUMPTIONS = [
|
|
31
32
|
{ id: "exists", says: "the problem is real", field: "statement", test: "Find people already paying for or building a workaround (search, forums, competitors' customers)" },
|
|
@@ -97,7 +98,7 @@ export const ASK = "Some things the assessment reads are missing. Fill them in f
|
|
|
97
98
|
|
|
98
99
|
/** The whole run: preflight first, then the assessment unless input is missing and as_is is not set. */
|
|
99
100
|
export function assess(dir, { as_is = false, today = new Date() } = {}) {
|
|
100
|
-
const f = join(dir, "purpose/validation/problem.json");
|
|
101
|
+
const f = join(ventureFile(dir, "purpose/validation/problem.json"));
|
|
101
102
|
const rec = existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null;
|
|
102
103
|
const missing = preflight(rec);
|
|
103
104
|
if (missing.length && !as_is) return { needs_input: { ask: ASK, missing, options: [
|
|
@@ -117,7 +118,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
117
118
|
const dir = process.argv[2];
|
|
118
119
|
if (!dir) { console.error("usage: node schema/assess-problem.mjs <venture dir> [--json] [--preflight] [--as-is]"); process.exit(2); }
|
|
119
120
|
const flag = (f) => process.argv.includes(f);
|
|
120
|
-
const out = flag("--preflight") ? { missing: preflight(existsSync(join(dir, "purpose/validation/problem.json")) ? JSON.parse(readFileSync(join(dir, "purpose/validation/problem.json"), "utf8")) : null) } : assess(dir, { as_is: flag("--as-is") });
|
|
121
|
+
const out = flag("--preflight") ? { missing: preflight(existsSync(join(ventureFile(dir, "purpose/validation/problem.json"))) ? JSON.parse(readFileSync(join(ventureFile(dir, "purpose/validation/problem.json")), "utf8")) : null) } : assess(dir, { as_is: flag("--as-is") });
|
|
121
122
|
if (flag("--json")) { console.log(JSON.stringify(out, null, 2)); process.exit(0); }
|
|
122
123
|
const list = (m) => m.map((x) => ` - ${x.field}${x.items ? ` (${x.items.join(", ")})` : ""}: ${x.why}. As is: ${x.as_is}`).join("\n");
|
|
123
124
|
if (out.needs_input) { console.log(`${out.needs_input.ask}\n${list(out.needs_input.missing)}\nFill in: ${out.needs_input.options[0].how}\nRun as is: ${out.needs_input.options[1].how}`); process.exit(0); }
|
package/schema/assets.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { createHash } from "node:crypto";
|
|
|
22
22
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
23
23
|
import { extname, join } from "node:path";
|
|
24
24
|
import { entries } from "../interface/provenance.mjs";
|
|
25
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
25
26
|
|
|
26
27
|
export const REGISTER = "public/assets.json";
|
|
27
28
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -124,11 +125,11 @@ export function publicFiles(dir) {
|
|
|
124
125
|
return walk(root).map((f) => f.slice(dir.length + 1)).filter((f) => f !== REGISTER).sort();
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
export const registerOf = (dir) => existsSync(
|
|
128
|
+
export const registerOf = (dir) => existsSync(ventureFile(dir, REGISTER)) ? load(ventureFile(dir, REGISTER)) : null;
|
|
128
129
|
|
|
129
130
|
/** The facts code reads from every file in public/. */
|
|
130
131
|
export function facts(dir) {
|
|
131
|
-
return publicFiles(dir).map((path) => ({ path, ...factsOf(readFileSync(
|
|
132
|
+
return publicFiles(dir).map((path) => ({ path, ...factsOf(readFileSync(ventureFile(dir, path))) }));
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
/** Which alt texts a person has confirmed, from the provenance log. */
|
|
@@ -156,8 +157,8 @@ export function checkAssets(dir) {
|
|
|
156
157
|
for (const a of reg.assets ?? []) {
|
|
157
158
|
if (seen.has(a.path)) errors.push(`${a.path}: listed twice`);
|
|
158
159
|
seen.add(a.path);
|
|
159
|
-
if (!existsSync(
|
|
160
|
-
const real = factsOf(readFileSync(
|
|
160
|
+
if (!existsSync(ventureFile(dir, a.path))) { errors.push(`${a.path}: listed in ${REGISTER} but the file is not there`); continue; }
|
|
161
|
+
const real = factsOf(readFileSync(ventureFile(dir, a.path)));
|
|
161
162
|
if (String(a.license).trim().toLowerCase() === "unknown") errors.push(`${a.path}: the license is unknown; find it before the venture ships the file (J-2)`);
|
|
162
163
|
if (!a.credit || !String(a.credit).trim()) errors.push(`${a.path}: no credit; say who made it (J-4)`);
|
|
163
164
|
if (real.type !== "unknown" && !extensionFits(a.path, real.type)) errors.push(`${a.path}: the bytes are ${real.type}, the name says otherwise`);
|
|
@@ -181,7 +182,7 @@ export function checkAssets(dir) {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
for (const r of reg.remote ?? []) {
|
|
184
|
-
const src = join(dir, "raw/sources", r.id, "source.json");
|
|
185
|
+
const src = join(ventureFile(dir, "raw/sources"), r.id, "source.json");
|
|
185
186
|
if (!existsSync(src)) errors.push(`remote ${r.url}: no note in raw/sources/${r.id}/ (J-3)`);
|
|
186
187
|
else if (load(src).origin !== r.url) errors.push(`remote ${r.url}: raw/sources/${r.id}/source.json names another origin`);
|
|
187
188
|
let host = null;
|
package/schema/brand.mjs
CHANGED
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { allowsCopy, factsOf, registerOf, REGISTER } from "./assets.mjs";
|
|
34
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
34
35
|
|
|
35
36
|
export const BRAND = "marketing/brand.json";
|
|
36
37
|
export const SITEMAPS = ["marketing/sitemap.json", "operations/marketing/sitemap.json"];
|
|
37
38
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
38
|
-
export const brandOf = (dir) => dir && existsSync(
|
|
39
|
+
export const brandOf = (dir) => dir && existsSync(ventureFile(dir, BRAND)) ? load(ventureFile(dir, BRAND)) : null;
|
|
39
40
|
|
|
40
41
|
// ---------------------------------------------------------------------------------------------
|
|
41
42
|
// Router hooks. interface/router.mjs spreads these into its tables; this module never imports the router.
|
|
@@ -110,7 +111,7 @@ const url = (path) => `/${path.replace(/^public\//, "")}`;
|
|
|
110
111
|
/** The marks as head links: the file-based icons Next.js turns into <link> tags. */
|
|
111
112
|
export function headLinks(dir, brand) {
|
|
112
113
|
const reg = registerOf(dir);
|
|
113
|
-
const factsFor = (p) => reg?.assets?.find((a) => a.path === p)?.facts ?? (existsSync(
|
|
114
|
+
const factsFor = (p) => reg?.assets?.find((a) => a.path === p)?.facts ?? (existsSync(ventureFile(dir, p)) ? factsOf(readFileSync(ventureFile(dir, p))) : null);
|
|
114
115
|
const out = [];
|
|
115
116
|
for (const m of brand.marks ?? []) {
|
|
116
117
|
if (m.kind === "logo") continue;
|
|
@@ -217,7 +218,7 @@ export function voiceCheck(text, rules) {
|
|
|
217
218
|
/** The venture's marketing content files, venture-relative. */
|
|
218
219
|
export function contentFiles(dir) {
|
|
219
220
|
const walk = (d) => existsSync(d) ? readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(join(d, e.name)) : /\.(mdx?|txt)$/.test(e.name) ? [join(d, e.name)] : []) : [];
|
|
220
|
-
return ["marketing/content", "operations/marketing/content"].flatMap((p) => walk(
|
|
221
|
+
return ["marketing/content", "operations/marketing/content"].flatMap((p) => walk(ventureFile(dir, p))).map((f) => f.slice(dir.length + 1)).sort();
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
/** Every marketing content file held to the voice rules in force at its path, and to other packages' local terms (warnings). */
|
|
@@ -225,7 +226,7 @@ export function voice(dir, brand = brandOf(dir)) {
|
|
|
225
226
|
const violations = [], leaks = [];
|
|
226
227
|
const local = [...glossaries(dir)].filter(([p]) => p !== "marketing").flatMap(([p, g]) => (g.terms ?? []).filter((t) => !t.export).map((t) => ({ ...t, from: p })));
|
|
227
228
|
for (const f of contentFiles(dir)) {
|
|
228
|
-
const text = readFileSync(
|
|
229
|
+
const text = readFileSync(ventureFile(dir, f), "utf8");
|
|
229
230
|
const route = f.replace(/^operations\//, "").replace(/\.(mdx?|txt)$/, "");
|
|
230
231
|
if (brand) for (const v of voiceCheck(text, resolveBrand(brand, route).voice.rules)) violations.push({ file: f, ...v });
|
|
231
232
|
for (const t of local) for (const v of voiceCheck(text, [{ id: t.term, avoid: t.term }])) leaks.push({ file: f, line: v.line, term: t.term, local_to: t.from });
|
|
@@ -258,11 +259,11 @@ export function checkBrand(dir) {
|
|
|
258
259
|
if (!(t.files ?? []).length) errors.push(`typeface ${t.id}: self-hosted but names no files in public/ (T-1)`);
|
|
259
260
|
for (const f of t.files ?? []) {
|
|
260
261
|
if (!f.startsWith("public/")) { errors.push(`typeface ${t.id}: ${f} is not in public/ (T-1)`); continue; }
|
|
261
|
-
if (!existsSync(
|
|
262
|
+
if (!existsSync(ventureFile(dir, f))) { errors.push(`typeface ${t.id}: ${f} is not there (T-1)`); continue; }
|
|
262
263
|
const a = asset(f);
|
|
263
264
|
if (!a) errors.push(`typeface ${t.id}: ${f} has no entry in ${REGISTER} (T-1, J-2)`);
|
|
264
265
|
else if (String(a.license).toLowerCase() !== String(t.license).toLowerCase()) errors.push(`typeface ${t.id}: licensed ${t.license}, but ${REGISTER} says ${f} is ${a.license} (T-1)`);
|
|
265
|
-
const type = factsOf(readFileSync(
|
|
266
|
+
const type = factsOf(readFileSync(ventureFile(dir, f))).type;
|
|
266
267
|
if (!["woff2", "woff", "ttf", "otf"].includes(type)) errors.push(`typeface ${t.id}: ${f} is ${type}, not a font file (T-1)`);
|
|
267
268
|
}
|
|
268
269
|
}
|
|
@@ -281,9 +282,9 @@ export function checkBrand(dir) {
|
|
|
281
282
|
// Marks (P-24): registered assets of the right type and size.
|
|
282
283
|
const KINDS = { favicon: ["ico"], icon: ["png", "svg", "ico"], "apple-icon": ["png"], logo: ["png", "svg", "jpeg", "webp"] };
|
|
283
284
|
for (const m of brand.marks ?? []) {
|
|
284
|
-
if (!existsSync(
|
|
285
|
+
if (!existsSync(ventureFile(dir, m.asset))) { errors.push(`mark ${m.kind}: ${m.asset} is not there (P-24)`); continue; }
|
|
285
286
|
if (!asset(m.asset)) errors.push(`mark ${m.kind}: ${m.asset} has no entry in ${REGISTER}, so its source and license are unknown (P-24, J-2)`);
|
|
286
|
-
const f = factsOf(readFileSync(
|
|
287
|
+
const f = factsOf(readFileSync(ventureFile(dir, m.asset)));
|
|
287
288
|
if (!KINDS[m.kind].includes(f.type)) errors.push(`mark ${m.kind}: ${m.asset} is ${f.type}; a ${m.kind} is ${KINDS[m.kind].join(" or ")} (P-24)`);
|
|
288
289
|
if (m.kind === "apple-icon" && f.type === "png" && (f.width !== 180 || f.height !== 180)) errors.push(`mark apple-icon: ${m.asset} is ${f.width}x${f.height}; an apple-icon is 180x180 (P-24)`);
|
|
289
290
|
if (m.kind === "icon" && f.width && f.width !== f.height) errors.push(`mark icon: ${m.asset} is ${f.width}x${f.height}; an icon is square (P-24)`);
|
|
@@ -311,8 +312,8 @@ export function checkBrand(dir) {
|
|
|
311
312
|
const xmlEscape = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
312
313
|
|
|
313
314
|
export function sitemapOf(dir) {
|
|
314
|
-
const f = SITEMAPS.find((p) => existsSync(
|
|
315
|
-
return f ? { file: f, record: load(
|
|
315
|
+
const f = SITEMAPS.find((p) => existsSync(ventureFile(dir, p)));
|
|
316
|
+
return f ? { file: f, record: load(ventureFile(dir, f)) } : null;
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
/** sitemaps.org XML from a marketing sitemap record: one <url> per planned page, home first. */
|
package/schema/caching.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import { createHash } from "node:crypto";
|
|
|
38
38
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
39
39
|
import { dirname, join } from "node:path";
|
|
40
40
|
import { entries as provEntries } from "../interface/provenance.mjs";
|
|
41
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
41
42
|
|
|
42
43
|
const repo = new URL("..", import.meta.url).pathname;
|
|
43
44
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
@@ -127,7 +128,7 @@ export const DEFAULTS = { enabled: true, life: "default", judgment_life: "max",
|
|
|
127
128
|
|
|
128
129
|
/** The venture's cache settings: the recommended defaults, then naive.config.json's cache, then NAIVE_CACHE. */
|
|
129
130
|
export function configOf(dir) {
|
|
130
|
-
const f = dir && join(dir, "naive.config.json");
|
|
131
|
+
const f = dir && join(ventureFile(dir, "naive.config.json"));
|
|
131
132
|
const own = f && existsSync(f) ? load(f).cache ?? {} : {};
|
|
132
133
|
const off = /^(0|off|false|no)$/i.test(process.env.NAIVE_CACHE ?? "");
|
|
133
134
|
return { ...DEFAULTS, ...own, routes: { ...(own.routes ?? {}) }, enabled: !off && own.enabled !== false };
|
|
@@ -372,7 +373,7 @@ function walkEntries(root) {
|
|
|
372
373
|
|
|
373
374
|
/** The entries one actor may see: the shared ones and their own private ones, never another person's (C-7). */
|
|
374
375
|
export function entries(dir, { identity = null, level } = {}) {
|
|
375
|
-
const base =
|
|
376
|
+
const base = ventureFile(dir, CACHE_DIR);
|
|
376
377
|
const roots = [join(base, "shared"), ...(identity ? [join(base, scopeOf(true, identity))] : [])];
|
|
377
378
|
return roots.flatMap(walkEntries).flatMap((f) => { try { const e = load(f); return [{ file: f.slice(dir.length + 1), bytes: statSync(f).size, ...e }]; } catch { return []; } })
|
|
378
379
|
.filter((e) => !level || e.level === level);
|
|
@@ -392,7 +393,7 @@ export function stats(dir, { identity = null, now = Date.now() } = {}) {
|
|
|
392
393
|
/** Removes expired entries in every scope. Returns how many. */
|
|
393
394
|
export function prune(dir, { now = Date.now() } = {}) {
|
|
394
395
|
let n = 0;
|
|
395
|
-
for (const f of walkEntries(
|
|
396
|
+
for (const f of walkEntries(ventureFile(dir, CACHE_DIR))) {
|
|
396
397
|
try { const e = load(f); if (e.expires_at && Date.parse(e.expires_at) <= now) { rmSync(f); n++; } } catch { rmSync(f, { force: true }); n++; }
|
|
397
398
|
}
|
|
398
399
|
return n;
|
|
@@ -401,7 +402,7 @@ export function prune(dir, { now = Date.now() } = {}) {
|
|
|
401
402
|
/** Removes entries, all or one level, in every scope. Returns how many. */
|
|
402
403
|
export function clear(dir, { level } = {}) {
|
|
403
404
|
let n = 0;
|
|
404
|
-
for (const f of walkEntries(
|
|
405
|
+
for (const f of walkEntries(ventureFile(dir, CACHE_DIR))) {
|
|
405
406
|
let e = null;
|
|
406
407
|
try { e = load(f); } catch {}
|
|
407
408
|
if (!level || e?.level === level) { rmSync(f, { force: true }); n++; }
|
package/schema/check-config.mjs
CHANGED
|
@@ -23,13 +23,14 @@ import { evaluate, patternRe, rulesFor } from "./gate.mjs";
|
|
|
23
23
|
import { teamPlan } from "./access.mjs";
|
|
24
24
|
import { gateOf, repo, resolve, routes } from "../interface/router.mjs";
|
|
25
25
|
import { agentFromEnv, record } from "../interface/provenance.mjs";
|
|
26
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
26
27
|
|
|
27
28
|
const FRAMEWORKS = ["idea-map"];
|
|
28
29
|
|
|
29
30
|
/** Problems with a venture's naive.config.json: { errors, warnings }. */
|
|
30
31
|
export function check(dir) {
|
|
31
32
|
const errors = [], warnings = [];
|
|
32
|
-
const f =
|
|
33
|
+
const f = ventureFile(dir, CONFIG);
|
|
33
34
|
if (!existsSync(f)) return { errors, warnings, note: `no ${CONFIG}: every default applies` };
|
|
34
35
|
let raw;
|
|
35
36
|
try { raw = rawConfig(dir); } catch (e) { return { errors: [e.message], warnings }; }
|
|
@@ -66,7 +67,7 @@ export function explain(dir, route, { actor = "agent", role, confidence } = {})
|
|
|
66
67
|
const concrete = r.collection === "item" ? r.route.replace("[id]", params.id) : r.route;
|
|
67
68
|
const cls = gateOf(r, dir);
|
|
68
69
|
const files = (r.collection === "item" ? r.files.map((x) => x.replace("*", params.id)) : r.files);
|
|
69
|
-
const exists = files.some((x) => existsSync(
|
|
70
|
+
const exists = files.some((x) => existsSync(ventureFile(dir, x)));
|
|
70
71
|
const verdict = evaluate({ config: configFor(dir), dir, route: concrete, table: r.route, record: r.record, actor, role, confidence, exists, decisionOwner: cls?.owner ?? undefined });
|
|
71
72
|
return { route: concrete, record: r.record, actor, role: role ?? null, confidence: confidence ?? null, decision_class: cls ? { class: cls.class, level: cls.level, drafts: cls.drafts && actor !== "person" } : null, gate: verdict };
|
|
72
73
|
}
|
|
@@ -78,7 +79,7 @@ export function pin(dir, names) {
|
|
|
78
79
|
const unknown = chosen.filter((n) => !have[n]);
|
|
79
80
|
if (unknown.length) throw new Error(`not installed: ${unknown.join(", ")}`);
|
|
80
81
|
raw.packages = { ...(raw.packages ?? {}), ...Object.fromEntries(chosen.map((n) => [n, have[n]])) };
|
|
81
|
-
writeFileSync(
|
|
82
|
+
writeFileSync(ventureFile(dir, CONFIG), JSON.stringify(raw, null, 2) + "\n");
|
|
82
83
|
record(dir, { entity: CONFIG, route: "system/naive-config", activity: "write", agent: agentFromEnv("person", "cli"), note: `pinned ${chosen.join(", ")} at the installed versions` });
|
|
83
84
|
return raw.packages;
|
|
84
85
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { spawnSync } from "node:child_process";
|
|
18
18
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
19
19
|
import { join, relative } from "node:path";
|
|
20
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
20
21
|
|
|
21
22
|
// EE_ROOT points the check at another copy of the repository (used by schema/test.mjs).
|
|
22
23
|
const root = process.env.EE_ROOT?.replace(/\/?$/, "/") ?? new URL("..", import.meta.url).pathname;
|
|
@@ -26,7 +27,7 @@ const errors = [];
|
|
|
26
27
|
const warnings = [];
|
|
27
28
|
|
|
28
29
|
const files = (dir) => readdirSync(join(root, dir), { withFileTypes: true }).flatMap((e) =>
|
|
29
|
-
[".git", "node_modules", ".naive"].includes(e.name) ? [] : e.isDirectory() ? files(
|
|
30
|
+
[".git", "node_modules", ".naive"].includes(e.name) ? [] : e.isDirectory() ? files(ventureFile(dir, e.name)) : [ventureFile(dir, e.name)]);
|
|
30
31
|
// The files git would track (so build output and installed packages under .gitignore are left out); a
|
|
31
32
|
// copy outside git, as the tests use, is walked instead.
|
|
32
33
|
const tracked = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: root, encoding: "utf8" });
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
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-data-packages.mjs <workspace or venture dir>"); process.exit(2); }
|
|
@@ -32,7 +33,7 @@ for (const v of walk(dir)) {
|
|
|
32
33
|
const id = load(join(v, "venture.json")).id;
|
|
33
34
|
const git = inGit(v);
|
|
34
35
|
for (const pack of PACKS) {
|
|
35
|
-
const base =
|
|
36
|
+
const base = ventureFile(v, pack);
|
|
36
37
|
if (!existsSync(base)) continue;
|
|
37
38
|
for (const name of readdirSync(base)) {
|
|
38
39
|
if (name.startsWith("_") || name.startsWith(".")) continue;
|
|
@@ -56,7 +57,7 @@ for (const v of walk(dir)) {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
const sources = join(v, "raw/sources");
|
|
60
|
+
const sources = join(ventureFile(v, "raw/sources"));
|
|
60
61
|
if (existsSync(sources)) for (const s of readdirSync(sources)) {
|
|
61
62
|
const sd = join(sources, s);
|
|
62
63
|
if (!statSync(sd).isDirectory() || s.startsWith(".")) continue;
|
|
@@ -64,9 +65,9 @@ for (const v of walk(dir)) {
|
|
|
64
65
|
if (!existsSync(join(sd, "text.md"))) errors.push(`${id}/raw/sources/${s}: missing text.md`);
|
|
65
66
|
for (const f of readdirSync(sd)) if (f.startsWith("original.") && (!git || tracked(v, join("raw/sources", s, f)))) errors.push(`${id}/raw/sources/${s}/${f}: original.* stays out of git`);
|
|
66
67
|
}
|
|
67
|
-
const map = ["ideas/map.mdx", "purpose/ideas/map.mdx"].map((f) =>
|
|
68
|
+
const map = ["ideas/map.mdx", "purpose/ideas/map.mdx"].map((f) => ventureFile(v, f)).find((p) => existsSync(p));
|
|
68
69
|
const rawIds = new Set();
|
|
69
|
-
const dump = join(v, "raw/dump");
|
|
70
|
+
const dump = join(ventureFile(v, "raw/dump"));
|
|
70
71
|
if (existsSync(dump)) for (const f of readdirSync(dump)) if (f !== ".gitkeep") rawIds.add(f.replace(/\.[^.]+$/, ""));
|
|
71
72
|
if (existsSync(sources)) for (const s of readdirSync(sources)) if (!s.startsWith(".")) rawIds.add(s);
|
|
72
73
|
if (map && rawIds.size) {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
15
15
|
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
16
17
|
|
|
17
18
|
const dir = process.argv[2];
|
|
18
19
|
if (!dir) { console.error("usage: node schema/check-execution.mjs <venture dir>"); process.exit(2); }
|
|
@@ -27,7 +28,7 @@ const template = load(join(root, "governance/venture-template/decision-rights.js
|
|
|
27
28
|
const errors = [];
|
|
28
29
|
|
|
29
30
|
const classes = new Map(template.classes.map((c) => [c.id, c]));
|
|
30
|
-
const own = join(dir, "governance/decision-rights.json");
|
|
31
|
+
const own = join(ventureFile(dir, "governance/decision-rights.json"));
|
|
31
32
|
if (existsSync(own)) for (const c of load(own).classes) {
|
|
32
33
|
const t = classes.get(c.id);
|
|
33
34
|
if (t && RANK[c.level] < RANK[t.level] && !c.decision) errors.push(`decision-rights/${c.id}: loosens ${t.level} to ${c.level} without naming a decision`);
|
|
@@ -36,14 +37,14 @@ if (existsSync(own)) for (const c of load(own).classes) {
|
|
|
36
37
|
for (const c of classes.values()) if (!roles.has(c.owner)) errors.push(`decision-rights/${c.id}: owner ${c.owner} is not a role`);
|
|
37
38
|
|
|
38
39
|
const results = new Map();
|
|
39
|
-
for (const f of jsonIn(join(dir, "evidence/results"))) {
|
|
40
|
+
for (const f of jsonIn(join(ventureFile(dir, "evidence/results")))) {
|
|
40
41
|
const e = load(f);
|
|
41
42
|
if (e.passed !== e.results.every((r) => r.passed)) errors.push(`evidence/${e.id}: passed says ${e.passed} but its results disagree`);
|
|
42
43
|
results.set(resolve(f), e);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
let count = 0;
|
|
46
|
-
for (const f of jsonIn(join(dir, "operations/handoffs"))) {
|
|
47
|
+
for (const f of jsonIn(join(ventureFile(dir, "operations/handoffs")))) {
|
|
47
48
|
const h = load(f), at = `handoff/${h.id}`;
|
|
48
49
|
count++;
|
|
49
50
|
if (h.venture !== venture.id) errors.push(`${at}: venture ${h.venture} is not ${venture.id}`);
|
package/schema/check-people.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { roles as ROLES, types as TYPES } from "./venture-type.mjs";
|
|
12
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
12
13
|
|
|
13
14
|
const ws = process.argv[2];
|
|
14
15
|
if (!ws || !existsSync(ws)) { console.error("usage: node schema/check-people.mjs <workspace>"); process.exit(2); }
|
|
@@ -26,28 +27,41 @@ for (const dir of walk(ws)) {
|
|
|
26
27
|
const at = (m) => errors.push(`${v.id}: ${m}`);
|
|
27
28
|
const type = v.type ? types.get(v.type) : null;
|
|
28
29
|
if (v.type && !type) at(`type ${v.type} is not in schema/venture-types.json`);
|
|
29
|
-
const rosterFile = join(dir, "people/people.json");
|
|
30
|
-
const fdir = join(dir, "people/founders");
|
|
30
|
+
const rosterFile = join(ventureFile(dir, "people/people.json"));
|
|
31
|
+
const fdir = join(ventureFile(dir, "people/founders"));
|
|
31
32
|
const files = existsSync(fdir) ? readdirSync(fdir).filter((f) => f.endsWith(".json")) : [];
|
|
32
33
|
if (!existsSync(rosterFile)) { if (files.length) at("people/founders/ has files but there is no people/people.json"); continue; }
|
|
33
34
|
rosters++;
|
|
34
35
|
const roster = load(rosterFile);
|
|
35
|
-
|
|
36
|
+
// In the structured layout (EE-086) the org chart is venture.json's functions, and a person's hats are the
|
|
37
|
+
// functions they own there; in the flat layout, the departments venture.json imports, worn as hats in people/.
|
|
38
|
+
const structured = v.layout === "structured";
|
|
39
|
+
const imported = new Set(structured ? Object.keys(v.functions ?? {}) : v.departments ?? []);
|
|
40
|
+
const ids = new Set(roster.people.map((p) => p.id));
|
|
41
|
+
if (structured) {
|
|
42
|
+
for (const [fn, f] of Object.entries(v.functions ?? {})) {
|
|
43
|
+
if (f.owner && !ids.has(f.owner)) at(`venture.json says ${f.owner} owns ${fn}, but people/people.json has no ${f.owner}`);
|
|
44
|
+
for (const c of f.contributors ?? []) if (!ids.has(c) && !roles.has(c)) at(`${fn} names contributor ${c}, who is neither a person in people/ nor a role`);
|
|
45
|
+
}
|
|
46
|
+
for (const p of roster.people) p.hats = Object.entries(v.functions ?? {}).filter(([, f]) => f.owner === p.id).map(([fn]) => fn);
|
|
47
|
+
}
|
|
36
48
|
for (const p of roster.people) {
|
|
37
49
|
const role = roles.get(p.role);
|
|
38
50
|
if (!role) { at(`${p.id} has role ${p.role}, which is not in governance/venture-template/roles/venture-roles.json`); continue; }
|
|
39
51
|
if (type && !type.roles.includes(p.role)) at(`${p.id} is a ${p.role}, which a ${type.label.toLowerCase()} does not have (${type.roles.join(", ")})`);
|
|
40
|
-
for (const h of p.hats) if (imported.size && !imported.has(h)) at(`${p.id} wears the ${h} hat, but venture.json does not import ${h}`);
|
|
52
|
+
for (const h of p.hats ?? []) if (imported.size && !imported.has(h)) at(`${p.id} wears the ${h} hat, but venture.json does not import ${h}`);
|
|
53
|
+
p.hats ??= [];
|
|
41
54
|
if (!role.owns_when_solo && p.hats.length) at(`${p.id} is a ${p.role}, who owns no function, but wears ${p.hats.join(", ")}`);
|
|
42
55
|
if (role.founder) {
|
|
43
56
|
const f = `people/founders/${p.id}.json`;
|
|
44
57
|
if (p.founder_file !== f) at(`${p.id} is a founder, so founder_file is ${f}`);
|
|
45
|
-
else if (!existsSync(
|
|
58
|
+
else if (!existsSync(ventureFile(dir, f))) at(`${f} is missing`);
|
|
46
59
|
else {
|
|
47
|
-
const rec = load(
|
|
60
|
+
const rec = load(ventureFile(dir, f));
|
|
48
61
|
if (rec.id !== p.id || rec.role !== p.role) at(`${f} says ${rec.id} is a ${rec.role}; the roster says ${p.id} is a ${p.role}`);
|
|
49
|
-
if (JSON.stringify([...rec.hats].sort()) !== JSON.stringify([...p.hats].sort())) at(`${f} and the roster disagree on ${p.id}'s hats`);
|
|
50
|
-
if (
|
|
62
|
+
if (!structured && JSON.stringify([...(rec.hats ?? [])].sort()) !== JSON.stringify([...p.hats].sort())) at(`${f} and the roster disagree on ${p.id}'s hats`);
|
|
63
|
+
if (structured && rec.hats) at(`${f} stores hats; in the structured layout they are read from venture.json`);
|
|
64
|
+
if (rec.agreement && !existsSync(ventureFile(dir, rec.agreement.path))) at(`${f} links a founder agreement at ${rec.agreement.path}, which does not exist`);
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
}
|
package/schema/check-private.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
10
11
|
|
|
11
12
|
const dir = process.argv[2];
|
|
12
13
|
if (!dir) { console.error("usage: node schema/check-private.mjs <venture dir>"); process.exit(2); }
|
|
@@ -49,7 +50,7 @@ for (const { package: name, input } of inputs) {
|
|
|
49
50
|
if (privateTargets.has(input) || privateRecords.has(input.split(".")[0])) errors.push(`${name}: use_model input ${input} is private and must not reach a model`);
|
|
50
51
|
}
|
|
51
52
|
if (readsRelease) {
|
|
52
|
-
const release = join(dir, ".release");
|
|
53
|
+
const release = join(ventureFile(dir, ".release"));
|
|
53
54
|
if (existsSync(release) && statSync(release).isDirectory()) {
|
|
54
55
|
for (const f of walk(release).filter((p) => p.endsWith(".json"))) {
|
|
55
56
|
const text = readFileSync(f, "utf8");
|
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).
|