@orangepro/orangepro-mcp 0.2.7 → 0.2.9
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 +125 -34
- package/dist/local/analyze/behaviorContracts.js +125 -75
- package/dist/local/analyze/confirm.js +160 -0
- package/dist/local/cli.js +10 -1
- package/dist/local/enrich/markdown.js +1 -1
- package/dist/local/flows/flowWalker.js +6 -9
- package/dist/local/generate/generator.js +30 -12
- package/dist/local/generate/promptV5.js +2 -0
- package/dist/local/score/risk.js +23 -8
- package/dist/local/viz/behaviorReportData.js +92 -29
- package/dist/local/viz/behaviorReportHtml.js +2 -1
- package/dist/local/viz/payload.js +1 -7
- package/package.json +1 -1
|
@@ -47,7 +47,7 @@ function extractionBackend(language) {
|
|
|
47
47
|
return "tsc"; // TS/JS compiler path (or unextracted)
|
|
48
48
|
return treeSitterReady(language) ? "ts" : "rx"; // tree-sitter AST vs regex fallback
|
|
49
49
|
}
|
|
50
|
-
import { runConfirmer } from "./confirm.js";
|
|
50
|
+
import { repoRootOf, resolveSpecifier, workspacePackages, runConfirmer } from "./confirm.js";
|
|
51
51
|
const DETECTOR = "repo_analyzer";
|
|
52
52
|
// Global ceiling on extracted code symbols. A SINGLE counter shared across the walk,
|
|
53
53
|
// so a low value lets whichever language is walked first (e.g. a Go `server/`) eat the
|
|
@@ -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,
|
|
@@ -1921,6 +1972,46 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1921
1972
|
seenPair.add(key);
|
|
1922
1973
|
candidates.push({ testRel, testAbs, implRel, implAbs });
|
|
1923
1974
|
}
|
|
1975
|
+
// Import-derived pairing: a test file is a candidate for every in-repo
|
|
1976
|
+
// impl file it imports — an import IS the relationship; resolution
|
|
1977
|
+
// (relative, tsconfig paths, npm/pnpm workspace names) decides
|
|
1978
|
+
// membership. Adds PAIRS only; the assertion-aware confirmer remains
|
|
1979
|
+
// the sole judge.
|
|
1980
|
+
{
|
|
1981
|
+
const pairRoot = repoRootOf(root);
|
|
1982
|
+
const wsPkgs = workspacePackages(pairRoot);
|
|
1983
|
+
const relByAbs = new Map(resolveFiles.map((f) => [f.abs, f.rel]));
|
|
1984
|
+
const IMPORT_SPEC_RE = /(?:import|export)[^'"\n]*from\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1985
|
+
for (const tf of resolveFiles) {
|
|
1986
|
+
if (tf.role !== "test")
|
|
1987
|
+
continue;
|
|
1988
|
+
let srcTxt = "";
|
|
1989
|
+
try {
|
|
1990
|
+
srcTxt = readFileSync(tf.abs, "utf8");
|
|
1991
|
+
}
|
|
1992
|
+
catch {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
let m;
|
|
1996
|
+
IMPORT_SPEC_RE.lastIndex = 0;
|
|
1997
|
+
while ((m = IMPORT_SPEC_RE.exec(srcTxt)) !== null) {
|
|
1998
|
+
const spec = m[1] ?? m[2];
|
|
1999
|
+
if (!spec)
|
|
2000
|
+
continue;
|
|
2001
|
+
const resolvedAbs = resolveSpecifier(spec, tf.abs, pairRoot, wsPkgs);
|
|
2002
|
+
if (!resolvedAbs)
|
|
2003
|
+
continue;
|
|
2004
|
+
const implRel = relByAbs.get(resolvedAbs);
|
|
2005
|
+
if (!implRel || !eligibleSymbolsByFile.has(implRel))
|
|
2006
|
+
continue;
|
|
2007
|
+
const key = `${tf.rel}|${implRel}`;
|
|
2008
|
+
if (seenPair.has(key))
|
|
2009
|
+
continue;
|
|
2010
|
+
seenPair.add(key);
|
|
2011
|
+
candidates.push({ testRel: tf.rel, testAbs: tf.abs, implRel, implAbs: resolvedAbs });
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
1924
2015
|
const confirmBudget = Math.max(1, Number(process.env.ORANGEPRO_MAX_CONFIRM_FILES) || 1500);
|
|
1925
2016
|
const riskSymbolLimit = Math.max(1, Number(process.env.ORANGEPRO_CONFIRM_RISK_SYMBOLS) || DEFAULT_CONFIRM_RISK_SYMBOLS);
|
|
1926
2017
|
const involved = new Set();
|
|
@@ -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 = [];
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// is the SOLE producer of hard TESTED_BY/COVERS edges.
|
|
23
23
|
import ts from "typescript";
|
|
24
24
|
import path from "node:path";
|
|
25
|
+
import { readFileSync as fsRead, existsSync as fsExists } from "node:fs";
|
|
25
26
|
import { loadTsConfigFor, resolveImport } from "../resolve/resolver.js";
|
|
26
27
|
import { walkBarrel } from "../resolve/barrelWalker.js";
|
|
27
28
|
import { isSelfAssertingCallee } from "./selfAssert.js";
|
|
@@ -33,10 +34,45 @@ const norm = (p) => path.resolve(p);
|
|
|
33
34
|
* the target repo. JSX is preserved and emit/lib-checks are off — we only read
|
|
34
35
|
* symbols, never compile.
|
|
35
36
|
*/
|
|
37
|
+
export function repoRootOf(anchor) {
|
|
38
|
+
let dir = anchor;
|
|
39
|
+
for (let i = 0; i < 15; i++) {
|
|
40
|
+
try {
|
|
41
|
+
const pj = JSON.parse(fsRead(path.join(dir, "package.json"), "utf8"));
|
|
42
|
+
if (pj && pj.workspaces)
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
catch { /* not here; walk up */ }
|
|
46
|
+
if (fsExists(path.join(dir, "pnpm-workspace.yaml")))
|
|
47
|
+
return dir;
|
|
48
|
+
if (fsExists(path.join(dir, ".git")) || fsExists(path.join(dir, "go.mod")))
|
|
49
|
+
return dir;
|
|
50
|
+
const parent = path.dirname(dir);
|
|
51
|
+
if (parent === dir)
|
|
52
|
+
return dir;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
36
57
|
export function buildConfirmProgram(absFiles, anchorFile) {
|
|
37
58
|
const base = loadTsConfigFor(anchorFile).options;
|
|
59
|
+
// Workspace-aware resolution: synthesize compilerOptions.paths from the
|
|
60
|
+
// repo's workspaces manifest so the STANDARD compiler resolves
|
|
61
|
+
// "@scope/pkg" and "@scope/pkg/lib/x" to source. Adds no confirmation
|
|
62
|
+
// path — only lets the existing assertion-aware bar evaluate tests it
|
|
63
|
+
// previously could not resolve at all.
|
|
64
|
+
const wsRoot = repoRootOf(anchorFile);
|
|
65
|
+
const wsPaths = {};
|
|
66
|
+
for (const [name, dir] of workspacePackages(wsRoot)) {
|
|
67
|
+
wsPaths[name] = [dir + "/src/index", dir + "/index", dir + "/src"];
|
|
68
|
+
wsPaths[name + "/lib/*"] = [dir + "/src/*"];
|
|
69
|
+
wsPaths[name + "/dist/*"] = [dir + "/src/*"];
|
|
70
|
+
wsPaths[name + "/*"] = [dir + "/src/*", dir + "/*"];
|
|
71
|
+
}
|
|
38
72
|
const options = {
|
|
39
73
|
...base,
|
|
74
|
+
baseUrl: base.baseUrl ?? wsRoot,
|
|
75
|
+
paths: { ...wsPaths, ...base.paths },
|
|
40
76
|
noEmit: true,
|
|
41
77
|
allowJs: true,
|
|
42
78
|
checkJs: false,
|
|
@@ -2376,6 +2412,130 @@ export function confirmPair(ctx, testAbsPath, implAbsPath, behaviorName) {
|
|
|
2376
2412
|
reason: "behavior not evaluated"
|
|
2377
2413
|
});
|
|
2378
2414
|
}
|
|
2415
|
+
// Workspace/alias resolution is useful for candidate linkage in TS monorepos,
|
|
2416
|
+
// but resolution and a call reference are not assertion evidence. Keep this
|
|
2417
|
+
// resolver separate from the hard-proof confirmer below.
|
|
2418
|
+
import { readFileSync as _rf, existsSync as _ex, readdirSync as _rd } from "node:fs";
|
|
2419
|
+
import { dirname as _dn, join as _jn, resolve as _rs } from "node:path";
|
|
2420
|
+
const EXT_TRIES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
2421
|
+
const BUILT_DIRS = /\/(lib|dist|build|out)(\/|$)/;
|
|
2422
|
+
function tryFile(base) {
|
|
2423
|
+
for (const e of EXT_TRIES)
|
|
2424
|
+
if (_ex(base + e))
|
|
2425
|
+
return base + e;
|
|
2426
|
+
return null;
|
|
2427
|
+
}
|
|
2428
|
+
function readJsonSafe(p) {
|
|
2429
|
+
try {
|
|
2430
|
+
return JSON.parse(_rf(p, "utf8"));
|
|
2431
|
+
}
|
|
2432
|
+
catch {
|
|
2433
|
+
return null;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
/** Nearest tsconfig paths/baseUrl for a file, walking extends chains (depth-capped). */
|
|
2437
|
+
function tsconfigPathsFor(fileAbs, stopDir) {
|
|
2438
|
+
let dir = _dn(fileAbs);
|
|
2439
|
+
for (let i = 0; i < 12 && dir.startsWith(stopDir); i++) {
|
|
2440
|
+
const tc = _jn(dir, "tsconfig.json");
|
|
2441
|
+
if (_ex(tc)) {
|
|
2442
|
+
let merged = {};
|
|
2443
|
+
let cur = tc;
|
|
2444
|
+
for (let d = 0; d < 6 && cur; d++) {
|
|
2445
|
+
const j = readJsonSafe(cur);
|
|
2446
|
+
if (!j)
|
|
2447
|
+
break;
|
|
2448
|
+
const co = (j.compilerOptions ?? {});
|
|
2449
|
+
merged = { baseUrl: merged.baseUrl ?? (co.baseUrl ? _rs(_dn(cur), co.baseUrl) : undefined), paths: { ...co.paths, ...merged.paths } };
|
|
2450
|
+
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;
|
|
2451
|
+
}
|
|
2452
|
+
return { baseUrl: merged.baseUrl ?? _dn(tc), paths: merged.paths ?? {} };
|
|
2453
|
+
}
|
|
2454
|
+
const parent = _dn(dir);
|
|
2455
|
+
if (parent === dir)
|
|
2456
|
+
break;
|
|
2457
|
+
dir = parent;
|
|
2458
|
+
}
|
|
2459
|
+
return null;
|
|
2460
|
+
}
|
|
2461
|
+
/** Workspace member name → package dir, from the root package.json workspaces globs. */
|
|
2462
|
+
export function workspacePackages(repoRoot) {
|
|
2463
|
+
const pnpmGlobs = [];
|
|
2464
|
+
try {
|
|
2465
|
+
const y = fsRead(path.join(repoRoot, "pnpm-workspace.yaml"), "utf8");
|
|
2466
|
+
for (const line of y.split("\n")) {
|
|
2467
|
+
const m = /^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/.exec(line);
|
|
2468
|
+
if (m)
|
|
2469
|
+
pnpmGlobs.push(m[1].trim());
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
catch { /* not a pnpm workspace */ }
|
|
2473
|
+
const out = new Map();
|
|
2474
|
+
const rootPkg = readJsonSafe(_jn(repoRoot, "package.json"));
|
|
2475
|
+
const ws = rootPkg?.workspaces;
|
|
2476
|
+
const npmGlobs = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
2477
|
+
const globs = [...npmGlobs, ...pnpmGlobs];
|
|
2478
|
+
const dirs = [];
|
|
2479
|
+
for (const g of globs) {
|
|
2480
|
+
if (g.endsWith("/*")) {
|
|
2481
|
+
const base = _jn(repoRoot, g.slice(0, -2));
|
|
2482
|
+
try {
|
|
2483
|
+
for (const d of _rd(base))
|
|
2484
|
+
dirs.push(_jn(base, d));
|
|
2485
|
+
}
|
|
2486
|
+
catch { /* absent */ }
|
|
2487
|
+
}
|
|
2488
|
+
else
|
|
2489
|
+
dirs.push(_jn(repoRoot, g));
|
|
2490
|
+
}
|
|
2491
|
+
for (const d of dirs) {
|
|
2492
|
+
const pj = readJsonSafe(_jn(d, "package.json"));
|
|
2493
|
+
const name = pj?.name;
|
|
2494
|
+
if (typeof name === "string")
|
|
2495
|
+
out.set(name, d);
|
|
2496
|
+
}
|
|
2497
|
+
return out;
|
|
2498
|
+
}
|
|
2499
|
+
/** Resolve one import specifier from a test file to an in-repo file, or null. */
|
|
2500
|
+
export function resolveSpecifier(spec, fromFile, repoRoot, wsPkgs) {
|
|
2501
|
+
if (spec.startsWith("."))
|
|
2502
|
+
return tryFile(_rs(_dn(fromFile), spec));
|
|
2503
|
+
const tc = tsconfigPathsFor(fromFile, repoRoot);
|
|
2504
|
+
if (tc) {
|
|
2505
|
+
for (const [pat, targets] of Object.entries(tc.paths)) {
|
|
2506
|
+
const star = pat.indexOf("*");
|
|
2507
|
+
const matches = star >= 0 ? spec.startsWith(pat.slice(0, star)) && spec.endsWith(pat.slice(star + 1)) : spec === pat;
|
|
2508
|
+
if (!matches)
|
|
2509
|
+
continue;
|
|
2510
|
+
const wild = star >= 0 ? spec.slice(pat.slice(0, star).length, spec.length - pat.slice(star + 1).length) : "";
|
|
2511
|
+
for (const t of targets) {
|
|
2512
|
+
const hit = tryFile(_rs(tc.baseUrl, t.replace("*", wild)));
|
|
2513
|
+
if (hit)
|
|
2514
|
+
return hit;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (!spec.startsWith("@") && !spec.includes(":")) {
|
|
2518
|
+
const viaBase = tryFile(_rs(tc.baseUrl, spec));
|
|
2519
|
+
if (viaBase)
|
|
2520
|
+
return viaBase;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
// workspace package name (longest-prefix match), with built→src subpath mapping
|
|
2524
|
+
for (const [name, dir] of wsPkgs) {
|
|
2525
|
+
if (spec !== name && !spec.startsWith(name + "/"))
|
|
2526
|
+
continue;
|
|
2527
|
+
const sub = spec === name ? "" : spec.slice(name.length + 1);
|
|
2528
|
+
const candidates = sub
|
|
2529
|
+
? [sub, sub.replace(BUILT_DIRS, "/src/"), "src/" + sub.replace(/^(lib|dist|build|out)\//, "")]
|
|
2530
|
+
: ["src/index", "index"];
|
|
2531
|
+
for (const c of candidates) {
|
|
2532
|
+
const hit = tryFile(_jn(dir, c));
|
|
2533
|
+
if (hit)
|
|
2534
|
+
return hit;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2379
2539
|
/**
|
|
2380
2540
|
* Run the confirmer over every candidate (test -> impl) pair (the resolver-derived
|
|
2381
2541
|
* MAY_RELATE_TO links). For each denominator-eligible exported symbol of the impl
|
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",
|
|
@@ -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 = [];
|
|
@@ -159,6 +159,8 @@ export function buildBatchGenerationSystemPromptV5() {
|
|
|
159
159
|
"- Never mock, stub, or spy on the behavior-under-test itself. The subject must execute for real. Mock only true external I/O boundaries — network calls, the system clock, third-party SDKs, outbound HTTP. If the behavior calls internal services in the same codebase, let them run (or use real test doubles at the I/O edge, never at the subject). A test that mocks the subject proves nothing and will be rejected.",
|
|
160
160
|
"- Each test is complete and runnable (all imports, setup, assertions, cleanup).",
|
|
161
161
|
"- Start each test with: // Concern: <concern> | Technique: <technique>",
|
|
162
|
+
"- When asserting an exact return value (string, number, constant), copy the expected value VERBATIM from the provided source code. Never invent an expected value.",
|
|
163
|
+
"- If the exact value is not visible in the provided source, assert structure instead (non-nil, error vs no-error, type, boolean outcome) — never a guessed literal.",
|
|
162
164
|
"- Assert all targets listed in each scenario.",
|
|
163
165
|
"- Do not copy source excerpts verbatim. Use them to understand, then write original code.",
|
|
164
166
|
"- Reuse SUBJECT IMPORTS. Do not invent module paths.",
|
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))
|
|
@@ -139,8 +139,8 @@ function deriveRouteWeight(node) {
|
|
|
139
139
|
function deriveDataSensitivity(node) {
|
|
140
140
|
const text = `${node.external_id} ${symbolFile(node)} ${symbolTitle(node)}`.toLowerCase();
|
|
141
141
|
const tiers = [
|
|
142
|
-
[/payment|stripe|
|
|
143
|
-
[/auth|token|session|password|credential|jwt|oauth/, 9],
|
|
142
|
+
[/payment|stripe|refund|charge(?!r)|billing|payout|chargeback/, 10],
|
|
143
|
+
[/auth(?!or\b)|token(?!iz)|session|password|credential|jwt|oauth/, 9],
|
|
144
144
|
[/order|cart|checkout|invoice|transaction/, 7],
|
|
145
145
|
[/customer|user|account|profile|pii|gdpr/, 6],
|
|
146
146
|
[/notification|email|sms|webhook|push/, 3]
|
|
@@ -389,7 +389,10 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
389
389
|
const i = Math.round(iExact);
|
|
390
390
|
const d = rawScores[idx].d;
|
|
391
391
|
const detectionTier = detectionFor(s.external_id);
|
|
392
|
-
|
|
392
|
+
let score = Math.round(pExact * iExact * d * 10) / 10;
|
|
393
|
+
const disconnected = (incoming.get(s.external_id) ?? 0) === 0 && fan_out === 0;
|
|
394
|
+
if (disconnected)
|
|
395
|
+
score = Math.round(score * 0.25 * 10) / 10;
|
|
393
396
|
const reasons = [
|
|
394
397
|
`ORS ${score} ≈ P${p} × I${i} × D${d}`,
|
|
395
398
|
`${incoming_refs} incoming structural reference${incoming_refs === 1 ? "" : "s"} (method-attributed)`,
|
|
@@ -400,6 +403,8 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
400
403
|
reasons.push("near an API/route/handler entry point");
|
|
401
404
|
if (is_new_code)
|
|
402
405
|
reasons.push("new code (< 30 days)");
|
|
406
|
+
if (disconnected)
|
|
407
|
+
reasons.push("no callers and no callees — structurally disconnected, score dampened");
|
|
403
408
|
if (detectionTier === "candidate")
|
|
404
409
|
reasons.push("lexical candidate test match only — unconfirmed");
|
|
405
410
|
return {
|
|
@@ -424,15 +429,25 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
424
429
|
};
|
|
425
430
|
})
|
|
426
431
|
.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
|
-
|
|
432
|
+
// The default API is the true global ranking because reports call this list
|
|
433
|
+
// "top risks". Callers may explicitly request a diversified portfolio, but
|
|
434
|
+
// that presentation policy must never silently redefine rank.
|
|
435
|
+
if (opts.maxPerFile === undefined)
|
|
436
|
+
return ranked.slice(0, limit);
|
|
437
|
+
const maxPerFile = Math.max(1, opts.maxPerFile);
|
|
430
438
|
const perFile = new Map();
|
|
439
|
+
// Multi-program repos flood identical titles (76 x main) across files; the
|
|
440
|
+
// per-FILE cap cannot see it. Same diversity principle, second axis.
|
|
441
|
+
const maxPerTitle = 2;
|
|
442
|
+
const perTitle = new Map();
|
|
431
443
|
const surfaced = [];
|
|
432
444
|
const overflow = [];
|
|
433
445
|
for (const gap of ranked) {
|
|
434
446
|
const used = perFile.get(gap.file) ?? 0;
|
|
435
|
-
|
|
447
|
+
const tKey = (gap.title || "").split("(")[0].trim();
|
|
448
|
+
const tUsed = perTitle.get(tKey) ?? 0;
|
|
449
|
+
if (used < maxPerFile && tUsed < maxPerTitle) {
|
|
450
|
+
perTitle.set(tKey, tUsed + 1);
|
|
436
451
|
perFile.set(gap.file, used + 1);
|
|
437
452
|
surfaced.push(gap);
|
|
438
453
|
}
|
|
@@ -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,
|
|
@@ -406,6 +409,12 @@ function fmtRefs(n) {
|
|
|
406
409
|
return "<1";
|
|
407
410
|
return String(Math.round(n));
|
|
408
411
|
}
|
|
412
|
+
function displayTitle(title, file) {
|
|
413
|
+
if (title.includes("."))
|
|
414
|
+
return title;
|
|
415
|
+
const pkg = file && file.includes("/") ? file.split("/").slice(-2, -1)[0] : "";
|
|
416
|
+
return pkg ? `${pkg}.${title}` : title;
|
|
417
|
+
}
|
|
409
418
|
/** Deterministic 1–2 line behavior context from graph facts only — no LLM.
|
|
410
419
|
* Sensitivity label mirrors deriveDataSensitivity's tiers. */
|
|
411
420
|
function riskContext(risk) {
|
|
@@ -455,17 +464,37 @@ export function computeReportDelta(prev, cur) {
|
|
|
455
464
|
export function reportBaselineOf(cur, ts) {
|
|
456
465
|
return { ts, summary: cur.summary, riskPaths: cur.risks.map((r) => r.path), generatedTotal: cur.generatedTotal };
|
|
457
466
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
const HTTP_TRIGGER_VERBS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"]);
|
|
468
|
+
const GRAPHQL_TRIGGER_VERBS = new Set(["MUTATION", "QUERY", "SUBSCRIPTION"]);
|
|
469
|
+
/** Map every trigger into a stable lane. Known protocol families get friendly
|
|
470
|
+
* labels; unknown framework adapters still receive a deterministic lane rather
|
|
471
|
+
* than being silently deleted from the system map. */
|
|
472
|
+
export function laneForTrigger(trigger) {
|
|
473
|
+
const verb = (trigger.verb || "OTHER").trim().toUpperCase();
|
|
474
|
+
const triggerPath = (trigger.path || "").toLowerCase();
|
|
475
|
+
if (HTTP_TRIGGER_VERBS.has(verb) || triggerPath.startsWith("http:"))
|
|
476
|
+
return { id: "http", label: "HTTP", priority: 20 };
|
|
477
|
+
if (GRAPHQL_TRIGGER_VERBS.has(verb) || triggerPath.startsWith("graphql:"))
|
|
478
|
+
return { id: "graphql", label: "GraphQL", priority: 10 };
|
|
479
|
+
if (["JOB", "QUEUE", "PROCESS", "WORKER"].includes(verb) || triggerPath.startsWith("queue:"))
|
|
480
|
+
return { id: "job", label: "Jobs", priority: 30 };
|
|
481
|
+
if (["SCHEDULE", "CRON", "TIMER"].includes(verb) || triggerPath.startsWith("schedule:") || triggerPath.startsWith("cron:"))
|
|
482
|
+
return { id: "schedule", label: "Scheduled", priority: 40 };
|
|
483
|
+
if (["EVENT", "MESSAGE", "CONSUME", "CONSUMER", "SUBSCRIBE"].includes(verb) || triggerPath.startsWith("event:"))
|
|
484
|
+
return { id: "event", label: "Events", priority: 50 };
|
|
485
|
+
if (["COMMAND", "CLI"].includes(verb) || triggerPath.startsWith("cli:"))
|
|
486
|
+
return { id: "cli", label: "CLI", priority: 60 };
|
|
487
|
+
if (["RPC", "GRPC"].includes(verb) || triggerPath.startsWith("rpc:") || triggerPath.startsWith("grpc:"))
|
|
488
|
+
return { id: "rpc", label: "RPC", priority: 70 };
|
|
489
|
+
const id = verb.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "other";
|
|
490
|
+
const label = verb
|
|
491
|
+
.toLowerCase()
|
|
492
|
+
.split(/[^a-z0-9]+/)
|
|
493
|
+
.filter(Boolean)
|
|
494
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
495
|
+
.join(" ") || "Other";
|
|
496
|
+
return { id, label, priority: 100 };
|
|
497
|
+
}
|
|
469
498
|
function ownerOfSig(sig) {
|
|
470
499
|
const base = sig.includes("#") ? sig.slice(sig.indexOf("#") + 1) : sig;
|
|
471
500
|
return base.includes(".") ? base.slice(0, base.indexOf(".")) : base;
|
|
@@ -477,7 +506,22 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
477
506
|
// on a full-scale repo. A flow's representative service is its most
|
|
478
507
|
// DISTINCTIVE owner: lowest global frequency, deepest step on ties, with
|
|
479
508
|
// near-ubiquitous owners eligible only when a flow touches nothing else.
|
|
480
|
-
const
|
|
509
|
+
const laneForFlow = (flow) => {
|
|
510
|
+
if (flow.trigger)
|
|
511
|
+
return laneForTrigger(flow.trigger);
|
|
512
|
+
// A program root without a framework trigger is still a real product entry
|
|
513
|
+
// point (CLI/main). Internal call-graph roots remain excluded.
|
|
514
|
+
return flow.root_entry ? { id: "entry", label: "Entry points", priority: 80 } : null;
|
|
515
|
+
};
|
|
516
|
+
const triggerFlows = data.flows.filter((flow) => laneForFlow(flow) !== null);
|
|
517
|
+
const laneMeta = new Map();
|
|
518
|
+
for (const flow of triggerFlows) {
|
|
519
|
+
const lane = laneForFlow(flow);
|
|
520
|
+
laneMeta.set(lane.id, lane);
|
|
521
|
+
}
|
|
522
|
+
const laneOrder = [...laneMeta.values()]
|
|
523
|
+
.sort((a, b) => a.priority - b.priority || a.label.localeCompare(b.label) || a.id.localeCompare(b.id))
|
|
524
|
+
.map((lane) => lane.id);
|
|
481
525
|
const ownerFreq = new Map();
|
|
482
526
|
for (const f of triggerFlows) {
|
|
483
527
|
const seen = new Set();
|
|
@@ -496,7 +540,7 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
496
540
|
const edgeFlows = new Map();
|
|
497
541
|
const svcLaneFlows = new Map();
|
|
498
542
|
for (const f of triggerFlows) {
|
|
499
|
-
const lane =
|
|
543
|
+
const lane = laneForFlow(f);
|
|
500
544
|
const steps = f.steps ?? [];
|
|
501
545
|
let svc = "";
|
|
502
546
|
let bestFreq = Infinity;
|
|
@@ -538,18 +582,18 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
538
582
|
best = l;
|
|
539
583
|
bestN = n;
|
|
540
584
|
}
|
|
541
|
-
return
|
|
585
|
+
return laneOrder.indexOf(best);
|
|
542
586
|
};
|
|
543
587
|
const byTraffic = [...svcFlows.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
544
588
|
const chosen = new Set();
|
|
545
589
|
// 1) Every lane is guaranteed its top 2 services — a lane with 56 flows must
|
|
546
590
|
// never render edge-less just because its traffic is spread thin.
|
|
547
|
-
for (const laneId of
|
|
591
|
+
for (const laneId of laneOrder) {
|
|
548
592
|
let taken = 0;
|
|
549
593
|
for (const [svc] of byTraffic) {
|
|
550
594
|
if (taken >= 2)
|
|
551
595
|
break;
|
|
552
|
-
if (laneRank(svc) ===
|
|
596
|
+
if (laneRank(svc) === laneOrder.indexOf(laneId) && !chosen.has(svc)) {
|
|
553
597
|
chosen.add(svc);
|
|
554
598
|
taken++;
|
|
555
599
|
}
|
|
@@ -606,7 +650,6 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
606
650
|
e.critical = true;
|
|
607
651
|
riskBySvc.set(svc, e);
|
|
608
652
|
}
|
|
609
|
-
const laneOrder = ["graphql", "http", "job"];
|
|
610
653
|
// Conservation: edges drawn per lane must sum to the lane's stated count.
|
|
611
654
|
// Everything below the cut aggregates into one dashed "+N more services"
|
|
612
655
|
// node per lane — 51 job flows must never silently vanish.
|
|
@@ -644,7 +687,7 @@ export function buildSystemMapModel(data, maxServices = 12) {
|
|
|
644
687
|
return {
|
|
645
688
|
lanes: laneOrder
|
|
646
689
|
.filter((id) => laneFlows.has(id))
|
|
647
|
-
.map((id) => ({ id, label: id
|
|
690
|
+
.map((id) => ({ id, label: laneMeta.get(id)?.label ?? id, flows: laneFlows.get(id) ?? 0 })),
|
|
648
691
|
services: [
|
|
649
692
|
...top.map(([label, flows]) => ({
|
|
650
693
|
id: label,
|
|
@@ -703,13 +746,16 @@ function riskTodo(risk, verb, path, generatedTests) {
|
|
|
703
746
|
const call = verb !== "BEHAVIOR"
|
|
704
747
|
? `issues ${verb} ${path}`
|
|
705
748
|
: risk.entry_point
|
|
706
|
-
? `invokes ${risk.title} through its entry point`
|
|
707
|
-
: `calls ${risk.title} directly`;
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
:
|
|
711
|
-
|
|
712
|
-
: "
|
|
749
|
+
? `invokes ${displayTitle(risk.title, risk.file)} through its entry point`
|
|
750
|
+
: `calls ${displayTitle(risk.title, risk.file)} directly`;
|
|
751
|
+
const s = risk.data_sensitivity ?? 1;
|
|
752
|
+
const sens = s >= 10
|
|
753
|
+
? " Include a failure case: a rejected transaction must leave no partial state."
|
|
754
|
+
: s >= 9
|
|
755
|
+
? " Include a negative case: invalid or expired credentials must fail closed."
|
|
756
|
+
: s >= 7
|
|
757
|
+
? " Include a failure case: a rejected transaction must leave no partial state."
|
|
758
|
+
: "";
|
|
713
759
|
if (risk.integration_signal === "candidate") {
|
|
714
760
|
return `A similarly named test exists but nothing links it. Write a test that imports and ${call}, asserting the observable outcome — that upgrades this from unconfirmed candidate to a hard link.${sens}`;
|
|
715
761
|
}
|
|
@@ -722,6 +768,23 @@ function riskRows(risks, graph) {
|
|
|
722
768
|
for (const r of risks)
|
|
723
769
|
if (!firstRowForFile.has(r.file))
|
|
724
770
|
firstRowForFile.set(r.file, r.id);
|
|
771
|
+
// Ambiguity-qualified display: identical titles from DIFFERENT files
|
|
772
|
+
// (multi-program repos: 76 x main) get their top-level dir as a prefix.
|
|
773
|
+
// Purely display; single-file titles render unchanged everywhere else.
|
|
774
|
+
const titleFiles = new Map();
|
|
775
|
+
for (const r of risks) {
|
|
776
|
+
const t = (r.title || "").split("(")[0].trim();
|
|
777
|
+
if (!titleFiles.has(t))
|
|
778
|
+
titleFiles.set(t, new Set());
|
|
779
|
+
titleFiles.get(t).add(r.file);
|
|
780
|
+
}
|
|
781
|
+
const qualify = (risk, path) => {
|
|
782
|
+
const t = (risk.title || "").split("(")[0].trim();
|
|
783
|
+
if ((titleFiles.get(t)?.size ?? 0) <= 1)
|
|
784
|
+
return path;
|
|
785
|
+
const top = (risk.file || "").split("/").filter(Boolean)[0] ?? "";
|
|
786
|
+
return top ? `${top}: ${path}` : path;
|
|
787
|
+
};
|
|
725
788
|
return risks.map((risk, idx) => {
|
|
726
789
|
const methodMatch = risk.title.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i);
|
|
727
790
|
const tags = [];
|
|
@@ -736,7 +799,7 @@ function riskRows(risks, graph) {
|
|
|
736
799
|
...(() => {
|
|
737
800
|
const generatedTests = riskGeneratedTests(graph, risk, riskIds, firstRowForFile.get(risk.file) === risk.id);
|
|
738
801
|
const verb = methodMatch?.[1]?.toUpperCase() ?? "BEHAVIOR";
|
|
739
|
-
const path = methodMatch?.[2] ?? risk.title;
|
|
802
|
+
const path = qualify(risk, methodMatch?.[2] ?? displayTitle(risk.title, risk.file));
|
|
740
803
|
const generatedCategories = [...new Set([
|
|
741
804
|
...generatedTests.map((t) => (t.bucket ? BUCKET_TO_CONCERN[t.bucket] : undefined)),
|
|
742
805
|
// An integration/api/e2e-layer draft targets integration_flow. This
|
|
@@ -770,7 +833,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
770
833
|
const flowIds = flowSymbolIds(graph);
|
|
771
834
|
const summary = summaryFromRows(rows, flowIds);
|
|
772
835
|
const repoRoot = opts.repoRoot ?? graph.workspace.root;
|
|
773
|
-
const riskGaps = rankRiskGaps(graph, { repoRoot, limit: opts.riskLimit ?? 20 });
|
|
836
|
+
const riskGaps = rankRiskGaps(graph, { repoRoot, limit: opts.riskLimit ?? 20, maxPerFile: 3 });
|
|
774
837
|
const lists = behaviorLists(rows, flowIds);
|
|
775
838
|
const risks = riskRows(riskGaps, graph);
|
|
776
839
|
const sortedBehaviors = [...lists.behaviors].sort((a, b) => tierRank(a) - tierRank(b));
|
|
@@ -381,7 +381,8 @@ const D=window.DATA,$=(s)=>document.querySelector(s);
|
|
|
381
381
|
var laneY={},svcY={};
|
|
382
382
|
M.lanes.forEach(function(l,i){laneY[l.id]=padT+i*rowH+(H-2*padT-M.lanes.length*rowH)/2+rowH/2;});
|
|
383
383
|
M.services.forEach(function(sv,i){svcY[sv.id]=padT+i*rowH+rowH/2;});
|
|
384
|
-
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];});
|
|
385
386
|
var tierColor=function(t){var tot=t.proven+t.assoc+t.candidate+t.none||1;
|
|
386
387
|
if(t.proven/tot>=0.5)return "var(--green)";
|
|
387
388
|
if((t.proven+t.assoc)/tot>=0.5)return "var(--blue)";
|
|
@@ -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.9",
|
|
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",
|