@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.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/local/agentWorkflow.js +81 -0
- package/dist/local/aiGraph/links.js +635 -0
- package/dist/local/analyze/analyzer.js +2129 -0
- package/dist/local/analyze/behaviorContracts.js +169 -0
- package/dist/local/analyze/boilerplate.js +42 -0
- package/dist/local/analyze/callGraph.js +458 -0
- package/dist/local/analyze/classify.js +219 -0
- package/dist/local/analyze/clustering.js +357 -0
- package/dist/local/analyze/confirm.js +2422 -0
- package/dist/local/analyze/coverage.js +518 -0
- package/dist/local/analyze/coverageArtifacts.js +607 -0
- package/dist/local/analyze/frameworks.js +115 -0
- package/dist/local/analyze/linkage/conventions.js +160 -0
- package/dist/local/analyze/parseCache.js +164 -0
- package/dist/local/analyze/selfAssert.js +53 -0
- package/dist/local/analyze/symbols.js +430 -0
- package/dist/local/analyze/testLayer.js +135 -0
- package/dist/local/analyze/treeSitter/engine.js +1253 -0
- package/dist/local/analyze/treeSitter/languages.js +101 -0
- package/dist/local/autoProve.js +620 -0
- package/dist/local/cli.js +1468 -0
- package/dist/local/cliArgs.js +112 -0
- package/dist/local/corpusScope.js +162 -0
- package/dist/local/enrich/csv.js +348 -0
- package/dist/local/enrich/index.js +43 -0
- package/dist/local/enrich/markdown.js +193 -0
- package/dist/local/explain/explain.js +91 -0
- package/dist/local/exportCli.js +26 -0
- package/dist/local/flows/flowWalker.js +215 -0
- package/dist/local/flows/llmFlowDiscovery.js +567 -0
- package/dist/local/freshness/changed.js +280 -0
- package/dist/local/freshness/manifest.js +35 -0
- package/dist/local/freshness/status.js +30 -0
- package/dist/local/gaps/gaps.js +114 -0
- package/dist/local/generate/buckets.js +73 -0
- package/dist/local/generate/compareJudge.js +124 -0
- package/dist/local/generate/compareReport.js +538 -0
- package/dist/local/generate/compareScore.js +105 -0
- package/dist/local/generate/deriveImports.js +91 -0
- package/dist/local/generate/generator.js +2586 -0
- package/dist/local/generate/prompt.js +144 -0
- package/dist/local/generate/promptV5.js +438 -0
- package/dist/local/generate/providers.js +400 -0
- package/dist/local/generate/runHints.js +304 -0
- package/dist/local/graph/citations.js +73 -0
- package/dist/local/graph/confirmable.js +72 -0
- package/dist/local/graph/factories.js +210 -0
- package/dist/local/graph/ontology.js +18 -0
- package/dist/local/interactive.js +53 -0
- package/dist/local/jobs/jobStore.js +80 -0
- package/dist/local/jobs/notify.js +29 -0
- package/dist/local/jobs/runner.js +75 -0
- package/dist/local/ledger.js +117 -0
- package/dist/local/localConfig.js +112 -0
- package/dist/local/mcp.js +548 -0
- package/dist/local/operations.js +1749 -0
- package/dist/local/pack/coverageReport.js +192 -0
- package/dist/local/pack/exporter.js +195 -0
- package/dist/local/pack/schema.js +128 -0
- package/dist/local/pack/summary.js +127 -0
- package/dist/local/pack/validate.js +25 -0
- package/dist/local/proofRunnability.js +366 -0
- package/dist/local/recipe/dbSqljs.js +255 -0
- package/dist/local/reprove/paths.js +13 -0
- package/dist/local/reprove/scoped.js +136 -0
- package/dist/local/resolve/barrelWalker.js +178 -0
- package/dist/local/resolve/exportIndex.js +270 -0
- package/dist/local/resolve/importGraph.js +347 -0
- package/dist/local/resolve/resolver.js +122 -0
- package/dist/local/resolve/resolverCache.js +117 -0
- package/dist/local/rtm.js +413 -0
- package/dist/local/score/coverage.js +99 -0
- package/dist/local/score/doctor.js +67 -0
- package/dist/local/score/risk.js +362 -0
- package/dist/local/score/score.js +182 -0
- package/dist/local/types.js +1 -0
- package/dist/local/util/hash.js +16 -0
- package/dist/local/util/ids.js +16 -0
- package/dist/local/util/progress.js +8 -0
- package/dist/local/util/redact.js +39 -0
- package/dist/local/util/time.js +1 -0
- package/dist/local/util/walk.js +174 -0
- package/dist/local/viz/behaviorReportData.js +367 -0
- package/dist/local/viz/behaviorReportHtml.js +664 -0
- package/dist/local/viz/d3.bundle.js +3 -0
- package/dist/local/viz/html.js +1152 -0
- package/dist/local/viz/payload.js +525 -0
- package/dist/local/workspace.js +99 -0
- package/docs/agent-workflow.md +167 -0
- package/docs/agents/claude-code.md +43 -0
- package/docs/agents/codex.md +52 -0
- package/docs/agents/cursor.md +39 -0
- package/docs/agents/opencode.md +43 -0
- package/docs/agents/vscode.md +34 -0
- package/docs/local-proof-kit.md +269 -0
- package/package.json +92 -0
- package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
- package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
- package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
- package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
- 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
|
+
}
|