@agenttrail/guardrails 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,28 +3,23 @@ export { Action, FixtureInput, Fixtures, Match, RuleInput, Severity } from './sc
3
3
  import 'zod';
4
4
 
5
5
  /**
6
- * The eight packs, and the order they are presented in.
6
+ * The eleven packs, and the order they are presented in.
7
7
  *
8
8
  * These ids are authoritative and shared with the rules authored against them,
9
- * and with the guard's `config.json` `enabledPacks` (spec 11). An earlier design
10
- * named ten differently-titled packs; the two could not both have been right.
11
- * Renaming one of these is a breaking change to a user's config file, not a
12
- * tidy-up — `safety-bypass` was `guardrail-bypass` until the rename, and that move
13
- * was only free because nothing was published yet.
14
- *
15
- * Working-tree destruction leads deliberately. Destructive git commands are the
16
- * most frequently documented real-world coding-agent failure, with filed
17
- * incidents across Claude Code, Gemini CLI and Codex — while our own real-trace
18
- * mining found production destructive-SQL at zero.
19
- *
20
- * The per-pack counts in spec 7.1 are a budget, not a quota: trim or extend at
21
- * authoring time. They are deliberately NOT encoded here, because a rule count
22
- * asserted in code becomes a reason to write a weak rule to hit a number.
9
+ * and with the guard's `config.json` `enabledPacks`. Renaming one of these is a
10
+ * breaking change to a user's config file.
11
+ *
12
+ * Working-tree destruction leads: destructive git commands are the most frequently
13
+ * documented coding-agent failure, with public incident reports across Claude
14
+ * Code, Gemini CLI and Codex.
15
+ *
16
+ * Per-pack rule counts are deliberately NOT encoded here: a rule count asserted in
17
+ * code becomes a reason to write a weak rule to hit a number.
23
18
  */
24
- declare const PACKS: readonly ["working-tree", "destructive-data", "prod-infra", "secret-exposure", "rce-supply-chain", "safety-bypass", "privilege-supply-chain", "file-scope"];
25
- /** One of the eight pack ids. A rule's `category` is always one of these. */
19
+ declare const PACKS: readonly ["working-tree", "destructive-data", "prod-infra", "secret-exposure", "rce-supply-chain", "safety-bypass", "privilege-supply-chain", "file-scope", "agent-context", "test-integrity", "exfiltration"];
20
+ /** One of the eleven pack ids. A rule's `category` is always one of these. */
26
21
  type Pack = (typeof PACKS)[number];
27
- /** Is this string one of the eight pack ids? */
22
+ /** Is this string one of the eleven pack ids? */
28
23
  declare function isPack(value: string): value is Pack;
29
24
 
