naive-framework 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/decisions.json +15 -0
- package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
- package/interface/adapters.mjs +7 -6
- package/interface/boundaries.mjs +5 -4
- package/interface/brand.mjs +6 -5
- package/interface/cli/mentura.mjs +4 -3
- package/interface/cli/naive.mjs +11 -6
- package/interface/context.mjs +7 -6
- package/interface/demo/board.mjs +9 -8
- package/interface/demo/decision-harness.mjs +4 -3
- package/interface/gate-hooks.mjs +2 -1
- package/interface/harness/evals.mjs +8 -7
- package/interface/harness/run.mjs +14 -13
- package/interface/instrumentation.mjs +4 -3
- package/interface/layer.json +4 -0
- package/interface/package-routes.mjs +4 -3
- package/interface/provenance.mjs +5 -4
- package/interface/reads.mjs +4 -3
- package/interface/release-card.mjs +2 -1
- package/interface/router.mjs +10 -9
- package/interface/skills/choosing-artifacts/SKILL.md +113 -0
- package/interface/vitals.mjs +3 -2
- package/knowledge/connectors/catalog.mjs +3 -2
- package/knowledge/connectors/filesystem.mjs +10 -5
- package/knowledge/connectors/index.mjs +3 -2
- package/operations/marketing/check-public.mjs +7 -6
- package/operations/marketing/site/plan.mjs +4 -3
- package/operations/marketing/site/presence.mjs +4 -3
- package/package.json +1 -1
- package/platform/integrations/rho.mjs +2 -1
- package/purpose/frameworks/idea-map/route.mjs +5 -4
- package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
- package/schema/access.mjs +2 -1
- package/schema/add-department.mjs +3 -2
- package/schema/assess-problem.mjs +3 -2
- package/schema/assets.mjs +6 -5
- package/schema/brand.mjs +11 -10
- package/schema/caching.mjs +5 -4
- package/schema/check-config.mjs +4 -3
- package/schema/check-data-model.mjs +2 -1
- package/schema/check-data-packages.mjs +6 -5
- package/schema/check-execution.mjs +4 -3
- package/schema/check-people.mjs +22 -8
- package/schema/check-private.mjs +2 -1
- package/schema/check-ready.mjs +6 -5
- package/schema/check-sources.mjs +2 -1
- package/schema/check-where-used.mjs +4 -3
- package/schema/decision-analytics.mjs +2 -1
- package/schema/decisions.mjs +14 -11
- package/schema/foundation.mjs +8 -7
- package/schema/founder.schema.json +2 -2
- package/schema/frames.mjs +4 -3
- package/schema/freshness.mjs +13 -12
- package/schema/gate-drafts.mjs +8 -7
- package/schema/init-venture.mjs +48 -20
- package/schema/migrate-venture.mjs +8 -7
- package/schema/migrations/kit-to-package.mjs +4 -3
- package/schema/migrations/package-layout.mjs +9 -5
- package/schema/model-steps.mjs +8 -7
- package/schema/naive-config.mjs +4 -3
- package/schema/need-now.mjs +5 -4
- package/schema/packages.mjs +11 -10
- package/schema/payouts.mjs +3 -2
- package/schema/people.schema.json +2 -3
- package/schema/port-venture.mjs +206 -132
- package/schema/release-read.mjs +5 -4
- package/schema/releases.mjs +21 -20
- package/schema/run-handoff.mjs +5 -4
- package/schema/setup.mjs +5 -4
- package/schema/stale.mjs +6 -5
- package/schema/structure.mjs +5 -4
- package/schema/time-to-ready.mjs +7 -6
- package/schema/triggers.mjs +7 -6
- package/schema/upgrades.mjs +31 -30
- package/schema/venture-paths.mjs +97 -0
- package/schema/venture.schema.json +25 -17
- package/schema/workspace.schema.json +5 -1
package/AGENTS.md
CHANGED
|
@@ -33,7 +33,7 @@ The Everything Ecosystem methodology, packaged as one private repo (EE-001). Pro
|
|
|
33
33
|
- `node schema/check-publication.mjs <dir>` checks each venture's publication policy and every handoff that publishes: a named channel and content rule, and a person approving (EE-025).
|
|
34
34
|
- `node schema/check-validation.mjs <dir> [--json]` grades each venture's validation records from their cited evidence (high to very low, adapted from GRADE). The validation package is `packages/validation/` (EE-071). A kit, Mentura's enhanced package, will live under `platform/kits/` and is not built yet.
|
|
35
35
|
- `node schema/check-execution.mjs <venture dir>` checks handoffs, evidence and decision rights; `node schema/run-handoff.mjs <venture dir> <handoff id>` runs a handoff's done-criteria and writes evidence (EE-017).
|
|
36
|
-
- `node interface/cli/naive.mjs typegen` writes `.naive/types/` from the route table and the MCP tools. `mentura typegen` is the same command. `naive upgrade <venture dir>` (was `mentura migrate`) runs the migrations in `schema/upgrades.json` up to the installed Naive and pins versions in `naive.config.json`; `--check`, `--guide`, `--adopt <package>`, `--canary`, `--agent` (a staged copy accepted with `--accept --confirm upgrade`) and `--framework` are in `schema/upgrade-venture.mjs` (`node schema/upgrades-test.mjs`). `mentura triggers` (`schema/triggers.mjs`) shows the triggers that run after each write and raises a failed run as a decision; `mentura notify` and `mentura stale` (`schema/stale.mjs`) mark summaries stale when a source changes; `mentura adapt` and `mentura adapters verify` (`interface/adapters.mjs`) write and test harness adapters. `naive port
|
|
36
|
+
- `node interface/cli/naive.mjs typegen` writes `.naive/types/` from the route table and the MCP tools. `mentura typegen` is the same command. `naive upgrade <venture dir>` (was `mentura migrate`) runs the migrations in `schema/upgrades.json` up to the installed Naive and pins versions in `naive.config.json`; `--check`, `--guide`, `--adopt <package>`, `--canary`, `--agent` (a staged copy accepted with `--accept --confirm upgrade`) and `--framework` are in `schema/upgrade-venture.mjs` (`node schema/upgrades-test.mjs`). `mentura triggers` (`schema/triggers.mjs`) shows the triggers that run after each write and raises a failed run as a decision; `mentura notify` and `mentura stale` (`schema/stale.mjs`) mark summaries stale when a source changes; `mentura adapt` and `mentura adapters verify` (`interface/adapters.mjs`) write and test harness adapters. `naive port <folder> --venture <dir>` plans moving a folder into the venture whole and unchanged (to `repo/<name>` unless `--to` says where), naming what the move would break (worktrees, links into it, processes inside it); `--apply --confirm port` moves it with a rename, names it in `sources.json` as a read-only source of truth and logs the move in provenance (EE-076, a move since 2026-10-29; `node schema/port-test.mjs`). Each run writes `.release/port-trace.json` for `platform/integrations/langsmith.mjs`. `naive migrate --from <project> --venture <dir>` has an agent rewrite the project as records, settled by `migrate-<function>` decisions and written through the router with `--apply` (`node schema/migrate-test.mjs`, with a stand-in agent). Neither opens a file that looks like a secret. `schema/init-venture.mjs` creates new ventures only. `node schema/check-private.mjs <dir>` fails when a private record or field could reach a model. `node schema/gate.mjs` is not a CLI: the gate reads `naive.config.json` and package `gate_rules`, matches the router's path patterns and never calls a model. `node schema/check-config.mjs <venture dir>` checks `naive.config.json` (`--explain <route>`, `--rules`, `--resolved`, `--pin`); `node schema/gate-drafts.mjs <venture dir> [--owner <role>]` lists held drafts; `node schema/gate-test.mjs` tests the gate, the config, who is acting (`interface/acting.mjs`) and access at the record.
|
|
37
37
|
- `node interface/cli/mentura.mjs <route> <action> --venture <id>` reads, writes, checks or runs one route; `node interface/mcp/server.mjs` is the MCP server and `node interface/api/server.mjs` the local API, all over `interface/router.mjs` (EE-030). Routes follow the Next.js App Router (EE-032): `[id]` collection items, nested ventures by path, layout, view, not-found, loading and error, route groups by phase, the `system/overview` slots and team-plan robots; see `docs/reference/ROUTES.md`. `node interface/test.mjs` tests them. Records are read and written through connectors for the venture's sources (`knowledge/connectors/`, EE-037); `node knowledge/connectors/eval.mjs` runs the framework's (filesystem) and `node platform/connectors/eval.mjs` the platform's (GitHub), both against `knowledge/connectors/suite.mjs`. The source map's `rules` are the source-of-truth matrix (EE-042, `knowledge/connectors/resolve.mjs`): a system of record and owner per record type, attribute and context; `node schema/check-sources.mjs <dir>` refuses ties, and `node schema/check-where-used.mjs <dir>` reports where-used children that drifted from their parent. Every write through them appends to the venture's provenance log, `provenance.jsonl` (EE-031); `node schema/check-provenance.mjs <dir>` checks its hash chain, and `mentura baseline --venture <id>` adopts records written before the log. `node schema/need-now.mjs <venture dir>` says what a venture needs now (EE-028); `node schema/time-to-ready.mjs <venture dir>` measures time to running and ready; `node schema/check-team.mjs <dir>` checks team plans against the agent capability register (EE-029).
|
|
38
38
|
- `node schema/assess-problem.mjs <venture dir> [--as-is]` (route `purpose/validation/assess`) says what the problem's evidence is strong enough for: four assumptions labelled demonstrated to contradicted, the commitment they support (explore, prototype, charge, raise) and the next test, with no single score; it asks first when the record is incomplete (EE-050). `node schema/foundation.mjs <dir>` (route `purpose/foundation`) reports the strategic foundation by the Five Ws and How plus whether, and whose each is; `mentura confirm <statement>` and `mentura accept` of a foundation record need the name typed (EE-051).
|
|
39
39
|
- `node schema/freshness.mjs <venture dir>` (route `system/freshness`, `mentura freshness`) says which records are fresh, stale, expired or live holes: by time from freshness profiles (`knowledge/freshness.json`, overridden under `freshness` in `naive.config.json`, with per-phase profiles) and on demand when a dependency changed or a tag, path or package was revalidated (`mentura revalidate`, `verify`, `phase`; route `system/revalidate`). A stale record is still read, with a `freshness` flag; `--freeze <dir>` writes a shell that leaves live records as holes (Revalidating, V-1 to V-8; `node schema/freshness-test.mjs`).
|
|
@@ -883,6 +883,21 @@
|
|
|
883
883
|
],
|
|
884
884
|
"decision": "Setup asks where each of the venture's functions keeps its truth, and records the answers as pointers in knowledge/sources.json under `functions` (the source-of-truth matrix, EE-042, not a second file), with `truth` naming where the venture's source of truth as a whole lives (local by default). schema/systems-catalog.json lists the systems offered per function (a local repo, GitHub, Vercel, Cloudflare, Stripe, Square, Shopify, Rho, Google Drive, DocuSign, a local folder). A system Naive reads through code is connected (Rho, through its MCP API: sandbox, live with RHO_API_TOKEN, or a saved fixture marked as one); the rest are pointed until a connector exists. A function with no system keeps the founder's words. naive dev serves a setup page at /setup (interface/dev/setup.mjs) that walks through people and hats, sources of truth, the strategic foundation and first decisions (each answer an approved decision; a foundation statement as a decision with `statement`), and a goal for discovery, and says when the structure is established and what comes next. A new venture's .gitignore keeps sensitive stream activity out of git.",
|
|
885
885
|
"why": "Torian, 2026-10-29: when scaffolding, ask the user to point Naive to the source of truth for each function (the repo, GitHub and Vercel for a software product; analytics from the host; Stripe, Square or Shopify for sales; Rho for financials in the demo; legal documents, with the connections to set them up if missing), let a function default or be answered in prose, and ship a local setup experience like the one Next.js gives, essentially the strategic foundation and a version of the decision docket, ending by setting a goal for the discovery phase. After setup a founder has the structure of the venture and a way of routing decisions; operating it is Mentura's, as deploying is Vercel's after Next.js."
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"id": "EE-086",
|
|
889
|
+
"date": "2026-10-29",
|
|
890
|
+
"status": "in-force",
|
|
891
|
+
"related": [
|
|
892
|
+
"EE-047",
|
|
893
|
+
"EE-070",
|
|
894
|
+
"EE-071",
|
|
895
|
+
"EE-082",
|
|
896
|
+
"EE-083",
|
|
897
|
+
"EE-085"
|
|
898
|
+
],
|
|
899
|
+
"decision": "The venture project structure (docs/design/VENTURE_STRUCTURE_2026-10-29.md). At the top: venture.json, README.md (was profile.md), AGENTS.md, knowledge/ (the knowledge base), meta/ (sources.json, naive.config.json, provenance.jsonl, releases) and an optional repo/ for code. Packages sit flat in knowledge/. venture.json declares the business functions once (the org chart, flat by default with one function per package owned by the founder); packages and records inherit their function from it, a founder composes functions over several packages, (group)/ folders are an optional view, and hats derive from function ownership. A record's route is its path inside knowledge/, with groups and the extension dropped: no name table, no reserved route names. Skills teach where files go; code guarantees schema, the person-only gate, private data, provenance and outside systems. A function's publishable items are in public.json where it has any.",
|
|
900
|
+
"why": "Torian, 2026-10-29: the folder structure is a visual aid and an educational device for how a founder should think about the business; put the knowledge base in a knowledge folder, code in an optional repo folder like src, and add a meta folder; let people group and merge functions their own way, starting from a flat org chart declared in the venture file that everything inherits from; routes were overcomplicated, and agents can handle file conventions with skills. Answers marked default in the design note are Claude's recommendations until Torian changes them."
|
|
886
901
|
}
|
|
887
902
|
]
|
|
888
903
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { join, extname, basename } from "node:path";
|
|
18
|
+
import { ventureFile } from "../../../schema/venture-paths.mjs";
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------- lexicons
|
|
20
21
|
|
|
@@ -59,7 +60,7 @@ const jsonOut = flag("json");
|
|
|
59
60
|
function walk(dir, acc = []) {
|
|
60
61
|
for (const entry of readdirSync(dir)) {
|
|
61
62
|
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
62
|
-
const full =
|
|
63
|
+
const full = ventureFile(dir, entry);
|
|
63
64
|
const st = statSync(full);
|
|
64
65
|
if (st.isDirectory()) walk(full, acc);
|
|
65
66
|
else if (extname(entry) === ".tsx" && !/\.(test|spec|stories)\.tsx$/.test(entry)) acc.push(full);
|
package/interface/adapters.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { dirname, 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
|
export const STATE = ".naive/adapters.json";
|
|
@@ -45,7 +46,7 @@ export async function skillsFor(dir) {
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const readState = (dir) => (existsSync(
|
|
49
|
+
const readState = (dir) => (existsSync(ventureFile(dir, STATE)) ? load(ventureFile(dir, STATE)) : { adapters: {} });
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Writes a harness adapter's files into a venture: its entry files, and a copy of each open package's
|
|
@@ -61,7 +62,7 @@ export async function adapt(dir, name) {
|
|
|
61
62
|
if (a.skills) for (const s of await skillsFor(dir)) want.push({ path: `${a.skills}/${s.id}/SKILL.md`, text: readFileSync(join(repo, s.path), "utf8"), skill: s.id, package: s.package });
|
|
62
63
|
const written = [], current = [], kept = [], files = {};
|
|
63
64
|
for (const w of want) {
|
|
64
|
-
const f =
|
|
65
|
+
const f = ventureFile(dir, w.path);
|
|
65
66
|
const now = existsSync(f) ? readFileSync(f, "utf8") : null;
|
|
66
67
|
if (now === w.text) { current.push(w.path); files[w.path] = sha(w.text); continue; }
|
|
67
68
|
// Never overwrite a file a person wrote, or one they changed after the adapter wrote it.
|
|
@@ -74,12 +75,12 @@ export async function adapt(dir, name) {
|
|
|
74
75
|
// A skill copy for a package that is no longer open goes, if the adapter wrote it and nobody changed it.
|
|
75
76
|
const removed = [];
|
|
76
77
|
for (const [p, h] of Object.entries(mine)) {
|
|
77
|
-
if (want.some((w) => w.path === p) || !existsSync(
|
|
78
|
-
if (sha(readFileSync(
|
|
78
|
+
if (want.some((w) => w.path === p) || !existsSync(ventureFile(dir, p))) continue;
|
|
79
|
+
if (sha(readFileSync(ventureFile(dir, p), "utf8")) === h) { rmSync(ventureFile(dir, p)); removed.push(p); } else kept.push({ path: p, why: "no longer needed, but changed by a person, so kept" });
|
|
79
80
|
}
|
|
80
81
|
state.adapters[name] = { at: new Date().toISOString(), files };
|
|
81
82
|
mkdirSync(join(dir, ".naive"), { recursive: true });
|
|
82
|
-
writeFileSync(
|
|
83
|
+
writeFileSync(ventureFile(dir, STATE), JSON.stringify(state, null, 2) + "\n");
|
|
83
84
|
return { adapter: name, written, current, kept, removed, skills: want.filter((w) => w.skill).map((w) => w.skill) };
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -102,7 +103,7 @@ async function harnessSuite(a) {
|
|
|
102
103
|
const up = spawnSync("node", [join(repo, "schema/upgrade-venture.mjs"), dir], { encoding: "utf8" });
|
|
103
104
|
t("a new venture is created and upgraded to the installed framework", init.status === 0 && up.status === 0, init.stderr + up.stderr);
|
|
104
105
|
const first = await adapt(dir, a.name);
|
|
105
|
-
t("every entry file is written with the adapter's text", a.files.every((f) => existsSync(
|
|
106
|
+
t("every entry file is written with the adapter's text", a.files.every((f) => existsSync(ventureFile(dir, f.path)) && readFileSync(ventureFile(dir, f.path), "utf8") === f.text), JSON.stringify(first.written));
|
|
106
107
|
t("the harness reaches the venture's AGENTS.md (it reads it, or an entry file imports or names it)", a.reads.includes("AGENTS.md") || a.files.some((f) => /(^|\s)@AGENTS\.md\b|AGENTS\.md/.test(f.text)));
|
|
107
108
|
t("no entry file is private or outside the venture", a.files.every((f) => !f.path.startsWith("_private/") && !f.path.includes("..") && !f.path.startsWith("/")));
|
|
108
109
|
const { call } = await import("./router.mjs");
|
package/interface/boundaries.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { spawnSync } from "node:child_process";
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { basename, dirname, join } from "node:path";
|
|
21
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
21
22
|
|
|
22
23
|
const repo = new URL("..", import.meta.url).pathname;
|
|
23
24
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
@@ -45,7 +46,7 @@ const VENTURE_SCHEMA = () => load(join(repo, "schema/venture.schema.json"));
|
|
|
45
46
|
export function minimalVenture(dir) {
|
|
46
47
|
const id = basename(dir).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "venture";
|
|
47
48
|
return { id, name: id, parent: null, phase: "discovery", owner: "self", status: "active", profile: "profile.md",
|
|
48
|
-
layers: Object.fromEntries(["purpose", "knowledge", "operations", "evidence", "governance", "interface"].map((l) => [l, existsSync(
|
|
49
|
+
layers: Object.fromEntries(["purpose", "knowledge", "operations", "evidence", "governance", "interface"].map((l) => [l, existsSync(ventureFile(dir, l)) ? { location: `${l}/` } : false])) };
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -154,7 +155,7 @@ export async function boundaries(dir, { only = null } = {}) {
|
|
|
154
155
|
const pats = r.files;
|
|
155
156
|
const hits = files(dir).filter((f) => pats.some((g) => glob(g).test(f) && routable(f, g)));
|
|
156
157
|
for (const f of hits) {
|
|
157
|
-
const problem = f.endsWith(".json") ? jsonProblem(
|
|
158
|
+
const problem = f.endsWith(".json") ? jsonProblem(ventureFile(dir, f)) ?? (r.schema ? validateFile(ventureFile(dir, f), r.schema) : null) : null;
|
|
158
159
|
records.push({ record: t, route: r.collection === "item" ? r.route.replace("[id]", starOf(f, pats) ?? basename(f)) : r.route, file: f, ok: !problem,
|
|
159
160
|
...(problem ? { error: problem, fix: { schema: r.schema, template: r.template, skills: pkg.skill?.path ? [pkg.skill.path] : [] } } : {}) });
|
|
160
161
|
}
|
|
@@ -191,9 +192,9 @@ export function recover(dir) {
|
|
|
191
192
|
const h = ventureHealth(dir);
|
|
192
193
|
if (!h) return { ok: true, result: "venture.json is sound: nothing to recover" };
|
|
193
194
|
const kept = join("_private", `venture.json.broken-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
194
|
-
if (existsSync(join(dir, "venture.json"))) { mkdirSync(join(dir, "_private"), { recursive: true }); renameSync(join(dir, "venture.json"),
|
|
195
|
+
if (existsSync(join(dir, "venture.json"))) { mkdirSync(join(ventureFile(dir, "_private")), { recursive: true }); renameSync(join(dir, "venture.json"), ventureFile(dir, kept)); }
|
|
195
196
|
writeFileSync(join(dir, "venture.json"), JSON.stringify(minimalVenture(dir), null, 2) + "\n");
|
|
196
|
-
return { ok: true, result: { written: "venture.json", kept: existsSync(
|
|
197
|
+
return { ok: true, result: { written: "venture.json", kept: existsSync(ventureFile(dir, kept)) ? kept : null, note: "a minimal venture.json: copy back what you need from the kept file, then check it" } };
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
package/interface/brand.mjs
CHANGED
|
@@ -25,13 +25,14 @@ import { call, ventureDir } from "./router.mjs";
|
|
|
25
25
|
import { agentFromEnv, record as prov } from "./provenance.mjs";
|
|
26
26
|
import { altStatement, checkAssets, facts as allFacts, factsOf, REGISTER, registerOf } from "../schema/assets.mjs";
|
|
27
27
|
import { brandOf, checkBrand, resolveBrand, sitemapOf, sitemapXml, termsFor, tokens, voice } from "../schema/brand.mjs";
|
|
28
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
28
29
|
|
|
29
30
|
const VERSION = "0.1.0";
|
|
30
31
|
const today = () => new Date().toISOString();
|
|
31
32
|
|
|
32
33
|
/** The facts code reads, in the register's shape. */
|
|
33
34
|
export const factsFor = (dir, path) => {
|
|
34
|
-
const f = factsOf(readFileSync(
|
|
35
|
+
const f = factsOf(readFileSync(ventureFile(dir, path)));
|
|
35
36
|
return Object.fromEntries(Object.entries({ type: f.type, width: f.width, height: f.height, sizes: f.sizes, bytes: f.bytes, sha256: f.sha256, filled_at: today() }).filter(([, v]) => v !== undefined));
|
|
36
37
|
};
|
|
37
38
|
|
|
@@ -47,7 +48,7 @@ export async function addAsset({ venture, workspace, path, source, license, cred
|
|
|
47
48
|
const dir = ventureDir(venture, workspace);
|
|
48
49
|
if (!dir) return { ok: false, error: `no venture ${venture}` };
|
|
49
50
|
if (!path?.startsWith("public/")) return { ok: false, error: `${path} is not in public/` };
|
|
50
|
-
if (!existsSync(
|
|
51
|
+
if (!existsSync(ventureFile(dir, path))) return { ok: false, error: `${path} is not in ${venture}` };
|
|
51
52
|
const reg = registerOf(dir) ?? emptyRegister();
|
|
52
53
|
const entry = Object.fromEntries(Object.entries({ path, source, license, credit, alt: alt ? { text: alt, by: actor === "person" ? "person" : "agent" } : undefined, decorative: decorative || undefined, origin_url, design_system, facts: factsFor(dir, path) }).filter(([, v]) => v !== undefined));
|
|
53
54
|
reg.assets = [...reg.assets.filter((a) => a.path !== path), entry].sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -64,7 +65,7 @@ export async function fill({ venture, workspace }) {
|
|
|
64
65
|
if (!reg) return { ok: false, error: `${venture} has no ${REGISTER}; register a file first: mentura assets add public/<file> ...` };
|
|
65
66
|
const changed = [], missing = [];
|
|
66
67
|
for (const a of reg.assets) {
|
|
67
|
-
if (!existsSync(
|
|
68
|
+
if (!existsSync(ventureFile(dir, a.path))) { missing.push(a.path); continue; }
|
|
68
69
|
const f = factsFor(dir, a.path);
|
|
69
70
|
if (!a.facts || ["type", "width", "height", "bytes", "sha256"].some((k) => (a.facts[k] ?? null) !== (f[k] ?? null))) { a.facts = f; changed.push(a.path); }
|
|
70
71
|
}
|
|
@@ -116,12 +117,12 @@ export async function addRemote({ venture, workspace, url, license, credit, id,
|
|
|
116
117
|
try { new URL(url); } catch { return { ok: false, error: `${url} is not a URL` }; }
|
|
117
118
|
const rid = id ?? rawId(url);
|
|
118
119
|
const reg = registerOf(dir) ?? emptyRegister();
|
|
119
|
-
if (!existsSync(join(dir, "raw/sources", rid, "source.json"))) {
|
|
120
|
+
if (!existsSync(join(ventureFile(dir, "raw/sources"), rid, "source.json"))) {
|
|
120
121
|
const w = await call({ venture, route: `raw/sources/${rid}`, action: "write", input: { id: rid, origin: url, title: `Remote image at ${new URL(url).host}`, fetched: today().slice(0, 10) }, actor, workspace, via: "cli", provenance: { note: "a remote image, kept as a link (J-3)" } });
|
|
121
122
|
if (!w.ok) return w;
|
|
122
123
|
const text = [`# Remote image`, "", `- Link: ${url}`, `- Host: ${new URL(url).host}`, `- License: ${license}`, `- Credit: ${credit ?? "(missing)"}`, ...(fetched ? [`- Fetched: ${fetched.type}${fetched.width ? `, ${fetched.width}x${fetched.height}` : ""}, ${fetched.bytes} bytes, sha256 ${fetched.sha256}`] : []), "",
|
|
123
124
|
copy ? `A copy is kept at ${copy}: the license allows it.` : "The venture links to this image. A full copy stays private (raw/sources/<id>/original.*, out of git) unless the license allows one.", ...(note ? ["", note] : []), ""].join("\n");
|
|
124
|
-
writeFileSync(join(dir, "raw/sources", rid, "text.md"), text);
|
|
125
|
+
writeFileSync(join(ventureFile(dir, "raw/sources"), rid, "text.md"), text);
|
|
125
126
|
prov(dir, { entity: `raw/sources/${rid}/text.md`, route: `raw/sources/${rid}`, activity: "write", agent: agentFromEnv(actor === "person" ? "person" : "agent", "cli"), note: "the note beside a remote image (J-3)" });
|
|
126
127
|
}
|
|
127
128
|
const entry = Object.fromEntries(Object.entries({ id: rid, url, license, credit, alt: alt ? { text: alt, by: actor === "person" ? "person" : "agent" } : undefined, fetched, copy }).filter(([, v]) => v !== undefined));
|
|
@@ -66,6 +66,7 @@ import { approve, decisions, propose } from "../../schema/decisions.mjs";
|
|
|
66
66
|
// Gate group hook (X-2): held drafts kept side by side.
|
|
67
67
|
import { pick as pickDraft, settle as settleDraft } from "../../schema/gate-drafts.mjs";
|
|
68
68
|
import { asRouted, findVenture } from "./find-venture.mjs";
|
|
69
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
69
70
|
|
|
70
71
|
// The danger zone (EE-051): a foundation statement is accepted or confirmed only when the person types
|
|
71
72
|
// its name, at the prompt or with --confirm <name>.
|
|
@@ -255,7 +256,7 @@ if (cmd === "understand") {
|
|
|
255
256
|
// Mentura got wrong, and confirms the understanding before the ideas are routed.
|
|
256
257
|
const venture = opt("venture"), dir = venture && ventureDir(venture, workspace);
|
|
257
258
|
if (!dir) usage();
|
|
258
|
-
const hits = ["purpose/ideas/map.mdx", "purpose/ideas/graph.json"].filter((f) => existsSync(
|
|
259
|
+
const hits = ["purpose/ideas/map.mdx", "purpose/ideas/graph.json"].filter((f) => existsSync(ventureFile(dir, f)));
|
|
259
260
|
if (!hits.length) { console.error(`${venture} has no idea map yet: map the idea dump first (purpose/ideas)`); process.exit(1); }
|
|
260
261
|
if (!(await typed("ideas", `Confirming Mentura's understanding of your ideas: ${hits.join(", ")}`, "Your ideas are routed to their records from this understanding."))) { console.error("not confirmed: the confirmation did not match"); process.exit(1); }
|
|
261
262
|
for (const f of hits) record(dir, { entity: f, route: f.endsWith(".mdx") ? "purpose/ideas" : "purpose/ideas/graph", activity: "confirm", statement: "ideas", agent: agentFromEnv("person", "cli"), note: opt("note") });
|
|
@@ -271,10 +272,10 @@ if (cmd === "confirm") {
|
|
|
271
272
|
const s = FRAME.questions.flatMap((q) => q.statements).find((x) => x.id === id);
|
|
272
273
|
if (!s?.record) { console.error(`${id} is not a foundation statement with a record (see mentura purpose/foundation run)`); process.exit(1); }
|
|
273
274
|
const rt = routes().find((x) => x.record === s.record && x.collection !== "list")?.route ?? s.record;
|
|
274
|
-
const hits = filesOf(dir, s.record).filter((f) => existsSync(
|
|
275
|
+
const hits = filesOf(dir, s.record).filter((f) => existsSync(ventureFile(dir, f)));
|
|
275
276
|
if (!hits.length) { console.error(`${venture} has no ${s.record} record to confirm`); process.exit(1); }
|
|
276
277
|
const get = (o, p) => p.split(".").reduce((x, k) => (x == null ? undefined : x[k]), o);
|
|
277
|
-
const shown = hits.map((f) => ` ${f}: ${JSON.stringify(get(JSON.parse(readFileSync(
|
|
278
|
+
const shown = hits.map((f) => ` ${f}: ${JSON.stringify(get(JSON.parse(readFileSync(ventureFile(dir, f), "utf8")), s.attribute) ?? null)}`).join("\n");
|
|
278
279
|
if (!(await typed(id, `Confirming ${id} as yours:\n${shown}`))) { console.error("not confirmed: the confirmation did not match"); process.exit(1); }
|
|
279
280
|
for (const f of hits) record(dir, { entity: f, route: rt, activity: "confirm", statement: id, agent: agentFromEnv("person", "cli"), note: opt("note") });
|
|
280
281
|
out(`confirmed: ${id}`);
|
package/interface/cli/naive.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// naive typegen write .naive/types/ from the route table and the MCP tools
|
|
18
18
|
// naive upgrade [<venture dir>] [--framework] move a venture onto the next Naive layout; --framework first
|
|
19
19
|
// updates the installed framework with the venture's package manager (I-29)
|
|
20
|
-
// naive port
|
|
20
|
+
// naive port <folder> --venture <dir> [--to <path>] plan moving a folder into the venture as it is (codebase/<name>)
|
|
21
21
|
// naive port --venture <dir> --apply --confirm port
|
|
22
22
|
// carry out that plan (EE-076)
|
|
23
23
|
// naive migrate --from <project> --venture <dir> an agent rewrites the project as Naive records, as a plan and decisions
|
|
@@ -30,6 +30,8 @@ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:
|
|
|
30
30
|
import { createServer } from "node:net";
|
|
31
31
|
import { basename, join, relative, resolve } from "node:path";
|
|
32
32
|
import { asRouted, findVenture } from "./find-venture.mjs";
|
|
33
|
+
import { logical } from "../../schema/venture-paths.mjs";
|
|
34
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
33
35
|
|
|
34
36
|
const repo = new URL("../..", import.meta.url).pathname;
|
|
35
37
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -83,15 +85,18 @@ async function dev(dir) {
|
|
|
83
85
|
console.log(`\n ▲ Naive ${version()}\n - Venture: ${name} (${relative(process.cwd(), dir) || "."})\n - Setup: http://127.0.0.1:${port}/setup\n - Board: http://127.0.0.1:${port}/\n - Map: http://127.0.0.1:${port}/map\n\n Watching for changes. Each saved record is checked against its schema. Ctrl-C to stop.\n`);
|
|
84
86
|
// Live feedback on save (I-24): a record that no longer matches its schema says so at once.
|
|
85
87
|
const pending = new Map();
|
|
86
|
-
watch(dir, { recursive: true }, (_,
|
|
87
|
-
if (!
|
|
88
|
+
watch(dir, { recursive: true }, (_, raw) => {
|
|
89
|
+
if (!raw || !raw.endsWith(".json") || raw.startsWith("node_modules") || raw.startsWith(".naive")) return;
|
|
90
|
+
// The record's logical path (schema/venture-paths.mjs): knowledge/foundation/icp.json is foundation/icp.json.
|
|
91
|
+
const physicalPath = raw, f = logical(dir, raw);
|
|
92
|
+
if (f.startsWith("_private")) return;
|
|
88
93
|
clearTimeout(pending.get(f));
|
|
89
94
|
pending.set(f, setTimeout(async () => {
|
|
90
95
|
pending.delete(f);
|
|
91
|
-
if (!existsSync(
|
|
96
|
+
if (!existsSync(ventureFile(dir, physicalPath))) return;
|
|
92
97
|
const schema = await schemaFor(f);
|
|
93
98
|
if (!schema) return;
|
|
94
|
-
const v = spawnSync("python3", [join(repo, "schema/validate.py"), "--schema", schema.replace(/^schema\//, ""), "--file",
|
|
99
|
+
const v = spawnSync("python3", [join(repo, "schema/validate.py"), "--schema", schema.replace(/^schema\//, ""), "--file", ventureFile(dir, physicalPath)], { encoding: "utf8" });
|
|
95
100
|
const out = (v.stdout + v.stderr).trim();
|
|
96
101
|
if (v.status === 0) console.log(` ✓ ${f}`);
|
|
97
102
|
else if (/needs jsonschema/.test(out)) console.log(` - ${f} changed (install python3's jsonschema package to check it as you save)`);
|
|
@@ -129,7 +134,7 @@ function info(dir) {
|
|
|
129
134
|
`Platform ${process.platform} ${process.arch}`,
|
|
130
135
|
`python3 ${spawnSync("python3", ["-c", "import jsonschema; print('jsonschema ' + __import__('importlib.metadata').metadata.version('jsonschema'))"], { encoding: "utf8" }).stdout.trim() || "jsonschema missing (needed by schema checks)"}`,
|
|
131
136
|
`Venture ${v ? `${v.name ?? v.id} (${v.phase}) at ${dir}` : "none here"}`,
|
|
132
|
-
"Agents " + agents.map(([name, f, ok]) => `${name}: ${dir && ok(read(
|
|
137
|
+
"Agents " + agents.map(([name, f, ok]) => `${name}: ${dir && ok(read(ventureFile(dir, f))) ? "connected" : "not connected"}`).join(", "),
|
|
133
138
|
];
|
|
134
139
|
console.log(lines.join("\n"));
|
|
135
140
|
}
|
package/interface/context.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import { dirname, join, relative } from "node:path";
|
|
|
46
46
|
import { call, draftOf, gateOf, glob, layout as routerLayout, resolve, routes, ventureDir } from "./router.mjs";
|
|
47
47
|
import { entries as provEntries } from "./provenance.mjs";
|
|
48
48
|
import { tokens } from "./instrumentation.mjs";
|
|
49
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
49
50
|
|
|
50
51
|
const here = dirname(new URL(import.meta.url).pathname);
|
|
51
52
|
const repoRoot = join(here, "..");
|
|
@@ -58,7 +59,7 @@ export const COMPUTED = ["need_now", "need-now", "next", "ranking", "rank", "ope
|
|
|
58
59
|
|
|
59
60
|
/** The venture's context settings: the defaults, overridden by naive.config.json context. */
|
|
60
61
|
export function settings(dir) {
|
|
61
|
-
const f = dir && join(dir, "naive.config.json");
|
|
62
|
+
const f = dir && join(ventureFile(dir, "naive.config.json"));
|
|
62
63
|
let c = {};
|
|
63
64
|
try { if (f && existsSync(f)) c = load(f).context ?? {}; } catch {}
|
|
64
65
|
return { ...DEFAULTS, ...c };
|
|
@@ -75,8 +76,8 @@ export function budgetFor(set, route) {
|
|
|
75
76
|
|
|
76
77
|
/** The state of one venture file: missing, draft waiting, agent-written, confirmed, baseline or unlogged. */
|
|
77
78
|
export function stateOf(dir, file, draft) {
|
|
78
|
-
if (draft && existsSync(
|
|
79
|
-
if (!file || !existsSync(
|
|
79
|
+
if (draft && existsSync(ventureFile(dir, draft))) return { state: "draft waiting", draft };
|
|
80
|
+
if (!file || !existsSync(ventureFile(dir, file))) return { state: "missing" };
|
|
80
81
|
let state = "unlogged", last = null;
|
|
81
82
|
for (const e of provEntries(dir).filter((x) => x.entity === file)) {
|
|
82
83
|
last = e;
|
|
@@ -102,7 +103,7 @@ export function titleOf(rec) {
|
|
|
102
103
|
function ownerOf(r, dir, route) {
|
|
103
104
|
const g = r.kind === "record" ? gateOf(r, dir) : null;
|
|
104
105
|
if (g?.owner) return g.owner;
|
|
105
|
-
const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
|
|
106
|
+
const plan = [join(ventureFile(dir, "people/team-plan.json")), join(ventureFile(dir, "operations/team-plan.json"))].find((p) => existsSync(p));
|
|
106
107
|
if (!plan) return null;
|
|
107
108
|
try { return load(plan).roles.find((x) => (x.routes?.write ?? []).some((gl) => glob(gl).test(route)))?.id ?? null; } catch { return null; }
|
|
108
109
|
}
|
|
@@ -134,7 +135,7 @@ export function headerOf({ r, route, dir, venture, workspace, res }) {
|
|
|
134
135
|
/** Every AGENTS.md from the venture root down the route's segments, nearest last. */
|
|
135
136
|
export function chainOf(dir, route) {
|
|
136
137
|
const out = [];
|
|
137
|
-
const add = (rel) => { const f =
|
|
138
|
+
const add = (rel) => { const f = ventureFile(dir, rel); if (existsSync(f)) out.push({ path: rel, text: readFileSync(f, "utf8") }); };
|
|
138
139
|
add("AGENTS.md");
|
|
139
140
|
let acc = "";
|
|
140
141
|
for (const p of route.split("/").filter(Boolean)) { acc = acc ? `${acc}/${p}` : p; add(`${acc}/AGENTS.md`); }
|
|
@@ -465,7 +466,7 @@ async function measureBudget(dir) {
|
|
|
465
466
|
: e.isDirectory() ? (existsSync(join(d, rel, e.name, "venture.json")) ? [] : agentsFiles(d, rel ? `${rel}/${e.name}` : e.name)) : e.name === "AGENTS.md" ? [rel ? `${rel}/AGENTS.md` : "AGENTS.md"] : []);
|
|
466
467
|
for (const f of agentsFiles(dir)) {
|
|
467
468
|
const folder = dirname(f) === "." ? "" : dirname(f);
|
|
468
|
-
const text = readFileSync(
|
|
469
|
+
const text = readFileSync(ventureFile(dir, f), "utf8");
|
|
469
470
|
for (const line of text.split("\n")) {
|
|
470
471
|
const s = line.match(SCOPED), m = s ? [null, s[2], s[3]] : line.match(PLAIN);
|
|
471
472
|
if (!m) continue;
|
package/interface/demo/board.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { scan } from "../harness/site.mjs";
|
|
|
23
23
|
import { MATRIX } from "../../operations/marketing/site/presence.mjs";
|
|
24
24
|
import { plan } from "../../operations/marketing/site/plan.mjs";
|
|
25
25
|
import { packages, write as writePackage, OUT as PACKAGE } from "../../schema/packages.mjs";
|
|
26
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
26
27
|
|
|
27
28
|
const arg = (n, d) => { const i = process.argv.indexOf(`--${n}`); return i >= 0 ? process.argv[i + 1] : d; };
|
|
28
29
|
const port = Number(arg("port", 4331));
|
|
@@ -31,14 +32,14 @@ const venture = arg("venture", ventures()[0]?.id);
|
|
|
31
32
|
function state(id) {
|
|
32
33
|
const dir = ventureDir(id);
|
|
33
34
|
if (!dir) return { error: `no venture ${id}` };
|
|
34
|
-
const log = existsSync(join(dir, "provenance.jsonl")) ? readFileSync(join(dir, "provenance.jsonl"), "utf8").trim().split("\n").filter(Boolean).slice(-8).map((l) => JSON.parse(l)).reverse() : [];
|
|
35
|
+
const log = existsSync(join(ventureFile(dir, "provenance.jsonl"))) ? readFileSync(join(ventureFile(dir, "provenance.jsonl")), "utf8").trim().split("\n").filter(Boolean).slice(-8).map((l) => JSON.parse(l)).reverse() : [];
|
|
35
36
|
// The kit for now (EE-028): what need-now says this venture needs in its phase, and which kit holds it.
|
|
36
37
|
const nn = spawnSync("node", [new URL("../../schema/need-now.mjs", import.meta.url).pathname, dir, "--json"], { encoding: "utf8" });
|
|
37
38
|
const kitOf = new Map(routes().filter((r) => r.record).map((r) => [r.record, { kit: r.kit, route: r.route.replace(/\/\[id\]$/, "") }]));
|
|
38
39
|
const rows = nn.status === 0 ? JSON.parse(nn.stdout).rows : [];
|
|
39
40
|
const kit = rows.filter((r) => r.now >= 0.5 || r.status === "coming soon").map((r) => ({ type: r.type, status: r.status, now: r.now, next: r.next, why: r.why, ...(kitOf.get(r.type) ?? {}) }));
|
|
40
41
|
// Money (EE-070): only the summary, never the transactions stream; the source stays the system of record.
|
|
41
|
-
const money = existsSync(join(dir, "money/_summary.json")) ? JSON.parse(readFileSync(join(dir, "money/_summary.json"), "utf8")) : null;
|
|
42
|
+
const money = existsSync(join(ventureFile(dir, "money/_summary.json"))) ? JSON.parse(readFileSync(join(ventureFile(dir, "money/_summary.json")), "utf8")) : null;
|
|
42
43
|
return { ...decisions(dir), kit, money, log: log.map((e) => ({ at: e.at, activity: e.activity, entity: e.entity, who: `${e.agent?.kind ?? "?"}${e.agent?.id ? ` ${e.agent.id}` : ""}`, via: e.agent?.via })) };
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -46,9 +47,9 @@ function state(id) {
|
|
|
46
47
|
function ideas(id) {
|
|
47
48
|
const dir = ventureDir(id);
|
|
48
49
|
if (!dir) return { error: `no venture ${id}` };
|
|
49
|
-
if (!existsSync(
|
|
50
|
+
if (!existsSync(ventureFile(dir, MAP))) return { venture: id, waiting: "no idea dump yet" };
|
|
50
51
|
const v = JSON.parse(readFileSync(join(dir, "venture.json"), "utf8"));
|
|
51
|
-
const map = parse(readFileSync(
|
|
52
|
+
const map = parse(readFileSync(ventureFile(dir, MAP), "utf8"));
|
|
52
53
|
return { venture: id, name: v.name, phase: v.phase, meta: map.meta, ideas: map.ideas, summary: summary(map, v.phase), pillars: CONVENTIONS.pillars, departments: CONVENTIONS.departments };
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -132,8 +133,8 @@ ReactDOM.createRoot(document.getElementById("flow")).render(h(App));
|
|
|
132
133
|
function graph(id) {
|
|
133
134
|
const dir = ventureDir(id);
|
|
134
135
|
if (!dir) return { error: `no venture ${id}` };
|
|
135
|
-
if (!existsSync(
|
|
136
|
-
return { venture: id, name: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).name, ...JSON.parse(readFileSync(
|
|
136
|
+
if (!existsSync(ventureFile(dir, GRAPH))) return { venture: id, waiting: "no idea graph yet" };
|
|
137
|
+
return { venture: id, name: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).name, ...JSON.parse(readFileSync(ventureFile(dir, GRAPH), "utf8")) };
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
const GRAPH_PAGE = MAP_PAGE.replace("<title>Idea map</title>", "<title>Idea graph</title>").replace(/<script>\nconst h=React[\s\S]*<\/script><\/body>/, `<script>
|
|
@@ -500,7 +501,7 @@ async function vendor(path) {
|
|
|
500
501
|
function packageOf(dir, writeIt = false) {
|
|
501
502
|
const r = packages(dir);
|
|
502
503
|
if (writeIt) writePackage(dir, r);
|
|
503
|
-
const has = (f) => existsSync(
|
|
504
|
+
const has = (f) => existsSync(ventureFile(dir, f));
|
|
504
505
|
let rt = null; try { rt = route(dir); } catch {}
|
|
505
506
|
const d = decisions(dir).decisions;
|
|
506
507
|
const settled = d.filter((x) => ["decided", "accepted"].includes(x.status)).length;
|
|
@@ -537,7 +538,7 @@ export function serve(p = port, id = venture) {
|
|
|
537
538
|
if (u.pathname === "/routes.json") { res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }); res.end(JSON.stringify(routesOf(u.searchParams.get("venture") ?? id))); return; }
|
|
538
539
|
if (u.pathname === "/site") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(SITE_PAGE); return; }
|
|
539
540
|
if (u.pathname === "/site.json") {
|
|
540
|
-
const dir = ventureDir(u.searchParams.get("venture") ?? id), f = dir && join(dir, "operations/marketing/site-presence.json");
|
|
541
|
+
const dir = ventureDir(u.searchParams.get("venture") ?? id), f = dir && join(ventureFile(dir, "operations/marketing/site-presence.json"));
|
|
541
542
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }); res.end(f && existsSync(f) ? readFileSync(f) : "{}"); return;
|
|
542
543
|
}
|
|
543
544
|
if (u.pathname === "/site/plan.json") {
|
|
@@ -14,6 +14,7 @@ import { cpSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { createInterface } from "node:readline";
|
|
17
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
17
18
|
|
|
18
19
|
const repo = new URL("../../", import.meta.url).pathname;
|
|
19
20
|
const auto = process.argv.includes("--auto");
|
|
@@ -57,7 +58,7 @@ try {
|
|
|
57
58
|
expect(!p.error && p.body.awaiting === "approval", `propose: ${JSON.stringify(p.body)}`);
|
|
58
59
|
await say("naive", `Proposed ${p.body.proposed}. It waits for a person: an agent cannot decide it.`);
|
|
59
60
|
|
|
60
|
-
const card = JSON.parse(readFileSync(join(dir, "products/shop-app.json"), "utf8"));
|
|
61
|
+
const card = JSON.parse(readFileSync(join(ventureFile(dir, "products/shop-app.json")), "utf8"));
|
|
61
62
|
const changed = { ...card, description: "Payroll first, then bookings and stock, for small service shops" };
|
|
62
63
|
const w1 = await tool("mentura_write", { venture, route: "purpose/product/shop-app", record: changed, agent_id: "builder", note: "reposition around payroll" });
|
|
63
64
|
expect(w1.error && /locked until decision reposition-shop-app/.test(w1.body.error), `first write: ${JSON.stringify(w1.body)}`);
|
|
@@ -78,9 +79,9 @@ try {
|
|
|
78
79
|
const w2 = await tool("mentura_write", { venture, route: "purpose/product/shop-app", record: changed, agent_id: "builder", decision: "reposition-shop-app", note: "reposition around payroll" });
|
|
79
80
|
expect(!w2.error, `second write: ${JSON.stringify(w2.body)}`);
|
|
80
81
|
await say("agent", `Resumed: wrote ${w2.body.written}.`);
|
|
81
|
-
const log = readFileSync(join(dir, "provenance.jsonl"), "utf8").trim().split("\n").slice(-3).map((l) => JSON.parse(l));
|
|
82
|
+
const log = readFileSync(join(ventureFile(dir, "provenance.jsonl")), "utf8").trim().split("\n").slice(-3).map((l) => JSON.parse(l));
|
|
82
83
|
await say("naive", `Provenance, last three entries:\n${log.map((e) => ` ${e.activity.padEnd(7)} ${e.entity.padEnd(48)} by ${e.agent.kind}${e.agent.id ? ` ${e.agent.id}` : ""} via ${e.agent.via}`).join("\n")}`);
|
|
83
|
-
const dec = JSON.parse(readFileSync(join(dir, "products/decisions/shop-app.reposition.decision.json"), "utf8"));
|
|
84
|
+
const dec = JSON.parse(readFileSync(join(ventureFile(dir, "products/decisions/shop-app.reposition.decision.json")), "utf8"));
|
|
84
85
|
await say("naive", `The decision carries its outcome to test later: "${dec.outcomes[0].projected}" (${dec.outcomes[0].result ?? "pending"}).`);
|
|
85
86
|
}
|
|
86
87
|
} finally {
|
package/interface/gate-hooks.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { evaluate } from "../schema/gate.mjs";
|
|
9
9
|
import { writeHeld } from "../schema/gate-drafts.mjs";
|
|
10
10
|
import { checksFor, configFor } from "../schema/naive-config.mjs";
|
|
11
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The gate's verdict on a local write, or null when it passes. A person is never held, only
|
|
@@ -19,7 +20,7 @@ export function gateWrite({ dir, r, route, requested, file, actor, actor_id, pro
|
|
|
19
20
|
catch (e) { return actor === "person" ? null : { then: "draft", rule: "naive.config.json", source: "naive.config.json", why: `${e.message}: the gate fails closed, written as a draft for a person to accept` }; }
|
|
20
21
|
const confidence = provenance.confidence === undefined || provenance.confidence === null || provenance.confidence === "" ? undefined : Number(provenance.confidence);
|
|
21
22
|
const v = evaluate({ config, dir, route, table: r.route, requested, record: r.record, actor, role: actor_id, confidence: Number.isNaN(confidence) ? undefined : confidence,
|
|
22
|
-
exists: existsSync(
|
|
23
|
+
exists: existsSync(ventureFile(dir, file)), decisionOwner: gate?.owner ?? undefined });
|
|
23
24
|
if (v.then === "pass") return null;
|
|
24
25
|
if (actor === "person" && v.then !== "refuse") return null;
|
|
25
26
|
return v;
|
|
@@ -11,6 +11,7 @@ import { DUMP, MAP, parse } from "../../purpose/frameworks/idea-map/ideamap.mjs"
|
|
|
11
11
|
import { GRAPH } from "../../purpose/frameworks/idea-map/graph.mjs";
|
|
12
12
|
import { ideasOf } from "../../purpose/frameworks/idea-map/route.mjs";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
+
import { ventureFile } from "../../schema/venture-paths.mjs";
|
|
14
15
|
|
|
15
16
|
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
16
17
|
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
@@ -22,29 +23,29 @@ export function evaluate(dir, { researched = [], settled = [] } = {}) {
|
|
|
22
23
|
const add = (key, score, comment) => out.push({ key, score, comment });
|
|
23
24
|
|
|
24
25
|
// The founder's ideas stay theirs: every idea on the map is their text, unchanged.
|
|
25
|
-
const dump = norm(readFileSync(
|
|
26
|
-
const ideas = existsSync(
|
|
26
|
+
const dump = norm(readFileSync(ventureFile(dir, DUMP), "utf8"));
|
|
27
|
+
const ideas = existsSync(ventureFile(dir, MAP)) ? parse(readFileSync(ventureFile(dir, MAP), "utf8")).ideas : [];
|
|
27
28
|
const changed = ideas.filter((i) => !dump.includes(norm(i.text)));
|
|
28
29
|
add("ideas_verbatim", ideas.length ? frac(ideas.length - changed.length, ideas.length) : 0, changed.length ? `rewritten: ${changed.map((i) => i.id).join(", ")}` : `${ideas.length} ideas, all verbatim`);
|
|
29
30
|
add("ideas_tagged", frac(ideas.filter((i) => i.departments?.length).length, ideas.length), `${ideas.filter((i) => !i.departments?.length).length} ideas without a department`);
|
|
30
31
|
|
|
31
32
|
// The idea graph is a Merkle DAG: every hash recomputes from the idea's text and what it builds on.
|
|
32
|
-
if (existsSync(
|
|
33
|
-
const g = load(
|
|
33
|
+
if (existsSync(ventureFile(dir, GRAPH))) {
|
|
34
|
+
const g = load(ventureFile(dir, GRAPH));
|
|
34
35
|
const byId = Object.fromEntries(g.nodes.map((n) => [n.id, n]));
|
|
35
36
|
const bad = g.nodes.filter((n) => createHash("sha256").update(JSON.stringify({ text: n.text, parents: n.builds_on.map((p) => byId[p]?.hash).sort() })).digest("hex").slice(0, 16) !== n.hash);
|
|
36
37
|
add("graph_merkle", bad.length ? 0 : 1, bad.length ? `hashes that do not recompute: ${bad.map((n) => n.id).join(", ")}` : `${g.nodes.length} hashes recompute; root ${g.root}`);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
// Agents propose; people decide. No accept, confirm or decision is an agent's.
|
|
40
|
-
const log = existsSync(join(dir, "provenance.jsonl")) ? readFileSync(join(dir, "provenance.jsonl"), "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) : [];
|
|
41
|
+
const log = existsSync(join(ventureFile(dir, "provenance.jsonl"))) ? readFileSync(join(ventureFile(dir, "provenance.jsonl")), "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) : [];
|
|
41
42
|
const byAgent = log.filter((e) => ["accept", "confirm"].includes(e.activity) && e.agent?.kind !== "person");
|
|
42
43
|
const r = decisions(dir);
|
|
43
44
|
const decidedByAgent = r.decisions.filter((d) => d.status === "decided" && d.provenance && d.provenance.approved_by && log.some((e) => e.activity === "accept" && e.entity === d.file && e.agent?.kind !== "person"));
|
|
44
45
|
add("people_decide", byAgent.length || decidedByAgent.length ? 0 : 1, byAgent.length ? `${byAgent.length} accept or confirm entries by an agent` : "every accept, confirm and decision is a person's");
|
|
45
46
|
|
|
46
47
|
// Evidence is cited: each item names a URL, and the URL came from a research result in this run.
|
|
47
|
-
const problem = [join(dir, "purpose/validation/problem.json"), join(dir, "_drafts/purpose__validation__problem.json.draft")].find(existsSync);
|
|
48
|
+
const problem = [join(ventureFile(dir, "purpose/validation/problem.json")), join(ventureFile(dir, "_drafts/purpose__validation__problem.json.draft"))].find(existsSync);
|
|
48
49
|
const items = problem ? load(problem).evidence ?? [] : [];
|
|
49
50
|
const urlOf = (e) => e.url ?? e.source?.url ?? e.source ?? null;
|
|
50
51
|
const cited = items.filter((e) => typeof urlOf(e) === "string" && /^https?:\/\//.test(urlOf(e)));
|
|
@@ -72,7 +73,7 @@ export function evaluate(dir, { researched = [], settled = [] } = {}) {
|
|
|
72
73
|
add("founder_settled", settled.length ? 1 : 0, settled.length ? `settled: ${settled.join(", ")}` : "the founder settled nothing");
|
|
73
74
|
// Money (EE-070): when the venture has a money summary, it is fresh, the agent cites it, and no raw
|
|
74
75
|
// transaction leaves its sensitive stream for another record.
|
|
75
|
-
const summaryFile = join(dir, "money/_summary.json");
|
|
76
|
+
const summaryFile = join(ventureFile(dir, "money/_summary.json"));
|
|
76
77
|
if (existsSync(summaryFile)) {
|
|
77
78
|
const m = load(summaryFile);
|
|
78
79
|
const age = (Date.now() - Date.parse(m.synced_at)) / 864e5;
|