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
|
@@ -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
|
}
|
|
@@ -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
|
}
|
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));
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
import { customerNeeds, factsOf, MATRIX, needs, typesOf, weights } from "./presence.mjs";
|
|
22
|
+
import { ventureFile } from "../../../schema/venture-paths.mjs";
|
|
22
23
|
|
|
23
24
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
24
25
|
export const SITEMAP = "operations/marketing/sitemap.json";
|
|
@@ -33,10 +34,10 @@ const RECORD_FILE = { venture: "venture.json", icp: "purpose/icp.json", "validat
|
|
|
33
34
|
|
|
34
35
|
/** The offerings: each product, what kind of offering it is, whom it is offered to and whom it serves through them. */
|
|
35
36
|
export function offeringsOf(dir) {
|
|
36
|
-
const icp = existsSync(join(dir, "purpose/icp.json")) ? load(join(dir, "purpose/icp.json")) : { profiles: [] };
|
|
37
|
+
const icp = existsSync(join(ventureFile(dir, "purpose/icp.json"))) ? load(join(ventureFile(dir, "purpose/icp.json"))) : { profiles: [] };
|
|
37
38
|
const profiles = new Map(icp.profiles.map((p) => [p.id, p]));
|
|
38
39
|
const primary = icp.profiles.find((p) => p.primary) ?? icp.profiles[0];
|
|
39
|
-
const pdir = join(dir, "products");
|
|
40
|
+
const pdir = join(ventureFile(dir, "products"));
|
|
40
41
|
const products = existsSync(pdir) ? readdirSync(pdir).filter((f) => f.endsWith(".json")).map((f) => load(join(pdir, f))).filter((p) => !["retired", "archived", "idea"].includes(p.status)) : [];
|
|
41
42
|
return { profiles, products: products.map((p) => {
|
|
42
43
|
// Who it is offered to: the product says so; otherwise the profiles others are reached through
|
|
@@ -104,7 +105,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
104
105
|
if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node plan.mjs <venture> [--raising] [--hiring] [--contributors] [--write] [--json]"); process.exit(2); }
|
|
105
106
|
const has = (n) => process.argv.includes(`--${n}`);
|
|
106
107
|
const r = plan(dir, { flags: Object.fromEntries(["raising", "hiring", "contributors"].filter(has).map((k) => [k, true])) });
|
|
107
|
-
if (has("write")) { mkdirSync(join(dir, dirname(SITEMAP)), { recursive: true }); writeFileSync(
|
|
108
|
+
if (has("write")) { mkdirSync(join(dir, dirname(SITEMAP)), { recursive: true }); writeFileSync(ventureFile(dir, SITEMAP), JSON.stringify(r, null, 2) + "\n"); }
|
|
108
109
|
if (has("json")) console.log(JSON.stringify(r, null, 2));
|
|
109
110
|
else {
|
|
110
111
|
console.log(`${r.name}: site planned for ${r.audience.offered_to.map((p) => p.id).join(", ") || "no ICP yet"} (${r.audience.types.map((t) => t.label).join(", ") || "customers in general"})${r.audience.partner_led ? "; partner-led" : ""}`);
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { dirname, join } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { ventureFile } from "../../../schema/venture-paths.mjs";
|
|
25
26
|
|
|
26
27
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -34,7 +35,7 @@ const r2 = (x) => Math.round(x * 100) / 100;
|
|
|
34
35
|
/** The venture's facts: flags the founder gave, else what its ICP record says about who it courts. */
|
|
35
36
|
export function factsOf(dir, flags = {}) {
|
|
36
37
|
const f = { raising: false, hiring: false, contributors: false };
|
|
37
|
-
const icpFile = dir && join(dir, "purpose/icp.json");
|
|
38
|
+
const icpFile = dir && join(ventureFile(dir, "purpose/icp.json"));
|
|
38
39
|
if (icpFile && existsSync(icpFile)) {
|
|
39
40
|
const said = JSON.stringify(load(icpFile)).toLowerCase();
|
|
40
41
|
if (/investor|angel|\bvc\b/.test(said)) f.raising = true;
|
|
@@ -57,7 +58,7 @@ export function icpOf(dir, site) {
|
|
|
57
58
|
const who = ok.filter((p) => /customer|solution|who|industr|use-case|about|for-/i.test(p.path));
|
|
58
59
|
const says = [...new Set([home, ...who].filter(Boolean).flatMap((p) => (p.text ?? "").match(SAYS) ?? []).map((x) => x.trim()))].slice(0, 8);
|
|
59
60
|
const opening = (home?.text ?? "").slice(0, 500);
|
|
60
|
-
const icpFile = dir && join(dir, "purpose/icp.json");
|
|
61
|
+
const icpFile = dir && join(ventureFile(dir, "purpose/icp.json"));
|
|
61
62
|
const stated = icpFile && existsSync(icpFile) ? (load(icpFile).profiles ?? []).map((p) => p.who ?? p.description ?? p.name).filter(Boolean) : null;
|
|
62
63
|
return { text: [says.join(" | "), opening].filter(Boolean).join(" || "), source: says.length ? "site" : "inferred", says,
|
|
63
64
|
cite: says.length ? `what the site says about who it is for (${says.length} phrase${says.length > 1 ? "s" : ""}), with the home page's opening` : "the home page's opening text (inferred: the site does not say outright who it is for)",
|
|
@@ -237,7 +238,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
237
238
|
const answers = opt("answers") ? (load(opt("answers")).answers ?? load(opt("answers"))) : null;
|
|
238
239
|
const facts = factsOf(dir, Object.fromEntries(["raising", "hiring", "contributors"].filter(has).map((k) => [k, true])));
|
|
239
240
|
const r = score(site, { answers, facts, icp: icpOf(dir, site), venture: dir ? load(join(dir, "venture.json")).id : null });
|
|
240
|
-
if (dir) { mkdirSync(join(dir, dirname(PRESENCE)), { recursive: true }); writeFileSync(
|
|
241
|
+
if (dir) { mkdirSync(join(dir, dirname(PRESENCE)), { recursive: true }); writeFileSync(ventureFile(dir, PRESENCE), JSON.stringify(r, null, 2) + "\n"); }
|
|
241
242
|
console.log(JSON.stringify(r, null, 2));
|
|
242
243
|
} else if (a && existsSync(join(a, "venture.json"))) {
|
|
243
244
|
if (!existsSync(join(a, PRESENCE))) { console.error(`${PRESENCE} does not exist yet: read the site first (node interface/harness/site.mjs <url> --venture <dir>)`); process.exit(1); }
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
24
25
|
|
|
25
26
|
export const SANDBOX = "https://rhoapi-sandbox.rho.co/mcp/v1";
|
|
26
27
|
export const LIVE = "https://rhoapi.rho.co/mcp/v1";
|
|
@@ -107,7 +108,7 @@ export function summarize(data, syncedAt) {
|
|
|
107
108
|
const json = (f, v) => { mkdirSync(join(f, ".."), { recursive: true }); writeFileSync(f, `${JSON.stringify(v, null, 2)}\n`); };
|
|
108
109
|
|
|
109
110
|
export function write(venture, data, syncedAt = new Date().toISOString()) {
|
|
110
|
-
const money = join(venture, "money");
|
|
111
|
+
const money = join(ventureFile(venture, "money"));
|
|
111
112
|
const pointer = (kind, id) => `${data.source}:${kind}:${id}`;
|
|
112
113
|
for (const a of data.accounts)
|
|
113
114
|
json(join(money, `${a.id}.json`), { id: a.id, name: a.account_name, type: a.account_type, last_4: a.account_number_last_4 ?? null, balance: a.balance, source: pointer("account", a.id), synced_at: syncedAt });
|