feat(ci): deterministic craftsmanship gate for CLAUDE.md § Code Style - #413
Conversation
…tyle
Nothing automated enforced file size, method size, layer-boundary
imports, or unsourced magic numbers — CLAUDE.md admitted as much
("enforced by code review today; no automated pre-commit hook checks
this yet"). A single PR the night before this change shipped a
301-line file reported as 280, three of four over-40-line methods
unseen, and a layer violation justified by a fabricated citation;
every catch came from a human or agent re-reading the diff.
scripts/check_craftsmanship.py runs on a diff's changed files only
(never the whole tree), AST-based (ast.FunctionDef/end_lineno, never
regex), against a versioned .craftsmanship-baseline.json ratchet: new
violations block, and so does a baseline entry whose violation no
longer reproduces (forces pruning instead of silent drift). Wired into
ci.yml as the `craftsmanship` job, gated by CI Green.
Co-Authored-By: Claude <noreply@anthropic.com>
origin/main split mcp_server/core/pg_recall.py (464 lines -> facade + pg_recall_context.py/pg_recall_signals.py/pg_recall_stages.py) since this branch's baseline was first generated; the gate correctly reported it as a stale entry (fixed in code but still listed) in CI. Regenerated via `python scripts/check_craftsmanship.py --write-baseline` against the merged tree. Co-Authored-By: Claude <noreply@anthropic.com>
|
ZETETIC-REVIEW: REQUEST_CHANGES SummaryBien conçu et bien testé (59/59 tests réels, pas de paraphrase), mais deux failles vérifiées empiriquement défont la promesse centrale de la gate : (1) le baseline peut être régénéré dans la même PR pour blanchir une violation qu'on vient d'introduire — CI ne s'en aperçoit pas ; (2) la règle 3 (import inter-couches) implémente une liste noire, pas la liste blanche du tableau qu'elle prétend appliquer — Toutes les commandes ci-dessous ont été exécutées sur un clone jetable, en quelques secondes, jamais sur la suite complète. 1. Contournement AST — ce qui échappe, vérifié par exécution directe des détecteursConfirmé par exécution (
Verdict sur ces échappatoires : les trois premières (lib tierce dans 2. Le baseline : cliquet ou passoire — vérifié par démonstrationPassoire, démontré. Le champ Correctif concret : charger 3. Table des couches : dupliquée en dur, pas lue depuis la doc — et incomplète
Pire : la table couvre seulement 4 des 8 lignes documentées ( Correctif minimal : soit corriger la prose de 4. Sourcing des seuils — la gate se soumet-elle à sa propre règle 4 ?
Mais deux constantes internes de la gate elle-même citent Auto-application vérifiée : le gate tourne sur ses 5 propres fichiers ( 5. Les 59 tests testent-ils la gate ou sa paraphrase ?Testent réellement la gate. Cas limites annoncés, tous présents et vérifiés par exécution locale (0.006s, 47+12=59 tests, tous verts) :
Absents (confirmés par grep, aucun test ne les couvre) : 6. Job CI : gréé correctement, sur les bons événements
Un point mineur, non-bloquant, pas nouveau à cette PR : sur un Hygiène du diff
Ce qui bloque le merge
Le reste (constantes calculées/de classe, sourcing des seuils internes) est réel mais non-bloquant — à traiter en suivi documenté, pas en note volatile. |
…hitelist Two review-round blockers, both reproduced live and closed: 1. The baseline was read from the working tree, which the same PR controls: add a violation, run `--write-baseline` in the same tree, the gate passed on it. Now compared against `git show <base-ref>:.craftsmanship-baseline.json` — immutable to the PR's own commits — with a new ratchet-file check refusing any entry present in the working-tree baseline but absent from the base ref's (the file may only shrink within a PR, never grow). The exploit and its close are both reproduced against a real throwaway git repo in tests_py/scripts/test_check_craftsmanship.py::SneakyLimitExploitTests. 2. The layer rule was a blacklist wearing a whitelist's name: `import numpy`/`import requests`/`import scripts.legacy_bridge` inside core/ all passed silently. Rewritten as a true whitelist, derived at runtime from docs/module-inventory.md's own table (scripts/craftsmanship_layer_table.py) instead of a second hardcoded copy — covers all eight documented layers, not four. Also: removed the AUTO_GENERATED_SCAN_LINES magic number (scan the leading comment/blank-line header block instead of a fixed line count); reworded the TRIVIAL_LITERALS comment, which cited "task instruction" as a §8 source (it is not one); documented three known constant-detection gaps (computed expressions, class-scope constants, default-argument values) with pinning tests. Baseline regenerated: 1393 entries (231 file-size, 127 layer-violation, 569 method-size, 466 unsourced-constant). Co-Authored-By: Claude <noreply@anthropic.com>
SneakyLimitExploitTests hardcoded "main" as the base ref but relied on `git init`'s default branch name to actually produce a branch called "main" — true on this machine, not guaranteed by `init.defaultBranch` on a CI runner (CI run 31348359157: `git init` there produced a different default, so `--base main` failed to resolve and both tests exited 2 instead of the expected 1). `git init -q -b main` pins it explicitly. Reproduced locally by temporarily setting `git config --global init.defaultBranch master` and confirmed both directions (fails without the fix, passes with it). Co-Authored-By: Claude <noreply@anthropic.com>
…a03fa140d38 # Conflicts: # CLAUDE.md
origin/main advanced (PR #409: pg_store.py split + infra->core layer fix) since the last regeneration; the merge itself resolved cleanly except for CLAUDE.md's Code Style section (both sides edited it — kept this branch's gate description, folded in the fact that it now supersedes the manual-grep verification step main's side described). Regenerated via `python scripts/check_craftsmanship.py --write-baseline` against the merged tree: 1362 entries (226 file-size, 114 layer-violation, 556 method-size, 466 unsourced-constant) — down from 1393 before the merge, reflecting #409's real fixes. Co-Authored-By: Claude <noreply@anthropic.com>
|
ZETETIC-REVIEW: REQUEST_CHANGES SummaryRound 2 closes both round-1 findings genuinely — verified live, not by re-reading the diff. But adversarial testing found two new, still-live gaps that let the ratchet be defeated with zero code fix. Judged at head Round 1 findings — re-verified closed (live reproduction, throwaway repo)
Two NEW blocking gaps (adversarial, live-reproduced, untested by the PR's own 90 tests)1. Hand-pruning a baseline entry with no matching code fix is undetected
This defeats the ratchet's entire premise for any of the 1362 currently-baselined violations, at the cost of deleting one JSON block — no Fix shape: 2. Layer-table parser silently truncates on a malformed mid-table row
Reproduced: inserted one row after Missing header / zero rows failing loudly, confirmed correct, is not sufficient — the loop needs to Debt-count reconciliation (requested)Baseline totals match the PR's claim exactly: CLAUDE.md merge-conflict checkDiffed the PR head's § Code Style directly against pre-#413 VerdictREQUEST_CHANGES. Both round-1 findings are genuinely closed — good work, verified live rather than trusted. But this round's fix is incomplete in the same spirit the brief warned about: "une gate fausse est pire que pas de gate." Findings 1 and 2 above are each, independently, a way for the gate to report success while enforcing nothing — fix both, add a regression test for each (a hand-pruned-without-fix entry; a malformed row after the first genuinely-parsed one), and this is close to mergeable. |
Two review-round blockers, both reproduced live and closed, plus a self-audited third instance of the same failure class. 1. The ratchet only checked ADDITIONS (added_entries: working - base). Hand-deleting a baseline entry's JSON line without touching the violating source file it describes went undetected: the file isn't *.py so it never enters the diff-scanned set, and the removal has no corresponding addition. Closed by falsified_removals - for every entry present at the base ref but absent from the working tree, its file is rescanned; if the violation still reproduces, the removal is refused. Reproduced end-to-end against a real throwaway git repo in FalsifiedRemovalExploitTests, alongside SneakyLimitExploitTests (the addition-side exploit from the prior round), both now split into test_check_craftsmanship_exploits.py to stay under the 300-line cap. 2. craftsmanship_layer_table.parse_layer_rules treated ANY line that failed the row-shape regex - including a malformed row in the MIDDLE of the table, not just genuine end-of-table prose - as "the table ended", silently dropping that row and every row after it. Reproduced: one broken row after validation/ silently removed errors/, handlers/, server/, and hooks/ from enforcement - four of eight layers, zero signal. Fixed by two independent checks: only a line that isn't even attempting to be a row (no leading pipe) ends the table; a row-count invariant (parsed rules vs. row lines seen) catches e.g. a duplicate layer name silently overwriting an earlier entry. 3. Self-audit (per review's instruction to look for a third instance of the same "control fails open on an ambiguous signal" pattern): _git_path_exists_at_ref swallowed every git cat-file -e failure - not just a genuinely absent path - into a bare False, which load_baseline_from_ref reads as "bootstrap, fall back to the tamperable working-tree baseline". Hardened to distinguish git's actual "path does not exist in <ref>" stderr from any other failure (bad ref, corrupt object, disk error), which now raises instead. Baseline regenerated (byte-identical: these are gate-integrity fixes, not new detection rules) - 1362 entries, unchanged breakdown. Co-Authored-By: Claude <noreply@anthropic.com>
|
ZETETIC-REVIEW: APPROVE SummaryRound 3 at head Independent adversarial reproduction (throwaway repos, seconds each, no full suite/bench run)All three attacks reproduced and confirmed blocked, all three legitimate paths confirmed pass:
Full 103-test suite ( Fourth-instance search (Move requested)Swept all 7 gate modules for the fail-open family (
|
Summary
scripts/check_craftsmanship.py(+craftsmanship_rules.py/craftsmanship_imports.py/craftsmanship_constants.py/craftsmanship_baseline.py): a deterministic, AST-based gate for the four rulesCLAUDE.md§ Code Style states but nothing checked — file size (300), method size (40, viaast.FunctionDef/end_lineno), layer-boundary imports (docs/module-inventory.md§ Dependency Rules, restricted to shared/core/infrastructure/server), and module-scope numeric constants missing a# source:comment..craftsmanship-baseline.jsonratchet: pre-existing debt (1307 entries, generated 2026-08-10 via--write-baselineover the full tracked tree) does not retroactively block; a genuinely new violation does; a baseline entry whose violation no longer reproduces also blocks (forces pruning rather than silent drift)..github/workflows/ci.ymlas a newcraftsmanshipjob, gated byCI Green(ci-green.needs, verified viascripts/check_ci_gate_complete.py).CLAUDE.md§ Code Style to describe the gate instead of admitting it doesn't exist yet; updatesSECURITY.md§ Change control to note the new job and that a repo admin must add it to GitHub's required-checks list (this PR does not and cannot do that itself).Test plan
scripts/check_craftsmanship.py, pointed atmcp_server/infrastructure/pg_store_stats.pyand the threewiki_*core modules named in the task, detects the known violations (file-size, method-size,pathlib/oslayer violations) before baselining.python scripts/check_craftsmanship.py --base origin/mainon this branch's real committed diff: clean (0).uv run --no-sync pytest tests_py/scripts/test_craftsmanship_rules.py tests_py/scripts/test_craftsmanship_imports.py tests_py/scripts/test_craftsmanship_constants.py tests_py/scripts/test_craftsmanship_baseline.py tests_py/scripts/test_check_craftsmanship.py -q: 59 passed, order-independent (verified both forward and reverse file order).ruff format --check/ruff checkon all touched files: clean.pyright(standalone) on all touched files: 0 errors/warnings/informations.scripts/check_ci_gate_complete.py,scripts/check_doc_claims.py,scripts/check_version_surfaces.py,scripts/generate_repo_badges.py --check: all pass.actionlint -color .github/workflows/ci.yml: clean.gh run watchafter push.🤖 Generated with Claude Code