30
25
  /**
@@ -42,17 +37,9 @@ declare function isPack(value: string): value is Pack;
42
37
  * echo "never run rm -rf /" -> DENY dd.rm-rf-absolute
43
38
  * curl --data "we ran rm -rf /tmp/x" https://… -> DENY dd.rm-rf-absolute
44
39
  *
45
- * A person documenting this tool is blocked by this tool. Measured on the
46
- * shipped bundle, **48 of the 56 rules had it — every command-channel rule.**
47
- * The eight that did not are the `file_glob` rules, which never see a command.
48
- *
49
- * It was NOT found by the 438 fixtures or the 307-entry quiet corpus, and could
50
- * not have been: every entry in both is a command somebody wrote on purpose. It
51
- * took 997 real transcripts, where an automated `curl` posting an issue-tracker comment
52
- * whose BODY discussed `rm -rf` was labelled "rm -rf against an absolute path".
53
- * That is why the quoted-mention family is now generated as a cross-product over
54
- * the whole corpus (`guardrails-quoted-mention.test.ts`) rather than listed by
55
- * hand: a hand-written list only covers the rules somebody remembered.
40
+ * Without an exemption, a person documenting this tool is blocked by this tool.
41
+ * That applies to every command-channel rule — 62 of the 74. The twelve `file_glob`
42
+ * rules never see a command.
56
43
  *
57
44
  * ── Why exemption by CARRIER VERB, and not by "the payload is quoted" ────────
58
45
  *
@@ -73,17 +60,14 @@ declare function isPack(value: string): value is Pack;
73
60
  * echo "rm -rf /" | bash -> NOT exempt: `|` is outside the quotes
74
61
  * echo "$(rm -rf /var)" -> NOT exempt: the shell expands `$( )`
75
62
  *
76
- * This is stricter than the `block-destructive-sql` treatment it generalises.
77
- * That rule — the ONLY one that had this exclusion, which is the defect — excluded
78
- * any command whose text NAMED a search tool, anywhere, and so left a documented
79
- * hole on compound commands: `grep x . && rm -rf /` was exempt. Anchoring closes
80
- * it. Its loose condition is KEPT alongside the anchored one rather than replaced:
81
- * the loose form still covers a search that is not the FIRST word — measured,
82
- * `cat runbook.md | grep -n TRUNCATE` is exempt only because of it — and a fix is
83
- * not allowed to regress what the rule already allowed. The other four rules with a pre-existing `none_of`
63
+ * `block-destructive-sql` also keeps a looser condition of its own: it exempts any
64
+ * command whose text NAMES a search or history tool, anywhere. That covers a search
65
+ * that is not the FIRST word, such as `cat runbook.md | grep -n TRUNCATE`, and it is
66
+ * why that rule misses a compound command that both searches and executes, as its
67
+ * description states. The other four rules with their own `none_of`
84
68
  * (`se.env-print`, `require-approval-rm-rf`, `wt.clean-fdx`, `wt.restore-path`)
85
69
  * exclude something else entirely — a dry run, a build directory, `--staged` —
86
- * and are likewise appended to, never replaced.
70
+ * and have these conditions appended, not replaced.
87
71
  *
88
72
  * ── The character classes, and why each character is in them ─────────────────
89
73
  *
@@ -204,8 +188,8 @@ declare const QUOTED_MENTION: readonly MatchCondition[];
204
188
  * harness reads `"command" in fixture`.
205
189
  *
206
190
  * Writing every fixture out longhand would work and would be unreadable, so
207
- * these exist instead. They are also more honest than the shorthand was: the
208
- * CHANNEL is now visible at every single fixture, and the channel is the thing
191
+ * these exist instead. They also make the CHANNEL visible at every single
192
+ * fixture, and the channel is the thing
209
193
  * that silently makes a fixture vacuous — a `file_glob` rule handed a command
210
194
  * fixture matches nothing, and an `allow` fixture that matches nothing passes.
211
195
  */
@@ -254,35 +238,31 @@ declare function mentions(text: string): Fixture[];
254
238
  * The catalog's own version and publication date.
255
239
  *
256
240
  * These are the two facts the offline guard needs in order to tell someone how old
257
- * their protection is. The guard has no update check and no network path for rules
258
- * (spec 8.4), so a user's rules are frozen on the day they installed and nothing
259
- * else in the system can tell them that.
241
+ * their protection is. The guard has no update check and no network path for rules,
242
+ * so a user's rules are frozen on the day they installed and nothing else can tell
243
+ * them that.
260
244
  *
261
245
  * ── Why the stamp lives HERE and not in `@agenttrail/guard` ──────────────────
262
- * The guard BUNDLES this package at build time (spec 11.1.1), so it is *this*
263
- * package's version that determines which rules a user is actually running. The
264
- * guard's own version answers a different question and would be the wrong number:
265
- * `guard@0.4.1` may ship an unchanged catalog, and two different guard versions may
266
- * carry the same rules. Spec 11.1.1 states it outright — "that is exactly why C6
267
- * has `status` print the bundled catalog version rather than the guard's."
246
+ * The guard BUNDLES this package at build time, so it is *this* package's version
247
+ * that determines which rules a user is actually running. The guard's own version
248
+ * answers a different question and would be the wrong number: `guard@0.4.1` may ship
249
+ * an unchanged catalog, and two different guard versions may carry the same rules.
268
250
  *
269
251
  * ── This module imports nothing, and that is load-bearing ───────────────────
270
- * It is exported from `rules.ts`, the zod-free entry `packages/guard` bundles into the
252
+ * It is exported from `rules.ts`, the zod-free entry the guard bundles into the
271
253
  * file Claude Code runs before every tool call. Two string constants add nothing to
272
254
  * that bundle; an import of `schema.ts` here would add zod to it. Add no imports here.
273
255
  *
274
256
  * ── Both constants are hand-written, and that is deliberate ──────────────────
