@orangepro/orangepro-mcp 0.2.9 → 0.2.11
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/dist/local/analyze/analyzer.js +64 -0
- package/dist/local/analyze/parseCache.js +5 -1
- package/dist/local/analyze/symbols.js +51 -4
- package/dist/local/operations.js +4 -1
- package/dist/local/score/risk.js +108 -26
- package/dist/local/viz/behaviorReportData.js +56 -21
- package/dist/local/viz/behaviorReportHtml.js +24 -8
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
3
4
|
import { LOCAL_GRAPH_SCHEMA_VERSION } from "../graph/ontology.js";
|
|
4
5
|
import { makeCandidateEdge, makeEdge, makeNode, makeProofEdges, makeTestCaseNode } from "../graph/factories.js";
|
|
5
6
|
import { hashString } from "../util/hash.js";
|
|
@@ -205,6 +206,69 @@ function packagePublicEntryPaths(root, relPaths) {
|
|
|
205
206
|
addCandidate(packageDir, `${conventional}${extension}`);
|
|
206
207
|
}
|
|
207
208
|
}
|
|
209
|
+
// Public package entries are often zero-logic barrels. Follow only explicit,
|
|
210
|
+
// relative AST re-exports so the callable implementation remains public
|
|
211
|
+
// without widening every internal helper into the denominator. Handles both
|
|
212
|
+
// ESM barrels and the conventional CommonJS `module.exports = require(...)`.
|
|
213
|
+
const queued = [...entries];
|
|
214
|
+
const visited = new Set();
|
|
215
|
+
const addRelativeTarget = (fromFile, rawTarget) => {
|
|
216
|
+
if (!rawTarget.startsWith("."))
|
|
217
|
+
return;
|
|
218
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), rawTarget));
|
|
219
|
+
if (base === ".." || base.startsWith("../"))
|
|
220
|
+
return;
|
|
221
|
+
const candidates = new Set([base]);
|
|
222
|
+
const withoutExt = base.replace(RUNTIME_SOURCE_EXT_RE, "");
|
|
223
|
+
for (const extension of extensions) {
|
|
224
|
+
candidates.add(`${withoutExt}${extension}`);
|
|
225
|
+
candidates.add(`${withoutExt}/index${extension}`);
|
|
226
|
+
}
|
|
227
|
+
for (const candidate of candidates) {
|
|
228
|
+
if (normalizedPaths.has(candidate) && !entries.has(candidate)) {
|
|
229
|
+
entries.add(candidate);
|
|
230
|
+
queued.push(candidate);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
while (queued.length) {
|
|
235
|
+
const entry = queued.shift();
|
|
236
|
+
if (!entry || visited.has(entry))
|
|
237
|
+
continue;
|
|
238
|
+
visited.add(entry);
|
|
239
|
+
try {
|
|
240
|
+
const source = ts.createSourceFile(entry, readFileSync(path.join(root, entry), "utf8"), ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
241
|
+
for (const statement of source.statements) {
|
|
242
|
+
if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
243
|
+
addRelativeTarget(entry, statement.moduleSpecifier.text);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!ts.isExpressionStatement(statement))
|
|
247
|
+
continue;
|
|
248
|
+
let assignment = ts.isBinaryExpression(statement.expression) && statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
249
|
+
? statement.expression
|
|
250
|
+
: undefined;
|
|
251
|
+
while (assignment) {
|
|
252
|
+
const left = assignment.left;
|
|
253
|
+
const isModuleExports = ts.isPropertyAccessExpression(left)
|
|
254
|
+
&& ts.isIdentifier(left.expression)
|
|
255
|
+
&& left.expression.text === "module"
|
|
256
|
+
&& left.name.text === "exports";
|
|
257
|
+
if (isModuleExports && ts.isCallExpression(assignment.right) && ts.isIdentifier(assignment.right.expression) && assignment.right.expression.text === "require") {
|
|
258
|
+
const [arg] = assignment.right.arguments;
|
|
259
|
+
if (arg && ts.isStringLiteral(arg))
|
|
260
|
+
addRelativeTarget(entry, arg.text);
|
|
261
|
+
}
|
|
262
|
+
assignment = ts.isBinaryExpression(assignment.right) && assignment.right.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
263
|
+
? assignment.right
|
|
264
|
+
: undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// An unreadable barrel cannot widen the public surface; keep the manifest entry only.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
208
272
|
return entries;
|
|
209
273
|
}
|
|
210
274
|
const FLOW_LINK_STOPWORDS = new Set([
|
|
@@ -55,7 +55,11 @@ import { createRequire } from "node:module";
|
|
|
55
55
|
// Swift protocol methods, and Rust trait signatures while dropping Rust aliases.
|
|
56
56
|
// v16: symbol extraction now carries source line spans for runtime coverage
|
|
57
57
|
// report ingestion; warm v15 entries lack the ranges and cannot be mapped.
|
|
58
|
-
|
|
58
|
+
// v17: Go method symbols are receiver-qualified (Recv.M + member_of).
|
|
59
|
+
// v18: TS/JS extracts direct callable CommonJS exports.
|
|
60
|
+
// v19: chained assignments (`exports = module.exports = fn`) also expose the
|
|
61
|
+
// callable subject; warm v18 entries can still omit conventional CJS entries.
|
|
62
|
+
export const PARSER_VERSION = 19;
|
|
59
63
|
/** Tool package version, folded into the cache guard so UPGRADES auto-invalidate
|
|
60
64
|
* the cache — bumping PARSER_VERSION by hand is a discipline; this is a lock.
|
|
61
65
|
* (The stale-cache incident: upgraded binary served old per-file results.) */
|
|
@@ -283,10 +283,11 @@ function collectTsJsExports(content) {
|
|
|
283
283
|
const localDecls = new Map();
|
|
284
284
|
const classNodes = new Map(); // for default-subject member extraction
|
|
285
285
|
const defaultExprs = [];
|
|
286
|
+
const commonJsAssignments = [];
|
|
286
287
|
for (const stmt of sf.statements) {
|
|
287
288
|
if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
288
289
|
const lines = nodeLines(sf, stmt);
|
|
289
|
-
localDecls.set(stmt.name.text, { kind: "function", eligible: true, lines });
|
|
290
|
+
localDecls.set(stmt.name.text, { kind: "function", eligible: true, callable: true, lines });
|
|
290
291
|
if (hasExportModifier(stmt))
|
|
291
292
|
record(stmt.name.text, "function", false, undefined, lines);
|
|
292
293
|
}
|
|
@@ -297,7 +298,7 @@ function collectTsJsExports(content) {
|
|
|
297
298
|
if (ts.canHaveModifiers(stmt) && ts.getModifiers(stmt)?.some((mod) => mod.kind === ts.SyntaxKind.DeclareKeyword))
|
|
298
299
|
continue;
|
|
299
300
|
const lines = nodeLines(sf, stmt);
|
|
300
|
-
localDecls.set(stmt.name.text, { kind: "class", eligible: true, lines });
|
|
301
|
+
localDecls.set(stmt.name.text, { kind: "class", eligible: true, callable: true, lines });
|
|
301
302
|
classNodes.set(stmt.name.text, stmt);
|
|
302
303
|
if (hasExportModifier(stmt)) {
|
|
303
304
|
record(stmt.name.text, "class", false, undefined, lines);
|
|
@@ -312,7 +313,7 @@ function collectTsJsExports(content) {
|
|
|
312
313
|
const init = decl.initializer ? unwrapInitializer(decl.initializer) : undefined;
|
|
313
314
|
const callable = !!init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
|
|
314
315
|
const lines = nodeLines(sf, decl);
|
|
315
|
-
localDecls.set(decl.name.text, { kind: "const", eligible: isComponentLikeConst(decl.name.text, init), lines });
|
|
316
|
+
localDecls.set(decl.name.text, { kind: "const", eligible: isComponentLikeConst(decl.name.text, init), callable, lines });
|
|
316
317
|
if (exported)
|
|
317
318
|
record(decl.name.text, "const", callable, undefined, lines); // regular exports keep the strict callable rule
|
|
318
319
|
}
|
|
@@ -322,6 +323,27 @@ function collectTsJsExports(content) {
|
|
|
322
323
|
// (`export { default } from './x'`) are ExportDeclarations, never collected.
|
|
323
324
|
defaultExprs.push(stmt.expression);
|
|
324
325
|
}
|
|
326
|
+
else if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
327
|
+
let assignment = stmt.expression;
|
|
328
|
+
while (assignment) {
|
|
329
|
+
const left = assignment.left;
|
|
330
|
+
if (ts.isPropertyAccessExpression(left)) {
|
|
331
|
+
const directDefault = ts.isIdentifier(left.expression) && left.expression.text === "module" && left.name.text === "exports";
|
|
332
|
+
const namedOnExports = ts.isIdentifier(left.expression) && left.expression.text === "exports";
|
|
333
|
+
const namedOnModule = ts.isPropertyAccessExpression(left.expression)
|
|
334
|
+
&& ts.isIdentifier(left.expression.expression)
|
|
335
|
+
&& left.expression.expression.text === "module"
|
|
336
|
+
&& left.expression.name.text === "exports";
|
|
337
|
+
if (directDefault)
|
|
338
|
+
commonJsAssignments.push({ exportName: null, expression: assignment.right });
|
|
339
|
+
else if (namedOnExports || namedOnModule)
|
|
340
|
+
commonJsAssignments.push({ exportName: left.name.text, expression: assignment.right });
|
|
341
|
+
}
|
|
342
|
+
assignment = ts.isBinaryExpression(assignment.right) && assignment.right.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
343
|
+
? assignment.right
|
|
344
|
+
: undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
325
347
|
}
|
|
326
348
|
// A default export is the file's primary behavior. Record its LOCAL subject
|
|
327
349
|
// only when it is a function/class or a component-like const — skips require()
|
|
@@ -341,6 +363,31 @@ function collectTsJsExports(content) {
|
|
|
341
363
|
recordClassMethods(sf, node, name); // default-exported class → its methods
|
|
342
364
|
}
|
|
343
365
|
}
|
|
366
|
+
// CommonJS public surface, judged by the same AST-backed callable bar as ESM:
|
|
367
|
+
// module.exports = localFn, exports.name = localFn, or an inline function.
|
|
368
|
+
// Re-export shims and config/object assignments remain excluded.
|
|
369
|
+
for (const assignment of commonJsAssignments) {
|
|
370
|
+
const expr = unwrapInitializer(assignment.expression);
|
|
371
|
+
if (ts.isIdentifier(expr)) {
|
|
372
|
+
const d = localDecls.get(expr.text);
|
|
373
|
+
if (!d || !d.callable)
|
|
374
|
+
continue;
|
|
375
|
+
record(expr.text, d.kind, d.kind === "const", undefined, d.lines);
|
|
376
|
+
if (d.kind === "class") {
|
|
377
|
+
const node = classNodes.get(expr.text);
|
|
378
|
+
if (node)
|
|
379
|
+
recordClassMethods(sf, node, expr.text);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else if (ts.isFunctionExpression(expr) || ts.isArrowFunction(expr)) {
|
|
383
|
+
const name = assignment.exportName ?? (ts.isFunctionExpression(expr) && expr.name ? expr.name.text : undefined);
|
|
384
|
+
if (name)
|
|
385
|
+
record(name, "function", false, undefined, nodeLines(sf, expr));
|
|
386
|
+
}
|
|
387
|
+
else if (ts.isClassExpression(expr) && expr.name) {
|
|
388
|
+
record(expr.name.text, "class", false, undefined, nodeLines(sf, expr));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
344
391
|
}
|
|
345
392
|
return acc;
|
|
346
393
|
}
|
|
@@ -381,7 +428,7 @@ export function extractSymbolsWithMeta(content, language) {
|
|
|
381
428
|
// TS/JS from the AST — comment/string-safe. Cheap gate: any "export"
|
|
382
429
|
// followed by whitespace (matches the old `export\s+`, so `export\tfunction`
|
|
383
430
|
// / `export\nclass` are NOT skipped).
|
|
384
|
-
if (/export\s
|
|
431
|
+
if (/export\s|module\s*\.\s*exports|exports\s*\./.test(content)) {
|
|
385
432
|
for (const [name, sym] of collectTsJsExports(content))
|
|
386
433
|
consider(name, sym.kind, sym.callable, sym.member_of, sym);
|
|
387
434
|
}
|
package/dist/local/operations.js
CHANGED
|
@@ -2003,7 +2003,10 @@ export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-co
|
|
|
2003
2003
|
// proof-attempts sidecar ONLY when it anchors to the current graph+commit
|
|
2004
2004
|
// (stale evidence is dropped — fail closed; display copy only, no tier math).
|
|
2005
2005
|
const dyn = dynamicProof ?? sidecarDynamicProof(root, graph);
|
|
2006
|
-
|
|
2006
|
+
// Artifacts may be written from a different working directory when callers
|
|
2007
|
+
// use `opro start <source>`. Risk scoring must follow the analyzed source
|
|
2008
|
+
// root recorded in the graph, never the artifact/output root.
|
|
2009
|
+
const data = buildBehaviorReportData(graph, loadLedger(root), { repoRoot: graph.workspace.root, dynamicProof: dyn });
|
|
2007
2010
|
// Delta-since-last-run: best-effort read of the previous snapshot; a missing
|
|
2008
2011
|
// or unreadable baseline means first run (banner hidden). Display-only —
|
|
2009
2012
|
// the delta never touches tiers, ranks, or counts.
|
package/dist/local/score/risk.js
CHANGED
|
@@ -24,13 +24,63 @@ function confirmedBehaviorIds(graph) {
|
|
|
24
24
|
if (nodeKinds.get(e.to_external_id) === "CodeSymbol" || nodeKinds.get(e.to_external_id) === "Requirement")
|
|
25
25
|
ids.add(e.to_external_id);
|
|
26
26
|
}
|
|
27
|
+
// A container type whose every method child is confirmed has no distinct
|
|
28
|
+
// untested surface left — suppress it from the gap ranking rather than
|
|
29
|
+
// listing it as an unlinked candidate above its own proven methods.
|
|
30
|
+
const symbolIds = graph.nodes.filter((n) => n.kind === "CodeSymbol").map((n) => n.external_id);
|
|
31
|
+
for (const id of symbolIds) {
|
|
32
|
+
if (ids.has(id))
|
|
33
|
+
continue;
|
|
34
|
+
const children = symbolIds.filter((s) => s.startsWith(id + "."));
|
|
35
|
+
if (children.length > 0 && children.every((c) => ids.has(c)))
|
|
36
|
+
ids.add(id);
|
|
37
|
+
}
|
|
27
38
|
return ids;
|
|
28
39
|
}
|
|
29
40
|
const GIT_CHURN_BATCH = 200;
|
|
41
|
+
export function inspectRiskInputHealth(root, churnWindow) {
|
|
42
|
+
const unavailableWindow = churnWindow ?? "180 days before HEAD";
|
|
43
|
+
if (!root)
|
|
44
|
+
return { sourceRoot: null, gitRoot: null, commit: null, commitDate: null, history: "unavailable", churnWindow: unavailableWindow, churnAvailable: false, reason: "source root unavailable" };
|
|
45
|
+
try {
|
|
46
|
+
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim();
|
|
47
|
+
const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim();
|
|
48
|
+
const commitDate = execFileSync("git", ["show", "-s", "--format=%cI", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim();
|
|
49
|
+
const shallow = execFileSync("git", ["rev-parse", "--is-shallow-repository"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() === "true";
|
|
50
|
+
let partial = false;
|
|
51
|
+
try {
|
|
52
|
+
partial = execFileSync("git", ["config", "--get-regexp", "^remote\\..*\\.promisor$"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 })
|
|
53
|
+
.split(/\r?\n/)
|
|
54
|
+
.some((line) => /\btrue$/i.test(line.trim()));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// A normal full clone has no promisor-remote configuration.
|
|
58
|
+
}
|
|
59
|
+
const commitMs = Date.parse(commitDate);
|
|
60
|
+
const resolvedWindow = churnWindow ?? (Number.isFinite(commitMs) ? new Date(commitMs - 180 * 24 * 60 * 60 * 1000).toISOString() : unavailableWindow);
|
|
61
|
+
return {
|
|
62
|
+
sourceRoot: root,
|
|
63
|
+
gitRoot,
|
|
64
|
+
commit,
|
|
65
|
+
commitDate,
|
|
66
|
+
history: shallow ? "shallow" : partial ? "partial" : "full",
|
|
67
|
+
churnWindow: resolvedWindow,
|
|
68
|
+
churnAvailable: !shallow && !partial,
|
|
69
|
+
reason: shallow
|
|
70
|
+
? "shallow Git history cannot support a complete churn window"
|
|
71
|
+
: partial
|
|
72
|
+
? "partial-clone Git objects cannot guarantee a complete offline churn window"
|
|
73
|
+
: undefined
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { sourceRoot: root, gitRoot: null, commit: null, commitDate: null, history: "unavailable", churnWindow: unavailableWindow, churnAvailable: false, reason: "Git history could not be read from the analyzed source root" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
30
80
|
function gitChurn(root, files, window) {
|
|
31
81
|
const out = new Map();
|
|
32
82
|
if (!root || files.length === 0)
|
|
33
|
-
return out;
|
|
83
|
+
return { values: out, complete: Boolean(root) };
|
|
34
84
|
for (let i = 0; i < files.length; i += GIT_CHURN_BATCH) {
|
|
35
85
|
const batch = files.slice(i, i + GIT_CHURN_BATCH);
|
|
36
86
|
try {
|
|
@@ -51,10 +101,10 @@ function gitChurn(root, files, window) {
|
|
|
51
101
|
}
|
|
52
102
|
}
|
|
53
103
|
catch {
|
|
54
|
-
|
|
104
|
+
return { values: new Map(), complete: false };
|
|
55
105
|
}
|
|
56
106
|
}
|
|
57
|
-
return out;
|
|
107
|
+
return { values: out, complete: true };
|
|
58
108
|
}
|
|
59
109
|
function gitFirstCommitBatch(root, files) {
|
|
60
110
|
const out = new Map();
|
|
@@ -137,18 +187,25 @@ function deriveRouteWeight(node) {
|
|
|
137
187
|
return 2;
|
|
138
188
|
}
|
|
139
189
|
function deriveDataSensitivity(node) {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
[
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
190
|
+
const raw = `${node.external_id} ${symbolFile(node)} ${symbolTitle(node)}`;
|
|
191
|
+
const tokens = new Set(raw
|
|
192
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
193
|
+
.toLowerCase()
|
|
194
|
+
.split(/[^a-z0-9]+/)
|
|
195
|
+
.filter(Boolean));
|
|
196
|
+
const has = (...values) => values.some((value) => tokens.has(value));
|
|
197
|
+
// `capture` alone is not a payment signal (for example CapturePanic). It is
|
|
198
|
+
// payment-sensitive only when the same symbol/path also contains payment context.
|
|
199
|
+
if (has("payment", "stripe", "refund", "charge", "billing", "payout", "chargeback") || (has("capture") && has("payment", "stripe", "transaction")))
|
|
200
|
+
return 10;
|
|
201
|
+
if (has("auth", "token", "session", "password", "credential", "jwt", "oauth"))
|
|
202
|
+
return 9;
|
|
203
|
+
if (has("order", "cart", "checkout", "invoice", "transaction"))
|
|
204
|
+
return 7;
|
|
205
|
+
if (has("customer", "user", "account", "profile", "pii", "gdpr"))
|
|
206
|
+
return 6;
|
|
207
|
+
if (has("notification", "email", "sms", "webhook", "push"))
|
|
208
|
+
return 3;
|
|
152
209
|
return 1;
|
|
153
210
|
}
|
|
154
211
|
export function buildFlowDepthContext(graph) {
|
|
@@ -281,9 +338,16 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
281
338
|
symbolsByFile.set(file, [s]);
|
|
282
339
|
}
|
|
283
340
|
const files = [...new Set(symbols.map(symbolFile))];
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const
|
|
341
|
+
const repoRoot = opts.repoRoot ?? graph.workspace.root;
|
|
342
|
+
const inputHealth = inspectRiskInputHealth(repoRoot, opts.churnWindow);
|
|
343
|
+
const churnWindow = inputHealth.churnWindow;
|
|
344
|
+
const churnResult = inputHealth.churnAvailable ? gitChurn(repoRoot, files, churnWindow) : { values: new Map(), complete: false };
|
|
345
|
+
const churn = churnResult.values;
|
|
346
|
+
const churnAvailable = inputHealth.churnAvailable && churnResult.complete;
|
|
347
|
+
const firstCommitTs = churnAvailable ? gitFirstCommitBatch(repoRoot, files) : new Map();
|
|
348
|
+
const commitMs = Date.parse(inputHealth.commitDate ?? "");
|
|
349
|
+
const graphMs = Date.parse(graph.updated_at || graph.created_at || "");
|
|
350
|
+
const nowSec = Math.floor((Number.isFinite(commitMs) ? commitMs : Number.isFinite(graphMs) ? graphMs : 0) / 1000);
|
|
287
351
|
// Method-level attribution. CALLS edges are already symbol-granular and count
|
|
288
352
|
// at full weight. IMPORTS edges are file-granular: previously every symbol in
|
|
289
353
|
// an imported file inherited the file's full import count, which made all 17
|
|
@@ -347,11 +411,13 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
347
411
|
const score = Math.round((incoming_refs * 0.4 + churnForScore * 0.4 + (isEntry ? 20 : 0)) * 10) / 10;
|
|
348
412
|
const reasons = [
|
|
349
413
|
`${incoming_refs} incoming structural reference${incoming_refs === 1 ? "" : "s"}`,
|
|
350
|
-
|
|
414
|
+
churnAvailable
|
|
415
|
+
? `${git_churn} git churn line${git_churn === 1 ? "" : "s"} in 180 days${git_churn > 500 ? " (score capped at 500)" : ""}`
|
|
416
|
+
: "Git churn unavailable — provisional static-only ranking"
|
|
351
417
|
];
|
|
352
418
|
if (isEntry)
|
|
353
419
|
reasons.push("near an API/route/handler entry point");
|
|
354
|
-
return { id: s.external_id, title: s.title || s.external_id, file, risk_score: score, incoming_refs, git_churn, entry_point: isEntry, reasons };
|
|
420
|
+
return { id: s.external_id, title: s.title || s.external_id, file, risk_score: score, incoming_refs, git_churn, churn_available: churnAvailable, entry_point: isEntry, reasons };
|
|
355
421
|
})
|
|
356
422
|
.sort((a, b) => b.risk_score - a.risk_score || b.incoming_refs - a.incoming_refs || b.git_churn - a.git_churn || a.id.localeCompare(b.id))
|
|
357
423
|
.slice(0, limit);
|
|
@@ -390,14 +456,16 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
390
456
|
const d = rawScores[idx].d;
|
|
391
457
|
const detectionTier = detectionFor(s.external_id);
|
|
392
458
|
let score = Math.round(pExact * iExact * d * 10) / 10;
|
|
393
|
-
const disconnected =
|
|
459
|
+
const disconnected = incoming_refs === 0 && fan_out === 0;
|
|
394
460
|
if (disconnected)
|
|
395
461
|
score = Math.round(score * 0.25 * 10) / 10;
|
|
396
462
|
const reasons = [
|
|
397
463
|
`ORS ${score} ≈ P${p} × I${i} × D${d}`,
|
|
398
464
|
`${incoming_refs} incoming structural reference${incoming_refs === 1 ? "" : "s"} (method-attributed)`,
|
|
399
|
-
|
|
400
|
-
|
|
465
|
+
churnAvailable
|
|
466
|
+
? `${git_churn} git churn line${git_churn === 1 ? "" : "s"} attributed to this symbol in 180 days`
|
|
467
|
+
: "Git churn unavailable — provisional static-only ranking",
|
|
468
|
+
`route weight ${route_weight}, data sensitivity ${data_sensitivity}, flow position ${flow_position}, complexity ${complexity_proxy}, fan-out ${fan_out}`
|
|
401
469
|
];
|
|
402
470
|
if (isEntry)
|
|
403
471
|
reasons.push("near an API/route/handler entry point");
|
|
@@ -414,6 +482,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
414
482
|
risk_score: score,
|
|
415
483
|
incoming_refs,
|
|
416
484
|
git_churn,
|
|
485
|
+
churn_available: churnAvailable,
|
|
417
486
|
entry_point: isEntry,
|
|
418
487
|
reasons,
|
|
419
488
|
probability: p,
|
|
@@ -438,7 +507,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
438
507
|
const perFile = new Map();
|
|
439
508
|
// Multi-program repos flood identical titles (76 x main) across files; the
|
|
440
509
|
// per-FILE cap cannot see it. Same diversity principle, second axis.
|
|
441
|
-
const maxPerTitle = 2;
|
|
510
|
+
const maxPerTitle = Math.max(1, opts.maxPerTitle ?? 2);
|
|
442
511
|
const perTitle = new Map();
|
|
443
512
|
const surfaced = [];
|
|
444
513
|
const overflow = [];
|
|
@@ -457,8 +526,21 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
457
526
|
if (surfaced.length >= limit)
|
|
458
527
|
break;
|
|
459
528
|
}
|
|
460
|
-
|
|
461
|
-
|
|
529
|
+
// A report-level title cap is a hard product constraint: relaxing it during
|
|
530
|
+
// backfill recreates duplicate Invoke/Config cards. We may relax only the
|
|
531
|
+
// per-file cap to fill remaining slots with distinct behavior titles.
|
|
532
|
+
if (surfaced.length < limit) {
|
|
533
|
+
for (const gap of overflow) {
|
|
534
|
+
const tKey = (gap.title || "").split("(")[0].trim();
|
|
535
|
+
const tUsed = perTitle.get(tKey) ?? 0;
|
|
536
|
+
if (tUsed >= maxPerTitle)
|
|
537
|
+
continue;
|
|
538
|
+
perTitle.set(tKey, tUsed + 1);
|
|
539
|
+
surfaced.push(gap);
|
|
540
|
+
if (surfaced.length >= limit)
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
462
544
|
// Guarantee: the surfaced list is ALWAYS highest-risk-first, even when the
|
|
463
545
|
// per-file diversity backfill re-admits overflow items (which otherwise land
|
|
464
546
|
// appended after lower-scored rows).
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { buildRtm } from "../rtm.js";
|
|
3
|
-
import { isEntryPoint, rankRiskGaps } from "../score/risk.js";
|
|
4
|
+
import { inspectRiskInputHealth, isEntryPoint, rankRiskGaps } from "../score/risk.js";
|
|
5
|
+
import { ORANGEPRO_VERSION } from "../version.js";
|
|
4
6
|
import { PROOF_BLOCKER_GUIDE } from "../proofDoctor.js";
|
|
5
7
|
/** Short human phrase per R-1 needs_setup category, for the "blocked because: …" panel copy. */
|
|
6
8
|
const BLOCK_CATEGORY_LABEL = {
|
|
@@ -105,13 +107,27 @@ function isNoneTier(tier) {
|
|
|
105
107
|
return tier !== "proven" && tier !== "associated" && tier !== "runtime" && tier !== "candidate";
|
|
106
108
|
}
|
|
107
109
|
function summaryFromRows(rows, flowIds) {
|
|
110
|
+
// buildRtm intentionally unions valid proof rows that fall outside the static
|
|
111
|
+
// denominator. They remain visible and count as Dynamically Proven, but must
|
|
112
|
+
// never increase the "Methods found" denominator.
|
|
113
|
+
const denominatorRows = rows.filter((r) => r.off_denominator !== true);
|
|
108
114
|
const proven = rows.filter((r) => r.evidence_tier === "proven").length;
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
115
|
+
const provenOutsideDenominator = rows.filter((r) => r.off_denominator === true && r.evidence_tier === "proven").length;
|
|
116
|
+
const associated = denominatorRows.filter((r) => r.evidence_tier === "associated" || r.evidence_tier === "runtime").length;
|
|
117
|
+
const candidate = denominatorRows.filter((r) => r.evidence_tier === "candidate").length;
|
|
118
|
+
const noneRows = denominatorRows.filter((r) => isNoneTier(r.evidence_tier));
|
|
112
119
|
// DISPLAY-ONLY split of `none`: a none-tier symbol that shows up in a static flow is "Reachable Untested".
|
|
113
120
|
const reachableUntested = noneRows.filter((r) => flowIds.has(r.behavior_id)).length;
|
|
114
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
total: denominatorRows.length,
|
|
123
|
+
proven,
|
|
124
|
+
...(provenOutsideDenominator > 0 ? { provenOutsideDenominator } : {}),
|
|
125
|
+
associated,
|
|
126
|
+
candidate,
|
|
127
|
+
none: noneRows.length,
|
|
128
|
+
reachableUntested,
|
|
129
|
+
noSignal: noneRows.length - reachableUntested
|
|
130
|
+
};
|
|
115
131
|
}
|
|
116
132
|
/** Verbatim 0-dynamic-proof explainer copy. Rendered only when summary.proven === 0. */
|
|
117
133
|
const ZERO_PROOF_EXPLAINER = {
|
|
@@ -393,7 +409,7 @@ function riskGeneratedTests(graph, gap, riskIds, isFirstRowForFile) {
|
|
|
393
409
|
// the assertion line stays pure metadata (framework, same-file, disclosure).
|
|
394
410
|
assertion: [
|
|
395
411
|
sameFile ? "same-file target" : "",
|
|
396
|
-
t.framework_hint,
|
|
412
|
+
t.framework_hint || (gap.file.endsWith(".go") ? "go" : ""),
|
|
397
413
|
t.weak_evidence_used ? "weak evidence disclosed" : ""
|
|
398
414
|
]
|
|
399
415
|
.filter(Boolean)
|
|
@@ -402,12 +418,10 @@ function riskGeneratedTests(graph, gap, riskIds, isFirstRowForFile) {
|
|
|
402
418
|
runnable: t.runnable !== false
|
|
403
419
|
}));
|
|
404
420
|
}
|
|
405
|
-
/** Incoming refs are method-attributed and can be fractional
|
|
406
|
-
*
|
|
421
|
+
/** Incoming refs are method-attributed and can be fractional when a file-level
|
|
422
|
+
* reference is split across its symbols. Preserve that weighting honestly. */
|
|
407
423
|
function fmtRefs(n) {
|
|
408
|
-
|
|
409
|
-
return "<1";
|
|
410
|
-
return String(Math.round(n));
|
|
424
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
411
425
|
}
|
|
412
426
|
function displayTitle(title, file) {
|
|
413
427
|
if (title.includes("."))
|
|
@@ -430,9 +444,12 @@ function riskContext(risk) {
|
|
|
430
444
|
? `${5 - (risk.flow_position ?? 0)} call${5 - (risk.flow_position ?? 0) === 1 ? "" : "s"} from the nearest entry point`
|
|
431
445
|
: "deep in the call graph";
|
|
432
446
|
const refs = fmtRefs(risk.incoming_refs);
|
|
447
|
+
const churn = risk.churn_available !== false
|
|
448
|
+
? `${risk.git_churn} line${risk.git_churn === 1 ? "" : "s"} changed in 180 days`
|
|
449
|
+
: "Git churn unavailable (provisional static-only ranking)";
|
|
433
450
|
const parts = [
|
|
434
451
|
`Sits at ${pos}${sens ? ` on ${sens} paths` : ""}.`,
|
|
435
|
-
`${refs}
|
|
452
|
+
`${refs} weighted incoming reference${risk.incoming_refs === 1 ? "" : "s"}, ${risk.fan_out ?? 0} downstream call${(risk.fan_out ?? 0) === 1 ? "" : "s"}, ${churn} — and no test proves its behavior.`
|
|
436
453
|
];
|
|
437
454
|
return parts.join(" ");
|
|
438
455
|
}
|
|
@@ -748,12 +765,11 @@ function riskTodo(risk, verb, path, generatedTests) {
|
|
|
748
765
|
: risk.entry_point
|
|
749
766
|
? `invokes ${displayTitle(risk.title, risk.file)} through its entry point`
|
|
750
767
|
: `calls ${displayTitle(risk.title, risk.file)} directly`;
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
: s >= 9
|
|
768
|
+
const sens = (risk.data_sensitivity ?? 1) >= 10
|
|
769
|
+
? " Include a failure case: a rejected payment must leave no partial state."
|
|
770
|
+
: (risk.data_sensitivity ?? 1) >= 9
|
|
755
771
|
? " Include a negative case: invalid or expired credentials must fail closed."
|
|
756
|
-
:
|
|
772
|
+
: (risk.data_sensitivity ?? 1) >= 7
|
|
757
773
|
? " Include a failure case: a rejected transaction must leave no partial state."
|
|
758
774
|
: "";
|
|
759
775
|
if (risk.integration_signal === "candidate") {
|
|
@@ -789,9 +805,11 @@ function riskRows(risks, graph) {
|
|
|
789
805
|
const methodMatch = risk.title.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i);
|
|
790
806
|
const tags = [];
|
|
791
807
|
const bucket = riskBucket(risk.risk_score, maxRiskScore);
|
|
792
|
-
if (
|
|
808
|
+
if (risk.churn_available === false)
|
|
809
|
+
tags.push(["provisional rank", "info"]);
|
|
810
|
+
else if (bucket)
|
|
793
811
|
tags.push([`${bucket} risk`, "risk"]);
|
|
794
|
-
tags.push([`${fmtRefs(risk.incoming_refs)}
|
|
812
|
+
tags.push([`${fmtRefs(risk.incoming_refs)} weighted refs`, "info"]);
|
|
795
813
|
if (risk.entry_point)
|
|
796
814
|
tags.push(["Entry point", "entry"]);
|
|
797
815
|
return {
|
|
@@ -808,7 +826,7 @@ function riskRows(risks, graph) {
|
|
|
808
826
|
].filter((c) => Boolean(c)))];
|
|
809
827
|
return {
|
|
810
828
|
generatedTests,
|
|
811
|
-
applicableCategories: riskApplicableConcerns(risk, verb),
|
|
829
|
+
applicableCategories: [...new Set([...riskApplicableConcerns(risk, verb), ...generatedCategories])],
|
|
812
830
|
generatedCategories,
|
|
813
831
|
verb,
|
|
814
832
|
path,
|
|
@@ -833,7 +851,23 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
833
851
|
const flowIds = flowSymbolIds(graph);
|
|
834
852
|
const summary = summaryFromRows(rows, flowIds);
|
|
835
853
|
const repoRoot = opts.repoRoot ?? graph.workspace.root;
|
|
836
|
-
const riskGaps = rankRiskGaps(graph, { repoRoot, limit: opts.riskLimit ?? 20, maxPerFile: 3 });
|
|
854
|
+
const riskGaps = rankRiskGaps(graph, { repoRoot, limit: opts.riskLimit ?? 20, maxPerFile: 3, maxPerTitle: 1 });
|
|
855
|
+
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
856
|
+
const churnAvailable = riskHealth.churnAvailable && riskGaps.every((risk) => risk.churn_available !== false);
|
|
857
|
+
const provenance = {
|
|
858
|
+
source: path.basename(repoRoot || graph.workspace.name || "repo"),
|
|
859
|
+
gitRoot: riskHealth.gitRoot ? path.basename(riskHealth.gitRoot) : null,
|
|
860
|
+
commit: riskHealth.commit,
|
|
861
|
+
history: riskHealth.history,
|
|
862
|
+
churn: churnAvailable ? "available" : "unavailable",
|
|
863
|
+
churnWindow: riskHealth.churnWindow,
|
|
864
|
+
toolVersion: ORANGEPRO_VERSION,
|
|
865
|
+
inputFingerprint: createHash("sha256")
|
|
866
|
+
.update(JSON.stringify({ root: graph.workspace.root_hash, commit: riskHealth.commit, history: riskHealth.history, churn: churnAvailable, window: riskHealth.churnWindow, version: ORANGEPRO_VERSION }))
|
|
867
|
+
.digest("hex")
|
|
868
|
+
.slice(0, 16),
|
|
869
|
+
reason: churnAvailable ? undefined : (riskHealth.reason ?? "Git churn scan did not complete")
|
|
870
|
+
};
|
|
837
871
|
const lists = behaviorLists(rows, flowIds);
|
|
838
872
|
const risks = riskRows(riskGaps, graph);
|
|
839
873
|
const sortedBehaviors = [...lists.behaviors].sort((a, b) => tierRank(a) - tierRank(b));
|
|
@@ -843,6 +877,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
843
877
|
scanned: (graph.updated_at || graph.created_at || new Date(0).toISOString()).slice(0, 10),
|
|
844
878
|
framework: frameworkLabel(graph),
|
|
845
879
|
analysisKind: summary.proven > 0 ? "static+dynamic" : "static",
|
|
880
|
+
provenance,
|
|
846
881
|
summary,
|
|
847
882
|
proofGuidance: proofGuidance(ledger, summary, opts.dynamicProof),
|
|
848
883
|
pipeline: pipeline(graph, ledger, summary),
|
|
@@ -33,9 +33,11 @@ a{color:var(--blue);text-decoration:none}
|
|
|
33
33
|
.hdr-name{font-size:15px;font-weight:650;letter-spacing:-.01em}
|
|
34
34
|
.hdr-right{font-size:11.5px;color:var(--muted);display:flex;gap:10px;align-items:center}
|
|
35
35
|
.hdr-tag{background:var(--s2);border:1px solid var(--bd);border-radius:4px;padding:2px 7px;font-family:var(--mono);font-size:10.5px;color:var(--ink2)}
|
|
36
|
+
.provenance{margin:10px 0 0;padding:8px 10px;border:1px solid var(--bd);border-radius:7px;background:var(--s1);color:var(--muted);font:10.5px/1.45 var(--mono)}
|
|
37
|
+
.provenance.warn{border-color:var(--rbd);background:var(--rbg);color:var(--red)}
|
|
36
38
|
|
|
37
39
|
/* KPI STRIP */
|
|
38
|
-
.kpis{display:grid;grid-template-columns:repeat(
|
|
40
|
+
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:16px 0 0}
|
|
39
41
|
.kpi{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:13px 14px;position:relative;overflow:hidden}
|
|
40
42
|
.kpi::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.4;background:radial-gradient(90px 70px at 100% 0%,var(--kw,transparent),transparent 70%)}
|
|
41
43
|
.kpi-lbl{font-size:10px;color:var(--muted);font-weight:600;letter-spacing:.05em;text-transform:uppercase;position:relative}
|
|
@@ -46,6 +48,8 @@ a{color:var(--blue);text-decoration:none}
|
|
|
46
48
|
.kpi[data-t="signal"]{--kw:var(--abg)} .kpi[data-t="signal"] .kpi-num{color:var(--amber)}
|
|
47
49
|
.kpi[data-t="reach"]{--kw:var(--bbg)} .kpi[data-t="reach"] .kpi-num{color:var(--blue)}
|
|
48
50
|
.kpi[data-t="nosig"]{--kw:var(--rbg)} .kpi[data-t="nosig"] .kpi-num{color:var(--red)}
|
|
51
|
+
.kpi[data-t="priority"]{--kw:var(--obg)} .kpi[data-t="priority"] .kpi-num{color:var(--orange)}
|
|
52
|
+
.metric-scope{margin:9px 2px 0;color:var(--muted);font-size:11.5px;line-height:1.5}
|
|
49
53
|
|
|
50
54
|
/* TABS */
|
|
51
55
|
nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
@@ -261,12 +265,14 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
261
265
|
<span id="scan-date">—</span>
|
|
262
266
|
</div>
|
|
263
267
|
</header>
|
|
268
|
+
<div class="provenance" id="provenance"></div>
|
|
264
269
|
|
|
265
270
|
<section class="kpis" id="kpis"></section>
|
|
271
|
+
<p class="metric-scope" id="metric-scope"></p>
|
|
266
272
|
|
|
267
273
|
<nav class="tabs" role="tablist">
|
|
268
274
|
<button class="tab" role="tab" aria-selected="true" data-tab="codebase">Your Code</button>
|
|
269
|
-
<button class="tab" role="tab" aria-selected="false" data-tab="risks">
|
|
275
|
+
<button class="tab" role="tab" aria-selected="false" data-tab="risks">Priority gaps <span class="tc" id="t-risk">—</span></button>
|
|
270
276
|
<button class="tab" role="tab" aria-selected="false" data-tab="flows">Flows <span class="tc" id="t-flow">—</span></button>
|
|
271
277
|
<button class="tab" role="tab" aria-selected="false" data-tab="behaviors">Behaviors <span class="tc" id="t-beh">—</span></button>
|
|
272
278
|
</nav>
|
|
@@ -276,7 +282,7 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
276
282
|
<p class="bridge">We scanned your repo and found <b id="br-methods">—</b> public methods across <b id="br-services">—</b> services, with <b id="br-tests">—</b> test files. Here's what we're working with.</p>
|
|
277
283
|
<div id="delta-banner"></div>
|
|
278
284
|
<div class="card" id="sysmap-card" hidden>
|
|
279
|
-
<p class="card-lbl">Your system, as the graph sees it — entry lanes flowing into the services they reach. Node size = flow traffic; color = dominant evidence tier; red ring = top-20 risk.
|
|
285
|
+
<p class="card-lbl">Your system, as the graph sees it — entry lanes flowing into the services they reach. Node size = flow traffic; color = dominant evidence tier; red ring = top-20 risk. Repeatable for the same commit, Git history, configuration, and OrangePro version.</p>
|
|
280
286
|
<div id="sysmap"></div>
|
|
281
287
|
</div>
|
|
282
288
|
<div class="cols2">
|
|
@@ -325,7 +331,7 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
325
331
|
|
|
326
332
|
<!-- TAB 4: RISKS — actionable -->
|
|
327
333
|
<section class="panel" id="panel-risks" role="tabpanel">
|
|
328
|
-
<p class="bridge">
|
|
334
|
+
<p class="bridge">This is the <b>priority-gap worklist</b>, ranked by blast radius and test weakness. It is separate from the coverage-status cards above: <b>Reachable · no test signal</b> is one strict coverage bucket, not the number of priority gaps.</p>
|
|
329
335
|
<p class="bridge" id="risk-cap-note" style="font-size:12px;opacity:.75"></p>
|
|
330
336
|
<div class="risk-tools" id="risk-tools"></div>
|
|
331
337
|
<div id="risk-list"></div>
|
|
@@ -356,7 +362,7 @@ const D=window.DATA,$=(s)=>document.querySelector(s);
|
|
|
356
362
|
var when=new Date(d.baselineTs);
|
|
357
363
|
var ago=isNaN(when.getTime())?"last run":when.toLocaleString();
|
|
358
364
|
if(!d.changed){
|
|
359
|
-
el2.innerHTML='<div class="delta-wrap"><span class="delta-chip dc-none">No changes since last run ('+ago+')
|
|
365
|
+
el2.innerHTML='<div class="delta-wrap"><span class="delta-chip dc-none">No report changes since last run ('+ago+') for the recorded inputs</span></div>';
|
|
360
366
|
return;
|
|
361
367
|
}
|
|
362
368
|
var chips=[];
|
|
@@ -471,6 +477,11 @@ const S=D.summary;
|
|
|
471
477
|
$("#repo-name").textContent=D.repo;
|
|
472
478
|
$("#scan-date").textContent=new Date(D.scanned+"T00:00:00").toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"});
|
|
473
479
|
$("#framework").textContent=D.framework;
|
|
480
|
+
const P=D.provenance;
|
|
481
|
+
const prov=$("#provenance");
|
|
482
|
+
const provisional=P.churn!=="available";
|
|
483
|
+
prov.classList.toggle("warn",provisional);
|
|
484
|
+
prov.textContent=(provisional?"PROVISIONAL RANKING · ":"VERIFIED INPUTS · ")+"source "+P.source+" · commit "+(P.commit?P.commit.slice(0,12):"unavailable")+" · history "+P.history+" · churn "+P.churn+" ("+P.churnWindow+") · OrangePro "+P.toolVersion+" · input "+P.inputFingerprint+(P.reason?" · "+P.reason:"");
|
|
474
485
|
$("#fw-pill").textContent=D.framework;
|
|
475
486
|
|
|
476
487
|
// bridge text in codebase tab
|
|
@@ -484,18 +495,23 @@ $("#t-flow").textContent=D.flows.length;
|
|
|
484
495
|
$("#t-risk").textContent=D.risks.length;
|
|
485
496
|
|
|
486
497
|
// KPIs
|
|
498
|
+
const priorityGapCount=D.risks.length;
|
|
487
499
|
[
|
|
488
500
|
{lbl:"Methods found",num:S.total,sub:"public, with observable outcome",t:"total"},
|
|
489
501
|
{lbl:"Dynamically Proven",num:S.proven,sub:"test breaks if you change it",t:"proven"},
|
|
490
502
|
{lbl:"Test signal",num:S.associated,sub:"hard static test link, no proof",t:"signal"},
|
|
491
503
|
{lbl:"Candidate",num:S.candidate??0,sub:"lexical match only — unconfirmed",t:"cand"},
|
|
492
|
-
{lbl:"Reachable",num:S.reachableUntested,sub:"
|
|
493
|
-
{lbl:"
|
|
504
|
+
{lbl:"Reachable · no test signal",num:S.reachableUntested,sub:"coverage bucket — not the risk count",t:"reach"},
|
|
505
|
+
{lbl:"Unreached · no test signal",num:S.noSignal,sub:"no detected flow or test signal",t:"nosig"},
|
|
506
|
+
{lbl:"Priority gaps",num:priorityGapCount,sub:"top-ranked worklist across unproven tiers",t:"priority"},
|
|
494
507
|
].forEach(k=>{
|
|
495
508
|
const d=el("div","kpi",\`<div class="kpi-lbl">\${k.lbl}</div><div class="kpi-num">\${k.num}</div><div class="kpi-sub">\${k.sub}</div>\`);
|
|
496
509
|
d.dataset.t=k.t;
|
|
497
510
|
$("#kpis").append(d);
|
|
498
511
|
});
|
|
512
|
+
const outsideProofs=S.provenOutsideDenominator??0;
|
|
513
|
+
const coverageAccounted=(S.proven-outsideProofs)+S.associated+(S.candidate??0)+S.reachableUntested+S.noSignal;
|
|
514
|
+
$("#metric-scope").textContent="Coverage status classifies "+coverageAccounted.toLocaleString()+" of "+S.total.toLocaleString()+" mapped behaviors. Priority gaps is a separate ranked worklist of "+priorityGapCount.toLocaleString()+" unproven behaviors; it does not equal the Reachable count."+(outsideProofs?" "+outsideProofs.toLocaleString()+" additional dynamically proven behavior"+(outsideProofs===1?" is":"s are")+" shown outside the static denominator.":"");
|
|
499
515
|
|
|
500
516
|
// codebase
|
|
501
517
|
D.scan.services.forEach(([nm,ct])=>$("#svc-list").append(el("div","svc",\`<span class="nm">\${esc(nm)}</span><span class="ct">\${ct}</span>\`)));
|
|
@@ -630,7 +646,7 @@ document.onkeydown=e=>{if(e.key==="Escape"){drill.classList.remove("open");docum
|
|
|
630
646
|
// flows
|
|
631
647
|
const fl=$("#flow-list");
|
|
632
648
|
D.flows.forEach(f=>{
|
|
633
|
-
const rBadge=f.risk==="critical"?\`<span class="badge b-risk"><span class="d"></span>critical</span
|
|
649
|
+
const rBadge=f.risk==="critical"?\`<span class="badge b-risk"><span class="d"></span>critical</span>\`:f.risk==="high"?\`<span class="badge b-signal"><span class="d"></span>high</span>\`:\`<span class="badge b-info"><span class="d"></span>medium risk</span>\`;
|
|
634
650
|
const pBadge=f.proof==="proven"?\`<span class="badge b-proven"><span class="d"></span>dynamically proven</span>\`:f.proof==="assoc"?\`<span class="badge b-signal"><span class="d"></span>signal</span>\`:\`<span class="badge b-nosig"><span class="d"></span>no proof</span>\`;
|
|
635
651
|
|
|
636
652
|
const totalNodes=(f.trigger?1:0)+f.steps.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OrangePro (`opro`) — a local-first, BYOK CLI + MCP server that builds an evidence graph from a local checkout, ingests runtime coverage, and generates grounded tests. Metadata-only exports; no source upload; generated tests stay local.",
|
|
6
6
|
"license": "MIT",
|