Files
zernie__vigiles/docs/experimental.md
Claude 79de307fe8 feat: render a plugin-guard sweep, without giving an unmeasured hook a number
`experimental_verifyPluginGuards` returned a discriminated union and nothing to
print it with, so the one use case it exists for — "point the battery at YOUR
hooks" — was a six-line fold over that union at every call site. That is a worse
code block than the two-liner it replaces, and `cohesive-feature-delivery` row 9
(output parity) could not be claimed.

`experimental_formatPluginGuardReport(report)` renders it as terminal text, and
its contract is that the union's honesty survives becoming a string:

  measured        prints `blocks n/7` and one row per battery event
  not-applicable  prints its REASON under a ⊘ heading, and no count
  unresolved      likewise
  notes           printed FIRST and in full

A rendered `0/7` beside a hook the battery never reached would reintroduce, one
layer above the type system, exactly the false confidence the union was built to
make unrepresentable. So it is asserted directly and in both directions: for a
sweep that measured nothing the output must match no `n/m` at all, and must
carry the sentence saying nothing was measured. Mutating the formatter to print
`blocks 0/7` for a not-applicable hook fails that assertion and three others.

MANY HOOKS stay readable by grouping the unmeasured half BY REASON — a repo with
thirty hooks usually has two or three — and naming at most three hooks per
reason before counting the rest. That is the only lossy step, and it is confined
to the half whose payload is the reason rather than the hook; the measured half
is never collapsed, since those are the hooks you came for.

The per-event rows come from a shared `guardrailRow`, so this renderer and
`formatGuardrailReport` cannot drift into two vocabularies for the same three
outcomes; `formatGuardrailReport`'s output is byte-identical.

The prefix is on the name because the report SHAPE is documented as likely to
move, and a stable formatter pinned to an unstable shape is a lie. It comes off
with the same change that takes it off the sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 22:13:57 +00:00

7.0 KiB

What experimental_ means

If a name you import from vigiles starts with experimental_, we ship it but do not promise to keep it. It can change shape, be renamed, or disappear in any release, including a patch release. Renaming it is not a breaking change and does not bump the major version.

Everything so named has a stable alternative. The feature's own docs page says what it is.

Contents

The rule

If we export it and it is not stable, its name starts with experimental_.

Why the name, and not a note in the docs or the changelog: the name is in every call site, every diff, every review and every autocomplete for as long as the thing is unstable. An import line scrolls out of view. A changelog is read once. A doc note is read never.

Two checks keep the rule honest:

  • The tag and the name cannot disagree. Every experimental export is declared once with the @experimental TSDoc tag. An ESLint rule (local/experimental-name) fails the build if a tagged export lacks the prefix.
  • One experimental root per feature. The prefix says whether this name is stable. It cannot say whether the names it depends on are. So a feature's whole vocabulary hangs off one prefixed root: experimental_agent.result(), never a plain result() that only works with experimental_agent(). The root's name also survives destructuring — const { result } = experimental_agent still names the root at the binding site — which an import path does not.

What is experimental today

Symbol Since Why it is not stable yet
experimental_defineHook and its five siblings (experimental_defineFileGate, experimental_definePromptGate, experimental_defineStopGate, experimental_defineInject, experimental_defineReact) 2026-08-20 The named-state API (record / state) has one author and no outside user. There is no supported way to seed that state in a test other than runHook. vigiles compile appends a duplicate wiring block when re-run. Details: compiled hooks.
experimental_alternateSpellings 2026-09-02 Days old. Its only user is this repo's own test. The set of rewrite rules and the id format of the generated cases are the parts most likely to change. Details: the disaster battery.
experimental_verifyPluginGuards 2026-09-07 Days old, with no user outside this repository. The REPORT SHAPE is what is most likely to move — whether not-applicable stays one status or splits by cause, and whether the per-hook counts stay arrays of event ids. The prefix comes off when that shape survives sweeping several real third-party repos unchanged. Details: sweep every hook a repo declares.
experimental_formatPluginGuardReport 2026-09-07 It renders PluginGuardReport, so it inherits that shape's instability exactly — a stable name here would promise more than its only input can. The prefix comes off with the same change that takes it off the sweep. Details: sweep every hook a repo declares.

When the prefix comes off

All four of these must hold. They are about other people on purpose: everything else we could judge ourselves, and we would be generous.

  1. Someone outside this repository uses it, and we know what for. Our own in-repo test does not count: same author, same assumptions.
  2. Nothing about its shape has moved for a full release cycle. No signature, no return type, no option name.
  3. Every gap in the table above is closed, or written down as a permanent limitation with its reason.
  4. Testing it needs no private knowledge. If the only way to test it is to hard-code a path the runtime computes internally, it is not finished: a user cannot test their own use of it.

Dropping the prefix is a rename, so it ships as a feat! with a major bump.

How we learned this

skill() shipped under a stable name while docs/skills.md opened with "skill() is experimental". Nothing caught the contradiction, because the rule was a habit rather than a check.

We also tried a vigiles/experimental import path. It marked the import line, which is out of view by the time anyone reads the call. It was retired.

See also

  • Compiled hooks — the largest experimental surface, and the guide that explains what each entry point does.
  • Stability — the stability promise for everything that is not prefixed.