mirror of
https://github.com/zernie/vigiles.git
synced 2026-09-14 20:53:57 +08:00
94af23a5b7
Closes #173, #174, #175, #176 and #170. #174 — decideHook read exit 2, `decision` and `permissionDecision` but not the harness's halt-the-turn field, so `assertBlocksDisasters` reported a guard that stopped every disaster as blocking none. The field is read from the PORT, not hard-coded: `continue` is documented for Claude Code and unverified for Codex. The issue's adjacent suggestion was deliberately NOT implemented — adding the halt to `hook-block-ineffective` as a block-attempt signal would fire on hooks that WORK, since a halt is not event-scoped. It reads as a suppressor instead, fixing a real false alarm. A mutation implementing the original suggestion literally is pinned by a test that fails on it. #173 — compile printed its errors, exited 1, and wrote a hash-valid CLAUDE.md anyway, so `lint` went green over refs it had just called dead. Nothing is written on a failed compile now. #176 — eight small defects, including `audit --out` outside the repo appending unreachable `../../..` entries to .gitignore (a documented read-only report editing a tracked file), an unknown command exiting 1 against a documented 2, and STABILITY.md promising the removed `scan` verb. #175 + #176.2 — three missing capabilities, each opt-in and additive: `{surface}` in testGlobs, `--single`, and `rulesDir` so `.claude/rules/**` stops being invisible. #170 — one enumeration of export forms written against the grammar, both naming checks driven off it, and `check-internal-tag.mjs` asking the TypeScript compiler instead of a regex. It immediately found the issue's open case, 13 tagged types the regex had never seen, and a live gap in the ESLint rule. BREAKING CHANGE: `HookRunResult` gains a required `haltsTurn`, so code that CONSTRUCTS one (a test fake) must set it. Reading a result is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
/**
|
|
* The export-form corpus, asserted against the enumeration BOTH naming checks
|
|
* rest on (#170).
|
|
*
|
|
* The point is not that these particular forms work. It is that "did we cover
|
|
* it?" stops being a memory question. Four forms were added to
|
|
* `local/experimental-name` one at a time across four review rounds, each after
|
|
* the previous fix was called complete, and `check-internal-tag.mjs`
|
|
* independently missed two of the same ones — the same construct unnamed in two
|
|
* checkers written days apart. Patch-on-report converges slowly and never tells
|
|
* you when it is done.
|
|
*
|
|
* So a form nobody has thought of is now a MISSING ROW in
|
|
* `scripts/lib/export-forms.mjs` — reviewable — rather than a silent gap, and an
|
|
* exclusion is a row too, with its reason, because an exclusion nobody wrote
|
|
* down is indistinguishable from an oversight.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
EXPORT_FORMS,
|
|
COVERED_FORMS,
|
|
EXCLUDED_FORMS,
|
|
withTag,
|
|
} from "./export-forms.mjs";
|
|
import { exportedNames, taggedDeclarations } from "./tagged-declarations.mjs";
|
|
|
|
describe("the corpus itself", () => {
|
|
it("names every excluded form's reason", () => {
|
|
// An exclusion without a stated reason is the thing this file exists to
|
|
// prevent: indistinguishable from having forgotten the case.
|
|
for (const f of EXCLUDED_FORMS)
|
|
expect(
|
|
f.why,
|
|
`${f.id} is excluded but says nothing about why`,
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it("has no duplicate ids", () => {
|
|
const ids = EXPORT_FORMS.map((f) => f.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
it("covers the four forms that arrived one review round at a time", () => {
|
|
// Named explicitly so deleting one is a visible act, not a quiet trim.
|
|
for (const id of [
|
|
"specifier-aliased",
|
|
"specifier",
|
|
"default-function",
|
|
"destructured-object",
|
|
])
|
|
expect(EXPORT_FORMS.some((f) => f.id === id && f.covered)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("the parser enumerates every covered form", () => {
|
|
for (const form of COVERED_FORMS) {
|
|
it(`${form.id}`, () => {
|
|
expect(exportedNames(form.code).sort()).toEqual(
|
|
[...form.exported].sort(),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("a tagged declaration is found in every covered form", () => {
|
|
for (const form of COVERED_FORMS) {
|
|
if (!form.local) continue;
|
|
it(`${form.id}`, () => {
|
|
// The tag goes on the DECLARATION, wherever the export happens to be —
|
|
// which is exactly what a shape-matcher anchored on `export` cannot see.
|
|
const tagged = taggedDeclarations(withTag(form, "@internal"), [
|
|
"internal",
|
|
]);
|
|
expect(tagged.length).toBeGreaterThan(0);
|
|
expect(tagged.some((d) => d.exported)).toBe(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("the excluded forms stay excluded", () => {
|
|
it("an anonymous default yields no name to judge", () => {
|
|
const form = EXCLUDED_FORMS.find((f) => f.id === "anonymous-default");
|
|
expect(
|
|
taggedDeclarations(withTag(form!, "@internal"), ["internal"]),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("`as default` still RESOLVES — only judging its name is excluded", () => {
|
|
// The exclusion is about the naming convention, not about the parser: the
|
|
// binding is perfectly knowable, and pretending otherwise would hide a real
|
|
// export from the enumeration half.
|
|
const form = EXCLUDED_FORMS.find((f) => f.id === "specifier-default-alias");
|
|
expect(exportedNames(form?.code ?? "")).toEqual(["default"]);
|
|
});
|
|
|
|
it("a re-export from another module is not claimed as local", () => {
|
|
const form = EXCLUDED_FORMS.find((f) => f.id === "re-export-from");
|
|
// The declaration — and any tag on it — lives in the other file.
|
|
expect(exportedNames(form?.code ?? "")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("kind: values are judged, types are not", () => {
|
|
it("an interface is a type", () => {
|
|
const [d] = taggedDeclarations(
|
|
`/** @internal */\nexport interface Widget { a: number }`,
|
|
["internal"],
|
|
);
|
|
expect(d?.kind).toBe("type");
|
|
});
|
|
|
|
it("an enum is a VALUE — it emits runtime code", () => {
|
|
const [d] = taggedDeclarations(
|
|
`/** @internal */\nexport enum Widget { A }`,
|
|
["internal"],
|
|
);
|
|
expect(d?.kind).toBe("value");
|
|
});
|
|
|
|
it("a const is a value", () => {
|
|
const [d] = taggedDeclarations(
|
|
`/** @internal */\nexport const widget = 1;`,
|
|
["internal"],
|
|
);
|
|
expect(d?.kind).toBe("value");
|
|
});
|
|
});
|