Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
15 changes: 15 additions & 0 deletions workers/baseline/MANIFEST.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
2 changes: 1 addition & 1 deletion workers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oddkit-mcp-worker",
"version": "0.26.0",
"version": "0.27.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions workers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
Expand All @@ -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,
Expand Down Expand Up @@ -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 },
},
Expand Down Expand Up @@ -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'."),
Comment thread
cursor[bot] marked this conversation as resolved.
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
Expand All @@ -390,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 },
},
Expand Down Expand Up @@ -438,6 +443,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,
Expand Down
61 changes: 51 additions & 10 deletions workers/src/orchestrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1348,9 +1357,10 @@ async function runSearch(
state?: OddkitState,
includeMetadata?: boolean,
resolvedGrouping: ResultGrouping = "merged",
searchScope: SearchScope = "merged",
): Promise<ActionResult> {
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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<ActionResult> {
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);
Expand Down Expand Up @@ -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:`,
Expand Down Expand Up @@ -2352,7 +2375,8 @@ async function runCatalog(
const result: Record<string, unknown> = {
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),
};
Expand All @@ -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,
Expand All @@ -2389,9 +2416,10 @@ async function runPreflight(
knowledgeBaseUrl?: string,
state?: OddkitState,
resolvedGrouping: ResultGrouping = "merged",
searchScope: SearchScope = "merged",
): Promise<ActionResult> {
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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -3333,14 +3364,24 @@ const VALID_ACTIONS = [
] as const;

export async function handleUnifiedAction(params: UnifiedParams): Promise<OddkitEnvelope> {
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).
// When KB is unset, default to "merged" (no behavior change).
const resolvedGrouping: ResultGrouping =
result_grouping ?? (knowledge_base_url ? "overlay_first" : "merged");

// Search-Corpus Boundary (E0008.5): when knowledge_base_url is set, the
// search corpus defaults to overlay + required-baseline only. 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 scope is
// forced to "merged" (the baseline IS the canon — there is nothing to
// scope away). Authority: klappy://canon/constraints/core-governance-baseline
// §"Search-Corpus Boundary".
const resolvedScope: SearchScope =
knowledge_base_url && !include_full_baseline ? "kb_with_required_baseline" : "merged";

if (!VALID_ACTIONS.includes(action as (typeof VALID_ACTIONS)[number])) {
return {
action: "error",
Expand Down Expand Up @@ -3371,7 +3412,7 @@ export async function handleUnifiedAction(params: UnifiedParams): Promise<Oddkit
result = await runEncodeAction(input, context, fetcher, knowledge_base_url, state);
break;
case "search":
result = await runSearch(input, fetcher, knowledge_base_url, state, include_metadata, resolvedGrouping);
result = await runSearch(input, fetcher, knowledge_base_url, state, include_metadata, resolvedGrouping, resolvedScope);
break;
case "get":
result = await runGet(input, fetcher, knowledge_base_url, state, include_metadata, section);
Expand All @@ -3383,13 +3424,13 @@ export async function handleUnifiedAction(params: UnifiedParams): Promise<Oddkit
result = await runAudit(input, fetcher, knowledge_base_url, state);
break;
case "catalog":
result = await runCatalog(fetcher, knowledge_base_url, state, { sort_by, limit, offset, filter_epoch });
result = await runCatalog(fetcher, knowledge_base_url, state, { sort_by, limit, offset, filter_epoch }, resolvedScope);
break;
case "validate":
result = await runValidate(input, state);
break;
case "preflight":
result = await runPreflight(input, fetcher, knowledge_base_url, state, resolvedGrouping);
result = await runPreflight(input, fetcher, knowledge_base_url, state, resolvedGrouping, resolvedScope);
break;
case "version":
result = runVersion(env);
Expand All @@ -3398,7 +3439,7 @@ export async function handleUnifiedAction(params: UnifiedParams): Promise<Oddkit
result = await runCleanupStorage(fetcher, knowledge_base_url);
break;
default:
result = await runSearch(input, fetcher, knowledge_base_url, state, undefined, resolvedGrouping);
result = await runSearch(input, fetcher, knowledge_base_url, state, undefined, resolvedGrouping, resolvedScope);
}

// Inject trace into debug envelope (E0008.1)
Expand Down
Loading
Loading