From 4c7e14a2557624d3001b3e01337f3a6f4fe70efc Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 6 Jun 2026 00:19:38 -0700 Subject: [PATCH] fix(seedgo): readme check-ignore must match dir-only patterns on clean checkout (DPLAN-0195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last 1%: 7 branches scored 99% in CI while 100% locally. Root cause proven by reproducing CI's exact path (tracked-only tree + real .git): .gitignore dir-only patterns (trailing slash — logs/, **/*_json/, .trinity/) do NOT match via 'git check-ignore ' when the path is absent from disk (clean checkout), because git cannot infer 'directory' to apply a dir-only pattern. The working tree has those dirs on disk, so it matched there — the exact working-tree-vs-clean-checkout divergence. _is_gitignored now also tests the trailing-slash form; all 7 readme failures (cli_json/logs/artifacts/.trinity/ etc flagged 'missing on disk') clear. Regression test builds a real git repo with dir-only patterns + non-existent paths. CI gate also now prints failing standards + check messages (says WHY). Verified: clean tree w/ real .git 13/13 100%; working tree 13/13 100%; seedgo 1053 tests green; pyright 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/seedgo_audit.py | 19 +++++++++++-- CHANGELOG.md | 11 ++++++-- .../handlers/aipass_standards/readme_check.py | 23 +++++++++++---- .../tests/test_readme_content_checks.py | 28 +++++++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/.github/scripts/seedgo_audit.py b/.github/scripts/seedgo_audit.py index 4f1a43c2..85c448f2 100644 --- a/.github/scripts/seedgo_audit.py +++ b/.github/scripts/seedgo_audit.py @@ -30,12 +30,27 @@ for branch in branches: avg = result.get("average", 0) print(f" {branch['name']:>12}: {avg:.0f}%") if avg < THRESHOLD: - failed.append((branch["name"], avg)) + failed.append((branch["name"], avg, result)) if failed: print(f"\nFAILED: {len(failed)} branch(es) below {THRESHOLD}%") - for name, score in failed: + for name, score, result in failed: print(f" {name}: {score:.0f}%") + # Name the failing standards + the specific checks that did not pass, + # so CI logs say WHY (not just the percentage). Critical for diagnosing + # working-tree-vs-clean-checkout divergence. + scores = result.get("scores", {}) + results = result.get("results", {}) + for std, sc in scores.items(): + if sc < 100: + checks = results.get(std, {}).get("checks", []) + msgs = [ + c.get("message", "") + for c in checks + if not c.get("passed", True) + ] + detail = " | ".join(m for m in msgs if m)[:400] + print(f" └ {std}: {sc:.0f}% {detail}") sys.exit(1) else: print(f"\nAll {len(branches)} branches pass (>={THRESHOLD}%)") diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa365a2..1c46f081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,8 +103,15 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format in the directory-tree and dead-link checks; `encapsulation` infers the branch from the path when the gitignored `AIPASS_REGISTRY.json` is unavailable (and no longer collides on the `aipass` branch); `architecture` skips cleanly when the - gitignored `passport.json` is absent. Clean-tree and working-tree audits now - both report 13/13 = 100%. (DPLAN-0195) + gitignored `passport.json` is absent. A follow-up refined `readme`'s + `git check-ignore` use: `.gitignore` dir-only patterns (trailing slash — + `logs/`, `**/*_json/`, `.trinity/`) don't match a clean checkout's + non-existent paths unless directory intent is signalled, so the check now + also tests the trailing-slash form (this was the last 1% — `readme` flagged + `cli_json`/`logs`/`artifacts` as "missing on disk" in CI only). The CI gate + (`.github/scripts/seedgo_audit.py`) now also prints the failing standards and + their check messages, so a sub-100 result says *why*, not just the percentage. + Clean-tree and working-tree audits both report 13/13 = 100%. (DPLAN-0195) - **Two latent Windows portability bugs caught by the new e2e harness** — both were always present in the code; they only surfaced now because this is the first CI to run `aipass init` scaffolding and real-branch `drone` routing on diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/readme_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/readme_check.py index 549dd014..b967d7e2 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/readme_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/readme_check.py @@ -52,12 +52,23 @@ def _is_gitignored(path: Path) -> bool: ) if top.returncode == 0: repo_root = top.stdout.strip() - result = subprocess.run( - ["git", "-C", repo_root, "check-ignore", "-q", str(path)], - capture_output=True, - timeout=5, - ) - return result.returncode == 0 + # Test both the bare path and its directory form. .gitignore dir-only + # patterns (trailing slash: logs/, artifacts/, **/*_json/, .trinity/) + # only match when git knows the path is a directory. In a clean + # checkout the gitignored dir does not exist on disk, so git cannot + # infer "directory" from the bare path and reports it un-ignored — + # appending a trailing slash signals directory intent and restores + # the match. (Without this, README dir-tree/dead-link checks fail in + # CI's clean checkout while passing in a working tree.) + for candidate in (str(path), str(path).rstrip("/") + "/"): + result = subprocess.run( + ["git", "-C", repo_root, "check-ignore", "-q", candidate], + capture_output=True, + timeout=5, + ) + if result.returncode == 0: + return True + return False except Exception: logger.info("git check-ignore unavailable for %s", path) diff --git a/src/aipass/seedgo/tests/test_readme_content_checks.py b/src/aipass/seedgo/tests/test_readme_content_checks.py index 17778e6b..99bca6dd 100644 --- a/src/aipass/seedgo/tests/test_readme_content_checks.py +++ b/src/aipass/seedgo/tests/test_readme_content_checks.py @@ -471,3 +471,31 @@ def test_check_module_missing_readme_has_8_failures(tmp_path): result = check_module(str(entry)) assert len(result["checks"]) == 8 assert result["score"] == 0 + + +def test_is_gitignored_directory_only_pattern_nonexistent(tmp_path, monkeypatch): + """Dir-only .gitignore patterns match non-existent paths via the slash form. + + Regression (DPLAN-0195): in a clean checkout (CI) the gitignored dir does + not exist on disk, so ``git check-ignore `` reports it un-ignored + because git cannot confirm "directory" to match a dir-only pattern (trailing + slash). ``_is_gitignored`` must also test the trailing-slash form. Without + this the README dir-tree/dead-link checks passed in a working tree but failed + in CI's clean checkout. + """ + import subprocess + + from aipass.seedgo.apps.handlers.aipass_standards.readme_check import ( + _is_gitignored, + ) + + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + (tmp_path / ".gitignore").write_text("logs/\n**/*_json/\n.trinity/\n") + monkeypatch.chdir(tmp_path) + + # Non-existent dirs — only match when the trailing-slash form is tested. + assert _is_gitignored(tmp_path / "logs") is True + assert _is_gitignored(tmp_path / "cli_json") is True + assert _is_gitignored(tmp_path / ".trinity") is True + # A path not covered by any pattern stays un-ignored. + assert _is_gitignored(tmp_path / "src") is False