Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)_

- [x] **voice-corpus-calibration_20260811** (P1) - Point Authentext at
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
12 changes: 8 additions & 4 deletions conductor/tracks/pattern-registry-contracts_20260811/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
8 changes: 8 additions & 0 deletions docs/agent-skills-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
207 changes: 205 additions & 2 deletions scripts/lib/skill-contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -204,6 +215,185 @@ export function validatePatternRecord(record, schema) {
return errors;
}

/**
* @param {unknown[]} records
* @param {Record<string, unknown>} 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<string, unknown>} schema
Expand Down Expand Up @@ -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<string, unknown>} */ (
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;
Expand Down
17 changes: 17 additions & 0 deletions src/document-intelligence/patterns-registry.schema.json
Original file line number Diff line number Diff line change
@@ -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" }
}
}
}
Loading
Loading