@orangepro/orangepro-mcp 0.2.10 → 0.2.12
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.
|
@@ -11,6 +11,7 @@ import { GENERATED_DIR } from "./runHints.js";
|
|
|
11
11
|
import { behaviorNodes, findNode, nodesByKind, priorityRank } from "../graph/factories.js";
|
|
12
12
|
import { structurallyUnconfirmable } from "../graph/confirmable.js";
|
|
13
13
|
import { shortHash } from "../util/hash.js";
|
|
14
|
+
import { targetFingerprint } from "../ledger.js";
|
|
14
15
|
import { reportProgress } from "../util/progress.js";
|
|
15
16
|
import { redactSecrets, redactSecretsPreservingLineCount } from "../util/redact.js";
|
|
16
17
|
import { systemClock } from "../util/time.js";
|
|
@@ -2042,6 +2043,25 @@ function planGroundedBuckets(graph, targets, framework, fileReader, limit, expli
|
|
|
2042
2043
|
}
|
|
2043
2044
|
return plan;
|
|
2044
2045
|
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Previously generated drafts that may be reused verbatim for `behavior`: the
|
|
2048
|
+
* newest run's RUNNABLE, non-stale drafts recorded against the target's CURRENT
|
|
2049
|
+
* fingerprint. A missing fingerprint (unanalyzed file, unparseable id) pins
|
|
2050
|
+
* nothing — the conservative direction is to regenerate, never to serve a draft
|
|
2051
|
+
* whose target we cannot prove is unchanged.
|
|
2052
|
+
*/
|
|
2053
|
+
function pinnedDraftsFor(graph, behavior, fingerprint) {
|
|
2054
|
+
if (!fingerprint || behavior.kind !== "CodeSymbol")
|
|
2055
|
+
return [];
|
|
2056
|
+
const matches = (graph.generated_tests ?? []).filter((t) => t.target_symbol_external_id === behavior.external_id &&
|
|
2057
|
+
t.target_fingerprint === fingerprint &&
|
|
2058
|
+
t.runnable !== false &&
|
|
2059
|
+
t.stale !== true);
|
|
2060
|
+
if (matches.length === 0)
|
|
2061
|
+
return [];
|
|
2062
|
+
const latestRunId = matches[matches.length - 1].run_id;
|
|
2063
|
+
return matches.filter((t) => t.run_id === latestRunId);
|
|
2064
|
+
}
|
|
2045
2065
|
/**
|
|
2046
2066
|
* Select the same target behaviors + gathered context for the A/B comparison, so
|
|
2047
2067
|
* both arms target identical behaviors and differ ONLY in whether the KG evidence
|
|
@@ -2067,7 +2087,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2067
2087
|
const { targets, warnings } = selectTargets(graph, opts);
|
|
2068
2088
|
const framework = pickFramework(graph, opts, targets, fileReader);
|
|
2069
2089
|
const runSelection = targetsForFramework(graph, targets, framework);
|
|
2070
|
-
|
|
2090
|
+
let runTargets = runSelection.targets;
|
|
2071
2091
|
warnings.push(...runSelection.warnings);
|
|
2072
2092
|
const systemPrompt = opts.systemPrompt ?? buildSystemPrompt();
|
|
2073
2093
|
const promptVersion = inputMode === "graph_grounded" && opts.prompt_version === "v5" ? PROMPT_VERSION_V5 : PROMPT_VERSION;
|
|
@@ -2076,6 +2096,24 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2076
2096
|
const run_id = `local-gen-${runSeed}`;
|
|
2077
2097
|
const generated = [];
|
|
2078
2098
|
const missing = [];
|
|
2099
|
+
// Pin: a target whose code is byte-identical to what a previously RUNNABLE draft
|
|
2100
|
+
// was written against gets that draft back verbatim — no model call, no new
|
|
2101
|
+
// persisted record, no drift in what the agent is told to run. Regeneration
|
|
2102
|
+
// resumes the moment the target's fingerprint changes.
|
|
2103
|
+
const fingerprintOf = (behavior) => behavior.kind === "CodeSymbol" ? targetFingerprint(graph, behavior.external_id) : undefined;
|
|
2104
|
+
if (opts.pin_unchanged && inputMode === "graph_grounded") {
|
|
2105
|
+
const remaining = [];
|
|
2106
|
+
for (const behavior of runTargets) {
|
|
2107
|
+
const pinned = pinnedDraftsFor(graph, behavior, fingerprintOf(behavior));
|
|
2108
|
+
if (pinned.length === 0 || generated.length >= limit) {
|
|
2109
|
+
remaining.push(behavior);
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
generated.push(...pinned.slice(0, limit - generated.length).map((t) => ({ ...t, pinned: true })));
|
|
2113
|
+
warnings.push(`Reused ${pinned.length} pinned draft(s) for "${behavior.title || behavior.external_id}" — the target is unchanged since they were generated (no model call).`);
|
|
2114
|
+
}
|
|
2115
|
+
runTargets = remaining;
|
|
2116
|
+
}
|
|
2079
2117
|
if (inputMode === "raw_prompt") {
|
|
2080
2118
|
// Internal baseline only: broad sampling, one raw test per target (no buckets).
|
|
2081
2119
|
// Cap ATTEMPTS (not successes): with empty completions skipped below, capping
|
|
@@ -2439,6 +2477,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2439
2477
|
grounding: { entity_ids: gc.entityIds, source_refs: [], weak_relationships_used: [] },
|
|
2440
2478
|
weak_evidence_used: false,
|
|
2441
2479
|
target_symbol_external_id: behavior.external_id,
|
|
2480
|
+
...(fingerprintOf(behavior) ? { target_fingerprint: fingerprintOf(behavior) } : {}),
|
|
2442
2481
|
runnable: false,
|
|
2443
2482
|
unresolved_reason: reason
|
|
2444
2483
|
});
|
|
@@ -2460,7 +2499,9 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2460
2499
|
import_provenance
|
|
2461
2500
|
},
|
|
2462
2501
|
weak_evidence_used: gc.weakUsed.length > 0,
|
|
2463
|
-
...(behavior.kind === "CodeSymbol"
|
|
2502
|
+
...(behavior.kind === "CodeSymbol"
|
|
2503
|
+
? { target_symbol_external_id: behavior.external_id, ...(fingerprintOf(behavior) ? { target_fingerprint: fingerprintOf(behavior) } : {}) }
|
|
2504
|
+
: {}),
|
|
2464
2505
|
runnable: true
|
|
2465
2506
|
});
|
|
2466
2507
|
}
|
|
@@ -2631,7 +2672,12 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2631
2672
|
import_provenance
|
|
2632
2673
|
},
|
|
2633
2674
|
weak_evidence_used: item.weakUsed.length > 0,
|
|
2634
|
-
...(item.behavior.kind === "CodeSymbol"
|
|
2675
|
+
...(item.behavior.kind === "CodeSymbol"
|
|
2676
|
+
? {
|
|
2677
|
+
target_symbol_external_id: item.behavior.external_id,
|
|
2678
|
+
...(fingerprintOf(item.behavior) ? { target_fingerprint: fingerprintOf(item.behavior) } : {})
|
|
2679
|
+
}
|
|
2680
|
+
: {}),
|
|
2635
2681
|
runnable,
|
|
2636
2682
|
...(unresolved_reason ? { unresolved_reason } : {})
|
|
2637
2683
|
});
|
|
@@ -2640,7 +2686,11 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2640
2686
|
if (targets.length === 0) {
|
|
2641
2687
|
warnings.push("No behavior anchors available to target. Add requirements/templates or analyze a path with tests.");
|
|
2642
2688
|
}
|
|
2643
|
-
|
|
2689
|
+
// A pinned draft belongs to the run that FIRST produced it: it must not be
|
|
2690
|
+
// re-listed under this run_id (nor re-persisted), or every pin-only generation
|
|
2691
|
+
// would duplicate the same draft in the graph.
|
|
2692
|
+
const freshTests = generated.filter((t) => !t.pinned);
|
|
2693
|
+
const run = freshTests.length
|
|
2644
2694
|
? {
|
|
2645
2695
|
run_id,
|
|
2646
2696
|
model_provider: provider.providerName,
|
|
@@ -2648,7 +2698,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2648
2698
|
input_mode: inputMode,
|
|
2649
2699
|
prompt_version: promptVersion,
|
|
2650
2700
|
created_at,
|
|
2651
|
-
generated_test_ids:
|
|
2701
|
+
generated_test_ids: freshTests.map((t) => t.id)
|
|
2652
2702
|
}
|
|
2653
2703
|
: null;
|
|
2654
2704
|
return { run, generated_tests: generated, missing_evidence: missing, warnings };
|
package/dist/local/operations.js
CHANGED
|
@@ -41,7 +41,7 @@ import { renderCoverageReport } from "./pack/coverageReport.js";
|
|
|
41
41
|
import { confirmedCoverageByLayer } from "./score/coverage.js";
|
|
42
42
|
import { prepareRuntimeCoverage } from "./analyze/coverageArtifacts.js";
|
|
43
43
|
import { appendLedgerRecord, canReproveLanguage, loadLedger, ledgerStats, proofEdgesFor, reproveTarget, resolveTargetSymbol, targetFingerprint, targetLanguage } from "./ledger.js";
|
|
44
|
-
import { buildRtm, renderRtmCsv, renderRtmMarkdown } from "./rtm.js";
|
|
44
|
+
import { buildRtm, provenSymbolIds, renderRtmCsv, renderRtmMarkdown } from "./rtm.js";
|
|
45
45
|
import { buildProofDoctor, distillProofAttempts, loadProofAttempts, proofAttemptsFresh, writeProofAttempts } from "./proofDoctor.js";
|
|
46
46
|
import { tryScopedReprove } from "./reprove/scoped.js";
|
|
47
47
|
import { resolveContained, toWorkspaceRel } from "./reprove/paths.js";
|
|
@@ -811,7 +811,10 @@ export function opProofDoctor(root) {
|
|
|
811
811
|
export function opGaps(root, opts = {}) {
|
|
812
812
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
813
813
|
const gaps = findGaps(graph, opts);
|
|
814
|
-
|
|
814
|
+
// Ledger proofs are invisible to the graph's hard edges; pass them so a type
|
|
815
|
+
// whose every method is Dynamically Proven is suppressed instead of ranked.
|
|
816
|
+
const provenIds = provenSymbolIds(graph, loadLedger(root));
|
|
817
|
+
const topRiskGaps = rankRiskGaps(graph, { limit: opts.limit ?? 10, repoRoot: root, provenIds }).map((gap) => ({
|
|
815
818
|
external_id: gap.id,
|
|
816
819
|
title: gap.title,
|
|
817
820
|
file: gap.file,
|
|
@@ -1598,7 +1601,11 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1598
1601
|
try {
|
|
1599
1602
|
const graphForGeneration = loadGraph(workspacePaths(root).graphPath);
|
|
1600
1603
|
const generatedTargets = new Set((graphForGeneration.generated_tests ?? []).map((t) => t.target_symbol_external_id).filter((id) => Boolean(id)));
|
|
1601
|
-
const targetIds = rankRiskGaps(graphForGeneration, {
|
|
1604
|
+
const targetIds = rankRiskGaps(graphForGeneration, {
|
|
1605
|
+
repoRoot: root,
|
|
1606
|
+
limit: START_GENERATE_RISK_LIMIT,
|
|
1607
|
+
provenIds: provenSymbolIds(graphForGeneration, loadLedger(root))
|
|
1608
|
+
})
|
|
1602
1609
|
.map((gap) => gap.id)
|
|
1603
1610
|
.filter((id) => !generatedTargets.has(id));
|
|
1604
1611
|
if (targetIds.length) {
|
|
@@ -1792,12 +1799,24 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1792
1799
|
};
|
|
1793
1800
|
}
|
|
1794
1801
|
const reader = fileReaderFor(graph.workspace.root);
|
|
1795
|
-
const result = await generateTests(graph, {
|
|
1796
|
-
|
|
1802
|
+
const result = await generateTests(graph, {
|
|
1803
|
+
target_ids: opts.target_ids,
|
|
1804
|
+
framework: opts.framework,
|
|
1805
|
+
limit: opts.limit,
|
|
1806
|
+
input_mode: opts.input_mode,
|
|
1807
|
+
prompt_version: opts.prompt_version,
|
|
1808
|
+
// Persisting lane: a runnable draft for an unchanged target is reused as-is
|
|
1809
|
+
// rather than re-bought from the model on every run.
|
|
1810
|
+
pin_unchanged: opts.pin_unchanged ?? true
|
|
1811
|
+
}, provider, reader, deps.clock);
|
|
1812
|
+
// Pinned drafts are already IN graph.generated_tests — appending them again
|
|
1813
|
+
// would duplicate a draft on every generation of an unchanged target.
|
|
1814
|
+
const freshTests = result.generated_tests.filter((t) => !t.pinned);
|
|
1815
|
+
if (result.run && freshTests.length) {
|
|
1797
1816
|
const next = {
|
|
1798
1817
|
...graph,
|
|
1799
1818
|
generation_runs: [...graph.generation_runs, result.run],
|
|
1800
|
-
generated_tests: [...graph.generated_tests, ...
|
|
1819
|
+
generated_tests: [...graph.generated_tests, ...freshTests],
|
|
1801
1820
|
updated_at: deps.clock()
|
|
1802
1821
|
};
|
|
1803
1822
|
saveGraph(paths.graphPath, next);
|
package/dist/local/rtm.js
CHANGED
|
@@ -47,6 +47,21 @@ export function buildRtm(graph, ledger, opts = {}) {
|
|
|
47
47
|
const rows = opts.limit && opts.limit > 0 ? filteredRows.slice(0, opts.limit) : filteredRows;
|
|
48
48
|
return { summary: summarizeRows(baseRows, unionRows), rows, ...(opts.scope ? { scope: opts.scope } : {}) };
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* CodeSymbol ids the RTM would call Dynamically Proven — the SAME selection
|
|
52
|
+
* buildRtm uses (`selectLedgerBySymbol(...).proven`), so proof judgment has one
|
|
53
|
+
* definition. Consumers outside the RTM (risk ranking's container suppression)
|
|
54
|
+
* need it because a proof leaves no graph edge and its target frequently sits
|
|
55
|
+
* outside the deterministic denominator.
|
|
56
|
+
*/
|
|
57
|
+
export function provenSymbolIds(graph, ledger) {
|
|
58
|
+
const out = new Set();
|
|
59
|
+
for (const [symbol, selected] of selectLedgerBySymbol(ledger, graph)) {
|
|
60
|
+
if (selected.proven)
|
|
61
|
+
out.add(symbol);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
50
65
|
function inScope(node, targetSet, fileSet) {
|
|
51
66
|
if (!targetSet && !fileSet)
|
|
52
67
|
return true;
|
package/dist/local/score/risk.js
CHANGED
|
@@ -11,7 +11,37 @@ function symbolFile(n) {
|
|
|
11
11
|
function symbolTitle(n) {
|
|
12
12
|
return n.title ?? n.external_id.split("#")[1] ?? n.external_id;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Structural container → member children, read from the analyzer's own
|
|
16
|
+
* `properties.member_of` (the same signal src/local/reprove/scoped.ts uses).
|
|
17
|
+
* Id-prefix matching cannot do this job: `sym:f.ts#a.b` is a child of
|
|
18
|
+
* `sym:f.ts#a` only by string luck, and a dotted symbol name with no container
|
|
19
|
+
* node would be mis-parented. A container is resolved inside the child's OWN
|
|
20
|
+
* file: a same-named symbol elsewhere is a different thing, and mis-parenting
|
|
21
|
+
* here would suppress a genuine gap.
|
|
22
|
+
*/
|
|
23
|
+
function containerChildren(graph) {
|
|
24
|
+
const symbols = graph.nodes.filter((n) => n.kind === "CodeSymbol");
|
|
25
|
+
const idByFileTitle = new Map();
|
|
26
|
+
for (const n of symbols)
|
|
27
|
+
idByFileTitle.set(`${symbolFile(n)}#${symbolTitle(n)}`, n.external_id);
|
|
28
|
+
const out = new Map();
|
|
29
|
+
for (const n of symbols) {
|
|
30
|
+
const memberOf = n.properties.member_of;
|
|
31
|
+
if (typeof memberOf !== "string" || memberOf === "")
|
|
32
|
+
continue;
|
|
33
|
+
const containerId = idByFileTitle.get(`${symbolFile(n)}#${memberOf}`);
|
|
34
|
+
if (!containerId || containerId === n.external_id)
|
|
35
|
+
continue;
|
|
36
|
+
const list = out.get(containerId);
|
|
37
|
+
if (list)
|
|
38
|
+
list.push(n.external_id);
|
|
39
|
+
else
|
|
40
|
+
out.set(containerId, [n.external_id]);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function confirmedBehaviorIds(graph, provenIds) {
|
|
15
45
|
const ids = new Set();
|
|
16
46
|
const nodeKinds = new Map(graph.nodes.map((n) => [n.external_id, n.kind]));
|
|
17
47
|
for (const e of graph.edges) {
|
|
@@ -24,6 +54,19 @@ function confirmedBehaviorIds(graph) {
|
|
|
24
54
|
if (nodeKinds.get(e.to_external_id) === "CodeSymbol" || nodeKinds.get(e.to_external_id) === "Requirement")
|
|
25
55
|
ids.add(e.to_external_id);
|
|
26
56
|
}
|
|
57
|
+
// A container type whose every method child is confirmed has no distinct
|
|
58
|
+
// untested surface left — suppress it from the gap ranking rather than
|
|
59
|
+
// listing it as an unlinked candidate above its own proven methods.
|
|
60
|
+
// "Confirmed" here means a hard static link OR a current ledger proof: the
|
|
61
|
+
// proof lane leaves no graph edge and usually sits outside the denominator,
|
|
62
|
+
// so a hard-edge-only check never fires for a dynamically proven type.
|
|
63
|
+
const eligible = new Set(graph.nodes.filter((n) => n.kind === "CodeSymbol" && n.denominator_eligible === true).map((n) => n.external_id));
|
|
64
|
+
for (const [containerId, children] of containerChildren(graph)) {
|
|
65
|
+
if (ids.has(containerId) || !eligible.has(containerId))
|
|
66
|
+
continue;
|
|
67
|
+
if (children.every((c) => ids.has(c) || provenIds?.has(c) === true))
|
|
68
|
+
ids.add(containerId);
|
|
69
|
+
}
|
|
27
70
|
return ids;
|
|
28
71
|
}
|
|
29
72
|
const GIT_CHURN_BATCH = 200;
|
|
@@ -314,7 +357,7 @@ function candidateSignalIds(graph, candidateIds) {
|
|
|
314
357
|
}
|
|
315
358
|
export function rankRiskGaps(graph, opts = {}) {
|
|
316
359
|
const limit = opts.limit ?? 20;
|
|
317
|
-
const confirmed = confirmedBehaviorIds(graph);
|
|
360
|
+
const confirmed = confirmedBehaviorIds(graph, opts.provenIds);
|
|
318
361
|
const symbols = graph.nodes.filter((n) => n.kind === "CodeSymbol" && n.denominator_eligible === true && !n.stale && !confirmed.has(n.external_id));
|
|
319
362
|
const symbolIds = new Set(symbols.map((s) => s.external_id));
|
|
320
363
|
const symbolsByFile = new Map();
|
|
@@ -409,7 +409,7 @@ function riskGeneratedTests(graph, gap, riskIds, isFirstRowForFile) {
|
|
|
409
409
|
// the assertion line stays pure metadata (framework, same-file, disclosure).
|
|
410
410
|
assertion: [
|
|
411
411
|
sameFile ? "same-file target" : "",
|
|
412
|
-
t.framework_hint,
|
|
412
|
+
t.framework_hint || (gap.file.endsWith(".go") ? "go" : ""),
|
|
413
413
|
t.weak_evidence_used ? "weak evidence disclosed" : ""
|
|
414
414
|
]
|
|
415
415
|
.filter(Boolean)
|
|
@@ -826,7 +826,7 @@ function riskRows(risks, graph) {
|
|
|
826
826
|
].filter((c) => Boolean(c)))];
|
|
827
827
|
return {
|
|
828
828
|
generatedTests,
|
|
829
|
-
applicableCategories: riskApplicableConcerns(risk, verb),
|
|
829
|
+
applicableCategories: [...new Set([...riskApplicableConcerns(risk, verb), ...generatedCategories])],
|
|
830
830
|
generatedCategories,
|
|
831
831
|
verb,
|
|
832
832
|
path,
|
|
@@ -758,11 +758,11 @@ function renderRisks(){
|
|
|
758
758
|
hiddenGeneratedFlows
|
|
759
759
|
? \`<div class="paywall-num">\${hiddenGeneratedFlows} more flows with tests</div>
|
|
760
760
|
<div class="paywall-txt">OrangePro accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Use the “Flows with tests” filter to review them first.</div>
|
|
761
|
-
<a class="paywall-btn" href="https://
|
|
761
|
+
<a class="paywall-btn" href="https://platform.orangepro.ai/" target="_blank">View all on OrangePro Platform →</a>\`
|
|
762
762
|
: remainingRiskFlows
|
|
763
763
|
? \`<div class="paywall-num">\${remainingRiskFlows} high-risk flows left</div>
|
|
764
764
|
<div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Generate the remaining high-risk flow tests on OrangePro Platform.</div>
|
|
765
|
-
<a class="paywall-btn" href="https://
|
|
765
|
+
<a class="paywall-btn" href="https://platform.orangepro.ai/" target="_blank">Generate remaining tests on Platform →</a>\`
|
|
766
766
|
: \`<div class="paywall-num">All generated tests are shown</div>
|
|
767
767
|
<div class="paywall-txt">OrangePro generated tests for every high-risk flow in this report, and every generated test is visible here.</div>\`));
|
|
768
768
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
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",
|