Skip to content
Open
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
23 changes: 23 additions & 0 deletions schemas/design.mdschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Change design structure: the approach with its beaten alternative,
# and any further sections the change needs.

heading_rules:
no_skip_levels: true
max_depth: 3

structure:
- heading:
pattern: "^# .+"
regex: true
count:
max: 1
allow_additional: true
children:
- heading: "## Approach"
severity: warning
word_count:
min: 8
- heading: "## Structure"
optional: true
- heading: "## Risks"
optional: true
35 changes: 35 additions & 0 deletions schemas/proposal.mdschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Change proposal structure: Why states the rationale, What Changes
# and Impact list the observable effects, Capabilities names the
# touched capability specs.

frontmatter:
fields:
- name: status
type: string
- name: adr
type: string
optional: true

heading_rules:
no_skip_levels: true
max_depth: 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [runeseer] reported by reviewdog 🐶
Highproposal.mdschema rejects every proposal already in this repository's spec tree, so rune spec validate, archive preflight, and doctor now emit proposal-schema-invalid on artifacts this change does not touch.

  • docs/openspec/changes/spec-compatibility/proposal.md:14 ### New Capabilities sits at depth 3, past max_depth: 2; decision-artifacts and development-lifecycle carry the same heading.
  • docs/openspec/changes/decision-artifacts/proposal.md:1 starts at ## Why with no frontmatter, so the required status field is absent.


structure:
- heading:
pattern: "^# .+"
regex: true
count:
max: 1
children:
- heading: "## Why"
word_count:
min: 10
- heading: "## What Changes"
lists:
- min: 1
- heading: "## Capabilities"
lists:
- min: 1
- heading: "## Impact"
lists:
- min: 1
17 changes: 17 additions & 0 deletions schemas/tasks.mdschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Change tasks structure: numbered sections of checkbox items in the
# `- [ ] N.M description` form.

heading_rules:
max_depth: 2

structure:
- heading:
pattern: "^## [0-9]+\\. .+"
regex: true
count:
min: 1
lists:
- min: 1
required_text:
- pattern: "^- \\[[ x]\\] [0-9]+\\.[0-9]+ "
regex: true
12 changes: 12 additions & 0 deletions src/spec/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ pub(super) const DELTA_SPEC_MDSCHEMA: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schemas/delta-spec.mdschema"
));
pub(super) const PROPOSAL_MDSCHEMA: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schemas/proposal.mdschema"
));
pub(super) const TASKS_MDSCHEMA: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schemas/tasks.mdschema"
));
pub(super) const DESIGN_MDSCHEMA: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schemas/design.mdschema"
));
24 changes: 24 additions & 0 deletions src/spec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,30 @@ fn nested_capabilities_are_discovered_validated_and_archived() {
assert!(merged.contains("### Requirement: Card payment"));
}

#[test]
fn change_artifacts_run_through_their_schemas() {
fn flag_every_file(_content: &str, file_path: &str, _schema: &str) -> Vec<MdschemaDiagnostic> {
vec![MdschemaDiagnostic {
file: file_path.to_string(),
line: None,
severity: DiagnosticSeverity::Warning,
message: "stub".to_string(),
}]
}
let root = TempDir::new().unwrap();
write_change(root.path(), "add-search", "- [ ] 1.1 pending\n", DELTA);

let diagnostics = validate_spec_tree(root.path(), flag_every_file).unwrap();
let codes: Vec<&str> = diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect();

assert!(codes.contains(&"delta-schema-invalid"));
assert!(codes.contains(&"proposal-schema-invalid"));
assert!(codes.contains(&"tasks-schema-invalid"));
}

#[test]
fn abandon_stamps_frontmatter_and_does_not_merge() {
let root = TempDir::new().unwrap();
Expand Down
62 changes: 53 additions & 9 deletions src/spec/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//! interop artifacts surfaced as warnings. One pipeline serves `validate`,
//! archive preflight, and doctor, so acceptance cannot differ by command.

use super::templates::{DELTA_SPEC_MDSCHEMA, SPEC_MDSCHEMA};
use super::templates::{
DELTA_SPEC_MDSCHEMA, DESIGN_MDSCHEMA, PROPOSAL_MDSCHEMA, SPEC_MDSCHEMA, TASKS_MDSCHEMA,
};
use super::{
DiagnosticSeverity, PrefixMatch, SpecRoot, SpecViolation, changes_root,
discover_capabilities_below, evaluate_change, load_with_override, parse_canonical,
Expand Down Expand Up @@ -103,6 +105,19 @@ pub(super) fn validate_spec_target(
"schemas/delta-spec.mdschema",
DELTA_SPEC_MDSCHEMA,
)?;
let (proposal_schema, _) =
load_with_override(repository, "schemas/proposal.mdschema", PROPOSAL_MDSCHEMA)?;
let (tasks_schema, _) =
load_with_override(repository, "schemas/tasks.mdschema", TASKS_MDSCHEMA)?;
let (design_schema, _) =
load_with_override(repository, "schemas/design.mdschema", DESIGN_MDSCHEMA)?;
let schemas = ChangeSchemas {
spec: &spec_schema,
delta: &delta_schema,
proposal: &proposal_schema,
tasks: &tasks_schema,
design: &design_schema,
};
let mut diagnostics = Vec::new();

match &target {
Expand All @@ -126,8 +141,7 @@ pub(super) fn validate_spec_target(
validate_change(
&spec_root,
&change_dir,
&spec_schema,
&delta_schema,
&schemas,
false,
mdschema_check,
&mut diagnostics,
Expand All @@ -139,8 +153,7 @@ pub(super) fn validate_spec_target(
validate_change(
&spec_root,
&spec_root.changes().join(change),
&spec_schema,
&delta_schema,
&schemas,
true,
mdschema_check,
&mut diagnostics,
Expand Down Expand Up @@ -273,11 +286,18 @@ fn validate_canonical(
Ok(())
}

struct ChangeSchemas<'schemas> {
spec: &'schemas str,
delta: &'schemas str,
proposal: &'schemas str,
tasks: &'schemas str,
design: &'schemas str,
}

fn validate_change(
spec_root: &SpecRoot,
change_dir: &Path,
spec_schema: &str,
delta_schema: &str,
schemas: &ChangeSchemas<'_>,
validate_referenced_canonical_schema: bool,
mdschema_check: MdschemaCheck,
diagnostics: &mut Vec<SpecViolation>,
Expand All @@ -301,7 +321,31 @@ fn validate_change(
change: Some(change),
},
&content,
delta_schema,
schemas.delta,
mdschema_check,
diagnostics,
);
}
for (artifact, schema, code) in [
("proposal.md", schemas.proposal, "proposal-schema-invalid"),
("tasks.md", schemas.tasks, "tasks-schema-invalid"),
("design.md", schemas.design, "design-schema-invalid"),
] {
let path = change_dir.join(artifact);
if !path.is_file() {
continue;
}
let content = read(&path)?;
append_schema_diagnostics(
SchemaDiagnosticContext {
repository: spec_root.repository(),
path: &path,
code,
capability: None,
change: Some(change),
},
&content,
schema,
mdschema_check,
diagnostics,
);
Expand All @@ -319,7 +363,7 @@ fn validate_change(
change: Some(change),
},
&content,
spec_schema,
schemas.spec,
mdschema_check,
diagnostics,
);
Expand Down
Loading