@orangepro/orangepro-mcp 0.1.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,144 @@
1
+ import { BUCKET_LABEL } from "./buckets.js";
2
+ export const PROMPT_VERSION = "orangepro.local.testgen.v2";
3
+ /** Generic local proof-kit guidance per scenario bucket. */
4
+ const BUCKET_GUIDANCE = {
5
+ happy_path: "the primary expected-success path — valid inputs produce the correct outcome.",
6
+ validation_error: "invalid or missing input is rejected with the right error/validation behavior.",
7
+ edge_case: "a boundary or unusual state (empty/null, limits, missing data, concurrency/timeouts).",
8
+ integration_flow: "a flow across modules/services/routes — exercise the real interaction, not a unit in isolation.",
9
+ security_privacy: "an access-control / privacy concern (auth, permissions, tokens, sessions, roles).",
10
+ regression: "a previously-broken or known-risk behavior stays correct (guard against regressions)."
11
+ };
12
+ export function buildSystemPrompt() {
13
+ return [
14
+ "You are OrangePro's local test-generation assistant.",
15
+ "Generate exactly one focused, concrete test for the target behavior.",
16
+ "Rules:",
17
+ "- Name the behavior or requirement under test.",
18
+ "- Target the given framework / test layer.",
19
+ "- EXISTING TESTS listed in the evidence are coverage that ALREADY exists. Generate a",
20
+ " scenario that is NOT among them — never re-derive or restate an existing test.",
21
+ "- Include at least one assertion tied to a stated acceptance criterion or expected behavior.",
22
+ "- Do NOT produce smoke-only or existence-only tests unless that is the actual requirement.",
23
+ "- Do NOT copy SOURCE EXCERPT lines verbatim. Use them to understand behavior, then write original test code.",
24
+ "- Write COMPLETE import statements for everything the test uses (framework, test helpers,",
25
+ " the module under test). Module paths and exported names are metadata — imports are",
26
+ " expected and exempt from any copying concern. Prefer the SUBJECT IMPORTS provided.",
27
+ "- If SUBJECT IMPORTS are provided, reuse those repo-proven imports instead of inventing",
28
+ " new module paths or switching test frameworks.",
29
+ "- Start the test body with two comment lines using the target language's comment syntax:",
30
+ " `// Bucket: <bucket>` and `// Refs: <evidence refs>` for TS/JS/Go/Java;",
31
+ " `# Bucket: <bucket>` and `# Refs: <evidence refs>` for Python.",
32
+ "- Framework format rules:",
33
+ " - pytest: output a valid Python file with pytest-style `def test_...` functions and `assert` statements.",
34
+ " - Go: output a same-package `_test.go` body. Include `package <same package>`, `import \"testing\"`,",
35
+ " and `func Test...(t *testing.T)`. Avoid third-party or module-path imports; use stdlib-only unless",
36
+ " the evidence gives an existing same-package helper.",
37
+ " - Java/JUnit: output a complete `.java` file, not a method fragment. Include a `class <Name>Test { ... }`,",
38
+ " the requested JUnit version's `@Test` import, and a JUnit assertion.",
39
+ " - TS/JS: output valid framework code for the named framework and use complete imports.",
40
+ "- Prefer specificity grounded in the provided evidence over generic phrasing.",
41
+ "- Treat WEAK CONTEXT as low-confidence hints, not facts.",
42
+ "- Output ONLY the test code — no Markdown code fences, no prose, no explanation."
43
+ ].join("\n");
44
+ }
45
+ export function buildGroundedUserPrompt(ctx, bucket) {
46
+ const lines = [];
47
+ lines.push(`BEHAVIOR: ${ctx.behavior_title}`);
48
+ if (ctx.description)
49
+ lines.push(`DESCRIPTION: ${ctx.description}`);
50
+ if (ctx.actors.length)
51
+ lines.push(`ACTORS: ${ctx.actors.join(", ")}`);
52
+ if (ctx.priority)
53
+ lines.push(`PRIORITY: ${ctx.priority}`);
54
+ lines.push(`FRAMEWORK: ${ctx.framework}`);
55
+ lines.push(`TEST LAYER: ${ctx.test_layer}`);
56
+ if (ctx.acceptance_criteria.length) {
57
+ lines.push("ACCEPTANCE CRITERIA:");
58
+ for (const ac of ctx.acceptance_criteria)
59
+ lines.push(`- ${ac}`);
60
+ }
61
+ if (ctx.workflow_steps.length) {
62
+ lines.push("WORKFLOW STEPS:");
63
+ for (const step of ctx.workflow_steps)
64
+ lines.push(`- ${step}`);
65
+ }
66
+ if (ctx.code_context.length) {
67
+ lines.push("CODE CONTEXT:");
68
+ for (const c of ctx.code_context)
69
+ lines.push(`- ${c}`);
70
+ }
71
+ if (ctx.existing_tests.length) {
72
+ lines.push("EXISTING TESTS (already covered — do NOT regenerate these scenarios):");
73
+ for (const t of ctx.existing_tests)
74
+ lines.push(`- ${t}`);
75
+ }
76
+ if (ctx.subject_imports.length) {
77
+ lines.push("SUBJECT IMPORTS (working import lines from the repo's own test for this area — reuse them):");
78
+ for (const imp of ctx.subject_imports)
79
+ lines.push(imp);
80
+ }
81
+ if (ctx.source_excerpts.length) {
82
+ lines.push("SOURCE EXCERPTS:");
83
+ lines.push("Use these for understanding only; do not copy their lines verbatim into the test body.");
84
+ for (const e of ctx.source_excerpts)
85
+ lines.push(e);
86
+ }
87
+ if (ctx.weak_context.length) {
88
+ lines.push("WEAK CONTEXT:");
89
+ for (const w of ctx.weak_context)
90
+ lines.push(`- ${w}`);
91
+ }
92
+ const fw = ctx.framework.toLowerCase();
93
+ lines.push("FRAMEWORK-SPECIFIC RUNNABILITY RULES:");
94
+ if (fw.includes("pytest") || fw.includes("python")) {
95
+ lines.push("- Use Python comments (`# Bucket`, `# Refs`), not `//` comments.");
96
+ lines.push("- Emit a pytest file with at least one `def test_...` function and a real `assert`.");
97
+ }
98
+ else if (fw.includes("go")) {
99
+ lines.push("- Emit same-package Go test code only: `package ...`, `import \"testing\"`, and `func Test...(t *testing.T)`.");
100
+ lines.push("- Do not import third-party or module-path packages such as `github.com/...`; prefer stdlib-only tests.");
101
+ lines.push("- Do not use testify/assert/require unless an existing same-package helper in the evidence clearly requires it.");
102
+ }
103
+ else if (fw.includes("junit") || fw.includes("java")) {
104
+ if (fw.includes("junit4")) {
105
+ lines.push("- Emit JUnit 4 code with `import org.junit.Test;` and `import static org.junit.Assert.*;`.");
106
+ }
107
+ else {
108
+ lines.push("- Emit JUnit 5 code with `import org.junit.jupiter.api.Test;` and a static JUnit assertion import.");
109
+ }
110
+ lines.push("- Return a complete `.java` file with a test class, not a bare method fragment.");
111
+ lines.push("- Include at least one `@Test` method with a real assertion.");
112
+ }
113
+ else {
114
+ lines.push(`- Emit ${ctx.framework} code only. Do not switch to another test framework or import its package.`);
115
+ if (fw.includes("ava")) {
116
+ lines.push("- For AVA, use `import test from \"ava\";` and assertions such as `t.is`, `t.deepEqual`, `t.true`, or `t.throws`.");
117
+ }
118
+ lines.push("- If SUBJECT IMPORTS are listed, reuse those exact repo-proven imports for the module under test.");
119
+ lines.push("- Do not invent module paths, selectors, helpers, or framework packages not shown in the evidence.");
120
+ lines.push("- Do not paste SOURCE EXCERPT statements into the test. Create new test data and assertions from the behavior.");
121
+ lines.push("- Emit one complete test file with complete imports and at least one assertion.");
122
+ }
123
+ lines.push("");
124
+ if (bucket) {
125
+ lines.push(`SCENARIO FOCUS (${BUCKET_LABEL[bucket]}): ${BUCKET_GUIDANCE[bucket]}`);
126
+ lines.push(`Write the single best ${BUCKET_LABEL[bucket]} test for this behavior, grounded ONLY in the evidence above.`);
127
+ lines.push("Label the test with this bucket. Cite the evidence anchors / source refs you used.");
128
+ lines.push("If you rely on weak/candidate evidence, disclose it. Do not invent APIs, modules, selectors, or");
129
+ lines.push("business rules not supported by the evidence. Do not produce a generic smoke test.");
130
+ }
131
+ else {
132
+ lines.push("Produce the single best test for this behavior.");
133
+ }
134
+ return lines.join("\n");
135
+ }
136
+ /** Raw-prompt baseline (internal comparison only) — no graph grounding. */
137
+ export function buildRawUserPrompt(behaviorTitle, framework) {
138
+ return [
139
+ `BEHAVIOR: ${behaviorTitle}`,
140
+ `FRAMEWORK: ${framework}`,
141
+ "",
142
+ "Write a test for this."
143
+ ].join("\n");
144
+ }
@@ -0,0 +1,438 @@
1
+ export const PROMPT_VERSION_V5 = "orangepro.local.testgen.v5";
2
+ const STANDARD_CONCERNS = new Set([
3
+ "contract",
4
+ "state_lifecycle",
5
+ "boundary_limits",
6
+ "integration_flow",
7
+ "failure_recovery",
8
+ "authorization_safety",
9
+ "concurrency_ordering",
10
+ "data_integrity"
11
+ ]);
12
+ export const TECHNIQUE_DESC = {
13
+ happy_path_validation: "Exercise the primary success path with valid input. Assert correct output AND all expected side effects.",
14
+ equivalence_partitioning: "Pick ONE representative from each input class. Assert consistent behavior within the class.",
15
+ boundary_value_analysis: "Test at exact boundaries (0, 1, max, max+1). Include both last valid and first invalid values.",
16
+ state_transition: "Set up initial state, trigger transition, assert new state AND that invalid transitions are rejected.",
17
+ decision_table: "Enumerate condition combinations and verify each produces the correct action/output.",
18
+ error_guessing: "Target the most likely production failure. Simulate the condition and assert graceful handling.",
19
+ pairwise_combination: "Combine 2+ independent parameters. Use minimal pairwise set covering all 2-way interactions.",
20
+ contract_verification: "Assert return types, status codes, error shapes, response schemas. Use type guards or validators.",
21
+ integration_chain: "Exercise the real service chain end-to-end. Mock only external I/O, not internal services. Assert final state reflects all intermediate transformations.",
22
+ data_flow_analysis: "Provide input at entry point, trace through transformations, assert final output reflects all steps.",
23
+ concurrency_interleaving: "Simulate parallel execution. Assert no races, no double-processing, no lost updates.",
24
+ idempotency_check: "Call operation twice with identical input. Assert same result, no duplicate side effects.",
25
+ rollback_recovery: "Trigger failure mid-chain. Assert upstream state is consistent. No orphaned records.",
26
+ permission_matrix: "Test with each actor type. Assert authorized succeed, unauthorized get 403/401, no data leakage.",
27
+ input_sanitization: "Provide malicious input. Assert rejected or sanitized, no side effects.",
28
+ chaos_injection: "Simulate infrastructure failure. Assert graceful degradation."
29
+ };
30
+ export function getFrameworkRules(framework) {
31
+ const fw = framework.toLowerCase();
32
+ if (fw.includes("pytest") || fw.includes("python")) {
33
+ return "Python: `def test_...` with `assert`. Use `# Concern:` and `# Technique:` comments.";
34
+ }
35
+ if (fw.includes("go")) {
36
+ return "Go: same-package `_test.go`, `func Test...(t *testing.T)`. Stdlib preferred.";
37
+ }
38
+ if (fw.includes("junit4") || fw.includes("java4")) {
39
+ return "JUnit 4: `import org.junit.Test;` + `import static org.junit.Assert.*;`. Complete .java file.";
40
+ }
41
+ if (fw.includes("junit") || fw.includes("java")) {
42
+ return "JUnit 5: `import org.junit.jupiter.api.Test;` with assertion imports. Complete .java file.";
43
+ }
44
+ if (fw.includes("ava")) {
45
+ return "AVA: `import test from \"ava\";` with `t.is`, `t.deepEqual`, `t.throws`.";
46
+ }
47
+ return `${framework}: use this framework only. Reuse SUBJECT IMPORTS. Do not invent module paths.`;
48
+ }
49
+ export function buildPlanningSystemPromptV5() {
50
+ return [
51
+ "You are a test gap identification engine.",
52
+ "Given a behavior and its evidence, identify every missing test scenario justified by the evidence.",
53
+ "",
54
+ "Concerns:",
55
+ "1. contract — wrong return type, missing field, wrong status code, schema violation",
56
+ "2. state_lifecycle — invalid transitions, corrupted state, terminal state bypass",
57
+ "3. boundary_limits — empty/null/zero/max/overflow, pagination edge, timeout threshold",
58
+ "4. integration_flow — cross-service chain produces wrong end state",
59
+ "5. failure_recovery — partial failure leaves inconsistent state, no rollback",
60
+ "6. authorization_safety — wrong actor accesses data, unsanitized input, sensitive errors",
61
+ "7. concurrency_ordering — race, double-processing, lost update, out-of-order event",
62
+ "8. data_integrity — transformation corrupts data, precision loss, stale cache",
63
+ "9. custom:<name> — repo-specific failure mode",
64
+ "",
65
+ "Rules:",
66
+ "- Only propose scenarios justified by evidence. No evidence = skip concern.",
67
+ "- Each scenario must be distinct.",
68
+ "- Existing tests are already covered. Never re-propose them.",
69
+ "- Rank all scenarios by risk_rank (1 = highest blast radius × likelihood × coverage absence).",
70
+ "- Find all gaps. No cap. If evidence justifies 3, output 3. If 30, output 30.",
71
+ "- When FLOW CHAIN exists, prioritize gaps at service boundaries.",
72
+ `- technique must be exactly one of: ${Object.keys(TECHNIQUE_DESC).join(", ")}.`,
73
+ "",
74
+ "Output contract:",
75
+ "- Return a raw JSON array only. No prose, no markdown fences, no heading, no explanation.",
76
+ "- If no missing scenario is justified, return [] exactly.",
77
+ "- The first character of your response must be [ and the last character must be ].",
78
+ '[{"id":1,"title":"...","concern":"...","technique":"...","rationale":"...","assertion_targets":["..."],"complexity":"basic|intermediate|advanced","risk_rank":1}]'
79
+ ].join("\n");
80
+ }
81
+ export function buildPlanningUserPromptV5(ctx) {
82
+ const lines = [];
83
+ lines.push(`BEHAVIOR: ${ctx.behavior_title}`);
84
+ if (ctx.description)
85
+ lines.push(`DESCRIPTION: ${ctx.description}`);
86
+ if (ctx.actors.length)
87
+ lines.push(`ACTORS: ${ctx.actors.join(", ")}`);
88
+ lines.push(`FRAMEWORK: ${ctx.framework} | LAYER: ${ctx.test_layer}`);
89
+ lines.push("");
90
+ if (ctx.flow_chain?.length) {
91
+ lines.push("FLOW CHAIN:");
92
+ for (const step of ctx.flow_chain) {
93
+ const marker = step.behavior_id === ctx.behavior_external_id ? " ←" : "";
94
+ lines.push(` ${step.position}. ${step.service}.${step.method}${marker}`);
95
+ }
96
+ lines.push("");
97
+ }
98
+ if (ctx.upstream_callers?.length)
99
+ lines.push(`UPSTREAM: ${ctx.upstream_callers.join(", ")}`);
100
+ if (ctx.downstream_callees?.length)
101
+ lines.push(`DOWNSTREAM: ${ctx.downstream_callees.join(", ")}`);
102
+ if (ctx.upstream_callers?.length || ctx.downstream_callees?.length)
103
+ lines.push("");
104
+ if (ctx.acceptance_criteria.length) {
105
+ lines.push("ACCEPTANCE CRITERIA:");
106
+ for (const ac of ctx.acceptance_criteria)
107
+ lines.push(`- ${ac}`);
108
+ lines.push("");
109
+ }
110
+ if (ctx.workflow_steps.length) {
111
+ lines.push("WORKFLOW:");
112
+ for (const step of ctx.workflow_steps)
113
+ lines.push(`- ${step}`);
114
+ lines.push("");
115
+ }
116
+ if (ctx.code_context.length) {
117
+ lines.push("CODE CONTEXT:");
118
+ for (const c of ctx.code_context)
119
+ lines.push(`- ${c}`);
120
+ lines.push("");
121
+ }
122
+ if (ctx.source_excerpts.length) {
123
+ lines.push("SOURCE (understand only):");
124
+ for (const e of ctx.source_excerpts)
125
+ lines.push(e);
126
+ lines.push("");
127
+ }
128
+ if (ctx.existing_tests.length) {
129
+ lines.push("EXISTING TESTS (already covered — do NOT re-propose):");
130
+ for (const t of ctx.existing_tests)
131
+ lines.push(`- ${t}`);
132
+ lines.push("");
133
+ }
134
+ if (ctx.docs_summary) {
135
+ lines.push(`DOCS: ${ctx.docs_summary}`);
136
+ lines.push("");
137
+ }
138
+ if (ctx.related_issues?.length) {
139
+ lines.push("ISSUES:");
140
+ for (const issue of ctx.related_issues)
141
+ lines.push(`- ${issue}`);
142
+ lines.push("");
143
+ }
144
+ if (ctx.weak_context.length) {
145
+ lines.push("WEAK HINTS:");
146
+ for (const w of ctx.weak_context)
147
+ lines.push(`- ${w}`);
148
+ lines.push("");
149
+ }
150
+ lines.push("Find missing test scenarios. Rank by risk. Return a raw JSON array only. If none are justified, return [] exactly.");
151
+ return lines.join("\n");
152
+ }
153
+ export function buildBatchGenerationSystemPromptV5() {
154
+ return [
155
+ "You are a test generation engine.",
156
+ "Generate one focused, runnable test for each scenario listed below.",
157
+ "",
158
+ "Rules:",
159
+ "- Never mock, stub, or spy on the behavior-under-test itself. The subject must execute for real. Mock only true external I/O boundaries — network calls, the system clock, third-party SDKs, outbound HTTP. If the behavior calls internal services in the same codebase, let them run (or use real test doubles at the I/O edge, never at the subject). A test that mocks the subject proves nothing and will be rejected.",
160
+ "- Each test is complete and runnable (all imports, setup, assertions, cleanup).",
161
+ "- Start each test with: // Concern: <concern> | Technique: <technique>",
162
+ "- Assert all targets listed in each scenario.",
163
+ "- Do not copy source excerpts verbatim. Use them to understand, then write original code.",
164
+ "- Reuse SUBJECT IMPORTS. Do not invent module paths.",
165
+ "- Existing tests are already covered. Do not regenerate them.",
166
+ "- Separate each test with: // ═══ SCENARIO <id> ═══",
167
+ "- Output only code. No prose, no markdown fences."
168
+ ].join("\n");
169
+ }
170
+ export function buildBatchGenerationUserPromptV5(ctx) {
171
+ const lines = [];
172
+ lines.push("═══ SHARED CONTEXT ═══");
173
+ lines.push(`BEHAVIOR: ${ctx.behavior_title}`);
174
+ if (ctx.description)
175
+ lines.push(`DESCRIPTION: ${ctx.description}`);
176
+ if (ctx.actors.length)
177
+ lines.push(`ACTORS: ${ctx.actors.join(", ")}`);
178
+ lines.push(`FRAMEWORK: ${ctx.framework} | LAYER: ${ctx.test_layer}`);
179
+ lines.push(`FRAMEWORK RULES: ${getFrameworkRules(ctx.framework)}`);
180
+ lines.push("");
181
+ if (ctx.flow_chain?.length) {
182
+ lines.push("FLOW CHAIN:");
183
+ for (const step of ctx.flow_chain)
184
+ lines.push(` ${step.position}. ${step.service}.${step.method}`);
185
+ lines.push("");
186
+ }
187
+ if (ctx.code_context.length) {
188
+ lines.push("CODE CONTEXT:");
189
+ for (const c of ctx.code_context)
190
+ lines.push(`- ${c}`);
191
+ lines.push("");
192
+ }
193
+ if (ctx.subject_imports.length) {
194
+ lines.push("SUBJECT IMPORTS (reuse these):");
195
+ for (const imp of ctx.subject_imports)
196
+ lines.push(imp);
197
+ lines.push("");
198
+ }
199
+ if (ctx.source_excerpts.length) {
200
+ lines.push("SOURCE (understand only — do not copy):");
201
+ for (const e of ctx.source_excerpts)
202
+ lines.push(e);
203
+ lines.push("");
204
+ }
205
+ if (ctx.existing_tests.length) {
206
+ lines.push("EXISTING TESTS (do NOT regenerate):");
207
+ for (const t of ctx.existing_tests)
208
+ lines.push(`- ${t}`);
209
+ lines.push("");
210
+ }
211
+ if (ctx.weak_context.length) {
212
+ lines.push("WEAK HINTS:");
213
+ for (const w of ctx.weak_context)
214
+ lines.push(`- ${w}`);
215
+ lines.push("");
216
+ }
217
+ lines.push("═══ SCENARIOS (generate one test per scenario) ═══");
218
+ lines.push("");
219
+ for (const s of ctx.scenarios) {
220
+ lines.push(`--- SCENARIO ${s.id} (risk_rank: ${s.risk_rank}) ---`);
221
+ lines.push(`TITLE: ${s.title}`);
222
+ lines.push(`CONCERN: ${s.concern}`);
223
+ lines.push(`TECHNIQUE: ${s.technique}`);
224
+ lines.push(`HOW: ${TECHNIQUE_DESC[s.technique]}`);
225
+ lines.push(`COMPLEXITY: ${s.complexity}`);
226
+ lines.push("ASSERT:");
227
+ for (const t of s.assertion_targets)
228
+ lines.push(` - ${t}`);
229
+ lines.push("");
230
+ }
231
+ lines.push("Generate one test per scenario. Separate with: // ═══ SCENARIO <id> ═══");
232
+ lines.push("Output code only.");
233
+ return lines.join("\n");
234
+ }
235
+ const KNOWN_TECHNIQUES = new Set(Object.keys(TECHNIQUE_DESC));
236
+ /** Strip a single leading/trailing Markdown code fence (```json … ```). */
237
+ function stripJsonFences(raw) {
238
+ return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
239
+ }
240
+ /**
241
+ * Extract the FIRST complete, balanced top-level JSON array from text that may be
242
+ * wrapped in prose. String-aware (ignores brackets inside quoted strings) so an
243
+ * assertion target like `returns [1,2]` cannot truncate the array early.
244
+ */
245
+ function extractFirstJsonArray(text) {
246
+ const start = text.indexOf("[");
247
+ if (start === -1)
248
+ return null;
249
+ let depth = 0;
250
+ let inStr = false;
251
+ let esc = false;
252
+ for (let i = start; i < text.length; i++) {
253
+ const ch = text[i];
254
+ if (inStr) {
255
+ if (esc)
256
+ esc = false;
257
+ else if (ch === "\\")
258
+ esc = true;
259
+ else if (ch === "\"")
260
+ inStr = false;
261
+ continue;
262
+ }
263
+ if (ch === "\"")
264
+ inStr = true;
265
+ else if (ch === "[")
266
+ depth++;
267
+ else if (ch === "]" && --depth === 0)
268
+ return text.slice(start, i + 1);
269
+ }
270
+ return null;
271
+ }
272
+ function toInteger(v) {
273
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
274
+ return Number.isInteger(n) ? n : null;
275
+ }
276
+ function toFinite(v) {
277
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
278
+ return Number.isFinite(n) ? n : null;
279
+ }
280
+ /** Validate one raw item against the CLOSED PlannedScenario schema. */
281
+ function validatePlannedScenario(v) {
282
+ const id = toInteger(v.id);
283
+ if (id === null)
284
+ return { ok: false, reason: "id not an integer" };
285
+ const title = typeof v.title === "string" ? v.title.trim() : "";
286
+ if (!title)
287
+ return { ok: false, reason: "empty title" };
288
+ const concern = typeof v.concern === "string" ? v.concern : "";
289
+ if (!STANDARD_CONCERNS.has(concern) && !/^custom:.+/.test(concern))
290
+ return { ok: false, reason: "unknown concern" };
291
+ const technique = typeof v.technique === "string" ? v.technique : "";
292
+ if (!KNOWN_TECHNIQUES.has(technique))
293
+ return { ok: false, reason: "unknown technique" };
294
+ if (!Array.isArray(v.assertion_targets))
295
+ return { ok: false, reason: "assertion_targets not an array" };
296
+ const targets = v.assertion_targets.filter((t) => typeof t === "string" && t.trim().length > 0);
297
+ if (targets.length === 0)
298
+ return { ok: false, reason: "empty assertion_targets" };
299
+ const complexity = v.complexity;
300
+ if (complexity !== "basic" && complexity !== "intermediate" && complexity !== "advanced") {
301
+ return { ok: false, reason: "invalid complexity" };
302
+ }
303
+ const riskRank = toFinite(v.risk_rank);
304
+ if (riskRank === null)
305
+ return { ok: false, reason: "non-finite risk_rank" };
306
+ return {
307
+ ok: true,
308
+ value: {
309
+ id,
310
+ title,
311
+ concern: concern,
312
+ technique: technique,
313
+ rationale: typeof v.rationale === "string" ? v.rationale : "",
314
+ assertion_targets: targets,
315
+ complexity,
316
+ risk_rank: riskRank
317
+ }
318
+ };
319
+ }
320
+ /**
321
+ * Hardened parse of v5 planning output. Strips Markdown fences, extracts the first
322
+ * complete JSON array even when wrapped in prose, then validates EVERY item against
323
+ * the closed PlannedScenario schema — dropping invalid items with counted reasons,
324
+ * capping, and stable-sorting by risk_rank. Throws only on hard failure (no JSON
325
+ * array extractable / not valid JSON / not an array), which is the caller's signal
326
+ * to attempt a single repair pass. An empty array is valid ("no missing scenarios")
327
+ * and never triggers repair. Error messages carry NO fragment of the raw output.
328
+ */
329
+ export function parsePlannedScenariosStrict(raw, maxScenarios = 20) {
330
+ const arrayText = extractFirstJsonArray(stripJsonFences(raw)) ?? extractFirstJsonArray(raw);
331
+ if (!arrayText)
332
+ throw new Error("Planning output contained no JSON array.");
333
+ let parsed;
334
+ try {
335
+ parsed = JSON.parse(arrayText);
336
+ }
337
+ catch {
338
+ throw new Error("Planning output was not valid JSON.");
339
+ }
340
+ if (!Array.isArray(parsed))
341
+ throw new Error("Planning output was not a JSON array.");
342
+ const out = [];
343
+ const reasons = new Map();
344
+ for (const item of parsed) {
345
+ if (!item || typeof item !== "object") {
346
+ reasons.set("not an object", (reasons.get("not an object") ?? 0) + 1);
347
+ continue;
348
+ }
349
+ const res = validatePlannedScenario(item);
350
+ if (res.ok)
351
+ out.push(res.value);
352
+ else
353
+ reasons.set(res.reason, (reasons.get(res.reason) ?? 0) + 1);
354
+ }
355
+ const scenarios = out.sort((a, b) => a.risk_rank - b.risk_rank || a.id - b.id).slice(0, maxScenarios);
356
+ const dropSummary = [...reasons.entries()].map(([reason, n]) => `${n} item(s): ${reason}`);
357
+ const dropped = [...reasons.values()].reduce((sum, n) => sum + n, 0);
358
+ return { scenarios, dropped, dropSummary };
359
+ }
360
+ /** Back-compat wrapper: the validated scenarios only (drops surfaced by the strict parser). */
361
+ export function parsePlannedScenarios(raw, maxScenarios = 20) {
362
+ return parsePlannedScenariosStrict(raw, maxScenarios).scenarios;
363
+ }
364
+ /** System prompt for the single transient JSON-repair pass (see generator v5 planning). */
365
+ export function buildPlanningRepairSystemPromptV5() {
366
+ return "You are a JSON repair engine. Convert ONLY the scenarios already present in the following malformed plan into a JSON array matching this schema. Output raw JSON only. The first character must be [ and the last character must be ]. Do NOT add, invent, or infer any scenario not already in the input. If the input contains no scenarios, output [] exactly.";
367
+ }
368
+ /**
369
+ * A cheap DETERMINISTIC pre-gate: does the raw planning output contain enough recoverable
370
+ * scenario structure to be worth repairing? Requires a JSON-array shape (`[` … `]` with an
371
+ * object) AND at least one closed-schema key. Total garbage / prose with no array → false, so the
372
+ * caller fails closed instead of letting the repair model invent a fresh plan from nothing.
373
+ */
374
+ export function hasRepairableScenarioStructure(raw) {
375
+ const s = stripJsonFences(raw);
376
+ const open = s.indexOf("[");
377
+ if (open === -1)
378
+ return false; // no array at all → total garbage / prose → fail closed
379
+ const after = s.slice(open); // a TRUNCATED array (no closing "]") is still recoverable, so don't require it
380
+ if (!after.includes("{"))
381
+ return false; // array of non-objects → nothing scenario-shaped to recover
382
+ return /"(?:title|concern|technique|assertion_targets|risk_rank|complexity)"\s*:/.test(after);
383
+ }
384
+ /**
385
+ * DETERMINISTIC tie-back: a repaired scenario is only trusted if it can be traced to the ORIGINAL
386
+ * malformed text — a significant title token OR an assertion-target substring must appear in it. An
387
+ * invented scenario (fabricated title/targets not in the input) fails this and is dropped. Conservative
388
+ * by design: an over-rephrased-but-real scenario may be dropped too, which is the fail-closed direction.
389
+ */
390
+ export function scenarioTiesBackToRaw(scenario, raw) {
391
+ const rawN = raw.toLowerCase();
392
+ const titleHit = scenario.title
393
+ .toLowerCase()
394
+ .split(/[^a-z0-9]+/)
395
+ .some((tok) => tok.length >= 4 && rawN.includes(tok));
396
+ const targetHit = scenario.assertion_targets.some((t) => {
397
+ const n = t.toLowerCase().trim();
398
+ return n.length >= 3 && rawN.includes(n.slice(0, 40));
399
+ });
400
+ return titleHit || targetHit;
401
+ }
402
+ /**
403
+ * User prompt for the repair pass. Carries the malformed output ONLY as transient
404
+ * input to the model — it is never persisted. Restates the closed schema so the
405
+ * model corrects, rather than invents, scenarios.
406
+ */
407
+ export function buildPlanningRepairUserPromptV5(malformed) {
408
+ return [
409
+ "SCHEMA — a JSON array of objects, each:",
410
+ '{"id":<integer>,"title":<nonempty string>,"concern":"contract|state_lifecycle|boundary_limits|integration_flow|failure_recovery|authorization_safety|concurrency_ordering|data_integrity|custom:<name>","technique":"<allowed technique id>","rationale":<string>,"assertion_targets":[<nonempty string>,"..."],"complexity":"basic|intermediate|advanced","risk_rank":<number>}',
411
+ `ALLOWED technique ids: ${Object.keys(TECHNIQUE_DESC).join(", ")}`,
412
+ "",
413
+ "MALFORMED PLAN:",
414
+ malformed,
415
+ "",
416
+ "Convert the malformed scenario plan above into a JSON array matching the schema. Output raw JSON only. Do not add new scenarios. If there are no recoverable scenarios, return [] exactly."
417
+ ].join("\n");
418
+ }
419
+ export function parseBatchGeneratedTests(raw) {
420
+ const clean = raw.trim();
421
+ if (!clean)
422
+ return [];
423
+ const delimiter = /\/\/\s*═══\s*SCENARIO\s+(\d+)\s*═══/g;
424
+ const matches = [...clean.matchAll(delimiter)];
425
+ if (matches.length === 0)
426
+ return [{ scenario_id: null, body: clean }];
427
+ const out = [];
428
+ for (let i = 0; i < matches.length; i++) {
429
+ const current = matches[i];
430
+ const next = matches[i + 1];
431
+ const start = (current.index ?? 0) + current[0].length;
432
+ const end = next?.index ?? clean.length;
433
+ const body = clean.slice(start, end).trim();
434
+ if (body)
435
+ out.push({ scenario_id: Number(current[1]), body });
436
+ }
437
+ return out;
438
+ }