275
- * A build-time `define` would reach `dist/` only. In this monorepo the guard resolves
276
- * `@agenttrail/guardrails` to `src/index.ts`, so a defined value would exist in the
277
- * published tarball and be absent everywhere a test can see it — the shipped value
278
- * would be the one value never exercised. `guard/src/core/version.ts` sets the same
279
- * precedent, and `__tests__/stamp.test.ts` pins `CATALOG_VERSION` to `package.json`
280
- * so the two cannot drift in silence.
257
+ * A build-time `define` would reach `dist/` only, so the value would exist in the
258
+ * published tarball and be absent from the source every test imports — the shipped
259
+ * value would be the one value never exercised. `__tests__/stamp.test.ts` pins
260
+ * `CATALOG_VERSION` to `package.json` so the two cannot drift in silence.
281
261
  *
282
262
  * ── AT RELEASE, BUMP BOTH CONSTANTS BELOW, TOGETHER ──────────────────────────
283
263
  * Bumping `package.json`'s version alone turns `stamp.test.ts` red, and that is the
284
264
  * point: it is what brings whoever cut the release into this file, where the date is
285
- * a few lines away. Publish order is `guardrails` first, then `guard` (spec 11.1.1);
265
+ * a few lines away. Publish order is `guardrails` first, then `guard`;
286
266
  * a guard release carries whatever stamp was here when it was built.
287
267
  *
288
268
  * Forgetting to advance the date understates how fresh the rules are, which tells a
@@ -296,36 +276,26 @@ declare function mentions(text: string): Fixture[];
296
276
  * Pinned to `package.json`'s `version` by `__tests__/stamp.test.ts` — this is a copy
297
277
  * of that value, not an independent one, and the test is what makes the copy safe.
298
278
  */
299
- declare const CATALOG_VERSION = "0.0.1";
279
+ declare const CATALOG_VERSION = "0.1.0";
300
280
  /**
301
- * When this catalog version was cut, as an ISO-8601 instant.
302
- *
303
- * For a version published to npm this is its publish date. For a pre-release cut from
304
- * the monorepo it is the commit that brought the corpus to this state — here,
305
- * `252c2e2c` ("feat(guardrails): rule schema, pack registry and fixture harness"),
306
- * which created the package at `0.0.1`. Both readings answer the user's actual
307
- * question, which is not "when did this reach a registry" but "how old are the rules
308
- * I am running".
309
- *
310
- * It is a real, sourced instant rather than a plausible-looking default. If a future
311
- * release cannot source one, leave it absent rather than approximate: the renderer in
312
- * `guard/src/core/catalog-stamp.ts` degrades honestly, and a wrong date that looks
313
- * right is worse than a missing one in a tool whose whole proposition is candour
314
- * about its own coverage.
281
+ * When the rules in this catalog reached their current state, as an ISO-8601
282
+ * instant. The question it answers is not "when did this reach a registry" but "how
283
+ * old are the rules I am running".
284
+ *
285
+ * If a release cannot source a real date, leave it absent rather than approximate:
286
+ * the guard handles a missing date, and a wrong date that looks right is worse than
287
+ * a missing one.
315
288
  */
316
- declare const CATALOG_PUBLISHED_AT = "2026-09-07T12:44:09Z";
289
+ declare const CATALOG_PUBLISHED_AT = "2026-09-15T07:13:18Z";
317
290
 
