diff --git a/CHANGELOG.md b/CHANGELOG.md index 68221f8..ef03e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `repokit-check.ps1` no longer fails START-HERE rows that declare planned-but-not-yet-created + paths (#31, ADR-0010): a backticked token suffixed `(planned)` skips the existence assertion + — the standard prescribes not pre-creating empty directories — while the marker covers only + the token it follows, so an unmarked broken path in the same row still fails. Documented in + the standard's *Variance declarations* (*Planned paths*) and covered by two smoke cases. + ## [0.6.0] - 2026-08-13 ### Added diff --git a/docs/adr/0010-planned-start-here-paths.md b/docs/adr/0010-planned-start-here-paths.md new file mode 100644 index 0000000..20b3eae --- /dev/null +++ b/docs/adr/0010-planned-start-here-paths.md @@ -0,0 +1,32 @@ +# ADR-0010: "(planned)" START-HERE paths — declare layout before it exists + +- **Status:** accepted +- **Date:** 2026-08-13 + +## Context + +Field report #31: `the-standard.md` prescribes not pre-creating empty directories ("directories +are created when first populated"), but `repokit-check.ps1` asserts that every path-like token +in the START-HERE map resolves. A row declaring a monorepo's planned component layout +(`firmware/`, `app/`) therefore failed the self-check — following one rule broke the other. The +observed workaround (rewording the row to avoid path-like tokens) hid useful, greppable path +names from the map. + +## Decision + +A per-token marker, not a per-row or label-allowlist rule: a backticked token immediately +followed by `(planned)` is skipped by the self-check's existence assertion. Chosen over the +issue's alternative (only asserting rows whose label is in the known required set) because that +would silently stop checking *every* documentation row — the existence check on declared paths +is most of the self-check's value, and the marker keeps it on by default with an explicit, +visible opt-out per token. The marker covers only the token it follows, so one planned entry +cannot mask an unrelated broken path in the same row. Documented in the standard's *Variance +declarations* (as *Planned paths*), covered by two smoke cases (pass without the directory; +unmarked broken path beside a planned one still fails). + +## Consequences + +Planned layout is declarable in the map with real path tokens, and the check stays honest: an +existing-path claim is still verified everywhere the marker is absent. The marker is trust-based +— nothing forces its removal when the path becomes real; a stale `(planned)` on an existing path +merely skips a check that would pass, so the failure mode is benign. diff --git a/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 b/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 index d94b30e..192eb48 100644 --- a/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 +++ b/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 @@ -5,7 +5,9 @@ # Verifies the repo's *declared* structure actually exists (see the RepoKit standard, # "Variance declarations"): # 1. AGENTS.md (canonical agent file) exists; CLAUDE.md (loader shim) exists and imports it. -# 2. Every path named in the START-HERE map resolves to an existing file or directory. +# 2. Every path named in the START-HERE map resolves to an existing file or directory. A +# token suffixed "(planned)" declares a path the repo has not grown into yet and is +# skipped — the standard says not to pre-create empty directories. # 3. A changelog exists at the default location or a declared one. # 4. An ADR directory (or declared substitute) exists. # 5. A resume-state row exists in the START-HERE map (docs/CHECKPOINT.md or a declared @@ -71,14 +73,19 @@ if ($dataRows.Count -eq 0) { } else { Pass "START-HERE map found ($($dataRows.Count) rows)" foreach ($row in $dataRows) { - $tokens = [regex]::Matches($row, '`([^`]+)`') | ForEach-Object { $_.Groups[1].Value } - foreach ($tok in $tokens) { - if ($tok.Contains('/') -and $tok -match '^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*/?$') { - if (Test-Path -LiteralPath (Join-Path $RepoRoot $tok.TrimEnd('/'))) { - Pass "START-HERE path resolves: $tok" - } else { - Fail "START-HERE path does not resolve: $tok" - } + foreach ($m in [regex]::Matches($row, '`([^`]+)`')) { + $tok = $m.Groups[1].Value + if (-not ($tok.Contains('/') -and $tok -match '^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*/?$')) { continue } + # "(planned)" right after the closing backtick marks a declared-but-not-yet-created + # path; it covers only the token it follows. + if ($row.Substring($m.Index + $m.Length) -match '^\s*\(planned\)') { + Pass "START-HERE path declared planned - not required to exist yet: $tok" + continue + } + if (Test-Path -LiteralPath (Join-Path $RepoRoot $tok.TrimEnd('/'))) { + Pass "START-HERE path resolves: $tok" + } else { + Fail "START-HERE path does not resolve: $tok" } } } diff --git a/plugins/repokit/skills/repo-standard/standard/the-standard.md b/plugins/repokit/skills/repo-standard/standard/the-standard.md index c2fa315..f66c7e4 100644 --- a/plugins/repokit/skills/repo-standard/standard/the-standard.md +++ b/plugins/repokit/skills/repo-standard/standard/the-standard.md @@ -209,6 +209,12 @@ it locally before a release, or add one CI line: run: ./scripts/repokit-check.ps1 ``` +**Planned paths.** A map row may name layout the repo has not grown into yet (say, a monorepo's +`firmware/` and `app/` components) — declaring intent is useful, and the standard says *not* to +pre-create empty directories. Suffix the backticked token with `(planned)` — +`` `firmware/` (planned) `` — and the self-check skips that token's existence assertion. The +marker covers only the token it follows; remove it when the path becomes real. + **Adoption marker — retro-adopted repos.** A repo that adopts the standard mid-life snaps to the conventions at the adoption commit; nothing before it should ever be re-flagged by an audit. Declare the compliance horizon with one line in `AGENTS.md`, right under the START-HERE map: diff --git a/scripts/smoke_test_repokit_check.ps1 b/scripts/smoke_test_repokit_check.ps1 index 0eab61f..ac3ec5b 100644 --- a/scripts/smoke_test_repokit_check.ps1 +++ b/scripts/smoke_test_repokit_check.ps1 @@ -6,7 +6,9 @@ # FAILS on each drift case from the retrospective (refs #16): missing shim, non-importing shim, # broken START-HERE path, missing changelog, missing ADR dir, missing resume-state row — plus # the author-identity cases (refs #25/#26): handle + noreply passes, a real name or personal -# email fails, and a declared variance row switches the check off. +# email fails, and a declared variance row switches the check off — plus the "(planned)" +# marker cases (refs #31): a planned path needn't exist, and the marker covers only the token +# it follows. $ErrorActionPreference = 'Stop' @@ -113,6 +115,20 @@ Assert-Check 'declared author-identity variance passes' $d 0 Assert-Check 'web-flow identity passes' (New-GitFixture 'GitHub' 'noreply@github.com') 0 Assert-Check 'real name on the web-flow email fails' (New-GitFixture 'Octo Cat' 'noreply@github.com') 1 +# --- "(planned)" marker cases (refs #31) ----------------------------------------------------- + +# 14. A row may declare planned-but-not-yet-created paths — the standard says not to pre-create +# empty directories, so the marker skips the existence assertion. +$d = New-Fixture +Add-Content (Join-Path $d 'AGENTS.md') '| Planned component layout | `firmware/` (planned), `app/` (planned) |' +Assert-Check 'planned paths pass without existing' $d 0 + +# 15. The marker covers only the token it follows — an unmarked broken path in the same row +# still fails. +$d = New-Fixture +Add-Content (Join-Path $d 'AGENTS.md') '| Mixed row | `missing/dir` and `future/dir` (planned) |' +Assert-Check 'unmarked broken path beside a planned one still fails' $d 1 + if ($script:failed -gt 0) { Write-Host "smoke_test_repokit_check: $script:failed failure(s)"; exit 1 } Write-Host 'smoke_test_repokit_check: all cases passed' # Explicit success exit: the last negative case leaves $LASTEXITCODE = 1, and GitHub's pwsh