From 6599e136d67472307d643012fb6fa8fe0ea8525a Mon Sep 17 00:00:00 2001 From: VinPropane Date: Fri, 3 Apr 2026 21:29:28 -0400 Subject: [PATCH] chore: CI check for decomposition hybrid vocabulary (suggestion 4) - Add scripts/check_decomposition_language.py (stdlib); PR gate step - CT.M1, content README, docs-review-agent, 2026-04-03 review updates --- .cursor/rules/docs-review-agent.md | 2 +- .github/workflows/pr-gate.yml | 3 + content/README.md | 1 + .../CT_M1_ContentValidationPipeline.md | 2 + ...3-branch-chore-game-design-docs-vs-main.md | 3 +- scripts/check_decomposition_language.py | 75 +++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 scripts/check_decomposition_language.py diff --git a/.cursor/rules/docs-review-agent.md b/.cursor/rules/docs-review-agent.md index a62e746..39c756d 100644 --- a/.cursor/rules/docs-review-agent.md +++ b/.cursor/rules/docs-review-agent.md @@ -27,7 +27,7 @@ Use this rule when the user is working in **`docs/`** (design, decomposition, pl 1. **Same folder and upstream links** — Read every `docs/...` link from the target doc(s); verify paths exist and headings match intent. 2. **Hybrid progression** — `docs/game-design/progression.md`, `skills.md`, `gigs.md`: **Gig** = combat role; **Skill** = non-combat; **Seams** live in `skills.md` unless a dedicated doc supersedes. **Combat encounters** → **gig XP** only (no default skill XP from the fight loop). **Mission/quest rewards** may still grant **skill XP** when explicitly defined—separate from the encounter **gig XP** path. -3. **Decomposition** — Map claims to `docs/decomposition/epics/` and `docs/decomposition/modules/` where relevant; flag **conflicts** (e.g. “combat skill” vs skills-as-non-combat). +3. **Decomposition** — Map claims to `docs/decomposition/epics/` and `docs/decomposition/modules/` where relevant; flag **conflicts** (e.g. “combat skill” vs skills-as-non-combat). **CI** enforces a minimal guard via [`scripts/check_decomposition_language.py`](../../scripts/check_decomposition_language.py) in the **PR gate** (extend the script if a vetted exception is needed). 4. **Plans** — If work maps to `docs/plans/`, check for **acceptance criteria** vs design doc (same spirit as [code-review-agent](code-review-agent.md) plan alignment). ## Review checklist diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index e9afc03..717a174 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -30,5 +30,8 @@ jobs: pip install -r scripts/requirements-content.txt python scripts/validate_content.py + - name: Check decomposition doc language (gig vs skill) + run: python scripts/check_decomposition_language.py + - name: Gate run: "true" diff --git a/content/README.md b/content/README.md index 5be4ccf..958468a 100644 --- a/content/README.md +++ b/content/README.md @@ -14,4 +14,5 @@ Server load path and hot-reload policy are TBD; keep files versioned here as the ```bash pip install -r scripts/requirements-content.txt python scripts/validate_content.py +python scripts/check_decomposition_language.py ``` diff --git a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md index b5ccee1..5e2ddb8 100644 --- a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md +++ b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md @@ -43,6 +43,8 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard **Skill catalogs:** the repo **PR gate** workflow ([`.github/workflows/pr-gate.yml`](../../../.github/workflows/pr-gate.yml)) runs [`scripts/validate_content.py`](../../../scripts/validate_content.py) on every push and pull request. It validates each object in `content/skills/*.json` under the top-level `skills` array against [`content/schemas/skill-def.schema.json`](../../../content/schemas/skill-def.schema.json) (Python **jsonschema**, Draft 2020-12). Extend the script when additional catalogs and schemas ship. +**Decomposition vocabulary:** the same workflow runs [`scripts/check_decomposition_language.py`](../../../scripts/check_decomposition_language.py) (stdlib only) over `docs/decomposition/**/*.md` to catch wording that treats **combat** as a leveled **`SkillDef`** line instead of **gig** progression (see script docstring for patterns). Adjust the script if a future doc needs a documented exception. + ## Source anchors - Plan: [`internal-content-authoring.md`](../../plans/internal-content-authoring.md) diff --git a/docs/reviews/2026-04-03-branch-chore-game-design-docs-vs-main.md b/docs/reviews/2026-04-03-branch-chore-game-design-docs-vs-main.md index 6025443..09b164c 100644 --- a/docs/reviews/2026-04-03-branch-chore-game-design-docs-vs-main.md +++ b/docs/reviews/2026-04-03-branch-chore-game-design-docs-vs-main.md @@ -45,7 +45,7 @@ None for **using the branch as design + schema guidance**. 1. ~~**Epic 2 Objective** — “gathering, crafting, and combat all feed one coherent progression model.”~~ **Fixed in this review** — now states **skill** vs **gig** split and mission **skill** rewards. 2. ~~**E2.M1 “Module Breakdown” stub**~~ **Done** — `epic_02` **E2.M1** block matches long-form module + schema links. 3. ~~**CT.M1 / CI:**~~ **Done** — PR gate runs [`scripts/validate_content.py`](../../scripts/validate_content.py) for `content/skills/*.json` vs `skill-def.schema.json`; docs updated ([CT.M1](../decomposition/modules/CT_M1_ContentValidationPipeline.md#ci-prototype), [`internal-content-authoring` Phase 0](../plans/internal-content-authoring.md#phase-0-contracts-validation)). -4. **Decomposition grep:** Periodically run `rg "combat skill|one combat skill"` under `docs/decomposition` after gig modules are drafted to catch regressions. +4. ~~**Decomposition grep:**~~ **Done** — automated via [`scripts/check_decomposition_language.py`](../../scripts/check_decomposition_language.py) in **PR gate**; see [CT.M1 — CI (prototype)](../decomposition/modules/CT_M1_ContentValidationPipeline.md#ci-prototype). ## Nits @@ -64,3 +64,4 @@ None for **using the branch as design + schema guidance**. - `epic_02_classless_progression.md` **Objective**: hybrid **skill** vs **gig** wording + links to `progression.md` and E2.M2. - `epic_02_classless_progression.md` **E2.M1** module stub: non-combat **`SkillDef`**, categories, **`allowedXpSourceKinds`**, links to E2.M1 module + JSON Schema. - **Content CI:** `.github/workflows/pr-gate.yml` + `scripts/validate_content.py` + `scripts/requirements-content.txt`; CT.M1 / E2.M1 / `content/README` / internal-content-authoring updates. +- **Decomposition language CI:** `scripts/check_decomposition_language.py` + PR gate + CT.M1 / docs-review-agent notes. diff --git a/scripts/check_decomposition_language.py b/scripts/check_decomposition_language.py new file mode 100644 index 0000000..5a2f09c --- /dev/null +++ b/scripts/check_decomposition_language.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Fail if decomposition docs regress hybrid vocabulary (gig vs non-combat skill). + +Catches phrases like "one combat skill" or bare "combat skill" that imply a combat +SkillDef. Allows "non-combat skill(s)" and "anti-combat …" when the hyphen prefix +is immediately before ``combat``. + +Run in CI (PR gate) and locally after editing ``docs/decomposition/**``. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +DECOMP_ROOT = REPO_ROOT / "docs/decomposition" + +# Standalone phrases that almost always indicate a doc bug vs game-design hybrid model. +LITERAL_BANS = [ + "one combat skill", + "one combat skills", +] + +# Match "combat skill" / "combat skills" unless prefixed by "non-" or "anti-" (hyphenated). +COMBAT_SKILL = re.compile(r"\bcombat skills?\b", re.IGNORECASE) + + +def line_has_bad_combat_skill(line: str) -> bool: + for m in COMBAT_SKILL.finditer(line): + start = m.start() + if start >= 4 and line[start - 4 : start].lower() == "non-": + continue + if start >= 5 and line[start - 5 : start].lower() == "anti-": + continue + return True + return False + + +def check_file(path: Path) -> list[str]: + errors: list[str] = [] + text = path.read_text(encoding="utf-8") + lower = text.lower() + for banned in LITERAL_BANS: + if banned in lower: + errors.append(f"{path.relative_to(REPO_ROOT)}: contains forbidden phrase {banned!r}") + + for i, line in enumerate(text.splitlines(), start=1): + if line_has_bad_combat_skill(line): + errors.append(f"{path.relative_to(REPO_ROOT)}:{i}: suspicious 'combat skill' (use gig or 'non-combat skill'): {line.strip()[:200]}") + return errors + + +def main() -> int: + if not DECOMP_ROOT.is_dir(): + print(f"error: missing {DECOMP_ROOT}", file=sys.stderr) + return 1 + + all_errors: list[str] = [] + for path in sorted(DECOMP_ROOT.rglob("*.md")): + all_errors.extend(check_file(path)) + + if all_errors: + for msg in all_errors: + print(msg, file=sys.stderr) + print(f"decomposition language check failed ({len(all_errors)} issue(s))", file=sys.stderr) + return 1 + + print("decomposition language OK: hybrid vocabulary (no stray combat SkillDef phrasing)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())