318
291
  /**
319
292
  * The corpus itself — rules only, and **deliberately free of zod**.
320
293
  *
321
294
  * ── Why this is a separate entry point from `index.ts` ───────────────────────
322
295
  *
323
- * `packages/guard` bundles this catalog into `plugin/scripts/guard-hook.mjs`, the
324
- * file Claude Code executes before EVERY tool call, in a fresh Node process, under
325
- * a 10s ceiling. `built-artifact.test.ts` asserts that bundle contains no
326
- * `ZodError` and no `ZodType`, captioned "the whole point of not importing
327
- * ./context" — because the guard "cannot afford to parse a validator it never
328
- * calls" (`guard/src/core/normalize.ts`).
296
+ * The guard bundles this catalog into its hook script, the file Claude Code
297
+ * executes before EVERY tool call, in a fresh Node process, under a 10s ceiling.
298
+ * That bundle must not carry a validator it never calls.
329
299
  *
330
300
  * The package ROOT cannot satisfy that: `index.ts` re-exports `./schema.js`, which
331
301
  * value-imports zod. So the guard imports `@agenttrail/guardrails/guardrails` — this
@@ -334,19 +304,15 @@ declare const CATALOG_PUBLISHED_AT = "2026-09-07T12:44:09Z";
334
304
  * reference to `./schema.js`, which esbuild erases. Everyone else keeps using the
335
305
  * root and gets the schema with it.
336
306
  *
337
- * Measured on this machine, best-of-N cold `node`: an empty process is 20ms; a cold
338
- * import of the package root, validating 56 rules through `defineRule`, is 40ms.
339
- * The subpath gives that 20ms back on every tool call, forever.
307
+ * A cold import of the package root, validating every rule through `defineRule`,
308
+ * takes about twice as long as an empty Node process. The subpath avoids that on
309
+ * every tool call.
340
310
  *
341
- * ── Where validation went ───────────────────────────────────────────────────
311
+ * ── Where validation runs ───────────────────────────────────────────────────
342
312
  *
343
- * This package used to validate at module load via `defineRule()`, on the stated
344
- * reasoning that "an authoring mistake fails at import — which is to say, at TEST
345
- * TIME."
346
- * A test IS test time: `__tests__/corpus.test.ts` runs `parseRule()` over all 56
347
- * and fails on any invalid one, and proves that sweep bites with a deliberately
348
- * malformed rule. The guarantee is unchanged; only the mechanism moved off the
349
- * hook's hot path. `defineRule` stays exported for contributors authoring locally.
313
+ * At test time: `__tests__/corpus.test.ts` runs `parseRule()` over every rule and
314
+ * fails on any invalid one, and proves that sweep bites with a deliberately
315
+ * malformed rule. `defineRule` is exported for contributors authoring locally.
350
316
  *
351
317
  * So every rule file exports a plain object literal typed `satisfies Rule` with a
352
318
  * TYPE-only schema import. `corpus.test.ts` is what stops one from drifting.
@@ -3,28 +3,23 @@ export { Action, FixtureInput, Fixtures, Match, RuleInput, Severity } from './sc
3
3
  import 'zod';
4
4
 
5
5
  /**
6
- * The eight packs, and the order they are presented in.
6
+ * The eleven packs, and the order they are presented in.
7
7
  *
8
8
  * These ids are authoritative and shared with the rules authored against them,
9
- * and with the guard's `config.json` `enabledPacks` (spec 11). An earlier design
10
- * named ten differently-titled packs; the two could not both have been right.
11
- * Renaming one of these is a breaking change to a user's config file, not a
12
- * tidy-up — `safety-bypass` was `guardrail-bypass` until the rename, and that move
13
- * was only free because nothing was published yet.
14
- *
15
- * Working-tree destruction leads deliberately. Destructive git commands are the
16
- * most frequently documented real-world coding-agent failure, with filed
17
- * incidents across Claude Code, Gemini CLI and Codex — while our own real-trace
18
- * mining found production destructive-SQL at zero.
19
- *
20
- * The per-pack counts in spec 7.1 are a budget, not a quota: trim or extend at
21
- * authoring time. They are deliberately NOT encoded here, because a rule count
22
- * asserted in code becomes a reason to write a weak rule to hit a number.
9
+ * and with the guard's `config.json` `enabledPacks`. Renaming one of these is a
10
+ * breaking change to a user's config file.
11
+ *
12
+ * Working-tree destruction leads: destructive git commands are the most frequently
13
+ * documented coding-agent failure, with public incident reports across Claude
14
+ * Code, Gemini CLI and Codex.
15
+ *
16
+ * Per-pack rule counts are deliberately NOT encoded here: a rule count asserted in
17
+ * code becomes a reason to write a weak rule to hit a number.
23
18
  */
24
- declare const PACKS: readonly ["working-tree", "destructive-data", "prod-infra", "secret-exposure", "rce-supply-chain", "safety-bypass", "privilege-supply-chain", "file-scope"];
25
- /** One of the eight pack ids. A rule's `category` is always one of these. */
19
+ declare const PACKS: readonly ["working-tree", "destructive-data", "prod-infra", "secret-exposure", "rce-supply-chain", "safety-bypass", "privilege-supply-chain", "file-scope", "agent-context", "test-integrity", "exfiltration"];
20
+ /** One of the eleven pack ids. A rule's `category` is always one of these. */
26
21
  type Pack = (typeof PACKS)[number];
