mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-21 07:21:46 +02:00
fix: preserve citation intent and current routing evidence gates
This commit is contained in:
@@ -44,10 +44,138 @@ function hasSuggestionEvidence(candidate) {
|
||||
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.
|
||||
// v5: implicit admission requires a name or trigger anchor term.
|
||||
const ROUTING_POLICY_VERSION = 5;
|
||||
// v5: indirect name citations cannot bypass review through exact or scored admission.
|
||||
// v6: ordered directives and singular named references share the same admission guard.
|
||||
// v7: independent directives scope negation and imperatives allow question punctuation.
|
||||
// v8: only recognized request/rejection scopes can authorize coordinated directives.
|
||||
// v9: neutral task preambles do not suppress a later explicit skill request.
|
||||
// v10: illustrative instructions do not supply or withdraw a direct request.
|
||||
// v11: a contrastive current-task instruction ends the preceding example scope.
|
||||
// v12: preserve current-main evidence floors and anchor checks.
|
||||
const ROUTING_POLICY_VERSION = 12;
|
||||
const TASK_KEYS = new Set(['sessionId', 'taskId', 'revision', 'phase', 'query', 'explicitIds', 'proposedIds', 'noWorkflow']);
|
||||
|
||||
const DIRECTIVE_VERB = /\b(use|apply|invoke|run|follow|load)\s+(the\s+)?/i;
|
||||
const DIRECTIVE_REQUEST = /^(?:please )?(?:(?:can|could|would|will) you (?:please )?|do )?(?:use|apply|invoke|run|follow|load)\b/;
|
||||
const DIRECTIVE_REJECTION = /^(?:please )?(?:(?:(?:do )?not|never) (?:use|apply|invoke|run|follow|load)\b|avoid\b)/;
|
||||
const DIRECTIVE_NAME_PREFIX = new RegExp(DIRECTIVE_REQUEST.source + '\\s+(?:the\\s+)?(?:skill\\s*)?$');
|
||||
const QUESTION_START = /^(?!do not\b)(can|could|would|should|shall|will|may|might|must|ought|do|does|did|is|are|why|how|what|when|where|which)\b/;
|
||||
const REPORTED_INSTRUCTION = /\b(say|says|said|reads|told|mentions?|quoted?|document|states?|stated|recommends?|recommended|asserts?|asserted)\b/;
|
||||
const EXAMPLE_PREFIX = /^(?:for (?:example|instance)|as an example|to illustrate)\b/;
|
||||
const CURRENT_TASK_SUFFIX = /\bfor (?:this|the current) task$/;
|
||||
// Subject-led statements may describe someone else's instructions or intentions.
|
||||
const SUBJECT_PREFIX = /^(?:the|a|an|i|you|he|she|it|we|they|my|your|his|her|its|our|their|this|that|these|those)\b/;
|
||||
// Rejections keep their whole list until another instruction/question begins.
|
||||
const INSTRUCTION_BOUNDARY = /(?:,\s*(?:(?:and|but)\s+)?|\s+(?:and|but)\s+)(?=(?:please\s+)?(?:do\s+(?:not\s+)?|not\s+|never\s+)?(?:use|apply|invoke|run|follow|load)\b|(?:can|could|would|should|shall|will|may|might|must|ought|do|does|did|is|are|why|how|what|when|where|which)\b)/i;
|
||||
// Admission additionally checks intervening scopes, including unknown wording.
|
||||
const ADMISSION_BOUNDARY = /(?:,\s*(?:(?:and|but)\s+)?|\s+(?:and|but)\s+)(?=\S)/i;
|
||||
|
||||
/** Track example spans without discarding the connectors between instructions.
|
||||
* A contrast inside an example remains illustrative unless its instruction
|
||||
* explicitly addresses the current task. Earlier non-example scopes stay intact.
|
||||
* @param {string} text Normalized clause with comma boundaries preserved.
|
||||
* @param {string[]} names Known candidate aliases whose internal connectors are names.
|
||||
* @returns {Array<{start: number, text: string, example: boolean}>} Ordered scopes.
|
||||
*/
|
||||
function citationScopes(text, names) {
|
||||
const mentions = names.flatMap(name => [...text.matchAll(new RegExp('\\b' + name + '\\b', 'g'))]
|
||||
.map(mention => ({ start: mention.index, end: mention.index + name.length })));
|
||||
const boundaries = [...text.matchAll(new RegExp(ADMISSION_BOUNDARY.source, 'gi'))]
|
||||
.filter(boundary => !mentions.some(mention => boundary.index > mention.start && boundary.index < mention.end));
|
||||
let example = false;
|
||||
return [0, ...boundaries.map(boundary => boundary.index + boundary[0].length)].map((start, index) => {
|
||||
const scope = text.slice(start, boundaries[index]?.index ?? text.length);
|
||||
const currentTask = CURRENT_TASK_SUFFIX.test(scope)
|
||||
&& (DIRECTIVE_REQUEST.test(scope) || DIRECTIVE_REJECTION.test(scope));
|
||||
if (currentTask && /\bbut\b/.test(boundaries[index - 1]?.[0] || '')) example = false;
|
||||
if (EXAMPLE_PREFIX.test(scope)) example = true;
|
||||
return { start, text: scope, example };
|
||||
});
|
||||
}
|
||||
|
||||
/** Normalize citation names while preserving contracted negation as a separate word.
|
||||
* @param {string} text Raw query text or a registry alias.
|
||||
* @returns {string} Lowercase alphanumeric words with negation preserved.
|
||||
*/
|
||||
const normalizedQueryName = text => text.replace(/n['\u2019]t\b/gi, ' not')
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/_/g, ' ')
|
||||
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
||||
|
||||
/** Return canonical/native aliases plus conservative singular forms. Singular
|
||||
* multiword aliases only guard indirect references; they never create exact admission.
|
||||
* @param {object} candidate Ranked skill candidate with its canonical ID and exact alias.
|
||||
* @param {string} nativeName Registry name that may be absent from exact retrieval.
|
||||
* @returns {Array<{name: string, exact: boolean}>} Normalized names and admission eligibility.
|
||||
*/
|
||||
function citationAliases(candidate, nativeName) {
|
||||
const names = [...new Set([candidate.exactAlias, candidate.id.slice('skill:'.length), nativeName]
|
||||
.filter(Boolean).map(normalizedQueryName).filter(Boolean))];
|
||||
return names.flatMap(name => [{ name, exact: true },
|
||||
...(/\s\w+[^s]s$/.test(name) ? [{ name: name.slice(0, -1), exact: false }] : [])]);
|
||||
}
|
||||
|
||||
/** Classify a retrieval candidate's references in query order. Genuine later
|
||||
* directives/rejections replace earlier instructions; questions, quotations and
|
||||
* reported speech do not withdraw a directive. Returns directive, indirect or none.
|
||||
* @param {object} candidate Ranked skill candidate with its canonical ID and exact alias.
|
||||
* @param {string} query Task text to classify without loading skill contents.
|
||||
* @param {string} nativeName Registry name, including aliases absent from exact retrieval.
|
||||
* @param {string[]} names All candidate aliases, used to preserve names across scopes.
|
||||
* @returns {'directive'|'indirect'|'none'} Admission evidence for this skill only.
|
||||
*/
|
||||
function citationFor(candidate, query, nativeName, names) {
|
||||
const aliases = citationAliases(candidate, nativeName);
|
||||
let citation = 'none';
|
||||
// Mask quoted instructions before splitting clauses so punctuation inside a
|
||||
// quotation cannot look like a separate instruction. Quoted names remain usable.
|
||||
const unquoted = query.replace(/"[^"]*"|\u201c[^\u201d]*\u201d|(?:^|[\s(:])'[^']*'|\u2018[^\u2019]*\u2019|\x60[^\x60]*\x60/g, part => {
|
||||
const text = normalizedQueryName(part);
|
||||
const instruction = DIRECTIVE_VERB.test(text);
|
||||
// Quoting a task qualifier cannot turn an illustrative instruction into a request.
|
||||
if (!instruction && !CURRENT_TASK_SUFFIX.test(text)) return part;
|
||||
if (instruction && aliases.some(({ name }) => new RegExp('\\b' + name + '\\b').test(text))) citation = 'indirect';
|
||||
return part.replace(/[^.!?;\n]/g, ' ');
|
||||
});
|
||||
// Keep the illustrative abbreviation together when its periods would split clauses.
|
||||
const expanded = unquoted.replace(/\be\.g\.(?=\s|[,;:!?]|$)/gi, 'for example');
|
||||
for (const clause of expanded.match(/[^.!?;\n]+[.!?;\n]*/g) || []) {
|
||||
// Keep comma boundaries for directive scope without changing alias normalization.
|
||||
const text = clause.split(',').map(normalizedQueryName).join(', ');
|
||||
const clauseScopes = citationScopes(text, names);
|
||||
const mentions = aliases.flatMap(({ name, exact }) => [...text.matchAll(new RegExp('\\b' + name + '\\b', 'g'))]
|
||||
.map(mention => ({ index: mention.index, exact }))).sort((a, b) => a.index - b.index);
|
||||
for (const mention of mentions) {
|
||||
const prefix = text.slice(0, mention.index);
|
||||
const precedingScopes = clauseScopes.filter(scope => scope.start <= mention.index);
|
||||
const example = precedingScopes.at(-1)?.example;
|
||||
const scopes = precedingScopes.filter(scope => !scope.example)
|
||||
.map(scope => scope.text.slice(0, mention.index - scope.start));
|
||||
const instructionPrefix = prefix.split(INSTRUCTION_BOUNDARY).at(-1);
|
||||
// A task preamble such as "fix the bug" does not change a later request.
|
||||
// Only preceding scopes may be neutral: an embedded instruction, question,
|
||||
// or subject-led statement still needs review. The final scope must remain
|
||||
// a direct request; neutral text never supplies admission evidence itself.
|
||||
const instruction = scopes.every((scope, index) => DIRECTIVE_REQUEST.test(scope) || DIRECTIVE_REJECTION.test(scope)
|
||||
|| (index < scopes.length - 1 && !DIRECTIVE_VERB.test(scope)
|
||||
&& !QUESTION_START.test(scope) && !REPORTED_INSTRUCTION.test(scope) && !SUBJECT_PREFIX.test(scope)));
|
||||
const question = (!DIRECTIVE_REQUEST.test(text) && QUESTION_START.test(text))
|
||||
|| (!DIRECTIVE_REQUEST.test(instructionPrefix)
|
||||
&& (QUESTION_START.test(instructionPrefix) || clause.includes('?')));
|
||||
if (example || question || scopes.some(scope => REPORTED_INSTRUCTION.test(scope)) || /^\s*["'\u201c\u2018\x60]/.test(clause)) {
|
||||
if (citation === 'none') citation = 'indirect';
|
||||
} else if (/\b(do not|never|no|not|avoid)\b/.test(instructionPrefix)) {
|
||||
citation = 'indirect';
|
||||
} else if (!instruction) {
|
||||
if (citation === 'none') citation = 'indirect';
|
||||
} else if (mention.exact && candidate.exact
|
||||
&& DIRECTIVE_NAME_PREFIX.test(scopes.at(-1))) {
|
||||
citation = 'directive';
|
||||
}
|
||||
}
|
||||
}
|
||||
return citation;
|
||||
}
|
||||
|
||||
function validateTask(task) {
|
||||
if (!task || typeof task !== 'object' || Array.isArray(task)) throw new Error('Task must be an object');
|
||||
for (const key of Object.keys(task)) if (!TASK_KEYS.has(key)) throw new Error(`Unknown task field: ${key}`);
|
||||
@@ -196,7 +324,13 @@ function routingEntries({ repoRoot = DEFAULT_REPO_ROOT, profileId = 'lean@1', ta
|
||||
routingPolicyVersion: ROUTING_POLICY_VERSION, triggersDigest: digestObject(triggers), entries };
|
||||
}
|
||||
|
||||
/** Pure task-scoped resolver. Returned context never invokes a native skill or changes permissions. */
|
||||
/** Resolve task-scoped skill context without invoking native skills or changing permissions.
|
||||
* @param {object} [options] Repository, task, profile, and selection constraints.
|
||||
* @param {object} options.task Task identity, query, and explicit or proposed skill IDs.
|
||||
* @param {boolean} [options.load=false] Include verified resource contents in the result.
|
||||
* @param {object|null} [options.previous=null] Receipt reusable only when its binding matches.
|
||||
* @returns {object} Selection decision, candidates, verified resources, and a bound receipt.
|
||||
*/
|
||||
function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'lean@1', target = 'codex',
|
||||
selectionMode = 'auto', include = [], exclude = [], load = false, previous = null, expectedDigest = null } = {}) {
|
||||
validateTask(task);
|
||||
@@ -229,28 +363,11 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l
|
||||
// questions, negations, reported speech, multiple cited names — never admit
|
||||
// implicitly. Everything else keeps the bounded-proposal path so the
|
||||
// primary agent decides ambiguous cases during work it was already doing.
|
||||
const DIRECTIVE_VERB = /\b(use|apply|invoke|run|follow|load)\s+(the\s+)?/i;
|
||||
const normalizedQueryName = text => text.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/_/g, ' ')
|
||||
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
||||
const directiveCitation = candidate => {
|
||||
if (!candidate || !candidate.exact) return false;
|
||||
const text = normalizedQueryName(task.query || '');
|
||||
const aliases = [...new Set([candidate.exactAlias,
|
||||
candidate.id.slice('skill:'.length).toLowerCase(),
|
||||
candidate.id.slice('skill:'.length).toLowerCase().replace(/-/g, ' ')].filter(Boolean))];
|
||||
for (const name of aliases) {
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`${DIRECTIVE_VERB.source}(skill\\s*:?\\s*)?${escaped}(\\s+(skill|workflow|guidance))?\\b`, 'i');
|
||||
const match = pattern.exec(text);
|
||||
if (!match) continue;
|
||||
const window = text.slice(Math.max(0, match.index - 28), match.index);
|
||||
if (/\b(do not|don't|never|no)\b/.test(window)) return false;
|
||||
if (/\b(says|said|reads|told|document)\b/i.test(task.query || '')) return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const exactAnchors = candidates.filter(directiveCitation);
|
||||
const names = [...new Set(candidates.flatMap(candidate => citationAliases(candidate, byId.get(candidate.id).name)
|
||||
.map(alias => alias.name)))];
|
||||
const citations = new Map(candidates.map(candidate => [candidate.id,
|
||||
citationFor(candidate, task.query || '', byId.get(candidate.id).name, names)]));
|
||||
const exactAnchors = candidates.filter(candidate => citations.get(candidate.id) === 'directive');
|
||||
let autoSelection = null;
|
||||
if (!task.noWorkflow && selectionMode === 'auto' && !reused && !explicitIds.length && !proposedIds.length && candidates.length) {
|
||||
if (exactAnchors.length === 1) {
|
||||
@@ -259,7 +376,7 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l
|
||||
} else if (!exactAnchors.length) {
|
||||
const top = candidates[0];
|
||||
const second = candidates[1];
|
||||
if (top.bm25 >= AUTO_ADMIT_MIN_BM25 && top.matchedTerms.length >= AUTO_ADMIT_MIN_TERMS
|
||||
if (citations.get(top.id) !== 'indirect' && 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 };
|
||||
@@ -271,7 +388,7 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l
|
||||
&& !explicitIds.length && !proposedIds.length && candidates.length && !exactAnchors.length) {
|
||||
const top = candidates[0];
|
||||
const second = candidates[1];
|
||||
if (meetsFallbackBar(top)
|
||||
if (citations.get(top.id) !== 'indirect' && meetsFallbackBar(top)
|
||||
&& 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 };
|
||||
|
||||
@@ -155,3 +155,183 @@ test('spawn failures and timeout signals remain unsuccessful without a native ex
|
||||
assert.equal(result.taskSuccess, 'unverified');
|
||||
}
|
||||
}));
|
||||
|
||||
for (const query of ["Don't use the feature skill.", 'Don\u2019t use the feature skill.',
|
||||
'Can I use the feature skill?', 'The README says use the feature skill.', 'The docs say use feature.',
|
||||
'The docs say do not use feature and use shared guidance.',
|
||||
'The docs say do not use feature, use shared guidance, okay?',
|
||||
'The spec states do not use feature and use shared guidance.',
|
||||
'The spec stated do not use feature, use shared guidance.',
|
||||
'The spec recommends we do not use feature and use shared guidance.',
|
||||
'The spec asserts we should not use feature and use shared guidance.',
|
||||
'Use standard tools, the spec stipulates do not use feature and use shared guidance.',
|
||||
'Should we avoid feature and use shared guidance?',
|
||||
'Should we avoid feature, use shared guidance?',
|
||||
'Use standard tools, should we use feature?',
|
||||
'Use standard tools, shall we use feature?',
|
||||
'Use standard tools, will we use feature?',
|
||||
'Use standard tools, ought we to use feature?',
|
||||
'Please use the existing code, can I use feature?',
|
||||
'Use standard tools and should we use feature?',
|
||||
'Use standard tools but should we use feature?',
|
||||
'Fix the bug, shall we use feature?',
|
||||
'Fix the bug, the spec states do not use feature and use shared guidance.',
|
||||
'The docs say fix the bug and use feature.',
|
||||
'Can we fix the bug and use feature?',
|
||||
'The spec stipulates: fix the bug and use feature.',
|
||||
'Our policy stipulates repair the bug and use feature.',
|
||||
'Fix the bug, ought we to repair it and use feature?',
|
||||
'Fix the bug, must we repair it and use feature?',
|
||||
'Fix the bug, feature.',
|
||||
'For example, use feature',
|
||||
'For instance, use feature.',
|
||||
'As an example, use feature.',
|
||||
'To illustrate, use feature.',
|
||||
'Fix the bug, for example, use feature.',
|
||||
'For example, use feature and use feature.',
|
||||
'For example, use feature but use shared guidance.',
|
||||
"For example, don't use feature but use shared guidance.",
|
||||
'For example, use feature and use feature for this task.',
|
||||
'For example, use feature, but should we use feature for this task?',
|
||||
'For example, use feature, but the docs say use feature for this task.',
|
||||
'For example, use feature, but "use feature for this task" is an example.',
|
||||
'The docs say for example, use feature, but use feature for this task.',
|
||||
'For example, use feature, but use feature "for this task"',
|
||||
'Should we, for example, use shared guidance, but use feature for this task?',
|
||||
'Our policy stipulates, for example, use shared guidance, but use feature for this task.',
|
||||
'Use feature. For example, use shared guidance, but do not use feature for this task.',
|
||||
'For example, use shared guidance, but use feature for this task and do not use feature.',
|
||||
'e.g., use feature.',
|
||||
'E.g. use feature.',
|
||||
'Do not use feature and shared guidance.',
|
||||
"Use feature, don't use feature.",
|
||||
"Use feature and don't use feature.",
|
||||
"Use feature but don't use feature."]) {
|
||||
test('indirect citation uses the proposal path and honors decline: ' + query, () => withFixture(repoRoot => {
|
||||
const phases = [];
|
||||
const result = launchTaskContext({ repoRoot, task: { ...input, query, explicitIds: [] },
|
||||
execute(command, args, options) {
|
||||
phases.push(options.phase);
|
||||
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
|
||||
assert.doesNotMatch(options.input, /# feature/);
|
||||
assert.doesNotMatch(options.input, /# shared/);
|
||||
return { status: 0, stdout: 'ok' };
|
||||
} });
|
||||
assert.deepEqual(phases, ['selection', 'task']);
|
||||
assert.equal(result.routingCalls, 1);
|
||||
assert.deepEqual(result.selection.selectedIds, []);
|
||||
assert.deepEqual(result.selection.loadedIds, []);
|
||||
assert.equal(result.selection.fallback, null);
|
||||
assert.equal(result.selection.reason, 'agent-declined-selection');
|
||||
}));
|
||||
}
|
||||
|
||||
for (const [query, skill] of [
|
||||
['Fix the bug, use feature', 'feature'],
|
||||
['Fix the bug and use feature.', 'feature'],
|
||||
['Please fix the bug, please use feature.', 'feature'],
|
||||
['Write the regression test, use feature.', 'feature'],
|
||||
['Investigate the bug but use feature.', 'feature'],
|
||||
['For this task, use feature.', 'feature'],
|
||||
["Fix the bug, don't use feature and use shared guidance.", 'shared'],
|
||||
["Don't use feature, use shared guidance.", 'shared'],
|
||||
["Don't use feature and use shared guidance.", 'shared'],
|
||||
["Don't use feature but use shared guidance.", 'shared'],
|
||||
["Don't use feature, use shared guidance, okay?", 'shared'],
|
||||
["Don't use feature and use shared guidance, okay?", 'shared'],
|
||||
["Don't use feature but use shared guidance, okay?", 'shared'],
|
||||
['Do not use feature, please use shared guidance?', 'shared'],
|
||||
['Do not use feature and please use shared guidance?', 'shared'],
|
||||
['Do not use feature but please use shared guidance?', 'shared'],
|
||||
["Don't use feature, use feature.", 'feature'],
|
||||
['Use feature?', 'feature'],
|
||||
['Please use feature?', 'feature'],
|
||||
['Use feature to fix the bug, okay?', 'feature'],
|
||||
['Use feature. Shall we use feature?', 'feature'],
|
||||
['Use feature, shall we use shared guidance?', 'feature'],
|
||||
['Shall we use shared guidance? Use feature.', 'feature'],
|
||||
['Use feature. The spec states do not use feature and use shared guidance.', 'feature'],
|
||||
['The spec states do not use feature and use shared guidance. Use feature.', 'feature'],
|
||||
['Use feature. For example, use shared guidance.', 'feature'],
|
||||
['For example, use shared guidance. Use feature.', 'feature'],
|
||||
["Use feature. For example, don't use feature.", 'feature'],
|
||||
['Use feature to build an example.', 'feature'],
|
||||
['Fix the example, use feature.', 'feature'],
|
||||
['Fix e.g.js, use feature.', 'feature'],
|
||||
["Use feature. E.g., don't use feature.", 'feature'],
|
||||
['For example, use feature, but use feature for this task', 'feature'],
|
||||
['For example, use shared guidance, but please use feature for this task.', 'feature'],
|
||||
['For instance, use shared guidance but use feature for this task.', 'feature'],
|
||||
['For example use shared guidance, but use feature for this task.', 'feature'],
|
||||
['For example, use shared guidance, but use feature for the current task.', 'feature'],
|
||||
['For example, use shared guidance, but use feature for this task, for instance, do not use feature.', 'feature'],
|
||||
['For example, the docs say use shared guidance, but use feature for this task.', 'feature'],
|
||||
]) {
|
||||
test('an unambiguous final directive injects context without a proposal: ' + query, () => withFixture(repoRoot => {
|
||||
const phases = [];
|
||||
const result = launchTaskContext({ repoRoot, task: { ...input, query, explicitIds: [] },
|
||||
execute(command, args, options) {
|
||||
phases.push(options.phase);
|
||||
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
|
||||
assert.match(options.input, new RegExp('# ' + skill));
|
||||
assert.doesNotMatch(options.input, new RegExp('# ' + (skill === 'shared' ? 'feature' : 'shared')));
|
||||
return { status: 0, stdout: 'ok' };
|
||||
} });
|
||||
assert.deepEqual(phases, ['task']);
|
||||
assert.equal(result.routingCalls, 0);
|
||||
assert.deepEqual(result.selection.loadedIds, ['skill:' + skill]);
|
||||
assert.equal(result.selection.reason, 'auto-selection');
|
||||
}));
|
||||
}
|
||||
|
||||
test('a final directive after a question reaches the task with its context', () => withFixture(repoRoot => {
|
||||
const phases = [];
|
||||
const result = launchTaskContext({ repoRoot,
|
||||
task: { ...input, query: 'Should we use feature? Use feature.', explicitIds: [] },
|
||||
execute(command, args, options) {
|
||||
phases.push(options.phase);
|
||||
assert.match(options.input, /# feature/);
|
||||
return { status: 0, stdout: 'ok' };
|
||||
} });
|
||||
assert.deepEqual(phases, ['task']);
|
||||
assert.equal(result.routingCalls, 0);
|
||||
assert.deepEqual(result.selection.loadedIds, ['skill:feature']);
|
||||
}));
|
||||
|
||||
for (const [suffix, expectedIds] of [['.', ['skill:feature']], [' and use shared guidance.', []]]) {
|
||||
test('a conjunction inside a native alias preserves launch scope: ' + suffix, () => withFixture(repoRoot => {
|
||||
fs.writeFileSync(path.join(repoRoot, 'skills/feature/SKILL.md'),
|
||||
'---\nname: research-and-development\ndescription: Feature workflow\n---\n# feature');
|
||||
const query = 'For example, use shared guidance, but use research and development for this task' + suffix;
|
||||
const phases = [];
|
||||
const result = launchTaskContext({ repoRoot, task: { ...input, query, explicitIds: [] },
|
||||
execute(command, args, options) {
|
||||
phases.push(options.phase);
|
||||
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
|
||||
if (expectedIds.length) assert.match(options.input, /# feature/);
|
||||
else assert.doesNotMatch(options.input, /# feature/);
|
||||
assert.doesNotMatch(options.input, /# shared/);
|
||||
return { status: 0, stdout: 'ok' };
|
||||
} });
|
||||
assert.deepEqual(phases, expectedIds.length ? ['task'] : ['selection', 'task']);
|
||||
assert.deepEqual(result.selection.selectedIds, expectedIds);
|
||||
assert.deepEqual(result.selection.loadedIds, expectedIds);
|
||||
assert.equal(result.selection.reason, expectedIds.length ? 'auto-selection' : 'agent-declined-selection');
|
||||
}));
|
||||
}
|
||||
|
||||
test('a singular rejected name cannot inject scored context at launch', () => {
|
||||
const phases = [];
|
||||
const query = "Don't use the database migration skill. Review a PostgreSQL migration that adds an indexed nullable column without downtime.";
|
||||
const result = launchTaskContext({ task: { ...input, query, explicitIds: [] },
|
||||
execute(command, args, options) {
|
||||
phases.push(options.phase);
|
||||
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
|
||||
assert.doesNotMatch(options.input, /# Database Migration Patterns/);
|
||||
return { status: 0, stdout: 'ok' };
|
||||
} });
|
||||
assert.deepEqual(phases, ['selection', 'task']);
|
||||
assert.equal(result.routingCalls, 1);
|
||||
assert.deepEqual(result.selection.loadedIds, []);
|
||||
assert.equal(result.selection.fallback, null);
|
||||
});
|
||||
|
||||
@@ -167,11 +167,13 @@ test('source changes invalidate reuse and source-bound load preview', () => with
|
||||
}));
|
||||
|
||||
test('bounded search uses canonical IDs and deterministic order', () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query: 'feature' });
|
||||
const result = resolve(repoRoot, { query: 'feature' }, { load: true });
|
||||
assert.equal(result.candidates[0].id, 'skill:feature');
|
||||
// A bare name mention ranks the skill but is not a directive citation.
|
||||
assert.deepEqual(result.selectedIds, []);
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
assert.equal(result.fallback, null);
|
||||
assert.ok(result.candidates.length <= 5);
|
||||
}));
|
||||
|
||||
@@ -201,6 +203,18 @@ test('multiple directive citations defer to an explicit agent proposal', () => w
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
}));
|
||||
|
||||
for (const [suffix, expectedIds] of [['.', ['skill:feature']], [' and use shared guidance.', []]]) {
|
||||
test('a conjunction inside a native alias preserves task request scope: ' + suffix, () => withFixture(repoRoot => {
|
||||
write(repoRoot, 'skills/feature/SKILL.md', '---\nname: research-and-development\ndescription: Feature workflow\n---\n# feature');
|
||||
const query = 'For example, use shared guidance, but use research and development for this task' + suffix;
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.selectedIds, expectedIds);
|
||||
assert.deepEqual(result.loadedIds, expectedIds);
|
||||
assert.equal(result.reason, expectedIds.length ? 'auto-selection' : 'agent-selection-required');
|
||||
if (!expectedIds.length) assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
test('name anchors require complete word boundaries', () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query: 'featurette sharedness' }, { load: true });
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
@@ -344,3 +358,372 @@ test('suggestion evidence requires an exact name or the absolute fallback floor'
|
||||
assert.equal(hasSuggestionEvidence(candidate({ exact: true, bm25: 1, matchedTerms: ['feature'] })), true);
|
||||
assert.equal(hasSuggestionEvidence(null), false);
|
||||
});
|
||||
|
||||
for (const query of [
|
||||
"You shouldn't use feature.",
|
||||
"You can\u2019t use feature.",
|
||||
"For feature, don't use the feature skill.",
|
||||
'For feature, never invoke feature.',
|
||||
'The phrase "Use feature" is an example.',
|
||||
'The phrase "Use feature." is an example.',
|
||||
'The phrase \u201cUse feature\u201d is an example.',
|
||||
'The phrase `Use feature` is an example.',
|
||||
"Don't use the feature skill.",
|
||||
'Don\u2019t use the feature skill.',
|
||||
'Do not use the feature skill.',
|
||||
'Never use the feature skill.',
|
||||
'Can I use the feature skill?',
|
||||
'Should we use the feature skill?',
|
||||
'Why use the feature skill?',
|
||||
'The README says to use the feature skill.',
|
||||
'The docs say use feature.',
|
||||
'The docs say to use feature.',
|
||||
'The docs said use feature.',
|
||||
'"Use the feature skill" is an example.',
|
||||
]) {
|
||||
test('indirect name citation needs an agent decision: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.ok(result.candidates.some(candidate => candidate.id === 'skill:feature'));
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
test('a negated citation does not suppress a separate affirmative directive', () => withFixture(repoRoot => {
|
||||
for (const query of [
|
||||
"Don't use feature. Use shared guidance.",
|
||||
'Don\u2019t use feature; please use shared guidance.',
|
||||
'The document says use feature. Use shared guidance.',
|
||||
]) {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.loadedIds, ['skill:shared']);
|
||||
}
|
||||
}));
|
||||
|
||||
for (const query of [
|
||||
"Don't use feature, use shared guidance.",
|
||||
"Don't use feature and use shared guidance.",
|
||||
"Don't use feature but use shared guidance.",
|
||||
'Do not use feature, please use shared guidance.',
|
||||
'Don\u2019t use feature, and please use shared guidance.',
|
||||
"Don't use feature, do use shared guidance.",
|
||||
"Don't use feature, use shared guidance, okay?",
|
||||
"Don't use feature and use shared guidance, okay?",
|
||||
"Don't use feature but use shared guidance, okay?",
|
||||
'Do not use feature, please use shared guidance?',
|
||||
'Do not use feature and please use shared guidance?',
|
||||
'Do not use feature but please use shared guidance?',
|
||||
]) {
|
||||
test('a new directive scopes an earlier rejection: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.selectedIds, ['skill:shared']);
|
||||
assert.deepEqual(result.loadedIds, ['skill:shared']);
|
||||
assert.equal(result.reason, 'auto-selection');
|
||||
assert.equal(result.receipt.autoSelection.exact, true);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const query of [
|
||||
'The docs say do not use feature and use shared guidance.',
|
||||
'The docs say do not use feature, use shared guidance.',
|
||||
'The docs say do not use feature, use shared guidance, okay?',
|
||||
'Use standard tools, the spec states do not use feature and use shared guidance.',
|
||||
'The spec states do not use feature and use shared guidance.',
|
||||
'The spec stated do not use feature, use shared guidance.',
|
||||
'The spec recommends we do not use feature and use shared guidance.',
|
||||
'The spec asserts we should not use feature and use shared guidance.',
|
||||
'Use standard tools, the spec stipulates do not use feature and use shared guidance.',
|
||||
'Use the documentation that says do not use feature and use shared guidance.',
|
||||
'Should we avoid feature and use shared guidance?',
|
||||
'Should we avoid feature, use shared guidance?',
|
||||
'Do not use feature and shared guidance.',
|
||||
'Do not use feature, shared guidance.',
|
||||
]) {
|
||||
test('a connector preserves indirect or negated context: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.ok(result.candidates.some(candidate => candidate.id === 'skill:shared'));
|
||||
assert.deepEqual(result.selectedIds, []);
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const query of [
|
||||
'Use standard tools, should we use feature?',
|
||||
'Use standard tools, shall we use feature?',
|
||||
'Use standard tools, will we use feature?',
|
||||
'Use standard tools, ought we to use feature?',
|
||||
'Please use the existing code, can I use feature?',
|
||||
'Use standard tools and should we use feature?',
|
||||
'Use standard tools but should we use feature?',
|
||||
]) {
|
||||
test('a generic directive does not authorize a later question: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.ok(result.candidates.some(candidate => candidate.id === 'skill:feature'));
|
||||
assert.deepEqual(result.selectedIds, []);
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const query of ['Use feature?', 'Please use feature?', 'Use feature to fix the bug, okay?']) {
|
||||
test('an imperative with question punctuation remains a directive: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.selectedIds, ['skill:feature']);
|
||||
assert.deepEqual(result.loadedIds, ['skill:feature']);
|
||||
assert.equal(result.reason, 'auto-selection');
|
||||
assert.equal(result.receipt.autoSelection.exact, true);
|
||||
}));
|
||||
}
|
||||
|
||||
test('ordinary and polite directives preserve automatic and explicit selection', () => withFixture(repoRoot => {
|
||||
for (const query of ['Please use the feature skill.', 'Can you use the feature skill?',
|
||||
'Use feature without changing the code.', 'Use feature to document the change.',
|
||||
'Use "feature".', 'Use `feature`.', 'Do use feature.', 'Please can you use feature?']) {
|
||||
assert.deepEqual(resolve(repoRoot, { query }, { load: true }).loadedIds, ['skill:feature']);
|
||||
}
|
||||
for (const field of ['explicitIds', 'proposedIds']) {
|
||||
assert.deepEqual(resolve(repoRoot, { query: 'Should we use feature?', [field]: ['skill:feature'] },
|
||||
{ load: true }).loadedIds, ['skill:feature']);
|
||||
}
|
||||
}));
|
||||
|
||||
for (const [query, expectedId] of [
|
||||
['Fix the bug, use feature', 'skill:feature'],
|
||||
['Fix the bug and use feature.', 'skill:feature'],
|
||||
['Please fix the bug, please use feature.', 'skill:feature'],
|
||||
['Write the regression test, use feature.', 'skill:feature'],
|
||||
['Investigate the bug but use feature.', 'skill:feature'],
|
||||
['For this task, use feature.', 'skill:feature'],
|
||||
["Fix the bug, don't use feature and use shared guidance.", 'skill:shared'],
|
||||
]) {
|
||||
test('a general task preamble preserves a later directive: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.selectedIds, [expectedId]);
|
||||
assert.deepEqual(result.loadedIds, [expectedId]);
|
||||
assert.equal(result.reason, 'auto-selection');
|
||||
assert.equal(result.receipt.autoSelection.exact, true);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const query of [
|
||||
'Fix the bug, shall we use feature?',
|
||||
'Fix the bug, the spec states do not use feature and use shared guidance.',
|
||||
'The docs say fix the bug and use feature.',
|
||||
'Can we fix the bug and use feature?',
|
||||
'The spec stipulates: fix the bug and use feature.',
|
||||
'Our policy stipulates repair the bug and use feature.',
|
||||
'Fix the bug, ought we to repair it and use feature?',
|
||||
'Fix the bug, must we repair it and use feature?',
|
||||
'Fix the bug, feature.',
|
||||
]) {
|
||||
test('a general task preamble cannot promote an indirect citation: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.ok(result.candidates.some(candidate => candidate.id === 'skill:feature'));
|
||||
assert.deepEqual(result.selectedIds, []);
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const prefix of ['database-migrations:', "Don't use database-migrations.", 'Do not use database-migrations.',
|
||||
'Should we use database-migrations?', 'The README says use database-migrations.']) {
|
||||
test('strong retrieval cannot bypass an indirect citation: ' + prefix, () => {
|
||||
const query = prefix + ' Review a PostgreSQL migration that adds an indexed nullable column without downtime.';
|
||||
const result = resolveTaskContext({ task: task({ query }), load: true });
|
||||
const candidate = result.candidates.find(value => value.id === 'skill:database-migrations');
|
||||
assert.ok(candidate && candidate.bm25 >= 20, 'Exercise the BM25 auto-admission threshold');
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.fallback, null);
|
||||
});
|
||||
}
|
||||
|
||||
for (const query of [
|
||||
'For example, use feature',
|
||||
'For instance, use feature.',
|
||||
'As an example, use feature.',
|
||||
'To illustrate, use feature.',
|
||||
'Fix the bug, for example, use feature.',
|
||||
'For example, use feature and use feature.',
|
||||
'For example, use feature but use shared guidance.',
|
||||
"For example, don't use feature but use shared guidance.",
|
||||
'For example, use feature and use feature for this task.',
|
||||
'For example, use feature, but should we use feature for this task?',
|
||||
'For example, use feature, but the docs say use feature for this task.',
|
||||
'For example, use feature, but "use feature for this task" is an example.',
|
||||
'The docs say for example, use feature, but use feature for this task.',
|
||||
'For example, use feature, but use feature "for this task"',
|
||||
'Should we, for example, use shared guidance, but use feature for this task?',
|
||||
'Our policy stipulates, for example, use shared guidance, but use feature for this task.',
|
||||
'e.g., use feature.',
|
||||
'E.g. use feature.',
|
||||
]) {
|
||||
test('an illustrative citation needs an agent decision: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.ok(result.candidates.some(candidate => candidate.id === 'skill:feature'));
|
||||
assert.deepEqual(result.selectedIds, []);
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.reason, 'agent-selection-required');
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const [routingPolicyVersion, query] of [
|
||||
[7, 'Use standard tools, shall we use feature?'],
|
||||
[9, 'For example, use feature'],
|
||||
]) {
|
||||
test('an old policy ' + routingPolicyVersion + ' selected receipt is invalidated after policy changes', () => withFixture(repoRoot => {
|
||||
const first = resolve(repoRoot, { query, explicitIds: ['skill:feature'] });
|
||||
const { digestObject } = require('../../scripts/lib/context-profile-support');
|
||||
const { compileContextProfile } = require('../../scripts/lib/context-profiles');
|
||||
const plan = compileContextProfile({ repoRoot, selectionMode: 'auto' });
|
||||
const bindingDigest = digestObject({ sessionId: 'session-1', taskId: 'task-1', revision: 1,
|
||||
phase: 'implement', planDigest: plan.planDigest, routingPolicyVersion,
|
||||
triggersDigest: digestObject({}), queryDigest: digestObject(query) });
|
||||
const { receiptDigest: _receiptDigest, ...receipt } = first.receipt;
|
||||
const oldReceipt = { ...receipt, bindingDigest, explicitIds: [],
|
||||
selectionDigest: digestObject({ bindingDigest, selectedIds: ['skill:feature'], explicitIds: [] }) };
|
||||
const previous = { ...oldReceipt, receiptDigest: digestObject(oldReceipt) };
|
||||
const result = resolve(repoRoot, { query }, { previous, load: true });
|
||||
assert.equal(result.reused, false);
|
||||
assert.notEqual(result.receipt.bindingDigest, previous.bindingDigest);
|
||||
assert.deepEqual(result.selectedIds, []);
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const [routingPolicyVersion, query, expectedId] of [
|
||||
[6, "Don't use feature, use shared guidance.", 'skill:shared'],
|
||||
[8, 'Fix the bug, use feature', 'skill:feature'],
|
||||
[10, 'For example, use feature, but use feature for this task', 'skill:feature'],
|
||||
]) {
|
||||
test('an old policy ' + routingPolicyVersion + ' empty selection is reconsidered after policy changes', () => withFixture(repoRoot => {
|
||||
const first = resolve(repoRoot, { query });
|
||||
const { digestObject } = require('../../scripts/lib/context-profile-support');
|
||||
const { compileContextProfile } = require('../../scripts/lib/context-profiles');
|
||||
const plan = compileContextProfile({ repoRoot, selectionMode: 'auto' });
|
||||
const bindingDigest = digestObject({ sessionId: 'session-1', taskId: 'task-1', revision: 1,
|
||||
phase: 'implement', planDigest: plan.planDigest, routingPolicyVersion,
|
||||
triggersDigest: digestObject({}), queryDigest: digestObject(query) });
|
||||
const { receiptDigest: _receiptDigest, autoSelection: _autoSelection, ...receipt } = first.receipt;
|
||||
const oldReceipt = { ...receipt, bindingDigest, decision: 'none', selectedIds: [], explicitIds: [],
|
||||
loadedIds: [], resources: [], selectionDigest: digestObject({ bindingDigest, selectedIds: [], explicitIds: [] }) };
|
||||
const previous = { ...oldReceipt, receiptDigest: digestObject(oldReceipt) };
|
||||
const result = resolve(repoRoot, { query }, { previous, load: true });
|
||||
assert.equal(result.reused, false);
|
||||
assert.notEqual(result.receipt.bindingDigest, previous.bindingDigest);
|
||||
assert.deepEqual(result.selectedIds, [expectedId]);
|
||||
assert.deepEqual(result.loadedIds, [expectedId]);
|
||||
assert.equal(result.reason, 'auto-selection');
|
||||
}));
|
||||
}
|
||||
|
||||
for (const query of [
|
||||
'Should we use feature? Use feature.',
|
||||
'Use feature. Should we use feature?',
|
||||
'Use feature, should we use feature?',
|
||||
'Use feature. Shall we use feature?',
|
||||
'Use feature, shall we use shared guidance?',
|
||||
'Shall we use shared guidance? Use feature.',
|
||||
'The docs say use feature. Use feature.',
|
||||
'Use feature. The docs say use feature.',
|
||||
'Use feature. The spec states do not use feature and use shared guidance.',
|
||||
'Use feature. Use the warning that says never use feature.',
|
||||
'The spec states do not use feature and use shared guidance. Use feature.',
|
||||
'The phrase "Use feature." is an example. Use feature.',
|
||||
'The docs say "Use feature." Use feature.',
|
||||
'The docs say "Should we use feature?" Use feature.',
|
||||
`Use feature. The docs say "don’t use feature."`,
|
||||
'Use feature. The phrase "Use feature." is an example.',
|
||||
"Don't use feature. Use feature.",
|
||||
"Don't use feature, use feature.",
|
||||
"Don't use feature and use feature.",
|
||||
"Don't use feature but use feature.",
|
||||
'The docs say never use feature. Use feature.',
|
||||
'Use feature. The docs say never use feature.',
|
||||
'Use feature to inspect the essay.',
|
||||
'Use feature. For example, use shared guidance.',
|
||||
'For example, use shared guidance. Use feature.',
|
||||
"Use feature. For example, don't use feature.",
|
||||
'Use feature to build an example.',
|
||||
'Fix the example, use feature.',
|
||||
'Fix e.g.js, use feature.',
|
||||
"Use feature. E.g., don't use feature.",
|
||||
'For example, use feature, but use feature for this task',
|
||||
'For example, use shared guidance, but please use feature for this task.',
|
||||
'For instance, use shared guidance but use feature for this task.',
|
||||
'For example use shared guidance, but use feature for this task.',
|
||||
'For example, use shared guidance, but use feature for the current task.',
|
||||
'For example, use shared guidance, but use feature for this task, for instance, do not use feature.',
|
||||
'For example, the docs say use shared guidance, but use feature for this task.',
|
||||
]) {
|
||||
test('a genuine directive survives surrounding discussion: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.loadedIds, ['skill:feature']);
|
||||
assert.equal(result.reason, 'auto-selection');
|
||||
assert.equal(result.receipt.autoSelection.exact, true);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const query of [
|
||||
"Use feature. Don't use feature.",
|
||||
'Use feature. Avoid feature.',
|
||||
'Use feature. You should not use feature.',
|
||||
'Use feature. You shouldn\u2019t use feature.',
|
||||
'Use feature, no feature.',
|
||||
'Use feature. For this task, do not use feature.',
|
||||
"Use feature, don't use feature.",
|
||||
"Use feature and don't use feature.",
|
||||
"Use feature but don't use feature.",
|
||||
'Use feature. Never use feature.',
|
||||
"Use feature. Don't use feature. Should we use feature?",
|
||||
'Use feature. For example, use shared guidance, but do not use feature for this task.',
|
||||
'For example, use shared guidance, but use feature for this task and do not use feature.',
|
||||
]) {
|
||||
test('a later rejection withdraws an earlier directive: ' + query, () => withFixture(repoRoot => {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.fallback, null);
|
||||
}));
|
||||
}
|
||||
|
||||
test('a later grouped rejection withdraws a previously requested skill', () => withFixture(repoRoot => {
|
||||
for (const query of ["Use shared. Don't use feature and shared guidance.",
|
||||
'Use shared. Do not use feature and our shared guidance.',
|
||||
'Use shared. Do not use feature and the existing shared guidance.',
|
||||
'Use shared. Do not use feature, manual, and shared guidance.',
|
||||
"Use shared. Don't use feature and the shared skill."]) {
|
||||
const result = resolve(repoRoot, { query }, { load: true });
|
||||
assert.deepEqual(result.loadedIds, [], query);
|
||||
assert.equal(result.fallback, null);
|
||||
}
|
||||
}));
|
||||
|
||||
for (const prefix of [
|
||||
"Don't use the database migration skill.",
|
||||
'Don\u2019t use the database migration skill.',
|
||||
'Never use database migration.',
|
||||
'Should we use the database migration skill?',
|
||||
'The docs say use the database migration skill.',
|
||||
]) {
|
||||
test('singular named references cannot bypass citation review: ' + prefix, () => {
|
||||
const query = prefix + ' Review a PostgreSQL migration that adds an indexed nullable column without downtime.';
|
||||
const result = resolveTaskContext({ task: task({ query }), load: true });
|
||||
const candidate = result.candidates.find(value => value.id === 'skill:database-migrations');
|
||||
assert.ok(candidate && candidate.bm25 >= 20, 'Exercise the BM25 auto-admission threshold');
|
||||
assert.equal(candidate.exact, false, 'Exercise the non-exact candidate path');
|
||||
assert.deepEqual(result.loadedIds, []);
|
||||
assert.equal(result.fallback, null);
|
||||
});
|
||||
}
|
||||
|
||||
test('singular forms do not create exact directive admission', () => {
|
||||
const result = resolveTaskContext({ task: task({ query: 'Use the database migration skill.' }), load: true });
|
||||
assert.ok(result.candidates.some(candidate => candidate.id === 'skill:database-migrations'));
|
||||
assert.ok(!result.receipt.autoSelection || !result.receipt.autoSelection.exact);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user