From d4f3cb9c3617c2fb8ec924f53710015ebe510d70 Mon Sep 17 00:00:00 2001 From: "Dylan Mordaunt (ISLHD)" Date: Wed, 12 Aug 2026 05:32:29 +1000 Subject: [PATCH 1/2] feat(contracts): seed the pattern registry and check headings Keep the 40 pattern bodies in Markdown and fail CI when headings, severity-table IDs, or the frontmatter count drift from patterns.json. Refs #278 Co-authored-by: Cursor --- .gitignore | 1 + conductor/tracks.md | 1 + .../metadata.json | 2 +- .../plan.md | 12 +- docs/agent-skills-contract.md | 8 + scripts/lib/skill-contracts.js | 207 +++++- .../patterns-registry.schema.json | 17 + src/document-intelligence/patterns.json | 662 ++++++++++++++++++ test/document-intelligence-contract.test.js | 2 + test/skill-contracts.test.js | 45 +- 10 files changed, 949 insertions(+), 8 deletions(-) create mode 100644 src/document-intelligence/patterns-registry.schema.json create mode 100644 src/document-intelligence/patterns.json diff --git a/.gitignore b/.gitignore index 9a9e592..cc0a7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ test-*.json pr*.diff pr*.json !src/document-intelligence/profile-registry.json +!src/document-intelligence/patterns.json !src/document-intelligence/protected-span.schema.json !src/document-intelligence/protected-span-classes.json issues*.json diff --git a/conductor/tracks.md b/conductor/tracks.md index e7a3e9d..3904da0 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -29,6 +29,7 @@ This file tracks all major tracks for the project. Each track has its own detail pattern registry and a stricter Agent Skills contract, extending `src/document-intelligence/`. [#278](https://github.com/edithatogo/authentext/issues/278). + Contracts are [PR #293](https://github.com/edithatogo/authentext/pull/293). _Link: [tracks/pattern-registry-contracts_20260811/index.md](./tracks/pattern-registry-contracts_20260811/index.md)_ - [~] **voice-corpus-calibration_20260811** (P1) - Point Authentext at diff --git a/conductor/tracks/pattern-registry-contracts_20260811/metadata.json b/conductor/tracks/pattern-registry-contracts_20260811/metadata.json index 0be54a2..f1e3f84 100644 --- a/conductor/tracks/pattern-registry-contracts_20260811/metadata.json +++ b/conductor/tracks/pattern-registry-contracts_20260811/metadata.json @@ -7,7 +7,7 @@ "status": "in_progress", "created_at": "2026-08-11", "updated_at": "2026-08-12", - "current_phase": "Phase 1: Pattern record schema", + "current_phase": "Phase 2: Compile prose from the registry", "parallel_safe": true, "depends_on": ["editorial-safety-invariants_20260811"], "coordinates_with": [ diff --git a/conductor/tracks/pattern-registry-contracts_20260811/plan.md b/conductor/tracks/pattern-registry-contracts_20260811/plan.md index a1be99d..1d05de5 100644 --- a/conductor/tracks/pattern-registry-contracts_20260811/plan.md +++ b/conductor/tracks/pattern-registry-contracts_20260811/plan.md @@ -4,15 +4,17 @@ - [x] Task: Write failing schema tests for required pattern fields, unique IDs, and severity enums. -- [~] Task: Add `pattern.schema.json` under `src/document-intelligence/` and +- [x] Task: Add `pattern.schema.json` under `src/document-intelligence/` and a first `patterns.json` covering the compiled severity list. Schema landed in PR 1; `patterns.json` seed is PR 2. - [ ] Task: Phase Verification & Checkpoint (Refer to `workflow.md`). ## Phase 2: Compile prose from the registry -- [ ] Task: Write failing tests that a duplicated Low-tier ID or a missing +- [x] Task: Write failing tests that a duplicated Low-tier ID or a missing 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 `patterns.json`. Keep pattern bodies in modules until a later slice migrates them. @@ -39,6 +41,8 @@ ## Handoff artifacts - `src/document-intelligence/pattern.schema.json` -- `src/document-intelligence/patterns.json` (or equivalent) -- Compiler path that emits severity tables +- `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) - Documented Agent Skills contract diff --git a/docs/agent-skills-contract.md b/docs/agent-skills-contract.md index 2b75f73..f773367 100644 --- a/docs/agent-skills-contract.md +++ b/docs/agent-skills-contract.md @@ -40,5 +40,13 @@ Schemas live next to the other document-intelligence contracts: - `src/document-intelligence/agent-skills-portable.schema.json` - `src/document-intelligence/pattern.schema.json` +- `src/document-intelligence/patterns-registry.schema.json` +- `src/document-intelligence/patterns.json` - `src/document-intelligence/protected-span.schema.json` - `src/document-intelligence/evaluation-fixture.schema.json` + +`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. diff --git a/scripts/lib/skill-contracts.js b/scripts/lib/skill-contracts.js index f01c859..dbb1850 100644 --- a/scripts/lib/skill-contracts.js +++ b/scripts/lib/skill-contracts.js @@ -3,20 +3,31 @@ import path from 'node:path'; export const CONTRACT_DIR = 'src/document-intelligence'; export const PATTERN_SCHEMA = 'pattern.schema.json'; +export const PATTERNS_REGISTRY = 'patterns.json'; +export const PATTERNS_REGISTRY_SCHEMA = 'patterns-registry.schema.json'; export const PROTECTED_SPAN_SCHEMA = 'protected-span.schema.json'; export const EVALUATION_FIXTURE_SCHEMA = 'evaluation-fixture.schema.json'; export const AGENT_SKILLS_PORTABLE_SCHEMA = 'agent-skills-portable.schema.json'; export const PROTECTED_SPAN_CATALOG = 'protected-span-classes.json'; +export const CORE_PATTERNS_MODULE = 'src/modules/SKILL_CORE_PATTERNS.md'; export const FORBIDDEN_PORTABLE_FIELDS = Object.freeze(['allowed-tools', 'compatibility']); export const PORTABLE_FIELDS = Object.freeze(['name', 'description', 'license', 'metadata']); const SCHEMA_FILES = [ PATTERN_SCHEMA, + PATTERNS_REGISTRY_SCHEMA, PROTECTED_SPAN_SCHEMA, EVALUATION_FIXTURE_SCHEMA, AGENT_SKILLS_PORTABLE_SCHEMA, ]; +const SEVERITY_ALIASES = Object.freeze({ + critical: 'critical', + high: 'high', + medium: 'medium', + low: 'low', +}); + /** * @param {string} root * @param {string} filename @@ -204,6 +215,185 @@ export function validatePatternRecord(record, schema) { return errors; } +/** + * @param {unknown[]} records + * @param {Record} schema + * @returns {string[]} + */ +export function parseCorePatternHeadings(source) { + const headings = []; + const text = source.replace(/\r\n?/g, '\n'); + const headingRe = /^### Pattern (\d+):\s*(.+)$/gm; + let match; + while ((match = headingRe.exec(text))) { + headings.push({ number: Number(match[1]), title: match[2].trim() }); + } + return headings; +} + +/** + * @param {string} source + * @returns {{ number: number, severity: string, must_preserve: boolean }[]} + */ +export function parsePatternBodySeverities(source) { + const text = source.replace(/\r\n?/g, '\n'); + const headingRe = /^### Pattern (\d+):\s*(.+)$/gm; + const headings = []; + let match; + while ((match = headingRe.exec(text))) { + headings.push({ number: Number(match[1]), index: match.index }); + } + return headings.map((heading, index) => { + const block = text.slice(heading.index, headings[index + 1]?.index ?? text.length); + const severityLine = block.match(/\*\*Severity:\*\*\s*(.+)/); + const raw = severityLine ? severityLine[1].trim() : ''; + return { + number: heading.number, + severity: normalizeSeverity(raw), + must_preserve: /must preserve/i.test(raw), + }; + }); +} + +/** + * @param {string} source + * @returns {{ number: number, title: string, severity: string }[]} + */ +export function parseSeverityTable(source) { + const text = source.replace(/\r\n?/g, '\n'); + const section = text.match(/^## SEVERITY CLASSIFICATION\n([\s\S]*?)(?=\n---\n|\n## )/m); + if (!section) return []; + const rows = []; + let severity = null; + for (const line of section[1].split('\n')) { + const tier = line.match(/^### (Critical|High|Medium|Low)\b/); + if (tier) { + severity = normalizeSeverity(tier[1]); + continue; + } + const row = line.match(/^- Pattern (\d+):\s*(.+)$/); + if (row && severity) { + rows.push({ number: Number(row[1]), title: row[2].trim(), severity }); + } + } + return rows; +} + +/** + * @param {string} source + * @returns {number|null} + */ +export function parseFrontmatterPatternCount(source) { + const yaml = extractFrontmatterSource(source); + if (!yaml) return null; + const match = yaml.match(/^patterns:\s*(\d+)\s*$/m); + return match ? Number(match[1]) : null; +} + +/** + * @param {string} raw + * @returns {string} + */ +function normalizeSeverity(raw) { + const token = raw + .trim() + .toLowerCase() + .split(/[\s(/]/, 1)[0]; + return SEVERITY_ALIASES[token] ?? token; +} + +/** + * @param {unknown} registry + * @param {string} source + * @returns {string[]} + */ +export function validatePatternRegistryConcordance(registry, source) { + const errors = []; + const records = Array.isArray(registry) ? registry : registry?.patterns; + if (!Array.isArray(records)) return ['pattern registry must contain a patterns array']; + + const headings = parseCorePatternHeadings(source); + const bodies = parsePatternBodySeverities(source); + const table = parseSeverityTable(source); + const frontmatterCount = parseFrontmatterPatternCount(source); + + if (frontmatterCount !== records.length) { + errors.push( + `pattern count mismatch: registry=${records.length} frontmatter=${frontmatterCount ?? 'missing'}` + ); + } + if (headings.length !== records.length) { + errors.push(`pattern count mismatch: registry=${records.length} headings=${headings.length}`); + } + + const byNumber = new Map(); + for (const record of records) { + if (record && typeof record === 'object' && typeof record.number === 'number') { + byNumber.set(record.number, record); + } + } + + const headingNumbers = new Set(); + for (const heading of headings) { + headingNumbers.add(heading.number); + const record = byNumber.get(heading.number); + if (!record) { + errors.push(`registry is missing heading Pattern ${heading.number}: ${heading.title}`); + continue; + } + if (record.title !== heading.title) { + errors.push( + `pattern-${heading.number} title does not match heading: registry=${JSON.stringify(record.title)} heading=${JSON.stringify(heading.title)}` + ); + } + } + for (const record of records) { + if (record && typeof record.number === 'number' && !headingNumbers.has(record.number)) { + errors.push(`heading is missing registry ${record.id ?? `pattern-${record.number}`}`); + } + } + + for (const body of bodies) { + const record = byNumber.get(body.number); + if (!record) continue; + if (record.severity !== body.severity) { + errors.push( + `pattern-${body.number} severity does not match body: registry=${record.severity} body=${body.severity}` + ); + } + if (Boolean(record.must_preserve) !== body.must_preserve) { + errors.push( + `pattern-${body.number} must_preserve does not match body: registry=${Boolean(record.must_preserve)} body=${body.must_preserve}` + ); + } + } + + const tableNumbers = new Set(); + for (const row of table) { + if (tableNumbers.has(row.number)) { + errors.push(`severity table has duplicate Pattern ${row.number}`); + } + tableNumbers.add(row.number); + const record = byNumber.get(row.number); + if (!record) { + errors.push(`severity table lists unknown Pattern ${row.number}`); + continue; + } + if (record.severity !== row.severity) { + errors.push( + `pattern-${row.number} severity does not match table: registry=${record.severity} table=${row.severity}` + ); + } + } + for (const record of records) { + if (record && typeof record.number === 'number' && !tableNumbers.has(record.number)) { + errors.push(`severity table is missing ${record.id ?? `pattern-${record.number}`}`); + } + } + + return errors; +} + /** * @param {unknown[]} records * @param {Record} schema @@ -388,11 +578,24 @@ export function collectContractErrors(root) { } } - const registryPath = path.join(root, CONTRACT_DIR, 'patterns.json'); - if (fs.existsSync(registryPath)) { + const registryPath = path.join(root, CONTRACT_DIR, PATTERNS_REGISTRY); + if (!fs.existsSync(registryPath)) { + errors.push(`missing pattern registry: ${CONTRACT_DIR}/${PATTERNS_REGISTRY}`); + } else { const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); + const registrySchema = /** @type {Record} */ ( + loadContractJson(root, PATTERNS_REGISTRY_SCHEMA) + ); + errors.push(...validateAgainstSchema(registry, registrySchema, 'patterns-registry')); const records = Array.isArray(registry) ? registry : registry.patterns; errors.push(...validatePatternRecords(records, patternSchema)); + + const modulePath = path.join(root, CORE_PATTERNS_MODULE); + if (fs.existsSync(modulePath)) { + errors.push( + ...validatePatternRegistryConcordance(registry, fs.readFileSync(modulePath, 'utf8')) + ); + } } return errors; diff --git a/src/document-intelligence/patterns-registry.schema.json b/src/document-intelligence/patterns-registry.schema.json new file mode 100644 index 0000000..87af6ce --- /dev/null +++ b/src/document-intelligence/patterns-registry.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/edithatogo/authentext/blob/main/src/document-intelligence/patterns-registry.schema.json", + "title": "Authentext pattern registry", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "source_module", "patterns"], + "properties": { + "schema_version": { "const": 1 }, + "source_module": { "const": "src/modules/SKILL_CORE_PATTERNS.md" }, + "patterns": { + "type": "array", + "minItems": 1, + "items": { "type": "object" } + } + } +} diff --git a/src/document-intelligence/patterns.json b/src/document-intelligence/patterns.json new file mode 100644 index 0000000..f85b9cc --- /dev/null +++ b/src/document-intelligence/patterns.json @@ -0,0 +1,662 @@ +{ + "schema_version": 1, + "source_module": "src/modules/SKILL_CORE_PATTERNS.md", + "patterns": [ + { + "schema_version": 1, + "id": "pattern-1", + "number": 1, + "title": "Undue Emphasis on Significance", + "severity": "high", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "stands/serves as", + "is a testament/reminder", + "a vital/significant/crucial/pivotal/key role/moment", + "underscores/highlights its importance/significance", + "reflects broader", + "symbolizing its ongoing/enduring/lasting", + "contributing to the", + "setting the stage for", + "marking/shaping the", + "represents/marks a shift", + "key turning point", + "evolving landscape", + "focal point", + "indelible mark", + "deeply rooted" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-2", + "number": 2, + "title": "Undue Emphasis on Notability", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "independent coverage", + "local/regional/national media outlets", + "written by a leading expert", + "active social media presence" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-3", + "number": 3, + "title": "Superficial -ing Analyses", + "severity": "high", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "highlighting/underscoring/emphasizing...", + "ensuring...", + "reflecting/symbolizing...", + "contributing to...", + "cultivating/fostering...", + "encompassing...", + "showcasing..." + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-4", + "number": 4, + "title": "Promotional Language", + "severity": "high", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "boasts a", + "vibrant", + "rich (figurative)", + "profound", + "enhancing its", + "showcasing", + "exemplifies", + "commitment to", + "natural beauty", + "nestled", + "in the heart of", + "groundbreaking (figurative)", + "renowned", + "breathtaking", + "must-visit", + "stunning" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-5", + "number": 5, + "title": "Vague Attributions and Back-References", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "Industry reports", + "Observers have cited", + "Experts argue", + "Some critics argue", + "several sources/publications (when few cited)", + "This ensures", + "This means", + "This allows", + "This makes it", + "This creates", + "This is why", + "This is", + "when the \"this\" points at a whole preceding clause rather than a named thing." + ], + "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." + ] + }, + { + "schema_version": 1, + "id": "pattern-6", + "number": 6, + "title": "Formulaic \"Challenges\" Sections", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "Despite its... faces several challenges...", + "Despite these challenges", + "Challenges and Legacy", + "Future Outlook" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-7", + "number": 7, + "title": "Overused AI Vocabulary", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "Additionally", + "align with", + "commendable", + "crucial", + "delve", + "emphasizing", + "enduring", + "enhance", + "fostering", + "garner", + "highlight (verb)", + "interplay", + "intricate/intricacies", + "key (adjective)", + "landscape (abstract noun)", + "meticulous", + "pivotal", + "quietly", + "showcase", + "tapestry (abstract noun)", + "testament", + "underscore (verb)", + "valuable", + "vibrant" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-8", + "number": 8, + "title": "Copula Avoidance", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": ["serves as/stands as/marks/represents [a]", "boasts/features/offers [a]"], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-9", + "number": 9, + "title": "Negative Parallelisms", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": ["Not only...but", "It's not just about", "It's not merely"], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-10", + "number": 10, + "title": "Rule of Three Overuse", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-11", + "number": 11, + "title": "Elegant Variation and Repeated Sentence Openings", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-12", + "number": 12, + "title": "False Ranges", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-13", + "number": 13, + "title": "Em/En Dash Hard Cut", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": ["—", "–", "--"], + "false_positive_guards": [ + "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." + ] + }, + { + "schema_version": 1, + "id": "pattern-14", + "number": 14, + "title": "Overuse of Boldface", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-15", + "number": 15, + "title": "Inline-Header Vertical Lists", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-16", + "number": 16, + "title": "Title Case in Headings", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-17", + "number": 17, + "title": "Emojis", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-18", + "number": 18, + "title": "Quotation Mark Issues", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-19", + "number": 19, + "title": "Collaborative Communication Artifacts", + "severity": "critical", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "I hope this helps", + "Of course!", + "Certainly!", + "You're absolutely right!", + "Would you like...", + "let me know", + "here is a..." + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-20", + "number": 20, + "title": "Knowledge-Cutoff Disclaimers and Speculative Gap-Filling (includes upstream v2.8 refinement)", + "severity": "critical", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "as of [date]", + "Up to my last training update", + "While specific details are limited/scarce...", + "based on available information...", + "maintains a low profile", + "keeps personal details private (when unsourced)" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-21", + "number": 21, + "title": "Sycophantic Tone", + "severity": "critical", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": ["Great question!", "You're absolutely right", "That's an excellent point"], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-22", + "number": 22, + "title": "Filler Phrases", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "In order to", + "Due to the fact that", + "At this point in time", + "In the event that", + "has the ability to", + "It is important to note that" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-23", + "number": 23, + "title": "Excessive Hedging", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "to be fair", + "it's also possible", + "could potentially", + "might arguably", + "in some cases it may", + "this is an inference" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-24", + "number": 24, + "title": "Generic Positive Conclusions", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-25", + "number": 25, + "title": "AI Signatures in Code", + "severity": "critical", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "// Generated by", + "Produced by", + "Created with [AI Model]", + "/* AI-generated */", + "// Here is the refactored code:" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-26", + "number": 26, + "title": "Over-Structuring", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "In summary", + "Table 1:", + "Breakdown:", + "Key takeaways: (when used with mechanical lists)" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-27", + "number": 27, + "title": "Technical Literal Preservation", + "severity": "critical", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "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 + }, + { + "schema_version": 1, + "id": "pattern-28", + "number": 28, + "title": "Persuasive Tropes", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "The real question is", + "At its core", + "What this really means is", + "The truth is" + ], + "false_positive_guards": ["Used in legitimate contexts like op-eds or presentation scripts."] + }, + { + "schema_version": 1, + "id": "pattern-29", + "number": 29, + "title": "Signposting", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "Let's dive in", + "Here's what you need to know", + "Let's explore", + "In this article we'll" + ], + "false_positive_guards": [ + "Used in legitimate contexts like presentation scripts or tutorials." + ] + }, + { + "schema_version": 1, + "id": "pattern-30", + "number": 30, + "title": "Fragmented Headers", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": ["Used in legitimate contexts like op-eds or persuasive writing."] + }, + { + "schema_version": 1, + "id": "pattern-31", + "number": 31, + "title": "Extended Thinking Tags", + "severity": "high", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "", + "", + "", + "", + "", + "", + "" + ], + "false_positive_guards": [ + "Model is explicitly asked to show its reasoning in structured format." + ] + }, + { + "schema_version": 1, + "id": "pattern-32", + "number": 32, + "title": "JSON Mode Artifacts", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": ["Here is the", "json", "JSON:", "as requested"], + "false_positive_guards": ["Actual API responses or configuration files."] + }, + { + "schema_version": 1, + "id": "pattern-33", + "number": 33, + "title": "Tool Use Documentation", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "I will use", + "I am going to use", + "Calling function", + "Executing", + "Running", + "invoking" + ], + "false_positive_guards": ["Explicit tutorials or documentation about tool usage."] + }, + { + "schema_version": 1, + "id": "pattern-34", + "number": 34, + "title": "Over-Polished Conclusions", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "hope this helps", + "let me know if", + "happy to help", + "feel free to", + "don't hesitate to", + "anytime" + ], + "false_positive_guards": ["Genuine customer service contexts."] + }, + { + "schema_version": 1, + "id": "pattern-35", + "number": 35, + "title": "Manufactured Punchlines and Staccato Drama (Upstream #31)", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-36", + "number": 36, + "title": "Aphorism Formulas (Upstream #32)", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "X is the Y of Z", + "X becomes a trap", + "X is not a tool but a mirror", + "the language of", + "the currency of", + "the architecture of" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-37", + "number": 37, + "title": "Conversational Rhetorical Openers (Upstream #33)", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "Honestly?", + "Look", + "Here's the thing", + "The thing is", + "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."] + }, + { + "schema_version": 1, + "id": "pattern-38", + "number": 38, + "title": "Diff-Anchored Writing, Shadowboxing, and Editorial Scar Tissue", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "This isn't (mainly/really) about", + "I'm not saying/arguing/trying to", + "To be clear", + "Don't get me wrong", + "This is not to say", + "You could argue/frame this differently but", + "Some might say... but", + "A tempting option/approach would be", + "One might be tempted to", + "An obvious approach would be", + "You might think... but", + "It would be easy to just", + "Some would suggest" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-39", + "number": 39, + "title": "Hyphenated Word Pair Overuse (narrowed rule, upstream v2.8)", + "severity": "low", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "trigger_terms": [ + "third-party", + "cross-functional", + "client-facing", + "data-driven", + "decision-making", + "well-known", + "high-quality", + "real-time", + "long-term", + "end-to-end" + ], + "false_positive_guards": [] + }, + { + "schema_version": 1, + "id": "pattern-40", + "number": 40, + "title": "Passive Voice and Subjectless Fragments (Upstream §13, #146)", + "severity": "medium", + "domain_applicability": ["all"], + "mode_carve_outs": [], + "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." + ] + } + ] +} diff --git a/test/document-intelligence-contract.test.js b/test/document-intelligence-contract.test.js index 56cb30a..de99d7e 100644 --- a/test/document-intelligence-contract.test.js +++ b/test/document-intelligence-contract.test.js @@ -52,6 +52,8 @@ test('document profile contract files are checked in and parseable', () => { 'diagnostic-receipt.schema.json', 'guidance-precedence.json', 'pattern.schema.json', + 'patterns-registry.schema.json', + 'patterns.json', 'protected-span.schema.json', 'evaluation-fixture.schema.json', 'agent-skills-portable.schema.json', diff --git a/test/skill-contracts.test.js b/test/skill-contracts.test.js index 5deb116..bd7c580 100644 --- a/test/skill-contracts.test.js +++ b/test/skill-contracts.test.js @@ -8,9 +8,12 @@ import { spawnSync } from 'node:child_process'; import { AGENT_SKILLS_PORTABLE_SCHEMA, CONTRACT_DIR, + CORE_PATTERNS_MODULE, EVALUATION_FIXTURE_SCHEMA, FORBIDDEN_PORTABLE_FIELDS, PATTERN_SCHEMA, + PATTERNS_REGISTRY, + PATTERNS_REGISTRY_SCHEMA, PORTABLE_FIELDS, PROTECTED_SPAN_CATALOG, PROTECTED_SPAN_SCHEMA, @@ -21,6 +24,7 @@ import { validatePackagedSkillLayout, validatePatternRecord, validatePatternRecords, + validatePatternRegistryConcordance, validatePortableFrontmatter, } from '../scripts/lib/skill-contracts.js'; @@ -51,11 +55,12 @@ function runValidator(root) { test('contract schemas are checked in, closed, and name required fields', () => { const pattern = loadContractJson(ROOT, PATTERN_SCHEMA); + const registrySchema = loadContractJson(ROOT, PATTERNS_REGISTRY_SCHEMA); const span = loadContractJson(ROOT, PROTECTED_SPAN_SCHEMA); const fixtures = loadContractJson(ROOT, EVALUATION_FIXTURE_SCHEMA); const portable = loadContractJson(ROOT, AGENT_SKILLS_PORTABLE_SCHEMA); - for (const schema of [pattern, span, fixtures, portable]) { + for (const schema of [pattern, registrySchema, span, fixtures, portable]) { assert.equal(schema.$schema, 'https://json-schema.org/draft/2020-12/schema'); assert.ok(schema.$id.includes('src/document-intelligence/')); } @@ -182,6 +187,44 @@ test('repo contracts validate and the CLI names the Agent Skills spec', () => { assert.match(result.stdout, /https:\/\/agentskills\.io\/specification/); }); +test('checked-in registry matches core-pattern headings and counts', () => { + const registry = loadContractJson(ROOT, PATTERNS_REGISTRY); + const source = fs.readFileSync(path.join(ROOT, CORE_PATTERNS_MODULE), 'utf8'); + assert.deepEqual( + validateAgainstSchema(registry, loadContractJson(ROOT, PATTERNS_REGISTRY_SCHEMA)), + [] + ); + assert.deepEqual(validatePatternRegistryConcordance(registry, source), []); + assert.equal(registry.patterns.length, 40); + const preserved = registry.patterns.find((pattern) => pattern.number === 27); + assert.equal(preserved?.severity, 'critical'); + assert.equal(preserved?.must_preserve, true); +}); + +test('a heading mismatch, duplicate severity-table ID, or count drift fails', () => { + const registry = structuredClone(loadContractJson(ROOT, PATTERNS_REGISTRY)); + const source = fs.readFileSync(path.join(ROOT, CORE_PATTERNS_MODULE), 'utf8'); + + const retitled = structuredClone(registry); + retitled.patterns[0].title = 'Wrong Title'; + assert.match( + validatePatternRegistryConcordance(retitled, source).join('\n'), + /does not match heading/ + ); + + const duplicated = source.replace( + '- Pattern 39: Hyphenated word pair overuse (narrowed, upstream)', + '- Pattern 9: Negative parallelisms\n- Pattern 39: Hyphenated word pair overuse (narrowed, upstream)' + ); + assert.match( + validatePatternRegistryConcordance(registry, duplicated).join('\n'), + /duplicate Pattern 9/ + ); + + const recount = source.replace(/^patterns: 40$/m, 'patterns: 39'); + assert.match(validatePatternRegistryConcordance(registry, recount).join('\n'), /frontmatter=39/); +}); + test('CLI fails on a known-bad portable field', () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'authentext-bad-skill-')); fs.cpSync(path.join(ROOT, CONTRACT_DIR), path.join(fixture, CONTRACT_DIR), { recursive: true }); From 775e90d59d3ef20ac8a24a8ae1f0cf02acce4e8a Mon Sep 17 00:00:00 2001 From: "Dylan Mordaunt (ISLHD)" Date: Wed, 12 Aug 2026 08:17:11 +1000 Subject: [PATCH 2/2] test(contracts): cover registry concordance failure paths Raise patch coverage for heading, severity-table, and missing-registry errors so the Codecov patch check can pass. Co-authored-by: Cursor --- test/skill-contracts.test.js | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/skill-contracts.test.js b/test/skill-contracts.test.js index bd7c580..a51efb8 100644 --- a/test/skill-contracts.test.js +++ b/test/skill-contracts.test.js @@ -19,7 +19,9 @@ import { PROTECTED_SPAN_SCHEMA, collectContractErrors, loadContractJson, + parseFrontmatterPatternCount, parsePortableFrontmatter, + parseSeverityTable, validateAgainstSchema, validatePackagedSkillLayout, validatePatternRecord, @@ -225,6 +227,70 @@ test('a heading mismatch, duplicate severity-table ID, or count drift fails', () assert.match(validatePatternRegistryConcordance(registry, recount).join('\n'), /frontmatter=39/); }); +test('concordance reports missing headings, table gaps, and severity drift', () => { + const registry = structuredClone(loadContractJson(ROOT, PATTERNS_REGISTRY)); + const source = fs.readFileSync(path.join(ROOT, CORE_PATTERNS_MODULE), 'utf8'); + + assert.deepEqual(validatePatternRegistryConcordance({}), [ + 'pattern registry must contain a patterns array', + ]); + assert.deepEqual(parseSeverityTable('# No table\n'), []); + assert.equal(parseFrontmatterPatternCount('# no yaml\n'), null); + assert.equal(parseFrontmatterPatternCount('---\nmodule_id: x\n---\n'), null); + + const missingHeading = structuredClone(registry); + missingHeading.patterns = missingHeading.patterns.filter((pattern) => pattern.number !== 1); + const missingHeadingErrors = validatePatternRegistryConcordance(missingHeading, source).join( + '\n' + ); + assert.match(missingHeadingErrors, /missing heading Pattern 1/); + + const orphan = structuredClone(registry); + orphan.patterns.push({ + schema_version: 1, + number: 99, + title: 'Ghost', + severity: 'low', + }); + const orphanErrors = validatePatternRegistryConcordance(orphan, source).join('\n'); + assert.match(orphanErrors, /heading is missing registry pattern-99/); + assert.match(orphanErrors, /severity table is missing pattern-99/); + + const drifted = structuredClone(registry); + drifted.patterns[0].severity = 'low'; + const driftedErrors = validatePatternRegistryConcordance(drifted, source).join('\n'); + assert.match(driftedErrors, /severity does not match body/); + assert.match(driftedErrors, /severity does not match table/); + + const unmarked = structuredClone(registry); + unmarked.patterns.find((pattern) => pattern.number === 27).must_preserve = false; + assert.match( + validatePatternRegistryConcordance(unmarked, source).join('\n'), + /must_preserve does not match body/ + ); + + const unknownTable = source.replace('- Pattern 39:', '- Pattern 98:'); + const tableErrors = validatePatternRegistryConcordance(registry, unknownTable).join('\n'); + assert.match(tableErrors, /unknown Pattern 98/); + assert.match(tableErrors, /severity table is missing pattern-39/); + + assert.deepEqual(validatePatternRegistryConcordance(registry.patterns, source), []); +}); + +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 }); + fs.cpSync(path.join(ROOT, 'test', 'fixtures'), path.join(fixture, 'test', 'fixtures'), { + recursive: true, + }); + fs.writeFileSync( + path.join(fixture, 'SKILL.md'), + '---\nname: authentext\ndescription: Rewrite prose.\n---\n' + ); + fs.rmSync(path.join(fixture, CONTRACT_DIR, PATTERNS_REGISTRY)); + assert.match(collectContractErrors(fixture).join('\n'), /missing pattern registry/); +}); + test('CLI fails on a known-bad portable field', () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'authentext-bad-skill-')); fs.cpSync(path.join(ROOT, CONTRACT_DIR), path.join(fixture, CONTRACT_DIR), { recursive: true });