27
- /** Is this string one of the eight pack ids? */
22
+ /** Is this string one of the eleven pack ids? */
28
23
  declare function isPack(value: string): value is Pack;
29
24
 
30
25
  /**
@@ -42,17 +37,9 @@ declare function isPack(value: string): value is Pack;
42
37
  * echo "never run rm -rf /" -> DENY dd.rm-rf-absolute
43
38
  * curl --data "we ran rm -rf /tmp/x" https://… -> DENY dd.rm-rf-absolute
44
39
  *
45
- * A person documenting this tool is blocked by this tool. Measured on the
46
- * shipped bundle, **48 of the 56 rules had it — every command-channel rule.**
47
- * The eight that did not are the `file_glob` rules, which never see a command.
48
- *
49
- * It was NOT found by the 438 fixtures or the 307-entry quiet corpus, and could
50
- * not have been: every entry in both is a command somebody wrote on purpose. It
51
- * took 997 real transcripts, where an automated `curl` posting an issue-tracker comment
52
- * whose BODY discussed `rm -rf` was labelled "rm -rf against an absolute path".
53
- * That is why the quoted-mention family is now generated as a cross-product over
54
- * the whole corpus (`guardrails-quoted-mention.test.ts`) rather than listed by
55
- * hand: a hand-written list only covers the rules somebody remembered.
40
+ * Without an exemption, a person documenting this tool is blocked by this tool.
41
+ * That applies to every command-channel rule — 62 of the 74. The twelve `file_glob`
42
+ * rules never see a command.
56
43
  *
57
44
  * ── Why exemption by CARRIER VERB, and not by "the payload is quoted" ────────
58
45
  *
@@ -73,17 +60,14 @@ declare function isPack(value: string): value is Pack;
73
60
  * echo "rm -rf /" | bash -> NOT exempt: `|` is outside the quotes
74
61
  * echo "$(rm -rf /var)" -> NOT exempt: the shell expands `$( )`
75
62
  *
76
- * This is stricter than the `block-destructive-sql` treatment it generalises.
77
- * That rule — the ONLY one that had this exclusion, which is the defect — excluded
78
- * any command whose text NAMED a search tool, anywhere, and so left a documented
79
- * hole on compound commands: `grep x . && rm -rf /` was exempt. Anchoring closes
80
- * it. Its loose condition is KEPT alongside the anchored one rather than replaced:
81
- * the loose form still covers a search that is not the FIRST word — measured,
82
- * `cat runbook.md | grep -n TRUNCATE` is exempt only because of it — and a fix is
83
- * not allowed to regress what the rule already allowed. The other four rules with a pre-existing `none_of`
63
+ * `block-destructive-sql` also keeps a looser condition of its own: it exempts any
64
+ * command whose text NAMES a search or history tool, anywhere. That covers a search
65
+ * that is not the FIRST word, such as `cat runbook.md | grep -n TRUNCATE`, and it is
66
+ * why that rule misses a compound command that both searches and executes, as its
67
+ * description states. The other four rules with their own `none_of`
84
68
  * (`se.env-print`, `require-approval-rm-rf`, `wt.clean-fdx`, `wt.restore-path`)
85
69
  * exclude something else entirely — a dry run, a build directory, `--staged` —
86
- * and are likewise appended to, never replaced.
70
+ * and have these conditions appended, not replaced.
87
71
  *
88
72
  * ── The character classes, and why each character is in them ─────────────────
89
73
  *
@@ -204,8 +188,8 @@ declare const QUOTED_MENTION: readonly MatchCondition[];
204
188
  * harness reads `"command" in fixture`.
205
189
  *
206
190
  * Writing every fixture out longhand would work and would be unreadable, so
207
- * these exist instead. They are also more honest than the shorthand was: the
208
- * CHANNEL is now visible at every single fixture, and the channel is the thing
191
+ * these exist instead. They also make the CHANNEL visible at every single
192
+ * fixture, and the channel is the thing
209
193
  * that silently makes a fixture vacuous — a `file_glob` rule handed a command
210
194
  * fixture matches nothing, and an `allow` fixture that matches nothing passes.
211
195
  */
