naive-framework 0.4.0 → 0.5.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.
@@ -17,6 +17,9 @@
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 phase [<next phase>] [--meet <criterion> --evidence <text>] [--reason] [--override <why>]
21
+ // the venture development lifecycle: where the venture is, its gate's exit
22
+ // criteria, and moving to the next phase once a person has met them
20
23
  // naive port <folder> --venture <dir> [--to <path>] plan moving a folder into the venture as it is (codebase/<name>)
21
24
  // naive port --venture <dir> --apply --confirm port
22
25
  // carry out that plan (EE-076)
@@ -178,6 +181,12 @@ else if (cmd === "build" || cmd === "start") {
178
181
  if (["list", "match", "check", "eval"].includes(sub)) process.exit(run("schema/frames.mjs", rest));
179
182
  const words = rest.filter((a, i) => a !== "--venture" && rest[i - 1] !== "--venture" && a !== "--frame" && rest[i - 1] !== "--frame" && a !== "--lookup" && rest[i - 1] !== "--lookup").join(" ");
180
183
  process.exit(run("schema/frames.mjs", ["run", here(), ...(opt("frame") ? [opt("frame")] : []), ...(words ? ["--text", words] : []), ...(opt("lookup") ? ["--lookup", opt("lookup")] : [])]));
184
+ } else if (cmd === "phase") {
185
+ // The venture development lifecycle (schema/lifecycle.mjs): where the venture is, its gate, and moving on.
186
+ // naive phase | naive phase --meet <criterion> [--evidence <text>] | naive phase <next> [--reason] [--override]
187
+ const target = rest.find((a, i) => !a.startsWith("--") && !["--venture", "--meet", "--evidence", "--reason", "--override", "--by"].includes(rest[i - 1]));
188
+ const pass = ["evidence", "reason", "override", "by"].flatMap((k) => opt(k) !== undefined ? [`--${k}`, opt(k)] : []);
189
+ process.exit(run("schema/lifecycle.mjs", [here(), ...(opt("meet") ? ["meet", opt("meet")] : target ? ["advance", target] : []), ...pass, ...(rest.includes("--json") ? ["--json"] : [])]));
181
190
  } else if (cmd === "setup") {
182
191
  // Setup in the model's harness (EE-083): the state, the artifact, and the founder confirming relayed answers.
183
192
  process.exit(run("schema/setup.mjs", [here(), ...(rest.length ? rest.filter((a, i) => a !== "--venture" && rest[i - 1] !== "--venture") : ["state"])]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naive-framework",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Naive, a venture development framework: conventions, routes, records, checks and an agent interface for building a venture.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // The venture development lifecycle (VDL): where a venture is, what its phase's gate asks, and whether
3
+ // it may move on. Phases and gates are defined once in purpose/frameworks/product-anatomy/phases.json;
4
+ // each venture states its own exit criteria per phase in its telos. A criterion is met when a person
5
+ // says so, with evidence; a phase change is a person's decision, and the gate refuses it while the
6
+ // current phase has unmet criteria, unless the person overrides it with a reason (logged).
7
+ //
8
+ // node schema/lifecycle.mjs <venture dir> where the venture is, as text (--json)
9
+ // node schema/lifecycle.mjs <venture dir> meet <criterion> [--evidence <text>] [--by <person>]
10
+ // node schema/lifecycle.mjs <venture dir> advance <phase> [--reason <text>] [--override <why>] [--by <person>]
11
+ //
12
+ // advance goes one phase forward; a step back (a venture can return to discovery) needs --reason, and
13
+ // skipping a phase needs --override. The change itself, with its staleness, is schema/freshness.mjs's phase().
14
+
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { agentFromEnv, record } from "../interface/provenance.mjs";
19
+ import { phase as changePhase } from "./freshness.mjs";
20
+ import { ventureFile } from "./venture-paths.mjs";
21
+
22
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
23
+ const load = (f) => JSON.parse(readFileSync(f, "utf8"));
24
+ export const PHASES = load(join(root, "purpose/frameworks/product-anatomy/phases.json")).phases.map(({ id, name, question, gate, binding_constraint }) => ({ id, name, question, gate, binding_constraint }));
25
+ // Where a venture keeps its telos: foundation/ (setup writes it there) or the older purpose/.
26
+ const TELOS = ["foundation/telos.json", "purpose/telos.json"];
27
+ const telosFile = (dir) => TELOS.map((t) => ventureFile(dir, t)).find(existsSync) ?? null;
28
+
29
+ /** Where the venture is in the lifecycle and what stands between it and the next phase. */
30
+ export function lifecycle(dir) {
31
+ const v = load(join(dir, "venture.json"));
32
+ const i = PHASES.findIndex((p) => p.id === v.phase);
33
+ const tf = telosFile(dir);
34
+ const telos = tf ? load(tf) : null;
35
+ const criteria = (telos?.phases?.find((p) => p.phase === v.phase)?.exit_criteria ?? []).map((c) => ({ id: c.id, statement: c.statement, evidence: c.evidence ?? null, met: c.met ?? null }));
36
+ const unmet = criteria.filter((c) => !c.met).map((c) => c.id);
37
+ return {
38
+ venture: v.id, phase: v.phase, step: i + 1, of: PHASES.length,
39
+ current: PHASES[i] ?? null, next: PHASES[i + 1] ?? null,
40
+ telos: tf ? tf.slice(dir.length + 1) : null,
41
+ exit_criteria: criteria,
42
+ ready: criteria.length > 0 && unmet.length === 0,
43
+ blocking: !criteria.length ? [`no exit criteria for ${v.phase} in the telos: state what must be true to leave it`] : unmet.map((id) => `exit criterion ${id} is not met`),
44
+ };
45
+ }
46
+
47
+ /** A person marks one of the current phase's exit criteria met, with the evidence for it. */
48
+ export function meet(dir, id, { evidence, by } = {}) {
49
+ const tf = telosFile(dir);
50
+ if (!tf) return { ok: false, error: "the venture has no telos yet, so no exit criteria to meet" };
51
+ const v = load(join(dir, "venture.json"));
52
+ const t = load(tf);
53
+ const c = t.phases?.find((p) => p.phase === v.phase)?.exit_criteria?.find((x) => x.id === id);
54
+ if (!c) return { ok: false, error: `${v.phase} has no exit criterion ${id}` };
55
+ const agent = by ?? agentFromEnv("person", "cli");
56
+ if (agent.kind !== "person") return { ok: false, error: "only a person can say an exit criterion is met; an agent can propose it" };
57
+ c.met = { at: new Date().toISOString(), by: agent.id ?? "person", ...(evidence ? { evidence } : {}) };
58
+ writeFileSync(tf, JSON.stringify(t, null, 2) + "\n");
59
+ record(dir, { entity: tf.slice(dir.length + 1), route: "foundation/telos", activity: "confirm", agent, note: `exit criterion ${v.phase}/${id} met${evidence ? `: ${evidence}` : ""}` });
60
+ return { ok: true, result: lifecycle(dir) };
61
+ }
62
+
63
+ /** A person moves the venture to another phase, through the gate. */
64
+ export function advance(dir, to, { reason, override, by, now } = {}) {
65
+ const agent = by ?? agentFromEnv("person", "cli");
66
+ if (agent.kind !== "person") return { ok: false, error: "a phase change is a person's decision; an agent can suggest it, never make it" };
67
+ const l = lifecycle(dir);
68
+ const from = PHASES.findIndex((p) => p.id === l.phase), target = PHASES.findIndex((p) => p.id === to);
69
+ if (target < 0) return { ok: false, error: `${to} is not a phase (${PHASES.map((p) => p.id).join(", ")})` };
70
+ if (target === from) return { ok: false, error: `${l.venture} is already in ${to}` };
71
+ if (target < from && !reason) return { ok: false, error: `going back to ${to} needs --reason` };
72
+ if (target > from + 1 && !override) return { ok: false, error: `${to} skips ${PHASES.slice(from + 1, target).map((p) => p.id).join(", ")}; pass --override "<why>" to skip` };
73
+ if (target > from && !l.ready && !override) return { ok: false, error: `the ${l.current.gate} gate is not passed: ${l.blocking.join("; ")}. Meet them with naive phase --meet <id>, or pass --override "<why>"`, result: l };
74
+ const why = [reason, override && `gate overridden: ${override}`].filter(Boolean).join("; ");
75
+ const r = changePhase(dir, to, { reason: why || `${l.current.gate} passed`, by: agent, now });
76
+ if (r.ok) r.result.gate = { phase: l.phase, gate: l.current.gate, passed: l.ready, overridden: override ?? null };
77
+ return r;
78
+ }
79
+
80
+ const text = (l) => [
81
+ `${l.venture}: ${l.current?.name ?? l.phase} (phase ${l.step} of ${l.of})`,
82
+ l.current ? ` ${l.current.question}` : "",
83
+ l.current ? ` Gate: ${l.current.gate}` : "",
84
+ ...(l.exit_criteria.length ? l.exit_criteria.map((c) => ` ${c.met ? "✓" : "○"} ${c.id}: ${c.statement}${c.met ? ` (met ${c.met.at.slice(0, 10)} by ${c.met.by})` : ""}`) : [` ○ ${l.blocking[0]}`]),
85
+ l.next ? `Next: ${l.next.name}, ${l.next.question}${l.ready ? " Ready: naive phase " + l.next.id : ""}` : "This is the last phase.",
86
+ ].filter(Boolean).join("\n");
87
+
88
+ if (import.meta.url === `file://${process.argv[1]}`) {
89
+ const [dir, cmd, arg, ...rest] = process.argv.slice(2);
90
+ const all = process.argv.slice(2);
91
+ const opt = (k) => { const i = all.indexOf(`--${k}`); return i < 0 ? undefined : all[i + 1]; };
92
+ if (!dir || !existsSync(join(dir, "venture.json"))) { console.error("usage: node schema/lifecycle.mjs <venture dir> [meet <criterion> | advance <phase>]"); process.exit(2); }
93
+ const by = opt("by") ? agentFromEnv("person", "cli", { id: opt("by") }) : undefined;
94
+ const out = (r) => { if (!r.ok) { console.error(r.error); process.exit(1); } return r.result; };
95
+ if (!cmd || cmd.startsWith("--")) { const l = lifecycle(dir); console.log(all.includes("--json") ? JSON.stringify(l, null, 2) : text(l)); }
96
+ else if (cmd === "meet") console.log(text(out(meet(dir, arg, { evidence: opt("evidence"), by }))));
97
+ else if (cmd === "advance") { const r = out(advance(dir, arg, { reason: opt("reason"), override: opt("override"), by })); console.log(`${r.venture}: ${r.from} -> ${r.to}. ${r.note}`); }
98
+ else { console.error(`unknown command ${cmd}`); process.exit(2); }
99
+ void rest;
100
+ }
@@ -141,6 +141,29 @@
141
141
  "evidence": {
142
142
  "type": "string",
143
143
  "description": "How it is shown: a check command, an evidence record, or a metric"
144
+ },
145
+ "met": {
146
+ "type": "object",
147
+ "required": [
148
+ "at",
149
+ "by"
150
+ ],
151
+ "additionalProperties": false,
152
+ "description": "Set when a person says the criterion is met (naive phase --meet); the gate to the next phase passes when every criterion of the current phase has it",
153
+ "properties": {
154
+ "at": {
155
+ "type": "string",
156
+ "format": "date-time"
157
+ },
158
+ "by": {
159
+ "type": "string",
160
+ "description": "The person who said it is met"
161
+ },
162
+ "evidence": {
163
+ "type": "string",
164
+ "description": "What shows it: a record, a metric, a check"
165
+ }
166
+ }
144
167
  }
145
168
  }
146
169
  }