@orangepro/orangepro-mcp 0.2.41 → 0.2.43
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 +415 -42
- package/dist/local/analyze/classify.js +5 -1
- package/dist/local/analyze/confirm.js +90 -6
- package/dist/local/analyze/frameworks.js +3 -0
- package/dist/local/analyze/treeSitter/engine.js +425 -5
- package/dist/local/flows/flowWalker.js +1 -1
- package/dist/local/graph/ontology.js +3 -3
- package/dist/local/operations.js +30 -5
- package/dist/local/proofDoctor.js +5 -4
- package/dist/local/provenance.js +84 -0
- package/dist/local/resolve/importGraph.js +15 -1
- package/dist/local/score/rankEligibility.js +15 -0
- package/dist/local/score/risk.js +99 -26
- package/dist/local/viz/behaviorReportData.js +91 -12
- package/dist/local/viz/behaviorReportHtml.js +19 -2
- package/dist/local/viz/html.js +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { LOCAL_GRAPH_SCHEMA_VERSION } from "./graph/ontology.js";
|
|
2
|
+
import { ORS_VERSION } from "./score/risk.js";
|
|
3
|
+
import { ORANGEPRO_VERSION } from "./version.js";
|
|
4
|
+
import { hashString } from "./util/hash.js";
|
|
5
|
+
export const ARTIFACT_IDENTITY_VERSION = "orangepro.artifact_identity.v1";
|
|
6
|
+
export const ANALYZER_VERSION = "orangepro.analyzer.v2";
|
|
7
|
+
export const PROOF_ORACLE_VERSION = "orangepro.targeted_mutation_oracle.v1";
|
|
8
|
+
function stable(value) {
|
|
9
|
+
if (value === null || typeof value !== "object")
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return `[${value.map(stable).join(",")}]`;
|
|
13
|
+
const record = value;
|
|
14
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stable(record[key])}`).join(",")}}`;
|
|
15
|
+
}
|
|
16
|
+
function digest(value) {
|
|
17
|
+
return hashString(stable(value));
|
|
18
|
+
}
|
|
19
|
+
/** Included paths + bytes after OrangePro ignore/cap rules. Git metadata is deliberately excluded. */
|
|
20
|
+
export function repositorySnapshot(graph) {
|
|
21
|
+
return digest(Object.entries(graph.manifest.files)
|
|
22
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
23
|
+
.map(([path, file]) => ({ path, hash: file.hash })));
|
|
24
|
+
}
|
|
25
|
+
function analysisScope(analysis) {
|
|
26
|
+
return {
|
|
27
|
+
max_files: analysis?.max_files ?? null,
|
|
28
|
+
max_symbols: analysis?.max_symbols ?? null,
|
|
29
|
+
max_inferred_flows: analysis?.max_inferred_flows ?? null,
|
|
30
|
+
files_cap_hit: analysis?.files_cap_hit ?? false,
|
|
31
|
+
symbol_cap_hit: analysis?.symbol_cap_hit ?? false,
|
|
32
|
+
budget_ms: analysis?.not_analyzed_due_to_budget?.budget_ms ?? null,
|
|
33
|
+
flow_options: analysis?.flows?.options ?? null,
|
|
34
|
+
tree_sitter_loaded: [...(analysis?.tree_sitter?.loaded ?? [])].sort(),
|
|
35
|
+
tree_sitter_downgraded: [...(analysis?.tree_sitter?.downgraded ?? [])].sort()
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function buildArtifactIdentity(graph, inputs) {
|
|
39
|
+
const repository_snapshot = repositorySnapshot(graph);
|
|
40
|
+
const analyzer_version = inputs.analyzerVersion ?? ANALYZER_VERSION;
|
|
41
|
+
const ors_version = inputs.orsVersion ?? ORS_VERSION;
|
|
42
|
+
const oracle_version = inputs.oracleVersion ?? PROOF_ORACLE_VERSION;
|
|
43
|
+
const analysis_fingerprint = digest({
|
|
44
|
+
repository_snapshot,
|
|
45
|
+
analyzer_version,
|
|
46
|
+
graph_schema_version: LOCAL_GRAPH_SCHEMA_VERSION,
|
|
47
|
+
source_upload_policy: graph.workspace.source_upload_policy,
|
|
48
|
+
scope: analysisScope(graph.analysis),
|
|
49
|
+
history: inputs.history
|
|
50
|
+
});
|
|
51
|
+
const ranking_fingerprint = digest({ analysis_fingerprint, risk_config_hash: inputs.configHash, ors_version });
|
|
52
|
+
const run_fingerprint = digest({ ranking_fingerprint, oracle_version });
|
|
53
|
+
return {
|
|
54
|
+
schema_version: ARTIFACT_IDENTITY_VERSION,
|
|
55
|
+
repository_snapshot,
|
|
56
|
+
analysis_fingerprint,
|
|
57
|
+
ranking_fingerprint,
|
|
58
|
+
run_fingerprint,
|
|
59
|
+
analyzer_version,
|
|
60
|
+
graph_schema_version: LOCAL_GRAPH_SCHEMA_VERSION,
|
|
61
|
+
ors_version,
|
|
62
|
+
oracle_version,
|
|
63
|
+
risk_config_hash: inputs.configHash,
|
|
64
|
+
tool_version: ORANGEPRO_VERSION,
|
|
65
|
+
git_commit: graph.manifest.git?.commit ?? null,
|
|
66
|
+
git_dirty: graph.manifest.git?.dirty ?? null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function comparisonCompatibility(previous, current) {
|
|
70
|
+
if (!previous || !current)
|
|
71
|
+
return "provenance_incomplete";
|
|
72
|
+
if (previous.schema_version !== ARTIFACT_IDENTITY_VERSION || current.schema_version !== ARTIFACT_IDENTITY_VERSION) {
|
|
73
|
+
return "provenance_incomplete";
|
|
74
|
+
}
|
|
75
|
+
if (previous.repository_snapshot !== current.repository_snapshot)
|
|
76
|
+
return "repository_changed";
|
|
77
|
+
if (previous.analysis_fingerprint !== current.analysis_fingerprint)
|
|
78
|
+
return "analysis_changed";
|
|
79
|
+
if (previous.ranking_fingerprint !== current.ranking_fingerprint)
|
|
80
|
+
return "ranking_changed";
|
|
81
|
+
if (previous.run_fingerprint !== current.run_fingerprint)
|
|
82
|
+
return "experiment_changed";
|
|
83
|
+
return "comparable";
|
|
84
|
+
}
|
|
@@ -110,6 +110,19 @@ export function extractImports(file) {
|
|
|
110
110
|
}
|
|
111
111
|
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, /*setParentNodes*/ false);
|
|
112
112
|
const imports = [];
|
|
113
|
+
const esmockBindings = new Set();
|
|
114
|
+
// `esmock("./module", overrides)` loads the real target module while replacing
|
|
115
|
+
// selected dependencies. Trust the first argument only when the callee is the
|
|
116
|
+
// default binding imported from the actual `esmock` package. A local helper
|
|
117
|
+
// merely named `esmock` must not create a structural module edge.
|
|
118
|
+
for (const statement of sourceFile.statements) {
|
|
119
|
+
if (ts.isImportDeclaration(statement) &&
|
|
120
|
+
ts.isStringLiteralLike(statement.moduleSpecifier) &&
|
|
121
|
+
statement.moduleSpecifier.text === "esmock" &&
|
|
122
|
+
statement.importClause?.name) {
|
|
123
|
+
esmockBindings.add(statement.importClause.name.text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
113
126
|
const moduleText = (node) => node && ts.isStringLiteralLike(node) ? node.text : null;
|
|
114
127
|
const visit = (node) => {
|
|
115
128
|
if (ts.isImportDeclaration(node)) {
|
|
@@ -129,8 +142,9 @@ export function extractImports(file) {
|
|
|
129
142
|
// Dynamic `import("lit")` or `require("lit")` with a string-literal arg.
|
|
130
143
|
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
131
144
|
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === "require";
|
|
145
|
+
const isEsmock = ts.isIdentifier(node.expression) && esmockBindings.has(node.expression.text);
|
|
132
146
|
// TODO(low): a concatenated dynamic-import arg yields a phantom specifier; ignored.
|
|
133
|
-
if (isDynamicImport || isRequire) {
|
|
147
|
+
if (isDynamicImport || isRequire || isEsmock) {
|
|
134
148
|
const specifier = moduleText(node.arguments[0]);
|
|
135
149
|
if (specifier !== null)
|
|
136
150
|
imports.push({ specifier, kind: "runtime", bindings: [] });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const TEST_SUPPORT_RANKING_REASON_CODE = "test_support_helper";
|
|
2
|
+
const TEST_SUPPORT_PATH_RE = /(^|\/)((?:[^/]+-)?e2e[-_]?testing|testing|testutils?|testhelpers?|fixtures?|mocks?|fakes?)(\/|$)/i;
|
|
3
|
+
/**
|
|
4
|
+
* Built-in ranking-only exclusions. These symbols remain in the graph and keep
|
|
5
|
+
* their denominator eligibility; they simply do not compete with production
|
|
6
|
+
* behaviors in the primary priority worklist.
|
|
7
|
+
*/
|
|
8
|
+
export function builtInRankExclusion(file) {
|
|
9
|
+
if (!TEST_SUPPORT_PATH_RE.test(file))
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
code: TEST_SUPPORT_RANKING_REASON_CODE,
|
|
13
|
+
reason: "Test-support helper — retained in the behavior denominator, excluded from the primary production priority ranking."
|
|
14
|
+
};
|
|
15
|
+
}
|
package/dist/local/score/risk.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { loadRiskConfig, globToRegExp } from "./riskConfig.js";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { hashString } from "../util/hash.js";
|
|
4
|
+
import { builtInRankExclusion } from "./rankEligibility.js";
|
|
5
|
+
/** Explicit scorer identity. Formula changes require a new value. */
|
|
6
|
+
export const ORS_VERSION = "orangepro.ors.stable_population.v2";
|
|
3
7
|
const ENTRY_PATH_RE = /(^|\/)(routes?|controllers?|handlers?|jobs?|workers?|processors?|queues?|consumers?|subscribers?|listeners?|server|cmd)\//i;
|
|
4
8
|
const ENTRY_FILE_RE = /(^|\/)[^/]*(controller|handler|route|router|job|processor|worker|queue|consumer|subscriber|listener|command|gateway)\.[^.\/]+$/i;
|
|
5
9
|
const API_HANDLER_NAME_RE = /^(GET|POST|PUT|PATCH|DELETE|handle.*|handler|route|controller|endpoint)$/i;
|
|
@@ -176,7 +180,26 @@ function gitFirstCommitBatch(root, files) {
|
|
|
176
180
|
}
|
|
177
181
|
return out;
|
|
178
182
|
}
|
|
183
|
+
/** Exact deterministic identity of the Git-derived inputs consumed by ORS. */
|
|
184
|
+
export function riskHistoryFingerprint(root, files, churnWindow) {
|
|
185
|
+
const health = inspectRiskInputHealth(root, churnWindow);
|
|
186
|
+
const sortedFiles = [...new Set(files)].sort();
|
|
187
|
+
if (!health.churnAvailable) {
|
|
188
|
+
return hashString(JSON.stringify({ state: health.history, window: health.churnWindow, available: false }));
|
|
189
|
+
}
|
|
190
|
+
const churn = gitChurn(root, sortedFiles, health.churnWindow);
|
|
191
|
+
const first = churn.complete ? gitFirstCommitBatch(root, sortedFiles) : new Map();
|
|
192
|
+
return hashString(JSON.stringify({
|
|
193
|
+
state: health.history,
|
|
194
|
+
window: health.churnWindow,
|
|
195
|
+
available: churn.complete,
|
|
196
|
+
churn: sortedFiles.map((file) => [file, churn.values.get(file) ?? 0]),
|
|
197
|
+
first_commit: sortedFiles.map((file) => [file, first.get(file) ?? 0])
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
179
200
|
export function isEntryPoint(node) {
|
|
201
|
+
if (node.properties.cli_entrypoint === true)
|
|
202
|
+
return true;
|
|
180
203
|
const file = symbolFile(node);
|
|
181
204
|
const title = symbolTitle(node);
|
|
182
205
|
if (API_HANDLER_NAME_RE.test(title) && /(^|\/)api(s)?\//i.test(file))
|
|
@@ -194,6 +217,8 @@ function deriveRouteWeight(node) {
|
|
|
194
217
|
const method = methodMatch?.[1].toUpperCase() ?? "";
|
|
195
218
|
const isStore = /\/store\//i.test(file) || /\/store\b/i.test(file);
|
|
196
219
|
const isAdmin = /\/admin\//i.test(file) || /\/admin\b/i.test(file);
|
|
220
|
+
if (node.properties.cli_entrypoint === true)
|
|
221
|
+
return 6;
|
|
197
222
|
if (isHttpRouteSymbol(node) && isStore) {
|
|
198
223
|
if (method === "POST")
|
|
199
224
|
return 10;
|
|
@@ -319,6 +344,10 @@ const NEW_CODE_SECONDS = NEW_CODE_DAYS * 24 * 60 * 60;
|
|
|
319
344
|
// `remove` kept but not for listener/handler/attribute/child tails — those detach,
|
|
320
345
|
// they don't destroy data.
|
|
321
346
|
const DESTRUCTIVE_CALL_RE = /^(?:delete(?!d)|purge|drop|forcedelete|destroy)[A-Za-z0-9_]*$/i;
|
|
347
|
+
// Replacement can overwrite a remote collection, but the verb is too broad to
|
|
348
|
+
// treat by name alone. It is a sink only when the analyzer proves the receiver
|
|
349
|
+
// is a runtime binding imported from an external module.
|
|
350
|
+
const IMPORTED_REPLACEMENT_CALL_RE = /^replace[A-Za-z0-9_]*$/i;
|
|
322
351
|
// `remove*` is only a data sink through a persistence-shaped field; in-memory
|
|
323
352
|
// removals (`pollers.Remove`, `RemoveSpeculativeWorkflowTaskTimeout`) are not.
|
|
324
353
|
const REMOVE_CALL_RE = /^remove(?![A-Za-z0-9_]*(?:listener|handler|observer|callback|attribute|attr|class|child|style|hook|timeout|timer)$)[A-Za-z0-9_]*$/i;
|
|
@@ -381,17 +410,19 @@ function candidateSignalIds(graph, candidateIds) {
|
|
|
381
410
|
}
|
|
382
411
|
export function rankRiskGaps(graph, opts = {}) {
|
|
383
412
|
const limit = opts.limit ?? 20;
|
|
384
|
-
const
|
|
413
|
+
const repoRoot = opts.repoRoot ?? graph.workspace.root;
|
|
414
|
+
const confirmed = opts.includeAssociated
|
|
415
|
+
? new Set(opts.provenIds ?? [])
|
|
416
|
+
: confirmedBehaviorIds(graph, opts.provenIds);
|
|
385
417
|
// Fix D: ranking hygiene. Test-support code, bare constants/variables, and trivial
|
|
386
418
|
// accessors stay in the behavior DENOMINATOR but never compete for a risk slot.
|
|
387
|
-
const
|
|
388
|
-
const cfgEarly = loadRiskConfig(opts.repoRoot ?? graph.workspace.root).config.classification;
|
|
419
|
+
const cfgEarly = loadRiskConfig(repoRoot).config.classification;
|
|
389
420
|
const extraTestSupportRef = cfgEarly.test_support_paths.map(globToRegExp);
|
|
390
421
|
const rankExcludeRef = cfgEarly.rank_exclude_paths.map(globToRegExp);
|
|
391
422
|
const ACCESSOR_RE = /(^|\.)(Get|Set|Is|Has)[A-Z][A-Za-z0-9]*$/;
|
|
392
423
|
const rankEligible = (n) => {
|
|
393
424
|
const props = (n.properties ?? {});
|
|
394
|
-
if (
|
|
425
|
+
if (builtInRankExclusion(symbolFile(n)))
|
|
395
426
|
return false;
|
|
396
427
|
if (extraTestSupportRef.some((re) => re.test(symbolFile(n))))
|
|
397
428
|
return false;
|
|
@@ -408,12 +439,17 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
408
439
|
return false;
|
|
409
440
|
return true;
|
|
410
441
|
};
|
|
411
|
-
const suppressedRef = loadRiskConfig(
|
|
442
|
+
const suppressedRef = loadRiskConfig(repoRoot).config.overrides.filter((o) => o.action === "suppress");
|
|
412
443
|
const isSuppressed = (id) => suppressedRef.some((o) => o.symbol === id || globToRegExp(o.symbol).test(id));
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
// ORS v2 computes structural inputs and normalizes P/I over the stable
|
|
445
|
+
// denominator-eligible production population. Evidence changes filter the
|
|
446
|
+
// worklist but do not rescale an unchanged peer.
|
|
447
|
+
const normalizationSymbols = graph.nodes.filter((n) => n.kind === "CodeSymbol" && n.denominator_eligible === true && !n.stale && rankEligible(n) && !isSuppressed(n.external_id));
|
|
448
|
+
const symbols = normalizationSymbols.filter((n) => !confirmed.has(n.external_id));
|
|
449
|
+
const worklistSymbols = symbols.filter((n) => n.properties.ranking_exclusion_reason !== "python_structural_container");
|
|
450
|
+
const symbolIds = new Set(normalizationSymbols.map((s) => s.external_id));
|
|
415
451
|
const symbolsByFile = new Map();
|
|
416
|
-
for (const s of
|
|
452
|
+
for (const s of normalizationSymbols) {
|
|
417
453
|
const file = symbolFile(s);
|
|
418
454
|
const list = symbolsByFile.get(file);
|
|
419
455
|
if (list)
|
|
@@ -421,8 +457,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
421
457
|
else
|
|
422
458
|
symbolsByFile.set(file, [s]);
|
|
423
459
|
}
|
|
424
|
-
const files = [...new Set(
|
|
425
|
-
const repoRoot = opts.repoRoot ?? graph.workspace.root;
|
|
460
|
+
const files = [...new Set(normalizationSymbols.map(symbolFile))];
|
|
426
461
|
const loadedCfg = loadRiskConfig(repoRoot);
|
|
427
462
|
const cfg = loadedCfg.config;
|
|
428
463
|
const overrideFor = (id) => cfg.overrides.find((o) => o.symbol === id || globToRegExp(o.symbol).test(id));
|
|
@@ -476,7 +511,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
476
511
|
const total = fileComplexityTotals.get(file) ?? 1;
|
|
477
512
|
return fileChurn * (Math.max(complexityProxy(s), 1) / Math.max(total, 1));
|
|
478
513
|
};
|
|
479
|
-
const entryPoint = new Map(
|
|
514
|
+
const entryPoint = new Map(normalizationSymbols.map((s) => [s.external_id, isEntryPoint(s)]));
|
|
480
515
|
// Single pass over edges (was one full edge scan PER symbol).
|
|
481
516
|
const fanOutTargets = new Map();
|
|
482
517
|
for (const e of graph.edges) {
|
|
@@ -486,7 +521,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
486
521
|
set.add(e.to_external_id);
|
|
487
522
|
fanOutTargets.set(e.from_external_id, set);
|
|
488
523
|
}
|
|
489
|
-
const fanOut = new Map(
|
|
524
|
+
const fanOut = new Map(normalizationSymbols.map((s) => [s.external_id, fanOutTargets.get(s.external_id)?.size ?? 0]));
|
|
490
525
|
// Fix C signals: destructive reach via CALLS (<=3 hops) to a sink; sinks are symbols
|
|
491
526
|
// whose own name, or a retained external callee, is a destructive call.
|
|
492
527
|
const nodeById = new Map(graph.nodes.map((n) => [n.external_id, n]));
|
|
@@ -503,11 +538,18 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
503
538
|
if (!n)
|
|
504
539
|
return undefined;
|
|
505
540
|
const ext = n.properties?.external_callees ?? [];
|
|
541
|
+
const importedStatic = n.properties?.imported_static_callees ?? [];
|
|
506
542
|
// A receiver FIELD path is `x.field.Method` — no call parentheses before the last
|
|
507
543
|
// segment. `q.Clock().Now().Truncate` is a chain of return values, not a surface.
|
|
508
544
|
const viaField = (c) => !c.slice(0, c.lastIndexOf(".")).includes("(");
|
|
509
545
|
const fieldOf = (c) => c.slice(0, c.lastIndexOf("."));
|
|
510
|
-
|
|
546
|
+
const destructive = ext.find((c) => viaField(c) && (DESTRUCTIVE_CALL_RE.test(lastSeg(c)) ||
|
|
547
|
+
(REMOVE_CALL_RE.test(lastSeg(c)) && PERSISTENCE_FIELD_RE.test(fieldOf(c))) ||
|
|
548
|
+
extraSinks.some((re) => re.test(lastSeg(c)))));
|
|
549
|
+
if (destructive)
|
|
550
|
+
return destructive;
|
|
551
|
+
return importedStatic.find((c) => viaField(c) && (DESTRUCTIVE_CALL_RE.test(lastSeg(c)) ||
|
|
552
|
+
IMPORTED_REPLACEMENT_CALL_RE.test(lastSeg(c)) ||
|
|
511
553
|
(REMOVE_CALL_RE.test(lastSeg(c)) && PERSISTENCE_FIELD_RE.test(fieldOf(c))) ||
|
|
512
554
|
extraSinks.some((re) => re.test(lastSeg(c)))));
|
|
513
555
|
};
|
|
@@ -528,17 +570,44 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
528
570
|
}
|
|
529
571
|
return undefined;
|
|
530
572
|
};
|
|
531
|
-
const sinkReached = new Map(
|
|
573
|
+
const sinkReached = new Map(normalizationSymbols.map((s) => [s.external_id, reachedSinkFrom(s.external_id, 2, new Set([s.external_id]))]));
|
|
532
574
|
const reachesSink = new Map([...sinkReached].map(([k, v]) => [k, v !== undefined]));
|
|
533
575
|
const depthCtx = buildFlowDepthContext(graph);
|
|
534
576
|
const staticLinked = staticTestLinkedIds(graph, symbolIds);
|
|
535
577
|
const candidateLinked = candidateSignalIds(graph, symbolIds);
|
|
536
578
|
const detectionFor = (id) => staticLinked.has(id) ? "associated" : candidateLinked.has(id) ? "candidate" : "none";
|
|
537
579
|
if (opts.legacy) {
|
|
538
|
-
|
|
580
|
+
// Preserve the legacy option's historical evidence-filtered attribution.
|
|
581
|
+
const legacyIds = new Set(symbols.map((s) => s.external_id));
|
|
582
|
+
const legacyByFile = new Map();
|
|
583
|
+
for (const s of symbols) {
|
|
584
|
+
const file = symbolFile(s);
|
|
585
|
+
const list = legacyByFile.get(file);
|
|
586
|
+
if (list)
|
|
587
|
+
list.push(s);
|
|
588
|
+
else
|
|
589
|
+
legacyByFile.set(file, [s]);
|
|
590
|
+
}
|
|
591
|
+
const legacyIncoming = new Map();
|
|
592
|
+
const legacyFileImports = new Map();
|
|
593
|
+
for (const e of graph.edges) {
|
|
594
|
+
if (e.relationship_type === "CALLS" && legacyIds.has(e.to_external_id)) {
|
|
595
|
+
legacyIncoming.set(e.to_external_id, (legacyIncoming.get(e.to_external_id) ?? 0) + 1);
|
|
596
|
+
}
|
|
597
|
+
else if (e.relationship_type === "IMPORTS" && legacyByFile.has(e.to_external_id)) {
|
|
598
|
+
legacyFileImports.set(e.to_external_id, (legacyFileImports.get(e.to_external_id) ?? 0) + 1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const [file, count] of legacyFileImports) {
|
|
602
|
+
const syms = legacyByFile.get(file) ?? [];
|
|
603
|
+
const share = syms.length > 0 ? count / syms.length : 0;
|
|
604
|
+
for (const s of syms)
|
|
605
|
+
legacyIncoming.set(s.external_id, (legacyIncoming.get(s.external_id) ?? 0) + share);
|
|
606
|
+
}
|
|
607
|
+
return worklistSymbols
|
|
539
608
|
.map((s) => {
|
|
540
609
|
const file = symbolFile(s);
|
|
541
|
-
const incoming_refs =
|
|
610
|
+
const incoming_refs = legacyIncoming.get(s.external_id) ?? 0;
|
|
542
611
|
const git_churn = churn.get(file) ?? 0;
|
|
543
612
|
const isEntry = entryPoint.get(s.external_id) ?? false;
|
|
544
613
|
const churnForScore = Math.min(git_churn, 500);
|
|
@@ -556,7 +625,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
556
625
|
.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))
|
|
557
626
|
.slice(0, limit);
|
|
558
627
|
}
|
|
559
|
-
const rawScores =
|
|
628
|
+
const rawScores = normalizationSymbols.map((s) => {
|
|
560
629
|
const file = symbolFile(s);
|
|
561
630
|
const incoming_refs = incoming.get(s.external_id) ?? 0;
|
|
562
631
|
const git_churn = symbolChurn(s);
|
|
@@ -578,15 +647,19 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
578
647
|
// matter how few callers it has. Impact FLOORS at 5/10 for such paths (a floor,
|
|
579
648
|
// not an increment — an additive bump vanishes under hub-dominated normalization).
|
|
580
649
|
const iScores = iScoresRaw.map((v, idx) => (rawScores[idx].reachesDestructiveSink ? Math.max(v, 5) : v));
|
|
581
|
-
const
|
|
582
|
-
|
|
650
|
+
const rawById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, rawScores[idx]]));
|
|
651
|
+
const pById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, pScores[idx]]));
|
|
652
|
+
const iById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, iScores[idx]]));
|
|
653
|
+
const ranked = worklistSymbols
|
|
654
|
+
.map((s) => {
|
|
583
655
|
const file = symbolFile(s);
|
|
584
656
|
const incoming_refs = Math.round((incoming.get(s.external_id) ?? 0) * 10) / 10;
|
|
585
657
|
const git_churn = Math.round(symbolChurn(s));
|
|
586
658
|
const fan_out = fanOut.get(s.external_id) ?? 0;
|
|
587
659
|
const isEntry = entryPoint.get(s.external_id) ?? false;
|
|
588
660
|
const route_weight = deriveRouteWeight(s);
|
|
589
|
-
const
|
|
661
|
+
const raw = rawById.get(s.external_id);
|
|
662
|
+
const data_sensitivity = raw.sensitivityOverride !== undefined ? raw.sensitivityOverride : raw.sensitivityIgnored ? 0 : deriveDataSensitivity(s);
|
|
590
663
|
const flow_position = Math.max(0, 5 - getFlowDepth(s, depthCtx));
|
|
591
664
|
const complexity_proxy = complexityProxy(s);
|
|
592
665
|
const firstTs = firstCommitTs.get(file) ?? 0;
|
|
@@ -595,11 +668,11 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
595
668
|
// before multiplying previously collapsed whole hot files into identical
|
|
596
669
|
// P×I×D ties (seventeen ORS-100 rows from one service). Integers remain
|
|
597
670
|
// display-only in the decomposition string.
|
|
598
|
-
const pExact =
|
|
599
|
-
const iExact =
|
|
671
|
+
const pExact = pById.get(s.external_id);
|
|
672
|
+
const iExact = iById.get(s.external_id);
|
|
600
673
|
const p = Math.round(pExact);
|
|
601
674
|
const i = Math.round(iExact);
|
|
602
|
-
const d =
|
|
675
|
+
const d = raw.d;
|
|
603
676
|
const detectionTier = detectionFor(s.external_id);
|
|
604
677
|
let score = Math.round(pExact * iExact * d * 10) / 10;
|
|
605
678
|
const disconnected = incoming_refs === 0 && fan_out === 0;
|
|
@@ -623,9 +696,9 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
623
696
|
reasons.push("lexical candidate test match only — unconfirmed");
|
|
624
697
|
// Reason chips for the two consequence signals — a reader can disagree with the
|
|
625
698
|
// weight without doubting the fact, and the fact is what's shown.
|
|
626
|
-
if (
|
|
699
|
+
if (raw.reachesDestructiveSink)
|
|
627
700
|
reasons.push("reaches a destructive external call (delete/purge) within 2 hops — impact floored at 5");
|
|
628
|
-
if (
|
|
701
|
+
if (raw.scheduledEntry)
|
|
629
702
|
reasons.push("scheduled/queue-triggered entry with no proof — failures surface nowhere, detection ×1.25");
|
|
630
703
|
const override = overrideFor(s.external_id);
|
|
631
704
|
if (override && override.action !== "suppress")
|
|
@@ -633,7 +706,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
633
706
|
return {
|
|
634
707
|
...(override && override.action !== "suppress" ? { override: { action: override.action, reason: override.reason } } : {}),
|
|
635
708
|
...(sinkReached.get(s.external_id) ? { sink_callee: sinkReached.get(s.external_id) } : {}),
|
|
636
|
-
...(
|
|
709
|
+
...(raw.scheduledEntry ? { scheduled_entry: true } : {}),
|
|
637
710
|
detection_tier: detectionTier,
|
|
638
711
|
id: s.external_id,
|
|
639
712
|
title: s.title || s.external_id,
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { loadRiskConfig } from "../score/riskConfig.js";
|
|
2
2
|
import { configDisclosureFor } from "../score/risk.js";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { buildRtm } from "../rtm.js";
|
|
6
|
-
import { inspectRiskInputHealth, isEntryPoint, rankPriorityGaps, rankRiskGaps } from "../score/risk.js";
|
|
7
|
-
import { ORANGEPRO_VERSION } from "../version.js";
|
|
5
|
+
import { inspectRiskInputHealth, isEntryPoint, rankPriorityGaps, rankRiskGaps, riskHistoryFingerprint } from "../score/risk.js";
|
|
8
6
|
import { PROOF_BLOCKER_GUIDE } from "../proofDoctor.js";
|
|
9
7
|
import { classifyGeneratedDraftBlocker } from "../generate/draftGuidance.js";
|
|
8
|
+
import { buildArtifactIdentity, comparisonCompatibility } from "../provenance.js";
|
|
10
9
|
/** Short human phrase per R-1 needs_setup category, for the "blocked because: …" panel copy. */
|
|
11
10
|
const BLOCK_CATEGORY_LABEL = {
|
|
12
11
|
module_not_found: "a missing module or dependency in the sandbox",
|
|
@@ -219,6 +218,16 @@ function proofGuidance(ledger, summary, dyn) {
|
|
|
219
218
|
action: HANDOFF_ACTION
|
|
220
219
|
};
|
|
221
220
|
}
|
|
221
|
+
export function excludedCliCommands(graph) {
|
|
222
|
+
return graph.nodes
|
|
223
|
+
.filter((node) => node.kind === "CodeSymbol" && node.properties.cli_entrypoint === true && node.denominator_eligible !== true)
|
|
224
|
+
.map((node) => ({
|
|
225
|
+
path: node.title ?? node.external_id,
|
|
226
|
+
file: String(node.properties.file ?? ""),
|
|
227
|
+
reason: node.denominator_reason ?? "Excluded from the behavior denominator."
|
|
228
|
+
}))
|
|
229
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.path.localeCompare(b.path));
|
|
230
|
+
}
|
|
222
231
|
function scanBlock(graph, rows) {
|
|
223
232
|
const services = new Map();
|
|
224
233
|
for (const row of rows) {
|
|
@@ -259,7 +268,8 @@ function scanBlock(graph, rows) {
|
|
|
259
268
|
excluded: {
|
|
260
269
|
count: excludedCount > 0 ? String(excludedCount) : "0",
|
|
261
270
|
text: "non-behavior symbols were excluded from the behavior count — generated code, framework internals, test-inferred flows, and infrastructure plumbing."
|
|
262
|
-
}
|
|
271
|
+
},
|
|
272
|
+
excludedCliCommands: excludedCliCommands(graph)
|
|
263
273
|
};
|
|
264
274
|
}
|
|
265
275
|
function behaviorLists(rows, flowIds) {
|
|
@@ -344,6 +354,7 @@ function flows(graph, rows, risks) {
|
|
|
344
354
|
title: flow.entry_point.title || flow.entry_point.external_id,
|
|
345
355
|
trigger,
|
|
346
356
|
root_entry,
|
|
357
|
+
...(rootNode?.properties.entry_lane === "cli" ? { entry_lane: "cli" } : {}),
|
|
347
358
|
risk: risk ? riskBucket(risk.risk_score, maxRiskScore) : null,
|
|
348
359
|
proof,
|
|
349
360
|
services,
|
|
@@ -500,6 +511,24 @@ function riskContext(risk) {
|
|
|
500
511
|
/** Pure delta between a persisted baseline and the current report data.
|
|
501
512
|
* Deterministic: same inputs, same delta. */
|
|
502
513
|
export function computeReportDelta(prev, cur) {
|
|
514
|
+
const comparisonState = comparisonCompatibility(prev.identity, cur.provenance.identity);
|
|
515
|
+
if (comparisonState !== "comparable") {
|
|
516
|
+
return {
|
|
517
|
+
baselineTs: prev.ts,
|
|
518
|
+
comparisonState,
|
|
519
|
+
comparisonReason: comparisonReason(comparisonState),
|
|
520
|
+
changed: false,
|
|
521
|
+
totalDelta: 0,
|
|
522
|
+
provenDelta: 0,
|
|
523
|
+
associatedDelta: 0,
|
|
524
|
+
candidateDelta: 0,
|
|
525
|
+
noneDelta: 0,
|
|
526
|
+
newRisks: [],
|
|
527
|
+
droppedRisks: [],
|
|
528
|
+
generatedDelta: 0,
|
|
529
|
+
configChanged: comparisonState === "ranking_changed"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
503
532
|
const curPaths = cur.risks.map((r) => r.path);
|
|
504
533
|
const prevSet = new Set(prev.riskPaths);
|
|
505
534
|
const curSet = new Set(curPaths);
|
|
@@ -507,6 +536,8 @@ export function computeReportDelta(prev, cur) {
|
|
|
507
536
|
const droppedRisks = prev.riskPaths.filter((p) => !curSet.has(p));
|
|
508
537
|
const d = {
|
|
509
538
|
baselineTs: prev.ts,
|
|
539
|
+
comparisonState,
|
|
540
|
+
comparisonReason: comparisonReason(comparisonState),
|
|
510
541
|
changed: false,
|
|
511
542
|
totalDelta: cur.summary.total - prev.summary.total,
|
|
512
543
|
provenDelta: cur.summary.proven - prev.summary.proven,
|
|
@@ -524,7 +555,34 @@ export function computeReportDelta(prev, cur) {
|
|
|
524
555
|
return d;
|
|
525
556
|
}
|
|
526
557
|
export function reportBaselineOf(cur, ts) {
|
|
527
|
-
return { ts, summary: cur.summary, riskPaths: cur.risks.map((r) => r.path), generatedTotal: cur.generatedTotal, configHash: cur.provenance?.configHash };
|
|
558
|
+
return { ts, summary: cur.summary, riskPaths: cur.risks.map((r) => r.path), generatedTotal: cur.generatedTotal, configHash: cur.provenance?.configHash, identity: cur.provenance.identity };
|
|
559
|
+
}
|
|
560
|
+
export function firstRunDelta() {
|
|
561
|
+
return {
|
|
562
|
+
baselineTs: null,
|
|
563
|
+
comparisonState: "first_run",
|
|
564
|
+
comparisonReason: comparisonReason("first_run"),
|
|
565
|
+
changed: false,
|
|
566
|
+
totalDelta: 0,
|
|
567
|
+
provenDelta: 0,
|
|
568
|
+
associatedDelta: 0,
|
|
569
|
+
candidateDelta: 0,
|
|
570
|
+
noneDelta: 0,
|
|
571
|
+
newRisks: [],
|
|
572
|
+
droppedRisks: [],
|
|
573
|
+
generatedDelta: 0
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function comparisonReason(state) {
|
|
577
|
+
switch (state) {
|
|
578
|
+
case "first_run": return "No prior identity-bearing baseline exists.";
|
|
579
|
+
case "comparable": return "Repository, analysis, ranking and proof-oracle identities match.";
|
|
580
|
+
case "repository_changed": return "The scanned repository paths or bytes changed; trend deltas are withheld.";
|
|
581
|
+
case "analysis_changed": return "Analyzer, schema, scope or history inputs changed; treat this as structural recalibration.";
|
|
582
|
+
case "ranking_changed": return "Risk configuration or ORS version changed; treat this as ranking recalibration.";
|
|
583
|
+
case "experiment_changed": return "A proof-oracle or declared experiment input changed; only controlled-experiment interpretation is valid.";
|
|
584
|
+
case "provenance_incomplete": return "The previous artifact lacks the required versioned identity; automated comparison is disabled.";
|
|
585
|
+
}
|
|
528
586
|
}
|
|
529
587
|
const HTTP_TRIGGER_VERBS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"]);
|
|
530
588
|
const GRAPHQL_TRIGGER_VERBS = new Set(["MUTATION", "QUERY", "SUBSCRIPTION"]);
|
|
@@ -571,6 +629,8 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
571
629
|
const laneForFlow = (flow) => {
|
|
572
630
|
if (flow.trigger)
|
|
573
631
|
return laneForTrigger(flow.trigger);
|
|
632
|
+
if (flow.entry_lane === "cli")
|
|
633
|
+
return { id: "cli", label: "CLI", priority: 60 };
|
|
574
634
|
// A program root without a framework trigger is still a real product entry
|
|
575
635
|
// point (CLI/main). Internal call-graph roots remain excluded.
|
|
576
636
|
return flow.root_entry ? { id: "entry", label: "Entry points", priority: 80 } : null;
|
|
@@ -939,10 +999,30 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
939
999
|
.map((r) => ({ path: r.title, file: r.file, score: r.risk_score, probability: r.probability ?? 0 }));
|
|
940
1000
|
// Fix: search the FULL ranking for sinks, not the top 200 — a stable destructive path
|
|
941
1001
|
// ranked 300th is exactly what this panel exists to recover.
|
|
942
|
-
|
|
1002
|
+
// Consequence view: a static test association is not dynamic proof. Keep
|
|
1003
|
+
// Associated sink paths visible here while the main priority-gap list retains
|
|
1004
|
+
// its existing behavior and excludes hard-linked rows.
|
|
1005
|
+
const irreversible = rankRiskGaps(graph, {
|
|
1006
|
+
repoRoot,
|
|
1007
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1008
|
+
provenIds,
|
|
1009
|
+
includeAssociated: true
|
|
1010
|
+
}).filter((r) => r.sink_callee).slice(0, 20)
|
|
943
1011
|
.map((r) => ({ path: r.title, file: r.file, score: r.risk_score, sink: r.sink_callee ?? "" }));
|
|
944
1012
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
945
1013
|
const churnAvailable = riskHealth.churnAvailable && riskGaps.every((risk) => risk.churn_available !== false);
|
|
1014
|
+
const configHash = loadRiskConfig(repoRoot).hash;
|
|
1015
|
+
const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
|
|
1016
|
+
const identity = buildArtifactIdentity(graph, {
|
|
1017
|
+
configHash,
|
|
1018
|
+
history: {
|
|
1019
|
+
state: riskHealth.history,
|
|
1020
|
+
churnWindow: riskHealth.churnWindow,
|
|
1021
|
+
churnAvailable,
|
|
1022
|
+
commitDate: riskHealth.commitDate,
|
|
1023
|
+
inputFingerprint: riskHistoryFingerprint(repoRoot, codeFiles, riskHealth.churnWindow)
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
946
1026
|
const provenance = {
|
|
947
1027
|
source: path.basename(repoRoot || graph.workspace.name || "repo"),
|
|
948
1028
|
gitRoot: riskHealth.gitRoot ? path.basename(riskHealth.gitRoot) : null,
|
|
@@ -950,12 +1030,10 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
950
1030
|
history: riskHealth.history,
|
|
951
1031
|
churn: churnAvailable ? "available" : "unavailable",
|
|
952
1032
|
churnWindow: riskHealth.churnWindow,
|
|
953
|
-
toolVersion:
|
|
954
|
-
configHash
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
.digest("hex")
|
|
958
|
-
.slice(0, 16),
|
|
1033
|
+
toolVersion: identity.tool_version,
|
|
1034
|
+
configHash,
|
|
1035
|
+
identity,
|
|
1036
|
+
inputFingerprint: identity.run_fingerprint.replace(/^sha256:/, "").slice(0, 16),
|
|
959
1037
|
reason: churnAvailable ? undefined : (riskHealth.reason ?? "Git churn scan did not complete")
|
|
960
1038
|
};
|
|
961
1039
|
const lists = behaviorLists(rows, flowIds);
|
|
@@ -982,6 +1060,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
982
1060
|
configDisclosure: configDisclosureFor(graph, repoRoot),
|
|
983
1061
|
zeroProofExplainer: summary.proven === 0 ? { title: ZERO_PROOF_EXPLAINER.title, body: [...ZERO_PROOF_EXPLAINER.body] } : null,
|
|
984
1062
|
mapModel: buildSystemMapModel({ flows: flowRows, risks, behaviors: sortedBehaviors }),
|
|
1063
|
+
delta: firstRunDelta(),
|
|
985
1064
|
viewMeta: {
|
|
986
1065
|
// Every denominator behavior is scored; the risks tab surfaces the top N.
|
|
987
1066
|
risks: { shown: risks.length, scored: summary.total },
|
|
@@ -50,6 +50,7 @@ a{color:var(--blue);text-decoration:none}
|
|
|
50
50
|
.kpi[data-t="nosig"]{--kw:var(--rbg)} .kpi[data-t="nosig"] .kpi-num{color:var(--red)}
|
|
51
51
|
.kpi[data-t="priority"]{--kw:var(--obg)} .kpi[data-t="priority"] .kpi-num{color:var(--orange)}
|
|
52
52
|
.metric-scope{margin:9px 2px 0;color:var(--muted);font-size:11.5px;line-height:1.5}
|
|
53
|
+
.scope-note{margin:9px 0 0;padding:9px 11px;border:1px solid var(--abd);background:var(--abg);color:var(--ink2);font-size:11.5px;line-height:1.5;border-radius:6px}
|
|
53
54
|
|
|
54
55
|
/* TABS */
|
|
55
56
|
nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
@@ -343,6 +344,7 @@ body[data-mode="expert"] .simple-only{display:none!important}
|
|
|
343
344
|
</div>
|
|
344
345
|
</header>
|
|
345
346
|
<div class="provenance" id="provenance"></div>
|
|
347
|
+
<div class="scope-note" id="cli-exclusions" hidden></div>
|
|
346
348
|
|
|
347
349
|
<section class="kpis" id="kpis"></section>
|
|
348
350
|
<p class="metric-scope" id="metric-scope"></p>
|
|
@@ -465,9 +467,13 @@ const D=window.DATA,$=(s)=>document.querySelector(s);
|
|
|
465
467
|
var el2=document.getElementById("delta-banner");
|
|
466
468
|
var d=D.delta;
|
|
467
469
|
if(!el2||d===undefined)return;
|
|
468
|
-
if(d===
|
|
470
|
+
if(d.comparisonState==='first_run'){el2.innerHTML='';return;}
|
|
469
471
|
var when=new Date(d.baselineTs);
|
|
470
472
|
var ago=isNaN(when.getTime())?"last run":when.toLocaleString();
|
|
473
|
+
if(d.comparisonState!=='comparable'){
|
|
474
|
+
el2.innerHTML='<div class="delta-wrap"><span class="delta-chip dc-none">Comparison withheld: '+d.comparisonReason+'</span></div>';
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
471
477
|
if(!d.changed){
|
|
472
478
|
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>';
|
|
473
479
|
return;
|
|
@@ -515,7 +521,12 @@ function scrollToRisk(path){
|
|
|
515
521
|
(function(){
|
|
516
522
|
var hero=document.getElementById("delta-hero");
|
|
517
523
|
var d=D.delta;
|
|
518
|
-
if(!hero||d===undefined||d===
|
|
524
|
+
if(!hero||d===undefined||d.comparisonState==='first_run')return;
|
|
525
|
+
if(d.comparisonState!=='comparable'){
|
|
526
|
+
hero.className="delta-hero simple-only dh-clear";
|
|
527
|
+
hero.innerHTML='<h3>Comparison withheld</h3><p>'+d.comparisonReason+'</p>';
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
519
530
|
if(!d.changed){
|
|
520
531
|
var when=new Date(d.baselineTs);
|
|
521
532
|
var ago=isNaN(when.getTime())?"last run":when.toLocaleDateString();
|
|
@@ -677,6 +688,12 @@ const prov=$("#provenance");
|
|
|
677
688
|
const provisional=P.churn!=="available";
|
|
678
689
|
prov.classList.toggle("warn",provisional);
|
|
679
690
|
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:"");
|
|
691
|
+
const excludedCli=D.scan.excludedCliCommands||[];
|
|
692
|
+
if(excludedCli.length){
|
|
693
|
+
const note=$("#cli-exclusions");
|
|
694
|
+
note.hidden=false;
|
|
695
|
+
note.textContent="CLI command entries excluded from the behavior count: "+excludedCli.map(x=>x.path+" ("+x.reason+")").join(" · ");
|
|
696
|
+
}
|
|
680
697
|
$("#fw-pill").textContent=D.framework;
|
|
681
698
|
|
|
682
699
|
// bridge text in codebase tab
|