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.
- package/interface/cli/naive.mjs +9 -0
- package/package.json +1 -1
- package/schema/lifecycle.mjs +100 -0
- package/schema/telos.schema.json +23 -0
package/interface/cli/naive.mjs
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
package/schema/telos.schema.json
CHANGED
|
@@ -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
|
}
|