@orangepro/orangepro-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/local/agentWorkflow.js +81 -0
- package/dist/local/aiGraph/links.js +635 -0
- package/dist/local/analyze/analyzer.js +2129 -0
- package/dist/local/analyze/behaviorContracts.js +169 -0
- package/dist/local/analyze/boilerplate.js +42 -0
- package/dist/local/analyze/callGraph.js +458 -0
- package/dist/local/analyze/classify.js +219 -0
- package/dist/local/analyze/clustering.js +357 -0
- package/dist/local/analyze/confirm.js +2422 -0
- package/dist/local/analyze/coverage.js +518 -0
- package/dist/local/analyze/coverageArtifacts.js +607 -0
- package/dist/local/analyze/frameworks.js +115 -0
- package/dist/local/analyze/linkage/conventions.js +160 -0
- package/dist/local/analyze/parseCache.js +164 -0
- package/dist/local/analyze/selfAssert.js +53 -0
- package/dist/local/analyze/symbols.js +430 -0
- package/dist/local/analyze/testLayer.js +135 -0
- package/dist/local/analyze/treeSitter/engine.js +1253 -0
- package/dist/local/analyze/treeSitter/languages.js +101 -0
- package/dist/local/autoProve.js +620 -0
- package/dist/local/cli.js +1468 -0
- package/dist/local/cliArgs.js +112 -0
- package/dist/local/corpusScope.js +162 -0
- package/dist/local/enrich/csv.js +348 -0
- package/dist/local/enrich/index.js +43 -0
- package/dist/local/enrich/markdown.js +193 -0
- package/dist/local/explain/explain.js +91 -0
- package/dist/local/exportCli.js +26 -0
- package/dist/local/flows/flowWalker.js +215 -0
- package/dist/local/flows/llmFlowDiscovery.js +567 -0
- package/dist/local/freshness/changed.js +280 -0
- package/dist/local/freshness/manifest.js +35 -0
- package/dist/local/freshness/status.js +30 -0
- package/dist/local/gaps/gaps.js +114 -0
- package/dist/local/generate/buckets.js +73 -0
- package/dist/local/generate/compareJudge.js +124 -0
- package/dist/local/generate/compareReport.js +538 -0
- package/dist/local/generate/compareScore.js +105 -0
- package/dist/local/generate/deriveImports.js +91 -0
- package/dist/local/generate/generator.js +2586 -0
- package/dist/local/generate/prompt.js +144 -0
- package/dist/local/generate/promptV5.js +438 -0
- package/dist/local/generate/providers.js +400 -0
- package/dist/local/generate/runHints.js +304 -0
- package/dist/local/graph/citations.js +73 -0
- package/dist/local/graph/confirmable.js +72 -0
- package/dist/local/graph/factories.js +210 -0
- package/dist/local/graph/ontology.js +18 -0
- package/dist/local/interactive.js +53 -0
- package/dist/local/jobs/jobStore.js +80 -0
- package/dist/local/jobs/notify.js +29 -0
- package/dist/local/jobs/runner.js +75 -0
- package/dist/local/ledger.js +117 -0
- package/dist/local/localConfig.js +112 -0
- package/dist/local/mcp.js +548 -0
- package/dist/local/operations.js +1749 -0
- package/dist/local/pack/coverageReport.js +192 -0
- package/dist/local/pack/exporter.js +195 -0
- package/dist/local/pack/schema.js +128 -0
- package/dist/local/pack/summary.js +127 -0
- package/dist/local/pack/validate.js +25 -0
- package/dist/local/proofRunnability.js +366 -0
- package/dist/local/recipe/dbSqljs.js +255 -0
- package/dist/local/reprove/paths.js +13 -0
- package/dist/local/reprove/scoped.js +136 -0
- package/dist/local/resolve/barrelWalker.js +178 -0
- package/dist/local/resolve/exportIndex.js +270 -0
- package/dist/local/resolve/importGraph.js +347 -0
- package/dist/local/resolve/resolver.js +122 -0
- package/dist/local/resolve/resolverCache.js +117 -0
- package/dist/local/rtm.js +413 -0
- package/dist/local/score/coverage.js +99 -0
- package/dist/local/score/doctor.js +67 -0
- package/dist/local/score/risk.js +362 -0
- package/dist/local/score/score.js +182 -0
- package/dist/local/types.js +1 -0
- package/dist/local/util/hash.js +16 -0
- package/dist/local/util/ids.js +16 -0
- package/dist/local/util/progress.js +8 -0
- package/dist/local/util/redact.js +39 -0
- package/dist/local/util/time.js +1 -0
- package/dist/local/util/walk.js +174 -0
- package/dist/local/viz/behaviorReportData.js +367 -0
- package/dist/local/viz/behaviorReportHtml.js +664 -0
- package/dist/local/viz/d3.bundle.js +3 -0
- package/dist/local/viz/html.js +1152 -0
- package/dist/local/viz/payload.js +525 -0
- package/dist/local/workspace.js +99 -0
- package/docs/agent-workflow.md +167 -0
- package/docs/agents/claude-code.md +43 -0
- package/docs/agents/codex.md +52 -0
- package/docs/agents/cursor.md +39 -0
- package/docs/agents/opencode.md +43 -0
- package/docs/agents/vscode.md +34 -0
- package/docs/local-proof-kit.md +269 -0
- package/package.json +92 -0
- package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
- package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
- package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
- package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
- package/scripts/spikes/failure-summary.mjs +29 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { baseName } from "./classify.js";
|
|
2
|
+
const TEST_FRAMEWORK_DEPS = {
|
|
3
|
+
ava: { layer: "unit" },
|
|
4
|
+
vitest: { layer: "unit" },
|
|
5
|
+
jest: { layer: "unit" },
|
|
6
|
+
mocha: { layer: "unit" },
|
|
7
|
+
jasmine: { layer: "unit" },
|
|
8
|
+
"@playwright/test": { layer: "e2e" },
|
|
9
|
+
playwright: { layer: "e2e" },
|
|
10
|
+
cypress: { layer: "e2e" },
|
|
11
|
+
"@testing-library/react": { layer: "component" },
|
|
12
|
+
supertest: { layer: "api" },
|
|
13
|
+
pytest: { layer: "unit" },
|
|
14
|
+
unittest: { layer: "unit" },
|
|
15
|
+
rspec: { layer: "unit" },
|
|
16
|
+
junit: { layer: "unit" }
|
|
17
|
+
};
|
|
18
|
+
const CONFIG_FRAMEWORK_HINTS = [
|
|
19
|
+
{ match: /^vitest\.config\.[tj]s$/, name: "vitest", layer: "unit" },
|
|
20
|
+
{ match: /^jest\.config\.[tj]s$/, name: "jest", layer: "unit" },
|
|
21
|
+
{ match: /^playwright\.config\.[tj]s$/, name: "playwright", layer: "e2e" },
|
|
22
|
+
{ match: /^cypress\.config\.[tj]s$/, name: "cypress", layer: "e2e" },
|
|
23
|
+
{ match: /^pytest\.ini$/, name: "pytest", layer: "unit" },
|
|
24
|
+
{ match: /^tox\.ini$/, name: "tox", layer: "unit" }
|
|
25
|
+
];
|
|
26
|
+
/** Detect a test framework from a config file's base name (no content read needed). */
|
|
27
|
+
export function frameworkFromConfig(relPath) {
|
|
28
|
+
const base = baseName(relPath);
|
|
29
|
+
for (const hint of CONFIG_FRAMEWORK_HINTS) {
|
|
30
|
+
if (hint.match.test(base)) {
|
|
31
|
+
return { name: hint.name, category: "test", test_layer: hint.layer, evidence_ref: relPath };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Parse package.json content for the project package + test frameworks in deps. */
|
|
37
|
+
export function detectFromPackageJson(relPath, content) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { pkg: null, frameworks: [] };
|
|
44
|
+
}
|
|
45
|
+
const deps = { ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) };
|
|
46
|
+
const depNames = Object.keys(deps).sort();
|
|
47
|
+
const pkg = {
|
|
48
|
+
name: parsed.name || "project",
|
|
49
|
+
ecosystem: "npm",
|
|
50
|
+
dependencies: depNames.slice(0, 100),
|
|
51
|
+
evidence_ref: relPath
|
|
52
|
+
};
|
|
53
|
+
const frameworks = [];
|
|
54
|
+
for (const dep of depNames) {
|
|
55
|
+
const hit = TEST_FRAMEWORK_DEPS[dep];
|
|
56
|
+
if (hit) {
|
|
57
|
+
frameworks.push({ name: dep.replace(/^@playwright\/test$/, "playwright"), category: "test", test_layer: hit.layer, evidence_ref: relPath });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { pkg, frameworks };
|
|
61
|
+
}
|
|
62
|
+
/** Detect test frameworks from non-npm manifests (pyproject.toml, requirements). */
|
|
63
|
+
export function detectFrameworksFromManifest(relPath, content) {
|
|
64
|
+
const base = baseName(relPath);
|
|
65
|
+
const frameworks = [];
|
|
66
|
+
const lower = content.toLowerCase();
|
|
67
|
+
const has = (re) => re.test(lower);
|
|
68
|
+
const isPython = base === "pyproject.toml" || base === "setup.cfg" || base === "tox.ini" || base.startsWith("requirements");
|
|
69
|
+
if (isPython) {
|
|
70
|
+
if (has(/\[tool\.pytest/) || has(/\bpytest\b/)) {
|
|
71
|
+
frameworks.push({ name: "pytest", category: "test", test_layer: "unit", evidence_ref: relPath });
|
|
72
|
+
}
|
|
73
|
+
if (has(/\bplaywright\b/)) {
|
|
74
|
+
frameworks.push({ name: "playwright", category: "test", test_layer: "e2e", evidence_ref: relPath });
|
|
75
|
+
}
|
|
76
|
+
if (has(/(^|\s)unittest(\s|$)/m)) {
|
|
77
|
+
frameworks.push({ name: "unittest", category: "test", test_layer: "unit", evidence_ref: relPath });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (base === "pom.xml") {
|
|
81
|
+
if (has(/\bjunit-jupiter\b/) || has(/\borg\.junit\.jupiter\b/)) {
|
|
82
|
+
frameworks.push({ name: "junit5", category: "test", test_layer: "unit", evidence_ref: relPath });
|
|
83
|
+
}
|
|
84
|
+
else if (has(/<groupid>\s*junit\s*<\/groupid>|<artifactid>\s*junit\s*<\/artifactid>/)) {
|
|
85
|
+
frameworks.push({ name: "junit4", category: "test", test_layer: "unit", evidence_ref: relPath });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return frameworks;
|
|
89
|
+
}
|
|
90
|
+
/** Detect ecosystem package manifests other than package.json (name only, cheap). */
|
|
91
|
+
export function detectManifestPackage(relPath, content) {
|
|
92
|
+
const base = baseName(relPath);
|
|
93
|
+
if (base === "pyproject.toml") {
|
|
94
|
+
const name = /name\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
|
|
95
|
+
return { name: name || "python-project", ecosystem: "pypi", dependencies: [], evidence_ref: relPath };
|
|
96
|
+
}
|
|
97
|
+
if (base === "go.mod") {
|
|
98
|
+
const name = /^module\s+(\S+)/m.exec(content)?.[1];
|
|
99
|
+
return { name: name || "go-module", ecosystem: "go", dependencies: [], evidence_ref: relPath };
|
|
100
|
+
}
|
|
101
|
+
if (base === "cargo.toml") {
|
|
102
|
+
const name = /name\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
|
|
103
|
+
return { name: name || "rust-crate", ecosystem: "cargo", dependencies: [], evidence_ref: relPath };
|
|
104
|
+
}
|
|
105
|
+
if (base === "requirements.txt") {
|
|
106
|
+
const deps = content
|
|
107
|
+
.split(/\r?\n/)
|
|
108
|
+
.map((l) => l.trim())
|
|
109
|
+
.filter((l) => l && !l.startsWith("#"))
|
|
110
|
+
.map((l) => l.split(/[=<>~!\s]/)[0])
|
|
111
|
+
.slice(0, 100);
|
|
112
|
+
return { name: "python-requirements", ecosystem: "pypi", dependencies: deps, evidence_ref: relPath };
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Per-language test<->source CONVENTION linkage (PR-2 of multi-language coverage).
|
|
2
|
+
//
|
|
3
|
+
// For non-TS/JS languages the import-graph resolver does not run, so the only
|
|
4
|
+
// test->source signal is a naming/path convention. The previous global
|
|
5
|
+
// basename-stem matcher was both too coarse (Java's `FooTest.java` stem
|
|
6
|
+
// `footest` never matched `Foo.java`) and too loose (any same-stem file in the
|
|
7
|
+
// repo). This module replaces guessing with PREDICT-AND-VERIFY: from a test
|
|
8
|
+
// file we DERIVE the exact source-sibling path its language convention implies,
|
|
9
|
+
// then confirm that file was actually scanned. A wrong derivation simply finds
|
|
10
|
+
// nothing — it can never invent a false link.
|
|
11
|
+
//
|
|
12
|
+
// These are CANDIDATE associations ("this behavior has an associated test"),
|
|
13
|
+
// never structural proof. Callers emit them as `weak` MAY_RELATE_TO edges so
|
|
14
|
+
// they are excluded from the TS/JS structural confirmer. Per the product bar,
|
|
15
|
+
// convention linkage is the "has-a-test" tier and is NEVER blended with the
|
|
16
|
+
// hard-confirmed tier.
|
|
17
|
+
// Languages with a strong, unambiguous test convention. For these, a file that
|
|
18
|
+
// does NOT match the convention is a test-tree helper/shadow, not a behavior
|
|
19
|
+
// test — callers must under-link rather than fall back to the coarse global
|
|
20
|
+
// basename-stem matcher (which resurrects cross-file false links). Keep in sync
|
|
21
|
+
// with the conventionSibling() switch.
|
|
22
|
+
const CONVENTION_LANGUAGES = new Set(["go", "java", "kotlin", "python"]);
|
|
23
|
+
/** True when conventionSibling() authoritatively decides linkage for `language`. */
|
|
24
|
+
export function isConventionLanguage(language) {
|
|
25
|
+
return CONVENTION_LANGUAGES.has(language);
|
|
26
|
+
}
|
|
27
|
+
const baseOf = (relPath) => relPath.split("/").pop() || relPath;
|
|
28
|
+
const dirOf = (relPath) => {
|
|
29
|
+
const i = relPath.lastIndexOf("/");
|
|
30
|
+
return i >= 0 ? relPath.slice(0, i) : "";
|
|
31
|
+
};
|
|
32
|
+
const join = (dir, base) => (dir ? `${dir}/${base}` : base);
|
|
33
|
+
/**
|
|
34
|
+
* Source sibling implied by the test file's language convention, or null when
|
|
35
|
+
* no predicted sibling was actually scanned. `codeFiles` is the set of scanned
|
|
36
|
+
* source-file relPaths (case-preserving, ignore-rules already applied), so an
|
|
37
|
+
* existence check here respects the analyzed scope.
|
|
38
|
+
*
|
|
39
|
+
* Languages without a strong, unambiguous convention (notably TS/JS, handled by
|
|
40
|
+
* the resolver + co-located stem fallback) return null so the caller's existing
|
|
41
|
+
* logic stays authoritative.
|
|
42
|
+
*/
|
|
43
|
+
export function conventionSibling(testRel, language, codeFiles) {
|
|
44
|
+
switch (language) {
|
|
45
|
+
case "go":
|
|
46
|
+
return goSibling(testRel, codeFiles);
|
|
47
|
+
case "java":
|
|
48
|
+
case "kotlin":
|
|
49
|
+
return jvmSibling(testRel, codeFiles);
|
|
50
|
+
case "python":
|
|
51
|
+
return pythonSibling(testRel, codeFiles);
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Go: `foo_test.go` ↔ `foo.go` in the SAME package directory (language rule). ──
|
|
57
|
+
function goSibling(testRel, codeFiles) {
|
|
58
|
+
const base = baseOf(testRel);
|
|
59
|
+
const m = /^(.+)_test\.go$/.exec(base);
|
|
60
|
+
if (!m)
|
|
61
|
+
return null;
|
|
62
|
+
const predicted = join(dirOf(testRel), `${m[1]}.go`);
|
|
63
|
+
if (predicted === testRel || !codeFiles.has(predicted))
|
|
64
|
+
return null;
|
|
65
|
+
return { relPath: predicted, confidence: 0.7, reason: `Go test sibling (${base} ↔ ${m[1]}.go, same package)` };
|
|
66
|
+
}
|
|
67
|
+
// ── JVM (Java/Kotlin): `FooTest`/`FooTests`/`FooIT`/`FooITCase`/`FooTestCase` ──
|
|
68
|
+
// ↔ `Foo`, in the src/test → src/main mirror or the same directory.
|
|
69
|
+
// Case is preserved (camelCase suffix), so the strip is unambiguous.
|
|
70
|
+
function jvmSibling(testRel, codeFiles) {
|
|
71
|
+
const base = baseOf(testRel);
|
|
72
|
+
const m = /^(.+?)(Tests?|IT|ITCase|TestCase|Spec)\.(java|kt)$/.exec(base);
|
|
73
|
+
if (!m)
|
|
74
|
+
return null;
|
|
75
|
+
const stem = m[1];
|
|
76
|
+
const ext = m[3];
|
|
77
|
+
const srcBase = `${stem}.${ext}`;
|
|
78
|
+
if (srcBase === base)
|
|
79
|
+
return null;
|
|
80
|
+
// src/test/<lang>/<pkg>/FooTest.java -> src/main/<lang>/<pkg>/Foo.java
|
|
81
|
+
const mirrorDir = mirrorTestToMain(dirOf(testRel));
|
|
82
|
+
if (mirrorDir != null) {
|
|
83
|
+
const predicted = join(mirrorDir, srcBase);
|
|
84
|
+
if (predicted !== testRel && codeFiles.has(predicted)) {
|
|
85
|
+
return { relPath: predicted, confidence: 0.7, reason: `JVM test sibling (${base} ↔ ${srcBase}, src/test→src/main mirror)` };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Co-located fallback: some projects keep the test beside the class.
|
|
89
|
+
const sameDir = join(dirOf(testRel), srcBase);
|
|
90
|
+
if (sameDir !== testRel && codeFiles.has(sameDir)) {
|
|
91
|
+
return { relPath: sameDir, confidence: 0.65, reason: `JVM test sibling (${base} ↔ ${srcBase}, same directory)` };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/** Replace a `src/test/...` (or `src/integration-test/`, `src/it/`) prefix segment with `src/main/...`. */
|
|
96
|
+
function mirrorTestToMain(dir) {
|
|
97
|
+
const replaced = dir.replace(/(^|\/)src\/(test|integration-test|it|androidTest)(\/|$)/, "$1src/main$3");
|
|
98
|
+
return replaced !== dir ? replaced : null;
|
|
99
|
+
}
|
|
100
|
+
// ── Python: `test_x.py` / `x_test.py` ↔ `x.py`. ──
|
|
101
|
+
// Python test trees vary (same-dir, or `tests/<...mirror...>/test_x.py` under an
|
|
102
|
+
// unknown package root like `mealie/` or `src/`). We strip the test-root dir
|
|
103
|
+
// names, then take the LONGEST path-suffix of the remaining test directory that
|
|
104
|
+
// uniquely identifies a module file of the right basename. The suffix match
|
|
105
|
+
// absorbs the unknown source-package prefix; "unique" keeps it from guessing.
|
|
106
|
+
const PY_TEST_ROOT = /^(tests?|unit_?tests?|integration_?tests?|functional_?tests?|e2e|it|specs?)$/i;
|
|
107
|
+
function pythonSibling(testRel, codeFiles) {
|
|
108
|
+
const base = baseOf(testRel);
|
|
109
|
+
let stem = null;
|
|
110
|
+
let m = /^test_(.+)\.py$/.exec(base);
|
|
111
|
+
if (m)
|
|
112
|
+
stem = m[1];
|
|
113
|
+
if (!stem) {
|
|
114
|
+
m = /^(.+)_test\.py$/.exec(base);
|
|
115
|
+
if (m)
|
|
116
|
+
stem = m[1];
|
|
117
|
+
}
|
|
118
|
+
if (!stem)
|
|
119
|
+
return null;
|
|
120
|
+
const srcBase = `${stem}.py`;
|
|
121
|
+
const dir = dirOf(testRel);
|
|
122
|
+
// 1. Same directory (tests co-located with modules).
|
|
123
|
+
const sameDir = join(dir, srcBase);
|
|
124
|
+
if (sameDir !== testRel && codeFiles.has(sameDir)) {
|
|
125
|
+
return { relPath: sameDir, confidence: 0.65, reason: `Python test sibling (${base} ↔ ${srcBase}, same package)` };
|
|
126
|
+
}
|
|
127
|
+
// 2. Module mirror. Candidate source files sharing the basename:
|
|
128
|
+
const sameBase = [];
|
|
129
|
+
for (const f of codeFiles) {
|
|
130
|
+
if (f !== testRel && baseOf(f) === srcBase)
|
|
131
|
+
sameBase.push(f);
|
|
132
|
+
}
|
|
133
|
+
if (sameBase.length === 0)
|
|
134
|
+
return null;
|
|
135
|
+
// Drop test-root dir names, then longest-unique path-suffix match.
|
|
136
|
+
const meaningful = dir.split("/").filter((s) => s && !PY_TEST_ROOT.test(s));
|
|
137
|
+
let ambiguous = false;
|
|
138
|
+
for (let start = 0; start < meaningful.length; start++) {
|
|
139
|
+
const tail = meaningful.slice(start).join("/"); // e.g. "core/security/providers"
|
|
140
|
+
const exact = `${tail}/${srcBase}`;
|
|
141
|
+
const suffix = `/${exact}`;
|
|
142
|
+
const matches = sameBase.filter((f) => f === exact || f.endsWith(suffix));
|
|
143
|
+
if (matches.length === 1) {
|
|
144
|
+
const depth = tail.split("/").length;
|
|
145
|
+
return { relPath: matches[0], confidence: depth >= 2 ? 0.62 : 0.58, reason: `Python test sibling (${base} ↔ ${srcBase}, module mirror)` };
|
|
146
|
+
}
|
|
147
|
+
if (matches.length > 1) {
|
|
148
|
+
ambiguous = true; // ambiguous at the most specific depth — do not guess
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 3. Flat layout (`tests/test_x.py` → `src/x.py`): the `test_`/`_test` prefix
|
|
153
|
+
// explicitly names the module, so a UNIQUE `x.py` anywhere is a confident
|
|
154
|
+
// match. Convention-gated (only fires for genuinely named test files), so a
|
|
155
|
+
// non-test file like `tests/helpers.py` never reaches here.
|
|
156
|
+
if (!ambiguous && sameBase.length === 1) {
|
|
157
|
+
return { relPath: sameBase[0], confidence: 0.5, reason: `Python test sibling (${base} ↔ ${srcBase}, unique module)` };
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5.4.2 — persistent PARSE cache.
|
|
3
|
+
*
|
|
4
|
+
* Caches ONLY pure, content-derived parse outputs (`extractSymbolsWithMeta`,
|
|
5
|
+
* `extractTestNames`) — never a trust decision that depends on graph context
|
|
6
|
+
* (resolution/confirmation/linkage live elsewhere / are recomputed).
|
|
7
|
+
*
|
|
8
|
+
* TRUST BOUNDARY (deliberately narrow — see Codex 5.4.2 round-3): this file is
|
|
9
|
+
* TRUSTED LOCAL WORKSPACE STATE, the SAME trust level as `.orangepro/graph.json`
|
|
10
|
+
* (which the kit also reads back and trusts wholesale). A process that can write
|
|
11
|
+
* `parse-cache.json` can write `graph.json` directly, so defending one and not the
|
|
12
|
+
* other is incoherent. We therefore do NOT claim tamper-resistance — a substring
|
|
13
|
+
* "membership" check was tried and removed because it was security theater (a name
|
|
14
|
+
* mentioned only in a comment passed it without being a real declaration).
|
|
15
|
+
*
|
|
16
|
+
* What the cache DOES guarantee:
|
|
17
|
+
* - STRICT SCHEMA on load — a malformed/garbage/old-version entry is dropped
|
|
18
|
+
* (misses, recomputes, pruned on save). Corruption never crashes or feeds
|
|
19
|
+
* off-shape data into the graph.
|
|
20
|
+
* - CONTENT-HASH key — a changed file (new hash) misses and re-parses; an
|
|
21
|
+
* unchanged file hits; PARSER_VERSION bump invalidates the whole cache.
|
|
22
|
+
* - PRUNE on save — only this-run keys persist, so a deleted file's entry
|
|
23
|
+
* cannot linger and re-enter a future graph.
|
|
24
|
+
*/
|
|
25
|
+
// Bump whenever extractSymbolsWithMeta / extractTestNames change their output.
|
|
26
|
+
// v2: Java/Python/Go symbol extraction moved from regex to tree-sitter (AST) — the
|
|
27
|
+
// cached regex symbols for those languages must not be served on a warm run.
|
|
28
|
+
// v3: tree-sitter now emits `trivial_accessor` (body-aware boilerplate exclusion).
|
|
29
|
+
// A warm v2 entry lacks it, so a Java getter would stay in the denominator forever.
|
|
30
|
+
// v4: TS/JS now extracts default-export subjects (`const X = …; export default X`,
|
|
31
|
+
// `export default connect(...)(X)`). A warm v3 entry would omit those components.
|
|
32
|
+
// v5: TS/JS now extracts class members (`Class.method` with member_of). A warm v4
|
|
33
|
+
// entry would omit them.
|
|
34
|
+
// v6: class-member extraction skips private/protected/#private members. A warm v5
|
|
35
|
+
// entry would have included those internal methods as behaviors.
|
|
36
|
+
// v7: class-member extraction requires a runtime body — abstract, ambient
|
|
37
|
+
// (`declare class`), and overload signatures no longer count. A warm v6 entry
|
|
38
|
+
// would still carry those declaration-only members.
|
|
39
|
+
// v8: a `declare class` NODE itself is no longer emitted (ambient type, no
|
|
40
|
+
// runtime impl). A warm v7 entry would still carry the ambient class symbol.
|
|
41
|
+
// v9: per-file symbol extraction cap raised from 40 to 1000 so large product
|
|
42
|
+
// files are no longer silently undercounted from a warm v8 truncated entry.
|
|
43
|
+
// v10: tree-sitter extraction now supports Ruby/Kotlin/Rust/PHP/C#/Swift/C/C++.
|
|
44
|
+
// Warm v9 entries for those languages had empty symbol sets.
|
|
45
|
+
// v11: C/C++ function names are read only from the declarator; warm v10 entries
|
|
46
|
+
// could include macro/namespace misparses as fake function symbols.
|
|
47
|
+
// v12: C/C++ requires a real function_declarator and drops keyword names, so
|
|
48
|
+
// warm v11 entries with C++ header keyword false positives must be invalidated.
|
|
49
|
+
// v13: C/C++ type nodes require an actual grammar name field; warm v12 entries
|
|
50
|
+
// could count anonymous enum members as class symbols.
|
|
51
|
+
// v14: C/C++ type nodes require a definition body and reserved keywords are
|
|
52
|
+
// dropped; warm v13 entries could count type references or `if` as symbols.
|
|
53
|
+
// v15: follow-up grammar fixes add Ruby singleton methods, C# record structs,
|
|
54
|
+
// Swift protocol methods, and Rust trait signatures while dropping Rust aliases.
|
|
55
|
+
// v16: symbol extraction now carries source line spans for runtime coverage
|
|
56
|
+
// report ingestion; warm v15 entries lack the ranges and cannot be mapped.
|
|
57
|
+
export const PARSER_VERSION = 16;
|
|
58
|
+
const SYMBOL_KINDS = new Set(["function", "class", "const", "method"]);
|
|
59
|
+
/** Strict structural validation — persisted data is untrusted FOR SHAPE; reject anything off-shape. */
|
|
60
|
+
function validSymbols(v) {
|
|
61
|
+
if (!v || typeof v !== "object")
|
|
62
|
+
return null;
|
|
63
|
+
const o = v;
|
|
64
|
+
if (typeof o.truncated !== "boolean" || !Array.isArray(o.symbols))
|
|
65
|
+
return null;
|
|
66
|
+
for (const s of o.symbols) {
|
|
67
|
+
if (!s || typeof s !== "object")
|
|
68
|
+
return null;
|
|
69
|
+
const sym = s;
|
|
70
|
+
if (typeof sym.name !== "string" || sym.name.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
if (typeof sym.symbol_kind !== "string" || !SYMBOL_KINDS.has(sym.symbol_kind))
|
|
73
|
+
return null;
|
|
74
|
+
if (sym.start_line !== undefined && (typeof sym.start_line !== "number" || !Number.isFinite(sym.start_line)))
|
|
75
|
+
return null;
|
|
76
|
+
if (sym.end_line !== undefined && (typeof sym.end_line !== "number" || !Number.isFinite(sym.end_line)))
|
|
77
|
+
return null;
|
|
78
|
+
if (sym.callable !== undefined && typeof sym.callable !== "boolean")
|
|
79
|
+
return null;
|
|
80
|
+
if (sym.trivial_accessor !== undefined && typeof sym.trivial_accessor !== "boolean")
|
|
81
|
+
return null;
|
|
82
|
+
if (sym.member_of !== undefined && typeof sym.member_of !== "string")
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return o;
|
|
86
|
+
}
|
|
87
|
+
function validTestNames(v) {
|
|
88
|
+
if (!Array.isArray(v))
|
|
89
|
+
return null;
|
|
90
|
+
for (const x of v)
|
|
91
|
+
if (typeof x !== "string")
|
|
92
|
+
return null;
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
export class ParseCache {
|
|
96
|
+
entries;
|
|
97
|
+
seen = new Set();
|
|
98
|
+
hits = 0;
|
|
99
|
+
misses = 0;
|
|
100
|
+
constructor(data) {
|
|
101
|
+
this.entries = new Map();
|
|
102
|
+
// A version mismatch (or no data) starts empty — never trust a stale schema.
|
|
103
|
+
if (!data || data.version !== PARSER_VERSION || !data.entries || typeof data.entries !== "object")
|
|
104
|
+
return;
|
|
105
|
+
for (const [key, raw] of Object.entries(data.entries)) {
|
|
106
|
+
if (!raw || typeof raw !== "object")
|
|
107
|
+
continue;
|
|
108
|
+
const entry = {};
|
|
109
|
+
const sym = validSymbols(raw.symbols);
|
|
110
|
+
if (sym)
|
|
111
|
+
entry.symbols = sym;
|
|
112
|
+
const tn = validTestNames(raw.testNames);
|
|
113
|
+
if (tn)
|
|
114
|
+
entry.testNames = tn;
|
|
115
|
+
// Drop entries that carried nothing valid.
|
|
116
|
+
if (entry.symbols || entry.testNames)
|
|
117
|
+
this.entries.set(key, entry);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Cached symbol extraction (keyed by content hash + language). */
|
|
121
|
+
symbols(hash, language, compute) {
|
|
122
|
+
const key = `sym:${hash}:${language ?? ""}`;
|
|
123
|
+
this.seen.add(key);
|
|
124
|
+
const hit = this.entries.get(key);
|
|
125
|
+
if (hit?.symbols) {
|
|
126
|
+
this.hits++;
|
|
127
|
+
return hit.symbols;
|
|
128
|
+
}
|
|
129
|
+
this.misses++;
|
|
130
|
+
const value = compute();
|
|
131
|
+
this.entries.set(key, { ...(hit ?? {}), symbols: value });
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
/** Cached test-name extraction (keyed by content hash; language-agnostic). */
|
|
135
|
+
testNames(hash, compute) {
|
|
136
|
+
const key = `tn:${hash}`;
|
|
137
|
+
this.seen.add(key);
|
|
138
|
+
const hit = this.entries.get(key);
|
|
139
|
+
if (hit?.testNames) {
|
|
140
|
+
this.hits++;
|
|
141
|
+
return hit.testNames;
|
|
142
|
+
}
|
|
143
|
+
this.misses++;
|
|
144
|
+
const value = compute();
|
|
145
|
+
this.entries.set(key, { ...(hit ?? {}), testNames: value });
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
/** hits / (hits + misses) as a percentage (1dp); 0 when nothing was looked up. */
|
|
149
|
+
hitRate() {
|
|
150
|
+
const total = this.hits + this.misses;
|
|
151
|
+
return total === 0 ? 0 : Math.round((this.hits / total) * 1000) / 10;
|
|
152
|
+
}
|
|
153
|
+
/** Serialize for persistence — pruned to only the keys looked up THIS run (a deleted
|
|
154
|
+
* file's entry cannot linger and re-enter a future graph). */
|
|
155
|
+
toData() {
|
|
156
|
+
const entries = {};
|
|
157
|
+
for (const key of this.seen) {
|
|
158
|
+
const e = this.entries.get(key);
|
|
159
|
+
if (e)
|
|
160
|
+
entries[key] = e;
|
|
161
|
+
}
|
|
162
|
+
return { version: PARSER_VERSION, entries };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Self-asserting framework helpers (confirmer conjunct 5, Phase 4.3).
|
|
2
|
+
//
|
|
3
|
+
// A self-asserting helper is a call whose invocation IS the assertion — it fails
|
|
4
|
+
// the test on its own, with no separate `expect(...)`/`assert*`. The confirmer
|
|
5
|
+
// (confirm.ts) lets one of these satisfy conjunct 5 ("the runtime use is
|
|
6
|
+
// observed") — but ONLY for a RENDER exercise (`render(<X/>)` / `<X/>` / mount),
|
|
7
|
+
// where the helper observes the rendered DOM. A plain value call whose result is
|
|
8
|
+
// discarded is NOT rescued by a co-located self-assert (review: a UI observation
|
|
9
|
+
// is not connected to a pure function call's return value).
|
|
10
|
+
//
|
|
11
|
+
// This list is STRUCTURAL and PUBLIC — a fixed set of helper-call shapes, never a
|
|
12
|
+
// tunable secret threshold. It is deliberately MINIMAL and grows ONLY with a
|
|
13
|
+
// committed golden fixture per addition (enforced by the meta-test in
|
|
14
|
+
// tests/local/confirm.test.ts). Auto-waiting / throwing Testing-Library queries
|
|
15
|
+
// (`findBy*`, `getBy*`, `queryBy*` throw or wait, so the query IS the assertion)
|
|
16
|
+
// are the render-observation members. Cypress `should`/`contains` are recognized
|
|
17
|
+
// (harmless) but only ever satisfy conjunct 5 in a render context; pure Cypress
|
|
18
|
+
// e2e flows do not import+exercise a symbol and are not_structurally_confirmable.
|
|
19
|
+
/**
|
|
20
|
+
* The allow-list. Each entry is recognized by {@link isSelfAssertingCallee} via a
|
|
21
|
+
* trailing method name, and is anchored to exactly one committed fixture.
|
|
22
|
+
*/
|
|
23
|
+
export const SELF_ASSERT_HELPERS = [
|
|
24
|
+
{
|
|
25
|
+
id: "testing-library-findby",
|
|
26
|
+
framework: "testing-library",
|
|
27
|
+
shape: "screen.findBy*(...) — auto-waiting query that throws when absent",
|
|
28
|
+
fixture: "P6-findby-render.test.tsx"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "testing-library-getby",
|
|
32
|
+
framework: "testing-library",
|
|
33
|
+
shape: "screen.getBy*(...) — synchronous query that throws when absent",
|
|
34
|
+
fixture: "P3-self-assert.test.tsx"
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* True when a call's resolved callee text (e.g. "screen.findByRole", "getByText",
|
|
39
|
+
* "should", "cy.contains") is a self-asserting helper. Matches on the trailing
|
|
40
|
+
* method name so a chained/destructured call is recognized regardless of base.
|
|
41
|
+
*/
|
|
42
|
+
export function isSelfAssertingCallee(method) {
|
|
43
|
+
if (!method)
|
|
44
|
+
return false;
|
|
45
|
+
const trailing = method.includes(".") ? method.slice(method.lastIndexOf(".") + 1) : method;
|
|
46
|
+
// Testing-Library render-observation queries (the symbol-confirmation members).
|
|
47
|
+
if (/^(find|get|query)(All)?By[A-Z]/.test(trailing))
|
|
48
|
+
return true;
|
|
49
|
+
// Cypress UI observations — recognized, but only confirm a render exercise.
|
|
50
|
+
if (trailing === "contains" || trailing === "should")
|
|
51
|
+
return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|