From b533bfccb7c2e63201acb748005a2db5830af25c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 13:56:33 +0000 Subject: [PATCH 1/2] feat(kb-scope): default-scope search corpus to overlay + required-baseline (E0008.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When knowledge_base_url is set, the search corpus now defaults to overlay + required-baseline-only (six files from canon's core-governance-baseline §'Required in Baseline'). Callers opt in to the legacy merged corpus via include_full_baseline: true. When knowledge_base_url is unset, the parameter is a no-op and behavior is unchanged. Affected actions: search, catalog, preflight. orient, get, challenge, validate, gate, encode, audit are unchanged — they read governance via the per-file resolver, not the search index. Implementation: - workers/baseline/MANIFEST.json: required-baseline manifest (six paths, Build-Time Invariant #4). - workers/src/zip-baseline-fetcher.ts: - new SearchScope type - exported REQUIRED_BASELINE_PATHS Set (synced with MANIFEST.json) - getIndex(knowledgeBaseUrl, scope?) filters baseline entries - cache key includes effective scope so scoped/merged caches do not poison each other - BaselineIndex.search_scope and stats.baseline_indexed surfaced - workers/src/orchestrate.ts: - UnifiedParams.include_full_baseline added - resolvedScope derived at dispatch - threaded into runSearch/runCatalog/runPreflight - search/catalog/preflight debug envelopes emit search_scope, overlay_doc_count, baseline_doc_count - catalog assistant_text and result reflect the scoped count and disclose baseline_total - workers/src/index.ts: include_full_baseline added to the unified oddkit schema and to oddkit_search/oddkit_catalog/oddkit_preflight. - workers/test/canon-tool-envelope.smoke.mjs: live-smoke assertions for the new envelope fields, scoped default behavior, opt-in to merged, leak prevention against ptxprint-mcp KB, and no-KB no-op. - Version bump 0.26.0 -> 0.27.0 per governance-change-discipline.md. Authority: klappy://canon/constraints/core-governance-baseline §'Search-Corpus Boundary' (klappy/klappy.dev #155, merged 2026-04-29). --- package.json | 2 +- workers/baseline/MANIFEST.json | 15 ++++ workers/package.json | 2 +- workers/src/index.ts | 5 ++ workers/src/orchestrate.ts | 61 +++++++++++--- workers/src/zip-baseline-fetcher.ts | 71 +++++++++++++++-- workers/test/canon-tool-envelope.smoke.mjs | 93 ++++++++++++++++++++++ 7 files changed, 232 insertions(+), 17 deletions(-) create mode 100644 workers/baseline/MANIFEST.json diff --git a/package.json b/package.json index 48308a0..6b338f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oddkit", - "version": "0.26.0", + "version": "0.27.0", "description": "Agent-first CLI for ODD-governed repos. Epistemic terrain rendering with portable baseline.", "type": "module", "bin": { diff --git a/workers/baseline/MANIFEST.json b/workers/baseline/MANIFEST.json new file mode 100644 index 0000000..13ab915 --- /dev/null +++ b/workers/baseline/MANIFEST.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://klappy.dev/canon/constraints/core-governance-baseline", + "comment": "Required-baseline manifest. The six files every knowledge-base-driven oddkit tool needs to function. Canon source: klappy://canon/constraints/core-governance-baseline §'Required in Baseline'. When knowledge_base_url is set and include_full_baseline is unset/false, the search corpus indexes the project KB plus only these files from the default baseline.", + "version": 1, + "epoch": "E0008.5", + "canon_uri": "klappy://canon/constraints/core-governance-baseline", + "required_paths": [ + "canon/values/orientation.md", + "canon/values/axioms.md", + "canon/meta/writing-canon.md", + "canon/constraints/definition-of-done.md", + "canon/constraints/telemetry-governance.md", + "odd/challenge/stakes-calibration.md" + ] +} diff --git a/workers/package.json b/workers/package.json index 09aa600..473c927 100644 --- a/workers/package.json +++ b/workers/package.json @@ -1,6 +1,6 @@ { "name": "oddkit-mcp-worker", - "version": "0.26.0", + "version": "0.27.0", "private": true, "type": "module", "scripts": { diff --git a/workers/src/index.ts b/workers/src/index.ts index 15150b9..94fb3fe 100644 --- a/workers/src/index.ts +++ b/workers/src/index.ts @@ -222,6 +222,7 @@ Use when: knowledge_base_url: z.string().optional().describe("Optional GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier rather than silently substituting from the default knowledge base."), result_grouping: z.enum(["merged", "overlay_first", "grouped"]).optional().describe("For action='search' or 'preflight': controls how overlay (knowledge_base) and baseline results are ordered. 'merged' = pure score order (default when knowledge_base_url unset). 'overlay_first' = overlay docs ranked above baseline (default when knowledge_base_url set). 'grouped' = separate overlay_hits/baseline_hits arrays in response."), include_metadata: z.boolean().optional().describe("When true, search/get responses include a metadata object with full parsed frontmatter. Default: false."), + include_full_baseline: z.boolean().optional().describe("Search-Corpus Boundary opt-in (E0008.5). When knowledge_base_url is set, the search corpus defaults to overlay + required-baseline only. Pass true to restore the legacy merged corpus (overlay + full baseline). When knowledge_base_url is unset, this parameter is a no-op. Authority: klappy://canon/constraints/core-governance-baseline §'Search-Corpus Boundary'."), section: z.string().optional().describe("For action='get': extract only the named ## section from the document. Returns section content or available sections if not found."), sort_by: z.enum(["date", "path"]).optional().describe("For action='catalog': sort articles. 'date' returns newest first (requires frontmatter). 'path' returns all docs alphabetically, including undated."), limit: z.number().min(1).max(500).optional().describe("For action='catalog': max articles to return when sort_by is provided. Default: 10, max: 500."), @@ -244,6 +245,7 @@ Use when: knowledge_base_url: args.knowledge_base_url, result_grouping: args.result_grouping, include_metadata: args.include_metadata, + include_full_baseline: args.include_full_baseline, section: args.section, sort_by: args.sort_by, limit: args.limit, @@ -325,6 +327,7 @@ Use when: knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."), result_grouping: z.enum(["merged", "overlay_first", "grouped"]).optional().describe("Controls how overlay (knowledge_base) and baseline results are ordered. 'merged' = pure score order (default when knowledge_base_url unset). 'overlay_first' = overlay docs ranked above baseline (default when knowledge_base_url set). 'grouped' = separate overlay_hits/baseline_hits arrays in response."), include_metadata: z.boolean().optional().describe("When true, each hit includes a metadata object with full parsed frontmatter. Default: false."), + include_full_baseline: z.boolean().optional().describe("Search-Corpus Boundary opt-in (E0008.5). When knowledge_base_url is set, the search corpus defaults to overlay + required-baseline only. Pass true to restore the legacy merged corpus (overlay + full baseline). When knowledge_base_url is unset, this is a no-op. Authority: klappy://canon/constraints/core-governance-baseline §'Search-Corpus Boundary'."), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, @@ -370,6 +373,7 @@ Use when: limit: z.number().min(1).max(500).optional().describe("Max articles to return when sort_by is provided. Default: 10, max: 500."), offset: z.number().min(0).optional().describe("Skip this many articles before returning results. Use with limit for pagination. Default: 0."), filter_epoch: z.string().optional().describe("Filter to articles with this epoch value in frontmatter (e.g. 'E0007')."), + include_full_baseline: z.boolean().optional().describe("Search-Corpus Boundary opt-in (E0008.5). When knowledge_base_url is set, the catalog reflects overlay + required-baseline only. Pass true to restore the legacy merged catalog (overlay + full baseline). Authority: klappy://canon/constraints/core-governance-baseline §'Search-Corpus Boundary'."), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, @@ -438,6 +442,7 @@ Use when: knowledge_base_url: args.knowledge_base_url as string | undefined, result_grouping: args.result_grouping as "merged" | "overlay_first" | "grouped" | undefined, include_metadata: args.include_metadata as boolean | undefined, + include_full_baseline: args.include_full_baseline as boolean | undefined, section: args.section as string | undefined, sort_by: args.sort_by as string | undefined, limit: args.limit as number | undefined, diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts index 2ca7440..08faf13 100644 --- a/workers/src/orchestrate.ts +++ b/workers/src/orchestrate.ts @@ -15,6 +15,7 @@ import { type Env, type BaselineIndex, type IndexEntry, + type SearchScope, type SectionResult, } from "./zip-baseline-fetcher"; import { buildBM25Index, searchBM25, tokenize, type BM25Index } from "./bm25"; @@ -235,6 +236,14 @@ export interface UnifiedParams { knowledge_base_url?: string; result_grouping?: ResultGrouping; include_metadata?: boolean; + /** + * Search-Corpus Boundary opt-in (E0008.5). When `knowledge_base_url` is set, + * the search corpus defaults to overlay + required-baseline-manifest only. + * Set this to true to restore the legacy merged corpus (overlay + full + * baseline). When `knowledge_base_url` is unset, this parameter is a no-op. + * Authority: klappy://canon/constraints/core-governance-baseline §"Search-Corpus Boundary". + */ + include_full_baseline?: boolean; section?: string; sort_by?: string; limit?: number; @@ -1348,9 +1357,10 @@ async function runSearch( state?: OddkitState, includeMetadata?: boolean, resolvedGrouping: ResultGrouping = "merged", + searchScope: SearchScope = "merged", ): Promise { const startMs = Date.now(); - const index = await fetcher.getIndex(knowledgeBaseUrl); + const index = await fetcher.getIndex(knowledgeBaseUrl, searchScope); const bm25 = getBM25Index(index.entries); // Issue #150 fix-forward: when grouping is active, retrieve a wider candidate @@ -1412,6 +1422,9 @@ async function runSearch( baseline_url: index.baseline_url, knowledge_base_url: knowledgeBaseUrl, search_index_size: bm25.N, + search_scope: index.search_scope, + overlay_doc_count: index.stats.canon, + baseline_doc_count: index.stats.baseline_indexed ?? index.stats.baseline, result_grouping: resolvedGrouping, duration_ms: Date.now() - startMs, generated_at: new Date().toISOString(), @@ -1499,6 +1512,9 @@ async function runSearch( baseline_url: index.baseline_url, knowledge_base_url: knowledgeBaseUrl, search_index_size: bm25.N, + search_scope: index.search_scope, + overlay_doc_count: index.stats.canon, + baseline_doc_count: index.stats.baseline_indexed ?? index.stats.baseline, result_grouping: resolvedGrouping, duration_ms: Date.now() - startMs, generated_at: new Date().toISOString(), @@ -2247,9 +2263,10 @@ async function runCatalog( knowledgeBaseUrl?: string, state?: OddkitState, options?: { sort_by?: string; limit?: number; offset?: number; filter_epoch?: string }, + searchScope: SearchScope = "merged", ): Promise { const startMs = Date.now(); - const index = await fetcher.getIndex(knowledgeBaseUrl); + const index = await fetcher.getIndex(knowledgeBaseUrl, searchScope); const { sort_by, limit: rawLimit, offset: rawOffset, filter_epoch } = options || {}; const effectiveLimit = Math.min(Math.max(rawLimit || 10, 1), 500); const effectiveOffset = Math.max(rawOffset || 0, 0); @@ -2315,10 +2332,16 @@ async function runCatalog( })); } + const baselineCount = index.stats.baseline_indexed ?? index.stats.baseline; + const scopeNote = + index.search_scope === "kb_with_required_baseline" + ? ` [scoped: required-baseline only; pass include_full_baseline=true to merge]` + : ""; + const assistantTextParts = [ `ODD Documentation Catalog`, ``, - `Total: ${index.stats.total} docs (${index.stats.canon} canon, ${index.stats.baseline} baseline)`, + `Total: ${index.stats.total} docs (${index.stats.canon} canon, ${baselineCount} baseline)${scopeNote}`, knowledgeBaseUrl ? `Canon override: ${knowledgeBaseUrl}` : "", ``, `Start here:`, @@ -2352,7 +2375,8 @@ async function runCatalog( const result: Record = { total: index.stats.total, canon: index.stats.canon, - baseline: index.stats.baseline, + baseline: baselineCount, + baseline_total: index.stats.baseline, categories: Object.keys(byTag), start_here: startHere.map((e) => e.path), }; @@ -2376,6 +2400,9 @@ async function runCatalog( debug: { knowledge_base_url: knowledgeBaseUrl, baseline_url: index.baseline_url, + search_scope: index.search_scope, + overlay_doc_count: index.stats.canon, + baseline_doc_count: index.stats.baseline_indexed ?? index.stats.baseline, generated_at: new Date().toISOString(), // response time — consistent with all other handlers index_built_at: index.generated_at, // preserve cache-freshness diagnostic under accurate name duration_ms: Date.now() - startMs, @@ -2389,9 +2416,10 @@ async function runPreflight( knowledgeBaseUrl?: string, state?: OddkitState, resolvedGrouping: ResultGrouping = "merged", + searchScope: SearchScope = "merged", ): Promise { const startMs = Date.now(); - const index = await fetcher.getIndex(knowledgeBaseUrl); + const index = await fetcher.getIndex(knowledgeBaseUrl, searchScope); const topic = message.replace(/^preflight:\s*/i, "").trim(); // Score all entries, then apply partition before slicing @@ -2453,6 +2481,9 @@ async function runPreflight( debug: { docs_considered: index.entries.length, knowledge_base_url: knowledgeBaseUrl, + search_scope: index.search_scope, + overlay_doc_count: index.stats.canon, + baseline_doc_count: index.stats.baseline_indexed ?? index.stats.baseline, result_grouping: resolvedGrouping, duration_ms: Date.now() - startMs, generated_at: new Date().toISOString(), @@ -3333,7 +3364,7 @@ const VALID_ACTIONS = [ ] as const; export async function handleUnifiedAction(params: UnifiedParams): Promise { - const { action, input, context, mode, knowledge_base_url, result_grouping, include_metadata, section, sort_by, limit, offset, filter_epoch, state, env, tracer } = params; + const { action, input, context, mode, knowledge_base_url, result_grouping, include_metadata, include_full_baseline, section, sort_by, limit, offset, filter_epoch, state, env, tracer } = params; // Conditional default: when knowledge_base_url is set and caller didn't // specify result_grouping, default to "overlay_first" (the fix for #150). @@ -3341,6 +3372,16 @@ export async function handleUnifiedAction(params: UnifiedParams): Promise = new Set([ + "canon/values/orientation.md", + "canon/values/axioms.md", + "canon/meta/writing-canon.md", + "canon/constraints/definition-of-done.md", + "canon/constraints/telemetry-governance.md", + "odd/challenge/stakes-calibration.md", +]); + export interface Env { DEFAULT_KNOWLEDGE_BASE_URL: string; ODDKIT_VERSION: string; @@ -93,8 +122,20 @@ export interface BaselineIndex { stats: { total: number; canon: number; + /** Total baseline files discovered in the default baseline repo. */ baseline: number; + /** + * Baseline files actually included in the search index, after scope filtering. + * Equal to `baseline` when scope is "merged"; equal to the number of + * required-baseline paths present when scope is "kb_with_required_baseline". + */ + baseline_indexed?: number; }; + /** + * Search corpus scope under which this index was built (E0008.5). + * Absent on indexes built before the field was introduced. + */ + search_scope?: SearchScope; commit_sha?: string; canon_commit_sha?: string; } @@ -881,11 +922,16 @@ export class KnowledgeBaseFetcher { * removing one KV read). Content-addressed by SHA — no TTL needed * for correctness. Module cache uses 5-min TTL for freshness. */ - async getIndex(knowledgeBaseUrl?: string): Promise { + async getIndex(knowledgeBaseUrl?: string, scope: SearchScope = "merged"): Promise { const baselineRepoUrl = "https://github.com/klappy/klappy.dev"; + // Effective scope: scoping only matters when an overlay is set. + // When no knowledge_base_url, the baseline IS the canon — there is + // nothing to scope away — so force "merged" regardless of caller intent. + const effectiveScope: SearchScope = knowledgeBaseUrl ? scope : "merged"; + // Step 0: Module-level memory cache (0ms, 5-min TTL) - const expectedKey = `v${INDEX_VERSION}/${getCacheKey(knowledgeBaseUrl || "default")}`; + const expectedKey = `v${INDEX_VERSION}/${getCacheKey(knowledgeBaseUrl || "default")}_scope-${effectiveScope}`; if (cachedIndex && cachedIndexKey === expectedKey && Date.now() - indexCachedAt < MODULE_CACHE_TTL_MS) { this.tracer?.recordFetch({ url: `memory://index/${expectedKey}`, duration_ms: 0, cached: true }); return cachedIndex; @@ -896,8 +942,10 @@ export class KnowledgeBaseFetcher { const canonRef = knowledgeBaseUrl ? extractBranchRef(knowledgeBaseUrl) : undefined; const canonSha = knowledgeBaseUrl ? await this.getLatestCommitSha(knowledgeBaseUrl, canonRef) : undefined; - // Content-addressed cache key: SHA + version - const shaKey = `${baselineSha || "unknown"}_${canonSha || "none"}`; + // Content-addressed cache key: SHA + version + scope. + // Scope is part of the key so a scoped index and a merged index against + // the same KB do not poison each other's cached form. + const shaKey = `${baselineSha || "unknown"}_${canonSha || "none"}_${effectiveScope}`; const cacheKey = `index/v${INDEX_VERSION}/${getCacheKey(knowledgeBaseUrl || "default")}_${shaKey}`; // Step 2: Cache API (~1ms edge read) — cacheGet records the cf-cache:// fetch. @@ -973,8 +1021,19 @@ export class KnowledgeBaseFetcher { canonEntries = await this.buildIndexFromRepo(knowledgeBaseUrl, "canon", skipCache); } + // Search-Corpus Boundary (E0008.5): when scoped, restrict baseline entries + // to only the required-baseline manifest before arbitration. Per + // klappy://canon/constraints/core-governance-baseline §"Search-Corpus + // Boundary", this preserves required-baseline as the floor while excluding + // co-located canon-only content (writings/, apocrypha/, odd/ledger/, etc.) + // that would otherwise outrank the project KB's own canon in BM25. + const scopedBaselineEntries = + effectiveScope === "kb_with_required_baseline" + ? baselineEntries.filter((e) => REQUIRED_BASELINE_PATHS.has(e.path)) + : baselineEntries; + // Arbitrate — canon overrides baseline - const allEntries = this.arbitrateEntries(canonEntries, baselineEntries); + const allEntries = this.arbitrateEntries(canonEntries, scopedBaselineEntries); const index: BaselineIndex = { version: INDEX_VERSION, @@ -986,7 +1045,9 @@ export class KnowledgeBaseFetcher { total: allEntries.length, canon: canonEntries.length, baseline: baselineEntries.length, + baseline_indexed: scopedBaselineEntries.length, }, + search_scope: effectiveScope, commit_sha: baselineSha || undefined, canon_commit_sha: canonSha || undefined, }; diff --git a/workers/test/canon-tool-envelope.smoke.mjs b/workers/test/canon-tool-envelope.smoke.mjs index f46c828..be7b00e 100644 --- a/workers/test/canon-tool-envelope.smoke.mjs +++ b/workers/test/canon-tool-envelope.smoke.mjs @@ -717,6 +717,99 @@ async function run() { typeof catalogResult.debug?.index_built_at === "string", `got: ${catalogResult.debug?.index_built_at}`); + // ── Search-Corpus Boundary (E0008.5) ─────────────────────────────────────── + // Asserts that when knowledge_base_url is set, the default scope filters the + // baseline to required-baseline only; that include_full_baseline=true + // restores the merged corpus; and that envelope fields surface scope. + // Authority: klappy://canon/constraints/core-governance-baseline §"Search-Corpus Boundary" + console.log(`\n─── Search-Corpus Boundary: catalog default scope ───`); + const PTXPRINT_KB = "https://github.com/klappy/ptxprint-mcp"; + const scopedCatalog = await callTool("oddkit_catalog", { knowledge_base_url: PTXPRINT_KB }); + expectFullEnvelope("oddkit_catalog (scoped)", scopedCatalog); + + ok(`scoped catalog: debug.search_scope === "kb_with_required_baseline"`, + scopedCatalog.debug?.search_scope === "kb_with_required_baseline", + `got: ${scopedCatalog.debug?.search_scope}`); + ok(`scoped catalog: debug.overlay_doc_count present and > 0`, + typeof scopedCatalog.debug?.overlay_doc_count === "number" && scopedCatalog.debug.overlay_doc_count > 0, + `got: ${scopedCatalog.debug?.overlay_doc_count}`); + ok(`scoped catalog: debug.baseline_doc_count <= 6 (required-baseline ceiling)`, + typeof scopedCatalog.debug?.baseline_doc_count === "number" && scopedCatalog.debug.baseline_doc_count <= 6, + `got: ${scopedCatalog.debug?.baseline_doc_count}`); + ok(`scoped catalog: result.baseline reflects scoped count (= debug.baseline_doc_count)`, + typeof scopedCatalog.result?.baseline === "number" && + scopedCatalog.result.baseline === scopedCatalog.debug?.baseline_doc_count, + `result.baseline=${scopedCatalog.result?.baseline} debug.baseline_doc_count=${scopedCatalog.debug?.baseline_doc_count}`); + ok(`scoped catalog: result.baseline_total >= result.baseline (full repo count disclosed)`, + typeof scopedCatalog.result?.baseline_total === "number" && + scopedCatalog.result.baseline_total >= scopedCatalog.result.baseline, + `baseline_total=${scopedCatalog.result?.baseline_total} baseline=${scopedCatalog.result?.baseline}`); + + console.log(`\n─── Search-Corpus Boundary: catalog include_full_baseline opt-in ───`); + const mergedCatalog = await callTool("oddkit_catalog", { + knowledge_base_url: PTXPRINT_KB, + include_full_baseline: true, + }); + expectFullEnvelope("oddkit_catalog (merged)", mergedCatalog); + + ok(`merged catalog: debug.search_scope === "merged"`, + mergedCatalog.debug?.search_scope === "merged", + `got: ${mergedCatalog.debug?.search_scope}`); + ok(`merged catalog: baseline_doc_count is full baseline (much greater than scoped)`, + typeof mergedCatalog.debug?.baseline_doc_count === "number" && + mergedCatalog.debug.baseline_doc_count > (scopedCatalog.debug?.baseline_doc_count ?? 0) + 50, + `merged=${mergedCatalog.debug?.baseline_doc_count} scoped=${scopedCatalog.debug?.baseline_doc_count}`); + + console.log(`\n─── Search-Corpus Boundary: search default scope ───`); + // Negative-control query: this term lives only in klappy.dev's canon, not + // ptxprint-mcp's. Under scoped default, klappy.dev hits must NOT surface. + const scopedSearch = await callTool("oddkit_search", { + input: "release validation gate Bugbot Sonnet validator", + knowledge_base_url: PTXPRINT_KB, + }); + expectFullEnvelope("oddkit_search (scoped, klappy.dev-only term)", scopedSearch); + + ok(`scoped search: debug.search_scope === "kb_with_required_baseline"`, + scopedSearch.debug?.search_scope === "kb_with_required_baseline", + `got: ${scopedSearch.debug?.search_scope}`); + ok(`scoped search: debug.search_index_size <= overlay_count + 6`, + typeof scopedSearch.debug?.search_index_size === "number" && + typeof scopedSearch.debug?.overlay_doc_count === "number" && + scopedSearch.debug.search_index_size <= scopedSearch.debug.overlay_doc_count + 6, + `index_size=${scopedSearch.debug?.search_index_size} overlay=${scopedSearch.debug?.overlay_doc_count}`); + // Klappy.dev release-validation-gate doc must NOT appear in scoped hits. + const scopedHitPaths = (scopedSearch.result?.hits || []).map((h) => h.path || ""); + const leakedReleaseGate = scopedHitPaths.some((p) => + p.includes("canon/constraints/release-validation-gate"), + ); + ok(`scoped search: klappy.dev-only doc 'release-validation-gate' does NOT leak into hits`, + !leakedReleaseGate, + `leak detected in: ${scopedHitPaths.join(", ")}`); + + console.log(`\n─── Search-Corpus Boundary: search include_full_baseline opt-in ───`); + const mergedSearch = await callTool("oddkit_search", { + input: "release validation gate Bugbot Sonnet validator", + knowledge_base_url: PTXPRINT_KB, + include_full_baseline: true, + }); + expectFullEnvelope("oddkit_search (merged)", mergedSearch); + + ok(`merged search: debug.search_scope === "merged"`, + mergedSearch.debug?.search_scope === "merged", + `got: ${mergedSearch.debug?.search_scope}`); + ok(`merged search: search_index_size strictly greater than scoped`, + typeof mergedSearch.debug?.search_index_size === "number" && + mergedSearch.debug.search_index_size > (scopedSearch.debug?.search_index_size ?? 0), + `merged=${mergedSearch.debug?.search_index_size} scoped=${scopedSearch.debug?.search_index_size}`); + + console.log(`\n─── Search-Corpus Boundary: search no-KB is no-op ───`); + // When knowledge_base_url is unset, the parameter must be a no-op and scope + // must be "merged" (the baseline IS the canon). + const defaultSearch = await callTool("oddkit_search", { input: "axioms" }); + ok(`default search (no KB): debug.search_scope === "merged"`, + defaultSearch.debug?.search_scope === "merged", + `got: ${defaultSearch.debug?.search_scope}`); + console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed === 0 ? 0 : 1); } From 8e88a9f837714b678c235bf8c5c7dd4bae91f019 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Apr 2026 14:06:32 +0000 Subject: [PATCH 2/2] fix(mcp): add include_full_baseline to oddkit_preflight tool schema --- workers/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/workers/src/index.ts b/workers/src/index.ts index 94fb3fe..5568c45 100644 --- a/workers/src/index.ts +++ b/workers/src/index.ts @@ -394,6 +394,7 @@ Use when: input: z.string().describe("Description of what you're about to implement."), knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."), result_grouping: z.enum(["merged", "overlay_first", "grouped"]).optional().describe("Controls how overlay (knowledge_base) and baseline start_here results are ordered. 'merged' = pure score order (default when knowledge_base_url unset). 'overlay_first' = overlay docs ranked above baseline (default when knowledge_base_url set). 'grouped' = separate start_here_overlay/start_here_baseline arrays."), + include_full_baseline: z.boolean().optional().describe("Search-Corpus Boundary opt-in (E0008.5). When knowledge_base_url is set, the preflight corpus defaults to overlay + required-baseline only. Pass true to restore the legacy merged corpus (overlay + full baseline). When knowledge_base_url is unset, this is a no-op. Authority: klappy://canon/constraints/core-governance-baseline §'Search-Corpus Boundary'."), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, },