Files
orca/cmd/verify-reqs/main.go
T
Jon Chery b6dd86fdf3 docs(P11): doc drift round 2 — README, cli.md, CHANGELOG, verify-reqs (REQ-160)
- README: status banner v0.12+v0.13, latest tag v0.12.10, subcommand
  table expanded (auth/nft/peer-setup/secrets rotate-master), "mTLS by
  default" corrected to "SSH-push canonical", docs table updated
- docs/cli.md: complete rewrite (521->1465 lines), all ~40 subcommands
- CHANGELOG: regenerated from git log (v0.11.29..HEAD)
- help text: job run HCL->markdown, job stop daemon->SSH-push
- docs/security-runbook.md: expanded to match P05 reality (seal/unseal,
  doctor audit/modes/oidc, incident response)
- docs/webauthn.md: added auth register (P06)
- docs/namespace.md: added inherit + set-constraint
- internal/proxmox/bootstrap.go: comments password->key auth
- internal/cli/status.go: deprecation warning
- scripts/verify-docs.sh + make verify-docs: cli.md <-> orca --help
- cmd/verify-reqs/main.go: fix bold-format regex (was bypassing v0.12)
  + case-insensitive status matching
- .ciagent/REQUIREMENTS.md: v0.12 REQs marked complete
- .ciagent/ROADMAP.md: v0.12 bolded COMPLETE

---ci---
project: orca
phase: 11
milestone: v0.13
status: complete
requirements:
  covered: [160]
---/ci---
2026-08-10 14:18:27 +00:00

