[Proposal] Schema-Driven Artifact Validation
Problem
openspec validate only validates spec and change artifacts. The rules are hardcoded in validator.ts and constants.ts. The validator never reads the schema.
Custom artifacts (research.md, adr.md, whatever a schema author creates) get zero validation. Built-in rules like MIN_PURPOSE_LENGTH = 50 or the required ## Purpose heading can't be tuned without editing the source. Adding a third artifact type means editing the source too; validateByType branches on two string literals ('change' / 'spec').
ArtifactSchema has id, generates, description, template, instruction, requires. No validation fields. There is nowhere for a schema author to say "this artifact needs these headings" or "this section must be at least N characters."
Relationship to Existing Issues
#666: Configurable Spec Format
Issue #666 proposes making the parser configurable: requirement/scenario header patterns, normative keywords, and section names. It also proposes sections.required and sections.optional fields on artifacts, which overlap with this proposal.
The sections fields in #666 configure the parser (how OpenSpec reads spec structure). This proposal's validation block configures enforcement (what openspec validate checks on any artifact). The field shapes are similar and should be unified in a final implementation. The work is separable regardless: this proposal doesn't touch parsers, and #666 doesn't address validation of non-spec artifacts.
This proposal uses a separate validation block. If #666 lands first and establishes an artifact-level sections block, these fields can nest there instead.
#777: Hardcoded Artifact Patterns Override Schema instruction Field
Issue #777 is about skill orchestration ignoring instruction fields. This proposal is about the validator ignoring schema-declared rules. Different system, same root cause.
#783: Cross-Artifact Quality Review
Semantic consistency checks across artifacts (contradictions, scope drift, duplication). This proposal handles per-artifact structural validation only. It can't detect "proposal says X but design says not-X." That's covered in Future Extensions below.
Proposed Schema Extension
The validation block is optional on any artifact in schema.yaml. Omit it to skip structural checks entirely.
artifacts:
- id: proposal
generates: proposal.md
template: proposal.md
requires: []
validation:
sections:
required: ["Why", "What Changes"]
optional: ["Impact", "Capabilities"]
minSectionLength:
Why: 50
requiredPatterns:
- pattern: "\\*\\*BREAKING\\*\\*"
description: "Breaking changes must be flagged."
scope: "What Changes"
level: "WARNING"
- id: specs
generates: "specs/**/*.md"
template: spec.md
requires: [proposal]
validation:
sections:
required: ["Purpose", "Requirements"]
optional: ["Scope", "Definitions"]
minSectionLength:
Purpose: 50
- id: design
generates: design.md
template: design.md
requires: [proposal]
validation:
sections:
required: ["Context", "Decisions"]
optional: ["Goals / Non-Goals", "Risks / Trade-offs", "Migration Plan", "Open Questions"]
- id: tasks
generates: tasks.md
template: tasks.md
requires: [specs, design]
# no validation block = no structural checks
Fields
| Field |
Type |
Default |
What it does |
validation |
object |
(omitted) |
Validation config for this artifact. |
sections.required |
string[] |
[] |
Headings (## Heading) that must exist. |
sections.optional |
string[] |
[] |
Recognised headings. Any ## heading not in required or optional triggers a WARNING. If optional is empty, no unknown-heading warnings are emitted. |
minSectionLength |
Record<string, number> |
{} |
Min character count for content under a named heading. |
requiredPatterns |
object[] |
[] |
Regex patterns that must match. |
requiredPatterns[].pattern |
string |
(required) |
The regex. |
requiredPatterns[].description |
string |
(required) |
Why the pattern matters (shown in error output). |
requiredPatterns[].scope |
string |
whole doc |
Limit the match to one section. |
requiredPatterns[].level |
"ERROR" | "WARNING" | "INFO" |
"ERROR" |
Severity when the pattern isn't found. |
Affected Code
Add an optional validation field to ArtifactSchema with Zod definitions for the fields above.
Add a validation block to specs that reproduces the current hardcoded behavior (backward compatibility baseline), and add blocks to proposal and design as new example declarations for schema authors.
New method: validateArtifact(filePath, artifactDef). Parses markdown into a heading-to-content map, checks required sections, checks min lengths, checks patterns. Returns ValidationIssue[].
applySpecRules and applyChangeRules stay untouched for now. Migrating them is a separate follow-up after this lands.
validateByType needs to load the active schema for the change being validated. The resolution chain already exists: each change stores its schema name in .openspec.yaml (via change-metadata.ts), then resolveSchema looks it up in project-local → user-override → package built-in order. Once the schema is loaded, the command iterates over artifacts that have a validation block, finds files matching generates, and calls validateArtifact.
New message templates:
SECTION_MISSING: 'Required section "{section}" not found'
SECTION_TOO_SHORT: 'Section "{section}" has {actual} characters, minimum is {min}'
PATTERN_NOT_FOUND: 'Required pattern not found: {description}'
Backward Compatibility
Every artifact except spec and change has no structural checks. Omitting validation preserves that. Existing schemas work without modification.
applySpecRules and applyChangeRules stay in place. Both paths run and their issues are merged. spec and change validation is additive until a follow-up migrates the hardcoded logic.
Future Extensions (Out of Scope)
- Filename convention validation. Enforce naming patterns on artifact files (e.g.,
specs/{kebab-case}/spec.md).
- Filename ID uniqueness. Prevent duplicate IDs across files under the same
generates glob.
- Migrate hardcoded rules to schema. Moving
applySpecRules / applyChangeRules into validation blocks makes the built-in schema fully self-describing. Partially blocked on #666.
- Cross-artifact validation. Rules that span artifacts (e.g., every capability in
proposal.md needs a matching specs/{name}/spec.md) require a graph-aware validation pass. See #783.
[Proposal] Schema-Driven Artifact Validation
Problem
openspec validateonly validatesspecandchangeartifacts. The rules are hardcoded invalidator.tsandconstants.ts. The validator never reads the schema.Custom artifacts (
research.md,adr.md, whatever a schema author creates) get zero validation. Built-in rules likeMIN_PURPOSE_LENGTH = 50or the required## Purposeheading can't be tuned without editing the source. Adding a third artifact type means editing the source too;validateByTypebranches on two string literals ('change'/'spec').ArtifactSchemahasid,generates,description,template,instruction,requires. No validation fields. There is nowhere for a schema author to say "this artifact needs these headings" or "this section must be at least N characters."Relationship to Existing Issues
#666: Configurable Spec Format
Issue #666 proposes making the parser configurable: requirement/scenario header patterns, normative keywords, and section names. It also proposes
sections.requiredandsections.optionalfields on artifacts, which overlap with this proposal.The
sectionsfields in #666 configure the parser (how OpenSpec reads spec structure). This proposal'svalidationblock configures enforcement (whatopenspec validatechecks on any artifact). The field shapes are similar and should be unified in a final implementation. The work is separable regardless: this proposal doesn't touch parsers, and #666 doesn't address validation of non-spec artifacts.This proposal uses a separate
validationblock. If #666 lands first and establishes an artifact-levelsectionsblock, these fields can nest there instead.#777: Hardcoded Artifact Patterns Override Schema
instructionFieldIssue #777 is about skill orchestration ignoring
instructionfields. This proposal is about the validator ignoring schema-declared rules. Different system, same root cause.#783: Cross-Artifact Quality Review
Semantic consistency checks across artifacts (contradictions, scope drift, duplication). This proposal handles per-artifact structural validation only. It can't detect "proposal says X but design says not-X." That's covered in Future Extensions below.
Proposed Schema Extension
The
validationblock is optional on any artifact inschema.yaml. Omit it to skip structural checks entirely.Fields
validationsections.requiredstring[][]## Heading) that must exist.sections.optionalstring[][]##heading not inrequiredoroptionaltriggers a WARNING. Ifoptionalis empty, no unknown-heading warnings are emitted.minSectionLengthRecord<string, number>{}requiredPatternsobject[][]requiredPatterns[].patternstringrequiredPatterns[].descriptionstringrequiredPatterns[].scopestringrequiredPatterns[].level"ERROR" | "WARNING" | "INFO""ERROR"Affected Code
types.tsAdd an optional
validationfield toArtifactSchemawith Zod definitions for the fields above.schema.yamlAdd a
validationblock tospecsthat reproduces the current hardcoded behavior (backward compatibility baseline), and add blocks toproposalanddesignas new example declarations for schema authors.validator.tsNew method:
validateArtifact(filePath, artifactDef). Parses markdown into a heading-to-content map, checks required sections, checks min lengths, checks patterns. ReturnsValidationIssue[].applySpecRulesandapplyChangeRulesstay untouched for now. Migrating them is a separate follow-up after this lands.validate.tsvalidateByTypeneeds to load the active schema for the change being validated. The resolution chain already exists: each change stores its schema name in.openspec.yaml(viachange-metadata.ts), thenresolveSchemalooks it up in project-local → user-override → package built-in order. Once the schema is loaded, the command iterates over artifacts that have avalidationblock, finds files matchinggenerates, and callsvalidateArtifact.constants.tsNew message templates:
SECTION_MISSING: 'Required section "{section}" not found'SECTION_TOO_SHORT: 'Section "{section}" has {actual} characters, minimum is {min}'PATTERN_NOT_FOUND: 'Required pattern not found: {description}'Backward Compatibility
Every artifact except
specandchangehas no structural checks. Omittingvalidationpreserves that. Existing schemas work without modification.applySpecRulesandapplyChangeRulesstay in place. Both paths run and their issues are merged.specandchangevalidation is additive until a follow-up migrates the hardcoded logic.Future Extensions (Out of Scope)
specs/{kebab-case}/spec.md).generatesglob.applySpecRules/applyChangeRulesintovalidationblocks makes the built-in schema fully self-describing. Partially blocked on #666.proposal.mdneeds a matchingspecs/{name}/spec.md) require a graph-aware validation pass. See #783.