Files
zernie__vigiles/docs/comparison.md
T
zernie 848538d888 feat(site): sell the test and eval tiers with measured OSS findings (#218)
Render the test and eval tiers as measured findings rather than feature
bullets: a real vendored OSS guard's own verdicts through runHook, and
measureTriggerRate on superpowers' TDD skill at 83% over 30 runs. Both render
from committed fixtures, one regenerated from the engine and pinned by a
browser test, the other copied from the lock so CI spends no model quota.

Adds a /comparison page rendered from a probe snapshot, retires four unmeasured
claims plus two shipped-but-"planned" rows, and type-checks rendered snippets
against the real .d.ts instead of scanning them with a regex.

Committing this repo's first eval lock also revealed that the CI eval-check
step had never once executed — it short-circuited on anyLocksCommitted and
exited 0. Two defects behind it are fixed: a quota consent gate that refused a
model-free verify, and a dogfood step that now names the one eval holding a
lock.
2026-09-09 19:31:45 +05:00

13 KiB

What Changes With vigiles

Claude Code

Without vigiles With vigiles
Instructions Hand-written CLAUDE.md Compiled from .spec.ts (build artifact)
Linter rule references Trust-based (nobody checks) Verified at compile time against real config
File paths Rot silently when renamed file() references checked against filesystem
Commands Stale scripts go unnoticed cmd() references checked against package.json
Direct edits to CLAUDE.md Anyone can, nobody knows PreToolUse hook blocks edits, redirects to spec
Linter config changes CLAUDE.md drifts out of sync PostToolUse hook auto-regenerates types
Spec edits N/A PostToolUse hook auto-compiles to markdown
guidance → enforce upgrades Manual guesswork /strengthen reads per-linter docs, suggests upgrades
New lint rules from PR feedback Copy-paste from review The pr-to-lint-rule skill generates the rule + tests via @vigiles/rule-enforcer
Does a skill/plugin actually help? Unknown — stars + vibes A/B measured on real tasks: bill + correctness (measuring-skills)
CI Nothing to verify vigiles lint catches hash drift, disabled rules, stale refs

Codex

Without vigiles With vigiles
Instructions Hand-written AGENTS.md Compiled from .spec.ts
Linter rule references Trust-based Verified at compile time
File paths / commands Rot silently Checked at compile time
Direct edits to AGENTS.md Undetected CI catches hash mismatch
Hooks Hand-written [hooks] in config.toml Compiled to that same native TOML
CI Nothing to verify Same vigiles lint pipeline as Claude

Codex reads hooks from [hooks] in config.toml, and vigiles compiles to that native format — src/adapters/codex/codex.test.ts asserts such a block loads with ${PLUGIN_ROOT} expanded. What is not claimed here is auto-recompilation on edit: run vigiles compile before committing, and CI catches drift. (This row read "Codex has no hook or plugin system" until 2026-09-09, contradicted by this repo's own adapter, which declares shellHooks: true.)

What's Deterministic vs What's Not

Everything vigiles compiles and lints is deterministic — same input, same output, no LLM in the loop. The non-deterministic parts (authoring specs, suggesting upgrades, writing custom rules) are agent skills that run outside the compilation pipeline.

Check Deterministic? How
Linter rule exists ✅ Yes Node API (builtinRules) or CLI (ruff rule, rubocop --show-cops)
Linter rule is enabled in config ✅ Yes calculateConfigForFile (ESLint), --show-settings (Ruff), --show-cops (RuboCop)
Cedar policy exists ✅ Yes Scan .cedar/ and cedar/ for @id("...") annotations, with filename fallback
File path exists ✅ Yes fs.existsSync
npm script exists ✅ Yes Parsed from package.json
SHA-256 hash matches ✅ Yes Recompute and compare
Duplicate rule detection ✅ Yes Normalized Compression Distance (NCD) with fixed threshold
Orphan docs detection ✅ Yes Scan configured doc directories for .md files no other markdown references
guidance → enforce suggestion ❌ No Agent reads linter docs, reasons about intent — /strengthen skill
PR comment → lint rule ❌ No pr-to-lint-rule drives @vigiles/rule-enforcer behind its blind-gold trust gate
Spec content authoring ❌ No Agent or human writes the spec — vigiles verifies it

What vigiles Does and Doesn't Validate in Markdown

vigiles validates vigiles-specific things in .md files: <!-- vigiles:enforce ... --> comments (inline mode) and vigiles: YAML frontmatter rules (frontmatter mode) — both on by default. It can also validate enforce("...") / file("...") / cmd("...") / ref("...") calls inside fenced TS/JS code blocks, but that one is opt-in (doc-refs, default off — see below). Same engines used for .spec.ts references.

Out of scope — use other tools:

Concern Use instead
Markdown formatting / structure markdownlint
TS / JS syntax in code blocks eslint-plugin-markdown
TS type-checking of code blocks twoslash (TS official docs, Astro, etc.)
Markdown link validity (URLs, paths) markdown-link-check
Spell / prose / grammar Vale, alex

Opt-in as of 2026-08-19 (doc-refs, default off). Measured across two real repositories — 2 582 markdown files, 52 builder refs — this pass produced 0 true positives, and every error it had ever raised was false: design prose sketching an API that doesn't exist yet, and a third-party CLAUDE.md vendored as benchmark data. The cause is structural, not a threshold: a fenced block in prose is a drawing of config, and the pass read it as config. Turn it on with {"rules": {"doc-refs": "error"}} where markdown genuinely is the source. Full measurement and the known gap: docs/rules/doc-refs.md.

What counts as a ref (2026-08-19). Blocks are PARSED, not pattern-matched. A ref is a call whose callee is a bare enforce / file / cmd / ref identifier with a string-literal first argument. That means a method call on some other object (ctx.file("OUT")), a mention inside a comment (// cmd("npm test")), and a string containing one ('cmd("x")') are all NOT refs — none of them is a call expression to the builder. Until this release those three were matched by a regex and reported as broken refs; \b sits happily after a ., and a regex has no notion of a comment or a string literal. Parsing makes all three inexpressible rather than individually excused.

Scope (when enabled). The pass reads **/*.md from the repo root and honours the top-level exclude in .vigilesrc.json, so vendored or benchmark markdown (a third-party CLAUDE.md captured verbatim as test data) can be kept out of it. Before 2026-08-19 exclude was not applied here at all, which meant a repository vendoring other people's markdown had no way to reach a clean lint — and a lint that cannot exit 0 gets its exit code discarded, at which point it gates nothing.

Illustrative code blocks (typo demos, template placeholders, speculative refs in design docs) opt out via <!-- vigiles:ignore --> immediately before the fence, or <!-- vigiles:ignore-file --> anywhere in a file that's entirely illustrative. Placeholders containing < or > are auto-skipped. Refs that can't be verified because the underlying tool isn't installed (e.g. pylint/X on a machine without pylint) are reported separately from real errors.

What vigiles composes with

vigiles owns one thing: compile-time verification of typed specs against real linter configs, filesystems, and package.json, plus testing the harness those specs describe. Everything else, compose:

  • Architectural linting — ast-grep, Dependency Cruiser, Steiger. Reference their rules via enforce().
  • File sync across agents — Ruler, rulesync, block/ai-rules. vigiles compiles the source; sync tools distribute. For non-markdown formats (.cursorrules, Copilot), rule-porter or rulesync convert the compiled output.
  • Markdown linting — markdownlint. vigiles generates markdown; structure is correct by construction.
  • Code-block linting in docs — eslint-plugin-markdown for syntax, twoslash for TS type-checking.
  • Prose quality — Vale. Different concern.
  • Runtime LLM rule checking — opposite paradigm: those tools send your code to a model on every check (tokens, non-reproducible verdicts); vigiles compiles once and checks deterministically forever after with eslint, ruff, tsc, Cedar evaluation.

Specs compile to CLAUDE.md by default; set target: "AGENTS.md" or target: ["CLAUDE.md", "AGENTS.md"] for multiple outputs from one spec. See the spec format reference.

Flow

                        DETERMINISTIC                          AGENT-ASSISTED
                  ┌─────────────────────────┐          ┌──────────────────────────┐
                  │                         │          │                          │
  .spec.ts ──────┤  vigiles compile         │          │  /strengthen             │
       │         │    ✓ linter rules exist   │          │    guidance → enforce    │
       │         │    ✓ rules enabled        │          │                          │
       │         │    ✓ file paths valid     │          │  /pr-to-lint-rule        │
       │         │    ✓ commands valid       │          │    rule synthesis skill  │
       │         │    → CLAUDE.md + hash     │          │                          │
       │         └─────────────────────────┘          │  /edit-spec              │
       │                                               │    agent edits .spec.ts  │
       │         ┌─────────────────────────┐          └──────────────────────────┘
       └────────▶│  vigiles lint           │                     │
                 │    ✓ hash integrity      │                     │
                 │    ✓ inline rule checks  │                     ▼
                 │    ✓ duplicate detection │          ┌──────────────────────────┐
                 │    ✓ orphan docs check   │          │  hooks (Claude Code)     │
                 │    ✓ coverage gaps       │          │    auto-compile on edit  │
                 └─────────────────────────┘          │    auto-regen types      │
                                                       │    block direct md edits │
                                                       └──────────────────────────┘