From 655249057962e5df47332773e73eb9765d21acd3 Mon Sep 17 00:00:00 2001 From: dd3ok Date: Fri, 31 Jul 2026 15:40:51 +0900 Subject: [PATCH] Harden routing evidence and branding scans --- adapters/antigravity/cli/scripts/stash.mjs | 16 ++- .../ide/skills/stash/scripts/stash.mjs | 16 ++- .../skills/stash/scripts/stash.mjs | 16 ++- adapters/codex/skills/stash/scripts/stash.mjs | 16 ++- benchmarks/routing-golden.jsonl | 4 +- docs/routing.md | 6 +- scripts/check-branding.mjs | 64 +-------- scripts/lib/branding.mjs | 76 ++++++++++ skills/stash/scripts/stash.mjs | 16 ++- src/internal/search.ts | 19 ++- tests/branding.test.ts | 134 ++++++++++++++++++ tests/routing-golden.test.ts | 25 ++++ 12 files changed, 324 insertions(+), 84 deletions(-) create mode 100644 scripts/lib/branding.mjs create mode 100644 tests/branding.test.ts diff --git a/adapters/antigravity/cli/scripts/stash.mjs b/adapters/antigravity/cli/scripts/stash.mjs index 7152369..520256e 100644 --- a/adapters/antigravity/cli/scripts/stash.mjs +++ b/adapters/antigravity/cli/scripts/stash.mjs @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) { } // src/internal/search.ts -var ROUTING_PROFILE_VERSION = 3; +var ROUTING_PROFILE_VERSION = 4; var FIELD_WEIGHTS = { name: 6, alias: 6, @@ -8351,6 +8351,7 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) let score = 0; const matchedTerms = /* @__PURE__ */ new Set(); const matchedKinds = /* @__PURE__ */ new Set(); + const descriptionMatchedTerms = /* @__PURE__ */ new Set(); const reasons = []; const seenReason = /* @__PURE__ */ new Set(); for (const field of prepared.fields) { @@ -8368,6 +8369,9 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) score += termIdf * (weightedTf * 2.2 / (weightedTf + 1.2)); matchedTerms.add(term); matchedKinds.add(field.kind); + if (field.kind === "description") { + descriptionMatchedTerms.add(term); + } if (field.kind !== "group") { const reasonKey = `${field.kind}:${term}`; if (!seenReason.has(reasonKey)) { @@ -8378,7 +8382,13 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) } } score *= negativePenalty(prepared.record, queryTerms); - return { score, matchedTerms, matchedKinds, reasons }; + return { + score, + matchedTerms, + matchedKinds, + descriptionMatchedTerms, + reasons + }; } function normalizedTerms(value) { const normalized = normalizeText(value); @@ -8444,7 +8454,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query, reasons: phrase.reason ? [phrase.reason, ...scored.reasons].slice(0, 8) : scored.reasons.slice(0, 8) }; } - const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"); + const denseDescriptionEvidence = scored.descriptionMatchedTerms.size >= 3; const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description")); const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold; if (materialEvidence && scored.score >= evidenceAdjustedThreshold) { diff --git a/adapters/antigravity/ide/skills/stash/scripts/stash.mjs b/adapters/antigravity/ide/skills/stash/scripts/stash.mjs index 7152369..520256e 100644 --- a/adapters/antigravity/ide/skills/stash/scripts/stash.mjs +++ b/adapters/antigravity/ide/skills/stash/scripts/stash.mjs @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) { } // src/internal/search.ts -var ROUTING_PROFILE_VERSION = 3; +var ROUTING_PROFILE_VERSION = 4; var FIELD_WEIGHTS = { name: 6, alias: 6, @@ -8351,6 +8351,7 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) let score = 0; const matchedTerms = /* @__PURE__ */ new Set(); const matchedKinds = /* @__PURE__ */ new Set(); + const descriptionMatchedTerms = /* @__PURE__ */ new Set(); const reasons = []; const seenReason = /* @__PURE__ */ new Set(); for (const field of prepared.fields) { @@ -8368,6 +8369,9 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) score += termIdf * (weightedTf * 2.2 / (weightedTf + 1.2)); matchedTerms.add(term); matchedKinds.add(field.kind); + if (field.kind === "description") { + descriptionMatchedTerms.add(term); + } if (field.kind !== "group") { const reasonKey = `${field.kind}:${term}`; if (!seenReason.has(reasonKey)) { @@ -8378,7 +8382,13 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) } } score *= negativePenalty(prepared.record, queryTerms); - return { score, matchedTerms, matchedKinds, reasons }; + return { + score, + matchedTerms, + matchedKinds, + descriptionMatchedTerms, + reasons + }; } function normalizedTerms(value) { const normalized = normalizeText(value); @@ -8444,7 +8454,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query, reasons: phrase.reason ? [phrase.reason, ...scored.reasons].slice(0, 8) : scored.reasons.slice(0, 8) }; } - const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"); + const denseDescriptionEvidence = scored.descriptionMatchedTerms.size >= 3; const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description")); const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold; if (materialEvidence && scored.score >= evidenceAdjustedThreshold) { diff --git a/adapters/claude-code/skills/stash/scripts/stash.mjs b/adapters/claude-code/skills/stash/scripts/stash.mjs index 7152369..520256e 100644 --- a/adapters/claude-code/skills/stash/scripts/stash.mjs +++ b/adapters/claude-code/skills/stash/scripts/stash.mjs @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) { } // src/internal/search.ts -var ROUTING_PROFILE_VERSION = 3; +var ROUTING_PROFILE_VERSION = 4; var FIELD_WEIGHTS = { name: 6, alias: 6, @@ -8351,6 +8351,7 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) let score = 0; const matchedTerms = /* @__PURE__ */ new Set(); const matchedKinds = /* @__PURE__ */ new Set(); + const descriptionMatchedTerms = /* @__PURE__ */ new Set(); const reasons = []; const seenReason = /* @__PURE__ */ new Set(); for (const field of prepared.fields) { @@ -8368,6 +8369,9 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) score += termIdf * (weightedTf * 2.2 / (weightedTf + 1.2)); matchedTerms.add(term); matchedKinds.add(field.kind); + if (field.kind === "description") { + descriptionMatchedTerms.add(term); + } if (field.kind !== "group") { const reasonKey = `${field.kind}:${term}`; if (!seenReason.has(reasonKey)) { @@ -8378,7 +8382,13 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) } } score *= negativePenalty(prepared.record, queryTerms); - return { score, matchedTerms, matchedKinds, reasons }; + return { + score, + matchedTerms, + matchedKinds, + descriptionMatchedTerms, + reasons + }; } function normalizedTerms(value) { const normalized = normalizeText(value); @@ -8444,7 +8454,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query, reasons: phrase.reason ? [phrase.reason, ...scored.reasons].slice(0, 8) : scored.reasons.slice(0, 8) }; } - const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"); + const denseDescriptionEvidence = scored.descriptionMatchedTerms.size >= 3; const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description")); const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold; if (materialEvidence && scored.score >= evidenceAdjustedThreshold) { diff --git a/adapters/codex/skills/stash/scripts/stash.mjs b/adapters/codex/skills/stash/scripts/stash.mjs index 7152369..520256e 100644 --- a/adapters/codex/skills/stash/scripts/stash.mjs +++ b/adapters/codex/skills/stash/scripts/stash.mjs @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) { } // src/internal/search.ts -var ROUTING_PROFILE_VERSION = 3; +var ROUTING_PROFILE_VERSION = 4; var FIELD_WEIGHTS = { name: 6, alias: 6, @@ -8351,6 +8351,7 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) let score = 0; const matchedTerms = /* @__PURE__ */ new Set(); const matchedKinds = /* @__PURE__ */ new Set(); + const descriptionMatchedTerms = /* @__PURE__ */ new Set(); const reasons = []; const seenReason = /* @__PURE__ */ new Set(); for (const field of prepared.fields) { @@ -8368,6 +8369,9 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) score += termIdf * (weightedTf * 2.2 / (weightedTf + 1.2)); matchedTerms.add(term); matchedKinds.add(field.kind); + if (field.kind === "description") { + descriptionMatchedTerms.add(term); + } if (field.kind !== "group") { const reasonKey = `${field.kind}:${term}`; if (!seenReason.has(reasonKey)) { @@ -8378,7 +8382,13 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) } } score *= negativePenalty(prepared.record, queryTerms); - return { score, matchedTerms, matchedKinds, reasons }; + return { + score, + matchedTerms, + matchedKinds, + descriptionMatchedTerms, + reasons + }; } function normalizedTerms(value) { const normalized = normalizeText(value); @@ -8444,7 +8454,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query, reasons: phrase.reason ? [phrase.reason, ...scored.reasons].slice(0, 8) : scored.reasons.slice(0, 8) }; } - const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"); + const denseDescriptionEvidence = scored.descriptionMatchedTerms.size >= 3; const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description")); const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold; if (materialEvidence && scored.score >= evidenceAdjustedThreshold) { diff --git a/benchmarks/routing-golden.jsonl b/benchmarks/routing-golden.jsonl index ce30449..abfac5a 100644 --- a/benchmarks/routing-golden.jsonl +++ b/benchmarks/routing-golden.jsonl @@ -6,7 +6,9 @@ {"id":"broad-generic-term","kind":"search","query":"design","expectedRelevant":["design-game-encounters","design-system"]} {"id":"short-ui-term-respects-boundaries","kind":"search","query":"ui","expectedRelevant":["design-system","ui-craft"]} {"id":"short-go-term-abstains","kind":"search","query":"go","expectedRelevant":[]} -{"id":"dense-description-three-term-material","kind":"search","query":"generate voice audio","expectedRelevant":["elevenlabs-tts"]} +{"id":"dense-description-three-term-material","kind":"search","query":"generate voice audio","expectedRelevant":["elevenlabs-tts"],"expectedPossible":["media-options-guide","narration-workflows","recording-cleanup","waveform-previews"]} +{"id":"dense-description-overlap-is-monotonic","kind":"search","query":"synthesize timbre cadence","expectedRelevant":["cadence-synthesizer","timbre-synthesizer"],"expectedPossible":["discouraged-synthesizer","synthesis-previews"]} +{"id":"dense-description-overlap-no-match","kind":"search","query":"synthesize quantum fragrance","expectedRelevant":[]} {"id":"typo-fallback","kind":"search","query":"security-audi","expectedRelevant":["security-audit"]} {"id":"negative-example-demotion","kind":"search","query":"database migration review","expectedRelevant":["api-contract-review","database-migration-review"],"expectedFirst":"database-migration-review"} {"id":"source-id-discovers-all","kind":"search","query":"mengto","expectedRelevant":["create-brand-world","design-game-encounters"]} diff --git a/docs/routing.md b/docs/routing.md index 48ff1f1..7edc7b0 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -21,7 +21,7 @@ It never invokes natural-language reranking. ## Discovery -Current routing profile: `3`. +Current routing profile: `4`. Discovery uses BM25F-style scoring with initial weights: @@ -40,8 +40,8 @@ A score alone cannot make a result relevant. The evidence gate also requires: - whole-term phrase evidence in name/alias; or - multiple query terms across multiple fields with a high-priority field; or -- at least three query terms in a description, above the dense-description - threshold; or +- at least three distinct query terms in a description, above the + dense-description threshold, even when another field also matches; or - a single specific term in name, alias, intent, or tag. Single-term description-only and group-only generic matches remain `possible`. diff --git a/scripts/check-branding.mjs b/scripts/check-branding.mjs index a0361fc..254c555 100644 --- a/scripts/check-branding.mjs +++ b/scripts/check-branding.mjs @@ -1,68 +1,12 @@ -import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { scanBranding } from "./lib/branding.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const brandSeparator = String.raw`(?:[-_]|\s)+`; -const forbiddenBrand = new RegExp( - ["agent", "skills", "stash"].join(brandSeparator), - "iu", -); -const ignoredDirectories = new Set([ - "coverage", - "dist", - "node_modules", -]); -const files = []; -const violations = new Set(); +const { violations } = await scanBranding(root); -function shouldIgnoreDirectory(name) { - return ignoredDirectories.has(name) || name.startsWith(".stash-cache"); -} - -function relativePath(filePath) { - return path.relative(root, filePath).split(path.sep).join("/"); -} - -async function walk(directory) { - for (const entry of await readdir(directory, { withFileTypes: true })) { - if ( - entry.name === ".git" || - (entry.isDirectory() && shouldIgnoreDirectory(entry.name)) - ) { - continue; - } - const entryPath = path.join(directory, entry.name); - if (entry.isFile() && entry.name.endsWith(".tgz")) { - continue; - } - const relative = relativePath(entryPath); - if (forbiddenBrand.test(relative)) { - violations.add(`${relative} (path)`); - } - if (entry.isDirectory()) { - await walk(entryPath); - } else if (entry.isFile()) { - files.push(entryPath); - } - } -} - -await walk(root); - -for (const file of files) { - const content = await readFile(file); - if (content.includes(0)) { - continue; - } - if (forbiddenBrand.test(content.toString("utf8"))) { - violations.add(relativePath(file)); - } -} - -if (violations.size > 0) { - const sortedViolations = [...violations].sort(); +if (violations.length > 0) { throw new Error( - `Legacy three-part brand remains in: ${sortedViolations.join(", ")}`, + `Legacy three-part brand remains in: ${violations.join(", ")}`, ); } diff --git a/scripts/lib/branding.mjs b/scripts/lib/branding.mjs new file mode 100644 index 0000000..0e684d6 --- /dev/null +++ b/scripts/lib/branding.mjs @@ -0,0 +1,76 @@ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; + +const brandParts = ["agent", "skills", "stash"]; +const contentSeparator = String.raw`(?:[-_]|\s)+`; +const pathSeparator = String.raw`(?:[-_/\\]|\s)+`; +const forbiddenContentBrand = new RegExp( + brandParts.join(contentSeparator), + "iu", +); +const forbiddenPathBrand = new RegExp( + brandParts.join(pathSeparator), + "iu", +); +const ignoredDirectories = new Set([ + "coverage", + "dist", + "node_modules", +]); + +function shouldIgnoreDirectory(name) { + return ignoredDirectories.has(name) || name.startsWith(".stash-cache"); +} + +export async function scanBranding(root) { + // The checkout root belongs to the environment, so scan descendants only. + const resolvedRoot = path.resolve(root); + const files = []; + const violations = new Set(); + + function relativePath(filePath) { + return path.relative(resolvedRoot, filePath).split(path.sep).join("/"); + } + + async function walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if ( + entry.name === ".git" || + (entry.isDirectory() && shouldIgnoreDirectory(entry.name)) + ) { + continue; + } + const entryPath = path.join(directory, entry.name); + if (entry.isFile() && entry.name.endsWith(".tgz")) { + continue; + } + const relative = relativePath(entryPath); + if (forbiddenPathBrand.test(relative)) { + violations.add(`${relative} (path)`); + } + // Check the link path above without traversing or reading its target. + if (entry.isSymbolicLink()) { + continue; + } + if (entry.isDirectory()) { + await walk(entryPath); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + } + + await walk(resolvedRoot); + + for (const file of files) { + const content = await readFile(file); + if (content.includes(0)) { + continue; + } + if (forbiddenContentBrand.test(content.toString("utf8"))) { + violations.add(relativePath(file)); + } + } + + return { violations: [...violations].sort() }; +} diff --git a/skills/stash/scripts/stash.mjs b/skills/stash/scripts/stash.mjs index 7152369..520256e 100644 --- a/skills/stash/scripts/stash.mjs +++ b/skills/stash/scripts/stash.mjs @@ -8251,7 +8251,7 @@ async function acquireLock(lockPath) { } // src/internal/search.ts -var ROUTING_PROFILE_VERSION = 3; +var ROUTING_PROFILE_VERSION = 4; var FIELD_WEIGHTS = { name: 6, alias: 6, @@ -8351,6 +8351,7 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) let score = 0; const matchedTerms = /* @__PURE__ */ new Set(); const matchedKinds = /* @__PURE__ */ new Set(); + const descriptionMatchedTerms = /* @__PURE__ */ new Set(); const reasons = []; const seenReason = /* @__PURE__ */ new Set(); for (const field of prepared.fields) { @@ -8368,6 +8369,9 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) score += termIdf * (weightedTf * 2.2 / (weightedTf + 1.2)); matchedTerms.add(term); matchedKinds.add(field.kind); + if (field.kind === "description") { + descriptionMatchedTerms.add(term); + } if (field.kind !== "group") { const reasonKey = `${field.kind}:${term}`; if (!seenReason.has(reasonKey)) { @@ -8378,7 +8382,13 @@ function scoreRecord(prepared, totalRecords, frequenciesByDocument, queryTerms) } } score *= negativePenalty(prepared.record, queryTerms); - return { score, matchedTerms, matchedKinds, reasons }; + return { + score, + matchedTerms, + matchedKinds, + descriptionMatchedTerms, + reasons + }; } function normalizedTerms(value) { const normalized = normalizeText(value); @@ -8444,7 +8454,7 @@ function classifyCandidate(prepared, totalRecords, frequenciesByDocument, query, reasons: phrase.reason ? [phrase.reason, ...scored.reasons].slice(0, 8) : scored.reasons.slice(0, 8) }; } - const denseDescriptionEvidence = scored.matchedTerms.size >= 3 && scored.matchedKinds.size === 1 && scored.matchedKinds.has("description"); + const denseDescriptionEvidence = scored.descriptionMatchedTerms.size >= 3; const materialEvidence = scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) || denseDescriptionEvidence || queryTerms.length === 1 && highPriorityMatch(scored.matchedKinds) && !(scored.matchedKinds.size === 1 && scored.matchedKinds.has("description")); const evidenceAdjustedThreshold = denseDescriptionEvidence ? materialScoreThreshold * 0.58 : scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && highPriorityMatch(scored.matchedKinds) ? materialScoreThreshold * 0.75 : materialScoreThreshold; if (materialEvidence && scored.score >= evidenceAdjustedThreshold) { diff --git a/src/internal/search.ts b/src/internal/search.ts index 43ba43f..f3de170 100644 --- a/src/internal/search.ts +++ b/src/internal/search.ts @@ -27,7 +27,7 @@ interface SearchResult { expandedTerms: string[]; } -export const ROUTING_PROFILE_VERSION = 3 as const; +export const ROUTING_PROFILE_VERSION = 4 as const; const FIELD_WEIGHTS = { name: 6, @@ -163,11 +163,13 @@ function scoreRecord( score: number; matchedTerms: Set; matchedKinds: Set; + descriptionMatchedTerms: Set; reasons: RelevanceReason[]; } { let score = 0; const matchedTerms = new Set(); const matchedKinds = new Set(); + const descriptionMatchedTerms = new Set(); const reasons: RelevanceReason[] = []; const seenReason = new Set(); @@ -186,6 +188,9 @@ function scoreRecord( score += termIdf * ((weightedTf * 2.2) / (weightedTf + 1.2)); matchedTerms.add(term); matchedKinds.add(field.kind); + if (field.kind === "description") { + descriptionMatchedTerms.add(term); + } if (field.kind !== "group") { const reasonKey = `${field.kind}:${term}`; if (!seenReason.has(reasonKey)) { @@ -197,7 +202,13 @@ function scoreRecord( } score *= negativePenalty(prepared.record, queryTerms); - return { score, matchedTerms, matchedKinds, reasons }; + return { + score, + matchedTerms, + matchedKinds, + descriptionMatchedTerms, + reasons, + }; } function normalizedTerms(value: string): string[] { @@ -302,9 +313,7 @@ function classifyCandidate( } const denseDescriptionEvidence = - scored.matchedTerms.size >= 3 && - scored.matchedKinds.size === 1 && - scored.matchedKinds.has("description"); + scored.descriptionMatchedTerms.size >= 3; const materialEvidence = (scored.matchedTerms.size >= 2 && scored.matchedKinds.size >= 2 && diff --git a/tests/branding.test.ts b/tests/branding.test.ts new file mode 100644 index 0000000..c08f505 --- /dev/null +++ b/tests/branding.test.ts @@ -0,0 +1,134 @@ +import assert from "node:assert/strict"; +import { + mkdir, + mkdtemp, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { scanBranding } from "../scripts/lib/branding.mjs"; + +const brandParts = ["agent", "skills", "stash"] as const; + +async function withTemporaryRoot( + run: (root: string) => Promise, +): Promise { + const root = await mkdtemp(path.join(os.tmpdir(), "stash-branding-")); + try { + await run(root); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +test("branding scan detects forbidden content and descendant paths", async () => { + await withTemporaryRoot(async (root) => { + const contentVariants = [ + brandParts.join(" "), + brandParts.join("\t"), + brandParts.join("\n"), + brandParts.join("-"), + brandParts.join("_"), + ]; + for (const [index, content] of contentVariants.entries()) { + await writeFile(path.join(root, `content-${index}.txt`), content); + } + + const forbiddenFile = `${brandParts.join("_")}.txt`; + await writeFile(path.join(root, forbiddenFile), "safe"); + const nestedDirectory = path.join(root, ...brandParts); + await mkdir(nestedDirectory, { recursive: true }); + await writeFile(path.join(nestedDirectory, "safe.txt"), "safe"); + const forbiddenBackslashFile = + process.platform === "win32" ? undefined : brandParts.join("\\"); + if (forbiddenBackslashFile) { + await writeFile(path.join(root, forbiddenBackslashFile), "safe"); + } + + const result = await scanBranding(root); + + for (const index of contentVariants.keys()) { + assert.ok(result.violations.includes(`content-${index}.txt`)); + } + assert.ok(result.violations.includes(`${forbiddenFile} (path)`)); + assert.ok( + result.violations.includes(`${brandParts.join("/")} (path)`), + ); + if (forbiddenBackslashFile) { + assert.ok( + result.violations.includes(`${forbiddenBackslashFile} (path)`), + ); + } + }); +}); + +test("branding scan honors ignored, archive, binary, git, and root boundaries", async () => { + await withTemporaryRoot(async (parent) => { + const forbidden = brandParts.join("-"); + const root = path.join(parent, forbidden); + await mkdir(root); + await writeFile(path.join(root, "safe.txt"), "safe"); + + for (const ignored of [ + "coverage", + "dist", + "node_modules", + ".stash-cache-test", + ]) { + const ignoredRoot = path.join(root, ignored); + await mkdir(ignoredRoot); + await writeFile(path.join(ignoredRoot, "ignored.txt"), forbidden); + } + + await writeFile(path.join(root, `${forbidden}.tgz`), forbidden); + await writeFile( + path.join(root, "binary.bin"), + Buffer.concat([Buffer.from([0]), Buffer.from(forbidden)]), + ); + + const gitDirectory = path.join(root, ".git"); + await mkdir(gitDirectory); + await writeFile(path.join(gitDirectory, "config"), forbidden); + + const gitfileRoot = path.join(root, "worktree"); + await mkdir(gitfileRoot); + await writeFile( + path.join(gitfileRoot, ".git"), + `gitdir: C:/work/${forbidden}/.git/worktrees/example`, + ); + + assert.deepEqual(await scanBranding(root), { violations: [] }); + }); +}); + +test("branding scan checks symlink paths without following targets", async (t) => { + await withTemporaryRoot(async (root) => { + const forbidden = brandParts.join("-"); + const target = path.join(path.dirname(root), `${path.basename(root)}-target`); + await mkdir(target); + await writeFile(path.join(target, "outside.txt"), forbidden); + try { + try { + const linkType = process.platform === "win32" ? "junction" : "dir"; + await symlink(target, path.join(root, "safe-link"), linkType); + await symlink(target, path.join(root, forbidden), linkType); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === "EACCES" || code === "EPERM") { + t.skip("symlinks are unavailable in this environment"); + return; + } + throw error; + } + + assert.deepEqual(await scanBranding(root), { + violations: [`${forbidden} (path)`], + }); + } finally { + await rm(target, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/routing-golden.test.ts b/tests/routing-golden.test.ts index 583e461..70ceb65 100644 --- a/tests/routing-golden.test.ts +++ b/tests/routing-golden.test.ts @@ -103,6 +103,31 @@ source: examples: negative: - generate voice audio +`, + }, + { + group: "cadence-tools", + name: "timbre-synthesizer", + description: "Synthesize timbre cadence.", + }, + { + group: "studio", + name: "cadence-synthesizer", + description: "Synthesize timbre cadence.", + }, + { + group: "cadence-tools", + name: "synthesis-previews", + description: "Synthesize timbre previews.", + }, + { + group: "cadence-tools", + name: "discouraged-synthesizer", + description: "Synthesize timbre cadence.", + sidecar: `schemaVersion: 1 +examples: + negative: + - synthesize timbre cadence `, }, {