198 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"sort"
"strings"
)
// reqRowRe captures a REQUIREMENTS.md table row's REQ-ID, Phase cell, and
// status in one pass. The leading .* is greedy so it consumes the
// Requirement and Priority cells (which may contain markdown-escaped pipes
// like `localhost\|linux\|proxmox` — see REQ-049) and backtracks to anchor
// the Phase + Status match at the END of the line, where those two columns
// always live. The status token is optionally wrapped in markdown bold
// (real rows use `**Complete**`; synthetic/future rows may use bare
// `pending` or `complete` in any case), and may carry trailing notes
// (e.g. "**Complete** (P01 shipped v0.2.1)") matched by [^|]* before
// the closing pipe. The (?i) flag makes the match case-insensitive so
// lowercase `pending` (used by v0.12/v0.13 REQ rows) is captured;
// normalizeStatus canonicalizes the captured value to title case.
var reqRowRe = regexp.MustCompile(`(?i)^\|\s*(REQ-\d+)\s*\|.*\|\s*([^|]*?)\s*\|\s*\*{0,2}(Complete|Pending)\*{0,2}[^|]*\|\s*$`)
// milestoneCompleteRe matches a ROADMAP.md milestone header that is marked
// COMPLETE. The bold markers are optional (GRILL #4 + REQ-160 T11): it
// matches `**COMPLETE**`, `**COMPLETE (merged to main via v0.3)**`, and
// bare `COMPLETE` (as used by the v0.12 milestone header). The word
// COMPLETE may be preceded or followed by non-asterisk text. The milestone
// version (v0.X) is captured.
var milestoneCompleteRe = regexp.MustCompile(`^##\s*Milestone\s+(v0\.\d+):.*—\s*\*{0,2}[^*]*\bCOMPLETE\b[^*]*\*{0,2}`)
// phaseRe extracts the milestone version from a REQUIREMENTS Phase cell such
// as `v0.7 P1`, `**v0.2 P01**`, `v0.2 P01P04`, or bare `v0.7`. The cell may
// contain multiple milestone refs separated by `/` or ``; each is extracted.
var phaseTokenRe = regexp.MustCompile(`v0\.\d+`)
// reqRow holds a parsed REQUIREMENTS.md row.
type reqRow struct {
id string
phase string // raw Phase cell (e.g. "v0.7 P1", "**v0.2 P01 / v0.3 P02**")
status string // "Complete" or "Pending"
}
// normalizeStatus canonicalizes a captured status token to the title-case
// form ("Complete" or "Pending") so that case-insensitive matches like
// "pending" or "complete" compare correctly against the drift assertions.
func normalizeStatus(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
// milestoneVersions returns the distinct v0.X milestones referenced in the
// phase cell (e.g. "v0.7 P1" → ["v0.7"]; "v0.2 P01 / v0.3 P02" →
// ["v0.2","v0.3"]).
func (r reqRow) milestoneVersions() []string {
matches := phaseTokenRe.FindAllString(r.phase, -1)
seen := map[string]bool{}
var out []string
for _, m := range matches {
if !seen[m] {
seen[m] = true
out = append(out, m)
}
}
return out
}
func main() {
roadmapPath := ".ciagent/ROADMAP.md"
reqsPath := ".ciagent/REQUIREMENTS.md"
if len(os.Args) > 1 {
roadmapPath = os.Args[1]
}
if len(os.Args) > 2 {
reqsPath = os.Args[2]
}
diff, count, err := verify(roadmapPath, reqsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "verify-reqs: %v\n", err)
os.Exit(2)
}
if len(diff) > 0 {
fmt.Fprintf(os.Stderr, "requirements drift detected (%d):\n", len(diff))
for _, line := range diff {
fmt.Fprintln(os.Stderr, line)
}
os.Exit(1)
}
fmt.Printf("✓ %d requirements consistent with roadmap\n", count)
}
// verify parses the ROADMAP and REQUIREMENTS markdown and returns a diff
// listing of any drift. On success diff is nil and count is the number of
// consistent REQ rows. A non-nil error signals a parse/read failure (not
// drift); drift is reported via the diff slice.
func verify(roadmapPath, reqsPath string) (diff []string, count int, err error) {
completeMilestones, err := parseRoadmap(roadmapPath)
if err != nil {
return nil, 0, fmt.Errorf("parse roadmap %q: %w", roadmapPath, err)
}
rows, err := parseRequirements(reqsPath)
if err != nil {
return nil, 0, fmt.Errorf("parse requirements %q: %w", reqsPath, err)
}
if len(rows) == 0 {
return nil, 0, fmt.Errorf("no REQ rows found in %s", reqsPath)
}
type driftEntry struct {
id string
current string
want string
dir string // "forward" or "reverse"
}
var drifts []driftEntry
for _, r := range rows {
milestones := r.milestoneVersions()
anyComplete := false
for _, m := range milestones {
if completeMilestones[m] {
anyComplete = true
break
}
}
// Forward assertion: a REQ whose milestone is COMPLETE in ROADMAP
// must be marked Complete in REQUIREMENTS.
if anyComplete && r.status != "Complete" {
drifts = append(drifts, driftEntry{r.id, r.status, "Complete", "forward"})
}
// Reverse assertion (GRILL #4): a REQ marked Complete in
// REQUIREMENTS must reference at least one milestone ROADMAP marks
// COMPLETE. If all referenced milestones are NOT complete (or no
// milestone is referenced), that is premature-Complete drift.
if r.status == "Complete" && !anyComplete {
drifts = append(drifts, driftEntry{r.id, r.status, "Pending (milestone not COMPLETE in ROADMAP)", "reverse"})
}
}
sort.Slice(drifts, func(i, j int) bool { return drifts[i].id < drifts[j].id })
for _, d := range drifts {
diff = append(diff, fmt.Sprintf(" %s: status=%s, expected=%s (direction=%s)", d.id, d.current, d.want, d.dir))
}
consistent := len(rows) - len(drifts)
return diff, consistent, nil
}
func parseRoadmap(path string) (map[string]bool, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
complete := map[string]bool{}
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
for sc.Scan() {
line := sc.Text()
m := milestoneCompleteRe.FindStringSubmatch(line)
if m != nil {
complete[m[1]] = true
}
}
if err := sc.Err(); err != nil {
return nil, err
}
return complete, nil
}
func parseRequirements(path string) ([]reqRow, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var rows []reqRow
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
for sc.Scan() {
line := sc.Text()
m := reqRowRe.FindStringSubmatch(line)
if m == nil {
continue
}
rows = append(rows, reqRow{id: m[1], phase: strings.TrimSpace(m[2]), status: normalizeStatus(m[3])})
}
if err := sc.Err(); err != nil {
return nil, err
}
return rows, nil
}