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/schema/port-venture.mjs
CHANGED
|
@@ -1,161 +1,235 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// naive port:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// project into the Naive convention) is the other way in
|
|
2
|
+
// naive port: moves an existing folder into a venture, as it is (EE-076, revised by Torian on
|
|
3
|
+
// 2026-10-29: port moves, it no longer copies files into packages). The folder is moved whole, with a
|
|
4
|
+
// rename, so not one file in it is opened or changed; the venture names it in sources.json as a source
|
|
5
|
+
// of truth (a read-only filesystem source) and logs the move in provenance.jsonl with the path it came
|
|
6
|
+
// from. Migrate (rewriting a project into the Naive convention) is the other way in.
|
|
7
7
|
//
|
|
8
|
-
// node schema/port-venture.mjs --
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// node schema/port-venture.mjs <folder> --venture <dir> [--to <path>] [--links <dir> ...]
|
|
9
|
+
// adds the move to the plan at <venture>/.release/port-plan.json and prints it; moves nothing.
|
|
10
|
+
// The folder goes to <venture>/codebase/<its name> unless --to says where (relative to the venture
|
|
11
|
+
// or absolute). Run once per folder: each run adds or replaces that folder's item.
|
|
12
|
+
// node schema/port-venture.mjs --venture <dir> --apply --confirm port [--even-if-busy]
|
|
13
|
+
// carries out every planned move the founder left as move; any item can be set to leave first
|
|
13
14
|
//
|
|
14
|
-
// The
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// The plan says what a move would break: worktrees of a git repository (they point at its old path
|
|
16
|
+
// until `git worktree repair` runs from the new one), symbolic links that point into the folder, and
|
|
17
|
+
// processes working inside it, which refuse the apply unless --even-if-busy. A port never overwrites,
|
|
18
|
+
// never copies (a move across disks is refused, not copied), and refuses an item whose folder changed
|
|
19
|
+
// identity since the plan. Every run writes a trace to .release/port-trace.json, in the shape
|
|
20
|
+
// platform/integrations/langsmith.mjs uploads.
|
|
17
21
|
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
18
23
|
import { createHash } from "node:crypto";
|
|
19
|
-
import {
|
|
20
|
-
import { basename, dirname,
|
|
21
|
-
import { record } from "../interface/provenance.mjs";
|
|
22
|
-
import {
|
|
24
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
26
|
+
import { agentFromEnv, record } from "../interface/provenance.mjs";
|
|
27
|
+
import { CODEBASE, ventureFile } from "./venture-paths.mjs";
|
|
23
28
|
|
|
24
29
|
export const PLAN = ".release/port-plan.json";
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
30
|
+
export const TRACE = ".release/port-trace.json";
|
|
31
|
+
const MAP = "knowledge/sources.json";
|
|
32
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+|-$/g, "") || "folder";
|
|
33
|
+
const inside = (a, b) => a === b || a.startsWith(b + sep);
|
|
34
|
+
const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
29
35
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
// Files and bytes under a folder, without following links, to show the size of what moves, and a
|
|
37
|
+
// manifest hash (each path with its size, never its contents) for the provenance entry.
|
|
38
|
+
function measure(root) {
|
|
39
|
+
let files = 0, bytes = 0;
|
|
40
|
+
const lines = [];
|
|
41
|
+
const stack = [root];
|
|
42
|
+
while (stack.length) {
|
|
43
|
+
const d = stack.pop();
|
|
44
|
+
let list = [];
|
|
45
|
+
try { list = readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
46
|
+
for (const e of list) {
|
|
47
|
+
const p = join(d, e.name);
|
|
48
|
+
if (e.isDirectory()) stack.push(p);
|
|
49
|
+
else { files++; let n = 0; try { n = lstatSync(p).size; } catch {} bytes += n; lines.push(`${relative(root, p)} ${n}`); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { files, bytes, manifest: createHash("sha256").update(lines.sort().join("\n")).digest("hex") };
|
|
53
|
+
}
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
// Symbolic links under `roots` (to `depth` levels, skipping node_modules) whose target is inside `src`.
|
|
56
|
+
// A link inside `src` that points inside it with a relative path keeps working, so only absolute ones count.
|
|
57
|
+
export function linksInto(src, roots, depth = 4) {
|
|
58
|
+
const out = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const visit = (d, n) => {
|
|
61
|
+
if (n > depth || seen.has(d)) return;
|
|
62
|
+
seen.add(d);
|
|
63
|
+
let list = [];
|
|
64
|
+
try { list = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
65
|
+
for (const e of list) {
|
|
66
|
+
if (e.name === "node_modules" || e.name === ".git") continue;
|
|
67
|
+
const p = join(d, e.name);
|
|
68
|
+
if (e.isSymbolicLink()) {
|
|
69
|
+
const t = readlinkSync(p);
|
|
70
|
+
const target = resolve(dirname(p), t);
|
|
71
|
+
if (inside(target, src) && !(inside(p, src) && !isAbsolute(t))) out.push({ link: p, target: t });
|
|
72
|
+
} else if (e.isDirectory()) visit(p, n + 1);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (const r of roots) visit(resolve(r), 0);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
51
78
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
if (
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (hit) return { action: "port", to: `${hit[1]}/${basename(rel)}`, function: hit[1].split("/").at(-1), confidence, reason: `${confidence === "high" ? "its name" : `its folder ${where}`} ${hit[2]}` };
|
|
63
|
-
}
|
|
64
|
-
if (/^readme$/i.test(name)) return { action: "leave", reason: "the venture has its own AGENTS.md and profile.md; read this to write profile.md" };
|
|
65
|
-
return { action: "leave", reason: "nothing in its name or folders matches a package; place it by hand or migrate it" };
|
|
79
|
+
// The git facts a move touches: whether the folder is a repository, its other worktrees, and how
|
|
80
|
+
// many changes are uncommitted (the move keeps them; it is only said).
|
|
81
|
+
function git(src) {
|
|
82
|
+
const g = join(src, ".git");
|
|
83
|
+
if (!existsSync(g)) return null;
|
|
84
|
+
if (statSync(g).isFile()) return { linked: true, worktrees: [], uncommitted: null };
|
|
85
|
+
const run = (...a) => spawnSync("git", ["-C", src, ...a], { encoding: "utf8" });
|
|
86
|
+
const wt = (run("worktree", "list", "--porcelain").stdout ?? "").split("\n").filter((l) => l.startsWith("worktree ")).map((l) => l.slice(9)).filter((p) => real(p) !== real(src));
|
|
87
|
+
const st = run("status", "--porcelain");
|
|
88
|
+
return { linked: false, worktrees: wt, uncommitted: st.status === 0 ? st.stdout.split("\n").filter(Boolean).length : null };
|
|
66
89
|
}
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
else if (
|
|
91
|
+
// Processes whose working directory is inside the folder (macOS and Linux lsof).
|
|
92
|
+
export function busy(src) {
|
|
93
|
+
const r = spawnSync("lsof", ["-d", "cwd", "-Fpcn"], { encoding: "utf8" });
|
|
94
|
+
if (r.error || !r.stdout) return [];
|
|
95
|
+
const out = [];
|
|
96
|
+
let p = null, c = null;
|
|
97
|
+
for (const l of r.stdout.split("\n")) {
|
|
98
|
+
if (l[0] === "p") p = l.slice(1);
|
|
99
|
+
else if (l[0] === "c") c = l.slice(1);
|
|
100
|
+
else if (l[0] === "n" && inside(l.slice(1), real(src)) && p !== String(process.pid)) out.push({ pid: Number(p), command: c, cwd: l.slice(1) });
|
|
77
101
|
}
|
|
78
102
|
return out;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
const readPlan = (dir) => existsSync(ventureFile(dir, PLAN)) ? JSON.parse(readFileSync(ventureFile(dir, PLAN), "utf8")) : null;
|
|
106
|
+
const readMap = (dir) => existsSync(ventureFile(dir, MAP)) ? JSON.parse(readFileSync(ventureFile(dir, MAP), "utf8")) : { sources: [], records: {} };
|
|
107
|
+
|
|
108
|
+
/** The planned move of `folder` into the venture at `dir`. Reads both, writes neither. */
|
|
109
|
+
export function planOne(folder, dir, { to, links = [] } = {}) {
|
|
110
|
+
if (!existsSync(folder) || !lstatSync(folder).isDirectory()) throw new Error(`${folder} is not a directory`);
|
|
85
111
|
if (!existsSync(join(dir, "venture.json"))) throw new Error(`${dir} is not a venture: create one first with naive init`);
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const src = real(folder);
|
|
113
|
+
const v = real(dir);
|
|
114
|
+
if (inside(v, src)) throw new Error("the venture cannot sit inside the folder it ports");
|
|
115
|
+
const dest = resolve(v, to ?? join(CODEBASE, basename(src)));
|
|
116
|
+
const s = statSync(src);
|
|
117
|
+
const item = { action: "move", from: src, to: dest, source: slug(basename(src)), identity: { dev: s.dev, ino: s.ino }, ...measure(src) };
|
|
118
|
+
const warnings = [];
|
|
119
|
+
const leave = (why) => Object.assign(item, { action: "leave", reason: why });
|
|
120
|
+
if (existsSync(dest)) leave(`${dest} already exists; a port never overwrites`);
|
|
121
|
+
else if (inside(dest, src)) leave("the destination is inside the folder itself");
|
|
122
|
+
else if (!inside(dest, v)) warnings.push("the destination is outside the venture folder; sources.json will name it by absolute path");
|
|
123
|
+
// A rename only works on one disk: the nearest existing parent of the destination must share it.
|
|
124
|
+
let up = dirname(dest);
|
|
125
|
+
while (!existsSync(up)) up = dirname(up);
|
|
126
|
+
if (item.action === "move" && statSync(up).dev !== s.dev) leave("the destination is on another disk; a port moves, it never copies");
|
|
127
|
+
if (item.action === "move" && readMap(dir).sources.some((x) => x.id === item.source)) leave(`sources.json already has a source ${item.source}`);
|
|
128
|
+
const g = git(src);
|
|
129
|
+
item.git = g;
|
|
130
|
+
if (g?.linked) warnings.push("this folder is a linked git worktree; its main repository will point at the old path until `git worktree repair` runs there");
|
|
131
|
+
if (g?.worktrees.length) warnings.push(`${g.worktrees.length} worktree${g.worktrees.length === 1 ? "" : "s"} of this repository point at its old path until \`git -C ${dest} worktree repair\` runs: ${g.worktrees.join(", ")}`);
|
|
132
|
+
if (g?.uncommitted) warnings.push(`${g.uncommitted} uncommitted change${g.uncommitted === 1 ? "" : "s"} move with it, unchanged`);
|
|
133
|
+
item.links = linksInto(src, [dirname(src), src, ...links]);
|
|
134
|
+
if (item.links.length) warnings.push(`${item.links.length} symbolic link${item.links.length === 1 ? "" : "s"} point into it and will break: ${item.links.map((x) => x.link).join(", ")}`);
|
|
135
|
+
item.busy = busy(src);
|
|
136
|
+
if (item.busy.length) warnings.push(`${item.busy.length} process${item.busy.length === 1 ? " is" : "es are"} working inside it (${[...new Set(item.busy.map((x) => x.command))].join(", ")}); apply refuses unless --even-if-busy`);
|
|
137
|
+
item.warnings = warnings;
|
|
138
|
+
return item;
|
|
107
139
|
}
|
|
108
140
|
|
|
109
|
-
/**
|
|
110
|
-
export function
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
/** Adds (or replaces) one folder's item in the venture's plan and writes it. */
|
|
142
|
+
export function plan(folder, dir, opts = {}) {
|
|
143
|
+
const item = planOne(folder, dir, opts);
|
|
144
|
+
const p = readPlan(dir) ?? { venture: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).id, items: [] };
|
|
145
|
+
const others = p.items.filter((i) => i.from !== item.from && i.action === "move" && !i.done);
|
|
146
|
+
if (others.some((i) => i.source === item.source)) Object.assign(item, { action: "leave", reason: `another planned move already uses the source id ${item.source}` });
|
|
147
|
+
if (others.some((i) => i.to === item.to)) Object.assign(item, { action: "leave", reason: `another planned move already goes to ${item.to}` });
|
|
148
|
+
p.items = [...p.items.filter((i) => i.from !== item.from || i.done), item];
|
|
149
|
+
p.at = new Date().toISOString();
|
|
150
|
+
mkdirSync(dirname(ventureFile(dir, PLAN)), { recursive: true });
|
|
151
|
+
writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
|
|
152
|
+
return { plan: p, item };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Carries out the plan as the founder left it. Checks every item before moving any. */
|
|
156
|
+
export function apply(dir, p, { agent = agentFromEnv("person", "cli"), evenIfBusy = false } = {}) {
|
|
157
|
+
const moves = p.items.filter((i) => i.action === "move" && !i.done);
|
|
158
|
+
const ids = new Set(readMap(dir).sources.map((s) => s.id));
|
|
159
|
+
for (const i of moves) {
|
|
160
|
+
if (!existsSync(i.from)) throw new Error(`${i.from} is gone; write the plan again`);
|
|
161
|
+
const s = statSync(i.from);
|
|
162
|
+
if (s.dev !== i.identity.dev || s.ino !== i.identity.ino) throw new Error(`${i.from} is not the folder the plan was written for; write the plan again`);
|
|
163
|
+
if (existsSync(i.to)) throw new Error(`${i.to} already exists; a port never overwrites`);
|
|
164
|
+
if (ids.has(i.source)) throw new Error(`sources.json already has a source ${i.source}`);
|
|
165
|
+
const b = busy(i.from);
|
|
166
|
+
if (b.length && !evenIfBusy) throw new Error(`${b.length} process${b.length === 1 ? " is" : "es are"} working inside ${i.from} (${b.map((x) => `${x.command} ${x.pid}`).join(", ")}); stop them or add --even-if-busy`);
|
|
115
167
|
}
|
|
116
|
-
const done =
|
|
117
|
-
for (const i of
|
|
118
|
-
mkdirSync(dirname(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
map.sources.push({ id: i.source, connector: "filesystem", writes: "none", root: join(src, i.from === "." ? "" : i.from), note: `ported as a pointer: ${i.reason}` });
|
|
130
|
-
done.pointed.push(i.source);
|
|
131
|
-
}
|
|
132
|
-
mkdirSync(dirname(f), { recursive: true });
|
|
133
|
-
writeFileSync(f, JSON.stringify(map, null, 2) + "\n");
|
|
134
|
-
if (done.pointed.length) record(dir, { entity: "knowledge/sources.json", activity: "port", agent, note: `pointers to ${done.pointed.join(", ")}` });
|
|
168
|
+
const done = [];
|
|
169
|
+
for (const i of moves) {
|
|
170
|
+
mkdirSync(dirname(i.to), { recursive: true });
|
|
171
|
+
renameSync(i.from, i.to);
|
|
172
|
+
const map = readMap(dir);
|
|
173
|
+
const root = inside(i.to, real(dir)) ? relative(real(dir), i.to) : i.to;
|
|
174
|
+
map.sources.push({ id: i.source, connector: "filesystem", writes: "none", root, note: `source of truth, moved here unchanged from ${i.from} by naive port` });
|
|
175
|
+
mkdirSync(dirname(ventureFile(dir, MAP)), { recursive: true });
|
|
176
|
+
writeFileSync(ventureFile(dir, MAP), JSON.stringify(map, null, 2) + "\n");
|
|
177
|
+
record(dir, { entity: root, activity: "port", agent, derived_from: [`file://${i.from}`], sha256: measure(i.to).manifest, note: `moved unchanged (${i.files} files); named in sources.json as source ${i.source}` });
|
|
178
|
+
i.done = new Date().toISOString();
|
|
179
|
+
done.push(i);
|
|
180
|
+
writeFileSync(ventureFile(dir, PLAN), JSON.stringify(p, null, 2) + "\n");
|
|
135
181
|
}
|
|
136
182
|
return done;
|
|
137
183
|
}
|
|
138
184
|
|
|
185
|
+
// One run as a trace for platform/integrations/langsmith.mjs: the run, and a child per item.
|
|
186
|
+
function trace(dir, name, inputs, start, items, error) {
|
|
187
|
+
const end = new Date().toISOString();
|
|
188
|
+
const t = { name, run_type: "chain", start, end, inputs, outputs: { items: items.map(({ from, to, action, done }) => ({ from, to, action, done })) }, ...(error ? { error } : {}),
|
|
189
|
+
tags: ["naive", "port"], children: items.map((i) => ({ name: `${i.action} ${basename(i.from)}`, run_type: "tool", start, end, inputs: { from: i.from, to: i.to },
|
|
190
|
+
outputs: { action: i.action, reason: i.reason, files: i.files, bytes: i.bytes, warnings: i.warnings, done: i.done } })) };
|
|
191
|
+
try { mkdirSync(dirname(ventureFile(dir, TRACE)), { recursive: true }); writeFileSync(ventureFile(dir, TRACE), JSON.stringify(t, null, 2) + "\n"); } catch { return; }
|
|
192
|
+
// The harness's way (interface/harness/run.mjs): with a key set, the platform exporter ships the trace.
|
|
193
|
+
if (process.env.LANGSMITH_API_KEY) {
|
|
194
|
+
const p = spawnSync("node", [join(dirname(new URL(import.meta.url).pathname), "../platform/integrations/langsmith.mjs"), ventureFile(dir, TRACE), "--project", process.env.LANGSMITH_PROJECT ?? "naive-port"], { encoding: "utf8" });
|
|
195
|
+
console.log(p.status === 0 ? `LangSmith: ${p.stdout.trim()}` : `LangSmith upload failed: ${(p.stderr || p.stdout).trim()}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const mb = (b) => `${(b / 1024 / 1024).toFixed(1)} MB`;
|
|
200
|
+
|
|
139
201
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
140
202
|
const args = process.argv.slice(2);
|
|
141
203
|
const opt = (k) => { const i = args.indexOf(`--${k}`); return i < 0 ? undefined : args[i + 1]; };
|
|
204
|
+
const all = (k) => args.flatMap((a, i) => a === `--${k}` ? [args[i + 1]] : []);
|
|
205
|
+
const VALUED = new Set(["--venture", "--to", "--links", "--confirm", "--from"]);
|
|
206
|
+
const folder = opt("from") ?? args.find((a, i) => !a.startsWith("--") && !VALUED.has(args[i - 1]));
|
|
142
207
|
const dir = opt("venture");
|
|
143
|
-
if (!dir) { console.error("usage: node schema/port-venture.mjs
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
208
|
+
if (!dir) { console.error("usage: node schema/port-venture.mjs <folder> --venture <dir> [--to <path>] | --venture <dir> --apply --confirm port"); process.exit(2); }
|
|
209
|
+
const start = new Date().toISOString();
|
|
210
|
+
if (args.includes("--apply")) {
|
|
211
|
+
if (opt("confirm") !== "port") { console.error("type --confirm port to carry out the plan"); process.exit(1); }
|
|
212
|
+
const p = readPlan(dir);
|
|
213
|
+
if (!p) { console.error(`no plan at ${ventureFile(dir, PLAN)}: run naive port <folder> first`); process.exit(1); }
|
|
214
|
+
try {
|
|
215
|
+
const d = apply(dir, p, { evenIfBusy: args.includes("--even-if-busy") });
|
|
216
|
+
trace(dir, "naive port apply", { venture: dir }, start, d);
|
|
217
|
+
for (const i of d) console.log(`moved ${i.from} -> ${i.to} (${i.files} files, unchanged; source ${i.source})`);
|
|
218
|
+
for (const i of d.filter((x) => x.git?.worktrees.length)) console.log(`\nrepair its worktrees: git -C ${i.to} worktree repair`);
|
|
219
|
+
console.log(`\nmoved ${d.length} folder${d.length === 1 ? "" : "s"}. Trace: ${relative(process.cwd(), ventureFile(dir, TRACE))}`);
|
|
220
|
+
} catch (e) { trace(dir, "naive port apply", { venture: dir }, start, p.items, e.message); console.error(e.message); process.exit(1); }
|
|
221
|
+
} else {
|
|
222
|
+
if (!folder) { console.error("name the folder to port: naive port <folder>"); process.exit(2); }
|
|
223
|
+
try {
|
|
224
|
+
const { plan: p, item } = plan(folder, dir, { to: opt("to"), links: all("links") });
|
|
225
|
+
trace(dir, "naive port plan", { folder, venture: dir, to: opt("to") }, start, [item]);
|
|
226
|
+
for (const i of p.items) {
|
|
227
|
+
console.log(`${(i.done ? "moved" : i.action).padEnd(5)} ${i.from} -> ${i.to} (${i.files} files, ${mb(i.bytes)}${i.reason ? `; ${i.reason}` : ""})`);
|
|
228
|
+
if (!i.done) for (const w of i.warnings ?? []) console.log(` ! ${w}`);
|
|
229
|
+
}
|
|
230
|
+
const n = p.items.filter((i) => i.action === "move" && !i.done).length;
|
|
231
|
+
console.log(`\n${n} folder${n === 1 ? "" : "s"} to move, unchanged. Nothing has moved yet.`);
|
|
232
|
+
console.log(`Read ${relative(process.cwd(), ventureFile(dir, PLAN))}, set any item's action to leave, then run with --apply --confirm port.`);
|
|
233
|
+
} catch (e) { trace(dir, "naive port plan", { folder, venture: dir }, start, [], e.message); console.error(e.message); process.exit(1); }
|
|
234
|
+
}
|
|
161
235
|
}
|
package/schema/release-read.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
18
18
|
import { join, normalize } from "node:path";
|
|
19
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
19
20
|
|
|
20
21
|
export const RELEASE_DIR = ".release";
|
|
21
22
|
export const MANIFEST = ".release/release.json";
|
|
@@ -24,7 +25,7 @@ export const RECORDS = ".release/records";
|
|
|
24
25
|
|
|
25
26
|
/** The release manifest of a venture, or null when it has none. */
|
|
26
27
|
export function readRelease(dir) {
|
|
27
|
-
const f =
|
|
28
|
+
const f = ventureFile(dir, MANIFEST);
|
|
28
29
|
return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -32,7 +33,7 @@ export function readRelease(dir) {
|
|
|
32
33
|
export function frozenText(dir, file) {
|
|
33
34
|
const rel = normalize(file);
|
|
34
35
|
if (rel.startsWith("..") || rel.startsWith("/")) return null;
|
|
35
|
-
const f = join(dir, RECORDS, rel);
|
|
36
|
+
const f = join(ventureFile(dir, RECORDS), rel);
|
|
36
37
|
return existsSync(f) ? readFileSync(f, "utf8") : null;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -94,10 +95,10 @@ export function answer(dir, method, path, { draw } = {}) {
|
|
|
94
95
|
if (p.split("/").includes("..")) return json(400, { error: "no .. in a release path" });
|
|
95
96
|
if (p === "/" || p === "") return json(200, index(m));
|
|
96
97
|
if (p === "/release.json") return json(200, m);
|
|
97
|
-
if (p === "/card.json") return existsSync(
|
|
98
|
+
if (p === "/card.json") return existsSync(ventureFile(dir, CARD)) ? json(200, JSON.parse(readFileSync(ventureFile(dir, CARD), "utf8"))) : json(404, { error: "this release has no card" });
|
|
98
99
|
if (p === "/card.svg") {
|
|
99
100
|
if (!draw) return json(404, { error: "no card drawing here" });
|
|
100
|
-
return { status: 200, type: "image/svg+xml; charset=utf-8", body: draw(JSON.parse(readFileSync(
|
|
101
|
+
return { status: 200, type: "image/svg+xml; charset=utf-8", body: draw(JSON.parse(readFileSync(ventureFile(dir, CARD), "utf8"))) };
|
|
101
102
|
}
|
|
102
103
|
if (p.startsWith("/records/")) {
|
|
103
104
|
const file = p.slice("/records/".length);
|
package/schema/releases.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import { attributeSources, connect } from "../knowledge/connectors/index.mjs";
|
|
|
43
43
|
import { FRAME, confirmedIn } from "./foundation.mjs";
|
|
44
44
|
import { HOME, packages } from "./packages.mjs";
|
|
45
45
|
import { CARD, MANIFEST, RECORDS, RELEASE_DIR, frozenText, readRelease } from "./release-read.mjs";
|
|
46
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
46
47
|
|
|
47
48
|
const repo = new URL("..", import.meta.url).pathname;
|
|
48
49
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -68,7 +69,7 @@ export function key() {
|
|
|
68
69
|
|
|
69
70
|
/** The local cache for a key: { entries, hits, misses, fresh, get(k, f), save() }. A different key starts it fresh. */
|
|
70
71
|
export function openCache(dir, k = key()) {
|
|
71
|
-
const f =
|
|
72
|
+
const f = ventureFile(dir, CACHE);
|
|
72
73
|
const kh = sha(JSON.stringify(k)).slice(0, 16);
|
|
73
74
|
let old = null;
|
|
74
75
|
try { old = existsSync(f) ? load(f) : null; } catch { old = null; }
|
|
@@ -87,7 +88,7 @@ export function openCache(dir, k = key()) {
|
|
|
87
88
|
|
|
88
89
|
/** Every file of a venture, relative: skips dot folders, nested ventures and _-folders other than _streams. */
|
|
89
90
|
export function ventureFiles(dir, sub = "") {
|
|
90
|
-
return readdirSync(
|
|
91
|
+
return readdirSync(ventureFile(dir, sub), { withFileTypes: true }).flatMap((e) => {
|
|
91
92
|
const rel = sub ? `${sub}/${e.name}` : e.name;
|
|
92
93
|
if (e.name.startsWith(".") || e.name === "node_modules") return [];
|
|
93
94
|
if (e.isDirectory()) return existsSync(join(dir, rel, "venture.json")) || (e.name.startsWith("_") && e.name !== "_streams") ? [] : ventureFiles(dir, rel);
|
|
@@ -121,7 +122,7 @@ export function confirmation(dir, file, type, log) {
|
|
|
121
122
|
const es = log.filter((e) => e.entity === file);
|
|
122
123
|
if (!es.length) return { ok: false, why: "no provenance entry: written outside the router", fix: "baseline" };
|
|
123
124
|
const last = es.at(-1);
|
|
124
|
-
const now = sha(readFileSync(
|
|
125
|
+
const now = sha(readFileSync(ventureFile(dir, file)));
|
|
125
126
|
if (last.sha256 && last.sha256 !== now) return { ok: false, why: "changed since its last provenance entry", fix: "rewrite" };
|
|
126
127
|
const statements = FRAME.questions.flatMap((q) => q.statements).filter((s) => s.record === type && s.id);
|
|
127
128
|
const ok = confirmedIn(dir, file, null) || (statements.length > 0 && statements.every((s) => confirmedIn(dir, file, s.id)));
|
|
@@ -195,7 +196,7 @@ export async function survey(dir, { cache } = {}) {
|
|
|
195
196
|
const stream = file.match(/^(.*\/_streams\/[^/]+)\//);
|
|
196
197
|
if (stream) { if (!streams.has(stream[1])) streams.set(stream[1], file); continue; }
|
|
197
198
|
if (file.endsWith(".decision.json") && file.split("/").at(-2) === "decisions") {
|
|
198
|
-
const d = load(
|
|
199
|
+
const d = load(ventureFile(dir, file));
|
|
199
200
|
const ro = routeOf(file, ms);
|
|
200
201
|
if (d.status === "decided") frozen.push({ file, record: "decision-record", route: ro?.route ?? `decisions/${d.id}`, route_template: ro?.template ?? "decisions/[id]", ...(ro?.id ? { id: ro.id } : {}),
|
|
201
202
|
as_of: d.provenance?.approved_at ?? null, confirmed_by: d.provenance?.approved_by ?? d.decided_by ?? "a person" });
|
|
@@ -209,7 +210,7 @@ export async function survey(dir, { cache } = {}) {
|
|
|
209
210
|
if (!type) { leftOut.push({ file, why: "not a record: notes and readmes never ship" }); continue; }
|
|
210
211
|
if (NOT_SHIPPED[type]) { leftOut.push({ file, why: NOT_SHIPPED[type] }); continue; }
|
|
211
212
|
if (LIVE[type]) { holes.push({ kind: type === "money-account" ? "money" : "stream", name: ro.id ?? file, file, record: type, route: ro.route, route_template: ro.template, why: LIVE[type], live: `${CLI} ${ro.route} read --venture ${v.id}`, to_freeze: "stays live by design: confirm a dated summary record to freeze a number" }); continue; }
|
|
212
|
-
const text = readFileSync(
|
|
213
|
+
const text = readFileSync(ventureFile(dir, file), "utf8");
|
|
213
214
|
let parsed = null;
|
|
214
215
|
if (file.endsWith(".json")) { try { parsed = JSON.parse(text); } catch { blockers.push({ file, route: ro.route, record: type, why: "not valid JSON", fix: `fix ${file}` }); continue; } }
|
|
215
216
|
if (parsed?.private === true) { privateCount.records++; continue; }
|
|
@@ -251,9 +252,9 @@ export async function survey(dir, { cache } = {}) {
|
|
|
251
252
|
const id = d.split("/").at(-1);
|
|
252
253
|
holes.push({ kind: "stream", name: d, file: first, record: "data-stream", stream: id, route: d, route_template: null, why: "a stream is append-only activity; it is never frozen", live: `read ${d}/ in the venture`, to_freeze: "stays live by design" });
|
|
253
254
|
}
|
|
254
|
-
if (existsSync(join(dir, "_private"))) privateCount.folder = ventureFilesAll(join(dir, "_private")).length;
|
|
255
|
+
if (existsSync(join(ventureFile(dir, "_private")))) privateCount.folder = ventureFilesAll(join(ventureFile(dir, "_private"))).length;
|
|
255
256
|
// Drafts (P-31): never shipped. One with no confirmed record behind it blocks its package.
|
|
256
|
-
const drafts = existsSync(join(dir, "_drafts")) ? readdirSync(join(dir, "_drafts")).filter((f) => f.endsWith(".draft")) : [];
|
|
257
|
+
const drafts = existsSync(join(ventureFile(dir, "_drafts"))) ? readdirSync(join(ventureFile(dir, "_drafts"))).filter((f) => f.endsWith(".draft")) : [];
|
|
257
258
|
for (const d of drafts) {
|
|
258
259
|
const file = d.replace(/\.draft$/, "").replace(/__/g, "/");
|
|
259
260
|
const ro = routeOf(file, ms);
|
|
@@ -293,7 +294,7 @@ async function frozenCommands(dir, v, frozen, plan, cache, log) {
|
|
|
293
294
|
try {
|
|
294
295
|
mkdirSync(shadow, { recursive: true });
|
|
295
296
|
writeFileSync(join(shadow, "venture.json"), JSON.stringify(v, null, 2) + "\n");
|
|
296
|
-
for (const f of frozen) { mkdirSync(dirname(join(shadow, f.file)), { recursive: true }); writeFileSync(join(shadow, f.file), f.text ?? readFileSync(
|
|
297
|
+
for (const f of frozen) { mkdirSync(dirname(join(shadow, f.file)), { recursive: true }); writeFileSync(join(shadow, f.file), f.text ?? readFileSync(ventureFile(dir, f.file), "utf8")); }
|
|
297
298
|
// The log, so confirmations hold in the copy; the release's own entries are left out of the key.
|
|
298
299
|
const kept = log.filter((e) => !(e.activity === "release" && e.entity === MANIFEST));
|
|
299
300
|
writeFileSync(join(shadow, "provenance.jsonl"), log.map((e) => JSON.stringify(e)).join("\n") + (log.length ? "\n" : ""));
|
|
@@ -369,19 +370,19 @@ function checks(dir) {
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
function writeRelease(dir, m, frozen) {
|
|
372
|
-
const stage = join(dir, RELEASE_DIR, `.stage-${process.pid}`);
|
|
373
|
+
const stage = join(ventureFile(dir, RELEASE_DIR), `.stage-${process.pid}`);
|
|
373
374
|
rmSync(stage, { recursive: true, force: true });
|
|
374
375
|
for (const f of frozen) {
|
|
375
376
|
const dest = join(stage, "records", f.file);
|
|
376
377
|
mkdirSync(dirname(dest), { recursive: true });
|
|
377
|
-
writeFileSync(dest, f.text ?? readFileSync(
|
|
378
|
+
writeFileSync(dest, f.text ?? readFileSync(ventureFile(dir, f.file), "utf8"));
|
|
378
379
|
}
|
|
379
380
|
mkdirSync(join(stage, "records"), { recursive: true });
|
|
380
|
-
rmSync(
|
|
381
|
-
renameSync(join(stage, "records"),
|
|
381
|
+
rmSync(ventureFile(dir, RECORDS), { recursive: true, force: true });
|
|
382
|
+
renameSync(join(stage, "records"), ventureFile(dir, RECORDS));
|
|
382
383
|
rmSync(stage, { recursive: true, force: true });
|
|
383
|
-
writeFileSync(
|
|
384
|
-
writeFileSync(
|
|
384
|
+
writeFileSync(ventureFile(dir, MANIFEST), JSON.stringify(m, null, 2) + "\n");
|
|
385
|
+
writeFileSync(ventureFile(dir, CARD), JSON.stringify(cardOf(dir, m), null, 2) + "\n");
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
const strip = ({ text, ...rest }) => ({ ...rest, sha256: sha(text ?? "") });
|
|
@@ -435,7 +436,7 @@ export async function add(dir, route, { now = new Date().toISOString(), actor =
|
|
|
435
436
|
if (!r) return { ok: false, error: `${route} is not a collection item route (such as product/<id>)` };
|
|
436
437
|
const id = route.slice(r.route.length - 4);
|
|
437
438
|
if (m.records.some((x) => x.route === route)) return { ok: false, error: `${route} is already in release ${m.release}; a full build updates it` };
|
|
438
|
-
const file = r.files.map((g) => g.replace("*", id)).find((f) => existsSync(
|
|
439
|
+
const file = r.files.map((g) => g.replace("*", id)).find((f) => existsSync(ventureFile(dir, f)));
|
|
439
440
|
if (!file) return { ok: false, error: `no ${r.record} ${id} in the venture` };
|
|
440
441
|
const s = await survey(dir);
|
|
441
442
|
const hit = s.frozen.find((f) => f.file === file);
|
|
@@ -451,10 +452,10 @@ export async function add(dir, route, { now = new Date().toISOString(), actor =
|
|
|
451
452
|
for (const p of m.packages) if (HOME[r.record] === p.id) { p.frozen = [...p.frozen, file]; p.gaps = p.gaps.filter((g) => g !== r.record); const t = p.frozen.length + p.holes.length; p.shell = t ? Math.round((p.frozen.length / t) * 100) / 100 : null; }
|
|
452
453
|
m.incremental = [...(m.incremental ?? []), { route, file, from: before, at: stamp(now) }];
|
|
453
454
|
m.release = releaseId(m);
|
|
454
|
-
mkdirSync(dirname(join(dir, RECORDS, file)), { recursive: true });
|
|
455
|
-
writeFileSync(join(dir, RECORDS, file), hit.text);
|
|
456
|
-
writeFileSync(
|
|
457
|
-
writeFileSync(
|
|
455
|
+
mkdirSync(dirname(join(ventureFile(dir, RECORDS), file)), { recursive: true });
|
|
456
|
+
writeFileSync(join(ventureFile(dir, RECORDS), file), hit.text);
|
|
457
|
+
writeFileSync(ventureFile(dir, MANIFEST), JSON.stringify(m, null, 2) + "\n");
|
|
458
|
+
writeFileSync(ventureFile(dir, CARD), JSON.stringify(cardOf(dir, m), null, 2) + "\n");
|
|
458
459
|
prov(dir, { entity: MANIFEST, route: "system/release", activity: "release", agent: agentFromEnv(actor === "agent" ? "agent" : "person", "cli"), note: `release ${m.release}: added ${route} to ${before} without rebuilding the rest` });
|
|
459
460
|
return { ok: true, manifest: m, added: { route, file, from: before } };
|
|
460
461
|
}
|
|
@@ -476,7 +477,7 @@ export function exportRelease(dir, out) {
|
|
|
476
477
|
if (!m) return { ok: false, error: `no release in ${dir}: build one first` };
|
|
477
478
|
if (existsSync(out) && readdirSync(out).length) return { ok: false, error: `${out} exists and is not empty: an export never writes over files` };
|
|
478
479
|
mkdirSync(out, { recursive: true });
|
|
479
|
-
const card = JSON.parse(readFileSync(
|
|
480
|
+
const card = JSON.parse(readFileSync(ventureFile(dir, CARD), "utf8"));
|
|
480
481
|
const lines = [`# ${m.name}`, "", `Phase: ${m.phase}. Release ${m.release}, built ${m.built_at} as of ${m.now}.`, "", "## Headline numbers", "", "| Number | Value | As of | Confirmed by |", "| --- | --- | --- | --- |",
|
|
481
482
|
...card.headline.filter((h) => h.label !== "Live holes").map((h) => `| ${h.label} | ${h.value}${h.unit ? ` ${h.unit}` : ""} | ${h.as_of ?? m.now} | ${h.confirmed_by.join(", ") || "none"} |`), "", "## Records", "", "| Record | As of | Confirmed by |", "| --- | --- | --- |"];
|
|
482
483
|
for (const r of m.records) {
|
package/schema/run-handoff.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { execSync, spawnSync } from "node:child_process";
|
|
13
13
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join, relative } from "node:path";
|
|
15
|
+
import { ventureFile } from "./venture-paths.mjs";
|
|
15
16
|
|
|
16
17
|
const [dir, id] = process.argv.slice(2);
|
|
17
18
|
if (!dir || !id) { console.error("usage: node schema/run-handoff.mjs <venture dir> <handoff id> [--runner <id>]"); process.exit(2); }
|
|
@@ -19,7 +20,7 @@ const opt = (k) => process.argv.includes(k) ? process.argv[process.argv.indexOf(
|
|
|
19
20
|
const runner = opt("--runner") ?? "local";
|
|
20
21
|
const [reviewId, verdict] = (opt("--review") ?? "").split("=");
|
|
21
22
|
const root = new URL("..", import.meta.url).pathname;
|
|
22
|
-
const hPath = join(dir, "operations/handoffs", `${id}.json`);
|
|
23
|
+
const hPath = join(ventureFile(dir, "operations/handoffs"), `${id}.json`);
|
|
23
24
|
const h = JSON.parse(readFileSync(hPath, "utf8"));
|
|
24
25
|
const dirty = execSync("git status --porcelain", { cwd: root, encoding: "utf8" }).trim() ? "+uncommitted" : "";
|
|
25
26
|
const version = execSync("git rev-parse --short HEAD", { cwd: root, encoding: "utf8" }).trim() + dirty;
|
|
@@ -41,10 +42,10 @@ for (const c of h.done_criteria.filter((d) => d.kind !== "review")) {
|
|
|
41
42
|
const at = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
42
43
|
const eid = `${h.id}-${at.replace(/[-:TZ]/g, "").slice(0, 14)}`;
|
|
43
44
|
const ev = { id: eid, subject: { kind: "handoff", id: h.id, version }, ran_at: at, runner, results, passed: results.every((r) => r.passed) };
|
|
44
|
-
mkdirSync(join(dir, "evidence/results"), { recursive: true });
|
|
45
|
-
const ePath = join(dir, "evidence/results", `${eid}.json`);
|
|
45
|
+
mkdirSync(join(ventureFile(dir, "evidence/results")), { recursive: true });
|
|
46
|
+
const ePath = join(ventureFile(dir, "evidence/results"), `${eid}.json`);
|
|
46
47
|
writeFileSync(ePath, JSON.stringify(ev, null, 2) + "\n");
|
|
47
|
-
h.evidence = [...(h.evidence ?? []), relative(join(dir, "operations/handoffs"), ePath)];
|
|
48
|
+
h.evidence = [...(h.evidence ?? []), relative(join(ventureFile(dir, "operations/handoffs")), ePath)];
|
|
48
49
|
writeFileSync(hPath, JSON.stringify(h, null, 2) + "\n");
|
|
49
50
|
const pending = h.done_criteria.filter((d) => d.kind === "review" && d.id !== reviewId).map((d) => `${d.id} (${d.reviewer})`);
|
|
50
51
|
console.log(`${ev.passed ? "every criterion run passed" : "some criteria failed"}${pending.length ? `; review still owed: ${pending.join(", ")}` : ""}: ${relative(process.cwd(), ePath)}`);
|