@@ -254,35 +238,31 @@ declare function mentions(text: string): Fixture[];
254
238
  * The catalog's own version and publication date.
255
239
  *
256
240
  * These are the two facts the offline guard needs in order to tell someone how old
257
- * their protection is. The guard has no update check and no network path for rules
258
- * (spec 8.4), so a user's rules are frozen on the day they installed and nothing
259
- * else in the system can tell them that.
241
+ * their protection is. The guard has no update check and no network path for rules,
242
+ * so a user's rules are frozen on the day they installed and nothing else can tell
243
+ * them that.
260
244
  *
261
245
  * ── Why the stamp lives HERE and not in `@agenttrail/guard` ──────────────────
262
- * The guard BUNDLES this package at build time (spec 11.1.1), so it is *this*
263
- * package's version that determines which rules a user is actually running. The
264
- * guard's own version answers a different question and would be the wrong number:
265
- * `guard@0.4.1` may ship an unchanged catalog, and two different guard versions may
266
- * carry the same rules. Spec 11.1.1 states it outright — "that is exactly why C6
267
- * has `status` print the bundled catalog version rather than the guard's."
246
+ * The guard BUNDLES this package at build time, so it is *this* package's version
247
+ * that determines which rules a user is actually running. The guard's own version
248
+ * answers a different question and would be the wrong number: `guard@0.4.1` may ship
249
+ * an unchanged catalog, and two different guard versions may carry the same rules.
268
250
  *
269
251
  * ── This module imports nothing, and that is load-bearing ───────────────────
270
- * It is exported from `rules.ts`, the zod-free entry `packages/guard` bundles into the
252
+ * It is exported from `rules.ts`, the zod-free entry the guard bundles into the
271
253
  * file Claude Code runs before every tool call. Two string constants add nothing to
272
254
  * that bundle; an import of `schema.ts` here would add zod to it. Add no imports here.
273
255
  *
274
256
  * ── Both constants are hand-written, and that is deliberate ──────────────────
275
- * A build-time `define` would reach `dist/` only. In this monorepo the guard resolves
276
- * `@agenttrail/guardrails` to `src/index.ts`, so a defined value would exist in the
277
- * published tarball and be absent everywhere a test can see it — the shipped value
278
- * would be the one value never exercised. `guard/src/core/version.ts` sets the same
279
- * precedent, and `__tests__/stamp.test.ts` pins `CATALOG_VERSION` to `package.json`
280
- * so the two cannot drift in silence.
257
+ * A build-time `define` would reach `dist/` only, so the value would exist in the
258
+ * published tarball and be absent from the source every test imports — the shipped
259
+ * value would be the one value never exercised. `__tests__/stamp.test.ts` pins
260
+ * `CATALOG_VERSION` to `package.json` so the two cannot drift in silence.
281
261
  *
282
262
  * ── AT RELEASE, BUMP BOTH CONSTANTS BELOW, TOGETHER ──────────────────────────
283
263
  * Bumping `package.json`'s version alone turns `stamp.test.ts` red, and that is the
284
264
  * point: it is what brings whoever cut the release into this file, where the date is
285
- * a few lines away. Publish order is `guardrails` first, then `guard` (spec 11.1.1);
265
+ * a few lines away. Publish order is `guardrails` first, then `guard`;
286
266
  * a guard release carries whatever stamp was here when it was built.
287
267
  *
288
268
  * Forgetting to advance the date understates how fresh the rules are, which tells a
@@ -296,36 +276,26 @@ declare function mentions(text: string): Fixture[];
296
276
  * Pinned to `package.json`'s `version` by `__tests__/stamp.test.ts` — this is a copy
297
277
  * of that value, not an independent one, and the test is what makes the copy safe.
298
278
  */
