@orangepro/orangepro-mcp 0.2.6 → 0.2.8
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/README.md +2 -2
- package/dist/local/analyze/analyzer.js +84 -33
- package/dist/local/analyze/behaviorContracts.js +125 -75
- package/dist/local/analyze/confirm.js +113 -0
- package/dist/local/autoProve.js +2 -1
- package/dist/local/cli.js +10 -1
- package/dist/local/enrich/markdown.js +1 -1
- package/dist/local/flows/flowWalker.js +7 -10
- package/dist/local/generate/generator.js +30 -12
- package/dist/local/operations.js +26 -22
- package/dist/local/score/risk.js +7 -4
- package/dist/local/viz/behaviorReportData.js +79 -25
- package/dist/local/viz/behaviorReportHtml.js +17 -16
- package/dist/local/viz/payload.js +1 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ The command writes:
|
|
|
32
32
|
orangepro_generated/ ← contained generated tests; existing source files are untouched
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The report opens on a **system map** of your repo — entry lanes (GraphQL/HTTP/Jobs) flowing into the services they reach, sized by traffic, colored by evidence tier, risk-ringed — identical on every run. Each rerun shows a **delta banner
|
|
35
|
+
The report opens on a **system map** of your repo — entry lanes (GraphQL/HTTP/Jobs) flowing into the services they reach, sized by traffic, colored by evidence tier, risk-ringed — identical on every run. Each completed rerun shows a **delta banner** against the previous completed run. The report discloses when bounded path enumeration prunes additional branch expansions. Every behavior gets a plain-English description; every top risk gets a deterministic context line and a state-aware next step.
|
|
36
36
|
|
|
37
37
|
Run `opro export` when you want a machine-readable evidence pack.
|
|
38
38
|
<img width="895" height="960" alt="Screenshot 2026-07-08 at 1 18 01 AM" src="https://github.com/user-attachments/assets/73b8a812-2eab-43a1-8aed-4289545e630b" />
|
|
@@ -189,7 +189,7 @@ If the environment can't run tests yet (dependencies not installed, runner uncon
|
|
|
189
189
|
|
|
190
190
|
## Test categories
|
|
191
191
|
|
|
192
|
-
Generation is evidence-gated. A category is produced only when the graph has supporting evidence — never padded with generic filler. These are the local generation buckets. The report additionally shows each risk's **applicable testing categories** (contract, boundary limits, integration flow, state lifecycle, failure recovery, …), derived deterministically from graph facts
|
|
192
|
+
Generation is evidence-gated. A category is produced only when the graph has supporting evidence — never padded with generic filler. These are the local generation buckets. The report additionally shows each risk's **applicable testing categories** (contract, boundary limits, integration flow, state lifecycle, failure recovery, …), derived deterministically from graph facts. Categories with generated drafts are highlighted as drafts; they are not coverage or proof, and remaining applicable categories stay outlined. Neither taxonomy changes evidence tiers.
|
|
193
193
|
|
|
194
194
|
| Category | What it targets |
|
|
195
195
|
|----------|-----------------|
|
|
@@ -70,13 +70,8 @@ const BEHAVIOR_SURFACE_FILE_RE = /(^|\/)[^/]*(api|controller|service|handler|res
|
|
|
70
70
|
const BEHAVIOR_SURFACE_NAME_RE = /(^|[.#])(__init__|handle|handler|route|controller|service|resolver|processor|job|worker|queue|consumer|subscriber|listener|command|gateway|execute|process|consume|dispatch|schedule|upload|download|sync|checkout|charge|refund|capture|authorize|login|logout|register|find|search|list|create|update|delete|remove|archive|enable|disable|validate|get|add|sub|sum|total|save|load|render|run|main|root|child|mul|about|behavior)([A-Z0-9_]|$)/i;
|
|
71
71
|
const CLIENT_FACTORY_NAME_RE = /(^|[.#])get[A-Z0-9_].*Client$/;
|
|
72
72
|
const BEHAVIOR_OWNER_RE = /(Service|Controller|Resolver|Handler|Processor|Job|Worker|Queue|Consumer|Subscriber|Listener|Command|Gateway|Route|Router)$/;
|
|
73
|
-
const UTILITY_DIRECTORY_EXCLUDE_RE = /\/(utils?|helpers?|tools?|loaders?|dml|dal|orchestration|codemods?|oas|models?|migrations?|migration-scripts?|instrumentation|
|
|
74
|
-
const
|
|
75
|
-
const CLI_PACKAGE_PATH_EXCLUDE_RE = /\/(?:cli\/[^/]+\/src\/(?:commands|core|reporter)|packages\/[^/]+\/src\/commands)\//i;
|
|
76
|
-
const BACKEND_RUNTIME_PATH_EXCLUDE_RE = /\/(?:packages\/core\/framework\/src|packages\/modules\/(?:workflow-engine-[^/]+|link-modules)\/src)(?:\/|$)/i;
|
|
77
|
-
const UI_PRODUCT_PATH_EXCLUDE_RE = /\/(?:packages\/admin\/(?:dashboard|admin-bundler|admin-vite-plugin)\/src|packages\/design-system\/(?:toolbox|icons)\/src|www\/(?:apps|packages)\/[^/]+\/(?:app|src|providers|components|lib))(?:\/|$)/i;
|
|
78
|
-
const SDK_CLIENT_PATH_EXCLUDE_RE = /\/(?:packages\/core\/js-sdk\/src|packages\/[^/]+\/(?:sdk|client)\/src)(?:\/|$)/i;
|
|
79
|
-
const PLUGIN_ADMIN_PATH_EXCLUDE_RE = /\/(?:plugins\/[^/]+\/src\/admin|admin\/routes)\//i;
|
|
73
|
+
const UTILITY_DIRECTORY_EXCLUDE_RE = /\/(utils?|helpers?|tools?|loaders?|dml|dal|orchestration|codemods?|oas|models?|migrations?|migration-scripts?|instrumentation|eslint-plugin)\//i;
|
|
74
|
+
const PRESENTATION_ASSET_PATH_RE = /\/(?:icons?|assets?)\//i;
|
|
80
75
|
const INFRA_METHOD_SUFFIX_RE = /^get[A-Z0-9_].*(Identifier|RegistrationKey|Config|Registry|Options|Settings|Path|Directory|TmpDir|Program|PackageManager|Command|Expression|Recommendation|CircularReferences|PivotTableName|PropertyName|PropertyKey|UnderlyingType|ComputedColumnRegistry|EntityOverrideRegistry|InverseRegistry|RelativeDate|SelectsAndRelations|SetDifference|ResolvedPlugins|Token|Scope|Module|Column|Pivot|Ttl|Timeout|Interval|Size|Limit|Offset|Prefix|Suffix|Pattern|Handler|Resource)$/;
|
|
81
76
|
const BUILD_BOOTSTRAP_PREFIX_RE = /^(load|build|compile)(Modules?|Routes?|Routers?|Plugins?|Config|Schema|Program|Package|Project|Files?|Commands?|Migrations?|Definitions?|Manifest|Artifacts?)([A-Z0-9_]|$)/;
|
|
82
77
|
// NOTE: the broad `.*Provider.*Service` clause was removed — it over-excluded FUNCTIONAL provider
|
|
@@ -106,23 +101,8 @@ function behaviorSurfaceExclusionReason(relPath, name, memberOf) {
|
|
|
106
101
|
if (UTILITY_DIRECTORY_EXCLUDE_RE.test(path)) {
|
|
107
102
|
return "Utility/model/tooling path — infrastructure plumbing, excluded from the behavior denominator.";
|
|
108
103
|
}
|
|
109
|
-
if (
|
|
110
|
-
return "
|
|
111
|
-
}
|
|
112
|
-
if (CLI_PACKAGE_PATH_EXCLUDE_RE.test(path)) {
|
|
113
|
-
return "CLI developer tooling path — excluded from the behavior denominator.";
|
|
114
|
-
}
|
|
115
|
-
if (BACKEND_RUNTIME_PATH_EXCLUDE_RE.test(path)) {
|
|
116
|
-
return "Framework/runtime/link plumbing path — excluded from backend behavior denominator.";
|
|
117
|
-
}
|
|
118
|
-
if (UI_PRODUCT_PATH_EXCLUDE_RE.test(path)) {
|
|
119
|
-
return "UI/docs/design-system path — excluded from backend behavior denominator.";
|
|
120
|
-
}
|
|
121
|
-
if (SDK_CLIENT_PATH_EXCLUDE_RE.test(path)) {
|
|
122
|
-
return "SDK/client package path — excluded from backend behavior denominator.";
|
|
123
|
-
}
|
|
124
|
-
if (PLUGIN_ADMIN_PATH_EXCLUDE_RE.test(path)) {
|
|
125
|
-
return "Plugin admin UI path — excluded from backend behavior denominator.";
|
|
104
|
+
if (PRESENTATION_ASSET_PATH_RE.test(path)) {
|
|
105
|
+
return "Presentation asset/icon path — excluded from the behavior denominator.";
|
|
126
106
|
}
|
|
127
107
|
if (FRAMEWORK_HOOK_METHOD_RE.test(methodName)) {
|
|
128
108
|
return "Framework lifecycle hook — excluded from the behavior denominator.";
|
|
@@ -160,6 +140,73 @@ function behaviorSurfaceReason(relPath, name, memberOf) {
|
|
|
160
140
|
}
|
|
161
141
|
return null;
|
|
162
142
|
}
|
|
143
|
+
const RUNTIME_SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
144
|
+
/**
|
|
145
|
+
* Resolve package-level public entry modules from package metadata and the two
|
|
146
|
+
* conventional source entry paths. This gives libraries a semantic behavior
|
|
147
|
+
* surface without treating every exported helper in the repository as product
|
|
148
|
+
* behavior.
|
|
149
|
+
*/
|
|
150
|
+
function packagePublicEntryPaths(root, relPaths) {
|
|
151
|
+
const normalizedPaths = new Set(relPaths.map((relPath) => relPath.replace(/\\/g, "/")));
|
|
152
|
+
const entries = new Set();
|
|
153
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
154
|
+
const addCandidate = (packageDir, rawTarget) => {
|
|
155
|
+
if (!rawTarget || rawTarget.includes("*") || rawTarget.startsWith("#"))
|
|
156
|
+
return;
|
|
157
|
+
const cleanTarget = rawTarget.replace(/^\.\//, "").split(/[?#]/, 1)[0];
|
|
158
|
+
if (!cleanTarget || cleanTarget.startsWith("../") || path.isAbsolute(cleanTarget))
|
|
159
|
+
return;
|
|
160
|
+
const relativeTarget = packageDir ? `${packageDir}/${cleanTarget}` : cleanTarget;
|
|
161
|
+
const candidates = new Set([relativeTarget]);
|
|
162
|
+
const withoutExt = relativeTarget.replace(RUNTIME_SOURCE_EXT_RE, "");
|
|
163
|
+
for (const extension of extensions)
|
|
164
|
+
candidates.add(`${withoutExt}${extension}`);
|
|
165
|
+
// Published packages often expose dist/index.js while analyzing src/index.ts.
|
|
166
|
+
const sourceBase = cleanTarget.replace(/^(?:dist|lib|build)\//, "src/").replace(RUNTIME_SOURCE_EXT_RE, "");
|
|
167
|
+
const sourceTarget = packageDir ? `${packageDir}/${sourceBase}` : sourceBase;
|
|
168
|
+
for (const extension of extensions)
|
|
169
|
+
candidates.add(`${sourceTarget}${extension}`);
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
if (normalizedPaths.has(candidate))
|
|
172
|
+
entries.add(candidate);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const collectTargets = (value, targets) => {
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
targets.push(value);
|
|
178
|
+
}
|
|
179
|
+
else if (Array.isArray(value)) {
|
|
180
|
+
for (const item of value)
|
|
181
|
+
collectTargets(item, targets);
|
|
182
|
+
}
|
|
183
|
+
else if (value && typeof value === "object") {
|
|
184
|
+
for (const item of Object.values(value))
|
|
185
|
+
collectTargets(item, targets);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
for (const manifestPath of normalizedPaths) {
|
|
189
|
+
if (path.posix.basename(manifestPath) !== "package.json")
|
|
190
|
+
continue;
|
|
191
|
+
const packageDir = path.posix.dirname(manifestPath) === "." ? "" : path.posix.dirname(manifestPath);
|
|
192
|
+
try {
|
|
193
|
+
const manifest = JSON.parse(readFileSync(path.join(root, manifestPath), "utf8"));
|
|
194
|
+
const targets = [];
|
|
195
|
+
for (const field of ["exports", "main", "module", "browser"])
|
|
196
|
+
collectTargets(manifest[field], targets);
|
|
197
|
+
for (const target of targets)
|
|
198
|
+
addCandidate(packageDir, target);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Malformed package metadata is reported elsewhere; it must not stop analysis.
|
|
202
|
+
}
|
|
203
|
+
for (const conventional of ["index", "src/index"]) {
|
|
204
|
+
for (const extension of extensions)
|
|
205
|
+
addCandidate(packageDir, `${conventional}${extension}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return entries;
|
|
209
|
+
}
|
|
163
210
|
const FLOW_LINK_STOPWORDS = new Set([
|
|
164
211
|
"test",
|
|
165
212
|
"tests",
|
|
@@ -282,6 +329,7 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
282
329
|
const maxSymbols = Math.max(1, opts.maxSymbols ?? MAX_TOTAL_SYMBOLS);
|
|
283
330
|
const ignore = loadIgnore(root);
|
|
284
331
|
const { files, truncated: filesCapHit, max_files: maxFiles } = walkFilesWithMeta(root, ignore, { maxFiles: opts.maxFiles });
|
|
332
|
+
const publicEntryFiles = packagePublicEntryPaths(root, files.map((file) => file.relPath));
|
|
285
333
|
const warnings = [];
|
|
286
334
|
// Wall-clock budget for the per-file scan (DISCLOSED partial when hit; never silent).
|
|
287
335
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -741,7 +789,8 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
741
789
|
: callableBehaviorCandidate && !surfaceExclusionReason
|
|
742
790
|
? behaviorSurfaceReason(file.relPath, sym.name, sym.member_of)
|
|
743
791
|
: null;
|
|
744
|
-
const
|
|
792
|
+
const publicApiEntry = callableBehaviorCandidate && !surfaceExclusionReason && publicEntryFiles.has(file.relPath);
|
|
793
|
+
const eligible = callableBehaviorCandidate && (surfaceReason !== null || publicApiEntry);
|
|
745
794
|
const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null;
|
|
746
795
|
const notEntryPointAdjacent = callableBehaviorCandidate && !eligible && !behaviorSurfaceExcluded;
|
|
747
796
|
if (isBoilerplate)
|
|
@@ -757,7 +806,7 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
757
806
|
...(sym.end_line ? { end_line: sym.end_line } : {}),
|
|
758
807
|
...(sym.member_of ? { member_of: sym.member_of } : {}),
|
|
759
808
|
...(sym.callable !== undefined ? { callable_const: sym.callable } : {}),
|
|
760
|
-
...(surfaceReason ? { behavior_surface: "entrypoint_adjacent" } : {}),
|
|
809
|
+
...(surfaceReason ? { behavior_surface: "entrypoint_adjacent" } : publicApiEntry ? { behavior_surface: "public_api_entry" } : {}),
|
|
761
810
|
...(behaviorSurfaceExcluded ? { denominator_reason_code: "infra_behavior_surface" } : {}),
|
|
762
811
|
...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {})
|
|
763
812
|
},
|
|
@@ -777,13 +826,15 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
777
826
|
? BOILERPLATE_REASON
|
|
778
827
|
: eligible && surfaceReason
|
|
779
828
|
? surfaceReason
|
|
780
|
-
:
|
|
781
|
-
?
|
|
782
|
-
:
|
|
783
|
-
?
|
|
784
|
-
:
|
|
785
|
-
? "
|
|
786
|
-
:
|
|
829
|
+
: publicApiEntry
|
|
830
|
+
? "Callable export from a package public entry module — countable library behavior surface."
|
|
831
|
+
: behaviorSurfaceExcluded && surfaceExclusionReason
|
|
832
|
+
? surfaceExclusionReason
|
|
833
|
+
: callableBehaviorCandidate
|
|
834
|
+
? "Callable export is not API/service/route/job-adjacent — kept for grounding, excluded from the behavior denominator."
|
|
835
|
+
: sym.symbol_kind === "class"
|
|
836
|
+
? "Exported class container — kept for grounding; methods/functions carry behavior in v1."
|
|
837
|
+
: "Exported const (not provably callable) — excluded from the denominator in v1."
|
|
787
838
|
}));
|
|
788
839
|
edges.push(makeEdge({
|
|
789
840
|
from_external_id: symId,
|
|
@@ -15,6 +15,10 @@ const NEST_GQL_METHOD_DECORATOR = /@(Query|Mutation|Subscription|ResolveField)\s
|
|
|
15
15
|
const NEST_PROCESSOR_DECORATOR = /@Processor\s*\(\s*(?:(["'`])([^"'`]*)\1|[A-Za-z_$][A-Za-z0-9_$.]*)?\s*\)(?:\s|@[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\((?:[^()]|\([^()]*\))*\))?|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*?(?:export\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
16
16
|
const NEST_PROCESS_METHOD_DECORATOR = /@Process\s*\((?:[^()]|\([^()]*\))*\)(?:\s|@[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\((?:[^()]|\([^()]*\))*\))?|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*?(?:public\s+|private\s+|protected\s+|async\s+)*([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
17
17
|
const NEST_CONTROLLER_DECORATOR = /@Controller\s*\(\s*(?:(["'`])([^"'`]*)\1)?\s*\)(?:\s|@[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\((?:[^()]|\([^()]*\))*\))?|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*?(?:export\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
18
|
+
const NEST_RESOLVER_DECORATOR = /@Resolver(?:\s*\((?:[^()]|\([^()]*\))*\))?(?:\s|@[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\((?:[^()]|\([^()]*\))*\))?|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*?(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
19
|
+
const NEST_WORKER_PROCESS_METHOD = /(?:^|[\n;}])\s*(?:public\s+|protected\s+|override\s+|async\s+)*\b(process)\s*\(/g;
|
|
20
|
+
const NEST_CRON_METHOD_DECORATOR = /@Cron\s*\((?:[^()]|\([^()]*\))*\)(?:\s|@[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\((?:[^()]|\([^()]*\))*\))?|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*?(?:public\s+|private\s+|protected\s+|async\s+)*([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
21
|
+
const NEST_EVENT_METHOD_DECORATOR = /@(OnEvent|EventPattern|MessagePattern)\s*\(\s*(?:(["'`])([^"'`]*)\2|(?:[^()]|\([^()]*\))*)\s*\)(?:\s|@[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\((?:[^()]|\([^()]*\))*\))?|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*?(?:public\s+|private\s+|protected\s+|async\s+)*([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
18
22
|
const EXPRESS_ROUTER_CALL = /\b(?:router|app)\s*\.\s*(get|post|put|delete|patch|options|head|all)\s*\(\s*(["'`])([^"'`]*)\2\s*,\s*([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?|\([^)]*\)\s*=>|async\s+\([^)]*\)\s*=>|function\s+[A-Za-z_$][A-Za-z0-9_$]*)/gi;
|
|
19
23
|
const EXPRESS_ROUTE_CHAIN = /\b(?:router|app)\s*\.\s*route\s*\(\s*(["'`])([^"'`]*)\1\s*\)((?:\s*\.\s*(?:get|post|put|delete|patch|options|head|all)\s*\([^)]*\))+)/gi;
|
|
20
24
|
const CHAINED_METHOD_CALL = /\.\s*(get|post|put|delete|patch|options|head|all)\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?|\([^)]*\)\s*=>|async\s+\([^)]*\)\s*=>|function\s+[A-Za-z_$][A-Za-z0-9_$]*)/gi;
|
|
@@ -26,6 +30,8 @@ export function extractBehaviorContracts(content, file) {
|
|
|
26
30
|
...extractNestContracts(content, file),
|
|
27
31
|
...extractNestGraphqlContracts(content, file),
|
|
28
32
|
...extractNestProcessorContracts(content, file),
|
|
33
|
+
...extractNestScheduledContracts(content, file),
|
|
34
|
+
...extractNestEventContracts(content, file),
|
|
29
35
|
...extractRouterContracts(content, file, EXPRESS_ROUTER_CALL, "express"),
|
|
30
36
|
...extractExpressRouteChains(content, file),
|
|
31
37
|
...extractRouterContracts(content, file, FASTIFY_CALL, "fastify")
|
|
@@ -49,104 +55,148 @@ function extractFileRouteContracts(content, file) {
|
|
|
49
55
|
return contracts;
|
|
50
56
|
}
|
|
51
57
|
function extractNestContracts(content, file) {
|
|
52
|
-
const controllers =
|
|
53
|
-
index: match.index ?? 0,
|
|
54
|
-
path: normalizeRoutePath(match[2] ?? ""),
|
|
55
|
-
name: match[3]
|
|
56
|
-
}));
|
|
58
|
+
const controllers = classRanges(content, NEST_CONTROLLER_DECORATOR, 3, 2);
|
|
57
59
|
if (controllers.length === 0)
|
|
58
60
|
return [];
|
|
59
61
|
const contracts = [];
|
|
60
|
-
for (const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
file,
|
|
68
|
-
framework: "nestjs",
|
|
69
|
-
method,
|
|
70
|
-
path: routePath,
|
|
71
|
-
handler,
|
|
72
|
-
controller: controller?.name
|
|
73
|
-
}));
|
|
62
|
+
for (const controller of controllers) {
|
|
63
|
+
for (const match of controller.body.matchAll(NEST_METHOD_DECORATOR)) {
|
|
64
|
+
const method = httpMethod(match[1]);
|
|
65
|
+
const routePath = joinRoutePaths(normalizeRoutePath(controller.path ?? ""), match[3] ?? match[5] ?? "");
|
|
66
|
+
const handler = match[6];
|
|
67
|
+
contracts.push(makeContract({ file, framework: "nestjs", method, path: routePath, handler, controller: controller.name }));
|
|
68
|
+
}
|
|
74
69
|
}
|
|
75
70
|
return contracts;
|
|
76
71
|
}
|
|
77
72
|
function extractNestGraphqlContracts(content, file) {
|
|
78
|
-
// GraphQL resolvers are first-class user-triggerable entry points. In
|
|
79
|
-
// NestJS-heavy monorepos (Twenty: 415 @Query/@Mutation methods vs 139 HTTP
|
|
80
|
-
// routes) skipping them left almost every user behavior without an Endpoint
|
|
81
|
-
// anchor, so static flows rooted at internal orphan methods instead.
|
|
82
73
|
if (!/@(Query|Mutation|Subscription)\s*\(/.test(content))
|
|
83
74
|
return [];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
name: match[1]
|
|
88
|
-
}));
|
|
75
|
+
// Only methods lexically inside an actual @Resolver class qualify. Looking
|
|
76
|
+
// for @Query anywhere in a file misclassified query builders and ORM metadata.
|
|
77
|
+
const resolvers = classRanges(content, NEST_RESOLVER_DECORATOR, 1);
|
|
89
78
|
if (resolvers.length === 0)
|
|
90
79
|
return [];
|
|
91
80
|
const contracts = [];
|
|
92
|
-
for (const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
contracts.push(makeContract({
|
|
101
|
-
file,
|
|
102
|
-
framework: "nestjs",
|
|
103
|
-
kind: "graphql_operation",
|
|
104
|
-
method: opKind,
|
|
105
|
-
path: `graphql:${handler}`,
|
|
106
|
-
handler,
|
|
107
|
-
controller: resolver.name
|
|
108
|
-
}));
|
|
81
|
+
for (const resolver of resolvers) {
|
|
82
|
+
for (const match of resolver.body.matchAll(NEST_GQL_METHOD_DECORATOR)) {
|
|
83
|
+
const opKind = match[1].toUpperCase();
|
|
84
|
+
if (opKind === "RESOLVEFIELD")
|
|
85
|
+
continue;
|
|
86
|
+
const handler = match[2];
|
|
87
|
+
contracts.push(makeContract({ file, framework: "nestjs", kind: "graphql_operation", method: opKind, path: `graphql:${handler}`, handler, controller: resolver.name }));
|
|
88
|
+
}
|
|
109
89
|
}
|
|
110
90
|
return contracts;
|
|
111
91
|
}
|
|
112
92
|
function extractNestProcessorContracts(content, file) {
|
|
113
|
-
|
|
114
|
-
// system-triggerable, cross-layer, observable outcome). Anchoring them lets
|
|
115
|
-
// the flow walker show queue-driven chains instead of orphan roots.
|
|
116
|
-
const processors = [...content.matchAll(NEST_PROCESSOR_DECORATOR)].map((match) => ({
|
|
117
|
-
index: match.index ?? 0,
|
|
118
|
-
path: normalizeRoutePath(match[2] ?? ""),
|
|
119
|
-
name: match[3]
|
|
120
|
-
}));
|
|
93
|
+
const processors = classRanges(content, NEST_PROCESSOR_DECORATOR, 3, 2);
|
|
121
94
|
if (processors.length === 0)
|
|
122
95
|
return [];
|
|
123
96
|
const contracts = [];
|
|
124
|
-
for (const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
method: "JOB",
|
|
134
|
-
|
|
135
|
-
handler,
|
|
136
|
-
controller: processor.name
|
|
137
|
-
}));
|
|
97
|
+
for (const processor of processors) {
|
|
98
|
+
const handlers = new Set();
|
|
99
|
+
for (const match of processor.body.matchAll(NEST_PROCESS_METHOD_DECORATOR))
|
|
100
|
+
handlers.add(match[1]);
|
|
101
|
+
if (/\bextends\s+WorkerHost\b/.test(processor.header)) {
|
|
102
|
+
for (const match of processor.body.matchAll(NEST_WORKER_PROCESS_METHOD))
|
|
103
|
+
handlers.add(match[1]);
|
|
104
|
+
}
|
|
105
|
+
for (const handler of handlers) {
|
|
106
|
+
contracts.push(makeContract({ file, framework: "nestjs", kind: "queue_processor", method: "JOB", path: `queue:${processor.path || processor.name}`, handler, controller: processor.name }));
|
|
107
|
+
}
|
|
138
108
|
}
|
|
139
109
|
return contracts;
|
|
140
110
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
for (const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
111
|
+
function extractNestScheduledContracts(content, file) {
|
|
112
|
+
const contracts = [];
|
|
113
|
+
for (const owner of classRanges(content, CLASS_DECLARATION, 1)) {
|
|
114
|
+
for (const match of owner.body.matchAll(NEST_CRON_METHOD_DECORATOR)) {
|
|
115
|
+
const handler = match[1];
|
|
116
|
+
contracts.push(makeContract({ file, framework: "nestjs", kind: "scheduled_task", method: "SCHEDULE", path: `schedule:${handler}`, handler, controller: owner.name }));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return contracts;
|
|
120
|
+
}
|
|
121
|
+
function extractNestEventContracts(content, file) {
|
|
122
|
+
const contracts = [];
|
|
123
|
+
for (const owner of classRanges(content, CLASS_DECLARATION, 1)) {
|
|
124
|
+
for (const match of owner.body.matchAll(NEST_EVENT_METHOD_DECORATOR)) {
|
|
125
|
+
const handler = match[4];
|
|
126
|
+
const event = match[3] ?? handler;
|
|
127
|
+
contracts.push(makeContract({ file, framework: "nestjs", kind: "event_consumer", method: "EVENT", path: `event:${event}`, handler, controller: owner.name }));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return contracts;
|
|
131
|
+
}
|
|
132
|
+
function classRanges(content, pattern, nameGroup, pathGroup) {
|
|
133
|
+
const ranges = [];
|
|
134
|
+
for (const match of content.matchAll(pattern)) {
|
|
135
|
+
const start = match.index ?? 0;
|
|
136
|
+
const open = content.indexOf("{", start + match[0].length);
|
|
137
|
+
if (open === -1)
|
|
138
|
+
continue;
|
|
139
|
+
const close = matchingBrace(content, open);
|
|
140
|
+
if (close === -1)
|
|
141
|
+
continue;
|
|
142
|
+
ranges.push({
|
|
143
|
+
name: match[nameGroup],
|
|
144
|
+
...(pathGroup ? { path: match[pathGroup] ?? "" } : {}),
|
|
145
|
+
header: content.slice(start, open),
|
|
146
|
+
body: content.slice(open + 1, close)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return ranges;
|
|
150
|
+
}
|
|
151
|
+
function matchingBrace(content, open) {
|
|
152
|
+
let depth = 0;
|
|
153
|
+
let quote = "";
|
|
154
|
+
let lineComment = false;
|
|
155
|
+
let blockComment = false;
|
|
156
|
+
for (let i = open; i < content.length; i++) {
|
|
157
|
+
const c = content[i];
|
|
158
|
+
const next = content[i + 1] ?? "";
|
|
159
|
+
if (lineComment) {
|
|
160
|
+
if (c === "\n")
|
|
161
|
+
lineComment = false;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (blockComment) {
|
|
165
|
+
if (c === "*" && next === "/") {
|
|
166
|
+
blockComment = false;
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (quote) {
|
|
172
|
+
if (c === "\\") {
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (c === quote)
|
|
177
|
+
quote = "";
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (c === "/" && next === "/") {
|
|
181
|
+
lineComment = true;
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (c === "/" && next === "*") {
|
|
186
|
+
blockComment = true;
|
|
187
|
+
i++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
191
|
+
quote = c;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (c === "{")
|
|
195
|
+
depth++;
|
|
196
|
+
else if (c === "}" && --depth === 0)
|
|
197
|
+
return i;
|
|
148
198
|
}
|
|
149
|
-
return
|
|
199
|
+
return -1;
|
|
150
200
|
}
|
|
151
201
|
function extractRouterContracts(content, file, pattern, framework) {
|
|
152
202
|
const contracts = [];
|
|
@@ -2376,6 +2376,119 @@ export function confirmPair(ctx, testAbsPath, implAbsPath, behaviorName) {
|
|
|
2376
2376
|
reason: "behavior not evaluated"
|
|
2377
2377
|
});
|
|
2378
2378
|
}
|
|
2379
|
+
// Workspace/alias resolution is useful for candidate linkage in TS monorepos,
|
|
2380
|
+
// but resolution and a call reference are not assertion evidence. Keep this
|
|
2381
|
+
// resolver separate from the hard-proof confirmer below.
|
|
2382
|
+
import { readFileSync as _rf, existsSync as _ex, readdirSync as _rd } from "node:fs";
|
|
2383
|
+
import { dirname as _dn, join as _jn, resolve as _rs } from "node:path";
|
|
2384
|
+
const EXT_TRIES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
2385
|
+
const BUILT_DIRS = /\/(lib|dist|build|out)(\/|$)/;
|
|
2386
|
+
function tryFile(base) {
|
|
2387
|
+
for (const e of EXT_TRIES)
|
|
2388
|
+
if (_ex(base + e))
|
|
2389
|
+
return base + e;
|
|
2390
|
+
return null;
|
|
2391
|
+
}
|
|
2392
|
+
function readJsonSafe(p) {
|
|
2393
|
+
try {
|
|
2394
|
+
return JSON.parse(_rf(p, "utf8"));
|
|
2395
|
+
}
|
|
2396
|
+
catch {
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
/** Nearest tsconfig paths/baseUrl for a file, walking extends chains (depth-capped). */
|
|
2401
|
+
function tsconfigPathsFor(fileAbs, stopDir) {
|
|
2402
|
+
let dir = _dn(fileAbs);
|
|
2403
|
+
for (let i = 0; i < 12 && dir.startsWith(stopDir); i++) {
|
|
2404
|
+
const tc = _jn(dir, "tsconfig.json");
|
|
2405
|
+
if (_ex(tc)) {
|
|
2406
|
+
let merged = {};
|
|
2407
|
+
let cur = tc;
|
|
2408
|
+
for (let d = 0; d < 6 && cur; d++) {
|
|
2409
|
+
const j = readJsonSafe(cur);
|
|
2410
|
+
if (!j)
|
|
2411
|
+
break;
|
|
2412
|
+
const co = (j.compilerOptions ?? {});
|
|
2413
|
+
merged = { baseUrl: merged.baseUrl ?? (co.baseUrl ? _rs(_dn(cur), co.baseUrl) : undefined), paths: { ...co.paths, ...merged.paths } };
|
|
2414
|
+
cur = typeof j.extends === "string" ? (tryFile(_rs(_dn(cur), j.extends)) ?? (_ex(_rs(_dn(cur), j.extends + ".json")) ? _rs(_dn(cur), j.extends + ".json") : null)) : null;
|
|
2415
|
+
}
|
|
2416
|
+
return { baseUrl: merged.baseUrl ?? _dn(tc), paths: merged.paths ?? {} };
|
|
2417
|
+
}
|
|
2418
|
+
const parent = _dn(dir);
|
|
2419
|
+
if (parent === dir)
|
|
2420
|
+
break;
|
|
2421
|
+
dir = parent;
|
|
2422
|
+
}
|
|
2423
|
+
return null;
|
|
2424
|
+
}
|
|
2425
|
+
/** Workspace member name → package dir, from the root package.json workspaces globs. */
|
|
2426
|
+
function workspacePackages(repoRoot) {
|
|
2427
|
+
const out = new Map();
|
|
2428
|
+
const rootPkg = readJsonSafe(_jn(repoRoot, "package.json"));
|
|
2429
|
+
const ws = rootPkg?.workspaces;
|
|
2430
|
+
const globs = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
2431
|
+
const dirs = [];
|
|
2432
|
+
for (const g of globs) {
|
|
2433
|
+
if (g.endsWith("/*")) {
|
|
2434
|
+
const base = _jn(repoRoot, g.slice(0, -2));
|
|
2435
|
+
try {
|
|
2436
|
+
for (const d of _rd(base))
|
|
2437
|
+
dirs.push(_jn(base, d));
|
|
2438
|
+
}
|
|
2439
|
+
catch { /* absent */ }
|
|
2440
|
+
}
|
|
2441
|
+
else
|
|
2442
|
+
dirs.push(_jn(repoRoot, g));
|
|
2443
|
+
}
|
|
2444
|
+
for (const d of dirs) {
|
|
2445
|
+
const pj = readJsonSafe(_jn(d, "package.json"));
|
|
2446
|
+
const name = pj?.name;
|
|
2447
|
+
if (typeof name === "string")
|
|
2448
|
+
out.set(name, d);
|
|
2449
|
+
}
|
|
2450
|
+
return out;
|
|
2451
|
+
}
|
|
2452
|
+
/** Resolve one import specifier from a test file to an in-repo file, or null. */
|
|
2453
|
+
export function resolveSpecifier(spec, fromFile, repoRoot, wsPkgs) {
|
|
2454
|
+
if (spec.startsWith("."))
|
|
2455
|
+
return tryFile(_rs(_dn(fromFile), spec));
|
|
2456
|
+
const tc = tsconfigPathsFor(fromFile, repoRoot);
|
|
2457
|
+
if (tc) {
|
|
2458
|
+
for (const [pat, targets] of Object.entries(tc.paths)) {
|
|
2459
|
+
const star = pat.indexOf("*");
|
|
2460
|
+
const matches = star >= 0 ? spec.startsWith(pat.slice(0, star)) && spec.endsWith(pat.slice(star + 1)) : spec === pat;
|
|
2461
|
+
if (!matches)
|
|
2462
|
+
continue;
|
|
2463
|
+
const wild = star >= 0 ? spec.slice(pat.slice(0, star).length, spec.length - pat.slice(star + 1).length) : "";
|
|
2464
|
+
for (const t of targets) {
|
|
2465
|
+
const hit = tryFile(_rs(tc.baseUrl, t.replace("*", wild)));
|
|
2466
|
+
if (hit)
|
|
2467
|
+
return hit;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (!spec.startsWith("@") && !spec.includes(":")) {
|
|
2471
|
+
const viaBase = tryFile(_rs(tc.baseUrl, spec));
|
|
2472
|
+
if (viaBase)
|
|
2473
|
+
return viaBase;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
// workspace package name (longest-prefix match), with built→src subpath mapping
|
|
2477
|
+
for (const [name, dir] of wsPkgs) {
|
|
2478
|
+
if (spec !== name && !spec.startsWith(name + "/"))
|
|
2479
|
+
continue;
|
|
2480
|
+
const sub = spec === name ? "" : spec.slice(name.length + 1);
|
|
2481
|
+
const candidates = sub
|
|
2482
|
+
? [sub, sub.replace(BUILT_DIRS, "/src/"), "src/" + sub.replace(/^(lib|dist|build|out)\//, "")]
|
|
2483
|
+
: ["src/index", "index"];
|
|
2484
|
+
for (const c of candidates) {
|
|
2485
|
+
const hit = tryFile(_jn(dir, c));
|
|
2486
|
+
if (hit)
|
|
2487
|
+
return hit;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2379
2492
|
/**
|
|
2380
2493
|
* Run the confirmer over every candidate (test -> impl) pair (the resolver-derived
|
|
2381
2494
|
* MAY_RELATE_TO links). For each denominator-eligible exported symbol of the impl
|
package/dist/local/autoProve.js
CHANGED
|
@@ -794,7 +794,8 @@ export async function autoProve(root, opts, deps) {
|
|
|
794
794
|
dynamicProofRunner: deps.dynamicProofRunner,
|
|
795
795
|
coverageRunner: deps.coverageRunner,
|
|
796
796
|
analyze: deps.analyze,
|
|
797
|
-
aiProvider: deps.aiProvider
|
|
797
|
+
aiProvider: deps.aiProvider,
|
|
798
|
+
suppressBehaviorReportRefresh: deps.suppressBehaviorReportRefresh
|
|
798
799
|
};
|
|
799
800
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
800
801
|
const sourceRoot = resolve(graph.workspace.root);
|
package/dist/local/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { WORKSPACE_DIR } from "./workspace.js";
|
|
|
22
22
|
import { summarizeCorpusScope } from "./corpusScope.js";
|
|
23
23
|
import { jobJsonPath, jobLogPath, listJobs, newJobId, readJobRecord, updateJobRecord, writeJobRecord } from "./jobs/jobStore.js";
|
|
24
24
|
import { runGenerateJob } from "./jobs/runner.js";
|
|
25
|
+
import { ORANGEPRO_VERSION } from "./version.js";
|
|
25
26
|
function out(line = "") {
|
|
26
27
|
process.stdout.write(line + "\n");
|
|
27
28
|
}
|
|
@@ -157,13 +158,21 @@ function bar(value) {
|
|
|
157
158
|
}
|
|
158
159
|
async function main() {
|
|
159
160
|
const argv = process.argv.slice(2);
|
|
161
|
+
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
|
|
162
|
+
out(ORANGEPRO_VERSION);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
160
165
|
const [rawCommand, ...rawRest] = argv;
|
|
161
166
|
const command = !rawCommand || rawCommand.startsWith("--") ? "start" : rawCommand;
|
|
162
167
|
const rest = !rawCommand || rawCommand.startsWith("--") ? argv : rawRest;
|
|
163
168
|
const { positionals, flags } = parseArgs(rest);
|
|
164
169
|
const json = asBool(flags.json, false);
|
|
165
170
|
const cwd = process.cwd();
|
|
166
|
-
if (command === "help" || flags.help) {
|
|
171
|
+
if (command === "help" || flags.help || flags.version) {
|
|
172
|
+
if (flags.version) {
|
|
173
|
+
out(ORANGEPRO_VERSION);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
167
176
|
out(HELP);
|
|
168
177
|
return 0;
|
|
169
178
|
}
|
|
@@ -11,7 +11,7 @@ const MAX_REQUIREMENTS = 60;
|
|
|
11
11
|
* REQ-md-the-author-should-do-the-following-if-applicable and surfaced as the
|
|
12
12
|
* report's top suggested next action. Product docs (README, docs/) still count.
|
|
13
13
|
*/
|
|
14
|
-
const GOVERNANCE_MD_RE = /(^|\/)\.github\/|(^|\/)(CONTRIBUTING|CODE_OF_CONDUCT|PULL_REQUEST_TEMPLATE|ISSUE_TEMPLATE|SECURITY|SUPPORT|CHANGELOG|LICENSE|GOVERNANCE|MAINTAINERS|CODEOWNERS|AUTHORS)[^\/]*$|(^|\/)\.changeset
|
|
14
|
+
const GOVERNANCE_MD_RE = /(^|\/)\.github\/|(^|\/)(CONTRIBUTING|CODE_OF_CONDUCT|PULL_REQUEST_TEMPLATE|ISSUE_TEMPLATE|SECURITY|SUPPORT|CHANGELOG|LICENSE|GOVERNANCE|MAINTAINERS|CODEOWNERS|AUTHORS)[^\/]*$|(^|\/)\.changeset\/|(^|\/)(AGENTS|CLAUDE|GEMINI|COPILOT)\.md$|(^|\/)\.cursor(rules)?\//i;
|
|
15
15
|
/** Words that suggest a heading describes a requirement/feature behavior. */
|
|
16
16
|
const REQUIREMENT_HINTS = [
|
|
17
17
|
"requirement",
|
|
@@ -3,7 +3,7 @@ import { LOCAL_GRAPH_SCHEMA_VERSION } from "../graph/ontology.js";
|
|
|
3
3
|
import { rankRiskGaps } from "../score/risk.js";
|
|
4
4
|
import { stableId } from "../util/ids.js";
|
|
5
5
|
const DEFAULT_MAX_DEPTH = 8;
|
|
6
|
-
const DEFAULT_MAX_FLOWS_PER_ENTRY =
|
|
6
|
+
const DEFAULT_MAX_FLOWS_PER_ENTRY = 20;
|
|
7
7
|
const DEFAULT_GLOBAL_CAP = 500;
|
|
8
8
|
const HIGH_ROUTE_RE = /payment|refund|checkout|cart|order|auth|login|token|customer|user|tax|fulfillment|ship/i;
|
|
9
9
|
const MUTATION_METHOD_RE = /^(POST|PUT|PATCH|DELETE)\b/i;
|
|
@@ -103,16 +103,13 @@ export function rankEntries(graph, entries, adjacency) {
|
|
|
103
103
|
gap.id,
|
|
104
104
|
gap.risk_score
|
|
105
105
|
]));
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// slots while every HTTP/GraphQL entry point went unrendered.
|
|
106
|
+
// One score space across every topology. Endpoint fallback weight keeps real
|
|
107
|
+
// external triggers prominent, while a materially riskier internal/library
|
|
108
|
+
// behavior can still outrank them. A hard Endpoint/Behavior partition made
|
|
109
|
+
// server-shaped repos look good but hid critical roots in mixed/library repos.
|
|
111
110
|
const score = (e) => Math.max(riskScores.get(e.start) ?? 0, fallbackScore(e, adjacency));
|
|
112
|
-
const byScore = (a, b) => score(b) - score(a) || a.external_id.localeCompare(b.external_id) || a.start.localeCompare(b.start);
|
|
113
|
-
|
|
114
|
-
const behaviors = entries.filter((e) => e.kind !== "Endpoint").sort(byScore);
|
|
115
|
-
return [...endpoints, ...behaviors];
|
|
111
|
+
const byScore = (a, b) => score(b) - score(a) || (a.kind === b.kind ? 0 : a.kind === "Endpoint" ? -1 : 1) || a.external_id.localeCompare(b.external_id) || a.start.localeCompare(b.start);
|
|
112
|
+
return [...entries].sort(byScore);
|
|
116
113
|
}
|
|
117
114
|
function prunePrefixSubsumed(flows) {
|
|
118
115
|
const sorted = [...flows].sort((a, b) => b.path.length - a.path.length || a.id.localeCompare(b.id));
|
|
@@ -552,7 +552,7 @@ export function gatherContext(graph, behavior, framework, fileReader) {
|
|
|
552
552
|
acceptance_criteria: acceptance,
|
|
553
553
|
workflow_steps: workflow,
|
|
554
554
|
framework,
|
|
555
|
-
test_layer:
|
|
555
|
+
test_layer: inferTestLayer(behavior, framework, graph),
|
|
556
556
|
code_context: dedupe(codeContext),
|
|
557
557
|
source_excerpts: excerpts,
|
|
558
558
|
weak_context: dedupe(weakContext),
|
|
@@ -602,7 +602,7 @@ function flowChainFor(graph, behavior) {
|
|
|
602
602
|
};
|
|
603
603
|
});
|
|
604
604
|
}
|
|
605
|
-
function
|
|
605
|
+
export function inferTestLayer(behavior, framework, graph) {
|
|
606
606
|
const fw = framework.toLowerCase();
|
|
607
607
|
if (fw.includes("playwright") || fw.includes("cypress"))
|
|
608
608
|
return "e2e";
|
|
@@ -613,22 +613,40 @@ function inferLayer(behavior, framework, graph) {
|
|
|
613
613
|
const hint = String(behavior.properties.test_layer ?? "");
|
|
614
614
|
if (hint)
|
|
615
615
|
return hint;
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
616
|
+
// Infer a layer only from evidence that actually distinguishes it. A CALLS
|
|
617
|
+
// edge alone says nothing about the intended test boundary: libraries and
|
|
618
|
+
// ordinary unit subjects call helpers too. External handlers are API targets;
|
|
619
|
+
// they become integration targets only when their reachable path crosses a
|
|
620
|
+
// source-file boundary. Ambiguous cases stay unknown instead of being tuned
|
|
621
|
+
// to a server application's topology.
|
|
621
622
|
if (graph) {
|
|
622
623
|
const id = behavior.external_id;
|
|
623
624
|
const isEntryHandler = graph.edges.some((e) => e.relationship_type === "IMPLEMENTED_IN" && e.to_external_id === id);
|
|
625
|
+
const fileById = new Map(graph.nodes
|
|
626
|
+
.filter((node) => node.kind === "CodeSymbol")
|
|
627
|
+
.map((node) => [node.external_id, String(node.properties.file ?? "")]));
|
|
628
|
+
const behaviorFile = fileById.get(id) ?? "";
|
|
629
|
+
const crossesFile = (from, to) => {
|
|
630
|
+
const fromFile = fileById.get(from) ?? "";
|
|
631
|
+
const toFile = fileById.get(to) ?? "";
|
|
632
|
+
return fromFile !== "" && toFile !== "" && fromFile !== toFile;
|
|
633
|
+
};
|
|
634
|
+
const directCrossBoundary = graph.edges.some((e) => e.relationship_type === "CALLS" && e.from_external_id === id && crossesFile(e.from_external_id, e.to_external_id));
|
|
635
|
+
const triggeredCrossBoundary = (graph.analysis?.flows?.flows ?? []).some((flow) => {
|
|
636
|
+
if (flow.entry_point.kind !== "Endpoint")
|
|
637
|
+
return false;
|
|
638
|
+
const chain = flow.hops.length > 0 ? [flow.hops[0].from, ...flow.hops.map((hop) => hop.to)] : [];
|
|
639
|
+
if (!chain.includes(id))
|
|
640
|
+
return false;
|
|
641
|
+
const files = new Set(chain.map((symbolId) => fileById.get(symbolId) ?? "").filter(Boolean));
|
|
642
|
+
return files.size > 1;
|
|
643
|
+
});
|
|
624
644
|
if (isEntryHandler)
|
|
625
|
-
return "integration";
|
|
626
|
-
|
|
627
|
-
(graph.analysis?.flows?.flows ?? []).some((f) => f.entry_point.external_id === id || f.hops.some((h) => h.from === id || h.to === id));
|
|
628
|
-
if (inChain)
|
|
645
|
+
return directCrossBoundary || triggeredCrossBoundary ? "integration" : "api";
|
|
646
|
+
if (behaviorFile && triggeredCrossBoundary)
|
|
629
647
|
return "integration";
|
|
630
648
|
}
|
|
631
|
-
return "
|
|
649
|
+
return "unknown";
|
|
632
650
|
}
|
|
633
651
|
function tooThin(ctx) {
|
|
634
652
|
const needed = [];
|
package/dist/local/operations.js
CHANGED
|
@@ -1327,11 +1327,13 @@ export function opProveLoop(root, opts, deps = defaultDeps()) {
|
|
|
1327
1327
|
// Best-effort report refresh (exactly like ai-flows --apply): a render failure
|
|
1328
1328
|
// must never fail a completed proof — the ledger/cert is already written.
|
|
1329
1329
|
let behaviorCoveragePath;
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1330
|
+
if (!deps.suppressBehaviorReportRefresh) {
|
|
1331
|
+
try {
|
|
1332
|
+
behaviorCoveragePath = opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`).behavior_coverage_path;
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
// ponytail: swallow — refresh is advisory; DynamicProofResult carries no warnings channel.
|
|
1336
|
+
}
|
|
1335
1337
|
}
|
|
1336
1338
|
return { ...proof, ...(behaviorCoveragePath ? { behavior_coverage_path: behaviorCoveragePath } : {}) };
|
|
1337
1339
|
}
|
|
@@ -1405,11 +1407,13 @@ export async function opAiFlows(root, opts = {}, deps = defaultDeps()) {
|
|
|
1405
1407
|
// Best-effort: a render failure must never fail apply (graph is saved).
|
|
1406
1408
|
const warnings = [...result.warnings];
|
|
1407
1409
|
let behaviorCoveragePath;
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1410
|
+
if (!deps.suppressBehaviorReportRefresh) {
|
|
1411
|
+
try {
|
|
1412
|
+
behaviorCoveragePath = opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`).behavior_coverage_path;
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
warnings.push(`behavior coverage view not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1416
|
+
}
|
|
1413
1417
|
}
|
|
1414
1418
|
return { ...result, warnings, ...(behaviorCoveragePath ? { behavior_coverage_path: behaviorCoveragePath } : {}) };
|
|
1415
1419
|
}
|
|
@@ -1471,11 +1475,7 @@ function writeStartStaticSnapshot(root, baseRef, warnings) {
|
|
|
1471
1475
|
let behaviorCoveragePath;
|
|
1472
1476
|
try {
|
|
1473
1477
|
reportProgress("artifacts: writing static behavior view (proof still running)", { current: 4, total: 8 });
|
|
1474
|
-
behaviorCoveragePath = opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`, {
|
|
1475
|
-
attempted: 0,
|
|
1476
|
-
proven: 0,
|
|
1477
|
-
needsSetup: []
|
|
1478
|
-
}).behavior_coverage_path;
|
|
1478
|
+
behaviorCoveragePath = opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`, { attempted: 0, proven: 0, needsSetup: [] }, { persistBaseline: false }).behavior_coverage_path;
|
|
1479
1479
|
}
|
|
1480
1480
|
catch (error) {
|
|
1481
1481
|
warnings.push(`static behavior view not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1493,7 +1493,9 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1493
1493
|
const providerOpts = startProviderOverride(root, opts);
|
|
1494
1494
|
const scanRoot = opts.source ? resolve(opts.source) : resolve(root);
|
|
1495
1495
|
const providerEnv = loadProviderEnv([root, scanRoot], deps.env);
|
|
1496
|
-
|
|
1496
|
+
// `start` writes advisory/intermediate views before the final report. Keep
|
|
1497
|
+
// those helpers from advancing report-baseline.json mid-run.
|
|
1498
|
+
const providerDeps = { ...deps, env: providerEnv, suppressBehaviorReportRefresh: true };
|
|
1497
1499
|
const scope = summarizeCorpusScope(scanRoot);
|
|
1498
1500
|
reportProgress(`start: preflight found ${scope.files.toLocaleString()} source/doc file(s)`, { current: 1, total: 8 });
|
|
1499
1501
|
reportProgress("start: running deterministic analysis", { current: 2, total: 8 });
|
|
@@ -1995,7 +1997,7 @@ export function opGraphHtml(root, outputPath = "orangepro-graph.html") {
|
|
|
1995
1997
|
return { graph_html_path: htmlPath };
|
|
1996
1998
|
}
|
|
1997
1999
|
/** Write the self-contained offline behavior-coverage view (deterministic, metadata only). */
|
|
1998
|
-
export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-coverage.html", dynamicProof) {
|
|
2000
|
+
export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-coverage.html", dynamicProof, options = {}) {
|
|
1999
2001
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
2000
2002
|
// Standalone regens have no this-run outcome: fall back to the persisted
|
|
2001
2003
|
// proof-attempts sidecar ONLY when it anchors to the current graph+commit
|
|
@@ -2017,11 +2019,13 @@ export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-co
|
|
|
2017
2019
|
const html = renderBehaviorReport(data);
|
|
2018
2020
|
const htmlPath = resolve(root, outputPath);
|
|
2019
2021
|
writeFileSync(htmlPath, html, "utf8");
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2022
|
+
if (options.persistBaseline !== false) {
|
|
2023
|
+
try {
|
|
2024
|
+
writeFileSync(baselinePath, JSON.stringify(reportBaselineOf(data, new Date().toISOString())), "utf8");
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
// baseline write is advisory
|
|
2028
|
+
}
|
|
2025
2029
|
}
|
|
2026
2030
|
return { behavior_coverage_path: htmlPath };
|
|
2027
2031
|
}
|
package/dist/local/score/risk.js
CHANGED
|
@@ -93,7 +93,7 @@ function gitFirstCommitBatch(root, files) {
|
|
|
93
93
|
}
|
|
94
94
|
return out;
|
|
95
95
|
}
|
|
96
|
-
function isEntryPoint(node) {
|
|
96
|
+
export function isEntryPoint(node) {
|
|
97
97
|
const file = symbolFile(node);
|
|
98
98
|
const title = symbolTitle(node);
|
|
99
99
|
if (API_HANDLER_NAME_RE.test(title) && /(^|\/)api(s)?\//i.test(file))
|
|
@@ -424,9 +424,12 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
424
424
|
};
|
|
425
425
|
})
|
|
426
426
|
.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));
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
427
|
+
// The default API is the true global ranking because reports call this list
|
|
428
|
+
// "top risks". Callers may explicitly request a diversified portfolio, but
|
|
429
|
+
// that presentation policy must never silently redefine rank.
|
|
430
|
+
if (opts.maxPerFile === undefined)
|
|
431
|
+
return ranked.slice(0, limit);
|
|
432
|
+
const maxPerFile = Math.max(1, opts.maxPerFile);
|
|
430
433
|
const perFile = new Map();
|
|
431
434
|
const surfaced = [];
|
|
432
435
|
const overflow = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { buildRtm } from "../rtm.js";
|
|
3
|
-
import { rankRiskGaps } from "../score/risk.js";
|
|
3
|
+
import { isEntryPoint, rankRiskGaps } from "../score/risk.js";
|
|
4
4
|
import { PROOF_BLOCKER_GUIDE } from "../proofDoctor.js";
|
|
5
5
|
/** Short human phrase per R-1 needs_setup category, for the "blocked because: …" panel copy. */
|
|
6
6
|
const BLOCK_CATEGORY_LABEL = {
|
|
@@ -285,7 +285,9 @@ function flows(graph, rows, risks) {
|
|
|
285
285
|
const proven = new Set(rows.filter((r) => r.evidence_tier === "proven").map((r) => r.behavior_id));
|
|
286
286
|
const riskById = new Map(risks.map((r) => [r.id, r]));
|
|
287
287
|
return (graph.analysis?.flows?.flows ?? []).map((flow) => {
|
|
288
|
-
const
|
|
288
|
+
const rootNode = nodesById.get(flow.entry_point.external_id);
|
|
289
|
+
const trigger = endpointTrigger(rootNode);
|
|
290
|
+
const root_entry = rootNode ? isEntryPoint(rootNode) : false;
|
|
289
291
|
const proof = proven.has(flow.entry_point.external_id) || proven.has(flow.hops[0]?.from ?? "") ? "proven" : "none";
|
|
290
292
|
const steps = [
|
|
291
293
|
{
|
|
@@ -306,6 +308,7 @@ function flows(graph, rows, risks) {
|
|
|
306
308
|
return {
|
|
307
309
|
title: flow.entry_point.title || flow.entry_point.external_id,
|
|
308
310
|
trigger,
|
|
311
|
+
root_entry,
|
|
309
312
|
risk: risk ? riskBucket(risk.risk_score, maxRiskScore) : null,
|
|
310
313
|
proof,
|
|
311
314
|
services,
|
|
@@ -455,17 +458,37 @@ export function computeReportDelta(prev, cur) {
|
|
|
455
458
|
export function reportBaselineOf(cur, ts) {
|
|
456
459
|
return { ts, summary: cur.summary, riskPaths: cur.risks.map((r) => r.path), generatedTotal: cur.generatedTotal };
|
|
457
460
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
461
|
+
const HTTP_TRIGGER_VERBS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"]);
|
|
462
|
+
const GRAPHQL_TRIGGER_VERBS = new Set(["MUTATION", "QUERY", "SUBSCRIPTION"]);
|
|
463
|
+
/** Map every trigger into a stable lane. Known protocol families get friendly
|
|
464
|
+
* labels; unknown framework adapters still receive a deterministic lane rather
|
|
465
|
+
* than being silently deleted from the system map. */
|
|
466
|
+
export function laneForTrigger(trigger) {
|
|
467
|
+
const verb = (trigger.verb || "OTHER").trim().toUpperCase();
|
|
468
|
+
const triggerPath = (trigger.path || "").toLowerCase();
|
|
469
|
+
if (HTTP_TRIGGER_VERBS.has(verb) || triggerPath.startsWith("http:"))
|
|
470
|
+
return { id: "http", label: "HTTP", priority: 20 };
|
|
471
|
+
if (GRAPHQL_TRIGGER_VERBS.has(verb) || triggerPath.startsWith("graphql:"))
|
|
472
|
+
return { id: "graphql", label: "GraphQL", priority: 10 };
|
|
473
|
+
if (["JOB", "QUEUE", "PROCESS", "WORKER"].includes(verb) || triggerPath.startsWith("queue:"))
|
|
474
|
+
return { id: "job", label: "Jobs", priority: 30 };
|
|
475
|
+
if (["SCHEDULE", "CRON", "TIMER"].includes(verb) || triggerPath.startsWith("schedule:") || triggerPath.startsWith("cron:"))
|
|
476
|
+
return { id: "schedule", label: "Scheduled", priority: 40 };
|
|
477
|
+
if (["EVENT", "MESSAGE", "CONSUME", "CONSUMER", "SUBSCRIBE"].includes(verb) || triggerPath.startsWith("event:"))
|
|
478
|
+
return { id: "event", label: "Events", priority: 50 };
|
|
479
|
+
if (["COMMAND", "CLI"].includes(verb) || triggerPath.startsWith("cli:"))
|
|
480
|
+
return { id: "cli", label: "CLI", priority: 60 };
|
|
481
|
+
if (["RPC", "GRPC"].includes(verb) || triggerPath.startsWith("rpc:") || triggerPath.startsWith("grpc:"))
|
|
482
|
+
return { id: "rpc", label: "RPC", priority: 70 };
|
|
483
|
+
const id = verb.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "other";
|
|
484
|
+
const label = verb
|
|
485
|
+
.toLowerCase()
|
|
486
|
+
.split(/[^a-z0-9]+/)
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
489
|
+
.join(" ") || "Other";
|
|
490
|
+
return { id, label, priority: 100 };
|
|
491
|
+
}
|
|
469
492
|
function ownerOfSig(sig) {
|
|
470
493
|
const base = sig.includes("#") ? sig.slice(sig.indexOf("#") + 1) : sig;
|
|
471
494
|
return base.includes(".") ? base.slice(0, base.indexOf(".")) : base;
|
|
@@ -477,7 +500,22 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
477
500
|
// on a full-scale repo. A flow's representative service is its most
|
|
478
501
|
// DISTINCTIVE owner: lowest global frequency, deepest step on ties, with
|
|
479
502
|
// near-ubiquitous owners eligible only when a flow touches nothing else.
|
|
480
|
-
const
|
|
503
|
+
const laneForFlow = (flow) => {
|
|
504
|
+
if (flow.trigger)
|
|
505
|
+
return laneForTrigger(flow.trigger);
|
|
506
|
+
// A program root without a framework trigger is still a real product entry
|
|
507
|
+
// point (CLI/main). Internal call-graph roots remain excluded.
|
|
508
|
+
return flow.root_entry ? { id: "entry", label: "Entry points", priority: 80 } : null;
|
|
509
|
+
};
|
|
510
|
+
const triggerFlows = data.flows.filter((flow) => laneForFlow(flow) !== null);
|
|
511
|
+
const laneMeta = new Map();
|
|
512
|
+
for (const flow of triggerFlows) {
|
|
513
|
+
const lane = laneForFlow(flow);
|
|
514
|
+
laneMeta.set(lane.id, lane);
|
|
515
|
+
}
|
|
516
|
+
const laneOrder = [...laneMeta.values()]
|
|
517
|
+
.sort((a, b) => a.priority - b.priority || a.label.localeCompare(b.label) || a.id.localeCompare(b.id))
|
|
518
|
+
.map((lane) => lane.id);
|
|
481
519
|
const ownerFreq = new Map();
|
|
482
520
|
for (const f of triggerFlows) {
|
|
483
521
|
const seen = new Set();
|
|
@@ -496,7 +534,7 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
496
534
|
const edgeFlows = new Map();
|
|
497
535
|
const svcLaneFlows = new Map();
|
|
498
536
|
for (const f of triggerFlows) {
|
|
499
|
-
const lane =
|
|
537
|
+
const lane = laneForFlow(f);
|
|
500
538
|
const steps = f.steps ?? [];
|
|
501
539
|
let svc = "";
|
|
502
540
|
let bestFreq = Infinity;
|
|
@@ -538,18 +576,18 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
538
576
|
best = l;
|
|
539
577
|
bestN = n;
|
|
540
578
|
}
|
|
541
|
-
return
|
|
579
|
+
return laneOrder.indexOf(best);
|
|
542
580
|
};
|
|
543
581
|
const byTraffic = [...svcFlows.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
544
582
|
const chosen = new Set();
|
|
545
583
|
// 1) Every lane is guaranteed its top 2 services — a lane with 56 flows must
|
|
546
584
|
// never render edge-less just because its traffic is spread thin.
|
|
547
|
-
for (const laneId of
|
|
585
|
+
for (const laneId of laneOrder) {
|
|
548
586
|
let taken = 0;
|
|
549
587
|
for (const [svc] of byTraffic) {
|
|
550
588
|
if (taken >= 2)
|
|
551
589
|
break;
|
|
552
|
-
if (laneRank(svc) ===
|
|
590
|
+
if (laneRank(svc) === laneOrder.indexOf(laneId) && !chosen.has(svc)) {
|
|
553
591
|
chosen.add(svc);
|
|
554
592
|
taken++;
|
|
555
593
|
}
|
|
@@ -606,7 +644,6 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
606
644
|
e.critical = true;
|
|
607
645
|
riskBySvc.set(svc, e);
|
|
608
646
|
}
|
|
609
|
-
const laneOrder = ["graphql", "http", "job"];
|
|
610
647
|
// Conservation: edges drawn per lane must sum to the lane's stated count.
|
|
611
648
|
// Everything below the cut aggregates into one dashed "+N more services"
|
|
612
649
|
// node per lane — 51 job flows must never silently vanish.
|
|
@@ -644,7 +681,7 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
644
681
|
return {
|
|
645
682
|
lanes: laneOrder
|
|
646
683
|
.filter((id) => laneFlows.has(id))
|
|
647
|
-
.map((id) => ({ id, label: id
|
|
684
|
+
.map((id) => ({ id, label: laneMeta.get(id)?.label ?? id, flows: laneFlows.get(id) ?? 0 })),
|
|
648
685
|
services: [
|
|
649
686
|
...top.map(([label, flows]) => ({
|
|
650
687
|
id: label,
|
|
@@ -722,6 +759,23 @@ function riskRows(risks, graph) {
|
|
|
722
759
|
for (const r of risks)
|
|
723
760
|
if (!firstRowForFile.has(r.file))
|
|
724
761
|
firstRowForFile.set(r.file, r.id);
|
|
762
|
+
// Ambiguity-qualified display: identical titles from DIFFERENT files
|
|
763
|
+
// (multi-program repos: 76 x main) get their top-level dir as a prefix.
|
|
764
|
+
// Purely display; single-file titles render unchanged everywhere else.
|
|
765
|
+
const titleFiles = new Map();
|
|
766
|
+
for (const r of risks) {
|
|
767
|
+
const t = (r.title || "").split("(")[0].trim();
|
|
768
|
+
if (!titleFiles.has(t))
|
|
769
|
+
titleFiles.set(t, new Set());
|
|
770
|
+
titleFiles.get(t).add(r.file);
|
|
771
|
+
}
|
|
772
|
+
const qualify = (risk, path) => {
|
|
773
|
+
const t = (risk.title || "").split("(")[0].trim();
|
|
774
|
+
if ((titleFiles.get(t)?.size ?? 0) <= 1)
|
|
775
|
+
return path;
|
|
776
|
+
const top = (risk.file || "").split("/").filter(Boolean)[0] ?? "";
|
|
777
|
+
return top ? `${top}: ${path}` : path;
|
|
778
|
+
};
|
|
725
779
|
return risks.map((risk, idx) => {
|
|
726
780
|
const methodMatch = risk.title.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i);
|
|
727
781
|
const tags = [];
|
|
@@ -736,17 +790,17 @@ function riskRows(risks, graph) {
|
|
|
736
790
|
...(() => {
|
|
737
791
|
const generatedTests = riskGeneratedTests(graph, risk, riskIds, firstRowForFile.get(risk.file) === risk.id);
|
|
738
792
|
const verb = methodMatch?.[1]?.toUpperCase() ?? "BEHAVIOR";
|
|
739
|
-
const path = methodMatch?.[2] ?? risk.title;
|
|
740
|
-
const
|
|
793
|
+
const path = qualify(risk, methodMatch?.[2] ?? risk.title);
|
|
794
|
+
const generatedCategories = [...new Set([
|
|
741
795
|
...generatedTests.map((t) => (t.bucket ? BUCKET_TO_CONCERN[t.bucket] : undefined)),
|
|
742
|
-
// An integration/api/e2e-layer
|
|
743
|
-
//
|
|
796
|
+
// An integration/api/e2e-layer draft targets integration_flow. This
|
|
797
|
+
// remains generation metadata until dynamic proof closes.
|
|
744
798
|
...generatedTests.map((t) => (t.concern === "integration" || t.concern === "api" || t.concern === "e2e" ? "integration_flow" : undefined))
|
|
745
799
|
].filter((c) => Boolean(c)))];
|
|
746
800
|
return {
|
|
747
801
|
generatedTests,
|
|
748
802
|
applicableCategories: riskApplicableConcerns(risk, verb),
|
|
749
|
-
|
|
803
|
+
generatedCategories,
|
|
750
804
|
verb,
|
|
751
805
|
path,
|
|
752
806
|
todo: riskTodo(risk, verb, path, generatedTests)
|
|
@@ -230,9 +230,8 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
230
230
|
.cat-strip-lbl{font-size:9.5px;color:var(--muted);font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin-bottom:6px}
|
|
231
231
|
.cat-pills{display:flex;flex-wrap:wrap;gap:5px}
|
|
232
232
|
.cat-pill{font-size:10px;padding:3px 8px;border-radius:4px;font-weight:600;letter-spacing:.01em;display:inline-flex;align-items:center;gap:4px}
|
|
233
|
-
.cat-pill.cp-
|
|
234
|
-
.cat-pill.cp-
|
|
235
|
-
.cat-pill.cp-locked::after{content:"\\1F512";font-size:8px}
|
|
233
|
+
.cat-pill.cp-drafted{background:var(--obg);border:1px solid var(--obd);color:var(--orange)}
|
|
234
|
+
.cat-pill.cp-open{background:transparent;border:1px dashed var(--bd2);color:var(--faint)}
|
|
236
235
|
/* PAYWALL CTA */
|
|
237
236
|
.paywall{background:linear-gradient(135deg,rgba(240,136,62,.08),rgba(240,136,62,.02));border:1px dashed var(--obd);border-radius:9px;padding:14px 16px;margin-top:14px;text-align:center}
|
|
238
237
|
.paywall-num{font-size:18px;font-weight:750;color:var(--orange);font-variant-numeric:tabular-nums}
|
|
@@ -382,7 +381,8 @@ const D=window.DATA,$=(s)=>document.querySelector(s);
|
|
|
382
381
|
var laneY={},svcY={};
|
|
383
382
|
M.lanes.forEach(function(l,i){laneY[l.id]=padT+i*rowH+(H-2*padT-M.lanes.length*rowH)/2+rowH/2;});
|
|
384
383
|
M.services.forEach(function(sv,i){svcY[sv.id]=padT+i*rowH+rowH/2;});
|
|
385
|
-
var
|
|
384
|
+
var lanePalette=["var(--purple,#a78bfa)","var(--blue)","var(--orange)","var(--green)","var(--red)","#22d3ee","#f472b6","var(--muted)"];
|
|
385
|
+
var laneColor={};M.lanes.forEach(function(l,i){laneColor[l.id]=lanePalette[i%lanePalette.length];});
|
|
386
386
|
var tierColor=function(t){var tot=t.proven+t.assoc+t.candidate+t.none||1;
|
|
387
387
|
if(t.proven/tot>=0.5)return "var(--green)";
|
|
388
388
|
if((t.proven+t.assoc)/tot>=0.5)return "var(--blue)";
|
|
@@ -443,7 +443,7 @@ const D=window.DATA,$=(s)=>document.querySelector(s);
|
|
|
443
443
|
bc.innerHTML='';
|
|
444
444
|
var backBtn=document.createElement("button");
|
|
445
445
|
backBtn.className="sm-crumb-btn";
|
|
446
|
-
backBtn.textContent="
|
|
446
|
+
backBtn.textContent="← Back to system map";
|
|
447
447
|
backBtn.addEventListener("click",function(){
|
|
448
448
|
if(search){search.value="";search.dispatchEvent(new Event("input"));}
|
|
449
449
|
bc.remove();
|
|
@@ -453,7 +453,7 @@ const D=window.DATA,$=(s)=>document.querySelector(s);
|
|
|
453
453
|
});
|
|
454
454
|
var clearBtn=document.createElement("button");
|
|
455
455
|
clearBtn.className="sm-crumb-btn";
|
|
456
|
-
clearBtn.textContent="
|
|
456
|
+
clearBtn.textContent="✕ Clear filter “"+svc+"”";
|
|
457
457
|
clearBtn.addEventListener("click",function(){
|
|
458
458
|
if(search){search.value="";search.dispatchEvent(new Event("input"));}
|
|
459
459
|
bc.remove();
|
|
@@ -523,7 +523,7 @@ function describeBehavior(b){
|
|
|
523
523
|
:b.tier==="assoc"?"A real test imports and calls it, but no mutation proof exists yet."
|
|
524
524
|
:b.tier==="candidate"?"Only a lexically similar test file was found — an unconfirmed lead, not evidence."
|
|
525
525
|
:"No test signal of any kind was found for it.";
|
|
526
|
-
const s1="
|
|
526
|
+
const s1="“"+h.words+"”"+(h.owner?" on "+h.owner:"")+" — a behavior in "+(mod||"this repo")+".";
|
|
527
527
|
const s2=reach.length?"A user or system reaches it via "+reach.join(" and ")+(reach.length>=2?" (and possibly more)":"")+".":(b.reachable?"It appears inside at least one statically traced call chain.":"No entry-anchored call chain reaches it in the static flow set.");
|
|
528
528
|
return s1+" "+s2+" "+tierTxt;
|
|
529
529
|
}
|
|
@@ -549,14 +549,14 @@ function renderBeh(){
|
|
|
549
549
|
const rows=D.behaviors.filter(b=>(!activeGrp||b.group===activeGrp)&&(!activeTier||tierOf(b).cls===activeTier)&&(!searchQ||(b.sig+" "+b.file).toLowerCase().includes(searchQ)));
|
|
550
550
|
if(rows.length===0){
|
|
551
551
|
const parts=[];
|
|
552
|
-
if(searchQ)parts.push('search
|
|
552
|
+
if(searchQ)parts.push('search “'+searchQ+'”');
|
|
553
553
|
if(activeTier)parts.push('the selected evidence tier');
|
|
554
|
-
if(activeGrp)parts.push('package
|
|
554
|
+
if(activeGrp)parts.push('package “'+activeGrp+'”');
|
|
555
555
|
const why=parts.length?parts.join(' + '):'the current filters';
|
|
556
556
|
const box=el("div","beh-empty",
|
|
557
|
-
'<p>No behaviors match '+esc(why)+'. Filters combine
|
|
557
|
+
'<p>No behaviors match '+esc(why)+'. Filters combine — clearing one usually brings results back.</p>');
|
|
558
558
|
if(searchQ){
|
|
559
|
-
const b1=el("button","sm-crumb-btn","
|
|
559
|
+
const b1=el("button","sm-crumb-btn","✕ Clear search");
|
|
560
560
|
b1.onclick=function(){searchEl.value="";searchQ="";renderBeh();};
|
|
561
561
|
box.append(b1);
|
|
562
562
|
}
|
|
@@ -697,15 +697,16 @@ function riskCardHtml(r){
|
|
|
697
697
|
// category strip: show which concern categories apply to this flow
|
|
698
698
|
let catHtml='';
|
|
699
699
|
if(r.applicableCategories&&r.applicableCategories.length){
|
|
700
|
-
const
|
|
700
|
+
const draftedConcerns=new Set(r.generatedCategories||[]);
|
|
701
701
|
const pills=r.applicableCategories.map(c=>{
|
|
702
|
-
const
|
|
702
|
+
const drafted=draftedConcerns.has(c);
|
|
703
703
|
const label=c.replace(/_/g,' ');
|
|
704
|
-
|
|
704
|
+
const hint=drafted?'Generated draft attached; not coverage or proof':'Applicable category without a generated draft';
|
|
705
|
+
return \`<span class="cat-pill \${drafted?'cp-drafted':'cp-open'}" title="\${hint}">\${esc(label)}</span>\`;
|
|
705
706
|
}).join('');
|
|
706
|
-
const shownN=r.applicableCategories.filter(c=>
|
|
707
|
+
const shownN=r.applicableCategories.filter(c=>draftedConcerns.has(c)).length;
|
|
707
708
|
const totalN=r.applicableCategories.length;
|
|
708
|
-
catHtml=\`<div class="cat-strip"><div class="cat-strip-lbl">
|
|
709
|
+
catHtml=\`<div class="cat-strip"><div class="cat-strip-lbl">Generated drafts: \${shownN} of \${totalN} applicable testing categories</div><div class="cat-pills">\${pills}</div></div>\`;
|
|
709
710
|
}
|
|
710
711
|
let testsHtml='';
|
|
711
712
|
if(r.generatedTests&&r.generatedTests.length){
|
|
@@ -61,20 +61,14 @@ function codeAreaOf(sourceRefOrPath) {
|
|
|
61
61
|
return parentDir(parts);
|
|
62
62
|
}
|
|
63
63
|
if (language === "python") {
|
|
64
|
-
if (["src", "app"
|
|
64
|
+
if (["src", "app"].includes(parts[0]) && parts.length > 2)
|
|
65
65
|
return parts.slice(1, 3).join("/");
|
|
66
66
|
return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
|
|
67
67
|
}
|
|
68
68
|
if (language === "go") {
|
|
69
|
-
if (parts[0] === "server" && parts[1] === "channels" && parts[2])
|
|
70
|
-
return `server/${parts[2]}`;
|
|
71
69
|
return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
|
|
72
70
|
}
|
|
73
71
|
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
72
|
return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
|
|
79
73
|
}
|
|
80
74
|
return parts.length > 2 ? parts.slice(0, 2).join("/") : parentDir(parts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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",
|