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
5 changes: 5 additions & 0 deletions .github/workflows/markdown-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ name: Markdown Validation
- "scripts/render_tags_doc.py"
- "scripts/generate_markdownlint_debt.py"
- "scripts/run_markdownlint.py"
- "tests/**/*.py"
- "requirements-lint.txt"
- ".markdownlint-cli2.jsonc"
- ".markdownlint-cli2-debt.jsonc"
Expand All @@ -40,6 +41,7 @@ name: Markdown Validation
- "scripts/render_tags_doc.py"
- "scripts/generate_markdownlint_debt.py"
- "scripts/run_markdownlint.py"
- "tests/**/*.py"
- "requirements-lint.txt"
- ".markdownlint-cli2.jsonc"
- ".markdownlint-cli2-debt.jsonc"
Expand Down Expand Up @@ -79,6 +81,9 @@ jobs:
- name: Validate canonical public placeholders
run: python scripts/check_placeholders.py

- name: Run validator contract tests
run: python -m unittest discover -s tests -p 'test_*.py'

- name: Run Markdown checks
run: python scripts/check_markdown.py

Expand Down
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ repos:
files: >-
^(README\.md|docs/.+\.md|patterns/.+\.md|schemas/pattern-library\.json|
scripts/check_pattern_library\.py|notes/.+\.md)$
- id: test-validation-contracts
name: test-validation-contracts
entry: python -m unittest discover -s tests -p test_*.py
language: python
additional_dependencies:
- PyYAML>=6.0,<7.0
- jsonschema>=4.23,<5.0
pass_filenames: false
always_run: true
- id: validate-frontmatter
name: validate-frontmatter
entry: python scripts/check_markdown.py
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Normalize and maintain this repository as a local-first canonical knowledge base
Run:

