@marver-design/marver 0.21.0 → 0.22.1
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/CHANGELOG.md +134 -15
- package/README.md +17 -4
- package/dist/{bake-BID6mo-N.mjs → bake-CvY8QR3L.mjs} +1 -1
- package/dist/board-status-CWGHIdo_.mjs +1307 -0
- package/dist/boards-BwKI0qWd.mjs +188 -0
- package/dist/{build-C7MqQ7hq.mjs → build-B1kavcpc.mjs} +74 -13
- package/dist/cli.mjs +69 -9
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-D4t59wDi.mjs +600 -0
- package/dist/{daemon-CRZFpl6K.mjs → daemon-PmNqoOAk.mjs} +1 -1
- package/dist/{dev-BNZF4Mup.mjs → dev-Cf4wLGe2.mjs} +15 -7
- package/dist/{init-C34BY3R4.mjs → init-C_K-cjBQ.mjs} +41 -45
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-mMfUhPtL.mjs → manifest-CcdWx7ud.mjs} +42 -376
- package/dist/{plugin-omHLCn91.mjs → plugin-C5u07Yhv.mjs} +234 -105
- package/dist/{poster-BvxiAzy1.mjs → poster-BduzQBYz.mjs} +1 -1
- package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-b0jUwvM_.mjs} +2 -2
- package/dist/{shot-DswS4iRK.mjs → shot-BAR8hmU9.mjs} +2 -2
- package/docs/boards-and-folders.md +161 -0
- package/docs/context.md +117 -0
- package/docs/sharing.md +12 -2
- package/package.json +1 -1
- package/src/client/shell/BoardList.tsx +33 -3
- package/src/client/shell/ContextMenu.tsx +16 -5
- package/src/client/shell/StatusPicker.tsx +91 -0
- package/src/client/shell/board-icons.tsx +75 -0
- package/src/client/shell/store.ts +88 -9
- package/src/client/shell/styles.css +34 -3
- package/src/shared/board-tree.ts +30 -13
- package/src/shared/board-types.ts +103 -0
- package/src/shared/context.ts +265 -0
- package/src/shared/status.ts +157 -0
- package/templates/AGENTS-embedded.md +12 -0
- package/templates/AGENTS-studio.md +12 -0
- package/templates/context/INDEX.md +50 -0
- package/templates/context/map.json +6 -0
- package/templates/context/shipped-knowledge.md +19 -0
- package/templates/context/shipped.md +28 -0
- package/templates/instructions/boards.md +33 -0
- package/templates/instructions/context.md +123 -0
- package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
- package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
- package/templates/playbooks/reorganize-context/eval.md +93 -0
- package/dist/boards-BwiDAmPf.mjs +0 -337
- package/dist/boards-DnLewfj8.mjs +0 -71
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { r as NAME } from "./cli.mjs";
|
|
2
|
+
import { C as tables, S as proseLines, _ as isRecordTable, b as looseTables, c as CITED_FILE, d as LEVEL, f as availableLevels, g as hasGlob, h as globRe, k as checkRealDirs, l as EVIDENCE_COLUMN, m as frontMatter, p as capabilityTable, s as CITATION, tt as readStatusWord, u as GENERATED, v as levelsIn, w as wordCount, x as parseMap, y as lf } from "./board-status-CWGHIdo_.mjs";
|
|
3
|
+
import { pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
4
|
+
import { t as writeManaged } from "./managed-write-Bo-oPc-i.mjs";
|
|
5
|
+
import { i as scanFrames } from "./manifest-CcdWx7ud.mjs";
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, join, normalize, relative, sep } from "node:path";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
//#region src/server/publish-graph.ts
|
|
10
|
+
/**
|
|
11
|
+
* What a canvas build carries, as repository paths - the same selection the build makes, so a
|
|
12
|
+
* check against it judges what actually ships: the published boards' frames (every frame when
|
|
13
|
+
* `all-scenes` ships), each frame's `_layout` chain, `design/providers`, and everything they import -
|
|
14
|
+
* relative paths, `design/tsconfig.json` aliases, `?raw` and asset strings. An import that looks
|
|
15
|
+
* local and does not resolve is reported, never silently skipped.
|
|
16
|
+
*/
|
|
17
|
+
const IMPORT = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)|require\(\s*['"]([^'"]+)['"]\s*\)|['"]((?:\.{1,2}\/|\/)[^'"\s]+\.[a-z0-9]{1,5})(?:\?[a-z]+)?['"]/g;
|
|
18
|
+
const CODE = /\.(tsx?|jsx?|mjs|cjs|mdx?)$/;
|
|
19
|
+
/** The published board names, straight from design/publish.json (an unreadable policy ships nothing). */
|
|
20
|
+
function publishedBoards(root) {
|
|
21
|
+
try {
|
|
22
|
+
return Object.keys(JSON.parse(readFileSync(join(root, "design", "publish.json"), "utf8")).boards ?? {});
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** JSON with comments and trailing commas, as tsconfig allows - comments stripped outside strings only
|
|
28
|
+
* (a "$schema": "https://..." is a string, not a comment). */
|
|
29
|
+
function parseJsonc(text) {
|
|
30
|
+
let out = "", i = 0, str = false;
|
|
31
|
+
const t = text.replace(/^\uFEFF/, "");
|
|
32
|
+
while (i < t.length) {
|
|
33
|
+
const c = t[i];
|
|
34
|
+
if (str) {
|
|
35
|
+
out += c;
|
|
36
|
+
if (c === "\\") {
|
|
37
|
+
out += t[i + 1] ?? "";
|
|
38
|
+
i += 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (c === "\"") str = false;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === "\"") {
|
|
46
|
+
str = true;
|
|
47
|
+
out += c;
|
|
48
|
+
i++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === "/" && t[i + 1] === "/") {
|
|
52
|
+
while (i < t.length && t[i] !== "\n") i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (c === "/" && t[i + 1] === "*") {
|
|
56
|
+
i += 2;
|
|
57
|
+
while (i < t.length && !(t[i] === "*" && t[i + 1] === "/")) i++;
|
|
58
|
+
i += 2;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out += c;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
|
|
65
|
+
}
|
|
66
|
+
/** design/tsconfig.json's path aliases, resolved against the config that declares them - its own,
|
|
67
|
+
* or one it `extends` (the host's tsconfig), as TypeScript and Vite would. */
|
|
68
|
+
function aliases(root) {
|
|
69
|
+
const read = (rel, depth) => {
|
|
70
|
+
if (depth > 4) return [];
|
|
71
|
+
let cfg;
|
|
72
|
+
try {
|
|
73
|
+
cfg = parseJsonc(readFileSync(join(root, rel), "utf8"));
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const dir = dirname(rel);
|
|
78
|
+
const base = typeof cfg.compilerOptions?.baseUrl === "string" ? normalize(join(dir, cfg.compilerOptions.baseUrl)) : dir;
|
|
79
|
+
const own = Object.entries(cfg.compilerOptions?.paths ?? {}).flatMap(([k, v]) => Array.isArray(v) ? [{
|
|
80
|
+
prefix: k.replace(/\*$/, ""),
|
|
81
|
+
targets: v.map((t) => normalize(join(base, t.replace(/\*$/, ""))))
|
|
82
|
+
}] : []);
|
|
83
|
+
const ext = (Array.isArray(cfg.extends) ? cfg.extends : [cfg.extends]).filter((e) => typeof e === "string" && e.startsWith("."));
|
|
84
|
+
let inherited = [];
|
|
85
|
+
for (const e of ext) {
|
|
86
|
+
const got = read(normalize(join(dir, e.endsWith(".json") ? e : `${e}.json`)), depth + 1);
|
|
87
|
+
inherited = [...got, ...inherited.filter((p) => !got.some((g) => g.prefix === p.prefix))];
|
|
88
|
+
}
|
|
89
|
+
return [...own, ...inherited.filter((p) => !own.some((o) => o.prefix === p.prefix))];
|
|
90
|
+
};
|
|
91
|
+
return read("design/tsconfig.json", 0);
|
|
92
|
+
}
|
|
93
|
+
function publishGraph(root) {
|
|
94
|
+
const files = /* @__PURE__ */ new Set();
|
|
95
|
+
const unresolved = [];
|
|
96
|
+
const boards = publishedBoards(root);
|
|
97
|
+
if (!boards.length) return {
|
|
98
|
+
files,
|
|
99
|
+
unresolved
|
|
100
|
+
};
|
|
101
|
+
const rel = (abs) => relative(root, abs).split(sep).join("/");
|
|
102
|
+
const isFile = (p) => {
|
|
103
|
+
try {
|
|
104
|
+
return statSync(join(root, p)).isFile();
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const manifest = scanFrames(root);
|
|
110
|
+
let wanted = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const b of boards) {
|
|
112
|
+
if (b === "all-scenes") {
|
|
113
|
+
wanted = null;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const j = JSON.parse(readFileSync(join(root, "design", "boards", `${b}.json`), "utf8"));
|
|
118
|
+
for (const n of Array.isArray(j?.nodes) ? j.nodes : []) if (typeof n?.frame === "string") wanted.add(n.frame);
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
const entries = manifest.frames.filter((f) => !wanted || wanted.has(f.id));
|
|
122
|
+
const frames = entries.map((f) => rel(join(root, f.file)));
|
|
123
|
+
const queue = [...frames];
|
|
124
|
+
for (const [i, f] of frames.entries()) {
|
|
125
|
+
const scene = entries[i].scene;
|
|
126
|
+
for (const dir of /* @__PURE__ */ new Set([scene ? `design/scenes/${scene}` : dirname(f), dirname(f)])) for (const n of ["_brief.md", "_note.md"]) if (isFile(`${dir}/${n}`)) queue.push(`${dir}/${n}`);
|
|
127
|
+
const note = f.replace(/\.(tsx|jsx|html)$/, ".note.md");
|
|
128
|
+
if (isFile(note)) queue.push(note);
|
|
129
|
+
}
|
|
130
|
+
for (const f of frames) {
|
|
131
|
+
const base = f.startsWith("design/components/") ? "design/components" : "design/scenes";
|
|
132
|
+
let dir = dirname(f);
|
|
133
|
+
while (dir.length >= base.length) {
|
|
134
|
+
for (const ext of ["tsx", "jsx"]) if (isFile(`${dir}/_layout.${ext}`)) queue.push(`${dir}/_layout.${ext}`);
|
|
135
|
+
if (dir === base) break;
|
|
136
|
+
dir = dirname(dir);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const ext of ["tsx", "jsx"]) if (isFile(`design/providers.${ext}`)) queue.push(`design/providers.${ext}`);
|
|
140
|
+
const al = aliases(root);
|
|
141
|
+
const resolve = (from, spec) => {
|
|
142
|
+
const bare = spec.replace(/\?[a-z]+$/, "");
|
|
143
|
+
let bases = [];
|
|
144
|
+
if (bare.startsWith(".")) bases = [normalize(join(dirname(from), bare))];
|
|
145
|
+
else if (bare.startsWith("/")) bases = [normalize(bare.slice(1))];
|
|
146
|
+
else {
|
|
147
|
+
const a = al.find((x) => bare.startsWith(x.prefix) && x.prefix);
|
|
148
|
+
if (!a) return "";
|
|
149
|
+
bases = a.targets.map((t) => normalize(join(t, bare.slice(a.prefix.length))));
|
|
150
|
+
}
|
|
151
|
+
for (const b of bases) for (const cand of [
|
|
152
|
+
b,
|
|
153
|
+
`${b}.ts`,
|
|
154
|
+
`${b}.tsx`,
|
|
155
|
+
`${b}.js`,
|
|
156
|
+
`${b}.jsx`,
|
|
157
|
+
`${b}/index.ts`,
|
|
158
|
+
`${b}/index.tsx`
|
|
159
|
+
]) if (!cand.startsWith("..") && isFile(cand)) return cand.split(sep).join("/");
|
|
160
|
+
return null;
|
|
161
|
+
};
|
|
162
|
+
while (queue.length) {
|
|
163
|
+
const p = queue.pop();
|
|
164
|
+
if (files.has(p)) continue;
|
|
165
|
+
files.add(p);
|
|
166
|
+
if (!CODE.test(p)) continue;
|
|
167
|
+
let text = "";
|
|
168
|
+
try {
|
|
169
|
+
text = readFileSync(join(root, p), "utf8");
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
for (const m of text.matchAll(IMPORT)) {
|
|
174
|
+
const spec = m[1] || m[2] || m[3] || m[4] || "";
|
|
175
|
+
if (!spec) continue;
|
|
176
|
+
const r = resolve(p, spec);
|
|
177
|
+
if (r === "") continue;
|
|
178
|
+
if (r === null) {
|
|
179
|
+
if (spec.startsWith(".") || spec.startsWith("/") || al.some((x) => x.prefix && spec.startsWith(x.prefix))) unresolved.push({
|
|
180
|
+
from: p,
|
|
181
|
+
spec
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
queue.push(r);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
files,
|
|
190
|
+
unresolved
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/cli/context.ts
|
|
195
|
+
/**
|
|
196
|
+
* `marver context` (spec 19) - the conventions P0 proved, for any repository:
|
|
197
|
+
* init create context/ (the index, the shipped record, the map, the playbooks Marver maintains)
|
|
198
|
+
* check keep it true - deterministic, run in ci; exit 0 pass, 1 fail, 2 cannot determine
|
|
199
|
+
* index rewrite the index's generated capability table from the map
|
|
200
|
+
* The parsing is shared with the dev server (shared/context.ts), so the canvas's statuses and the
|
|
201
|
+
* check read the files one way.
|
|
202
|
+
*/
|
|
203
|
+
const INDEX_BUDGET = 800;
|
|
204
|
+
const CONTEXT = "context";
|
|
205
|
+
/** Create context/ - never overwriting. The playbooks are managed: pristine ones take Marver's
|
|
206
|
+
* updates on a re-run, edited ones are kept and the new version staged for a merge. */
|
|
207
|
+
function contextInit(root, kind = "product") {
|
|
208
|
+
const created = [];
|
|
209
|
+
const templates = join(pkgDir(), "templates");
|
|
210
|
+
const name = projectName(root);
|
|
211
|
+
const bad = checkRealDirs(root, [[join(root, CONTEXT), `${CONTEXT}/`], [join(root, CONTEXT, "playbooks"), `${CONTEXT}/playbooks/`]]);
|
|
212
|
+
if (bad) throw new Error(bad);
|
|
213
|
+
const plain = (rel, body) => {
|
|
214
|
+
const file = join(root, CONTEXT, rel);
|
|
215
|
+
mkdirFor(file);
|
|
216
|
+
try {
|
|
217
|
+
writeFileSync(file, body, { flag: "wx" });
|
|
218
|
+
created.push(`${CONTEXT}/${rel}`);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
if (e.code !== "EEXIST") throw e;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
plain("INDEX.md", readFileSync(join(templates, "context", "INDEX.md"), "utf8").replaceAll("{{NAME}}", name).replace("{{RECORD_QUESTION}}", kind === "knowledge" ? "Was it delivered, to whom, and when?" : "Is it available, where, for whom, since when?"));
|
|
224
|
+
plain("shipped.md", readFileSync(join(templates, "context", kind === "knowledge" ? "shipped-knowledge.md" : "shipped.md"), "utf8"));
|
|
225
|
+
plain("map.json", readFileSync(join(templates, "context", "map.json"), "utf8"));
|
|
226
|
+
const pbRoot = join(templates, "playbooks");
|
|
227
|
+
for (const pb of readdirSync(pbRoot)) for (const f of readdirSync(join(pbRoot, pb)).filter((x) => x.endsWith(".md"))) {
|
|
228
|
+
const rel = `playbooks/${pb}/${f}`;
|
|
229
|
+
writeManaged({
|
|
230
|
+
base: join(root, CONTEXT),
|
|
231
|
+
rel,
|
|
232
|
+
body: readFileSync(join(pbRoot, pb, f), "utf8"),
|
|
233
|
+
shown: `${CONTEXT}/${rel}`,
|
|
234
|
+
stageDir: join(root, "design", ".local", "latest", CONTEXT),
|
|
235
|
+
created,
|
|
236
|
+
rerun: `\`npx ${NAME} context init\``
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (!existsSync(join(root, "design", "instructions", "context.md"))) writeManaged({
|
|
240
|
+
base: join(root, CONTEXT),
|
|
241
|
+
rel: "README.md",
|
|
242
|
+
body: readFileSync(join(templates, "instructions", "context.md"), "utf8"),
|
|
243
|
+
shown: `${CONTEXT}/README.md`,
|
|
244
|
+
stageDir: join(root, "design", ".local", "latest", CONTEXT),
|
|
245
|
+
created,
|
|
246
|
+
rerun: `\`npx ${NAME} context init\``
|
|
247
|
+
});
|
|
248
|
+
const agents = join(root, "AGENTS.md");
|
|
249
|
+
const route = "Any question about the product - what is available, how it works, why, what is next: start at context/INDEX.md.";
|
|
250
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
251
|
+
const cur = existsSync(agents) ? readFileSync(agents, "utf8") : null;
|
|
252
|
+
if (cur !== null && cur.includes("context/INDEX.md")) break;
|
|
253
|
+
if (cur === null) try {
|
|
254
|
+
writeFileSync(agents, `# ${name}\n\n${route}\n`, { flag: "wx" });
|
|
255
|
+
created.push("AGENTS.md");
|
|
256
|
+
break;
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (e.code === "EEXIST") continue;
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
261
|
+
const tmp = `${agents}.${process.pid}.${attempt}.tmp`;
|
|
262
|
+
writeFileSync(tmp, cur.replace(/\n*$/, "") + `\n\n${route}\n`, { flag: "wx" });
|
|
263
|
+
if (readFileSync(agents, "utf8") !== cur) {
|
|
264
|
+
rmSync(tmp, { force: true });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
renameSync(tmp, agents);
|
|
268
|
+
created.push("AGENTS.md (updated)");
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
return created;
|
|
272
|
+
}
|
|
273
|
+
function projectName(root) {
|
|
274
|
+
try {
|
|
275
|
+
const n = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).name;
|
|
276
|
+
if (typeof n === "string" && n) return n.replace(/^@[^/]+\//, "");
|
|
277
|
+
} catch {}
|
|
278
|
+
return basename(root);
|
|
279
|
+
}
|
|
280
|
+
const mkdirFor = (file) => mkdirSync(dirname(file), { recursive: true });
|
|
281
|
+
/** Rewrite the index's generated table from the map. Returns the words now in the index. */
|
|
282
|
+
function contextIndex(root) {
|
|
283
|
+
const indexFile = join(root, CONTEXT, "INDEX.md");
|
|
284
|
+
const mapFile = join(root, CONTEXT, "map.json");
|
|
285
|
+
if (!existsSync(indexFile)) throw new Error(`${CONTEXT}/INDEX.md is missing - run \`npx ${NAME} context init\``);
|
|
286
|
+
if (!existsSync(mapFile)) throw new Error(`${CONTEXT}/map.json is missing - run \`npx ${NAME} context init\``);
|
|
287
|
+
const map = parseMap(readFileSync(mapFile, "utf8"));
|
|
288
|
+
if (typeof map === "string") throw new Error(map);
|
|
289
|
+
const text = readFileSync(indexFile, "utf8");
|
|
290
|
+
const m = GENERATED.exec(text);
|
|
291
|
+
if (!m) throw new Error(`${CONTEXT}/INDEX.md has no <!-- generated --> ... <!-- /generated --> fences for the capability table`);
|
|
292
|
+
const opening = text.slice(m.index, m.index + m[0].indexOf("-->") + 3);
|
|
293
|
+
const next = text.slice(0, m.index) + `${opening}\n${capabilityTable(map)}\n<!-- /generated -->` + text.slice(m.index + m[0].length);
|
|
294
|
+
if (next !== text) writeFileSync(indexFile, next);
|
|
295
|
+
return {
|
|
296
|
+
words: wordCount(next),
|
|
297
|
+
changed: next !== text
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** The supersession smells a current document never carries. */
|
|
301
|
+
const SUPERSEDED = /wins where it differs|wins over sections?|overrides (section|§)|section \d+ wins|supersedes section|amended by section/i;
|
|
302
|
+
/** Availability is the shipped record's alone - a contract saying what is live is the smell. */
|
|
303
|
+
const AVAILABILITY = /\b(on production|on staging|in production since|live since|went live|deployed to|released to|rolled out to|shipped (on|to|in|since)|(was|were|been) delivered (to|on)|delivered on \d|handed over (to|on))\b/i;
|
|
304
|
+
const CONTRACT_STATES = [
|
|
305
|
+
"current",
|
|
306
|
+
"proposed",
|
|
307
|
+
"historical"
|
|
308
|
+
];
|
|
309
|
+
const FEEDBACK_STATES = [
|
|
310
|
+
"new",
|
|
311
|
+
"triaged",
|
|
312
|
+
"proposed",
|
|
313
|
+
"planned",
|
|
314
|
+
"shipped",
|
|
315
|
+
"declined"
|
|
316
|
+
];
|
|
317
|
+
const LINK = /\[[^\]]*\]\(([^)\s]+)\)/g;
|
|
318
|
+
/** A `path:line` citation - a path from the root, or a bare name the resolver looks up by basename
|
|
319
|
+
* (`run.ts:12` when one tracked file is run.ts; shorthand for the file named before it when several are). */
|
|
320
|
+
const CITE = /`((?:\.{0,2}[\w@.-]+\/)*[\w@.$-]+\.[a-z]{1,5}):(\d+)(?:-(\d+))?`/g;
|
|
321
|
+
function contextCheck(root, opts = {}) {
|
|
322
|
+
const failures = [], unsure = [], notes = [];
|
|
323
|
+
const fail = (rule, where, what) => failures.push({
|
|
324
|
+
rule,
|
|
325
|
+
where,
|
|
326
|
+
what
|
|
327
|
+
});
|
|
328
|
+
const cannot = (rule, what) => unsure.push({
|
|
329
|
+
rule,
|
|
330
|
+
where: "",
|
|
331
|
+
what
|
|
332
|
+
});
|
|
333
|
+
const note = (rule, what) => notes.push({
|
|
334
|
+
rule,
|
|
335
|
+
where: "",
|
|
336
|
+
what
|
|
337
|
+
});
|
|
338
|
+
const exists = (p) => existsSync(join(root, p));
|
|
339
|
+
const read = (p) => readFileSync(join(root, p), "utf8");
|
|
340
|
+
const git = (...args) => execFileSync("git", args, {
|
|
341
|
+
cwd: root,
|
|
342
|
+
encoding: "utf8",
|
|
343
|
+
stdio: [
|
|
344
|
+
"ignore",
|
|
345
|
+
"pipe",
|
|
346
|
+
"pipe"
|
|
347
|
+
],
|
|
348
|
+
maxBuffer: 536870912
|
|
349
|
+
});
|
|
350
|
+
if (!exists(CONTEXT)) {
|
|
351
|
+
fail("context", `${CONTEXT}/`, `missing - run \`npx ${NAME} context init\``);
|
|
352
|
+
return {
|
|
353
|
+
exit: 1,
|
|
354
|
+
failures,
|
|
355
|
+
unsure,
|
|
356
|
+
notes
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const walk = (dir) => {
|
|
360
|
+
if (!exists(dir)) return [];
|
|
361
|
+
const out = [];
|
|
362
|
+
for (const n of readdirSync(join(root, dir))) {
|
|
363
|
+
const p = `${dir}/${n}`;
|
|
364
|
+
const st = lstatSync(join(root, p));
|
|
365
|
+
if (st.isSymbolicLink()) continue;
|
|
366
|
+
if (st.isDirectory()) out.push(...walk(p));
|
|
367
|
+
else out.push(p);
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
};
|
|
371
|
+
const docs = walk(CONTEXT).filter((p) => p.endsWith(".md")).map((path) => {
|
|
372
|
+
const text = read(path);
|
|
373
|
+
return {
|
|
374
|
+
path,
|
|
375
|
+
text,
|
|
376
|
+
...frontMatter(text)
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
for (const d of docs) if (d.error) fail("front-matter", d.path, d.error);
|
|
380
|
+
const isContract = (d) => d.data?.state === "current" && /^context\/(product|system)\//.test(d.path);
|
|
381
|
+
for (const d of docs.filter(isContract)) for (const [n, line] of proseLines(d.body, d.offset)) {
|
|
382
|
+
if (/^\s*\**status\**\s*:/i.test(line)) fail("status-line", `${d.path}:${n}`, `a status line in a current contract - status lives in ${CONTEXT}/shipped.md`);
|
|
383
|
+
const a = line.match(AVAILABILITY);
|
|
384
|
+
if (a) fail("availability", `${d.path}:${n}`, `"${a[0]}" - availability lives in ${CONTEXT}/shipped.md`);
|
|
385
|
+
}
|
|
386
|
+
for (const d of docs.filter((x) => x.data?.state === "current")) for (const [n, line] of proseLines(d.body, d.offset)) {
|
|
387
|
+
const m = line.match(SUPERSEDED);
|
|
388
|
+
if (m) fail("supersession", `${d.path}:${n}`, `"${m[0]}" - fold the amendment in; one answer per rule`);
|
|
389
|
+
}
|
|
390
|
+
let tracked = null;
|
|
391
|
+
try {
|
|
392
|
+
tracked = new Set(git("ls-files", "-z").split("\0").filter(Boolean));
|
|
393
|
+
} catch {
|
|
394
|
+
cannot("git", "git ls-files failed - not a repository?");
|
|
395
|
+
}
|
|
396
|
+
/** A cited file, resolved: a path from the root, or a bare name (`run.test.ts`) that one tracked
|
|
397
|
+
* file carries - several mean the citation is shorthand for the one named before it. Returns the
|
|
398
|
+
* path, 'ambiguous', or null when nothing carries it. */
|
|
399
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
400
|
+
if (tracked) for (const t of tracked) {
|
|
401
|
+
const b = t.slice(t.lastIndexOf("/") + 1);
|
|
402
|
+
byBase.set(b, [...byBase.get(b) ?? [], t]);
|
|
403
|
+
}
|
|
404
|
+
const resolveCited = (f) => {
|
|
405
|
+
if (exists(f)) return statSync(join(root, f)).isFile() ? f : null;
|
|
406
|
+
if (f.includes("/")) return null;
|
|
407
|
+
const hits = byBase.get(f) ?? [];
|
|
408
|
+
return hits.length === 1 ? hits[0] : hits.length > 1 ? "ambiguous" : null;
|
|
409
|
+
};
|
|
410
|
+
const lines = /* @__PURE__ */ new Map();
|
|
411
|
+
const linesOf = (p) => {
|
|
412
|
+
if (!lines.has(p)) lines.set(p, read(p).split("\n").length);
|
|
413
|
+
return lines.get(p);
|
|
414
|
+
};
|
|
415
|
+
/** What is wrong with a citation - a file nothing carries, a line past its end, a range backwards - or null. */
|
|
416
|
+
const citationProblem = (file, from, to) => {
|
|
417
|
+
const f = file.replace(/^\.\//, "");
|
|
418
|
+
const r = resolveCited(f);
|
|
419
|
+
if (r === null) return `${f} does not exist`;
|
|
420
|
+
const a = Number(from), z = Number(to ?? from);
|
|
421
|
+
if (from && z < a) return `${f}:${from}-${to} runs backwards`;
|
|
422
|
+
if (r === "ambiguous" || !from) return null;
|
|
423
|
+
if (z > linesOf(r)) return `${f}:${from}${to ? `-${to}` : ""} is past the end of ${r} (${linesOf(r)} lines)`;
|
|
424
|
+
return null;
|
|
425
|
+
};
|
|
426
|
+
const shippedPath = `${CONTEXT}/shipped.md`;
|
|
427
|
+
if (!exists(shippedPath)) fail("shipped", shippedPath, "missing");
|
|
428
|
+
else {
|
|
429
|
+
const text = read(shippedPath);
|
|
430
|
+
if (!tables(text).some(isRecordTable)) fail("shipped-shape", shippedPath, "no Capability (or Project) table with an Available (or Delivered) column - the canvas cannot read a status from it");
|
|
431
|
+
for (const n of looseTables(text)) fail("table-format", `${shippedPath}:${n}`, "a table without outer pipes - start and end every row with |");
|
|
432
|
+
for (const t of tables(text)) for (const r of t.rows) {
|
|
433
|
+
const where = `${shippedPath}:${r.line}`;
|
|
434
|
+
if (!r.cells.some((c) => levelsIn(c).length)) fail("shipped-level", where, "a row with no evidence level");
|
|
435
|
+
r.cells.forEach((c, i) => {
|
|
436
|
+
if (EVIDENCE_COLUMN.test(t.header[i] ?? "") && !/^nowhere\b/i.test(c) && !levelsIn(c).length) fail("shipped-level", where, `the ${t.header[i]} cell has no evidence level`);
|
|
437
|
+
for (const m of c.matchAll(LEVEL)) if (m[1] !== "unknown" && !CITATION.test(c)) fail("shipped-citation", where, `\`${m[1]}\` with no citation in its cell`);
|
|
438
|
+
if (levelsIn(c).length) for (const m of c.matchAll(CITED_FILE)) {
|
|
439
|
+
const problem = citationProblem(m[1], m[2], m[3]);
|
|
440
|
+
if (problem) fail("dead-citation", where, problem);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const d of docs) for (const [n, line] of proseLines(d.body, d.offset)) {
|
|
446
|
+
for (const m of line.matchAll(LINK)) {
|
|
447
|
+
const href = m[1].split("#")[0];
|
|
448
|
+
if (!href || /^[a-z]+:/i.test(href)) continue;
|
|
449
|
+
if (!exists(normalize(join(dirname(d.path), href)))) fail("dead-link", `${d.path}:${n}`, `${m[1]} does not exist`);
|
|
450
|
+
}
|
|
451
|
+
if (d.data?.state === "historical") continue;
|
|
452
|
+
for (const m of line.matchAll(CITE)) {
|
|
453
|
+
const problem = citationProblem(m[1], m[2], m[3]);
|
|
454
|
+
if (problem) fail("dead-citation", `${d.path}:${n}`, problem);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
let map = null;
|
|
458
|
+
const mapPath = `${CONTEXT}/map.json`;
|
|
459
|
+
if (!exists(mapPath)) fail("map", mapPath, "missing");
|
|
460
|
+
else {
|
|
461
|
+
const m = parseMap(read(mapPath));
|
|
462
|
+
if (typeof m === "string") fail("map", mapPath, m);
|
|
463
|
+
else map = m;
|
|
464
|
+
}
|
|
465
|
+
const indexPath = `${CONTEXT}/INDEX.md`;
|
|
466
|
+
if (!exists(indexPath)) fail("index", indexPath, "missing");
|
|
467
|
+
else {
|
|
468
|
+
const text = read(indexPath);
|
|
469
|
+
const words = wordCount(text);
|
|
470
|
+
if (words > 800) fail("index-budget", indexPath, `${words} words, budget 800`);
|
|
471
|
+
else note("index", `${words} / 800 words`);
|
|
472
|
+
if (map) {
|
|
473
|
+
const fenced = GENERATED.exec(text)?.[1];
|
|
474
|
+
if (fenced === void 0) fail("index-table", indexPath, "no <!-- generated --> fences around the capability table");
|
|
475
|
+
else if (capabilityTable(map).trim() !== lf(fenced).trim()) fail("index-table", indexPath, `the capability table differs from ${mapPath} - run \`npx ${NAME} context index\``);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (tracked) {
|
|
479
|
+
for (const d of docs) if (d.data?.audience === "restricted" && tracked.has(d.path)) fail("audience", d.path, "restricted, yet tracked by git");
|
|
480
|
+
}
|
|
481
|
+
const graph = publishGraph(root);
|
|
482
|
+
let inContext = 0;
|
|
483
|
+
for (const p of graph.files) {
|
|
484
|
+
const declared = /\.mdx?$/.test(p) ? frontMatter(read(p)).data?.audience : void 0;
|
|
485
|
+
if (p.startsWith(`${CONTEXT}/`)) {
|
|
486
|
+
inContext++;
|
|
487
|
+
const audience = typeof declared === "string" ? declared : "team";
|
|
488
|
+
if (audience !== "publishable") fail("audience", p, `${audience} material reachable from design/publish.json`);
|
|
489
|
+
} else if (typeof declared === "string" && declared !== "publishable") fail("audience", p, `${declared} material reachable from design/publish.json`);
|
|
490
|
+
}
|
|
491
|
+
for (const u of graph.unresolved) if (/context\//.test(u.spec)) cannot("audience", `${u.from} imports ${u.spec}, which does not resolve - cannot tell what it would publish`);
|
|
492
|
+
note("audience", `${graph.files.size} files reachable from design/publish.json; ${inContext} under ${CONTEXT}/`);
|
|
493
|
+
for (const d of docs) {
|
|
494
|
+
if (d.path.startsWith(`${CONTEXT}/product/`) && d.data && !CONTRACT_STATES.includes(String(d.data.state))) fail("state", d.path, `state "${String(d.data.state)}" is not a contract state (${CONTRACT_STATES.join(", ")})`);
|
|
495
|
+
if (!d.path.startsWith(`${CONTEXT}/feedback/`)) continue;
|
|
496
|
+
for (const n of looseTables(d.text)) fail("table-format", `${d.path}:${n}`, "a table without outer pipes - start and end every row with |");
|
|
497
|
+
for (const t of tables(d.text)) {
|
|
498
|
+
const si = t.header.findIndex((h) => /^state$/i.test(h));
|
|
499
|
+
if (si < 0) continue;
|
|
500
|
+
let ri = t.header.findIndex((h) => /resolv/i.test(h));
|
|
501
|
+
if (ri < 0) ri = t.header.length - 1;
|
|
502
|
+
for (const r of t.rows) {
|
|
503
|
+
const state = (r.cells[si] ?? "").replace(/`/g, "").trim();
|
|
504
|
+
const where = `${d.path}:${r.line}`;
|
|
505
|
+
if (!FEEDBACK_STATES.includes(state)) fail("state", where, `"${state}" is not a feedback state (${FEEDBACK_STATES.join(", ")})`);
|
|
506
|
+
const resolution = r.cells[ri] ?? "";
|
|
507
|
+
if (state !== "shipped") continue;
|
|
508
|
+
const claims = resolution.split(";").filter((c) => !/\b(implement(ed|ation)|built|merged|coded|tested|test suite|ci)\b/i.test(c) || /\b(available|availability|production|live|delivered)\b/i.test(c)).join(";");
|
|
509
|
+
if (ri === si || !availableLevels(claims).includes("confirmed") || !CITATION.test(resolution) || /availab\w*[^;]*`unknown`/i.test(resolution)) fail("feedback-closed", where, "shipped without a cited `confirmed` availability in its resolution");
|
|
510
|
+
for (const m of resolution.matchAll(CITED_FILE)) {
|
|
511
|
+
const problem = citationProblem(m[1], m[2], m[3]);
|
|
512
|
+
if (problem) fail("dead-citation", where, problem);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (map) {
|
|
518
|
+
for (const [cap, c] of Object.entries(map.capabilities)) {
|
|
519
|
+
if (c.contract && !exists(c.contract)) fail("map", mapPath, `${cap}: contract ${c.contract} does not exist`);
|
|
520
|
+
for (const t of c.tests ?? []) if (!(hasGlob(t) ? tracked ? [...tracked].some((f) => globRe(t).test(f)) : true : exists(t))) fail("map", mapPath, `${cap}: test ${t} ${hasGlob(t) ? "matches no tracked file" : "does not exist"}`);
|
|
521
|
+
}
|
|
522
|
+
if (tracked && map.source?.length) {
|
|
523
|
+
const inSource = map.source.map(globRe);
|
|
524
|
+
const covered = [...Object.values(map.capabilities).flatMap((c) => [...c.paths, ...c.tests ?? []]), ...map.excluded.map((e) => e.path)].map(globRe);
|
|
525
|
+
const loose = [...tracked].filter((p) => inSource.some((r) => r.test(p)) && !covered.some((r) => r.test(p)));
|
|
526
|
+
note("map", loose.length ? `${loose.length} source files neither mapped nor excluded, e.g. ${loose.slice(0, 5).join(", ")}` : "every source file is mapped or excluded");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const boardsDir = join(root, "design", "boards");
|
|
530
|
+
if (existsSync(boardsDir)) for (const f of readdirSync(boardsDir).filter((x) => x.endsWith(".json") && !x.startsWith("_"))) try {
|
|
531
|
+
const j = JSON.parse(readFileSync(join(boardsDir, f), "utf8"));
|
|
532
|
+
if (readStatusWord(j?.status) === "done") fail("board-status", `design/boards/${f}`, "\"status\": \"done\" - Done comes only from the shipped record; remove it");
|
|
533
|
+
} catch {}
|
|
534
|
+
for (const d of docs.filter((x) => /\/playbooks\/[^/]+\/PLAYBOOK\.md$/.test(x.path))) {
|
|
535
|
+
const ls = d.data?.last_success;
|
|
536
|
+
const rev = typeof ls === "object" && ls ? ls.revision : void 0;
|
|
537
|
+
const all = [].concat(d.data?.depends_on ?? []).filter((x) => typeof x === "string");
|
|
538
|
+
const deps = all.filter((x) => !/^(service|account)\s*:/.test(x));
|
|
539
|
+
const outside = all.length - deps.length;
|
|
540
|
+
if (typeof rev !== "string") {
|
|
541
|
+
note("playbook", `${d.path}: unknown - no last_success.revision`);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
git("cat-file", "-e", `${rev}^{commit}`);
|
|
546
|
+
const changed = deps.length ? git("diff", "--name-only", rev, "--", ...deps).trim() : "";
|
|
547
|
+
note("playbook", `${d.path}: ${changed ? `stale - ${changed.split("\n").length} dependencies changed since ${rev}` : outside ? `unknown - its files are unchanged since ${rev}, ${outside} dependenc${outside === 1 ? "y" : "ies"} outside git (a service, an account) cannot be checked` : `current since ${rev} (working tree included)`}`);
|
|
548
|
+
} catch {
|
|
549
|
+
note("playbook", `${d.path}: unknown - ${rev} is not in this clone's history`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (opts.prError) cannot("pr", opts.prError);
|
|
553
|
+
if (opts.base && map) {
|
|
554
|
+
let changed = null;
|
|
555
|
+
try {
|
|
556
|
+
changed = git("diff", "--name-only", "-z", `${opts.base}...${opts.head ?? "HEAD"}`).split("\0").filter(Boolean);
|
|
557
|
+
} catch {
|
|
558
|
+
cannot("pr", `no merge base between ${opts.base} and ${opts.head ?? "HEAD"} - fetch full history (fetch-depth: 0)`);
|
|
559
|
+
}
|
|
560
|
+
if (changed) for (const [cap, c] of Object.entries(map.capabilities)) {
|
|
561
|
+
if (!c.contract) continue;
|
|
562
|
+
const res = c.paths.map(globRe);
|
|
563
|
+
const hit = changed.filter((p) => res.some((r) => r.test(p)));
|
|
564
|
+
if (!hit.length || changed.includes(c.contract)) continue;
|
|
565
|
+
if (!new RegExp(`no-contract-change:\\s*${cap}\\s*-\\s*\\S`, "i").test(opts.body ?? "")) fail("pr", cap, `changes ${hit.length} mapped file(s) (${hit.slice(0, 3).join(", ")}) without ${c.contract} - update it, or write "no-contract-change: ${cap} - <why>" in the pull request`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
exit: failures.length ? 1 : unsure.length ? 2 : 0,
|
|
570
|
+
failures,
|
|
571
|
+
unsure,
|
|
572
|
+
notes
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/** The pull request this ci run checks, from GitHub's event payload: base, head and body. */
|
|
576
|
+
function pullRequestFromEnv(env = process.env) {
|
|
577
|
+
if (!/^pull_request(_target)?$/.test(env.GITHUB_EVENT_NAME ?? "")) return null;
|
|
578
|
+
try {
|
|
579
|
+
const pr = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH ?? "", "utf8")).pull_request;
|
|
580
|
+
if (typeof pr?.base?.sha !== "string" || typeof pr?.head?.sha !== "string") throw new Error("no base and head in the event");
|
|
581
|
+
return {
|
|
582
|
+
base: pr.base.sha,
|
|
583
|
+
head: pr.head.sha,
|
|
584
|
+
body: typeof pr.body === "string" ? pr.body : ""
|
|
585
|
+
};
|
|
586
|
+
} catch (e) {
|
|
587
|
+
return { prError: `the pull request event (GITHUB_EVENT_PATH) could not be read: ${e.message}` };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/** The human report. */
|
|
591
|
+
function printCheck(r) {
|
|
592
|
+
for (const n of r.notes) console.log(` ${n.rule} ${n.what}`);
|
|
593
|
+
for (const u of r.unsure) console.log(`? ${u.rule} cannot determine: ${u.what}`);
|
|
594
|
+
for (const f of r.failures) console.log(`✗ ${f.rule} ${f.where} ${f.what}`);
|
|
595
|
+
console.log(`\ncontext check: ${r.exit === 0 ? "pass" : r.exit === 1 ? `${r.failures.length} failing` : "cannot determine"}`);
|
|
596
|
+
}
|
|
597
|
+
/** Where this repository's context conventions live - the canvas's instruction, else context/README.md. */
|
|
598
|
+
const conventionsPath = (root) => existsSync(join(root, "design", "instructions", "context.md")) ? "design/instructions/context.md" : `${CONTEXT}/README.md`;
|
|
599
|
+
//#endregion
|
|
600
|
+
export { INDEX_BUDGET, contextCheck, contextIndex, contextInit, conventionsPath, printCheck, pullRequestFromEnv };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as replay } from "./events-B3LBn74P.mjs";
|
|
2
2
|
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
3
3
|
import { n as localProfile } from "./profile-BjAPAJSb.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { o as toFrameId } from "./manifest-CcdWx7ud.mjs";
|
|
5
5
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
6
6
|
import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
|
|
7
7
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as PKG, r as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
4
|
-
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { i as detectHost, n as loadConfig } from "./config-DJxMRVD8.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-C5u07Yhv.mjs";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { createLogger, createServer, parseAst, searchForWorkspaceRoot } from "vite";
|
|
@@ -413,7 +413,9 @@ async function dev(root, portFlag) {
|
|
|
413
413
|
"**/coverage/**",
|
|
414
414
|
"**/.gstack/**",
|
|
415
415
|
"**/.git/**",
|
|
416
|
-
"**/.playwright-mcp/**"
|
|
416
|
+
"**/.playwright-mcp/**",
|
|
417
|
+
"**/context/map.json",
|
|
418
|
+
"**/context/sync/**"
|
|
417
419
|
] },
|
|
418
420
|
warmup: { clientFiles: [
|
|
419
421
|
join(clientDir, "frame-host", "main.tsx"),
|
|
@@ -452,12 +454,12 @@ async function dev(root, portFlag) {
|
|
|
452
454
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
453
455
|
setTimeout(async () => {
|
|
454
456
|
try {
|
|
455
|
-
await (await import("./shot-
|
|
457
|
+
await (await import("./shot-BAR8hmU9.mjs")).sweepGhosts((m) => console.log(` ${m}`));
|
|
456
458
|
} catch {}
|
|
457
459
|
}, 0);
|
|
458
460
|
try {
|
|
459
461
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
460
|
-
const { installedVersion } = await import("./plugin-
|
|
462
|
+
const { installedVersion } = await import("./plugin-C5u07Yhv.mjs").then((n) => n.i);
|
|
461
463
|
const stale = staleManagedInstructions(root);
|
|
462
464
|
if (stale.length) {
|
|
463
465
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -465,6 +467,12 @@ async function dev(root, portFlag) {
|
|
|
465
467
|
console.log(` instructions predate ${v ? NAME + " " + v : "the installed version"} (${shown}) - run: npx ${NAME} init\n`);
|
|
466
468
|
}
|
|
467
469
|
} catch {}
|
|
470
|
+
try {
|
|
471
|
+
if (!existsSync(join(root, "context"))) {
|
|
472
|
+
const n = (await import("./manifest-CcdWx7ud.mjs").then((n) => n.r)).statusBoards(root).length;
|
|
473
|
+
if (n) console.log(` ${n} feature/project board${n === 1 ? "" : "s"}, no context/ - every status reads Backlog. To start it, tell your agent: "Set up our context."\n`);
|
|
474
|
+
}
|
|
475
|
+
} catch {}
|
|
468
476
|
const { loadCollab, syncOnce } = await import("./sync-BZaCWqK-.mjs").then((n) => n.o);
|
|
469
477
|
if (loadCollab(root)) console.log(` comments: syncing with the published canvas\n`);
|
|
470
478
|
let syncing = false;
|
|
@@ -495,7 +503,7 @@ async function dev(root, portFlag) {
|
|
|
495
503
|
});
|
|
496
504
|
}
|
|
497
505
|
if (config.jam) {
|
|
498
|
-
const { startJam } = await import("./daemon-
|
|
506
|
+
const { startJam } = await import("./daemon-PmNqoOAk.mjs");
|
|
499
507
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
500
508
|
if (jam) {
|
|
501
509
|
const close = server.close.bind(server);
|