diff --git a/conductor/tracks.md b/conductor/tracks.md index aeb3175..00efd46 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -30,6 +30,7 @@ This file tracks all major tracks for the project. Each track has its own detail `src/document-intelligence/`. [#278](https://github.com/edithatogo/authentext/issues/278). Contracts are [PR #293](https://github.com/edithatogo/authentext/pull/293). + Registry seed is [PR #296](https://github.com/edithatogo/authentext/pull/296). _Link: [tracks/pattern-registry-contracts_20260811/index.md](./tracks/pattern-registry-contracts_20260811/index.md)_ - [x] **voice-corpus-calibration_20260811** (P1) - Point Authentext at diff --git a/conductor/tracks/pattern-registry-contracts_20260811/plan.md b/conductor/tracks/pattern-registry-contracts_20260811/plan.md index 1d05de5..918aecf 100644 --- a/conductor/tracks/pattern-registry-contracts_20260811/plan.md +++ b/conductor/tracks/pattern-registry-contracts_20260811/plan.md @@ -15,7 +15,7 @@ registry entry fails `npm run sync` / validate. Concordance is enforced by `validate-skill-contracts.js` against Markdown headings rather than by rewriting `compile-skill.js`. -- [ ] Task: Teach `compile-skill.js` to emit the severity tables from +- [x] Task: Teach `compile-skill.js` to emit the severity tables from `patterns.json`. Keep pattern bodies in modules until a later slice migrates them. - [ ] Task: Migrate pattern bodies in small PRs, not one dump. @@ -44,5 +44,5 @@ - `src/document-intelligence/patterns-registry.schema.json` - `src/document-intelligence/patterns.json` - Concordance check: Markdown headings, severity-table IDs, frontmatter count -- Compiler path that emits severity tables (deferred if `compile-skill.js` is busy) +- Compiler path that emits severity tables from `patterns.json` - Documented Agent Skills contract diff --git a/docs/agent-skills-contract.md b/docs/agent-skills-contract.md index f773367..4fd59a9 100644 --- a/docs/agent-skills-contract.md +++ b/docs/agent-skills-contract.md @@ -48,5 +48,5 @@ Schemas live next to the other document-intelligence contracts: `patterns.json` is the machine-readable seed for the 40 core patterns. The validator checks it against `src/modules/SKILL_CORE_PATTERNS.md` headings, body severities, the severity-table IDs, and the frontmatter `patterns` -count. Pattern bodies stay in the Markdown module until a later compile -slice. +count. `npm run sync` emits the severity tables from that registry. Pattern +bodies stay in the Markdown module until a later compile slice. diff --git a/scripts/compile-skill.js b/scripts/compile-skill.js index 18a51b6..58288d4 100644 --- a/scripts/compile-skill.js +++ b/scripts/compile-skill.js @@ -15,6 +15,12 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { + loadContractJson, + PATTERNS_REGISTRY, + renderSeverityClassification, + replaceSeveritySection, +} from './lib/skill-contracts.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -271,6 +277,11 @@ policy: `; } +function renderedSeverityFromRegistry() { + const registry = loadContractJson(ROOT_DIR, PATTERNS_REGISTRY); + return renderSeverityClassification(registry.patterns); +} + /** * @param {Record} modules @@ -285,7 +296,11 @@ function writeReferenceTree(modules) { continue; } - const body = addReferenceNavigation(stripFrontmatter(moduleContent).trim()); + let stripped = stripFrontmatter(moduleContent).trim(); + if (key === 'core') { + stripped = replaceSeveritySection(stripped, renderedSeverityFromRegistry()); + } + const body = addReferenceNavigation(stripped); const targetPath = path.join(referencesDir, filename); fs.writeFileSync(targetPath, `${body}\n`, 'utf-8'); console.log(`✓ Written: ${OUTPUT.referencesDir}/${filename}`); @@ -306,11 +321,7 @@ function compileStandardSkill(modules) { const strippedCore = stripFrontmatter(modules.core); const intro = buildStandardIntro(strippedCore); - const severity = extractSection( - strippedCore, - 'SEVERITY CLASSIFICATION', - '\n---\n\n_Module Version' - ); + const severity = renderedSeverityFromRegistry(); const detection = extractSection(strippedCore, 'DETECTION GUIDANCE'); const referenceLinks = [ diff --git a/scripts/lib/skill-contracts.js b/scripts/lib/skill-contracts.js index dbb1850..4c6f60a 100644 --- a/scripts/lib/skill-contracts.js +++ b/scripts/lib/skill-contracts.js @@ -394,6 +394,54 @@ export function validatePatternRegistryConcordance(registry, source) { return errors; } +const SEVERITY_TABLE_HEADINGS = Object.freeze({ + critical: '### Critical (immediate AI detection)', + high: '### High (strong AI signals)', + medium: '### Medium (moderate AI signals)', + low: '### Low (weak AI signals)', +}); + +/** + * @param {unknown[]} records + * @returns {string} + */ +export function renderSeverityClassification(records) { + if (!Array.isArray(records)) { + throw new TypeError('pattern records must be an array'); + } + const buckets = { critical: [], high: [], medium: [], low: [] }; + for (const record of [...records].sort((left, right) => left.number - right.number)) { + const label = record.table_label || record.title; + const suffix = + record.must_preserve && !/\(must preserve\)/i.test(label) ? ' (must preserve)' : ''; + buckets[record.severity].push(`- Pattern ${record.number}: ${label}${suffix}`); + } + const parts = ['## SEVERITY CLASSIFICATION', '']; + for (const severity of Object.keys(SEVERITY_TABLE_HEADINGS)) { + parts.push(SEVERITY_TABLE_HEADINGS[severity], '', ...buckets[severity], ''); + } + return parts.join('\n').trim(); +} + +/** + * Replace the compiled severity block while leaving pattern bodies in place. + * @param {string} content + * @param {string} rendered + * @returns {string} + */ +export function replaceSeveritySection(content, rendered) { + const startToken = '\n## SEVERITY CLASSIFICATION\n'; + const start = content.indexOf(startToken); + if (start === -1) { + throw new Error('missing ## SEVERITY CLASSIFICATION section'); + } + const endMarker = '\n---\n\n_Module Version'; + const end = content.indexOf(endMarker, start + 1); + const prefix = content.slice(0, start + 1); + const suffix = end === -1 ? '\n' : content.slice(end); + return `${prefix}${rendered}\n${suffix}`; +} + /** * @param {unknown[]} records * @param {Record} schema diff --git a/src/document-intelligence/pattern.schema.json b/src/document-intelligence/pattern.schema.json index 6dd85fe..d565adf 100644 --- a/src/document-intelligence/pattern.schema.json +++ b/src/document-intelligence/pattern.schema.json @@ -20,6 +20,7 @@ "id": { "type": "string", "pattern": "^pattern-[1-9][0-9]*$" }, "number": { "type": "integer", "minimum": 1 }, "title": { "type": "string", "minLength": 1 }, + "table_label": { "type": "string", "minLength": 1 }, "severity": { "$ref": "#/$defs/severity" }, "must_preserve": { "type": "boolean" }, "domain_applicability": { diff --git a/src/document-intelligence/patterns.json b/src/document-intelligence/patterns.json index f85b9cc..2d4f59d 100644 --- a/src/document-intelligence/patterns.json +++ b/src/document-intelligence/patterns.json @@ -27,7 +27,8 @@ "indelible mark", "deeply rooted" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Undue emphasis on significance" }, { "schema_version": 1, @@ -43,7 +44,8 @@ "written by a leading expert", "active social media presence" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Undue emphasis on notability" }, { "schema_version": 1, @@ -62,7 +64,8 @@ "encompassing...", "showcasing..." ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Superficial -ing analyses" }, { "schema_version": 1, @@ -90,7 +93,8 @@ "must-visit", "stunning" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Promotional language" }, { "schema_version": 1, @@ -117,7 +121,8 @@ ], "false_positive_guards": [ "A single \"This\" or \"This means\" has a clear antecedent you can point to. Demonstratives are ordinary English. The tell is a run of them, or one whose antecedent you cannot name." - ] + ], + "table_label": "Vague attributions and back-references" }, { "schema_version": 1, @@ -133,7 +138,8 @@ "Challenges and Legacy", "Future Outlook" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Formulaic \"Challenges\" sections" }, { "schema_version": 1, @@ -169,7 +175,8 @@ "valuable", "vibrant" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Overused AI vocabulary" }, { "schema_version": 1, @@ -180,7 +187,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": ["serves as/stands as/marks/represents [a]", "boasts/features/offers [a]"], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Copula avoidance" }, { "schema_version": 1, @@ -191,7 +199,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": ["Not only...but", "It's not just about", "It's not merely"], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Negative parallelisms" }, { "schema_version": 1, @@ -202,7 +211,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Rule of three overuse" }, { "schema_version": 1, @@ -213,7 +223,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Elegant variation and repeated openings" }, { "schema_version": 1, @@ -224,7 +235,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "False ranges" }, { "schema_version": 1, @@ -239,7 +251,8 @@ "Used sparingly by a human editor or journalist and not clustered with other sales-y tells.", "Match a user writing sample's em-dash frequency instead of banning dashes.", "Keep annotated-link or definition separators; embedded mode cannot ask, so keep them." - ] + ], + "table_label": "Em dash overuse" }, { "schema_version": 1, @@ -250,7 +263,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Overuse of boldface" }, { "schema_version": 1, @@ -261,7 +275,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Inline-header lists" }, { "schema_version": 1, @@ -272,7 +287,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Title case in headings" }, { "schema_version": 1, @@ -283,7 +299,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Emojis" }, { "schema_version": 1, @@ -294,7 +311,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Quotation mark issues" }, { "schema_version": 1, @@ -313,7 +331,8 @@ "let me know", "here is a..." ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Collaborative communication artifacts" }, { "schema_version": 1, @@ -331,7 +350,8 @@ "maintains a low profile", "keeps personal details private (when unsourced)" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Knowledge-cutoff disclaimers" }, { "schema_version": 1, @@ -342,7 +362,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": ["Great question!", "You're absolutely right", "That's an excellent point"], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Sycophantic tone" }, { "schema_version": 1, @@ -360,7 +381,8 @@ "has the ability to", "It is important to note that" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Filler phrases" }, { "schema_version": 1, @@ -378,7 +400,8 @@ "in some cases it may", "this is an inference" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Excessive hedging" }, { "schema_version": 1, @@ -389,7 +412,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Generic positive conclusions" }, { "schema_version": 1, @@ -406,7 +430,8 @@ "/* AI-generated */", "// Here is the refactored code:" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "AI signatures in code" }, { "schema_version": 1, @@ -422,7 +447,8 @@ "Breakdown:", "Key takeaways: (when used with mechanical lists)" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Over-structuring" }, { "schema_version": 1, @@ -436,7 +462,8 @@ "false_positive_guards": [ "Never modify inline code, fenced code, URLs, paths, versions, hashes, API names, identifiers, CLI flags, config keys, or error messages." ], - "must_preserve": true + "must_preserve": true, + "table_label": "Technical literal preservation (must preserve)" }, { "schema_version": 1, @@ -452,7 +479,8 @@ "What this really means is", "The truth is" ], - "false_positive_guards": ["Used in legitimate contexts like op-eds or presentation scripts."] + "false_positive_guards": ["Used in legitimate contexts like op-eds or presentation scripts."], + "table_label": "Persuasive tropes" }, { "schema_version": 1, @@ -470,7 +498,8 @@ ], "false_positive_guards": [ "Used in legitimate contexts like presentation scripts or tutorials." - ] + ], + "table_label": "Signposting" }, { "schema_version": 1, @@ -481,7 +510,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": ["Used in legitimate contexts like op-eds or persuasive writing."] + "false_positive_guards": ["Used in legitimate contexts like op-eds or persuasive writing."], + "table_label": "Fragmented headers" }, { "schema_version": 1, @@ -502,7 +532,8 @@ ], "false_positive_guards": [ "Model is explicitly asked to show its reasoning in structured format." - ] + ], + "table_label": "Extended thinking tags" }, { "schema_version": 1, @@ -513,7 +544,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": ["Here is the", "json", "JSON:", "as requested"], - "false_positive_guards": ["Actual API responses or configuration files."] + "false_positive_guards": ["Actual API responses or configuration files."], + "table_label": "JSON mode artifacts" }, { "schema_version": 1, @@ -531,7 +563,8 @@ "Running", "invoking" ], - "false_positive_guards": ["Explicit tutorials or documentation about tool usage."] + "false_positive_guards": ["Explicit tutorials or documentation about tool usage."], + "table_label": "Tool use documentation" }, { "schema_version": 1, @@ -549,7 +582,8 @@ "don't hesitate to", "anytime" ], - "false_positive_guards": ["Genuine customer service contexts."] + "false_positive_guards": ["Genuine customer service contexts."], + "table_label": "Over-polished conclusions" }, { "schema_version": 1, @@ -560,7 +594,8 @@ "domain_applicability": ["all"], "mode_carve_outs": [], "trigger_terms": [], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Manufactured punchlines and staccato drama (Upstream #31)" }, { "schema_version": 1, @@ -578,7 +613,8 @@ "the currency of", "the architecture of" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Aphorism formulas (Upstream #32)" }, { "schema_version": 1, @@ -596,7 +632,8 @@ "Let's be honest", "Real talk (when used as standalone hooks or fake-candid pauses before an ordinary point)." ], - "false_positive_guards": ["Genuine conversational speech or quoted dialogue."] + "false_positive_guards": ["Genuine conversational speech or quoted dialogue."], + "table_label": "Conversational rhetorical openers (Upstream #33)" }, { "schema_version": 1, @@ -621,7 +658,8 @@ "It would be easy to just", "Some would suggest" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Diff-anchored writing, shadowboxing, and editorial scar tissue" }, { "schema_version": 1, @@ -643,7 +681,8 @@ "long-term", "end-to-end" ], - "false_positive_guards": [] + "false_positive_guards": [], + "table_label": "Hyphenated word pair overuse (narrowed, upstream)" }, { "schema_version": 1, @@ -656,7 +695,8 @@ "trigger_terms": [], "false_positive_guards": [ "The source already uses a standard imperative (\"Hit reply to opt out.\") or ordinary conversational ellipsis. Do not invent a subject the source never implied. Distinguished from Pattern 35: that pattern is a run of short fragments stacked for drama. This pattern is an isolated missing-subject line, including one produced while trying to sound casual." - ] + ], + "table_label": "Passive voice and subjectless fragments (Upstream §13)" } ] } diff --git a/test/skill-contracts.test.js b/test/skill-contracts.test.js index a51efb8..cc21b64 100644 --- a/test/skill-contracts.test.js +++ b/test/skill-contracts.test.js @@ -22,6 +22,8 @@ import { parseFrontmatterPatternCount, parsePortableFrontmatter, parseSeverityTable, + renderSeverityClassification, + replaceSeveritySection, validateAgainstSchema, validatePackagedSkillLayout, validatePatternRecord, @@ -277,6 +279,24 @@ test('concordance reports missing headings, table gaps, and severity drift', () assert.deepEqual(validatePatternRegistryConcordance(registry.patterns, source), []); }); +test('severity tables compile from the registry without duplicate IDs', () => { + const registry = loadContractJson(ROOT, PATTERNS_REGISTRY); + const rendered = renderSeverityClassification(registry.patterns); + const ids = [...rendered.matchAll(/^- Pattern (\d+):/gm)].map((match) => match[1]); + assert.equal(ids.length, 40); + assert.equal(new Set(ids).size, 40); + assert.match(rendered, /### Critical \(immediate AI detection\)/); + assert.match(rendered, /Pattern 27: Technical literal preservation \(must preserve\)/); + assert.match(rendered, /Pattern 13: Em dash overuse/); + assert.throws(() => renderSeverityClassification({}), /must be an array/); + + const source = fs.readFileSync(path.join(ROOT, CORE_PATTERNS_MODULE), 'utf8'); + const replaced = replaceSeveritySection(source, rendered); + assert.match(replaced, /Pattern 13: Em dash overuse/); + assert.match(replaced, /### Pattern 13: Em\/En Dash Hard Cut/); + assert.throws(() => replaceSeveritySection('# no table\n', rendered), /missing ## SEVERITY/); +}); + test('collectContractErrors fails when the registry file is missing', () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'authentext-no-registry-')); fs.cpSync(path.join(ROOT, CONTRACT_DIR), path.join(fixture, CONTRACT_DIR), { recursive: true });