naive-framework 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/decisions.json +15 -0
- package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
- package/interface/adapters.mjs +7 -6
- package/interface/boundaries.mjs +5 -4
- package/interface/brand.mjs +6 -5
- package/interface/cli/mentura.mjs +4 -3
- package/interface/cli/naive.mjs +11 -6
- package/interface/context.mjs +7 -6
- package/interface/demo/board.mjs +9 -8
- package/interface/demo/decision-harness.mjs +4 -3
- package/interface/gate-hooks.mjs +2 -1
- package/interface/harness/evals.mjs +8 -7
- package/interface/harness/run.mjs +14 -13
- package/interface/instrumentation.mjs +4 -3
- package/interface/layer.json +4 -0
- package/interface/package-routes.mjs +4 -3
- package/interface/provenance.mjs +5 -4
- package/interface/reads.mjs +4 -3
- package/interface/release-card.mjs +2 -1
- package/interface/router.mjs +10 -9
- package/interface/skills/choosing-artifacts/SKILL.md +113 -0
- package/interface/vitals.mjs +3 -2
- package/knowledge/connectors/catalog.mjs +3 -2
- package/knowledge/connectors/filesystem.mjs +10 -5
- package/knowledge/connectors/index.mjs +3 -2
- package/operations/marketing/check-public.mjs +7 -6
- package/operations/marketing/site/plan.mjs +4 -3
- package/operations/marketing/site/presence.mjs +4 -3
- package/package.json +1 -1
- package/platform/integrations/rho.mjs +2 -1
- package/purpose/frameworks/idea-map/route.mjs +5 -4
- package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
- package/schema/access.mjs +2 -1
- package/schema/add-department.mjs +3 -2
- package/schema/assess-problem.mjs +3 -2
- package/schema/assets.mjs +6 -5
- package/schema/brand.mjs +11 -10
- package/schema/caching.mjs +5 -4
- package/schema/check-config.mjs +4 -3
- package/schema/check-data-model.mjs +2 -1
- package/schema/check-data-packages.mjs +6 -5
- package/schema/check-execution.mjs +4 -3
- package/schema/check-people.mjs +22 -8
- package/schema/check-private.mjs +2 -1
- package/schema/check-ready.mjs +6 -5
- package/schema/check-sources.mjs +2 -1
- package/schema/check-where-used.mjs +4 -3
- package/schema/decision-analytics.mjs +2 -1
- package/schema/decisions.mjs +14 -11
- package/schema/foundation.mjs +8 -7
- package/schema/founder.schema.json +2 -2
- package/schema/frames.mjs +4 -3
- package/schema/freshness.mjs +13 -12
- package/schema/gate-drafts.mjs +8 -7
- package/schema/init-venture.mjs +48 -20
- package/schema/migrate-venture.mjs +8 -7
- package/schema/migrations/kit-to-package.mjs +4 -3
- package/schema/migrations/package-layout.mjs +9 -5
- package/schema/model-steps.mjs +8 -7
- package/schema/naive-config.mjs +4 -3
- package/schema/need-now.mjs +5 -4
- package/schema/packages.mjs +11 -10
- package/schema/payouts.mjs +3 -2
- package/schema/people.schema.json +2 -3
- package/schema/port-venture.mjs +206 -132
- package/schema/release-read.mjs +5 -4
- package/schema/releases.mjs +21 -20
- package/schema/run-handoff.mjs +5 -4
- package/schema/setup.mjs +5 -4
- package/schema/stale.mjs +6 -5
- package/schema/structure.mjs +5 -4
- package/schema/time-to-ready.mjs +7 -6
- package/schema/triggers.mjs +7 -6
- package/schema/upgrades.mjs +31 -30
- package/schema/venture-paths.mjs +97 -0
- package/schema/venture.schema.json +25 -17
- package/schema/workspace.schema.json +5 -1
|
@@ -42,6 +42,7 @@ import { understood } from "../../purpose/frameworks/idea-map/route.mjs";
|
|
|
42
42
|
import { createHash } from "node:crypto";
|
|
43
43
|
import { agentFromEnv, record as prov } from "../provenance.mjs";
|
|
44
44
|
import { evaluate } from "./evals.mjs";
|
|
45
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
45
46
|
|
|
46
47
|
const repo = new URL("../../", import.meta.url).pathname;
|
|
47
48
|
const argv = process.argv.slice(2);
|
|
@@ -71,8 +72,8 @@ if (!existsSync(join(dir, "venture.json"))) {
|
|
|
71
72
|
if (p.status !== 0) { console.error(p.stderr || p.stdout); process.exit(1); }
|
|
72
73
|
if (opt("name")) { const v = JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")); v.name = opt("name"); writeFileSync(join(dir, "venture.json"), JSON.stringify(v, null, 2) + "\n"); }
|
|
73
74
|
}
|
|
74
|
-
mkdirSync(join(dir, "purpose/ideas"), { recursive: true });
|
|
75
|
-
copyFileSync(dumpFile,
|
|
75
|
+
mkdirSync(join(ventureFile(dir, "purpose/ideas")), { recursive: true });
|
|
76
|
+
copyFileSync(dumpFile, ventureFile(dir, DUMP));
|
|
76
77
|
prov(dir, { entity: DUMP, route: "purpose/idea-dump", activity: "create", agent: agentFromEnv("person", "harness", { id: founder }), note: "the founder's idea dump, verbatim" });
|
|
77
78
|
lap(`venture ${id} scaffolded in ${ws}; idea dump saved (${readFileSync(dumpFile, "utf8").length} characters)`);
|
|
78
79
|
|
|
@@ -83,8 +84,8 @@ if (opt("money")) {
|
|
|
83
84
|
const m = opt("money");
|
|
84
85
|
const src = m === "live" ? ["--live"] : m === "sandbox" ? [] : ["--fixture", resolve(m)];
|
|
85
86
|
const p = spawnSync("node", [join(repo, "platform/integrations/rho.mjs"), "sync", "--venture", dir, ...src], { encoding: "utf8" });
|
|
86
|
-
if (p.status === 0 && existsSync(join(dir, "money/_summary.json"))) {
|
|
87
|
-
money = JSON.parse(readFileSync(join(dir, "money/_summary.json"), "utf8"));
|
|
87
|
+
if (p.status === 0 && existsSync(join(ventureFile(dir, "money/_summary.json")))) {
|
|
88
|
+
money = JSON.parse(readFileSync(join(ventureFile(dir, "money/_summary.json")), "utf8"));
|
|
88
89
|
prov(dir, { entity: "money/_summary.json", route: "money/_summary", activity: "create", agent: agentFromEnv("agent", "harness", { id: "naive-money" }), note: `synced from ${money.source}; ${money.transactions} transactions kept as pointers in a sensitive stream` });
|
|
89
90
|
lap(`money synced from ${money.source}${money.business ? ` (${money.business})` : ""}: cash ${(money.cash.amount / 100).toFixed(2)} ${money.cash.currency}, monthly net ${(money.monthly_net.amount / 100).toFixed(2)}, runway ${money.runway_months ?? "not limited by burn"}, as of ${money.as_of}`);
|
|
90
91
|
} else console.log(` money sync failed, so the run goes on without it: ${(p.stderr || p.stdout).trim().slice(0, 300)}`);
|
|
@@ -100,7 +101,7 @@ if (flag("board")) { process.env.MENTURA_WORKSPACE = ws; board = (await import("
|
|
|
100
101
|
|
|
101
102
|
// The trace (EE-058): every step and every tool call the agent made, with timings, for LangSmith.
|
|
102
103
|
const iso = () => new Date().toISOString();
|
|
103
|
-
const trace = { name: `naive discovery: ${id}`, run_type: "chain", start: iso(), inputs: { dump: DUMP, ideas: readFileSync(
|
|
104
|
+
const trace = { name: `naive discovery: ${id}`, run_type: "chain", start: iso(), inputs: { dump: DUMP, ideas: readFileSync(ventureFile(dir, DUMP), "utf8"), founder, phase }, children: [], metadata: {
|
|
104
105
|
venture: id, venture_id: id, pipeline_step: phase, money: money?.source ?? null,
|
|
105
106
|
// LangSmith groups a founder's runs into one thread by thread_id: one session per venture per day.
|
|
106
107
|
thread_id: process.env.NAIVE_SESSION ?? `${id}-${new Date().toISOString().slice(0, 10)}`,
|
|
@@ -155,11 +156,11 @@ async function agent(prompt, label, { tools = "mcp__naive__*,mcp__research__*",
|
|
|
155
156
|
// The run's evals and trace: written beside the workspace, and sent to LangSmith when a key is set.
|
|
156
157
|
function finish(code = 0) {
|
|
157
158
|
trace.end = iso();
|
|
158
|
-
const evals = existsSync(
|
|
159
|
+
const evals = existsSync(ventureFile(dir, DUMP)) ? evaluate(dir, { researched, settled }) : [];
|
|
159
160
|
trace.feedback = evals;
|
|
160
161
|
// What the run drafted, so a judge reading only the root run (LangSmith's online evaluators, Jev)
|
|
161
162
|
// sees the problem record and the agent's reports beside the founder's dump in the inputs.
|
|
162
|
-
const problem = join(dir, "purpose/validation/problem.json");
|
|
163
|
+
const problem = join(ventureFile(dir, "purpose/validation/problem.json"));
|
|
163
164
|
const drafted = { problem: existsSync(problem) ? JSON.parse(readFileSync(problem, "utf8")) : null, reports: trace.children.filter((c) => c.outputs?.report).map((c) => ({ pass: c.name, report: c.outputs.report })) };
|
|
164
165
|
trace.outputs = { settled, docket: { steps: docketSteps, now: compact(docket(dir)) }, drafted, evals: Object.fromEntries(evals.map((e) => [e.key, e.score])), laps };
|
|
165
166
|
writeFileSync(join(ws, "trace.json"), JSON.stringify(trace, null, 2) + "\n");
|
|
@@ -180,7 +181,7 @@ const compact = (k) => ({ leading: k.functions[0]?.function ?? null, functions:
|
|
|
180
181
|
|
|
181
182
|
// 1. The idea map: Jev tags each idea with Naive's conventions; without Jev, the agent does, with the
|
|
182
183
|
// same vocabulary. Either way the ideas stay verbatim and unknown tags are dropped.
|
|
183
|
-
const ideas = split(readFileSync(
|
|
184
|
+
const ideas = split(readFileSync(ventureFile(dir, DUMP), "utf8"));
|
|
184
185
|
const venture = JSON.parse(readFileSync(join(dir, "venture.json"), "utf8"));
|
|
185
186
|
let tags, tagger;
|
|
186
187
|
const mapNode = { name: "idea map", run_type: "tool", start: iso(), inputs: { ideas: ideas.length }, children: [] };
|
|
@@ -200,24 +201,24 @@ Ideas: ${JSON.stringify(Object.fromEntries(ideas.map((i) => [i.id, i.section ? `
|
|
|
200
201
|
tags = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
|
|
201
202
|
tagger = "claude-code";
|
|
202
203
|
}
|
|
203
|
-
writeFileSync(
|
|
204
|
+
writeFileSync(ventureFile(dir, MAP), render(ideas, clean(tags), { venture: id, tagger }));
|
|
204
205
|
prov(dir, { entity: MAP, route: "purpose/idea-map", activity: "create", derived_from: [DUMP], agent: agentFromEnv("agent", "harness", { id: tagger }), note: `${ideas.length} ideas tagged with Naive's conventions` });
|
|
205
|
-
const sum = summary(parse(readFileSync(
|
|
206
|
+
const sum = summary(parse(readFileSync(ventureFile(dir, MAP), "utf8")), phase);
|
|
206
207
|
trace.children.push(Object.assign(mapNode, { end: iso(), outputs: { tagger, summary: sum }, metadata: { tagger } }));
|
|
207
208
|
lap(`idea map: ${sum.ideas} ideas; ${Object.entries(sum.by_pillar).map(([k, v]) => `${k} ${v}`).join(", ")}; clusters ${sum.strengths.map((s) => s.department).join(", ")}; ${sum.product.reading}`);
|
|
208
209
|
|
|
209
210
|
// 1b. The idea graph: the map's structured form. Jev judges which ideas build on which; without Jev,
|
|
210
211
|
// Naive's record order does. Signal, lanes and layout follow in code.
|
|
211
212
|
const graphNode = { name: "idea graph", run_type: "tool", start: iso(), inputs: { ideas: ideas.length }, children: [] };
|
|
212
|
-
const mapIdeas = parse(readFileSync(
|
|
213
|
+
const mapIdeas = parse(readFileSync(ventureFile(dir, MAP), "utf8")).ideas;
|
|
213
214
|
let graphAnswers = null;
|
|
214
215
|
if (jev) {
|
|
215
216
|
const p = spawnSync("node", [join(repo, "platform/integrations/jev.mjs")], { input: JSON.stringify(graphRequest(mapIdeas, venture)), encoding: "utf8", maxBuffer: 1 << 26 });
|
|
216
217
|
if (p.status === 0) { const out = JSON.parse(p.stdout); graphAnswers = out.answers; console.log(` Jev answered ${Object.keys(out.answers).length} dependency and concreteness questions in ${out.ms} ms`); }
|
|
217
218
|
else console.log(` Jev could not judge the dependencies (${p.stderr.trim().slice(0, 200)}); using Naive's record order`);
|
|
218
219
|
}
|
|
219
|
-
const g = buildGraph(mapIdeas, { answers: graphAnswers, phase, venture: id, source: `${MAP}@${createHash("sha256").update(readFileSync(
|
|
220
|
-
writeFileSync(
|
|
220
|
+
const g = buildGraph(mapIdeas, { answers: graphAnswers, phase, venture: id, source: `${MAP}@${createHash("sha256").update(readFileSync(ventureFile(dir, MAP))).digest("hex").slice(0, 16)}` });
|
|
221
|
+
writeFileSync(ventureFile(dir, GRAPH), JSON.stringify(g, null, 2) + "\n");
|
|
221
222
|
prov(dir, { entity: GRAPH, route: "purpose/idea-graph", activity: "create", derived_from: [MAP], agent: agentFromEnv("agent", "harness", { id: g.judged_by }), note: `Merkle root ${g.root}` });
|
|
222
223
|
trace.children.push(Object.assign(graphNode, { end: iso(), outputs: { root: g.root, edges: g.edges.length, lanes: g.lanes.map((l) => ({ mode: l.mode, ideas: l.ideas.length })), next: g.next } }));
|
|
223
224
|
lap(`idea graph: ${g.edges.length} dependencies (${g.judged_by}); ${g.lanes.filter((l) => l.mode === "sync").length} lane(s) in order, ${g.lanes.filter((l) => l.mode === "async").length} on their own; develop next: ${g.next.join(", ")}; root ${g.root}`);
|
|
@@ -17,6 +17,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
20
21
|
|
|
21
22
|
export const EVENTS = ".naive/events.jsonl";
|
|
22
23
|
const listeners = new Set();
|
|
@@ -29,7 +30,7 @@ export const tokens = (x) => Math.ceil((x === undefined ? 0 : typeof x === "stri
|
|
|
29
30
|
export function register(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
30
31
|
|
|
31
32
|
const configOf = (dir) => {
|
|
32
|
-
const f = dir && join(dir, "naive.config.json");
|
|
33
|
+
const f = dir && join(ventureFile(dir, "naive.config.json"));
|
|
33
34
|
if (!f || !existsSync(f)) return {};
|
|
34
35
|
try { return JSON.parse(readFileSync(f, "utf8")); } catch { return {}; }
|
|
35
36
|
};
|
|
@@ -48,14 +49,14 @@ export function emit(dir, event) {
|
|
|
48
49
|
for (const k of Object.keys(e)) if (e[k] === undefined) delete e[k];
|
|
49
50
|
for (const fn of listeners) { try { fn(e, dir); } catch {} }
|
|
50
51
|
if (dir && enabled(dir)) {
|
|
51
|
-
try { mkdirSync(join(dir, ".naive"), { recursive: true }); appendFileSync(
|
|
52
|
+
try { mkdirSync(join(dir, ".naive"), { recursive: true }); appendFileSync(ventureFile(dir, EVENTS), JSON.stringify(e) + "\n"); } catch {}
|
|
52
53
|
}
|
|
53
54
|
return e;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/** The venture's events, oldest first. */
|
|
57
58
|
export function events(dir) {
|
|
58
|
-
const f =
|
|
59
|
+
const f = ventureFile(dir, EVENTS);
|
|
59
60
|
if (!existsSync(f)) return [];
|
|
60
61
|
return readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).flatMap((l) => { try { return [JSON.parse(l)]; } catch { return []; } });
|
|
61
62
|
}
|
package/interface/layer.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
22
23
|
|
|
23
24
|
const repo = new URL("..", import.meta.url).pathname;
|
|
24
25
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
@@ -33,7 +34,7 @@ export const packageDef = (n) => load(join(repo, "packages", n, "package.json"))
|
|
|
33
34
|
|
|
34
35
|
/** The venture's route groups: the framework's, then its own from naive.config.json. */
|
|
35
36
|
export function groupsOf(dir) {
|
|
36
|
-
const f = dir && join(dir, "naive.config.json");
|
|
37
|
+
const f = dir && join(ventureFile(dir, "naive.config.json"));
|
|
37
38
|
const own = f && existsSync(f) ? load(f).groups ?? {} : {};
|
|
38
39
|
return { ...GROUPS, ...own };
|
|
39
40
|
}
|
|
@@ -78,7 +79,7 @@ export const routeOfType = async (type) => {
|
|
|
78
79
|
async function pendingIn(dir, types, venture) {
|
|
79
80
|
const { recordOf } = await import("../schema/decisions.mjs");
|
|
80
81
|
const { entries } = await import("./provenance.mjs");
|
|
81
|
-
const d = join(dir, "_drafts");
|
|
82
|
+
const d = join(ventureFile(dir, "_drafts"));
|
|
82
83
|
if (!existsSync(d)) return [];
|
|
83
84
|
const log = entries(dir);
|
|
84
85
|
const out = [];
|
|
@@ -179,7 +180,7 @@ export async function specialCall({ venture, route, action, workspace }) {
|
|
|
179
180
|
const g = gs[sp.id];
|
|
180
181
|
if (!g) return { ok: false, error: `no route group (${sp.id}): the groups are ${Object.keys(gs).map((x) => `(${x})`).join(", ")}` };
|
|
181
182
|
const lay = groupLayout(g.packages[0], dir);
|
|
182
|
-
if (action === "layout" || !dir) return { ok: true, result: { ...lay, chain: lay.agents && dir ? [{ path: lay.agents, text: readFileSync(
|
|
183
|
+
if (action === "layout" || !dir) return { ok: true, result: { ...lay, chain: lay.agents && dir ? [{ path: lay.agents, text: readFileSync(ventureFile(dir, lay.agents), "utf8") }] : [] } };
|
|
183
184
|
const states = [];
|
|
184
185
|
for (const n of g.packages.filter((x) => packageNames().includes(x))) states.push(await packageState(dir, n, venture));
|
|
185
186
|
const res = { group: `(${sp.id})`, context: g.context ?? null, packages: states.map((s) => ({ package: s.package, state: s.state, progress: s.progress ?? null, pending: s.loading?.drafts.length ?? 0 })) };
|
package/interface/provenance.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { spawnSync } from "node:child_process";
|
|
|
9
9
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { emit } from "./instrumentation.mjs";
|
|
12
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
12
13
|
|
|
13
14
|
export const LOG = "provenance.jsonl";
|
|
14
15
|
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
@@ -17,7 +18,7 @@ export const lineHash = (prev, entry) => { const { hash, ...rest } = entry; retu
|
|
|
17
18
|
export const contentHash = (file) => sha(readFileSync(file));
|
|
18
19
|
|
|
19
20
|
export function entries(dir) {
|
|
20
|
-
const f =
|
|
21
|
+
const f = ventureFile(dir, LOG);
|
|
21
22
|
return existsSync(f) ? readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l)) : [];
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -37,9 +38,9 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
|
|
|
37
38
|
const prior = entries(dir);
|
|
38
39
|
const prev = prior.length ? prior.at(-1).hash : "genesis";
|
|
39
40
|
const entry = Object.fromEntries(Object.entries({ at: new Date().toISOString(), entity, route, activity, agent,
|
|
40
|
-
derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(
|
|
41
|
+
derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(ventureFile(dir, entity)), prev }).filter(([, v]) => v !== undefined));
|
|
41
42
|
entry.hash = lineHash(prev, entry);
|
|
42
|
-
appendFileSync(
|
|
43
|
+
appendFileSync(ventureFile(dir, LOG), JSON.stringify(entry) + "\n");
|
|
43
44
|
// Context hook: every provenance entry (write, draft, accept, confirm, decision) is an instrumentation event (P-4).
|
|
44
45
|
emit(dir, { kind: "provenance", activity: entry.activity, entity: entry.entity, route: entry.route, decision: entry.decision, by: entry.agent?.kind, via: entry.agent?.via });
|
|
45
46
|
fireTriggers(dir);
|
|
@@ -52,7 +53,7 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
|
|
|
52
53
|
function fireTriggers(dir) {
|
|
53
54
|
if (process.env.NAIVE_TRIGGERS_ACTIVE) return;
|
|
54
55
|
let cfg;
|
|
55
|
-
try { cfg = JSON.parse(readFileSync(join(dir, "naive.config.json"), "utf8")); } catch { return; }
|
|
56
|
+
try { cfg = JSON.parse(readFileSync(join(ventureFile(dir, "naive.config.json")), "utf8")); } catch { return; }
|
|
56
57
|
if (!cfg?.triggers?.length && !cfg?.kits?.length) return;
|
|
57
58
|
// The write has landed; nothing here may undo it or throw past it.
|
|
58
59
|
try {
|
package/interface/reads.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
import { entries } from "./provenance.mjs";
|
|
26
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
26
27
|
|
|
27
28
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
28
29
|
const get = (o, p) => p.split(".").reduce((x, k) => (x == null ? undefined : x[k]), o);
|
|
@@ -45,7 +46,7 @@ export function frontmatter(text) {
|
|
|
45
46
|
/** A record's state from the log and the drafts folder (O-2). */
|
|
46
47
|
export async function stateOf(dir, file) {
|
|
47
48
|
const { confirmedIn } = await import("../schema/foundation.mjs");
|
|
48
|
-
if (existsSync(join(dir, "_drafts", `${file.replace(/\//g, "__")}.draft`))) return "draft";
|
|
49
|
+
if (existsSync(join(ventureFile(dir, "_drafts"), `${file.replace(/\//g, "__")}.draft`))) return "draft";
|
|
49
50
|
const es = entries(dir).filter((e) => e.entity === file);
|
|
50
51
|
if (!es.length) return "unlogged";
|
|
51
52
|
return confirmedIn(dir, file, null) ? "confirmed" : "agent";
|
|
@@ -58,7 +59,7 @@ export function defaults(dir, file, id) {
|
|
|
58
59
|
const last = es.at(-1);
|
|
59
60
|
return { id, created: es[0].at, updated: last.at, by: { kind: last.agent?.kind ?? null, ...(last.agent?.id ? { id: last.agent.id } : {}), via: last.agent?.via ?? null }, activity: last.activity, writes: es.length, from: "provenance" };
|
|
60
61
|
}
|
|
61
|
-
const st = existsSync(
|
|
62
|
+
const st = existsSync(ventureFile(dir, file)) ? statSync(ventureFile(dir, file)) : null;
|
|
62
63
|
return { id, created: st ? new Date(st.birthtimeMs || st.mtimeMs).toISOString() : null, updated: st ? st.mtime.toISOString() : null, by: null, writes: 0, from: "file", note: "written outside the router: mentura baseline adopts it into the log" };
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -138,7 +139,7 @@ const parseInstructions = (chain) => {
|
|
|
138
139
|
export function withParams(layout, { dir, params, file, group }) {
|
|
139
140
|
const chain = [...(layout.chain ?? [])];
|
|
140
141
|
const have = new Set(chain.map((c) => c.path));
|
|
141
|
-
const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(
|
|
142
|
+
const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(ventureFile(dir, rel))) { chain.splice(at, 0, { path: rel, text: readFileSync(ventureFile(dir, rel), "utf8") }); have.add(rel); } };
|
|
142
143
|
if (group?.agents) add(group.agents, chain.findIndex((c) => c.path === "AGENTS.md") + 1);
|
|
143
144
|
if (file) {
|
|
144
145
|
const parts = dirname(file).split("/").filter((p) => p && p !== ".");
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
11
12
|
|
|
12
13
|
const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13
14
|
const day = (t) => (t ? String(t).slice(0, 10) : "");
|
|
@@ -36,7 +37,7 @@ export function svg(card) {
|
|
|
36
37
|
|
|
37
38
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
38
39
|
const dir = process.argv[2];
|
|
39
|
-
const f = dir && join(dir, ".release/card.json");
|
|
40
|
+
const f = dir && join(ventureFile(dir, ".release/card.json"));
|
|
40
41
|
if (!f || !existsSync(f)) { console.error("usage: node interface/release-card.mjs <venture dir> (after node schema/build-release.mjs)"); process.exit(2); }
|
|
41
42
|
process.stdout.write(svg(JSON.parse(readFileSync(f, "utf8"))));
|
|
42
43
|
}
|
package/interface/router.mjs
CHANGED
|
@@ -65,6 +65,7 @@ import { routable } from "../schema/structure.mjs";
|
|
|
65
65
|
import { BRAND_ROUTES, brandCommands, brandLayout } from "../schema/brand.mjs";
|
|
66
66
|
// Upgrades hook (V-9): a read says when what the record reads changed in its system of record.
|
|
67
67
|
import { staleOf } from "../schema/stale.mjs";
|
|
68
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
68
69
|
|
|
69
70
|
export const repo = new URL("..", import.meta.url).pathname;
|
|
70
71
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -223,7 +224,7 @@ export function ventures(workspace = defaultWorkspace()) {
|
|
|
223
224
|
function decisionRights(dir) {
|
|
224
225
|
const t = load(join(repo, "governance/venture-template/decision-rights.json"));
|
|
225
226
|
const classes = new Map(t.classes.map((c) => [c.id, c]));
|
|
226
|
-
const own = join(dir, "governance/decision-rights.json");
|
|
227
|
+
const own = join(ventureFile(dir, "governance/decision-rights.json"));
|
|
227
228
|
if (existsSync(own)) for (const c of load(own).classes) classes.set(c.id, { ...classes.get(c.id), ...c, level: classes.get(c.id)?.level === "escalate" ? "escalate" : c.level });
|
|
228
229
|
return classes;
|
|
229
230
|
}
|
|
@@ -239,7 +240,7 @@ export function gateOf(r, dir) {
|
|
|
239
240
|
// Robots: a team-plan role may list the routes it may read and write (globs over routes).
|
|
240
241
|
export function allowed(dir, actorId, action, route) {
|
|
241
242
|
if (!actorId || !dir) return true;
|
|
242
|
-
const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
|
|
243
|
+
const plan = [join(ventureFile(dir, "people/team-plan.json")), join(ventureFile(dir, "operations/team-plan.json"))].find((p) => existsSync(p));
|
|
243
244
|
if (!plan) return true;
|
|
244
245
|
const role = load(plan).roles.find((x) => x.id === actorId);
|
|
245
246
|
if (!role?.routes) return true;
|
|
@@ -264,11 +265,11 @@ const PACKAGE_ROUTE = {
|
|
|
264
265
|
const fileFor = (r, params, dir) => {
|
|
265
266
|
const cands = (r.collection === "item" ? r.files.map((f) => f.replace("*", params.id)) : r.files).filter((f) => !f.includes("*"));
|
|
266
267
|
if (dir) {
|
|
267
|
-
const hit = cands.find((f) => existsSync(
|
|
268
|
+
const hit = cands.find((f) => existsSync(ventureFile(dir, f)));
|
|
268
269
|
if (hit) return hit;
|
|
269
270
|
// A new collection item joins the folder the venture already keeps that collection in.
|
|
270
271
|
if (r.collection === "item") {
|
|
271
|
-
const lived = cands.find((f) => { const d =
|
|
272
|
+
const lived = cands.find((f) => { const d = ventureFile(dir, dirname(f)); return existsSync(d) && readdirSync(d).some((n) => n.endsWith(".json") || n.endsWith(".md") || n.endsWith(".mdx")); });
|
|
272
273
|
if (lived) return lived;
|
|
273
274
|
}
|
|
274
275
|
}
|
|
@@ -289,7 +290,7 @@ export function draftOf(route, venture, workspace) {
|
|
|
289
290
|
function agentsChain(dir, route) {
|
|
290
291
|
if (!dir) return [];
|
|
291
292
|
const chain = [];
|
|
292
|
-
const add = (rel) => { const f =
|
|
293
|
+
const add = (rel) => { const f = ventureFile(dir, rel); if (existsSync(f)) chain.push({ path: rel, text: readFileSync(f, "utf8") }); };
|
|
293
294
|
add("AGENTS.md");
|
|
294
295
|
const parts = route.split("/").filter(Boolean);
|
|
295
296
|
let acc = "";
|
|
@@ -392,7 +393,7 @@ async function callInAction({ venture, route, action, input, actor = "agent", ac
|
|
|
392
393
|
if (r.internal === "robots") return { ok: true, result: robots(dir), ...deprecated };
|
|
393
394
|
// Caching hook: system/cache, system/judge and system/answer are handled in schema/caching.mjs.
|
|
394
395
|
if (["cache", "judge", "answer"].includes(r.internal)) return { ...(await cacheInternal(r.internal, { dir, venture, workspace, input, actor, actor_id, via, provenance }, { call, resolve, draftOf })), ...deprecated };
|
|
395
|
-
const lacking = (r.needs ?? []).filter((f) => !existsSync(
|
|
396
|
+
const lacking = (r.needs ?? []).filter((f) => !existsSync(ventureFile(dir, f)));
|
|
396
397
|
if (lacking.length) return { ok: false, error: `${route} needs ${lacking.join(", ")} in ${venture}`, fix: { missing: lacking } };
|
|
397
398
|
if (r.value_flag && !input?.value) return { ok: false, error: `${route} needs a value (${r.note})` };
|
|
398
399
|
const args = r.venture_arg === "id" ? [dir, ...(input?.value ? [String(input.value)] : []), "--json"] : r.value_flag ? [dir, r.value_flag, String(input.value), "--json"] : [dir, ...(r.args ?? []), ...(r.as_is && input?.as_is ? ["--as-is"] : []), "--json"];
|
|
@@ -430,7 +431,7 @@ async function callInAction({ venture, route, action, input, actor = "agent", ac
|
|
|
430
431
|
if (r.collection === "list" && actor_id) items = items.filter((x) => mayReadItem(dir, actor_id, route, itemId(r.files, x.path)));
|
|
431
432
|
} catch (e) { return { ok: false, error: `${src.id}: ${String(e.message ?? e)}` }; }
|
|
432
433
|
const found = items.map((x) => x.path);
|
|
433
|
-
const draft = !remote && r.collection !== "list" && fileFor(r, params, dir) && existsSync(
|
|
434
|
+
const draft = !remote && r.collection !== "list" && fileFor(r, params, dir) && existsSync(ventureFile(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
|
|
434
435
|
const loading = draft ? { pending_draft: { file: draft, since: provEntries(dir).filter((e) => e.entity === draft).at(-1)?.at ?? null, accept: `mentura accept ${route} --venture ${venture}` } } : {};
|
|
435
436
|
if (remote) {
|
|
436
437
|
const prs = provEntries(dir).filter((e) => e.source?.pull_request && found.concat(patterns).some((f) => e.entity === address(src.id, f)));
|
|
@@ -493,7 +494,7 @@ async function callInAction({ venture, route, action, input, actor = "agent", ac
|
|
|
493
494
|
if (!input || typeof input !== "object") return { ok: false, error: "write needs the record as input" };
|
|
494
495
|
if (r.collection === "item" && !/^[a-z0-9][a-z0-9-]*$/.test(params.id)) return { ok: false, error: `${params.id} is not a valid id (lowercase letters, digits and hyphens)` };
|
|
495
496
|
const file = fileFor(r, params, dir);
|
|
496
|
-
if (["idea-dump", "raw-source", "raw-text"].includes(r.record) && existsSync(
|
|
497
|
+
if (["idea-dump", "raw-source", "raw-text"].includes(r.record) && existsSync(ventureFile(dir, file))) return { ok: false, error: `${route} is raw and immutable: add a new capture, do not edit this one` };
|
|
497
498
|
const gate = gateOf(r, dir);
|
|
498
499
|
const text = JSON.stringify(input, null, 2) + "\n";
|
|
499
500
|
// The schema first, on a scratch copy, so nothing reaches any source unless it matches.
|
|
@@ -619,7 +620,7 @@ async function overview(venture, workspace) {
|
|
|
619
620
|
|
|
620
621
|
// Robots: per role, the routes it may read and write.
|
|
621
622
|
function robots(dir) {
|
|
622
|
-
const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
|
|
623
|
+
const plan = [join(ventureFile(dir, "people/team-plan.json")), join(ventureFile(dir, "operations/team-plan.json"))].find((p) => existsSync(p));
|
|
623
624
|
if (!plan) return { note: "no team plan: every actor may reach every route; gates still apply", roles: [] };
|
|
624
625
|
return { note: "a role without routes may reach every route; gates still apply", roles: load(plan).roles.map((r) => ({ role: r.id, occupant: r.occupant, read: (r.routes?.read ?? ["**"]).join(", "), write: (r.routes?.write ?? (r.routes ? [] : ["**"])).join(", ") || "none" })) };
|
|
625
626
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: knowledge
|
|
3
|
+
tags: [everything-ecosystem, naive, skills, interface, artifacts, records, views]
|
|
4
|
+
ai-model: claude-opus-5.5
|
|
5
|
+
created: 2026-10-29T20:12:08Z
|
|
6
|
+
source: hybrid
|
|
7
|
+
name: choosing-artifacts
|
|
8
|
+
description: >
|
|
9
|
+
Decide whether a venture action should build an artifact (a rendered view a
|
|
10
|
+
person reads, presents, shares or fills in) and which kind, and keep records,
|
|
11
|
+
the source of truth in k-base/, separate from artifacts, which are views built
|
|
12
|
+
from records and cite them. General for any agent, with a section for Claude's
|
|
13
|
+
own artifacts in Claude Code and claude.ai.
|
|
14
|
+
MANDATORY TRIGGERS: make an artifact, build a page, make a deck, make a
|
|
15
|
+
dashboard, pitch deck, investor update, show me, visualize, one-pager, which
|
|
16
|
+
artifact, should this be a doc, render this record.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Choosing artifacts
|
|
20
|
+
|
|
21
|
+
Interface layer. An artifact is a rendered view; a record is the source of truth. Build an artifact from records when a person needs to look at, present, share or fill in something. Never let the artifact become the record.
|
|
22
|
+
|
|
23
|
+
## The core rule: records are not artifacts
|
|
24
|
+
|
|
25
|
+
- A **record** is a file in the venture's `k-base/` with a schema. It is what agents read, what decisions cite and what `naive check` tests.
|
|
26
|
+
- An **artifact** is a page, deck, dashboard or file made from one or more records for a reader. A marketing site describes the company, but the company's facts live in its records, not on the site.
|
|
27
|
+
- Every artifact names the records it was built from (route and revision or date), so a reader can trace each claim.
|
|
28
|
+
- When a record changes, rebuild the artifact. Never hand-edit an artifact to change a fact.
|
|
29
|
+
- What people type into an artifact (a form, a docket, comments) is input. Write it back through the router as a proposal or a draft in `_drafts/`; a person settles it. The artifact's own stored state is never the source of truth.
|
|
30
|
+
- Outside a Naive venture the same rule holds: name what the view was made from, and keep the facts where they already live.
|
|
31
|
+
|
|
32
|
+
## Should this be an artifact at all?
|
|
33
|
+
|
|
34
|
+
Answer in the conversation when the result is short, read once and acted on now. Build an artifact when any of these is true:
|
|
35
|
+
|
|
36
|
+
1. Someone other than the requester will read or use it.
|
|
37
|
+
2. It will be presented, printed, exported or sent.
|
|
38
|
+
3. A person needs to fill it in, decide on it or come back to it.
|
|
39
|
+
4. It is longer than about fifteen lines, or a picture explains it better than prose.
|
|
40
|
+
|
|
41
|
+
If it belongs in the venture's code (a README, a spec that ships, code docs), write a file in `codebase/` instead.
|
|
42
|
+
|
|
43
|
+
## Kinds of artifact
|
|
44
|
+
|
|
45
|
+
| Kind | Use it for |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Document | Writing people keep editing or pass around: memo, plan, spec, brief, report, notes |
|
|
48
|
+
| Deck | Something to present or page through: pitch, investor update, demo-day talk |
|
|
49
|
+
| Dashboard | Numbers people read: KPIs, runway, scorecards. Every number shows its source record |
|
|
50
|
+
| Design | Visual work: landing pages, screens, mockups, ads, social posts |
|
|
51
|
+
| Design system | The brand's tokens, type, components and assets, built from the brand record |
|
|
52
|
+
| Page | Explainers, diagrams, interactive tools, trackers, forms |
|
|
53
|
+
| File | Only when a format is named (.pptx, .docx, .xlsx, PDF) or an existing file is to be changed |
|
|
54
|
+
|
|
55
|
+
## Decide in order
|
|
56
|
+
|
|
57
|
+
1. Is it an artifact at all? (above)
|
|
58
|
+
2. Did the person name a format? Use it.
|
|
59
|
+
3. Writing to edit: document. To present: deck. Data to read: dashboard. A visual: design. A brand: design system.
|
|
60
|
+
4. Otherwise a page. Add a diagram, charts or interactivity only when each earns its place.
|
|
61
|
+
5. List the records it reads, and put that source list in the artifact.
|
|
62
|
+
6. Read the records through Naive (`mentura_read`, or `naive` routes), never from memory. A record that is missing, a draft or stale is shown as such, never filled in.
|
|
63
|
+
|
|
64
|
+
## Actions and their artifacts
|
|
65
|
+
|
|
66
|
+
Each function's skills say which of its actions build which artifact. This is the starting map; it is a proposal until the founder confirms it in `naive.config.json` or the function's own skill.
|
|
67
|
+
|
|
68
|
+
| Function | Action | Artifact | Built from |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| Foundation | Review the strategic foundation | Document (brief) | foundation/problem, foundation/icp, telos, the phase in venture.json |
|
|
71
|
+
| Foundation | Decide pending decisions | Page that collects answers (the docket) | decisions/; answers go back as decisions a person settles |
|
|
72
|
+
| Validation | Plan or report an experiment | Document | validation records and evidence/ |
|
|
73
|
+
| Validation | Track evidence over time | Dashboard | evidence/ and metric records |
|
|
74
|
+
| Validation | Run interviews | Page that collects answers (intake form) | the interview schema; answers become records |
|
|
75
|
+
| Marketing | Draft a landing page or ad | Design | positioning and marketing/brand |
|
|
76
|
+
| Marketing | Set up the brand | Design system | marketing/brand |
|
|
77
|
+
| Marketing | Plan content | Document | positioning and calendar records |
|
|
78
|
+
| Product | Write a spec or roadmap | Document | product/ and decisions/ |
|
|
79
|
+
| Product | Show a flow or architecture | Page with a diagram | product/ |
|
|
80
|
+
| Product | Prototype a screen | Design | product specs |
|
|
81
|
+
| Money | Show runway, burn, revenue | Dashboard | money/ (from the bank or accounting source in sources.json) |
|
|
82
|
+
| Money | Build a model or budget | File (.xlsx) when asked, else dashboard | money/ and finance/ |
|
|
83
|
+
| Investors | Pitch or investor update | Deck | foundation/, evidence/ (traction) and money/ |
|
|
84
|
+
| Any | Release notes for a release | Document | the release in meta/releases/ |
|
|
85
|
+
|
|
86
|
+
## Checks before you publish
|
|
87
|
+
|
|
88
|
+
- [ ] The artifact names its source records.
|
|
89
|
+
- [ ] No fact appears that is missing from a record. If one is needed, write the record first (as a draft) or mark the claim as a proposal.
|
|
90
|
+
- [ ] Agent-drafted content is marked as a proposal, never as the founder's words.
|
|
91
|
+
- [ ] Input the artifact collects has a path back to records.
|
|
92
|
+
- [ ] Nothing from `_private/`, no secrets, keys or `.env` contents.
|
|
93
|
+
- [ ] Nothing imitates a real person or organization.
|
|
94
|
+
|
|
95
|
+
## In Claude (Claude Code and claude.ai)
|
|
96
|
+
|
|
97
|
+
Claude publishes artifacts with its `Artifact` tool. The kinds above map onto the artifact types on the person's account, which change, so always start a new artifact with `Artifact` `action: "quickstart"` and the fitting `intent`; it names the type to start from and the default design system.
|
|
98
|
+
|
|
99
|
+
| Kind | Claude artifact type | Intent |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Document | Docs (through the Claude Docs connector) | `document` |
|
|
102
|
+
| Deck | Slides | `slides` |
|
|
103
|
+
| Dashboard | Dashboard | `other` |
|
|
104
|
+
| Design | Design | `design` |
|
|
105
|
+
| Design system | Design System | `other` |
|
|
106
|
+
| Page | plain HTML page, after loading `artifact-design` | `other` |
|
|
107
|
+
| File | the matching file skill (docx, pptx, xlsx, pdf) | none |
|
|
108
|
+
|
|
109
|
+
- **Diagram:** load `artifact-diagramming` when a picture shows the real mechanism.
|
|
110
|
+
- **Charts:** load `dataviz` before drawing any chart; prefer the Dashboard type when the page is mostly data.
|
|
111
|
+
- **Collecting input:** load `artifact-capabilities` and declare the smallest set: `db` and `user` for answers from several people (the docket, an intake form), `artifact` for a page that saves its own versions. Sync what it holds back to records through Naive; never treat the page's store as the record.
|
|
112
|
+
- **Updating:** republish to the same URL when the records change, so the link people hold stays current.
|
|
113
|
+
- Publish privately and give the link. Sharing it further is the founder's call.
|
package/interface/vitals.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { events } from "./instrumentation.mjs";
|
|
23
23
|
import { entries } from "./provenance.mjs";
|
|
24
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
24
25
|
|
|
25
26
|
export const THRESHOLDS = { calls_per_task: 25, tokens_per_call: 6000, time_to_useful_ms: 10000, decision_hours: 72 };
|
|
26
27
|
const DECISION = /(^|\/)decisions\/[^/]+\.decision\.json$/;
|
|
@@ -28,7 +29,7 @@ const DECISION = /(^|\/)decisions\/[^/]+\.decision\.json$/;
|
|
|
28
29
|
/** The 75th percentile, nearest rank. */
|
|
29
30
|
export const p75 = (xs) => { if (!xs.length) return null; const s = [...xs].sort((a, b) => a - b); return s[Math.ceil(0.75 * s.length) - 1]; };
|
|
30
31
|
const startOf = (e) => Date.parse(e.at) - (e.ms ?? 0);
|
|
31
|
-
const config = (dir) => { const f = join(dir, "naive.config.json"); try { return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : {}; } catch { return {}; } };
|
|
32
|
+
const config = (dir) => { const f = join(ventureFile(dir, "naive.config.json")); try { return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : {}; } catch { return {}; } };
|
|
32
33
|
|
|
33
34
|
/** Top-level calls grouped by session: one task per session. */
|
|
34
35
|
export function tasks(dir) {
|
|
@@ -55,7 +56,7 @@ export function decisionLatency(dir, now = Date.now()) {
|
|
|
55
56
|
}
|
|
56
57
|
return [...by.values()].map((d) => {
|
|
57
58
|
let id = d.file.split("/").at(-1).replace(/\.decision\.json$/, "");
|
|
58
|
-
try { id = JSON.parse(readFileSync(
|
|
59
|
+
try { id = JSON.parse(readFileSync(ventureFile(dir, d.file), "utf8")).id ?? id; } catch {}
|
|
59
60
|
const end = d.decided ? Date.parse(d.decided) : now;
|
|
60
61
|
return { id, ...d, open: !d.decided, hours: Math.round(((end - Date.parse(d.opened)) / 36e5) * 100) / 100 };
|
|
61
62
|
});
|
|
@@ -15,6 +15,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { address, isText, words } from "./contract.mjs";
|
|
17
17
|
import { connectAll } from "./index.mjs";
|
|
18
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
18
19
|
|
|
19
20
|
export const FILE = "_catalog/catalog.json";
|
|
20
21
|
const MAX_BYTES = 1_000_000;
|
|
@@ -50,7 +51,7 @@ async function routeOf(path) {
|
|
|
50
51
|
return { route: null, record: null };
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export const load = (dir) => existsSync(
|
|
54
|
+
export const load = (dir) => existsSync(ventureFile(dir, FILE)) ? JSON.parse(readFileSync(ventureFile(dir, FILE), "utf8")) : null;
|
|
54
55
|
|
|
55
56
|
/** Builds or refreshes the catalog. Returns it with stats: items, read (items fetched), reused. */
|
|
56
57
|
export async function build(dir, { write = true, sources } = {}) {
|
|
@@ -75,7 +76,7 @@ export async function build(dir, { write = true, sources } = {}) {
|
|
|
75
76
|
}
|
|
76
77
|
stats.items = items.length;
|
|
77
78
|
const catalog = { built_at: new Date().toISOString(), sources: conns.map(({ source }) => ({ id: source.id, connector: source.connector })), items };
|
|
78
|
-
if (write) { mkdirSync(join(dir, "_catalog"), { recursive: true }); writeFileSync(
|
|
79
|
+
if (write) { mkdirSync(join(dir, "_catalog"), { recursive: true }); writeFileSync(ventureFile(dir, FILE), JSON.stringify(catalog) + "\n"); }
|
|
79
80
|
return { catalog, stats };
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -13,6 +13,7 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { blobSha, isText, matches, safePath } from "./contract.mjs";
|
|
16
|
+
import { layoutOf, logicalFiles, ventureFile } from "../../schema/venture-paths.mjs";
|
|
16
17
|
|
|
17
18
|
const SKIP = new Set(["node_modules"]);
|
|
18
19
|
|
|
@@ -29,7 +30,11 @@ export function connect({ id = "venture", root, writes = "direct", venture = fal
|
|
|
29
30
|
}
|
|
30
31
|
return e.isFile() ? [r] : [];
|
|
31
32
|
});
|
|
32
|
-
|
|
33
|
+
// A venture in the structured layout (EE-086) keeps its records in knowledge/ and meta/: read and write them
|
|
34
|
+
// by their logical paths, and list them that way, so routes never see the layout.
|
|
35
|
+
const structured = venture && layoutOf(root) === "structured";
|
|
36
|
+
const file = (p) => (structured ? ventureFile(root, safePath(p)) : join(root, safePath(p)));
|
|
37
|
+
const listed = () => structured ? logicalFiles(root).filter((r) => !r.split("/").some((s) => s.startsWith(".")) && !r.startsWith("_") && !r.startsWith("node_modules/")) : walk(root);
|
|
33
38
|
const git = (...args) => spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
|
|
34
39
|
const lastCommit = (p, content) => {
|
|
35
40
|
const tracked = git("ls-files", "-s", "--", p);
|
|
@@ -42,8 +47,8 @@ export function connect({ id = "venture", root, writes = "direct", venture = fal
|
|
|
42
47
|
id, kind: "filesystem", writes, root,
|
|
43
48
|
async list(prefix = "") {
|
|
44
49
|
const pre = prefix ? safePath(prefix) : "";
|
|
45
|
-
return
|
|
46
|
-
.map((path) => ({ path, version: blobSha(readFileSync(
|
|
50
|
+
return listed().filter((p) => !pre || p === pre || p.startsWith(`${pre}/`)).sort()
|
|
51
|
+
.map((path) => ({ path, version: blobSha(readFileSync(file(path))) }));
|
|
47
52
|
},
|
|
48
53
|
async read(p) {
|
|
49
54
|
const f = file(p);
|
|
@@ -58,9 +63,9 @@ export function connect({ id = "venture", root, writes = "direct", venture = fal
|
|
|
58
63
|
},
|
|
59
64
|
async search(query, { limit = 50 } = {}) {
|
|
60
65
|
const out = [];
|
|
61
|
-
for (const path of
|
|
66
|
+
for (const path of listed().sort()) {
|
|
62
67
|
if (out.length >= limit) break;
|
|
63
|
-
const content = readFileSync(
|
|
68
|
+
const content = readFileSync(file(path), "utf8");
|
|
64
69
|
if (isText(path, content) && matches(`${path}\n${content}`, query)) out.push({ path, version: blobSha(content) });
|
|
65
70
|
}
|
|
66
71
|
return out;
|
|
@@ -17,6 +17,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
17
17
|
import { isAbsolute, join, resolve } from "node:path";
|
|
18
18
|
import { conformance } from "./contract.mjs";
|
|
19
19
|
import { ANY, resolve as resolveRule } from "./resolve.mjs";
|
|
20
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
20
21
|
|
|
21
22
|
const repo = new URL("../..", import.meta.url).pathname;
|
|
22
23
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -34,8 +35,8 @@ export function registry() {
|
|
|
34
35
|
|
|
35
36
|
/** A venture's source map, with the implicit venture source first. */
|
|
36
37
|
export function sourcesOf(dir) {
|
|
37
|
-
const own = existsSync(
|
|
38
|
-
return { sources: [{ id: LOCAL, connector: "filesystem", root: ".", writes: "direct", implicit: true }, ...(own.sources ?? [])], records: own.records ?? {}, rules: own.rules ?? [], mapped: existsSync(
|
|
38
|
+
const own = existsSync(ventureFile(dir, MAP)) ? load(ventureFile(dir, MAP)) : { sources: [], records: {} };
|
|
39
|
+
return { sources: [{ id: LOCAL, connector: "filesystem", root: ".", writes: "direct", implicit: true }, ...(own.sources ?? [])], records: own.records ?? {}, rules: own.rules ?? [], mapped: existsSync(ventureFile(dir, MAP)) };
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { join, relative } from "node:path";
|
|
13
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
13
14
|
|
|
14
15
|
export const CONTENT = "operations/marketing/content";
|
|
15
16
|
const RUN = 10;
|
|
@@ -27,24 +28,24 @@ export function frontmatter(text) {
|
|
|
27
28
|
|
|
28
29
|
function files(dir) {
|
|
29
30
|
if (!existsSync(dir)) return [];
|
|
30
|
-
return readdirSync(dir).flatMap((f) => statSync(
|
|
31
|
+
return readdirSync(dir).flatMap((f) => statSync(ventureFile(dir, f)).isDirectory() ? files(ventureFile(dir, f)) : f.endsWith(".mdx") ? [ventureFile(dir, f)] : []);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function check(dir) {
|
|
34
35
|
const venture = existsSync(join(dir, "venture.json")) ? JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")) : {};
|
|
35
36
|
const publicText = [venture.name, venture.brand?.tagline].filter(Boolean);
|
|
36
|
-
const pdir = join(dir, "products");
|
|
37
|
+
const pdir = join(ventureFile(dir, "products"));
|
|
37
38
|
if (existsSync(pdir)) for (const f of readdirSync(pdir).filter((f) => f.endsWith(".json"))) publicText.push(JSON.parse(readFileSync(join(pdir, f), "utf8")).name ?? "");
|
|
38
39
|
const allowed = new Set(publicText.flatMap((t) => [...runs(t)]));
|
|
39
|
-
const results = files(
|
|
40
|
+
const results = files(ventureFile(dir, CONTENT)).map((f) => {
|
|
40
41
|
const rel = relative(dir, f), { meta, body } = frontmatter(readFileSync(f, "utf8")), errors = [];
|
|
41
42
|
if (!meta.variant_of) errors.push("names no internal record (variant_of): marketing content is a variant of an internal record");
|
|
42
|
-
else if (!existsSync(
|
|
43
|
+
else if (!existsSync(ventureFile(dir, meta.variant_of))) errors.push(`variant_of ${meta.variant_of} does not exist`);
|
|
43
44
|
if (!meta.audience) errors.push("names no audience");
|
|
44
45
|
if (meta.publish === undefined) errors.push("does not say whether it is published (publish: true or false)");
|
|
45
46
|
if (meta.publish === true && !meta.approved_by) errors.push("is published without a person's approval (approved_by, EE-025)");
|
|
46
|
-
if (meta.variant_of && existsSync(
|
|
47
|
-
const src =
|
|
47
|
+
if (meta.variant_of && existsSync(ventureFile(dir, meta.variant_of))) {
|
|
48
|
+
const src = ventureFile(dir, meta.variant_of);
|
|
48
49
|
const raw = readFileSync(src, "utf8");
|
|
49
50
|
const internal = new Set(strings(src.endsWith(".json") ? JSON.parse(raw) : raw).flatMap((s) => [...runs(s)]));
|
|
50
51
|
const leaks = [...runs(body)].filter((r) => internal.has(r) && !allowed.has(r));
|