From deeb27d87b7c8e626cffff09661c344f1748cfc8 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Wed, 7 Oct 2026 16:16:57 +0000 Subject: [PATCH] fix(context): require a name or trigger anchor for implicit admission --- docs/design/context-profile-delivery.md | 2 +- docs/design/context-profile-delivery.tdd.md | 9 +++++++++ scripts/lib/context-retrieval.js | 2 ++ scripts/lib/context-selection.js | 9 ++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/design/context-profile-delivery.md b/docs/design/context-profile-delivery.md index 82d6151fe..57d985fcc 100644 --- a/docs/design/context-profile-delivery.md +++ b/docs/design/context-profile-delivery.md @@ -52,7 +52,7 @@ A task input contains caller-assigned `sessionId`, `taskId`, positive integer `r } ``` -Auto uses explicit user IDs first, then a completed pinned decision, an unambiguous ranked match, one cited skill name, or admitted agent-proposed IDs. Ambiguous free text shortlists up to five candidates for a bounded proposal. Manual uses explicit IDs; suggest emits a proposal without bodies. `--load` returns selected UTF-8 instructions and declared required resources, capped at 32,000 bytes across at most eight skills. `--task-input -` accepts one UTF-8 JSON object on standard input, capped at 65,536 bytes. These byte caps are output and transport bounds, not native tokenizer results. +Auto uses explicit user IDs first, then a completed pinned decision, an unambiguous ranked match anchored by a term in the skill name or curated triggers, one cited skill name, or admitted agent-proposed IDs. Ambiguous free text shortlists up to five candidates for a bounded proposal. Manual uses explicit IDs; suggest emits a proposal without bodies. `--load` returns selected UTF-8 instructions and declared required resources, capped at 32,000 bytes across at most eight skills. `--task-input -` accepts one UTF-8 JSON object on standard input, capped at 65,536 bytes. These byte caps are output and transport bounds, not native tokenizer results. Save the returned `selection.receipt` as a separate JSON document to use `--previous receipt.json`. `--expected-digest` can bind a load to a prior selection digest. Source, trigger content, routing-policy version, profile, mode, exclusions, session, task revision, phase, and a digest of the query invalidate stale reuse. A pending proposal cannot be reused as a completed decision. Receipts are integrity checks for local operation, not an authorization signature. diff --git a/docs/design/context-profile-delivery.tdd.md b/docs/design/context-profile-delivery.tdd.md index 725eb94a2..33a8396d6 100644 --- a/docs/design/context-profile-delivery.tdd.md +++ b/docs/design/context-profile-delivery.tdd.md @@ -88,4 +88,13 @@ Current combined verification after recovery: - The default sandbox checkout's 5,000-path capture limit truncated a real Tier 1 install diff and failed closed. The reviewed ECC-029 sandbox implementation raises the bounded cap to 50,000, passes its 26-case boundary suite, and produced both final reports. The driver receipt binds its 51-file implementation digest `a84e09ab848b8cd05f33792c13734f7aabe16bfe16d50d8f8292eb5261a93c3a`. - No real AI outcome call ran because `CODEX_API_KEY` was absent. Host ChatGPT authentication was neither copied nor exposed to the disposable evaluator. +## Auto-admission anchor (routing policy v5) + +The 77-prompt routing corpus from #2945 (52 direct, 25 paraphrased) found one wrong implicit load. "two services both think they own the same record" admitted `skill:santa-method` on four description-only words (`two`, `both`, `they`, `same`) with BM25 24.3 and a 1.94 margin, clearing every v4 bar. + +- RED: the prompt and the corpus check fail against v4 (`skill:santa-method` loaded). +- GREEN: auto admission and the tier-2 fallback also require one matched term in the skill name or curated triggers. Exact directive citations are unchanged. +- Corpus result: zero loads outside the expected set; 19 implicit admissions become 17. The other dropped admission, `skill:cost-tracking`, now defers to the bounded proposal instead of loading. +- The existing nine-query auto/agent corpus, fallback, launch, evaluation and retrieval suites pass unchanged. + These boundaries keep the shipped behavior distinct from the M1 release gate. Authenticated outcome observations, a complete Tier 2 disk diff, live-install migration, other-provider activation, whole-context token truth and release defaults remain unverified until their explicit prerequisites are available. diff --git a/scripts/lib/context-retrieval.js b/scripts/lib/context-retrieval.js index c4a93a800..adadd3b64 100644 --- a/scripts/lib/context-retrieval.js +++ b/scripts/lib/context-retrieval.js @@ -171,12 +171,14 @@ function searchRetrieval(index, query, { limit = 5 } = {}) { .slice(0, limit) .map(([document, score]) => { const matched = [...new Set(queryTokens)].filter(term => document.weighted.has(term)); + const anchorTerms = matched.filter(term => document.fields.name.includes(term) || document.fields.triggers.includes(term)); const exact = anchored.has(document); return { id: document.entry.id, score: Math.round(score * 10000) / 10000, exact, exactAlias: exact ? anchored.get(document) : undefined, dense: Math.round((dense.get(document) || 0) * 10000) / 10000, bm25: Math.round((bm25.get(document) || 0) * 10000) / 10000, matchedTerms: matched, + anchorTerms, description: document.entry.description.slice(0, 2048), descriptionTruncated: document.entry.description.length > 2048 }; }); diff --git a/scripts/lib/context-selection.js b/scripts/lib/context-selection.js index 85a496800..073084979 100644 --- a/scripts/lib/context-selection.js +++ b/scripts/lib/context-selection.js @@ -26,9 +26,14 @@ const AUTO_ADMIT_MARGIN = 1.5; const FALLBACK_MIN_BM25 = 12; const FALLBACK_MIN_TERMS = 2; const FALLBACK_MARGIN = 1.1; +// Both tiers also require at least one matched term in the skill's name or +// curated triggers. Description-only overlap on common words (two, both, same) +// can clear the score bars without naming the skill's subject. +const MIN_ANCHOR_TERMS = 1; // v4: an explicit empty proposal (decline) is honored; the tier-2 fallback no // longer overrides declines at the launch/selection call sites. -const ROUTING_POLICY_VERSION = 4; +// v5: implicit admission requires a name or trigger anchor term. +const ROUTING_POLICY_VERSION = 5; const TASK_KEYS = new Set(['sessionId', 'taskId', 'revision', 'phase', 'query', 'explicitIds', 'proposedIds', 'noWorkflow']); function validateTask(task) { @@ -210,6 +215,7 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l const top = candidates[0]; const second = candidates[1]; if (top.bm25 >= AUTO_ADMIT_MIN_BM25 && top.matchedTerms.length >= AUTO_ADMIT_MIN_TERMS + && top.anchorTerms.length >= MIN_ANCHOR_TERMS && (!second || top.bm25 >= AUTO_ADMIT_MARGIN * (second.bm25 || 0))) { autoSelection = { id: top.id, bm25: top.bm25, matchedTerms: top.matchedTerms.length, exact: false }; } @@ -221,6 +227,7 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l const top = candidates[0]; const second = candidates[1]; if (top.bm25 >= FALLBACK_MIN_BM25 && top.matchedTerms.length >= FALLBACK_MIN_TERMS + && top.anchorTerms.length >= MIN_ANCHOR_TERMS && (!second || top.bm25 >= FALLBACK_MARGIN * (second.bm25 || 0))) { fallback = { id: top.id, bm25: top.bm25, matchedTerms: top.matchedTerms.length }; }