- python scripts/render_tags_doc.py --check
- python -m unittest discover -s tests -p 'test_*.py'
- python scripts/check_placeholders.py `<changed files>`
- python scripts/check_markdown.py
- python -m pre_commit run --files `<changed files>`
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ Run these before publishing or merging materially edited public notes:
```text
python scripts/render_tags_doc.py --check
python scripts/render_readme_snapshot.py --check
python -m unittest discover -s tests -p 'test_*.py'
python scripts/check_placeholders.py <changed files>
python scripts/check_markdown.py
python -m pre_commit run --files <changed files>
Expand Down
4 changes: 3 additions & 1 deletion patterns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ These cards remain useful but are not part of the featured stable set:
| `reviewed` | Structurally reviewed and supported, but not yet promoted as a stable library entry. |
| `stable` | Evidence-bounded, linked to a core implementation, and supported by at least one source note. |

Every card records `maturity` and `last_reviewed` in front matter.
Every card records `maturity` and `last_reviewed` in front matter. The review
date must be a valid current or historical date; future provenance claims fail
validation.

## Card Contract

Expand Down
3 changes: 3 additions & 0 deletions scripts/check_pattern_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def parse_card(
except (TypeError, ValueError):
errors.append(f"{relative(path)}: last_reviewed must use YYYY-MM-DD")
return None
if reviewed > date.today():
errors.append(f"{relative(path)}: last_reviewed must not be in the future")
return None

body = text[match.end() :]
headings = list(HEADING_RE.finditer(body))
Expand Down
249 changes: 249 additions & 0 deletions tests/test_validation_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
from __future__ import annotations

import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch


ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))

import check_markdown # noqa: E402
import check_pattern_library # noqa: E402
import check_placeholders # noqa: E402


REQUIRED_PATTERN_SECTIONS = [
"Signal",
"Why it matters",
"False-positive contexts",
"Evidence limits",
"Defensive next step",
"Related implementation",
"Supporting notes",
]


def pattern_card_text(
*,
title: str,
maturity: str = "stable",
last_reviewed: str = "2000-01-01",
implementation_url: str = "https://github.com/stacknil/core-project",
) -> str:
sections: list[str] = []
for section in REQUIRED_PATTERN_SECTIONS:
if section == "Related implementation":
content = f"[Core project]({implementation_url})"
elif section == "Supporting notes":
content = "[Source note](../notes/source.md)"
else:
content = "Decision-bearing evidence."
sections.append(f"## {section}\n\n{content}")

return (
"---\n"
f"maturity: {maturity}\n"
f"last_reviewed: {last_reviewed}\n"
"---\n"
f"# {title}\n\n"
+ "\n\n".join(sections)
+ "\n"
)


class PatternLibraryContractTests(unittest.TestCase):
def test_parse_card_future_review_date_is_rejected(self) -> None:
with TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
card_path = temp_root / "patterns" / "future-review.md"
card_path.parent.mkdir(parents=True)
card_path.write_text(
pattern_card_text(
title="Future review",
last_reviewed="2999-01-01",
),
encoding="utf-8",
)
errors: list[str] = []

with patch.object(check_pattern_library, "ROOT", temp_root):
card = check_pattern_library.parse_card(
card_path,
REQUIRED_PATTERN_SECTIONS,
{"draft", "reviewed", "stable"},
errors,
)

self.assertIsNone(card)
self.assertEqual(
errors,
[
"patterns/future-review.md: "
"last_reviewed must not be in the future"
],
)

def test_validate_stable_card_without_core_project_is_rejected(self) -> None:
with TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
self._write_pattern_library(
temp_root,
implementation_overrides={0: "https://example.com/other-project"},
)

errors, card_count, stable_count, supporting_count = self._validate(
temp_root
)

self.assertEqual((card_count, stable_count, supporting_count), (6, 6, 1))
self.assertEqual(
errors,
[
"patterns/card-0.md: "
"stable card must link to a core project"
],
)

def test_validate_featured_reviewed_card_is_rejected(self) -> None:
with TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
self._write_pattern_library(
temp_root,
maturity_overrides={0: "reviewed"},
)

errors, card_count, stable_count, supporting_count = self._validate(
temp_root
)

self.assertEqual((card_count, stable_count, supporting_count), (6, 5, 1))
self.assertEqual(
errors,
["featured pattern is not stable: patterns/card-0.md"],
)

def _write_pattern_library(
self,
root: Path,
*,
implementation_overrides: dict[int, str] | None = None,
maturity_overrides: dict[int, str] | None = None,
) -> None:
implementation_overrides = implementation_overrides or {}
maturity_overrides = maturity_overrides or {}
patterns = root / "patterns"
schemas = root / "schemas"
notes = root / "notes"
patterns.mkdir(parents=True)
schemas.mkdir(parents=True)
notes.mkdir(parents=True)
(notes / "source.md").write_text("# Source\n", encoding="utf-8")

featured: list[str] = []
for index in range(6):
relative_path = f"patterns/card-{index}.md"
featured.append(relative_path)
(root / relative_path).write_text(
pattern_card_text(
title=f"Card {index}",
maturity=maturity_overrides.get(index, "stable"),
implementation_url=implementation_overrides.get(
index,
"https://github.com/stacknil/core-project",
),
),
encoding="utf-8",
)

config = {
"version": 1,
"maturity_values": ["draft", "reviewed", "stable"],
"required_sections": REQUIRED_PATTERN_SECTIONS,
"core_projects": [
{
"name": "Core project",
"url_prefix": "https://github.com/stacknil/core-project",
}
],
"featured_patterns": featured,
"flagship_case_studies": [],
"metric_surfaces": [],
}
(schemas / "pattern-library.json").write_text(
json.dumps(config),
encoding="utf-8",
)

def _validate(
self,
root: Path,
) -> tuple[list[str], int, int, int]:
with (
patch.object(check_pattern_library, "ROOT", root),
patch.object(
check_pattern_library,
"CONFIG_PATH",
root / "schemas" / "pattern-library.json",
),
):
return check_pattern_library.validate()


class FrontmatterContractTests(unittest.TestCase):
def test_validate_structure_path_mismatch_is_reported(self) -> None:
issues = check_markdown.validate_structure(
"notes/10-web/example.md",
"# Example\n\n## Summary\n\nSafe summary.\n",
{
"path": "notes/10-web/wrong.md",
"topic": "10-web",
},
)

self.assertEqual(
[(issue.path, issue.message) for issue in issues],
[
(
"notes/10-web/example.md",
"path field mismatch: expected 'notes/10-web/example.md', "
"found 'notes/10-web/wrong.md'",
)
],
)


class PlaceholderContractTests(unittest.TestCase):
def test_noncanonical_placeholder_does_not_flag_literal_identifier(self) -> None:
with TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
note = temp_root / "notes" / "10-web" / "example.md"
note.parent.mkdir(parents=True)
note.write_text(
"Use NONCANONICAL_TARGET, but preserve LD_PRELOAD.\n",
encoding="utf-8",
)
literal_policy = check_placeholders.LiteralPolicy(
exact_tokens=frozenset(),
angle_tokens=frozenset(),
)

with patch.object(check_placeholders, "ROOT", temp_root):
issues = check_placeholders.collect_issues(
note,
canonical={"TARGET_IP"},
replacements={},
literal_policy=literal_policy,
)

self.assertEqual(
[(issue.token, issue.category) for issue in issues],
[("NONCANONICAL_TARGET", "noncanonical-placeholder")],
)


if __name__ == "__main__":
unittest.main()
Loading