onto-mcp 0.4.13 → 0.4.14
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/.onto/processes/review/external-oauth-worker-contract.md +12 -5
- package/README.md +33 -17
- package/dist/core-api/reconstruct-api.js +41 -18
- package/dist/core-api/review-api.js +9 -1
- package/dist/core-runtime/cli/prepare-review-session.js +16 -1
- package/dist/core-runtime/cli/review-invocation-runner.js +6 -0
- package/dist/core-runtime/cli/review-invoke.js +26 -2
- package/dist/core-runtime/discovery/llm-override.js +262 -0
- package/dist/core-runtime/discovery/review-cert-assemble.js +1 -1
- package/dist/core-runtime/discovery/review-cert-record.js +119 -21
- package/dist/core-runtime/discovery/settings-chain.js +33 -11
- package/dist/core-runtime/llm/model-switcher.js +7 -1
- package/dist/core-runtime/reconstruct/run.js +1 -1
- package/dist/core-runtime/review/materializers.js +4 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +227 -32
- package/dist/mcp/server.js +177 -14
- package/dist/mcp/tool-schemas.js +22 -5
- package/package.json +1 -1
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** The built-in code fixtures this gate can score — the value-level single
|
|
2
|
+
* source (the fixture-id type derives from it), mirroring
|
|
3
|
+
* SEMANTIC_QUALITY_GATE_CHECK_IDS below. FIXTURES is keyed by this type, so a new
|
|
4
|
+
* id here must ship a preset; the V2 scoring-completeness meta-test pins its
|
|
5
|
+
* scored set against THIS list at runtime, so a new fixture cannot enter the cert
|
|
6
|
+
* set with unproven (possibly stuck-on-pass) scoring. */
|
|
7
|
+
export const SEMANTIC_QUALITY_GATE_FIXTURE_IDS = [
|
|
8
|
+
"review-pipeline-target-v1",
|
|
9
|
+
"retry-policy-target-v1",
|
|
10
|
+
"clean-target-v1",
|
|
11
|
+
"shared-root-target-v1",
|
|
12
|
+
];
|
|
1
13
|
/** The complete check-id universe this gate can emit — the value-level single
|
|
2
14
|
* source (the check_id type derives from it). The review-cert/v2 evidence
|
|
3
15
|
* contract pins its per-run check universe against THIS list, so a harness
|
|
@@ -17,6 +29,21 @@ export const SEMANTIC_QUALITY_GATE_CHECK_IDS = [
|
|
|
17
29
|
"count_list_consistency",
|
|
18
30
|
"grounding",
|
|
19
31
|
];
|
|
32
|
+
/**
|
|
33
|
+
* Checks the gate OMITS for a clean-target fixture (expectsNoMaterialDefects):
|
|
34
|
+
* recall has nothing to recall, and grounding/actionability structurally fail
|
|
35
|
+
* on an empty material set (they require a material issue to ground / act on).
|
|
36
|
+
* These are declared N/A via the fixture's applicable_check_ids; the gate emits
|
|
37
|
+
* exactly the complementary applicable set. Kept as a module constant so the
|
|
38
|
+
* record-layer emission check (A-1b) can pin the same set.
|
|
39
|
+
*/
|
|
40
|
+
export const CLEAN_TARGET_EXCLUDED_CHECK_IDS = new Set([
|
|
41
|
+
"material_issue_recall",
|
|
42
|
+
"final_result_material_issue_recall",
|
|
43
|
+
"artifact_material_issue_recall",
|
|
44
|
+
"grounding",
|
|
45
|
+
"actionability",
|
|
46
|
+
]);
|
|
20
47
|
const DEFAULT_FIXTURE_ID = "review-pipeline-target-v1";
|
|
21
48
|
const FIXTURES = {
|
|
22
49
|
"review-pipeline-target-v1": {
|
|
@@ -73,6 +100,71 @@ const FIXTURES = {
|
|
|
73
100
|
targetAnchor: "src/retry.ts",
|
|
74
101
|
targetAnchorTerms: ["src/retry.ts", "retry.ts"],
|
|
75
102
|
},
|
|
103
|
+
// v3 G1 clean-target (design §D1/§D3): ZERO material defects + one boundary
|
|
104
|
+
// decoy (an accepted-but-unread field whose defect status cannot be decided
|
|
105
|
+
// without caller/public-API evidence). materialTerms is empty (nothing to
|
|
106
|
+
// recall — accepted only because expectsNoMaterialDefects); recall/grounding/
|
|
107
|
+
// actionability are excluded from emission (CLEAN_TARGET_EXCLUDED). The control
|
|
108
|
+
// is the boundary decoy: yes-man → false_materiality_guard fails; empty silence
|
|
109
|
+
// → boundary_uncertainty_preservation MUST-preserve fails; correct silence
|
|
110
|
+
// (decoy preserved as non-material boundary context) passes.
|
|
111
|
+
"clean-target-v1": {
|
|
112
|
+
fixtureId: "clean-target-v1",
|
|
113
|
+
materialTerms: [],
|
|
114
|
+
expectedMaterialTruth: "no material defect (clean target)",
|
|
115
|
+
boundaryUncertaintyTerms: ["telemetry label", "debug export"],
|
|
116
|
+
boundaryContextTerms: [
|
|
117
|
+
"evidence gap",
|
|
118
|
+
"needs evidence",
|
|
119
|
+
"insufficient evidence",
|
|
120
|
+
"without caller",
|
|
121
|
+
"without public api",
|
|
122
|
+
],
|
|
123
|
+
actionMaterialTerms: [],
|
|
124
|
+
actionRemediationTerms: [],
|
|
125
|
+
targetAnchor: "src/clean-target.ts",
|
|
126
|
+
targetAnchorTerms: ["src/clean-target.ts", "clean-target.ts"],
|
|
127
|
+
expectsNoMaterialDefects: true,
|
|
128
|
+
requiresBoundaryPreservation: true,
|
|
129
|
+
},
|
|
130
|
+
// v3 G2 shared-root (design §D1/§D3): ONE common root (rawFormat delegating to
|
|
131
|
+
// JSON.stringify, which returns undefined for top-level undefined) surfaces as
|
|
132
|
+
// TWO distinct defects (unstableFormat + alternateFormat both delegate to it),
|
|
133
|
+
// plus one INDEPENDENT defect (truncate off-by-one) and a lensId boundary decoy.
|
|
134
|
+
// The control is expectedSharedCauseAnchorPairs: the shared_cause branch of
|
|
135
|
+
// causal_relation_correctness no longer passes vacuously (`.every` over an empty
|
|
136
|
+
// relation set) — a valid shared_cause_candidate relation MUST connect the
|
|
137
|
+
// unstableFormat and alternate findings. All 12 checks are emitted (full
|
|
138
|
+
// universe): shared-root has real material defects, so the recall floor applies.
|
|
139
|
+
"shared-root-target-v1": {
|
|
140
|
+
fixtureId: "shared-root-target-v1",
|
|
141
|
+
materialTerms: ["unstableformat", "json.stringify", "undefined"],
|
|
142
|
+
expectedMaterialTruth: "unstableFormat + JSON.stringify + undefined",
|
|
143
|
+
boundaryUncertaintyTerms: ["lensid", "lens id", "lens ids", "lens identity"],
|
|
144
|
+
boundaryContextTerms: [
|
|
145
|
+
"evidence gap",
|
|
146
|
+
"needs evidence",
|
|
147
|
+
"insufficient evidence",
|
|
148
|
+
"low-confidence",
|
|
149
|
+
"unresolved",
|
|
150
|
+
"without caller",
|
|
151
|
+
"without public api",
|
|
152
|
+
"caller evidence",
|
|
153
|
+
"public api evidence",
|
|
154
|
+
],
|
|
155
|
+
actionMaterialTerms: ["unstableformat", "json.stringify", "undefined"],
|
|
156
|
+
actionRemediationTerms: [
|
|
157
|
+
"return type",
|
|
158
|
+
"fallback",
|
|
159
|
+
"widen",
|
|
160
|
+
"guard",
|
|
161
|
+
"focused test",
|
|
162
|
+
"verify",
|
|
163
|
+
],
|
|
164
|
+
targetAnchor: "src/shared-root.ts",
|
|
165
|
+
targetAnchorTerms: ["src/shared-root.ts", "shared-root.ts"],
|
|
166
|
+
expectedSharedCauseAnchorPairs: [[["unstableformat"], ["alternate"]]],
|
|
167
|
+
},
|
|
76
168
|
};
|
|
77
169
|
function semanticFixture(fixtureId) {
|
|
78
170
|
const resolved = fixtureId ?? DEFAULT_FIXTURE_ID;
|
|
@@ -81,6 +173,13 @@ function semanticFixture(fixtureId) {
|
|
|
81
173
|
}
|
|
82
174
|
return FIXTURES[resolved];
|
|
83
175
|
}
|
|
176
|
+
/** SSOT accessor for a built-in fixture's pinned expectation preset (design §D3 —
|
|
177
|
+
* Phase A fixtures are code-presets). Returns a deep copy so callers cannot mutate
|
|
178
|
+
* the shared preset; tests and the cert manifest read the SAME source, so an
|
|
179
|
+
* injected copy can never drift from what the gate actually evaluates. */
|
|
180
|
+
export function semanticQualityFixturePreset(fixtureId) {
|
|
181
|
+
return structuredClone(semanticFixture(fixtureId));
|
|
182
|
+
}
|
|
84
183
|
function records(value) {
|
|
85
184
|
return Array.isArray(value)
|
|
86
185
|
? value.filter((item) => typeof item === "object" && item !== null)
|
|
@@ -151,6 +250,24 @@ function validCausalPath(value) {
|
|
|
151
250
|
nonEmptyString(step.claim) &&
|
|
152
251
|
nonEmptyStringArray(step.evidence_refs));
|
|
153
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* A shared_cause_candidate relation is internally consistent: it carries a
|
|
255
|
+
* shared_cause block whose from/to cause refs are owned by the relation's own
|
|
256
|
+
* endpoint findings. The single authority for shared-cause validity — consumed
|
|
257
|
+
* both by causal_relation_correctness's negative sweep (every relation must be
|
|
258
|
+
* valid) and by its positive anchor-pair existence requirement (v3 G2).
|
|
259
|
+
*/
|
|
260
|
+
function validSharedCauseRelation(relation, causeOwnerById) {
|
|
261
|
+
const sharedCause = record(relation.shared_cause);
|
|
262
|
+
const fromFindingId = typeof relation.from_finding_id === "string" ? relation.from_finding_id : "";
|
|
263
|
+
const toFindingId = typeof relation.to_finding_id === "string" ? relation.to_finding_id : "";
|
|
264
|
+
return (sharedCause !== null &&
|
|
265
|
+
nonEmptyString(sharedCause.cause_claim) &&
|
|
266
|
+
nonEmptyString(sharedCause.from_cause_ref) &&
|
|
267
|
+
nonEmptyString(sharedCause.to_cause_ref) &&
|
|
268
|
+
causeOwnerById.get(sharedCause.from_cause_ref) === fromFindingId &&
|
|
269
|
+
causeOwnerById.get(sharedCause.to_cause_ref) === toFindingId);
|
|
270
|
+
}
|
|
154
271
|
/**
|
|
155
272
|
* Whether two findings inside one issue are connected through
|
|
156
273
|
* same_root_candidate relations cited in that issue's relation_refs — the
|
|
@@ -313,28 +430,53 @@ function issueArtifactChecks(artifacts, declaredNonMaterialFindingCount, nonMate
|
|
|
313
430
|
`preserved_non_material_ids=${nonMaterialPreservationIds.join(",") || "none"}`,
|
|
314
431
|
"non-material findings must preserve target-specific boundary uncertainty and remain outside relation coverage",
|
|
315
432
|
]);
|
|
433
|
+
// v3 G2: when the fixture declares anchor pairs, each pair MUST be connected
|
|
434
|
+
// by a valid shared_cause_candidate relation between findings anchored to the
|
|
435
|
+
// two term-groups — closing the `.every`-vacuous pass that lets a missing
|
|
436
|
+
// relation through. A finding is anchored to a group when its text contains
|
|
437
|
+
// every term in the group (terms are lowercased to match normalizedText).
|
|
438
|
+
const anchorPairs = fixture.expectedSharedCauseAnchorPairs ?? [];
|
|
439
|
+
const findingById = new Map();
|
|
440
|
+
for (const finding of findings) {
|
|
441
|
+
if (typeof finding.finding_id === "string") {
|
|
442
|
+
findingById.set(finding.finding_id, finding);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const findingMatchesAnchorGroup = (finding, group) => {
|
|
446
|
+
if (!finding)
|
|
447
|
+
return false;
|
|
448
|
+
const text = normalizedText(finding);
|
|
449
|
+
return group.every((term) => text.includes(term.toLowerCase()));
|
|
450
|
+
};
|
|
451
|
+
const anchorPairsSatisfied = anchorPairs.every(([groupA, groupB]) => relationRows.some((relation) => {
|
|
452
|
+
if (relation.relation !== "shared_cause_candidate")
|
|
453
|
+
return false;
|
|
454
|
+
// G2 requires TWO DISTINCT surface defects sharing a root — a self-relation
|
|
455
|
+
// (from === to) on one finding that happens to match both groups is a
|
|
456
|
+
// degenerate loop, not a genuine cross-defect link.
|
|
457
|
+
if (relation.from_finding_id === relation.to_finding_id)
|
|
458
|
+
return false;
|
|
459
|
+
if (!validSharedCauseRelation(relation, causeOwnerById))
|
|
460
|
+
return false;
|
|
461
|
+
const fromFinding = typeof relation.from_finding_id === "string"
|
|
462
|
+
? findingById.get(relation.from_finding_id)
|
|
463
|
+
: undefined;
|
|
464
|
+
const toFinding = typeof relation.to_finding_id === "string"
|
|
465
|
+
? findingById.get(relation.to_finding_id)
|
|
466
|
+
: undefined;
|
|
467
|
+
return ((findingMatchesAnchorGroup(fromFinding, groupA) &&
|
|
468
|
+
findingMatchesAnchorGroup(toFinding, groupB)) ||
|
|
469
|
+
(findingMatchesAnchorGroup(fromFinding, groupB) &&
|
|
470
|
+
findingMatchesAnchorGroup(toFinding, groupA)));
|
|
471
|
+
}));
|
|
316
472
|
const causalRelationCorrectness = check("causal_relation_correctness", materialFindingIds.every((findingId) => relationCoveredIds.has(findingId)) &&
|
|
317
|
-
relationRows.every((relation) =>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const sharedCause = record(relation.shared_cause);
|
|
321
|
-
const fromFindingId = typeof relation.from_finding_id === "string"
|
|
322
|
-
? relation.from_finding_id
|
|
323
|
-
: "";
|
|
324
|
-
const toFindingId = typeof relation.to_finding_id === "string"
|
|
325
|
-
? relation.to_finding_id
|
|
326
|
-
: "";
|
|
327
|
-
return (sharedCause !== null &&
|
|
328
|
-
nonEmptyString(sharedCause.cause_claim) &&
|
|
329
|
-
nonEmptyString(sharedCause.from_cause_ref) &&
|
|
330
|
-
nonEmptyString(sharedCause.to_cause_ref) &&
|
|
331
|
-
causeOwnerById.get(sharedCause.from_cause_ref) ===
|
|
332
|
-
fromFindingId &&
|
|
333
|
-
causeOwnerById.get(sharedCause.to_cause_ref) === toFindingId);
|
|
334
|
-
}), [
|
|
473
|
+
relationRows.every((relation) => relation.relation !== "shared_cause_candidate" ||
|
|
474
|
+
validSharedCauseRelation(relation, causeOwnerById)) &&
|
|
475
|
+
anchorPairsSatisfied, [
|
|
335
476
|
`material_finding_ids=${materialFindingIds.join(",") || "none"}`,
|
|
336
477
|
`relation_covered_ids=${[...relationCoveredIds].join(",") || "none"}`,
|
|
337
478
|
`shared_cause_relation_ids=${sharedCauseRelationIds.join(",") || "none"}`,
|
|
479
|
+
`expected_shared_cause_anchor_pairs=${anchorPairs.length} satisfied=${anchorPairsSatisfied}`,
|
|
338
480
|
]);
|
|
339
481
|
const dependencyPreservation = check("issue_dependency_preservation", relationRows.every((relation) => {
|
|
340
482
|
if (relation.relation !== "shared_cause_candidate")
|
|
@@ -420,20 +562,41 @@ export function evaluateReviewPipelineSemanticQualityGate(args) {
|
|
|
420
562
|
const fixture = args.expectations ?? semanticFixture(args.fixtureId);
|
|
421
563
|
if (fixture.materialTerms.length === 0) {
|
|
422
564
|
// textContainsAll over an empty list is vacuously true, so the material
|
|
423
|
-
// recall checks would prove nothing — fail loud
|
|
424
|
-
|
|
565
|
+
// recall checks would prove nothing — fail loud UNLESS this is a
|
|
566
|
+
// clean-target fixture, where there is genuinely nothing to recall and the
|
|
567
|
+
// recall checks are excluded from emission (see CLEAN_TARGET_EXCLUDED).
|
|
568
|
+
if (!fixture.expectsNoMaterialDefects) {
|
|
569
|
+
throw new Error("SemanticQualityExpectations.materialTerms must not be empty");
|
|
570
|
+
}
|
|
425
571
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
572
|
+
else {
|
|
573
|
+
// 빈 문자열 term/alternate는 text.includes("")가 항상 참이라 해당 entry를
|
|
574
|
+
// 공허 충족시킨다 — 게이트 진입에서 fail loud.
|
|
575
|
+
for (const term of fixture.materialTerms) {
|
|
576
|
+
const alternates = Array.isArray(term) ? term : [term];
|
|
577
|
+
if (alternates.length === 0 ||
|
|
578
|
+
alternates.some((alternate) => alternate.trim().length === 0)) {
|
|
579
|
+
throw new Error("SemanticQualityExpectations.materialTerms entries must be non-empty strings or non-empty groups of non-empty strings");
|
|
580
|
+
}
|
|
433
581
|
}
|
|
434
582
|
}
|
|
583
|
+
// A clean-target's whole control is the boundary decoy: with no material
|
|
584
|
+
// defect the recall floor cannot catch silence, so distinguishing correct
|
|
585
|
+
// silence from lazy empty silence REQUIRES MUST-preserve. A clean-target that
|
|
586
|
+
// forgets requiresBoundaryPreservation would let empty silence pass vacuously
|
|
587
|
+
// — fail loud on the misconfiguration rather than certify a broken control.
|
|
588
|
+
if (fixture.expectsNoMaterialDefects && !fixture.requiresBoundaryPreservation) {
|
|
589
|
+
throw new Error("SemanticQualityExpectations.expectsNoMaterialDefects requires requiresBoundaryPreservation — without the boundary decoy control, empty silence passes vacuously");
|
|
590
|
+
}
|
|
435
591
|
const summary = args.reviewRecord.result_classification_summary ?? null;
|
|
436
592
|
const materialIssues = records(summary?.material_issues);
|
|
593
|
+
// Clean-target (v3 G1): material promotion must be caught on BOTH surfaces —
|
|
594
|
+
// the summary AND the finding-ledger authority. false_materiality_guard reads
|
|
595
|
+
// the summary; this counts artifact-ledger material findings so a fabricated
|
|
596
|
+
// material finding injected only into the ledger cannot slip past the guard.
|
|
597
|
+
const artifactMaterialFindingCount = fixture.expectsNoMaterialDefects
|
|
598
|
+
? recordArray(record(args.issueArtifacts?.findingLedger)?.findings).filter((finding) => ["blocker", "high", "medium"].includes(String(finding.severity))).length
|
|
599
|
+
: 0;
|
|
437
600
|
const nonMaterialFindings = records(summary?.non_material_findings);
|
|
438
601
|
const actionCandidates = records(summary?.action_candidates);
|
|
439
602
|
const materialText = normalizedText(materialIssues);
|
|
@@ -469,13 +632,27 @@ export function evaluateReviewPipelineSemanticQualityGate(args) {
|
|
|
469
632
|
: `Final Review Result chars=${finalReviewResult.length}`,
|
|
470
633
|
`expected final explanation to preserve ${fixture.expectedMaterialTruth}`,
|
|
471
634
|
]);
|
|
472
|
-
const falseMaterialityGuard = check("false_materiality_guard",
|
|
635
|
+
const falseMaterialityGuard = check("false_materiality_guard",
|
|
636
|
+
// Clean-target (v3 G1): the target has zero material defects, so ANY
|
|
637
|
+
// admitted material issue is a false positive — on EITHER surface. A yes-man
|
|
638
|
+
// that promotes a fabricated issue fails here whether it lands in the summary
|
|
639
|
+
// (materialIssues) or only in the finding-ledger authority
|
|
640
|
+
// (artifactMaterialFindingCount), not just the boundary case the base branch
|
|
641
|
+
// catches.
|
|
642
|
+
(!fixture.expectsNoMaterialDefects ||
|
|
643
|
+
(materialIssues.length === 0 && artifactMaterialFindingCount === 0)) &&
|
|
644
|
+
!materialBoundarySensitiveFalsePositive &&
|
|
473
645
|
(!falseMaterialityCandidateObserved ||
|
|
474
646
|
(textContainsAny(preservedBoundaryText, fixture.boundaryUncertaintyTerms) &&
|
|
475
647
|
textContainsAny(preservedBoundaryText, fixture.boundaryContextTerms))), [
|
|
476
648
|
`boundary-sensitive uncertainty terms must be disclosed with boundary context when they are not admitted material issues: ${fixture.boundaryUncertaintyTerms.join(", ")}`,
|
|
477
649
|
`non_material_finding_count=${nonMaterialFindings.length}`,
|
|
478
650
|
`boundary_notes_chars=${boundaryNotes?.length ?? 0}`,
|
|
651
|
+
...(fixture.expectsNoMaterialDefects
|
|
652
|
+
? [
|
|
653
|
+
`clean-target: admitted material_issue_count=${materialIssues.length}, artifact_material_finding_count=${artifactMaterialFindingCount} (both must be 0)`,
|
|
654
|
+
]
|
|
655
|
+
: []),
|
|
479
656
|
]);
|
|
480
657
|
// Boundary uncertainty's preservation AUTHORITY is the finding-ledger
|
|
481
658
|
// (non-material findings), not the final Boundary Notes projection. A model
|
|
@@ -488,10 +665,19 @@ export function evaluateReviewPipelineSemanticQualityGate(args) {
|
|
|
488
665
|
// false_materiality_guard, which checks the ORTHOGONAL axis: the decoy was not
|
|
489
666
|
// mis-promoted to a material issue. (A decoy surfaced only in the final note but
|
|
490
667
|
// absent from the authority still fails here — authority is where it must live.)
|
|
491
|
-
const boundaryUncertainty = check("boundary_uncertainty_preservation",
|
|
492
|
-
|
|
668
|
+
const boundaryUncertainty = check("boundary_uncertainty_preservation",
|
|
669
|
+
// Clean-target (v3 G1): the boundary decoy MUST be preserved — an empty,
|
|
670
|
+
// lazy review that observes nothing no longer passes the vacuous first
|
|
671
|
+
// clause. Otherwise the base rule stands: preserve only what was observed.
|
|
672
|
+
fixture.requiresBoundaryPreservation
|
|
673
|
+
? containsBoundarySensitiveUncertainty(nonMaterialText, fixture)
|
|
674
|
+
: !falseMaterialityCandidateObserved ||
|
|
675
|
+
containsBoundarySensitiveUncertainty(nonMaterialText, fixture), [
|
|
493
676
|
`non_material_boundary_chars=${nonMaterialText.length}`,
|
|
494
677
|
`expected boundary uncertainty (${fixture.boundaryUncertaintyTerms.join(", ")}) preserved in the finding-ledger authority (non-material findings)`,
|
|
678
|
+
...(fixture.requiresBoundaryPreservation
|
|
679
|
+
? ["clean-target: boundary decoy MUST be preserved (empty silence fails)"]
|
|
680
|
+
: []),
|
|
495
681
|
]);
|
|
496
682
|
const materialActionRefs = new Set(materialIssues.flatMap((issue) => strings(issue.action_candidates)));
|
|
497
683
|
for (const item of actionCandidates) {
|
|
@@ -530,12 +716,21 @@ export function evaluateReviewPipelineSemanticQualityGate(args) {
|
|
|
530
716
|
actionability,
|
|
531
717
|
grounding,
|
|
532
718
|
];
|
|
719
|
+
// Clean-target (v3 G1): emit only the applicable set — the recall/grounding/
|
|
720
|
+
// actionability checks are N/A with no material defect and would structurally
|
|
721
|
+
// fail on an empty material set. The record layer's per-fixture emission pin
|
|
722
|
+
// (A-1b) expects exactly this reduced set for such a fixture.
|
|
723
|
+
const emittedChecks = fixture.expectsNoMaterialDefects
|
|
724
|
+
? checks.filter((item) => !CLEAN_TARGET_EXCLUDED_CHECK_IDS.has(item.check_id))
|
|
725
|
+
: checks;
|
|
533
726
|
return {
|
|
534
|
-
status:
|
|
727
|
+
status: emittedChecks.every((item) => item.status === "passed")
|
|
728
|
+
? "passed"
|
|
729
|
+
: "failed",
|
|
535
730
|
fixture_id: fixture.fixtureId,
|
|
536
731
|
scope: "fixture_specific",
|
|
537
732
|
fixture_target_anchor: fixture.targetAnchor,
|
|
538
733
|
applicability: "real_model_only",
|
|
539
|
-
checks,
|
|
734
|
+
checks: emittedChecks,
|
|
540
735
|
};
|
|
541
736
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -18,6 +18,32 @@ const MCP_COMPACT_SIGNAL_MAX_CHARS = 360;
|
|
|
18
18
|
const MCP_COMPACT_ID_MAX_CHARS = 120;
|
|
19
19
|
const reviewApi = createOntoReviewCoreApi();
|
|
20
20
|
const reconstructApi = createOntoReconstructCoreApi();
|
|
21
|
+
// Shared advertised JSON-schema fragment for the per-call `llmOverride` property.
|
|
22
|
+
// Kept in sync with PerCallLlmOverrideSchema (settings-chain); tool-surface.test.ts
|
|
23
|
+
// asserts both the zod and advertised surfaces carry it.
|
|
24
|
+
const LLM_OVERRIDE_PROPERTY_SCHEMA = {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
provider: {
|
|
29
|
+
type: "string",
|
|
30
|
+
enum: ["openai", "anthropic", "grok", "lmstudio"],
|
|
31
|
+
},
|
|
32
|
+
auth: { type: "string", enum: ["api_key", "oauth", "local"] },
|
|
33
|
+
model: { type: "string" },
|
|
34
|
+
effort: { type: "string" },
|
|
35
|
+
service_tier: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
// A provider switch needs an explicit model (the switched-in provider has no
|
|
38
|
+
// default model to inherit). The zod boundary enforces this; declaring it here
|
|
39
|
+
// too means a client reading tools/list sees the same contract instead of
|
|
40
|
+
// discovering it from a rejection. Both draft-07 (`if`/`then`) and 2019-09+
|
|
41
|
+
// (`dependentRequired`) spellings are supplied so either validator honors it.
|
|
42
|
+
if: { required: ["provider"] },
|
|
43
|
+
then: { required: ["model"] },
|
|
44
|
+
dependentRequired: { provider: ["model"] },
|
|
45
|
+
description: "Optional per-call LLM override: an ephemeral settings-llm overlay applied to this invocation's dispatch seats (provider switch replaces the actor block; a partial block field-overlays same-provider). Transport (base_url/api_key_env/timeout) stays settings-owned. Omit → settings unchanged (byte-identical default-off).",
|
|
46
|
+
};
|
|
21
47
|
const REVIEW_INPUT_SCHEMA = {
|
|
22
48
|
type: "object",
|
|
23
49
|
additionalProperties: false,
|
|
@@ -97,6 +123,7 @@ const REVIEW_INPUT_SCHEMA = {
|
|
|
97
123
|
type: "number",
|
|
98
124
|
description: "Optional synchronous wait budget in milliseconds. When exceeded after session planning, onto_review returns a running handle and background execution continues; recover via onto_review_read(latest=true). The default is profile-aware (simple 45s, full 25s; override with env ONTO_MCP_REVIEW_RETURN_RUNNING_AFTER_MS or ..._SIMPLE) — most core-axis reviews exceed any host-safe window and return a handle.",
|
|
99
125
|
},
|
|
126
|
+
llmOverride: LLM_OVERRIDE_PROPERTY_SCHEMA,
|
|
100
127
|
},
|
|
101
128
|
};
|
|
102
129
|
const SESSION_INPUT_SCHEMA = {
|
|
@@ -357,10 +384,6 @@ const RECONSTRUCT_INPUT_SCHEMA = {
|
|
|
357
384
|
enum: ["direct_call"],
|
|
358
385
|
description: "Explicit confirmation provider realization. direct_call uses configured llm provider.",
|
|
359
386
|
},
|
|
360
|
-
llmEffort: {
|
|
361
|
-
type: "string",
|
|
362
|
-
description: "Optional reasoning-effort pin applied to both reconstruct actors (live only; mock ignores it).",
|
|
363
|
-
},
|
|
364
387
|
judgeLlmEffort: {
|
|
365
388
|
type: "string",
|
|
366
389
|
description: "Opt-in: run the answer-support judge at a different reasoning effort than the semantic author (live only). Reduces same-model rubber-stamping.",
|
|
@@ -369,6 +392,7 @@ const RECONSTRUCT_INPUT_SCHEMA = {
|
|
|
369
392
|
type: "string",
|
|
370
393
|
description: "Opt-in: swap the answer-support judge MODEL (on the semantic author's provider; live only). An unsupported model degrades to the author model (INV-MODEL-1); a degrade is recorded as a runtime status note.",
|
|
371
394
|
},
|
|
395
|
+
llmOverride: LLM_OVERRIDE_PROPERTY_SCHEMA,
|
|
372
396
|
},
|
|
373
397
|
};
|
|
374
398
|
const RECONSTRUCT_SESSION_INPUT_SCHEMA = {
|
|
@@ -526,12 +550,124 @@ function resolveToolProfile() {
|
|
|
526
550
|
? "simple"
|
|
527
551
|
: "full";
|
|
528
552
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
553
|
+
// Published MCP protocol revisions onto's response shapes conform to, newest
|
|
554
|
+
// first. onto emits additive fields from later revisions — tool annotations
|
|
555
|
+
// (2025-03-26), structuredContent/outputSchema (2025-06-18) — and does not use
|
|
556
|
+
// JSON-RPC batching (removed in 2025-06-18), so it honestly supports the whole
|
|
557
|
+
// range. The optional 2025-11-25 server features (icons, tasks, elicitation)
|
|
558
|
+
// are not advertised and not required for conformance. Negotiation echoes the
|
|
559
|
+
// client's requested version when supported; otherwise it returns the latest.
|
|
560
|
+
//
|
|
561
|
+
// Scope: this negotiator governs the `initialize` handshake used by every stable
|
|
562
|
+
// revision through 2025-11-25. The in-progress draft (RC 2026-07-28) removes
|
|
563
|
+
// `initialize` for a stateless model (per-request `_meta` version +
|
|
564
|
+
// `server/discover`); it is intentionally out of scope here. onto degrades
|
|
565
|
+
// gracefully with RC-aware clients — an unknown `server/discover` returns -32601
|
|
566
|
+
// (Method not found), so the client falls back to this handshake — a path locked
|
|
567
|
+
// by protocol-version.test.ts. Full RC support is a separate, deferred workstream
|
|
568
|
+
// (the draft is not yet ready for consumption).
|
|
569
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
570
|
+
"2025-11-25",
|
|
571
|
+
"2025-06-18",
|
|
572
|
+
"2025-03-26",
|
|
573
|
+
"2024-11-05",
|
|
574
|
+
];
|
|
575
|
+
export const LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
576
|
+
// The floor is the oldest supported revision — the version a client that never
|
|
577
|
+
// completed negotiation is treated as, so it receives the pre-2025 tool surface.
|
|
578
|
+
const FLOOR_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
|
|
579
|
+
export function negotiateProtocolVersion(requested) {
|
|
580
|
+
return typeof requested === "string" &&
|
|
581
|
+
SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
582
|
+
? requested
|
|
583
|
+
: LATEST_PROTOCOL_VERSION;
|
|
584
|
+
}
|
|
585
|
+
// Connection-scoped: a stdio server serves one client per process, so the
|
|
586
|
+
// version negotiated at `initialize` is remembered here and gates which
|
|
587
|
+
// additive tool-definition fields tools/list may emit. It starts at the floor
|
|
588
|
+
// so a client that skips negotiation gets the conservative legacy surface.
|
|
589
|
+
let negotiatedProtocolVersion = FLOOR_PROTOCOL_VERSION;
|
|
590
|
+
export function setNegotiatedProtocolVersion(version) {
|
|
591
|
+
negotiatedProtocolVersion = version;
|
|
592
|
+
}
|
|
593
|
+
// Revisions that introduced each additive tool-definition field. onto emits a
|
|
594
|
+
// field only to clients that negotiated its revision (or later), so an older
|
|
595
|
+
// client — including an older Claude Desktop that negotiates 2024-11-05 —
|
|
596
|
+
// receives byte-identical pre-2025 tool definitions. YYYY-MM-DD strings compare
|
|
597
|
+
// chronologically under lexical ordering.
|
|
598
|
+
const TOOL_ANNOTATIONS_SINCE = "2025-03-26";
|
|
599
|
+
const TOOL_OUTPUT_SCHEMA_SINCE = "2025-06-18";
|
|
600
|
+
// Advisory MCP tool hints (introduced 2025-03-26, carried unchanged into the
|
|
601
|
+
// draft/RC). onto confines every write to `.onto/` and never destroys user
|
|
602
|
+
// sources, so destructiveHint is false throughout. readOnlyHint marks the pure
|
|
603
|
+
// read/list tools; openWorldHint marks the tools that themselves dispatch to
|
|
604
|
+
// external LLM providers (they spend tokens and are non-deterministic). Keyed by
|
|
605
|
+
// OntoToolName for compile-time completeness — a new advertised tool must
|
|
606
|
+
// declare its hints here or the build fails.
|
|
607
|
+
const TOOL_ANNOTATIONS = {
|
|
608
|
+
onto_review: { title: "Run review", readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
609
|
+
onto_prepare_review: { title: "Prepare review session", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
610
|
+
onto_review_continue: { title: "Resume review", readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
611
|
+
onto_review_round: { title: "Get review round (host-orchestrated)", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
612
|
+
onto_review_advance: { title: "Advance review round", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
613
|
+
onto_review_cancel: { title: "Cancel review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
614
|
+
onto_review_read: { title: "Read review session", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
615
|
+
onto_observe_source: { title: "Observe reconstruct sources", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
616
|
+
onto_validate_reconstruct_directive: { title: "Validate reconstruct directive", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
617
|
+
onto_reconstruct: { title: "Reconstruct ontology seed", readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
618
|
+
onto_reconstruct_read: { title: "Read reconstruct session", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
619
|
+
onto_list: { title: "List registry", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
620
|
+
};
|
|
621
|
+
// Declared result schema for tools whose structuredContent shape is stable
|
|
622
|
+
// enough to pin without brittleness. MCP (2025-06-18+, loosened in the draft/RC)
|
|
623
|
+
// requires a tool that declares outputSchema to return conforming
|
|
624
|
+
// structuredContent, so this is intentionally selective. onto_list's three
|
|
625
|
+
// per-kind envelopes are typed and stable; the read/record tools vary by
|
|
626
|
+
// projectionLevel and are deliberately deferred. Parity with the real output is
|
|
627
|
+
// enforced by tool-surface.behavior.test.ts against these declared shapes.
|
|
628
|
+
const TOOL_OUTPUT_SCHEMAS = {
|
|
629
|
+
onto_list: {
|
|
630
|
+
type: "object",
|
|
631
|
+
anyOf: [
|
|
632
|
+
{
|
|
633
|
+
required: ["full", "coreAxis"],
|
|
634
|
+
properties: {
|
|
635
|
+
full: { type: "array", items: { type: "string" } },
|
|
636
|
+
coreAxis: { type: "array", items: { type: "string" } },
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
required: ["domains"],
|
|
641
|
+
properties: { domains: { type: "array", items: { type: "string" } } },
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
required: ["sourceProfiles"],
|
|
645
|
+
properties: { sourceProfiles: { type: "array" } },
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
export function advertisedToolDefinitions(protocolVersion = negotiatedProtocolVersion) {
|
|
651
|
+
const base = resolveToolProfile() === "simple"
|
|
652
|
+
? TOOL_DEFINITIONS.filter((tool) => new Set(OntoSimpleProfileToolNames).has(tool.name))
|
|
653
|
+
: TOOL_DEFINITIONS;
|
|
654
|
+
const withAnnotations = protocolVersion >= TOOL_ANNOTATIONS_SINCE;
|
|
655
|
+
const withOutputSchema = protocolVersion >= TOOL_OUTPUT_SCHEMA_SINCE;
|
|
656
|
+
// Merge additive fields onto a copy — TOOL_DEFINITIONS stays the pristine
|
|
657
|
+
// dispatch source; only tools/list carries them, and only for clients that
|
|
658
|
+
// negotiated the revision that introduced each field. A client below both
|
|
659
|
+
// thresholds receives the definition unchanged.
|
|
660
|
+
return base.map((tool) => {
|
|
661
|
+
const merged = { ...tool };
|
|
662
|
+
if (withAnnotations)
|
|
663
|
+
merged.annotations = TOOL_ANNOTATIONS[tool.name];
|
|
664
|
+
if (withOutputSchema) {
|
|
665
|
+
const outputSchema = TOOL_OUTPUT_SCHEMAS[tool.name];
|
|
666
|
+
if (outputSchema !== undefined)
|
|
667
|
+
merged.outputSchema = outputSchema;
|
|
668
|
+
}
|
|
669
|
+
return merged;
|
|
670
|
+
});
|
|
535
671
|
}
|
|
536
672
|
export const USAGE_GUIDE = `# Using onto via MCP
|
|
537
673
|
|
|
@@ -611,6 +747,24 @@ Discover material profiles with \`onto_list\` (kind="source_profiles").
|
|
|
611
747
|
they are presentation guidance, not operating instructions.
|
|
612
748
|
- onto writes only under \`{projectRoot}/.onto/\`; it never mutates your sources.
|
|
613
749
|
`;
|
|
750
|
+
// Server-level orientation returned in the `initialize` result so a host LLM
|
|
751
|
+
// gets the big picture on connect. Deliberately a short summary plus a pointer:
|
|
752
|
+
// the durable, detailed authority is the `onto://usage` resource, which is why
|
|
753
|
+
// this stays initialize-era-only. The draft/RC drops the initialize handshake,
|
|
754
|
+
// so orientation there falls back to that same resource — no detail is stranded
|
|
755
|
+
// in this field.
|
|
756
|
+
const SERVER_INSTRUCTIONS = [
|
|
757
|
+
"onto is an MCP-native ontology-as-code runtime with two paths: `review`",
|
|
758
|
+
"(structured, context-isolated multi-lens review of a target) and `reconstruct`",
|
|
759
|
+
"(derive a bounded, actionable ontology seed from real sources). The runtime",
|
|
760
|
+
"owns every artifact and validation gate; tool results put structured data in",
|
|
761
|
+
"structuredContent and carry llmPresentation prompts for user-facing",
|
|
762
|
+
"explanation. Both paths execute real LLM work and FAIL LOUD without a provider",
|
|
763
|
+
"configured in .onto/settings.json (or ~/.onto/settings.json). review is",
|
|
764
|
+
"long-running — poll onto_review_read until it is no longer running. Read the",
|
|
765
|
+
"`onto://usage` resource for provider setup and the full workflows before first",
|
|
766
|
+
"use.",
|
|
767
|
+
].join(" ");
|
|
614
768
|
const RESOURCE_DEFINITIONS = [
|
|
615
769
|
{
|
|
616
770
|
uri: "onto://usage",
|
|
@@ -697,6 +851,9 @@ function toReviewRequest(input) {
|
|
|
697
851
|
...(parsed.confirmValueAlignment !== undefined
|
|
698
852
|
? { confirmValueAlignment: parsed.confirmValueAlignment }
|
|
699
853
|
: {}),
|
|
854
|
+
...(parsed.llmOverride !== undefined
|
|
855
|
+
? { llmOverride: parsed.llmOverride }
|
|
856
|
+
: {}),
|
|
700
857
|
};
|
|
701
858
|
return request;
|
|
702
859
|
}
|
|
@@ -1537,9 +1694,9 @@ export async function callTool(name, args, options = {}) {
|
|
|
1537
1694
|
...(parsed.resumeMode !== undefined ? { resumeMode: parsed.resumeMode } : {}),
|
|
1538
1695
|
semanticAuthorRealization: parsed.semanticAuthorRealization,
|
|
1539
1696
|
confirmationProviderRealization: parsed.confirmationProviderRealization,
|
|
1540
|
-
...(parsed.llmEffort !== undefined ? { llmEffort: parsed.llmEffort } : {}),
|
|
1541
1697
|
...(parsed.judgeLlmEffort !== undefined ? { judgeLlmEffort: parsed.judgeLlmEffort } : {}),
|
|
1542
1698
|
...(parsed.judgeModel !== undefined ? { judgeModel: parsed.judgeModel } : {}),
|
|
1699
|
+
...(parsed.llmOverride !== undefined ? { llmOverride: parsed.llmOverride } : {}),
|
|
1543
1700
|
...(sessionRoot ? { sessionRoot } : {}),
|
|
1544
1701
|
...(profilesRoot ? { profilesRoot } : {}),
|
|
1545
1702
|
...(filesystemAllowedRoots ? { filesystemAllowedRoots } : {}),
|
|
@@ -1597,20 +1754,26 @@ function jsonRpcError(id, code, message) {
|
|
|
1597
1754
|
error: { code, message },
|
|
1598
1755
|
};
|
|
1599
1756
|
}
|
|
1600
|
-
async function handleRequest(message) {
|
|
1757
|
+
export async function handleRequest(message) {
|
|
1601
1758
|
if (!message.id && message.method?.startsWith("notifications/")) {
|
|
1602
1759
|
return null;
|
|
1603
1760
|
}
|
|
1604
1761
|
switch (message.method) {
|
|
1605
|
-
case "initialize":
|
|
1762
|
+
case "initialize": {
|
|
1763
|
+
const params = message.params;
|
|
1764
|
+
const negotiated = negotiateProtocolVersion(params?.protocolVersion);
|
|
1765
|
+
// Remember it so tools/list gates its additive fields on the same version.
|
|
1766
|
+
setNegotiatedProtocolVersion(negotiated);
|
|
1606
1767
|
return jsonRpcResult(message.id, {
|
|
1607
|
-
protocolVersion:
|
|
1768
|
+
protocolVersion: negotiated,
|
|
1608
1769
|
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
1609
1770
|
serverInfo: {
|
|
1610
1771
|
name: "onto-mcp",
|
|
1611
1772
|
version: await readPackageVersion(),
|
|
1612
1773
|
},
|
|
1774
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
1613
1775
|
});
|
|
1776
|
+
}
|
|
1614
1777
|
case "ping":
|
|
1615
1778
|
return jsonRpcResult(message.id, {});
|
|
1616
1779
|
case "tools/list":
|