Skip to content

PR 3a — audit binary substrate + extract/status subcommands#193

Merged
jakebromberg merged 4 commits into
mainfrom
feat/pr3a-binary-substrate
May 30, 2026
Merged

PR 3a — audit binary substrate + extract/status subcommands#193
jakebromberg merged 4 commits into
mainfrom
feat/pr3a-binary-substrate

Conversation

@jakebromberg

Copy link
Copy Markdown
Owner

Summary

First slice of the audit binary per tracker #177's PR 3 plan. Ships the substrate (front-matter / manifest / catalog-envelope parsers), .audit/ cache, discovery, extractor subprocess runner, and the extract + status subcommands. The query subcommand stubs out with a pointer to the follow-up PR.

What this PR delivers

  • audit extract <name> — runs every [[command]] block in extractors/<name>/manifest.toml as a subprocess; materializes catalogs into .audit/catalogs/<file>; updates .audit/meta.json; auto-bootstraps .gitignore.
  • audit status — prints .audit/ state, resolved queries/extractors source paths, source-mtime staleness, and root-vs-cwd warnings.
  • audit query <name> — stub pointing at the follow-up PR.
  • New Go module at the repo root (github.com/jakebromberg/code-audit-pipeline). Pinned BurntSushi/toml v1.4.0. gojq lands in the follow-up.
  • go generate ./... copies pipeline/queries/*.jq into cmd/audit/queries/ so the //go:embed directive ships the canonical query corpus alongside the binary.
  • CI gains a go job (vet + test + build).

Architecture (per the ADRs)

  • ADR-0001 (amended by 0007): .audit/meta.json schema with audit_version, last_touched_at, root, catalogs[<kind>] (path, source_sha, envelope_summary, cli_args).
  • ADR-0002: two parsers — #! key: value front-matter on .jq files; TOML manifest.toml on extractor dirs.
  • ADR-0004: binary as router. Extractors stay external and are invoked via os/exec.
  • ADR-0006: lookup-order discovery for both queries and extractors (--flag$AUDIT_HOME → cwd-relative → embedded queries / ~/.config/audit/extractors).
  • ADR-0007: meta.json.envelope_summary is a derived cache; catalog file is authoritative; refreshed on every status/query if the file's sha256 differs.

Diff size note

~2.8k lines including ~490 lines of plan doc and ~800 lines of Go tests. Production Go is ~1100 lines across 12 files. Over the 1000-line soft limit, but coherent as one reviewable unit: every package is exercised by extract + status. The follow-up PR (engine + query) is sized at ~800 lines.

Test plan

  • go vet ./... passes
  • go test ./... passes (with -race)
  • go build ./cmd/audit produces a binary
  • Smoke test: audit extract file-hashes --root <tmp> --audit-root <tmp> produces .audit/catalogs/file-hashes.json and a populated meta.json
  • Smoke test: audit status --root <tmp> reports cached catalogs; when invoked from the audit-pipeline checkout, it resolves queries+extractors via cwd-relative discovery and exits 0
  • Smoke test: audit query <anything> exits 2 with the follow-up-PR message
  • Existing CI (shellcheck, pipeline, swift) untouched

Closes #191

@jakebromberg

Copy link
Copy Markdown
Owner Author

Code review

Found 4 issues:

  1. Discovery lookup order inverted vs ADR-0006. docs/adr/0006-bundling-and-discovery.md prescribes "explicit flags, then cwd-relative paths, then $AUDIT_HOME, then a fallback." The implementation appends $AUDIT_HOME before cwd-relative. A contributor with AUDIT_HOME set (which audit init plants in shell profiles per ADR-0006's "Consequences") would never pick up local edits in ./pipeline/queries/ — the cwd-relative contributor path is dead in their environment.

candidates := []struct{ path, label string }{}
if opts.Flag != "" {
candidates = append(candidates, struct{ path, label string }{opts.Flag, "--queries-dir " + opts.Flag})
}
if opts.AuditHome != "" {
candidates = append(candidates, struct{ path, label string }{
filepath.Join(opts.AuditHome, "pipeline", "queries"),
"$AUDIT_HOME (" + opts.AuditHome + ")",
})
}
candidates = append(candidates, struct{ path, label string }{
filepath.Join(cwd, "pipeline", "queries"),
"cwd-relative " + filepath.Join(cwd, "pipeline", "queries"),
})

  1. audit status exits 0 when a registered catalog file is missing on disk. The print loop sets marker = "MISSING" but does not set staleAny = true. The exit-code logic only flips to 1 on rootMismatch, query/extractor resolution failure, or staleAny. The function's own doc comment says "0 if all catalogs are fresh and the cwd matches meta.json.root; 1 otherwise" — MISSING violates "fresh," so the path should exit 1.

staleAny := false
for _, r := range rows {
marker := "ok"
if !r.Exists {
marker = "MISSING"
} else if r.StaleSourceCount > 0 {
marker = fmt.Sprintf("%d files newer than catalog", r.StaleSourceCount)
staleAny = true
}

  1. audit extract leaves meta.json inconsistent with disk on partial failure. The typescript manifest has two [[command]] blocks. If the first succeeds (catalog file written, cache.PutCatalog updates the in-memory map) and the second fails, the function returns 1 without calling cache.Save(). The first catalog file remains on disk but meta.json never records it. Next audit status shows nothing because it reads the stale on-disk meta.json.

results, err := extractor.Run(ctx, extractorDir, cmd, args, catalogsDir)
if err != nil {
fmt.Fprintf(out, "audit: extract %s/%s: %v\n", name, cmd.Catalog, err)
return 1
}
for _, r := range results {
outBase := filepath.Base(r.OutputPath)
if err := cache.PutCatalog(r.Catalog, outBase, r.CLIArgs); err != nil {
fmt.Fprintf(out, "audit: cache put %s: %v\n", r.Catalog, err)
return 1
}
}
ran++
}
if ran == 0 {
fmt.Fprintf(out, "audit: no [[command]] blocks matched (manifest has %d, filter selected 0)\n", len(m.Commands))
return 2
}
if err := cache.Save(); err != nil {
fmt.Fprintf(out, "audit: save cache: %v\n", err)
return 1

  1. ErrNoCommand is exported but unused (~/.claude/CLAUDE.md says "Don't add features, refactor, or introduce abstractions beyond what the task requires"). The comment claims it "distinguishes 'extractor invocation failed' from 'extractor was filtered out by --catalog flags'" but cli/extract.go checks ran == 0 directly and emits its own message — the sentinel has no consumer.

https://github.com/jakebromberg/code-audit-pipeline/blob/4d385f0ac525402930ff3b00519ffc27c23c2651/internal/extractor/runner.go#L171-L175

jakebromberg added a commit that referenced this pull request May 30, 2026
… drop ErrNoCommand

Four code-review findings on PR 3a (#193):

internal/discovery/discovery.go: candidate ordering put $AUDIT_HOME before cwd-relative, contradicting ADR-0006's documented "explicit flag, cwd-relative, then $AUDIT_HOME, then fallback" chain. A contributor with AUDIT_HOME set (which `audit init` plants in shell profiles) could never pick up local edits in ./pipeline/queries. Swapped the append order and added two tests (TestQueriesCwdBeatsAuditHome, TestExtractorsCwdBeatsAuditHome) locking in the precedence.

internal/cli/status.go: a registered catalog whose file is missing on disk printed "MISSING" but did not set staleAny=true, so `audit status` exited 0 — directly contradicting its own doc comment ("exit 1 if any catalog is...not fresh"). Set staleAny=true in the !r.Exists branch.

internal/cli/extract.go: when one [[command]] block in a multi-command manifest succeeded but a later one failed, the function returned 1 without calling cache.Save(), leaving meta.json out of sync with the catalog files on disk. Now: capture firstErr, break the loop, save the cache if any command succeeded, then propagate the error. Multi-command manifests (typescript has type-catalog + function-catalog) get an accurate meta.json even on partial failure.

internal/extractor/runner.go: removed the exported ErrNoCommand sentinel — it had no consumer; the CLI checks `ran == 0` directly. Removing it also dropped the now-unused `errors` import.
jakebromberg added a commit that referenced this pull request May 30, 2026
Follow-up to PR 3a (#193). Implements the gojq evaluator and the `audit query` handler.

internal/engine adds a gojq wrapper supporting -L, --arg, --argjson, --slurpfile, -n, -r, $ENV.NAME, plus a `#! engine: jq` shell-out fallback per ADR-0005. Library resolution works against both filesystem-rooted and embed.FS-rooted query directories.

internal/cli/query.go parses each query's front-matter to validate --arg / --argjson and wires the catalog declarations into positional input + slurpfile bindings. Covers all seven distinct catalog combinations produced by PR 2's front-matter (including the two-of-same-kind cross-catalog-name-collisions case).

cmd/audit/main.go re-enables the `query` case that PR 3a stubbed out, and cmd/audit/audit_test.go ships an integration test exercising extract → query → status end-to-end on a synthetic file-hashes fixture.

go.mod pins itchyny/gojq v0.12.19 — the version verified parity-clean in #188.
First slice of the audit binary per tracker #177's PR 3 plan. Implements the substrate parsers (front-matter for .jq files, manifest.toml for extractor dirs, catalog envelope reader), per-repo .audit/ cache management per ADR-0001 amended by ADR-0007, lookup-order discovery for queries and extractors per ADR-0006, and subprocess invocation of extractors via each manifest's [[command]] blocks.

Subcommands shipped: extract, status. The query subcommand stubs out with a message pointing at the follow-up PR (gojq engine).

go.mod pins BurntSushi/toml v1.4.0. gojq lands in the follow-up.

CI adds a `go` job running vet + test + build on the embedded queries (populated by `go generate ./...`).

See plans/pr3-binary-skeleton.md for the full design; the split rationale is at the top of that file.
…st helpers

Hook-flagged fixes:

- ci.yml: my copy from a pre-rebase worktree clobbered the v6/v5 action pins from 841a99b. Restore actions/checkout@v6, actions/setup-node@v6, actions/cache@v5, and use them on the new go job too.
- internal/cli/extract.go + status.go: discovery cwd was the user's audit-root (where catalogs live), but extractors/queries live next to the audit-pipeline source. Use the binary's actual os.Getwd() instead, with HomeDir set on the first call rather than a fallback retry.
- internal/extractor/runner.go: drop hard-coded AUDIT_VERSION=skeleton env. No extractor reads it yet — add it back when a need lands.
- internal/extractor/runner_test.go: replace lookNode/exec_LookPath/lookPath wrappers with a direct exec.LookPath("node") call. The "import cycle" comment was wrong; there's no cycle.
… drop ErrNoCommand

Four code-review findings on PR 3a (#193):

internal/discovery/discovery.go: candidate ordering put $AUDIT_HOME before cwd-relative, contradicting ADR-0006's documented "explicit flag, cwd-relative, then $AUDIT_HOME, then fallback" chain. A contributor with AUDIT_HOME set (which `audit init` plants in shell profiles) could never pick up local edits in ./pipeline/queries. Swapped the append order and added two tests (TestQueriesCwdBeatsAuditHome, TestExtractorsCwdBeatsAuditHome) locking in the precedence.

internal/cli/status.go: a registered catalog whose file is missing on disk printed "MISSING" but did not set staleAny=true, so `audit status` exited 0 — directly contradicting its own doc comment ("exit 1 if any catalog is...not fresh"). Set staleAny=true in the !r.Exists branch.

internal/cli/extract.go: when one [[command]] block in a multi-command manifest succeeded but a later one failed, the function returned 1 without calling cache.Save(), leaving meta.json out of sync with the catalog files on disk. Now: capture firstErr, break the loop, save the cache if any command succeeded, then propagate the error. Multi-command manifests (typescript has type-catalog + function-catalog) get an accurate meta.json even on partial failure.

internal/extractor/runner.go: removed the exported ErrNoCommand sentinel — it had no consumer; the CLI checks `ran == 0` directly. Removing it also dropped the now-unused `errors` import.
Commit 22f00f0 added `{ flag = "--include-imports", when = "include_imports_enabled" }` to the TypeScript manifest after this branch's substrate commit. The manifest parser rejected the unknown `when` value, breaking `go test ./...`.

Adds `include_imports_enabled` to the validWhen allowlist, plumbs IncludeImports through Args/whenSatisfied/recordArg, and wires `--include-imports` into `audit extract`.
@jakebromberg
jakebromberg force-pushed the feat/pr3a-binary-substrate branch from 91f51c6 to 5e8a596 Compare May 30, 2026 18:55
jakebromberg added a commit that referenced this pull request May 30, 2026
Follow-up to PR 3a (#193). Implements the gojq evaluator and the `audit query` handler.

internal/engine adds a gojq wrapper supporting -L, --arg, --argjson, --slurpfile, -n, -r, $ENV.NAME, plus a `#! engine: jq` shell-out fallback per ADR-0005. Library resolution works against both filesystem-rooted and embed.FS-rooted query directories.

internal/cli/query.go parses each query's front-matter to validate --arg / --argjson and wires the catalog declarations into positional input + slurpfile bindings. Covers all seven distinct catalog combinations produced by PR 2's front-matter (including the two-of-same-kind cross-catalog-name-collisions case).

cmd/audit/main.go re-enables the `query` case that PR 3a stubbed out, and cmd/audit/audit_test.go ships an integration test exercising extract → query → status end-to-end on a synthetic file-hashes fixture.

go.mod pins itchyny/gojq v0.12.19 — the version verified parity-clean in #188.
@jakebromberg
jakebromberg merged commit 0955f5f into main May 30, 2026
4 checks passed
jakebromberg added a commit that referenced this pull request May 30, 2026
… drop ErrNoCommand

Four code-review findings on PR 3a (#193):

internal/discovery/discovery.go: candidate ordering put $AUDIT_HOME before cwd-relative, contradicting ADR-0006's documented "explicit flag, cwd-relative, then $AUDIT_HOME, then fallback" chain. A contributor with AUDIT_HOME set (which `audit init` plants in shell profiles) could never pick up local edits in ./pipeline/queries. Swapped the append order and added two tests (TestQueriesCwdBeatsAuditHome, TestExtractorsCwdBeatsAuditHome) locking in the precedence.

internal/cli/status.go: a registered catalog whose file is missing on disk printed "MISSING" but did not set staleAny=true, so `audit status` exited 0 — directly contradicting its own doc comment ("exit 1 if any catalog is...not fresh"). Set staleAny=true in the !r.Exists branch.

internal/cli/extract.go: when one [[command]] block in a multi-command manifest succeeded but a later one failed, the function returned 1 without calling cache.Save(), leaving meta.json out of sync with the catalog files on disk. Now: capture firstErr, break the loop, save the cache if any command succeeded, then propagate the error. Multi-command manifests (typescript has type-catalog + function-catalog) get an accurate meta.json even on partial failure.

internal/extractor/runner.go: removed the exported ErrNoCommand sentinel — it had no consumer; the CLI checks `ran == 0` directly. Removing it also dropped the now-unused `errors` import.
jakebromberg added a commit that referenced this pull request May 30, 2026
Follow-up to PR 3a (#193). Implements the gojq evaluator and the `audit query` handler.

internal/engine adds a gojq wrapper supporting -L, --arg, --argjson, --slurpfile, -n, -r, $ENV.NAME, plus a `#! engine: jq` shell-out fallback per ADR-0005. Library resolution works against both filesystem-rooted and embed.FS-rooted query directories.

internal/cli/query.go parses each query's front-matter to validate --arg / --argjson and wires the catalog declarations into positional input + slurpfile bindings. Covers all seven distinct catalog combinations produced by PR 2's front-matter (including the two-of-same-kind cross-catalog-name-collisions case).

cmd/audit/main.go re-enables the `query` case that PR 3a stubbed out, and cmd/audit/audit_test.go ships an integration test exercising extract → query → status end-to-end on a synthetic file-hashes fixture.

go.mod pins itchyny/gojq v0.12.19 — the version verified parity-clean in #188.
jakebromberg added a commit that referenced this pull request May 30, 2026
Follow-up to PR 3a (#193). Implements the gojq evaluator and the `audit query` handler.

internal/engine adds a gojq wrapper supporting -L, --arg, --argjson, --slurpfile, -n, -r, $ENV.NAME, plus a `#! engine: jq` shell-out fallback per ADR-0005. Library resolution works against both filesystem-rooted and embed.FS-rooted query directories.

internal/cli/query.go parses each query's front-matter to validate --arg / --argjson and wires the catalog declarations into positional input + slurpfile bindings. Covers all seven distinct catalog combinations produced by PR 2's front-matter (including the two-of-same-kind cross-catalog-name-collisions case).

cmd/audit/main.go re-enables the `query` case that PR 3a stubbed out, and cmd/audit/audit_test.go ships an integration test exercising extract → query → status end-to-end on a synthetic file-hashes fixture.

go.mod pins itchyny/gojq v0.12.19 — the version verified parity-clean in #188.
@jakebromberg
jakebromberg deleted the feat/pr3a-binary-substrate branch May 30, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR 3a — audit binary substrate + extract/status subcommands

1 participant