@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,525 @@
1
+ import { behaviorNodes, isDenominatorEligible } from "../graph/factories.js";
2
+ import { structurallyUnconfirmable } from "../graph/confirmable.js";
3
+ import { languageOf } from "../analyze/classify.js";
4
+ import { LEDGER_SCHEMA_VERSION } from "../ledger.js";
5
+ import { buildRtm } from "../rtm.js";
6
+ // What counts as a behavior comes ONLY from the canonical helpers
7
+ // (factories.behaviorNodes over ontology.BEHAVIOR_KINDS) — a duplicated local
8
+ // kind set lived here once and could silently drift from score's definition.
9
+ const DEFAULT_KINDS = [
10
+ "TenantStub",
11
+ "Requirement",
12
+ "UserFlow",
13
+ "BusinessRule",
14
+ "AcceptanceCriterion",
15
+ "Service",
16
+ "Endpoint",
17
+ "Incident",
18
+ "Framework",
19
+ "Package"
20
+ ];
21
+ function tally(items) {
22
+ const m = new Map();
23
+ for (const k of items)
24
+ m.set(k, (m.get(k) ?? 0) + 1);
25
+ return [...m.entries()].map(([kind, count]) => ({ kind, count })).sort((a, b) => b.count - a.count);
26
+ }
27
+ function behaviorEvidence(graph, externalId) {
28
+ const hard = graph.edges.some((e) => (e.relationship_type === "TESTED_BY" || e.relationship_type === "COVERS") &&
29
+ (e.from_external_id === externalId || e.to_external_id === externalId));
30
+ if (hard)
31
+ return "covered";
32
+ const weak = graph.candidate_edges.some((e) => (e.relationship_type === "MAY_BE_TESTED_BY" || e.relationship_type === "MAY_COVER") &&
33
+ (e.from_external_id === externalId || e.to_external_id === externalId));
34
+ return weak ? "weak" : "none";
35
+ }
36
+ /** First path segment of a provenance ref / source path (the "area"). */
37
+ function areaOf(sourceRefOrPath) {
38
+ if (!sourceRefOrPath)
39
+ return "core";
40
+ const cleaned = sourceRefOrPath.replace(/^[./]+/, "").split(/[#?]/)[0];
41
+ const first = cleaned.split(/[\\/]/).filter(Boolean)[0];
42
+ return first || "core";
43
+ }
44
+ function cleanPathParts(sourceRefOrPath) {
45
+ if (!sourceRefOrPath)
46
+ return [];
47
+ return sourceRefOrPath.replace(/^[./]+/, "").split(/[#?]/)[0].split(/[\\/]/).filter(Boolean);
48
+ }
49
+ function parentDir(parts) {
50
+ return parts.length > 1 ? parts[parts.length - 2] : parts[0] || "core";
51
+ }
52
+ function codeAreaOf(sourceRefOrPath) {
53
+ const parts = cleanPathParts(sourceRefOrPath);
54
+ if (parts.length === 0)
55
+ return "core";
56
+ const language = languageOf(sourceRefOrPath ?? "");
57
+ if (language === "java" || language === "kotlin") {
58
+ const srcIdx = parts.findIndex((p, i) => p === "src" && (parts[i + 1] === "main" || parts[i + 1] === "test"));
59
+ if (srcIdx >= 0 && parts[srcIdx + 2] && ["java", "kotlin"].includes(parts[srcIdx + 2]))
60
+ return parentDir(parts.slice(srcIdx + 3));
61
+ return parentDir(parts);
62
+ }
63
+ if (language === "python") {
64
+ if (["src", "app", "mealie"].includes(parts[0]) && parts.length > 2)
65
+ return parts.slice(1, 3).join("/");
66
+ return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
67
+ }
68
+ if (language === "go") {
69
+ if (parts[0] === "server" && parts[1] === "channels" && parts[2])
70
+ return `server/${parts[2]}`;
71
+ return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
72
+ }
73
+ if (language === "typescript" || language === "javascript") {
74
+ if (parts[0] === "webapp" && parts[1] === "channels" && parts[2] === "src" && parts[3])
75
+ return `webapp/${parts[3]}`;
76
+ if (parts[0] === "frontend" && parts[1] === "app" && parts[2])
77
+ return `frontend/${parts[2]}`;
78
+ return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
79
+ }
80
+ return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
81
+ }
82
+ /**
83
+ * 4-state coverage for a UserFlow (precedence: confirmed > nsc > inferred > none):
84
+ * confirmed = hard TESTED_BY/COVERS edge; not_structurally_confirmable = no hard
85
+ * edge but all tests are e2e/api; inferred = weak candidate edge; none = neither.
86
+ */
87
+ function flowCoverage(graph, externalId, nsc) {
88
+ const ev = behaviorEvidence(graph, externalId);
89
+ if (ev === "covered")
90
+ return "confirmed";
91
+ if (nsc.has(externalId))
92
+ return "not_structurally_confirmable";
93
+ if (ev === "weak")
94
+ return "inferred";
95
+ return "none";
96
+ }
97
+ /** TestCase external_ids weakly linked to this flow via MAY_BE_TESTED_BY / MAY_COVER (capped). */
98
+ function flowTestIds(graph, externalId) {
99
+ return flowTestLinks(graph, externalId)
100
+ .filter((l) => l.status === "possible")
101
+ .map((l) => l.to)
102
+ .slice(0, 2);
103
+ }
104
+ function flowTestLinks(graph, externalId) {
105
+ const nodeById = new Map(graph.nodes.map((n) => [n.external_id, n]));
106
+ const links = new Map();
107
+ for (const e of graph.candidate_edges) {
108
+ if (e.relationship_type !== "MAY_BE_TESTED_BY" && e.relationship_type !== "MAY_COVER")
109
+ continue;
110
+ let other;
111
+ if (e.from_external_id === externalId)
112
+ other = e.to_external_id;
113
+ else if (e.to_external_id === externalId)
114
+ other = e.from_external_id;
115
+ if (!other || links.has(other))
116
+ continue;
117
+ const node = nodeById.get(other);
118
+ if (node?.kind === "TestCase") {
119
+ links.set(other, {
120
+ from: externalId,
121
+ to: other,
122
+ status: "possible",
123
+ label: "Possible test link",
124
+ confidence: e.confidence
125
+ });
126
+ }
127
+ }
128
+ for (const e of graph.edges) {
129
+ if (e.relationship_type !== "TESTED_BY" && e.relationship_type !== "COVERS")
130
+ continue;
131
+ let testId;
132
+ if (e.from_external_id === externalId)
133
+ testId = e.to_external_id;
134
+ else if (e.to_external_id === externalId)
135
+ testId = e.from_external_id;
136
+ if (!testId || nodeById.get(testId)?.kind !== "TestCase")
137
+ continue;
138
+ links.set(testId, {
139
+ from: externalId,
140
+ to: testId,
141
+ status: "confirmed",
142
+ label: "Static test link",
143
+ confidence: e.confidence ?? 1,
144
+ ...(typeof e.last_verified === "number" ? { last_verified: e.last_verified } : {})
145
+ });
146
+ }
147
+ return [...links.values()].sort((a, b) => (a.status === b.status ? a.to.localeCompare(b.to) : a.status === "confirmed" ? -1 : 1));
148
+ }
149
+ function nodeFileRef(n) {
150
+ if (typeof n.properties.file === "string")
151
+ return n.properties.file;
152
+ if (typeof n.provenance?.source_ref === "string")
153
+ return n.provenance.source_ref;
154
+ return n.external_id.replace(/^sym:/, "").split("#")[0];
155
+ }
156
+ function symbolProofLinks(graph) {
157
+ const nodeById = new Map(graph.nodes.map((n) => [n.external_id, n]));
158
+ const links = new Map();
159
+ for (const e of graph.edges) {
160
+ if (e.relationship_type !== "TESTED_BY" && e.relationship_type !== "COVERS")
161
+ continue;
162
+ const from = nodeById.get(e.from_external_id);
163
+ const to = nodeById.get(e.to_external_id);
164
+ let symbol = from?.kind === "CodeSymbol" ? from : to?.kind === "CodeSymbol" ? to : undefined;
165
+ let test = from?.kind === "TestCase" ? from : to?.kind === "TestCase" ? to : undefined;
166
+ if (!symbol || !test || symbol.denominator_eligible === false)
167
+ continue;
168
+ const key = `${symbol.external_id}|${test.external_id}`;
169
+ links.set(key, {
170
+ from: symbol.external_id,
171
+ to: test.external_id,
172
+ symbol_label: symbol.title || symbol.external_id,
173
+ test_label: test.title || test.external_id,
174
+ area: areaOf(nodeFileRef(symbol)),
175
+ confidence: e.confidence ?? 1,
176
+ ...(typeof e.last_verified === "number" ? { last_verified: e.last_verified } : {})
177
+ });
178
+ }
179
+ return [...links.values()].sort((a, b) => a.area.localeCompare(b.area) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
180
+ }
181
+ function behaviorHasAc(graph, externalId) {
182
+ if (graph.edges.some((e) => e.relationship_type === "HAS_ACCEPTANCE_CRITERION" && e.from_external_id === externalId)) {
183
+ return true;
184
+ }
185
+ const node = graph.nodes.find((n) => n.external_id === externalId);
186
+ const inline = node?.properties.acceptance_criteria;
187
+ return Array.isArray(inline) ? inline.length > 0 : Boolean(inline);
188
+ }
189
+ function languageLabel(language) {
190
+ const labels = {
191
+ typescript: "TypeScript/JavaScript",
192
+ javascript: "TypeScript/JavaScript",
193
+ python: "Python",
194
+ go: "Go",
195
+ java: "Java",
196
+ ruby: "Ruby",
197
+ kotlin: "Kotlin",
198
+ rust: "Rust",
199
+ php: "PHP",
200
+ csharp: "C#",
201
+ swift: "Swift",
202
+ c: "C",
203
+ cpp: "C++"
204
+ };
205
+ return labels[language] ?? language;
206
+ }
207
+ function emptyLedger() {
208
+ return { schema_version: LEDGER_SCHEMA_VERSION, records: [] };
209
+ }
210
+ function publicRowsById(graph, ledger) {
211
+ return new Map(buildRtm(graph, ledger ?? emptyLedger()).rows.map((row) => [row.behavior_id, row]));
212
+ }
213
+ function eligibleCodeSymbols(graph) {
214
+ return graph.nodes.filter((n) => n.kind === "CodeSymbol" && isDenominatorEligible(n));
215
+ }
216
+ function languageTiers(graph, ledger) {
217
+ const publicRows = publicRowsById(graph, ledger);
218
+ const rows = new Map();
219
+ for (const n of eligibleCodeSymbols(graph)) {
220
+ const file = nodeFileRef(n) ?? "";
221
+ const publicRow = publicRows.get(n.external_id);
222
+ const language = languageLabel(languageOf(file));
223
+ let row = rows.get(language);
224
+ if (!row) {
225
+ row = { language, total: 0, proven: 0, runtime_covered: 0, associated: 0, unlinked: 0, proven_pct: 0, runtime_pct: 0, associated_pct: 0 };
226
+ rows.set(language, row);
227
+ }
228
+ row.total++;
229
+ if (publicRow?.evidence_tier === "proven")
230
+ row.proven++;
231
+ else if (publicRow?.evidence_tier === "runtime")
232
+ row.runtime_covered++;
233
+ else if (publicRow?.evidence_tier === "associated")
234
+ row.associated++;
235
+ else
236
+ row.unlinked++;
237
+ }
238
+ return [...rows.values()]
239
+ .map((r) => ({
240
+ ...r,
241
+ proven_pct: r.total > 0 ? (r.proven / r.total) * 100 : 0,
242
+ runtime_pct: r.total > 0 ? (r.runtime_covered / r.total) * 100 : 0,
243
+ associated_pct: r.total > 0 ? (r.associated / r.total) * 100 : 0
244
+ }))
245
+ .sort((a, b) => b.total - a.total || a.language.localeCompare(b.language));
246
+ }
247
+ function codeAreaSummary(graph, ledger) {
248
+ const publicRows = publicRowsById(graph, ledger);
249
+ const areaMap = new Map();
250
+ for (const n of eligibleCodeSymbols(graph)) {
251
+ const file = nodeFileRef(n) ?? "";
252
+ const publicRow = publicRows.get(n.external_id);
253
+ const area = codeAreaOf(file);
254
+ let a = areaMap.get(area);
255
+ if (!a) {
256
+ a = { total: 0, confirmed: 0, runtime_covered: 0, inferred: 0, none: 0, noLinkSamples: [], associatedSamples: [] };
257
+ areaMap.set(area, a);
258
+ }
259
+ a.total++;
260
+ if (publicRow?.evidence_tier === "proven") {
261
+ a.confirmed++;
262
+ }
263
+ else if (publicRow?.evidence_tier === "runtime") {
264
+ a.runtime_covered++;
265
+ }
266
+ else {
267
+ const sample = { title: n.title || n.external_id, file, status: "none" };
268
+ if (publicRow?.evidence_tier === "associated") {
269
+ a.inferred++;
270
+ a.associatedSamples.push({ ...sample, status: "associated" });
271
+ }
272
+ else {
273
+ a.none++;
274
+ a.noLinkSamples.push(sample);
275
+ }
276
+ }
277
+ }
278
+ return [...areaMap.entries()]
279
+ .map(([area, a]) => ({
280
+ area,
281
+ total: a.total,
282
+ confirmed: a.confirmed,
283
+ runtime_covered: a.runtime_covered,
284
+ inferred: a.inferred,
285
+ not_structurally_confirmable: 0,
286
+ none: a.none,
287
+ confirmed_pct: a.total > 0 ? (a.confirmed / a.total) * 100 : 0,
288
+ sample_gaps: [...a.noLinkSamples, ...a.associatedSamples].slice(0, 6)
289
+ }))
290
+ .sort((x, y) => y.total - x.total || x.area.localeCompare(y.area));
291
+ }
292
+ function codeBehaviorRows(graph, ledger) {
293
+ const publicRows = publicRowsById(graph, ledger);
294
+ return eligibleCodeSymbols(graph)
295
+ .map((n) => {
296
+ const file = nodeFileRef(n) ?? "";
297
+ const tier = publicRows.get(n.external_id)?.evidence_tier;
298
+ const evidence = tier === "proven"
299
+ ? "proven"
300
+ : tier === "runtime"
301
+ ? "runtime"
302
+ : tier === "associated"
303
+ ? "associated"
304
+ : "none";
305
+ return {
306
+ id: n.external_id,
307
+ title: n.title || n.external_id,
308
+ file,
309
+ area: codeAreaOf(file),
310
+ language: languageLabel(languageOf(file)),
311
+ evidence
312
+ };
313
+ })
314
+ .sort((a, b) => {
315
+ const rank = (x) => (x.evidence === "none" ? 0 : x.evidence === "associated" ? 1 : x.evidence === "runtime" ? 2 : 3);
316
+ return rank(a) - rank(b) || a.area.localeCompare(b.area) || a.file.localeCompare(b.file) || a.title.localeCompare(b.title);
317
+ });
318
+ }
319
+ function staticFlowRows(graph) {
320
+ const flows = graph.analysis?.flows?.flows ?? [];
321
+ const items = flows.slice(0, 100).map((flow) => ({
322
+ id: flow.id,
323
+ entry_id: flow.entry_point.external_id,
324
+ entry_title: flow.entry_point.title || flow.entry_point.external_id,
325
+ entry_kind: flow.entry_point.kind,
326
+ terminal: flow.terminal,
327
+ depth: flow.depth,
328
+ tier: flow.flow_tier,
329
+ truncated: flow.truncated === true,
330
+ hops: flow.hops.map((h) => ({
331
+ from: h.from,
332
+ to: h.to,
333
+ strength: h.evidence_strength,
334
+ ...(h.resolution ? { resolution: h.resolution } : {})
335
+ }))
336
+ }));
337
+ return {
338
+ total: graph.analysis?.flows?.total_flows ?? flows.length,
339
+ hard_reachable: graph.analysis?.flows?.by_tier["hard: reachable"] ?? flows.filter((f) => f.flow_tier === "hard: reachable").length,
340
+ framework_derived_reachable: graph.analysis?.flows?.by_tier["framework-derived: reachable"] ?? flows.filter((f) => f.flow_tier === "framework-derived: reachable").length,
341
+ truncated: graph.analysis?.flows?.truncated_flows ?? flows.filter((f) => f.truncated === true).length,
342
+ items
343
+ };
344
+ }
345
+ /** Build the gap-first view model (heatmap / connectivity / flows). */
346
+ function buildGap(graph, ledger) {
347
+ const nsc = structurallyUnconfirmable(graph);
348
+ const publicRtm = buildRtm(graph, ledger ?? emptyLedger());
349
+ const flows = graph.nodes
350
+ .filter((n) => n.kind === "UserFlow")
351
+ .map((n) => {
352
+ const ref = n.provenance?.source_ref;
353
+ const propArea = typeof n.properties.area === "string" ? n.properties.area : undefined;
354
+ const area = ref ? areaOf(ref) : propArea || "core";
355
+ const behaviors = Array.isArray(n.properties.example_behaviors)
356
+ ? n.properties.example_behaviors.filter((b) => typeof b === "string").slice(0, 3)
357
+ : [];
358
+ const coverage = flowCoverage(graph, n.external_id, nsc);
359
+ const test_links = flowTestLinks(graph, n.external_id);
360
+ return {
361
+ id: n.external_id,
362
+ title: n.title || n.external_id,
363
+ area,
364
+ coverage,
365
+ has_test: coverage === "confirmed",
366
+ ...(coverage === "not_structurally_confirmable" ? { defer_reason: nsc.get(n.external_id) } : {}),
367
+ test_ids: flowTestIds(graph, n.external_id),
368
+ test_links,
369
+ confidence: n.confidence,
370
+ behaviors
371
+ };
372
+ });
373
+ const userflows = flows.length;
374
+ const confirmed = flows.filter((f) => f.coverage === "confirmed").length;
375
+ const inferred = flows.filter((f) => f.coverage === "inferred").length;
376
+ const notStructurallyConfirmable = flows.filter((f) => f.coverage === "not_structurally_confirmable").length;
377
+ const none = flows.filter((f) => f.coverage === "none").length;
378
+ // confirmed_pct EXCLUDES nsc: e2e/api behaviors never dilute the structural %.
379
+ const confirmableTotal = confirmed + inferred + none;
380
+ const confirmed_pct = confirmableTotal > 0 ? (confirmed / confirmableTotal) * 100 : 0;
381
+ const area_summary = codeAreaSummary(graph, ledger);
382
+ const testCaseNodes = graph.nodes.filter((n) => n.kind === "TestCase");
383
+ const testcases_sample = testCaseNodes.slice(0, 60).map((n) => ({
384
+ id: n.external_id,
385
+ title: n.title || n.external_id,
386
+ test_names: Array.isArray(n.properties.test_names)
387
+ ? n.properties.test_names.filter((t) => typeof t === "string").slice(0, 3)
388
+ : [],
389
+ test_layer: typeof n.properties.test_layer === "string" ? n.properties.test_layer : "unknown"
390
+ }));
391
+ const symbol_test_links = symbolProofLinks(graph);
392
+ const services = [...new Set([...flows.map((f) => f.area), ...symbol_test_links.map((l) => l.area)])].map((name) => ({
393
+ id: `area:${name}`,
394
+ title: name,
395
+ area: name
396
+ }));
397
+ // Headline Proven is computed over the public denominator behaviors from the
398
+ // same RTM gate as COVERAGE_REPORT.md: dynamic targeted proof only.
399
+ return {
400
+ workspace: graph.workspace.name,
401
+ stats: {
402
+ userflows,
403
+ testcases: testCaseNodes.length,
404
+ confirmed,
405
+ inferred,
406
+ not_structurally_confirmable: notStructurallyConfirmable,
407
+ none,
408
+ confirmable_total: confirmableTotal,
409
+ confirmed_pct,
410
+ coverage_pct: confirmed_pct,
411
+ behavior_confirmed: publicRtm.summary.proven,
412
+ behavior_total: publicRtm.summary.total,
413
+ behavior_confirmed_pct: publicRtm.summary.coverage_pct
414
+ },
415
+ area_summary,
416
+ language_tiers: languageTiers(graph, ledger),
417
+ code_behaviors: codeBehaviorRows(graph, ledger),
418
+ static_flows: staticFlowRows(graph),
419
+ all_userflows: flows,
420
+ symbol_test_links,
421
+ testcases_sample,
422
+ services,
423
+ ...(graph.analysis?.not_analyzed_due_to_budget
424
+ ? {
425
+ partial_scan: {
426
+ files_not_analyzed: graph.analysis.not_analyzed_due_to_budget.files_not_analyzed,
427
+ budget_ms: graph.analysis.not_analyzed_due_to_budget.budget_ms
428
+ }
429
+ }
430
+ : {}),
431
+ ...(graph.analysis?.confirmed_coverage && graph.analysis.confirmed_coverage.skipped_files_budget > 0
432
+ ? {
433
+ proof_limited: {
434
+ skipped_files_budget: graph.analysis.confirmed_coverage.skipped_files_budget,
435
+ ...(graph.analysis.confirmed_coverage.scoped_by_risk ? { scoped_by_risk: graph.analysis.confirmed_coverage.scoped_by_risk } : {})
436
+ }
437
+ }
438
+ : {})
439
+ };
440
+ }
441
+ export function buildVizPayload(graph, score, ledger) {
442
+ const behaviors = behaviorNodes(graph);
443
+ const behaviorMeta = new Map();
444
+ let gaps = 0;
445
+ for (const b of behaviors) {
446
+ const ac = behaviorHasAc(graph, b.external_id);
447
+ const evidence = behaviorEvidence(graph, b.external_id);
448
+ behaviorMeta.set(b.external_id, { ac, evidence });
449
+ if (!ac || evidence !== "covered")
450
+ gaps++;
451
+ }
452
+ const nodes = graph.nodes.map((n) => {
453
+ const node = {
454
+ id: n.external_id,
455
+ label: (n.title || n.external_id).slice(0, 80),
456
+ kind: n.kind,
457
+ strength: n.evidence_strength,
458
+ confidence: n.confidence,
459
+ ref: n.provenance?.source_ref
460
+ };
461
+ const bm = behaviorMeta.get(n.external_id);
462
+ if (bm) {
463
+ node.ac = bm.ac;
464
+ node.evidence = bm.evidence;
465
+ }
466
+ return node;
467
+ });
468
+ const edges = [
469
+ ...graph.edges.map((e) => ({
470
+ from: e.from_external_id,
471
+ to: e.to_external_id,
472
+ kind: e.relationship_type,
473
+ weak: e.evidence_strength === "framework-derived",
474
+ strength: e.evidence_strength
475
+ })),
476
+ ...graph.candidate_edges.map((e) => ({
477
+ from: e.from_external_id,
478
+ to: e.to_external_id,
479
+ kind: e.relationship_type,
480
+ weak: true,
481
+ confidence: e.confidence
482
+ }))
483
+ ];
484
+ const strength = {};
485
+ for (const n of graph.nodes)
486
+ strength[n.evidence_strength] = (strength[n.evidence_strength] ?? 0) + 1;
487
+ const frameworks = graph.nodes.filter((n) => n.kind === "Framework").map((n) => n.title || n.external_id);
488
+ const sources = graph.sources.map((s) => ({ system: s.source_system, name: s.display_name }));
489
+ const generatedItems = graph.generated_tests.map((t) => ({
490
+ title: t.title,
491
+ entity_ids: t.grounding.entity_ids,
492
+ bucket: t.bucket
493
+ }));
494
+ const generatedByBucket = tally(graph.generated_tests.map((t) => t.bucket).filter((b) => b !== undefined)).map((x) => ({ bucket: x.kind, count: x.count }));
495
+ return {
496
+ meta: {
497
+ workspace: graph.workspace.name,
498
+ created_at: graph.updated_at,
499
+ source_upload_policy: graph.workspace.source_upload_policy,
500
+ score: {
501
+ overall: score.overall,
502
+ band: score.band,
503
+ breakdown: score.breakdown,
504
+ missing_evidence: score.missing_evidence
505
+ },
506
+ analysis: graph.analysis ?? null,
507
+ counts: {
508
+ nodes: graph.nodes.length,
509
+ edges: graph.edges.length,
510
+ candidates: graph.candidate_edges.length,
511
+ nodesByKind: tally(graph.nodes.map((n) => n.kind)),
512
+ edgesByKind: tally([...graph.edges, ...graph.candidate_edges].map((e) => e.relationship_type)),
513
+ strength
514
+ },
515
+ frameworks,
516
+ sources,
517
+ generated: { count: graph.generated_tests.length, byBucket: generatedByBucket, items: generatedItems },
518
+ gaps
519
+ },
520
+ nodes,
521
+ edges,
522
+ defaultKinds: DEFAULT_KINDS,
523
+ gap: buildGap(graph, ledger)
524
+ };
525
+ }
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { LOCAL_GRAPH_SCHEMA_VERSION } from "./graph/ontology.js";
4
+ import { defaultPrivacySettings } from "./localConfig.js";
5
+ export const WORKSPACE_DIR = ".orangepro";
6
+ export const GRAPH_FILE = "graph.json";
7
+ export const CONFIG_FILE = "config.json";
8
+ export function workspacePaths(root) {
9
+ const absRoot = resolve(root);
10
+ const dir = join(absRoot, WORKSPACE_DIR);
11
+ return {
12
+ root: absRoot,
13
+ dir,
14
+ graphPath: join(dir, GRAPH_FILE),
15
+ configPath: join(dir, CONFIG_FILE)
16
+ };
17
+ }
18
+ export function workspaceInitialized(root) {
19
+ return existsSync(workspacePaths(root).configPath);
20
+ }
21
+ export function graphExists(root) {
22
+ return existsSync(workspacePaths(root).graphPath);
23
+ }
24
+ const ORANGEPROIGNORE_TEMPLATE = `# .orangeproignore — paths the OrangePro local proof kit should never read.
25
+ # Same spirit as .gitignore. Secrets and large assets are excluded by default.
26
+ *.env
27
+ *.pem
28
+ *.key
29
+ secrets/
30
+ *evidence-pack.json
31
+ *evidence-pack.md
32
+
33
+ # Product-denominator defaults: example/demo apps are useful references, but
34
+ # they usually should not count as product behavior coverage.
35
+ examples/
36
+ example/
37
+ demos/
38
+ demo/
39
+ samples/
40
+ sample/
41
+ docs/examples/
42
+ docs/demo/
43
+ docs/demos/
44
+ `;
45
+ export function initWorkspace(root, now) {
46
+ const paths = workspacePaths(root);
47
+ mkdirSync(paths.dir, { recursive: true });
48
+ const config = {
49
+ workspace_name: deriveWorkspaceName(paths.root),
50
+ created_at: now,
51
+ local_only: true,
52
+ privacy: defaultPrivacySettings()
53
+ };
54
+ if (!existsSync(paths.configPath)) {
55
+ writeJson(paths.configPath, config);
56
+ }
57
+ const ignorePath = join(paths.root, ".orangeproignore");
58
+ if (!existsSync(ignorePath)) {
59
+ writeFileSync(ignorePath, ORANGEPROIGNORE_TEMPLATE, "utf8");
60
+ }
61
+ return { paths, config: loadConfig(paths) };
62
+ }
63
+ export function loadConfig(paths) {
64
+ if (!existsSync(paths.configPath)) {
65
+ return {
66
+ workspace_name: deriveWorkspaceName(paths.root),
67
+ created_at: "",
68
+ local_only: true,
69
+ privacy: defaultPrivacySettings()
70
+ };
71
+ }
72
+ return JSON.parse(readFileSync(paths.configPath, "utf8"));
73
+ }
74
+ export function saveConfig(paths, config) {
75
+ writeJson(paths.configPath, config);
76
+ }
77
+ export function loadGraph(graphPath) {
78
+ if (!existsSync(graphPath)) {
79
+ throw new Error(`No graph found at ${graphPath}. Run \`opro analyze .\` first.`);
80
+ }
81
+ const graph = JSON.parse(readFileSync(graphPath, "utf8"));
82
+ if (graph.schema_version !== LOCAL_GRAPH_SCHEMA_VERSION) {
83
+ // Force-rebuild on any schema mismatch: the graph is fully derived from the
84
+ // repo, so rebuilding loses nothing and silently consuming an old schema
85
+ // (e.g. v0 without denominator fields) would corrupt coverage math.
86
+ throw new Error(`Graph schema mismatch: found ${graph.schema_version ?? "unknown"}, expected ${LOCAL_GRAPH_SCHEMA_VERSION}. ` +
87
+ "Run `opro analyze .` to rebuild — no data loss (the graph is derived from your repo).");
88
+ }
89
+ return graph;
90
+ }
91
+ export function saveGraph(graphPath, graph) {
92
+ writeJson(graphPath, graph);
93
+ }
94
+ export function deriveWorkspaceName(root) {
95
+ return resolve(root).split(/[\\/]/).filter(Boolean).pop() || "workspace";
96
+ }
97
+ function writeJson(path, value) {
98
+ writeFileSync(path, JSON.stringify(value, null, 2) + "\n", "utf8");
99
+ }