fix(seedgo): readme check-ignore must match dir-only patterns on clean checkout (DPLAN-0195)

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 <bare-path>' 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) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-06 00:19:38 -07:00
co-authored by Claude Opus 4.8
parent 24065f11b3
commit 4c7e14a255
4 changed files with 71 additions and 10 deletions
+17 -2
View File
@@ -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}%)")
+9 -2
View File
@@ -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
@@ -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)
@@ -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 <bare-path>`` 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