scriptonia 0.9.0-rc.1 → 0.9.0-rc.2
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/LICENSE +21 -0
- package/README.md +32 -11
- package/assets/grammar-manifest.json +32 -0
- package/assets/grammars/tree-sitter-bash.wasm +0 -0
- package/assets/grammars/tree-sitter-c.wasm +0 -0
- package/assets/grammars/tree-sitter-cpp.wasm +0 -0
- package/assets/grammars/tree-sitter-dart.wasm +0 -0
- package/assets/grammars/tree-sitter-go.wasm +0 -0
- package/assets/grammars/tree-sitter-java.wasm +0 -0
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
- package/assets/grammars/tree-sitter-objc.wasm +0 -0
- package/assets/grammars/tree-sitter-python.wasm +0 -0
- package/assets/grammars/tree-sitter-rust.wasm +0 -0
- package/assets/grammars/tree-sitter-swift.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/licenses/tree-sitter-wasms-LICENSE +24 -0
- package/assets/licenses/web-tree-sitter-LICENSE +21 -0
- package/assets/queries/manifest.json +12 -0
- package/assets/queries/v1/dart.scm +7 -0
- package/assets/queries/v1/go.scm +5 -0
- package/assets/queries/v1/javascript.scm +15 -0
- package/assets/queries/v1/python.scm +5 -0
- package/assets/queries/v1/typescript.scm +17 -0
- package/assets/tree-sitter.wasm +0 -0
- package/bin/scriptonia.mjs +243 -76
- package/bin/verify.mjs +40 -6
- package/dist/brain-ast-worker.js +62 -0
- package/dist/brain-ast-worker.js.map +1 -0
- package/dist/chunk-6TLQP3LV.js +4048 -0
- package/dist/chunk-6TLQP3LV.js.map +1 -0
- package/dist/chunk-ZLKAW77A.js +16613 -0
- package/dist/chunk-ZLKAW77A.js.map +1 -0
- package/dist/index.js +1407 -662
- package/dist/index.js.map +1 -1
- package/dist/legacy-brain-bridge.js +156 -0
- package/dist/legacy-brain-bridge.js.map +1 -0
- package/package.json +32 -3
package/bin/scriptonia.mjs
CHANGED
|
@@ -25,6 +25,12 @@ const DEFAULT_URL = process.env.SCRIPTONIA_URL || "https://scriptonia.dev";
|
|
|
25
25
|
const CONFIG_DIR = path.join(os.homedir(), ".scriptonia");
|
|
26
26
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
27
27
|
const PROJECTS_DIR = path.join(CONFIG_DIR, "projects");
|
|
28
|
+
let brainBridgePromise;
|
|
29
|
+
|
|
30
|
+
function loadBrainBridge() {
|
|
31
|
+
brainBridgePromise ??= import(new URL("../dist/legacy-brain-bridge.js", import.meta.url).href);
|
|
32
|
+
return brainBridgePromise;
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
const [, , cmd, ...rest] = process.argv;
|
|
30
36
|
|
|
@@ -91,7 +97,7 @@ async function login(args) {
|
|
|
91
97
|
|
|
92
98
|
const C = colors();
|
|
93
99
|
console.log(`\n ${C.g}✓${C.r} Signed in as ${result.email} ${C.dim}(project "${result.project}", with sample data to try)${C.r}`);
|
|
94
|
-
console.log(` ${C.g}✓${C.r}
|
|
100
|
+
console.log(` ${C.g}✓${C.r} Claude skill installed — run \`npx scriptonia init\` inside each repository to add AGENTS.md`);
|
|
95
101
|
console.log(`\n ${C.b}The loop — feed signal, turn an issue into a plan your agent runs:${C.r}\n`);
|
|
96
102
|
console.log(` ${C.g}npx scriptonia add${C.r} feedback.txt call.vtt ${C.dim}feed customer signal${C.r}`);
|
|
97
103
|
console.log(` ${C.g}npx scriptonia plan${C.r} "the thing to build" ${C.dim}→ PLAN.md (sourced, checked)${C.r}`);
|
|
@@ -341,15 +347,34 @@ async function comment(args) {
|
|
|
341
347
|
if (target.startsWith("plan/")) {
|
|
342
348
|
const slug = target.slice(5);
|
|
343
349
|
console.log(`\n ${C.dim}Folding your note into "${slug}" and regenerating…${C.r}`);
|
|
350
|
+
// A regeneration receives both a freshly retrieved pack and the complete
|
|
351
|
+
// snapshot-bound census inventory. Without both, create-file absence
|
|
352
|
+
// cannot be proved and the server fails closed.
|
|
353
|
+
let evidence;
|
|
354
|
+
try {
|
|
355
|
+
evidence = await freshPackForExistingPlan(cfg, slug);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
358
|
+
}
|
|
359
|
+
if (!evidence) {
|
|
360
|
+
fail("Cannot prove the repository census for this plan. Restore its local PLAN.md issue metadata and rebuild the Deep Brain before regenerating.");
|
|
361
|
+
}
|
|
344
362
|
const res = await fetch(`${cfg.url}/api/plan/comment`, {
|
|
345
363
|
method: "POST",
|
|
346
364
|
headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
|
|
347
|
-
body: JSON.stringify({
|
|
365
|
+
body: JSON.stringify({
|
|
366
|
+
slug,
|
|
367
|
+
body: text,
|
|
368
|
+
author: flags.as ?? cfg.email,
|
|
369
|
+
...(evidence ? {
|
|
370
|
+
context_pack: evidence.contextPack,
|
|
371
|
+
repository_inventory: evidence.repositoryInventory,
|
|
372
|
+
} : {}),
|
|
373
|
+
}),
|
|
348
374
|
});
|
|
349
|
-
const data = await res
|
|
350
|
-
if (res.status === 402) failCredits(data);
|
|
375
|
+
const data = await readResponseJson(res);
|
|
351
376
|
if (res.status === 404) fail(`no plan "${slug}" — generate one first: npx scriptonia plan "<issue>"`);
|
|
352
|
-
if (!res.ok)
|
|
377
|
+
if (!res.ok) failPlanResponse("comment", res, data);
|
|
353
378
|
// Approval-chain receipt: which human note produced which plan version.
|
|
354
379
|
if (cfg.inited) {
|
|
355
380
|
const cdir = path.join(cfg.dir, "comments", "plan", slug);
|
|
@@ -360,7 +385,7 @@ async function comment(args) {
|
|
|
360
385
|
);
|
|
361
386
|
appendLog(cfg.dir, "comment", `plan/${slug} — regenerated to v${data.version}`);
|
|
362
387
|
}
|
|
363
|
-
writePlan(cfg, data, flags);
|
|
388
|
+
await writePlan(cfg, data, flags, "commented");
|
|
364
389
|
return;
|
|
365
390
|
}
|
|
366
391
|
|
|
@@ -482,6 +507,71 @@ function failCredits(data) {
|
|
|
482
507
|
process.exit(1);
|
|
483
508
|
}
|
|
484
509
|
|
|
510
|
+
async function readResponseJson(response) {
|
|
511
|
+
const text = await response.text();
|
|
512
|
+
if (!text) return {};
|
|
513
|
+
try { return JSON.parse(text); } catch { return { error: "invalid_json_response", message: text.slice(0, 2000) }; }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function issueText(value) {
|
|
517
|
+
if (typeof value === "string") return value;
|
|
518
|
+
if (!value || typeof value !== "object") return String(value);
|
|
519
|
+
const pathText = Array.isArray(value.path) && value.path.length ? `${value.path.join(".")}: ` : "";
|
|
520
|
+
return `${pathText}${typeof value.message === "string" ? value.message : JSON.stringify(value)}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function planWarnings(data) {
|
|
524
|
+
const values = [
|
|
525
|
+
...(Array.isArray(data?.warnings) ? data.warnings : []),
|
|
526
|
+
...(Array.isArray(data?.quality?.warnings) ? data.quality.warnings : []),
|
|
527
|
+
].map(issueText).filter(Boolean);
|
|
528
|
+
return [...new Set(values)];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function failPlanResponse(operation, response, data) {
|
|
532
|
+
if (response.status === 402) failCredits(data);
|
|
533
|
+
const error = typeof data?.error === "string" ? data.error : `${operation}_failed`;
|
|
534
|
+
const message = typeof data?.message === "string" ? data.message : "";
|
|
535
|
+
const issues = Array.isArray(data?.issues) ? data.issues.map(issueText) : [];
|
|
536
|
+
const warnings = planWarnings(data);
|
|
537
|
+
const details = [message, ...issues.map((item) => `- ${item}`), ...warnings.map((item) => `- warning: ${item}`)].filter(Boolean);
|
|
538
|
+
if (response.status === 409) {
|
|
539
|
+
fail(`\nPlan blocked (409 · ${error}).${details.length ? `\n${details.join("\n")}` : ""}\nRun \`npx scriptonia brain status\`; rebuild if needed, then retry.`);
|
|
540
|
+
}
|
|
541
|
+
if (response.status === 422) {
|
|
542
|
+
fail(`\nPlan quality rejected (422 · ${error}).${details.length ? `\n${details.join("\n")}` : ""}\nNo PLAN.md was overwritten.`);
|
|
543
|
+
}
|
|
544
|
+
fail(`${operation} failed (${response.status} · ${error})${details.length ? `:\n${details.join("\n")}` : ""}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function issueFromPlanMarkdown(markdown) {
|
|
548
|
+
const raw = markdown.match(/^issue:\s*(.+)$/m)?.[1]?.trim();
|
|
549
|
+
if (!raw) return undefined;
|
|
550
|
+
try {
|
|
551
|
+
const parsed = JSON.parse(raw);
|
|
552
|
+
return typeof parsed === "string" && parsed.trim() ? parsed.trim() : undefined;
|
|
553
|
+
} catch {
|
|
554
|
+
return raw.replace(/^['"]|['"]$/g, "").trim() || undefined;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function freshPackForExistingPlan(cfg, slug) {
|
|
559
|
+
if (!cfg.inited) return undefined;
|
|
560
|
+
const candidates = [path.join(cfg.plansDir, `${slug}.md`), path.resolve(process.cwd(), "PLAN.md")];
|
|
561
|
+
let issue;
|
|
562
|
+
for (const filename of candidates) {
|
|
563
|
+
try {
|
|
564
|
+
const markdown = fs.readFileSync(filename, "utf8");
|
|
565
|
+
if (filename.endsWith("PLAN.md") && !new RegExp(`^plan:\\s*${slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m").test(markdown)) continue;
|
|
566
|
+
issue = issueFromPlanMarkdown(markdown);
|
|
567
|
+
if (issue) break;
|
|
568
|
+
} catch { /* server pack remains the safe fallback */ }
|
|
569
|
+
}
|
|
570
|
+
if (!issue) return undefined;
|
|
571
|
+
const { buildFreshPlanEvidence } = await loadBrainBridge();
|
|
572
|
+
return buildFreshPlanEvidence(cfg.repoPath, issue);
|
|
573
|
+
}
|
|
574
|
+
|
|
485
575
|
async function logout() {
|
|
486
576
|
if (fs.existsSync(CONFIG_PATH)) fs.rmSync(CONFIG_PATH);
|
|
487
577
|
console.log("logged out.");
|
|
@@ -752,9 +842,11 @@ async function init(args) {
|
|
|
752
842
|
|
|
753
843
|
const existing = findProjectForCwd();
|
|
754
844
|
if (existing && !flags.force) {
|
|
755
|
-
|
|
845
|
+
injectAgentsMd(cwd);
|
|
846
|
+
injectBrainGitignore(cwd);
|
|
756
847
|
console.log(`\n ${C.g}✓${C.r} already initialized as ${C.b}${existing.data.name}${C.r} ${C.dim}(${existing.dir})${C.r}`);
|
|
757
848
|
console.log(` ${C.dim}re-scan from scratch:${C.r} ${C.g}npx scriptonia init --force${C.r}`);
|
|
849
|
+
await buildFastBrainForInit(existing.data.repoPath ?? cwd, existing.dir, C);
|
|
758
850
|
console.log(` ${C.dim}next:${C.r} ${C.g}npx scriptonia add <files>${C.r}\n`);
|
|
759
851
|
return;
|
|
760
852
|
}
|
|
@@ -788,9 +880,8 @@ async function init(args) {
|
|
|
788
880
|
for (const sub of ["signals", "rules", "contexts", "plans", "comments/plan", "logs"]) {
|
|
789
881
|
fs.mkdirSync(path.join(dir, sub), { recursive: true });
|
|
790
882
|
}
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
if (fs.existsSync(legacyBrain)) fs.rmSync(legacyBrain, { recursive: true, force: true });
|
|
883
|
+
// Legacy plain-file brains remain readable. Init never deletes repository
|
|
884
|
+
// memory or the last READY generation while replacing a project binding.
|
|
794
885
|
|
|
795
886
|
fs.writeFileSync(
|
|
796
887
|
path.join(dir, "project.json"),
|
|
@@ -798,22 +889,68 @@ async function init(args) {
|
|
|
798
889
|
{ mode: 0o600 },
|
|
799
890
|
);
|
|
800
891
|
writeProjectMd(dir, scan, pitch);
|
|
801
|
-
writeRepoMd(dir, scan);
|
|
802
892
|
writeAgentsBrainMd(dir, { name, pitch, stack: scan.stack }, []);
|
|
803
893
|
fs.writeFileSync(path.join(dir, ".gitignore"), "logs/\nproject.json\n");
|
|
804
894
|
injectAgentsMd(cwd);
|
|
895
|
+
injectBrainGitignore(cwd);
|
|
805
896
|
installSkill();
|
|
806
897
|
appendLog(dir, "init", `Initialized brain for ${name} (${repoPath}) — stack: ${scan.stack}`);
|
|
807
898
|
|
|
808
899
|
console.log(`\n ${C.g}✓${C.r} Brain created for ${C.b}${name}${C.r} ${C.dim}→ ${dir}${C.r}`);
|
|
809
|
-
console.log(` ${C.dim}AGENTS.md · PROJECT.md ·
|
|
900
|
+
console.log(` ${C.dim}AGENTS.md · PROJECT.md · signals/ · rules/ · contexts/ · plans/ · comments/ · logs/${C.r}`);
|
|
810
901
|
console.log(` ${C.g}✓${C.r} Scanned repo (${scan.stack})`);
|
|
811
902
|
console.log(` ${C.g}✓${C.r} Wrote agent instructions ${C.dim}→ ./AGENTS.md${C.r}`);
|
|
903
|
+
await buildFastBrainForInit(cwd, dir, C);
|
|
812
904
|
console.log(`\n ${C.b}Feed it customer signal, then turn an issue into a plan:${C.r}\n`);
|
|
813
905
|
console.log(` ${C.g}npx scriptonia add${C.r} feedback.txt call.vtt ${C.dim}(or: npx scriptonia add "Issue #42: …")${C.r}`);
|
|
814
906
|
console.log(` ${C.g}npx scriptonia plan${C.r} "the issue you want to build"\n`);
|
|
815
907
|
}
|
|
816
908
|
|
|
909
|
+
async function buildFastBrainForInit(repoRoot, projectDir, C) {
|
|
910
|
+
console.log(` ${C.dim}Building the local fast brain…${C.r}`);
|
|
911
|
+
try {
|
|
912
|
+
const { buildFastBrainAfterInit } = await loadBrainBridge();
|
|
913
|
+
const result = await buildFastBrainAfterInit(repoRoot);
|
|
914
|
+
const projection = mirrorDeepRepoProjection(repoRoot, projectDir);
|
|
915
|
+
appendLog(projectDir, "brain", `READY generation ${result.generation} (${result.buildId}) after init`);
|
|
916
|
+
console.log(` ${C.g}✓${C.r} Local brain READY ${C.dim}generation ${result.generation}${C.r}`);
|
|
917
|
+
console.log(` ${C.g}✓${C.r} Full repository map ${C.dim}→ ${projection}${C.r}`);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
920
|
+
appendLog(projectDir, "brain", `Fast build failed after binding persisted — ${message}`);
|
|
921
|
+
console.error(` ${C.w}⚠${C.r} Fast brain build could not finish: ${message}`);
|
|
922
|
+
console.error(` ${C.dim}Your login and project binding are saved; any prior READY generation is unchanged.${C.r}`);
|
|
923
|
+
}
|
|
924
|
+
console.log(` ${C.b}Next:${C.r} ${C.g}npx scriptonia brain build --deep${C.r} ${C.dim}— full repository understanding (5–7 minutes)${C.r}`);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* The legacy project binding still exposes REPO.md to agents working outside
|
|
929
|
+
* the repository. Its contents must be the activated Deep Brain projection,
|
|
930
|
+
* never a second, shallow filesystem scan with different facts.
|
|
931
|
+
*/
|
|
932
|
+
function mirrorDeepRepoProjection(repoRoot, projectDir) {
|
|
933
|
+
const source = path.join(realPath(repoRoot), ".scriptonia", "REPO.md");
|
|
934
|
+
if (!fs.existsSync(source)) throw new Error("READY generation did not produce .scriptonia/REPO.md");
|
|
935
|
+
const markdown = fs.readFileSync(source, "utf8");
|
|
936
|
+
if (!markdown.startsWith("# Repository Brain") || markdown.includes("Tree (depth 3)") || /Stack:\s*unknown/i.test(markdown)) {
|
|
937
|
+
throw new Error("READY generation produced an invalid repository projection");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Pre-v0.6 project bindings used brain/; keep them readable without
|
|
941
|
+
// manufacturing a different repository map.
|
|
942
|
+
const targetDir = fs.existsSync(path.join(projectDir, "brain")) && !fs.existsSync(path.join(projectDir, "REPO.md"))
|
|
943
|
+
? path.join(projectDir, "brain")
|
|
944
|
+
: projectDir;
|
|
945
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
946
|
+
const target = path.join(targetDir, "REPO.md");
|
|
947
|
+
const temporary = `${target}.tmp-${process.pid}`;
|
|
948
|
+
fs.writeFileSync(temporary, markdown, { mode: 0o600 });
|
|
949
|
+
fs.chmodSync(temporary, 0o600);
|
|
950
|
+
fs.renameSync(temporary, target);
|
|
951
|
+
return target;
|
|
952
|
+
}
|
|
953
|
+
|
|
817
954
|
// ── sync ──────────────────────────────────────────────────────
|
|
818
955
|
async function sync() {
|
|
819
956
|
const cfg = activeCreds();
|
|
@@ -853,28 +990,36 @@ async function plan(args) {
|
|
|
853
990
|
|
|
854
991
|
if (flags.refresh) {
|
|
855
992
|
const res = await fetch(`${cfg.url}/api/plan?slug=${encodeURIComponent(flags.refresh)}`, { headers: { authorization: `Bearer ${cfg.token}` } });
|
|
856
|
-
const data = await res
|
|
857
|
-
if (!res.ok)
|
|
858
|
-
return writePlan(cfg, data, flags);
|
|
993
|
+
const data = await readResponseJson(res);
|
|
994
|
+
if (!res.ok) failPlanResponse("refresh", res, data);
|
|
995
|
+
return writePlan(cfg, data, flags, "refreshed");
|
|
859
996
|
}
|
|
860
997
|
|
|
861
998
|
const issue = args.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
862
999
|
if (!issue) fail('usage: npx scriptonia plan "<the issue>" [--out PLAN.md] [--refresh <slug>]');
|
|
863
1000
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1001
|
+
if (!cfg.inited) fail("run `npx scriptonia init` in this repository first — plans require a fresh local Deep Brain context pack");
|
|
1002
|
+
let evidence;
|
|
1003
|
+
try {
|
|
1004
|
+
const { buildFreshPlanEvidence } = await loadBrainBridge();
|
|
1005
|
+
evidence = buildFreshPlanEvidence(cfg.repoPath, issue);
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1008
|
+
}
|
|
867
1009
|
|
|
868
|
-
console.log(`\n ${C.dim}Planning "${issue}" — retrieving signal, checking decisions, drafting
|
|
1010
|
+
console.log(`\n ${C.dim}Planning "${issue}" — using fresh local brain evidence, retrieving signal, checking decisions, drafting…${C.r}`);
|
|
869
1011
|
const res = await fetch(`${cfg.url}/api/plan`, {
|
|
870
1012
|
method: "POST",
|
|
871
1013
|
headers: { authorization: `Bearer ${cfg.token}`, "content-type": "application/json" },
|
|
872
|
-
body: JSON.stringify({
|
|
1014
|
+
body: JSON.stringify({
|
|
1015
|
+
issue,
|
|
1016
|
+
context_pack: evidence.contextPack,
|
|
1017
|
+
repository_inventory: evidence.repositoryInventory,
|
|
1018
|
+
}),
|
|
873
1019
|
});
|
|
874
|
-
const data = await res
|
|
875
|
-
if (res.
|
|
876
|
-
|
|
877
|
-
writePlan(cfg, data, flags);
|
|
1020
|
+
const data = await readResponseJson(res);
|
|
1021
|
+
if (!res.ok) failPlanResponse("plan", res, data);
|
|
1022
|
+
await writePlan(cfg, data, flags, "fresh");
|
|
878
1023
|
}
|
|
879
1024
|
|
|
880
1025
|
// ── verify — PLAN.md + git diff → separate CI gates ──────────────────
|
|
@@ -883,7 +1028,7 @@ async function verify(args) {
|
|
|
883
1028
|
process.exitCode = result.exitCode;
|
|
884
1029
|
}
|
|
885
1030
|
|
|
886
|
-
function writePlan(cfg, data, flags) {
|
|
1031
|
+
async function writePlan(cfg, data, flags, flow) {
|
|
887
1032
|
const C = colors();
|
|
888
1033
|
const out = flags.out ?? "PLAN.md";
|
|
889
1034
|
fs.writeFileSync(path.resolve(process.cwd(), out), data.markdown);
|
|
@@ -895,8 +1040,25 @@ function writePlan(cfg, data, flags) {
|
|
|
895
1040
|
fs.writeFileSync(path.join(cfg.brainDir, "plan.md"), data.markdown);
|
|
896
1041
|
appendLog(cfg.dir, "plan", `${data.slug} v${data.version} — ${(data.sources ?? []).length} sources, ${data.contradictions ?? 0} unresolved`);
|
|
897
1042
|
}
|
|
1043
|
+
if (cfg.inited && cfg.repoPath) {
|
|
1044
|
+
try {
|
|
1045
|
+
const { recordPlanQualityEvidence } = await loadBrainBridge();
|
|
1046
|
+
recordPlanQualityEvidence(cfg.repoPath, {
|
|
1047
|
+
flow,
|
|
1048
|
+
slug: data.slug,
|
|
1049
|
+
version: data.version,
|
|
1050
|
+
quality: data.quality,
|
|
1051
|
+
brainGeneration: data.brain_generation,
|
|
1052
|
+
contextPackHash: data.context_pack_hash,
|
|
1053
|
+
});
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1056
|
+
console.error(` ${C.dim}Plan was written, but build-scoped quality evidence could not be recorded: ${message}${C.r}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
898
1059
|
console.log(`\n ${C.g}✓${C.r} Plan ${C.b}${data.slug}${C.r} ${C.dim}v${data.version}${C.r} → ${C.v}${out}${C.r}`);
|
|
899
1060
|
console.log(` ${C.dim}${(data.sources ?? []).length} cited signal(s)${data.contradictions ? `, ${data.contradictions} unresolved contradiction(s)` : ""}${C.r}`);
|
|
1061
|
+
for (const warning of planWarnings(data)) console.log(` ${C.w}⚠ ${warning}${C.r}`);
|
|
900
1062
|
console.log(`\n ${C.b}Hand it to your agent:${C.r} ${C.g}claude "execute PLAN.md"${C.r} ${C.dim}(or codex · opencode · hermes)${C.r}`);
|
|
901
1063
|
console.log(` ${C.dim}refine:${C.r} ${C.g}npx scriptonia comment plan/${data.slug} "…"${C.r} ${C.dim}(rewrites PLAN.md)${C.r}`);
|
|
902
1064
|
if (data.credits) {
|
|
@@ -914,7 +1076,6 @@ function scanRepo(cwd) {
|
|
|
914
1076
|
let pkg = null;
|
|
915
1077
|
try { pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")); } catch {}
|
|
916
1078
|
const stack = detectStack(cwd, pkg);
|
|
917
|
-
const tree = buildTree(cwd, 3);
|
|
918
1079
|
const name =
|
|
919
1080
|
(pkg && pkg.name && String(pkg.name).replace(/^@[^/]+\//, "")) ||
|
|
920
1081
|
gitRemoteName(cwd) ||
|
|
@@ -922,11 +1083,16 @@ function scanRepo(cwd) {
|
|
|
922
1083
|
const scripts = pkg && pkg.scripts ? Object.keys(pkg.scripts) : [];
|
|
923
1084
|
const deps = pkg ? Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }).slice(0, 20) : [];
|
|
924
1085
|
const entryPoints = findEntryPoints(cwd);
|
|
925
|
-
return { name, readmeHead, stack,
|
|
1086
|
+
return { name, readmeHead, stack, scripts, deps, entryPoints, pkgDescription: pkg?.description || "" };
|
|
926
1087
|
}
|
|
927
1088
|
|
|
928
1089
|
function detectStack(cwd, pkg) {
|
|
929
1090
|
const has = (f) => fs.existsSync(path.join(cwd, f));
|
|
1091
|
+
let pubspec = "";
|
|
1092
|
+
if (has("pubspec.yaml")) {
|
|
1093
|
+
try { pubspec = fs.readFileSync(path.join(cwd, "pubspec.yaml"), "utf8"); } catch {}
|
|
1094
|
+
if (/(^|\n)\s*flutter\s*:|sdk\s*:\s*flutter\b|flutter_test\s*:/m.test(pubspec)) return "Flutter/Dart";
|
|
1095
|
+
}
|
|
930
1096
|
if (pkg) {
|
|
931
1097
|
const d = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
932
1098
|
if (d.next) return "Next.js / TypeScript";
|
|
@@ -936,6 +1102,7 @@ function detectStack(cwd, pkg) {
|
|
|
936
1102
|
}
|
|
937
1103
|
if (has("go.mod")) return "Go";
|
|
938
1104
|
if (has("Cargo.toml")) return "Rust";
|
|
1105
|
+
if (pubspec) return "Dart";
|
|
939
1106
|
if (has("requirements.txt") || has("pyproject.toml")) return "Python";
|
|
940
1107
|
if (has("Gemfile")) return "Ruby";
|
|
941
1108
|
if (has("pom.xml") || has("build.gradle")) return "Java/JVM";
|
|
@@ -951,24 +1118,6 @@ function findEntryPoints(cwd) {
|
|
|
951
1118
|
return cands.filter((c) => fs.existsSync(path.join(cwd, c)));
|
|
952
1119
|
}
|
|
953
1120
|
|
|
954
|
-
function buildTree(cwd, maxDepth) {
|
|
955
|
-
const IGNORE = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "coverage", ".vercel", "out", "__pycache__", ".venv", "target", "vendor"]);
|
|
956
|
-
const lines = [];
|
|
957
|
-
const walk = (d, depth, prefix) => {
|
|
958
|
-
if (depth > maxDepth) return;
|
|
959
|
-
let entries;
|
|
960
|
-
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
961
|
-
entries = entries.filter((e) => !e.name.startsWith(".") || e.name === ".env.example").filter((e) => !IGNORE.has(e.name));
|
|
962
|
-
entries.sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1));
|
|
963
|
-
for (const e of entries.slice(0, 40)) {
|
|
964
|
-
lines.push(`${prefix}${e.name}${e.isDirectory() ? "/" : ""}`);
|
|
965
|
-
if (e.isDirectory()) walk(path.join(d, e.name), depth + 1, prefix + " ");
|
|
966
|
-
}
|
|
967
|
-
};
|
|
968
|
-
walk(cwd, 1, "");
|
|
969
|
-
return lines.slice(0, 200).join("\n");
|
|
970
|
-
}
|
|
971
|
-
|
|
972
1121
|
function gitRemoteName(cwd) {
|
|
973
1122
|
try {
|
|
974
1123
|
const url = execSync("git remote get-url origin", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
@@ -1003,15 +1152,6 @@ function writeProjectMd(dir, scan, pitch) {
|
|
|
1003
1152
|
fs.writeFileSync(path.join(dir, "PROJECT.md"), md);
|
|
1004
1153
|
}
|
|
1005
1154
|
|
|
1006
|
-
function writeRepoMd(dir, scan) {
|
|
1007
|
-
// v0.6 flat layout; keep writing into brain/ for pre-0.6 projects.
|
|
1008
|
-
const target = fs.existsSync(path.join(dir, "brain")) && !fs.existsSync(path.join(dir, "REPO.md"))
|
|
1009
|
-
? path.join(dir, "brain")
|
|
1010
|
-
: dir;
|
|
1011
|
-
fs.mkdirSync(target, { recursive: true });
|
|
1012
|
-
fs.writeFileSync(path.join(target, "REPO.md"), buildRepoMd(scan));
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
1155
|
/**
|
|
1016
1156
|
* AGENTS.md — the brain's master contract. What an agent must respect when
|
|
1017
1157
|
* acting on this project: identity, the Scriptonia workflow, and the
|
|
@@ -1030,17 +1170,24 @@ function writeAgentsBrainMd(dir, meta, decisions) {
|
|
|
1030
1170
|
"",
|
|
1031
1171
|
"## 2. Workflow — non-negotiable",
|
|
1032
1172
|
"",
|
|
1033
|
-
|
|
1173
|
+
"1. Check repository evidence with `npx scriptonia brain status`. If it is",
|
|
1174
|
+
" missing or stale, run `npx scriptonia brain build --fast --offline`.",
|
|
1175
|
+
'2. Before building any user-facing change, run `npx scriptonia plan "<the issue>"`.',
|
|
1034
1176
|
" It writes PLAN.md, grounded in real customer signal and checked against",
|
|
1035
|
-
" the rules below.",
|
|
1036
|
-
"
|
|
1177
|
+
" the rules below and the active local Deep Brain generation.",
|
|
1178
|
+
"3. Use `npx scriptonia why <file>` to inspect the exact facts, dependents,",
|
|
1179
|
+
" and tests behind a planned file before editing it.",
|
|
1180
|
+
"4. Execute PLAN.md **exactly**. Its **Non-goals** section is a fence — do not",
|
|
1037
1181
|
" build past it.",
|
|
1038
|
-
"
|
|
1182
|
+
"5. Any constraint marked **⚠ UNRESOLVED** is an approval gate: STOP and get a",
|
|
1039
1183
|
' human comment (`npx scriptonia comment plan/<slug> "…"`) before writing code.',
|
|
1040
|
-
"
|
|
1041
|
-
"
|
|
1184
|
+
"6. Cite PLAN.md's `sources` signal ids in your PR description.",
|
|
1185
|
+
"7. After implementation, run `npx scriptonia verify`; fix every BLOCK",
|
|
1042
1186
|
" before merge and preserve the generated `.scriptonia/evals/*.yaml` case.",
|
|
1043
1187
|
"",
|
|
1188
|
+
"After setup, run `npx scriptonia brain build --deep` for the enriched",
|
|
1189
|
+
"repository model (allow 5–7 minutes).",
|
|
1190
|
+
"",
|
|
1044
1191
|
"## 3. Non-negotiable constraints (from the decisions corpus)",
|
|
1045
1192
|
"",
|
|
1046
1193
|
];
|
|
@@ -1148,22 +1295,6 @@ function appendLog(dir, name, msg) {
|
|
|
1148
1295
|
} catch { /* audit trail is best-effort */ }
|
|
1149
1296
|
}
|
|
1150
1297
|
|
|
1151
|
-
function buildRepoMd(scan) {
|
|
1152
|
-
return [
|
|
1153
|
-
`# Repo map — ${scan.name}`,
|
|
1154
|
-
"",
|
|
1155
|
-
`**Stack:** ${scan.stack}`,
|
|
1156
|
-
scan.entryPoints.length ? `**Entry points:** ${scan.entryPoints.join(", ")}` : "",
|
|
1157
|
-
"",
|
|
1158
|
-
"## Tree (depth 3)",
|
|
1159
|
-
"```",
|
|
1160
|
-
scan.tree,
|
|
1161
|
-
"```",
|
|
1162
|
-
"",
|
|
1163
|
-
"*When you name files in a plan, use paths from this tree. Refresh with `npx scriptonia init --force`.*",
|
|
1164
|
-
].filter(Boolean).join("\n");
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
1298
|
function renderContextFile(c) {
|
|
1168
1299
|
const L = [`# ${c.title}`, "", `> **${c.type}** · ${c.segment} · strength ${c.strength}/10 · v${c.version}`, "", c.summary, ""];
|
|
1169
1300
|
if (c.sources?.length) { L.push("## Sources"); for (const s of c.sources) L.push(`- \`${s.id.slice(0, 8)}\` ${s.source}${s.ref ? ` — ${s.ref}` : ""}`); L.push(""); }
|
|
@@ -1182,8 +1313,12 @@ function injectAgentsMd(cwd) {
|
|
|
1182
1313
|
This repo has a product brain (customer signal + prior decisions). Before building
|
|
1183
1314
|
any user-facing change:
|
|
1184
1315
|
|
|
1316
|
+
- \`npx scriptonia brain status\` → confirms the local Deep Brain is READY and fresh.
|
|
1317
|
+
If missing or stale, run \`npx scriptonia brain build --fast --offline\`.
|
|
1185
1318
|
- \`npx scriptonia plan "<the issue>"\` → writes **PLAN.md** here. Execute PLAN.md
|
|
1186
1319
|
exactly; respect its **Non-goals**; cite its \`sources\` ids in your PR.
|
|
1320
|
+
- \`npx scriptonia why <file>\` → shows the facts, dependents, and related tests
|
|
1321
|
+
behind a planned file before you edit it.
|
|
1187
1322
|
- \`npx scriptonia query "<topic>"\` → JSON: what customers asked for, contradictions,
|
|
1188
1323
|
edge cases.
|
|
1189
1324
|
- If a plan constraint is **UNRESOLVED**, stop and ask the human. If a step
|
|
@@ -1191,6 +1326,8 @@ any user-facing change:
|
|
|
1191
1326
|
and re-read PLAN.md.
|
|
1192
1327
|
- After implementation, run \`npx scriptonia verify\`. Fix every BLOCK before
|
|
1193
1328
|
merge; keep the generated \`.scriptonia/evals/*.yaml\` regression case.
|
|
1329
|
+
- After setup, run \`npx scriptonia brain build --deep\` for the enriched repository
|
|
1330
|
+
model (allow 5–7 minutes).
|
|
1194
1331
|
${END}`;
|
|
1195
1332
|
|
|
1196
1333
|
let existing = "";
|
|
@@ -1205,6 +1342,36 @@ ${END}`;
|
|
|
1205
1342
|
}
|
|
1206
1343
|
}
|
|
1207
1344
|
|
|
1345
|
+
// The database, object store, and regenerated projections are local artifacts.
|
|
1346
|
+
// Keep policies/evals outside these rules so teams can commit their shared
|
|
1347
|
+
// product constraints and regression cases.
|
|
1348
|
+
function injectBrainGitignore(cwd) {
|
|
1349
|
+
const file = path.join(cwd, ".gitignore");
|
|
1350
|
+
const required = [
|
|
1351
|
+
".scriptonia/brain.db*",
|
|
1352
|
+
".scriptonia/artifacts/",
|
|
1353
|
+
".scriptonia/objects/",
|
|
1354
|
+
".scriptonia/staging/",
|
|
1355
|
+
".scriptonia/REPO.md",
|
|
1356
|
+
".scriptonia/GRAPH.md",
|
|
1357
|
+
".scriptonia/ARCHITECTURE.md",
|
|
1358
|
+
".scriptonia/PATTERNS.md",
|
|
1359
|
+
".scriptonia/DECISION_CANDIDATES.md",
|
|
1360
|
+
".scriptonia/brain-coverage.json",
|
|
1361
|
+
".scriptonia/result*.json",
|
|
1362
|
+
".scriptonia/result*.sarif",
|
|
1363
|
+
];
|
|
1364
|
+
let existing = "";
|
|
1365
|
+
try { existing = fs.readFileSync(file, "utf8"); } catch {}
|
|
1366
|
+
|
|
1367
|
+
const present = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
1368
|
+
const missing = required.filter((line) => !present.has(line));
|
|
1369
|
+
if (!missing.length) return;
|
|
1370
|
+
|
|
1371
|
+
const prefix = existing.length && !existing.endsWith("\n") ? `${existing}\n` : existing;
|
|
1372
|
+
fs.writeFileSync(file, `${prefix}${missing.join("\n")}\n`);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1208
1375
|
// ── misc ──────────────────────────────────────────────────────
|
|
1209
1376
|
function slugify(s) {
|
|
1210
1377
|
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60) || "project";
|
package/bin/verify.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export async function runVerification(args, { cfg, cwd = process.cwd(), fetchImp
|
|
|
27
27
|
});
|
|
28
28
|
const repositoryChecks = flags["skip-checks"] ? skippedRepositoryGate() : runRepositoryChecks(cwd, files);
|
|
29
29
|
const unresolvedConstraints = buildUnresolvedGate(plan);
|
|
30
|
-
const testCoverage = buildTestCoverageGate(files);
|
|
30
|
+
const testCoverage = buildTestCoverageGate(files, cwd);
|
|
31
31
|
|
|
32
32
|
const response = await semanticPromise;
|
|
33
33
|
const semantic = await response.json().catch(() => ({}));
|
|
@@ -82,9 +82,9 @@ export function buildUnresolvedGate(plan) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function buildTestCoverageGate(files) {
|
|
85
|
+
export function buildTestCoverageGate(files, cwd) {
|
|
86
86
|
const tests = files.filter(isTestFile);
|
|
87
|
-
const behavior = files.filter((file) => isSourceFile(file
|
|
87
|
+
const behavior = files.filter((file) => isSourceFile(file, cwd));
|
|
88
88
|
if (tests.length) return gate("PASS", {
|
|
89
89
|
evidence: tests,
|
|
90
90
|
plan_criterion: "Add or update tests for changed product behavior.",
|
|
@@ -205,12 +205,46 @@ function diffFiles(diff) {
|
|
|
205
205
|
return [...new Set([...diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)].map((match) => match[2]))];
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
209
|
+
".c", ".h", ".cc", ".cp", ".cpp", ".cxx", ".hh", ".hpp", ".hxx", ".inc", ".m", ".mm", ".cs",
|
|
210
|
+
".css", ".scss", ".sass", ".less", ".dart", ".ex", ".exs", ".go", ".graphql", ".gql", ".html", ".htm",
|
|
211
|
+
".java", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc", ".kt", ".kts", ".lua", ".pl", ".pm", ".php",
|
|
212
|
+
".proto", ".py", ".pyi", ".rb", ".rs", ".scala", ".sh", ".bash", ".zsh", ".fish", ".sql", ".bzl",
|
|
213
|
+
".bazel", ".star", ".swift", ".toml", ".ts", ".tsx", ".mts", ".cts", ".vue", ".xml", ".plist", ".yaml", ".yml",
|
|
214
|
+
]);
|
|
215
|
+
const STARLARK_BASENAMES = new Set(["BUILD", "BUILD.bazel", "WORKSPACE", "WORKSPACE.bzlmod", "MODULE.bazel"]);
|
|
216
|
+
const CONFIG_BASENAMES = new Set([
|
|
217
|
+
"package.json", "package-lock.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "yarn.lock", "pubspec.yaml", "pubspec.lock",
|
|
218
|
+
"Cargo.toml", "go.mod", "go.sum", "pom.xml", "composer.json", "composer.lock", "pyproject.toml", "requirements.txt",
|
|
219
|
+
"tsconfig.json", "analysis_options.yaml", "CMakeLists.txt", "Makefile", "Dockerfile", "CODEOWNERS",
|
|
220
|
+
]);
|
|
221
|
+
const TEST_EXTENSION = "(?:c|cc|cp|cpp|cxx|h|hh|hpp|hxx|inc|m|mm|cs|dart|ex|exs|go|java|js|jsx|mjs|cjs|kt|kts|lua|pl|pm|php|proto|py|pyi|rb|rs|scala|sh|bash|zsh|fish|bzl|bazel|star|swift|ts|tsx|mts|cts|vue)";
|
|
222
|
+
const TEST_BASENAME = new RegExp(`(?:^|/)(?:test_[^/]+|[^/]+_(?:test|spec)|[^/]+\\.(?:test|spec))\\.${TEST_EXTENSION}$`, "i");
|
|
223
|
+
|
|
208
224
|
function isTestFile(file) {
|
|
209
|
-
|
|
225
|
+
const normalized = file.replaceAll("\\", "/");
|
|
226
|
+
return /(^|\/)(?:__tests__|tests?|spec|integration_test)(?:\/|$)/i.test(normalized) ||
|
|
227
|
+
TEST_BASENAME.test(normalized) ||
|
|
228
|
+
/(?:^|\/)[^/]+(?:Test|Tests|Spec)\.(?:cs|java|kt|kts|scala|swift)$/.test(normalized);
|
|
210
229
|
}
|
|
211
230
|
|
|
212
|
-
function isSourceFile(file) {
|
|
213
|
-
|
|
231
|
+
function isSourceFile(file, cwd) {
|
|
232
|
+
const normalized = file.replaceAll("\\", "/");
|
|
233
|
+
if (isTestFile(normalized) || /(?:^|\/)(?:third_party|vendor|external|docs?|adr|changelogs?)(?:\/|$)/i.test(normalized)) return false;
|
|
234
|
+
const base = path.posix.basename(normalized);
|
|
235
|
+
if (/(?:^|\/)generated(?:\/|$)/i.test(normalized) || /\.(?:g|freezed)\.dart$|\.pb\.go$|\.generated\.[^.]+$|\.min\.(?:js|css)$/i.test(base)) return false;
|
|
236
|
+
if (STARLARK_BASENAMES.has(base)) return true;
|
|
237
|
+
if (CONFIG_BASENAMES.has(base) || /\.(?:lock|config|conf|ini|properties)$/i.test(base) ||
|
|
238
|
+
/(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(normalized) ||
|
|
239
|
+
/^(?:Dockerfile(?:\..+)?|requirements(?:[-_.][^/]*)?\.txt|tsconfig(?:\.[^/]+)?\.json|(?:docker-)?compose(?:\.[^/]+)?\.ya?ml)$/i.test(base)) return false;
|
|
240
|
+
if (SOURCE_EXTENSIONS.has(path.posix.extname(base).toLowerCase())) return true;
|
|
241
|
+
if (!cwd) return false;
|
|
242
|
+
try {
|
|
243
|
+
const firstLine = fs.readFileSync(path.join(cwd, normalized), "utf8").split(/\r?\n/, 1)[0] ?? "";
|
|
244
|
+
return /^#!.*\b(?:python3?|bash|sh|zsh|fish|node|ruby)\b/.test(firstLine);
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
214
248
|
}
|
|
215
249
|
|
|
216
250
|
function gate(verdict, check) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AST_WORKER_PROTOCOL_VERSION,
|
|
4
|
+
isAstWorkerRequest,
|
|
5
|
+
parseSource
|
|
6
|
+
} from "./chunk-6TLQP3LV.js";
|
|
7
|
+
|
|
8
|
+
// ../packages/brain-ast/src/worker-runtime.ts
|
|
9
|
+
import { isMainThread, parentPort, threadId } from "worker_threads";
|
|
10
|
+
function serializeError(error) {
|
|
11
|
+
if (!(error instanceof Error)) return { name: "Error", message: String(error) };
|
|
12
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
13
|
+
return {
|
|
14
|
+
name: error.name,
|
|
15
|
+
message: error.message,
|
|
16
|
+
...error.stack ? { stack: error.stack } : {},
|
|
17
|
+
...code ? { code } : {}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function startAstWorkerThread() {
|
|
21
|
+
if (isMainThread || !parentPort) {
|
|
22
|
+
throw new Error("the Deep Brain AST worker entry must run inside a worker thread");
|
|
23
|
+
}
|
|
24
|
+
const port = parentPort;
|
|
25
|
+
port.on("message", async (request) => {
|
|
26
|
+
if (!isAstWorkerRequest(request)) {
|
|
27
|
+
const response = {
|
|
28
|
+
protocolVersion: AST_WORKER_PROTOCOL_VERSION,
|
|
29
|
+
type: "error",
|
|
30
|
+
jobId: request && typeof request === "object" && "jobId" in request && typeof request.jobId === "number" ? request.jobId : 0,
|
|
31
|
+
threadId,
|
|
32
|
+
error: { name: "AstWorkerProtocolError", message: "received an invalid AST worker request" }
|
|
33
|
+
};
|
|
34
|
+
port.postMessage(response);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const result = await parseSource(request.input);
|
|
39
|
+
const response = {
|
|
40
|
+
protocolVersion: AST_WORKER_PROTOCOL_VERSION,
|
|
41
|
+
type: "result",
|
|
42
|
+
jobId: request.jobId,
|
|
43
|
+
threadId,
|
|
44
|
+
result
|
|
45
|
+
};
|
|
46
|
+
port.postMessage(response);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const response = {
|
|
49
|
+
protocolVersion: AST_WORKER_PROTOCOL_VERSION,
|
|
50
|
+
type: "error",
|
|
51
|
+
jobId: request.jobId,
|
|
52
|
+
threadId,
|
|
53
|
+
error: serializeError(error)
|
|
54
|
+
};
|
|
55
|
+
port.postMessage(response);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/brain-ast-worker.ts
|
|
61
|
+
startAstWorkerThread();
|
|
62
|
+
//# sourceMappingURL=brain-ast-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../packages/brain-ast/src/worker-runtime.ts","../src/brain-ast-worker.ts"],"sourcesContent":["import { isMainThread, parentPort, threadId } from \"node:worker_threads\";\nimport { parseSource } from \"./parse-source\";\nimport {\n AST_WORKER_PROTOCOL_VERSION,\n isAstWorkerRequest,\n type AstWorkerResponse,\n type SerializedWorkerError,\n} from \"./worker-protocol\";\n\nfunction serializeError(error: unknown): SerializedWorkerError {\n if (!(error instanceof Error)) return { name: \"Error\", message: String(error) };\n const code = \"code\" in error && typeof error.code === \"string\" ? error.code : undefined;\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {}),\n ...(code ? { code } : {}),\n };\n}\n\n/** Starts the dedicated AST worker message loop. This is intentionally not run\n * when the module is imported so the CLI can bundle it as a separate entry. */\nexport function startAstWorkerThread(): void {\n if (isMainThread || !parentPort) {\n throw new Error(\"the Deep Brain AST worker entry must run inside a worker thread\");\n }\n const port = parentPort;\n port.on(\"message\", async (request: unknown) => {\n if (!isAstWorkerRequest(request)) {\n const response: AstWorkerResponse = {\n protocolVersion: AST_WORKER_PROTOCOL_VERSION,\n type: \"error\",\n jobId: request && typeof request === \"object\" && \"jobId\" in request && typeof request.jobId === \"number\" ? request.jobId : 0,\n threadId,\n error: { name: \"AstWorkerProtocolError\", message: \"received an invalid AST worker request\" },\n };\n port.postMessage(response);\n return;\n }\n try {\n const result = await parseSource(request.input);\n const response: AstWorkerResponse = {\n protocolVersion: AST_WORKER_PROTOCOL_VERSION,\n type: \"result\",\n jobId: request.jobId,\n threadId,\n result,\n };\n port.postMessage(response);\n } catch (error) {\n const response: AstWorkerResponse = {\n protocolVersion: AST_WORKER_PROTOCOL_VERSION,\n type: \"error\",\n jobId: request.jobId,\n threadId,\n error: serializeError(error),\n };\n port.postMessage(response);\n }\n });\n}\n","import { startAstWorkerThread } from \"@scriptonia/brain-ast/worker\";\n\nstartAstWorkerThread();\n"],"mappings":";;;;;;;;AAAA,SAAS,cAAc,YAAY,gBAAgB;AASnD,SAAS,eAAe,OAAuC;AAC7D,MAAI,EAAE,iBAAiB,OAAQ,QAAO,EAAE,MAAM,SAAS,SAAS,OAAO,KAAK,EAAE;AAC9E,QAAM,OAAO,UAAU,SAAS,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAC9E,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,SAAS,MAAM;AAAA,IACf,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,IAC5C,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,EACzB;AACF;AAIO,SAAS,uBAA6B;AAC3C,MAAI,gBAAgB,CAAC,YAAY;AAC/B,UAAM,IAAI,MAAM,iEAAiE;AAAA,EACnF;AACA,QAAM,OAAO;AACb,OAAK,GAAG,WAAW,OAAO,YAAqB;AAC7C,QAAI,CAAC,mBAAmB,OAAO,GAAG;AAChC,YAAM,WAA8B;AAAA,QAClC,iBAAiB;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,WAAW,OAAO,YAAY,YAAY,WAAW,WAAW,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,QAC3H;AAAA,QACA,OAAO,EAAE,MAAM,0BAA0B,SAAS,yCAAyC;AAAA,MAC7F;AACA,WAAK,YAAY,QAAQ;AACzB;AAAA,IACF;AACA,QAAI;AACF,YAAM,SAAS,MAAM,YAAY,QAAQ,KAAK;AAC9C,YAAM,WAA8B;AAAA,QAClC,iBAAiB;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,QAAQ;AAAA,QACf;AAAA,QACA;AAAA,MACF;AACA,WAAK,YAAY,QAAQ;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,WAA8B;AAAA,QAClC,iBAAiB;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,OAAO,eAAe,KAAK;AAAA,MAC7B;AACA,WAAK,YAAY,QAAQ;AAAA,IAC3B;AAAA,EACF,CAAC;AACH;;;AC1DA,qBAAqB;","names":[]}
|