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
pull/17/head
VinPropane 2026-04-03 21:29:28 -04:00
parent d11641159d
commit 6599e136d6
6 changed files with 84 additions and 2 deletions

View File

@ -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

View File

@ -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"

View File

@ -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
```

View File

@ -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)

View File

@ -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.

View File

@ -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())