299
- declare const CATALOG_VERSION = "0.0.1";
279
+ declare const CATALOG_VERSION = "0.1.0";
300
280
  /**
301
- * When this catalog version was cut, as an ISO-8601 instant.
302
- *
303
- * For a version published to npm this is its publish date. For a pre-release cut from
304
- * the monorepo it is the commit that brought the corpus to this state — here,
305
- * `252c2e2c` ("feat(guardrails): rule schema, pack registry and fixture harness"),
306
- * which created the package at `0.0.1`. Both readings answer the user's actual
307
- * question, which is not "when did this reach a registry" but "how old are the rules
308
- * I am running".
309
- *
310
- * It is a real, sourced instant rather than a plausible-looking default. If a future
311
- * release cannot source one, leave it absent rather than approximate: the renderer in
312
- * `guard/src/core/catalog-stamp.ts` degrades honestly, and a wrong date that looks
313
- * right is worse than a missing one in a tool whose whole proposition is candour
314
- * about its own coverage.
281
+ * When the rules in this catalog reached their current state, as an ISO-8601
282
+ * instant. The question it answers is not "when did this reach a registry" but "how
283
+ * old are the rules I am running".
284
+ *
285
+ * If a release cannot source a real date, leave it absent rather than approximate:
286
+ * the guard handles a missing date, and a wrong date that looks right is worse than
287
+ * a missing one.
315
288
  */
316
- declare const CATALOG_PUBLISHED_AT = "2026-09-07T12:44:09Z";
289
+ declare const CATALOG_PUBLISHED_AT = "2026-09-15T07:13:18Z";
317
290
 
318
291
  /**
319
292
  * The corpus itself — rules only, and **deliberately free of zod**.
320
293
  *
321
294
  * ── Why this is a separate entry point from `index.ts` ───────────────────────
322
295
  *
323
- * `packages/guard` bundles this catalog into `plugin/scripts/guard-hook.mjs`, the
324
- * file Claude Code executes before EVERY tool call, in a fresh Node process, under
325
- * a 10s ceiling. `built-artifact.test.ts` asserts that bundle contains no
326
- * `ZodError` and no `ZodType`, captioned "the whole point of not importing
327
- * ./context" — because the guard "cannot afford to parse a validator it never
328
- * calls" (`guard/src/core/normalize.ts`).
296
+ * The guard bundles this catalog into its hook script, the file Claude Code
297
+ * executes before EVERY tool call, in a fresh Node process, under a 10s ceiling.
298
+ * That bundle must not carry a validator it never calls.
329
299
  *
330
300
  * The package ROOT cannot satisfy that: `index.ts` re-exports `./schema.js`, which
331
301
  * value-imports zod. So the guard imports `@agenttrail/guardrails/guardrails` — this
@@ -334,19 +304,15 @@ declare const CATALOG_PUBLISHED_AT = "2026-09-07T12:44:09Z";
334
304
  * reference to `./schema.js`, which esbuild erases. Everyone else keeps using the
335
305
  * root and gets the schema with it.
336
306
  *
337
- * Measured on this machine, best-of-N cold `node`: an empty process is 20ms; a cold
338
- * import of the package root, validating 56 rules through `defineRule`, is 40ms.
339
- * The subpath gives that 20ms back on every tool call, forever.
307
+ * A cold import of the package root, validating every rule through `defineRule`,
308
+ * takes about twice as long as an empty Node process. The subpath avoids that on
309
+ * every tool call.
340
310
  *
341
- * ── Where validation went ───────────────────────────────────────────────────
311
+ * ── Where validation runs ───────────────────────────────────────────────────
342
312
  *
343
- * This package used to validate at module load via `defineRule()`, on the stated
344
- * reasoning that "an authoring mistake fails at import — which is to say, at TEST
345
- * TIME."
346
- * A test IS test time: `__tests__/corpus.test.ts` runs `parseRule()` over all 56
347
- * and fails on any invalid one, and proves that sweep bites with a deliberately
348
- * malformed rule. The guarantee is unchanged; only the mechanism moved off the
349
- * hook's hot path. `defineRule` stays exported for contributors authoring locally.
313
+ * At test time: `__tests__/corpus.test.ts` runs `parseRule()` over every rule and
314
+ * fails on any invalid one, and proves that sweep bites with a deliberately
315
+ * malformed rule. `defineRule` is exported for contributors authoring locally.
350
316
  *
351
317
  * So every rule file exports a plain object literal typed `satisfies Rule` with a
352
318
  * TYPE-only schema import. `corpus.test.ts` is what stops one from drifting.
@@ -20,7 +20,7 @@ import {
20
20
  mentions,
21
21
  pwsh,
22
22
  rulesForPack
23
- } from "./chunk-DKLS2UEL.js";
23
+ } from "./chunk-DBB7HO4T.js";
24
24
  export {
25
25
  CATALOG_PUBLISHED_AT,
26
26
  CATALOG_VERSION,