@webpieces/ai-hook-rules 0.4.594 → 0.4.595

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.
@@ -0,0 +1,141 @@
1
+ import type { TreeKind } from './effective-tree';
2
+ /**
3
+ * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.
4
+ *
5
+ * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the
6
+ * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.
7
+ */
8
+ export type L1Kind = 'f' | 'o' | 'p' | 'w';
9
+ /** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */
10
+ export type L1KindMatch = 'f' | 'o' | 'w' | 'pw' | '-';
11
+ /** R and G are yes/no, written `y`/`n` in the doc, with `-` for "does not matter". */
12
+ export type L1Flag = 'y' | 'n' | '-';
13
+ /** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */
14
+ export type L1Agent = 'c' | 's' | '-';
15
+ /** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */
16
+ export type L1ActionKind = 'exempt' | 'down' | 'block';
17
+ /**
18
+ * WHICH structural block a blocking row dispatches to. This is the field that makes the array
19
+ * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,
20
+ * so deleting a row from the array removes the block.
21
+ */
22
+ export type L1BlockId = 'coordinator-in-worktree' | 'force-to-root';
23
+ /**
24
+ * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
25
+ *
26
+ * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), A (coordinator or
27
+ * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).
28
+ */
29
+ export declare class L1Classification {
30
+ readonly kind: L1Kind;
31
+ readonly coordinator: boolean;
32
+ readonly readOnly: boolean;
33
+ readonly git: boolean;
34
+ readonly atRoot: boolean;
35
+ constructor(kind: L1Kind, coordinator: boolean, readOnly: boolean, git: boolean, atRoot: boolean);
36
+ /**
37
+ * The classification the RUNNER enforces on, built from the resolved tree and the caller.
38
+ *
39
+ * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by
40
+ * effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is
41
+ * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what
42
+ * SHOULD happen and is deliberately unreachable from here until the "Not done" fix in
43
+ * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,
44
+ * so the two ship together or neither does. Mapping it to `p` here is what preserves today's
45
+ * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.
46
+ */
47
+ static forEnforcement(treeKind: TreeKind, coordinator: boolean, readOnly: boolean, git: boolean, atRoot: boolean): L1Classification;
48
+ }
49
+ /** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */
50
+ export declare class L1Action {
51
+ readonly label: string;
52
+ readonly kind: L1ActionKind;
53
+ constructor(label: string, kind: L1ActionKind);
54
+ }
55
+ export declare const ACT_EXEMPT: L1Action;
56
+ export declare const ACT_DOWN: L1Action;
57
+ export declare const ACT_BLOCK: L1Action;
58
+ /**
59
+ * The CURE a blocking row prescribes.
60
+ *
61
+ * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,
62
+ * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — "spawn a
63
+ * subagent bound to the worktree" — which no allowlist can accept and no reclassification can model,
64
+ * so it declares `runnable: false` and is asserted only on the deny text.
65
+ */
66
+ export declare class L1Cure {
67
+ /** How the doc's `why` column spells it. */
68
+ readonly summary: string;
69
+ /** A substring that MUST appear in the deny text the guard emits for this row. */
70
+ readonly denyMention: string;
71
+ readonly runnable: boolean;
72
+ constructor(
73
+ /** How the doc's `why` column spells it. */
74
+ summary: string,
75
+ /** A substring that MUST appear in the deny text the guard emits for this row. */
76
+ denyMention: string, runnable: boolean);
77
+ }
78
+ /**
79
+ * One row of the "L1 use cases" table: what you SEE, the state it puts you in, the verdict, the fix.
80
+ *
81
+ * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed
82
+ * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement
83
+ * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither
84
+ * of which is a row) can carry `null` there.
85
+ */
86
+ export declare class L1UseCase {
87
+ readonly num: number;
88
+ readonly symptom: string;
89
+ readonly state: string;
90
+ readonly verdict: string;
91
+ readonly fix: string;
92
+ readonly classification: L1Classification | null;
93
+ constructor(num: number, symptom: string, state: string, verdict: string, fix: string, classification?: L1Classification | null);
94
+ }
95
+ /** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */
96
+ export declare class L1Row {
97
+ readonly num: number;
98
+ readonly k: L1KindMatch;
99
+ readonly a: L1Agent;
100
+ readonly r: L1Flag;
101
+ readonly g: L1Flag;
102
+ readonly p: 'root' | 'sub' | '-';
103
+ readonly action: L1Action;
104
+ /** The `why` cell, verbatim. */
105
+ readonly why: string;
106
+ readonly cure: L1Cure | null;
107
+ readonly blockId: L1BlockId | null;
108
+ readonly useCases: readonly L1UseCase[];
109
+ constructor(num: number, k: L1KindMatch, a: L1Agent, r: L1Flag, g: L1Flag, p: 'root' | 'sub' | '-', action: L1Action,
110
+ /** The `why` cell, verbatim. */
111
+ why: string, cure: L1Cure | null, blockId: L1BlockId | null, useCases: readonly L1UseCase[]);
112
+ matches(c: L1Classification): boolean;
113
+ private kindMatches;
114
+ }
115
+ /**
116
+ * THE six L1 rows, in first-match-wins order.
117
+ *
118
+ * Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they
119
+ * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. "L1 has no
120
+ * objection" — which is why only rows 3 and 5 carry a blockId.
121
+ */
122
+ export declare const L1_ROWS: readonly L1Row[];
123
+ /**
124
+ * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0
125
+ * allowlist that runs ahead of L1 (15).
126
+ *
127
+ * They are use cases of L1 all the same — "exempt" is what emerges when the filter empties the rule
128
+ * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the
129
+ * doc's one numbered table. They carry no classification because no row classifies them.
130
+ */
131
+ export declare const L1_UNROWED_USE_CASES: readonly L1UseCase[];
132
+ /** Every use case, in the doc's numbering — the order the table is rendered and read in. */
133
+ export declare function allL1UseCases(): readonly L1UseCase[];
134
+ /**
135
+ * FIRST MATCH WINS — the one lookup the guard and the tests share.
136
+ *
137
+ * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a
138
+ * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly
139
+ * that, which is why this returns L1Row rather than L1Row | null.
140
+ */
141
+ export declare function firstMatchingL1Row(c: L1Classification): L1Row;
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.L1_UNROWED_USE_CASES = exports.L1_ROWS = exports.L1Row = exports.L1UseCase = exports.L1Cure = exports.ACT_BLOCK = exports.ACT_DOWN = exports.ACT_EXEMPT = exports.L1Action = exports.L1Classification = void 0;
4
+ exports.allL1UseCases = allL1UseCases;
5
+ exports.firstMatchingL1Row = firstMatchingL1Row;
6
+ /**
7
+ * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.
8
+ *
9
+ * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), A (coordinator or
10
+ * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).
11
+ */
12
+ class L1Classification {
13
+ kind;
14
+ coordinator;
15
+ readOnly;
16
+ git;
17
+ atRoot;
18
+ // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape
19
+ constructor(kind, coordinator, readOnly, git, atRoot) {
20
+ this.kind = kind;
21
+ this.coordinator = coordinator;
22
+ this.readOnly = readOnly;
23
+ this.git = git;
24
+ this.atRoot = atRoot;
25
+ }
26
+ /**
27
+ * The classification the RUNNER enforces on, built from the resolved tree and the caller.
28
+ *
29
+ * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by
30
+ * effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is
31
+ * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what
32
+ * SHOULD happen and is deliberately unreachable from here until the "Not done" fix in
33
+ * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,
34
+ * so the two ship together or neither does. Mapping it to `p` here is what preserves today's
35
+ * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.
36
+ */
37
+ // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to
38
+ // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject
39
+ static forEnforcement(treeKind, coordinator, readOnly, git, atRoot) {
40
+ const kind = treeKind === 'foreign' ? 'f' : treeKind === 'worktree' ? 'w' : 'p';
41
+ return new L1Classification(kind, coordinator, readOnly, git, atRoot);
42
+ }
43
+ }
44
+ exports.L1Classification = L1Classification;
45
+ /** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */
46
+ class L1Action {
47
+ label;
48
+ kind;
49
+ constructor(label, kind) {
50
+ this.label = label;
51
+ this.kind = kind;
52
+ }
53
+ }
54
+ exports.L1Action = L1Action;
55
+ exports.ACT_EXEMPT = new L1Action('2 exempt', 'exempt');
56
+ exports.ACT_DOWN = new L1Action('→ L2', 'down');
57
+ exports.ACT_BLOCK = new L1Action('4 block', 'block');
58
+ /**
59
+ * The CURE a blocking row prescribes.
60
+ *
61
+ * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,
62
+ * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — "spawn a
63
+ * subagent bound to the worktree" — which no allowlist can accept and no reclassification can model,
64
+ * so it declares `runnable: false` and is asserted only on the deny text.
65
+ */
66
+ class L1Cure {
67
+ summary;
68
+ denyMention;
69
+ runnable;
70
+ constructor(
71
+ /** How the doc's `why` column spells it. */
72
+ summary,
73
+ /** A substring that MUST appear in the deny text the guard emits for this row. */
74
+ denyMention, runnable) {
75
+ this.summary = summary;
76
+ this.denyMention = denyMention;
77
+ this.runnable = runnable;
78
+ }
79
+ }
80
+ exports.L1Cure = L1Cure;
81
+ /**
82
+ * One row of the "L1 use cases" table: what you SEE, the state it puts you in, the verdict, the fix.
83
+ *
84
+ * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed
85
+ * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement
86
+ * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither
87
+ * of which is a row) can carry `null` there.
88
+ */
89
+ class L1UseCase {
90
+ num;
91
+ symptom;
92
+ state;
93
+ verdict;
94
+ fix;
95
+ classification;
96
+ // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them
97
+ constructor(num, symptom, state, verdict, fix, classification = null) {
98
+ this.num = num;
99
+ this.symptom = symptom;
100
+ this.state = state;
101
+ this.verdict = verdict;
102
+ this.fix = fix;
103
+ this.classification = classification;
104
+ }
105
+ }
106
+ exports.L1UseCase = L1UseCase;
107
+ /** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */
108
+ class L1Row {
109
+ num;
110
+ k;
111
+ a;
112
+ r;
113
+ g;
114
+ p;
115
+ action;
116
+ why;
117
+ cure;
118
+ blockId;
119
+ useCases;
120
+ // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases
121
+ constructor(num, k, a, r, g, p, action,
122
+ /** The `why` cell, verbatim. */
123
+ why, cure, blockId, useCases) {
124
+ this.num = num;
125
+ this.k = k;
126
+ this.a = a;
127
+ this.r = r;
128
+ this.g = g;
129
+ this.p = p;
130
+ this.action = action;
131
+ this.why = why;
132
+ this.cure = cure;
133
+ this.blockId = blockId;
134
+ this.useCases = useCases;
135
+ }
136
+ matches(c) {
137
+ if (!this.kindMatches(c.kind))
138
+ return false;
139
+ if (this.a !== '-' && (this.a === 'c') !== c.coordinator)
140
+ return false;
141
+ if (!flagMatches(this.r, c.readOnly))
142
+ return false;
143
+ if (!flagMatches(this.g, c.git))
144
+ return false;
145
+ return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');
146
+ }
147
+ kindMatches(kind) {
148
+ if (this.k === '-')
149
+ return true;
150
+ if (this.k === 'pw')
151
+ return kind === 'p' || kind === 'w';
152
+ return this.k === kind;
153
+ }
154
+ }
155
+ exports.L1Row = L1Row;
156
+ // R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the
157
+ // same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)
158
+ // webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module
159
+ function flagMatches(cell, value) {
160
+ if (cell === '-')
161
+ return true;
162
+ return (cell === 'y') === value;
163
+ }
164
+ /**
165
+ * THE six L1 rows, in first-match-wins order.
166
+ *
167
+ * Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they
168
+ * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. "L1 has no
169
+ * objection" — which is why only rows 3 and 5 carry a blockId.
170
+ */
171
+ exports.L1_ROWS = [
172
+ new L1Row(1, 'f', '-', '-', '-', '-', exports.ACT_EXEMPT, 'different git repo — hands off', null, null, [
173
+ new L1UseCase(1, '`cd repositories/vendored && git commit` goes through untouched', '`f` / `y` / - — row 1', 'ALLOW_EXEMPT', 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off', new L1Classification('f', false, false, true, false)),
174
+ ]),
175
+ new L1Row(2, 'o', '-', '-', '-', '-', exports.ACT_DOWN, 'see "Not done" below', null, null, []),
176
+ new L1Row(3, 'w', 'c', 'n', '-', '-', exports.ACT_BLOCK, 'the coordinator\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree', new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false), 'coordinator-in-worktree', [
177
+ new L1UseCase(12, 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked', '`w` / `c` / `n` — row 3', 'BLOCK', 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it', new L1Classification('w', true, false, false, false)),
178
+ ]),
179
+ new L1Row(4, 'pw', '-', '-', 'n', '-', exports.ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [
180
+ new L1UseCase(5, '`ls` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', '→ L2', 'none — force-to-root has no jurisdiction over non-git commands', new L1Classification('p', false, true, false, false)),
181
+ new L1UseCase(6, '`pnpm test` from `packages/http/` runs normally', '`pw` / `n` / - — row 4', '→ L2', 'none — deliberately untouched, so package-local test runs stay natural', new L1Classification('p', false, false, false, false)),
182
+ new L1UseCase(10, '`echo "cd sub && git push"` passes', '`pw` / `n` / `root` — row 4', '→ L2', 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape', new L1Classification('p', false, false, false, true)),
183
+ new L1UseCase(13, 'the same command from a **subagent** runs normally', '`w` / `s` — row 3 does not match', '→ L2', 'none — a subagent pinned to a worktree is the correct pattern', new L1Classification('w', false, false, false, false)),
184
+ new L1UseCase(14, 'the coordinator\'s `cd <worktree> && ls`/`cat`/`grep` still runs', '`w` / `c` / `y` — row 3 does not match', '→ L2', 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you', new L1Classification('w', true, true, false, false)),
185
+ ]),
186
+ new L1Row(5, 'pw', '-', '-', 'y', 'sub', exports.ACT_BLOCK, '`cd <root> && <original>`', new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true), 'force-to-root', [
187
+ new L1UseCase(7, '`git status` from `packages/http/` is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK', 'Option 1 (preferred): `cd <root> && git status`', new L1Classification('p', false, false, true, false)),
188
+ new L1UseCase(8, '`cd packages/http && git status` **typed from the root** is blocked', '`pw` / `y` / `sub` — row 5', 'BLOCK', 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION', new L1Classification('p', false, false, true, false)),
189
+ new L1UseCase(11, '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one', '`pw` / `y` / `sub` — row 5; force-to-root runs first', 'BLOCK', 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\'s real answer ← costs one extra turn by design; still blocked', new L1Classification('p', false, false, true, false)),
190
+ ]),
191
+ new L1Row(6, 'pw', '-', '-', 'y', 'root', exports.ACT_DOWN, '', null, null, [
192
+ new L1UseCase(9, '`cd <root> && git status` passes from anywhere', '`pw` / `y` / `root` — row 6', '→ L2', 'none — this IS the prescribed cure', new L1Classification('p', false, false, true, true)),
193
+ ]),
194
+ ];
195
+ /**
196
+ * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0
197
+ * allowlist that runs ahead of L1 (15).
198
+ *
199
+ * They are use cases of L1 all the same — "exempt" is what emerges when the filter empties the rule
200
+ * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the
201
+ * doc's one numbered table. They carry no classification because no row classifies them.
202
+ */
203
+ exports.L1_UNROWED_USE_CASES = [
204
+ new L1UseCase(2, 'Edit `repositories/vendored/foo.ts` allowed even on stale main', 'filter — the path is in `excludePaths`', 'ALLOW_EXEMPT', 'none needed'),
205
+ new L1UseCase(3, 'Edit `packages/http/foo.ts` blocked on stale main', 'filter keeps the rules → L2 fires', 'BLOCK (at L2)', 'that is L2\'s write-on-main verdict, not L1\'s — follow the L2 message'),
206
+ new L1UseCase(4, 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`', 'filter, on the TARGET path', '→ L2', 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to "fix" it'),
207
+ new L1UseCase(15, 'the coordinator\'s `cd <worktree> && pnpm install` still runs while row 3 is live', 'L0 allowlist, ahead of L1', 'ALLOW', 'none — a cure must stay reachable from every tree'),
208
+ ];
209
+ /** Every use case, in the doc's numbering — the order the table is rendered and read in. */
210
+ // webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module
211
+ function allL1UseCases() {
212
+ const all = [...exports.L1_ROWS.flatMap((row) => row.useCases), ...exports.L1_UNROWED_USE_CASES];
213
+ return all.sort((a, b) => a.num - b.num);
214
+ }
215
+ /**
216
+ * FIRST MATCH WINS — the one lookup the guard and the tests share.
217
+ *
218
+ * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a
219
+ * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly
220
+ * that, which is why this returns L1Row rather than L1Row | null.
221
+ */
222
+ // webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads
223
+ function firstMatchingL1Row(c) {
224
+ const row = exports.L1_ROWS.find((r) => r.matches(c));
225
+ if (row === undefined)
226
+ throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);
227
+ return row;
228
+ }
229
+ //# sourceMappingURL=l1-rows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l1-rows.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l1-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TreeKind } from './effective-tree';\n\n// ---------------------------------------------------------------------------\n// L1 — the LOCATION layer, as data.\n//\n// L1 answers three questions: do we govern this call at all, is the WRONG AGENT standing here, and is\n// the agent stranded away from the root? Drawn as a decision matrix that is SIX ordered rows over five\n// dimensions (K/A/R/G/P), first match wins.\n//\n// This module holds those rows, and l1-doc.ts renders them into guards/L1-location.md — the doc a human\n// reads on GitHub. A unit test locks that file byte-identical to the renderer, and the guard itself\n// CONSULTS L1_ROWS to decide which structural block fires (see runner.l1LocationBlock). Doc and code\n// come from the SAME array, so they cannot drift.\n//\n// This module is deliberately import-free at runtime (only a type-only import above), so\n// `pnpm guards:generate` can load it without the package's transitive dependencies.\n// ---------------------------------------------------------------------------\n\n/**\n * The K dimension as a CLASSIFICATION carries — one concrete tree, never the `pw` union.\n *\n * `p` and `w` are the same PROJECT and every rule-scoped guard treats them alike; the doc writes the\n * pair as `pw` in the row's MATCHER (below), which is a different vocabulary on purpose.\n */\nexport type L1Kind = 'f' | 'o' | 'p' | 'w';\n\n/** The K value a ROW matches on. `pw` matches both `p` and `w`; `-` is the wildcard. */\nexport type L1KindMatch = 'f' | 'o' | 'w' | 'pw' | '-';\n\n/** R and G are yes/no, written `y`/`n` in the doc, with `-` for \"does not matter\". */\nexport type L1Flag = 'y' | 'n' | '-';\n\n/** A is the same one boolean wearing the doc's own letters: `c` the coordinator, `s` a subagent. */\nexport type L1Agent = 'c' | 's' | '-';\n\n/** What L1 does with a row. The labels are the doc's own action codebook (see GUARD_MATRIX.md). */\nexport type L1ActionKind = 'exempt' | 'down' | 'block';\n\n/**\n * WHICH structural block a blocking row dispatches to. This is the field that makes the array\n * load-bearing rather than decorative: runner.l1LocationBlock looks the row up and switches on it,\n * so deleting a row from the array removes the block.\n */\nexport type L1BlockId = 'coordinator-in-worktree' | 'force-to-root';\n\n/**\n * One point in the five-dimensional space L1 classifies over. Data-only → a class, per CLAUDE.md.\n *\n * The dimensions are exactly the doc's legend: K (tree kind of the resolved target), A (coordinator or\n * subagent), R (provably read-only inspection), G (invokes git/gh), P (root or subdirectory).\n */\nexport class L1Classification {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimensions is the matrix's shape\n constructor(\n readonly kind: L1Kind,\n readonly coordinator: boolean,\n readonly readOnly: boolean,\n readonly git: boolean,\n readonly atRoot: boolean,\n ) {}\n\n /**\n * The classification the RUNNER enforces on, built from the resolved tree and the caller.\n *\n * `'outside'` maps to `p`, and that is not a typo. TreeKind `'outside'` is produced by\n * effective-tree.ts (`gitRoot === null`) and consumed NOWHERE, so a command in no git repo is\n * judged against the governed repo exactly as if it stood in it. Row 2 (`o` → L2) describes what\n * SHOULD happen and is deliberately unreachable from here until the \"Not done\" fix in\n * guards/L1-location.md lands — exempting `o` alone opens a `cd /tmp &&` bypass of every L2 guard,\n * so the two ship together or neither does. Mapping it to `p` here is what preserves today's\n * behaviour (a `git` command from /tmp is still force-to-root blocked); it is not an endorsement.\n */\n // eslint-disable-next-line @typescript-eslint/max-params -- mirrors the constructor it delegates to\n // webpieces-disable no-function-outside-class -- a named constructor for this data class, not a service: it takes the runner's TreeKind and returns the same class, so there is nothing to inject\n static forEnforcement(\n treeKind: TreeKind,\n coordinator: boolean,\n readOnly: boolean,\n git: boolean,\n atRoot: boolean,\n ): L1Classification {\n const kind: L1Kind = treeKind === 'foreign' ? 'f' : treeKind === 'worktree' ? 'w' : 'p';\n return new L1Classification(kind, coordinator, readOnly, git, atRoot);\n }\n}\n\n/** The `act` cell of a row: the doc's literal label, plus the machine-readable kind behind it. */\nexport class L1Action {\n constructor(readonly label: string, readonly kind: L1ActionKind) {}\n}\n\nexport const ACT_EXEMPT = new L1Action('2 exempt', 'exempt');\nexport const ACT_DOWN = new L1Action('→ L2', 'down');\nexport const ACT_BLOCK = new L1Action('4 block', 'block');\n\n/**\n * The CURE a blocking row prescribes.\n *\n * `runnable` is the axis that matters to the tests: a cure that is a command must, once applied,\n * actually stop the row from matching (cure reachability). Row 3's cure is an INSTRUCTION — \"spawn a\n * subagent bound to the worktree\" — which no allowlist can accept and no reclassification can model,\n * so it declares `runnable: false` and is asserted only on the deny text.\n */\nexport class L1Cure {\n constructor(\n /** How the doc's `why` column spells it. */\n readonly summary: string,\n /** A substring that MUST appear in the deny text the guard emits for this row. */\n readonly denyMention: string,\n readonly runnable: boolean,\n ) {}\n}\n\n/**\n * One row of the \"L1 use cases\" table: what you SEE, the state it puts you in, the verdict, the fix.\n *\n * The four text fields are rendered VERBATIM into the doc. `classification` is the same case expressed\n * in the matrix's own vocabulary so the tests can run it through the matcher — it is test/enforcement\n * data, never rendered, which is why a use case that exercises the FILTER or the L0 allowlist (neither\n * of which is a row) can carry `null` there.\n */\nexport class L1UseCase {\n // eslint-disable-next-line @typescript-eslint/max-params -- four verbatim doc cells plus the classification behind them\n constructor(\n readonly num: number,\n readonly symptom: string,\n readonly state: string,\n readonly verdict: string,\n readonly fix: string,\n readonly classification: L1Classification | null = null,\n ) {}\n}\n\n/** One row of L1's decision table. Data-only → a class, per CLAUDE.md. */\nexport class L1Row {\n // eslint-disable-next-line @typescript-eslint/max-params -- five dimension cells plus act/why/cure/blockId/useCases\n constructor(\n readonly num: number,\n readonly k: L1KindMatch,\n readonly a: L1Agent,\n readonly r: L1Flag,\n readonly g: L1Flag,\n readonly p: 'root' | 'sub' | '-',\n readonly action: L1Action,\n /** The `why` cell, verbatim. */\n readonly why: string,\n readonly cure: L1Cure | null,\n readonly blockId: L1BlockId | null,\n readonly useCases: readonly L1UseCase[],\n ) {}\n\n matches(c: L1Classification): boolean {\n if (!this.kindMatches(c.kind)) return false;\n if (this.a !== '-' && (this.a === 'c') !== c.coordinator) return false;\n if (!flagMatches(this.r, c.readOnly)) return false;\n if (!flagMatches(this.g, c.git)) return false;\n return this.p === '-' || this.p === (c.atRoot ? 'root' : 'sub');\n }\n\n private kindMatches(kind: L1Kind): boolean {\n if (this.k === '-') return true;\n if (this.k === 'pw') return kind === 'p' || kind === 'w';\n return this.k === kind;\n }\n}\n\n// R and G are one boolean each behind a `y`/`n`/`-` cell, so one helper answers for both. (A is the\n// same shape but spelled `c`/`s`, and is matched inline above so the row literals read like the doc.)\n// webpieces-disable no-function-outside-class -- pure predicate for L1Row.matches above, in this data module\nfunction flagMatches(cell: L1Flag, value: boolean): boolean {\n if (cell === '-') return true;\n return (cell === 'y') === value;\n}\n\n/**\n * THE six L1 rows, in first-match-wins order.\n *\n * Rows 3 and 5 are the two structural blocks and they run as ONE step (runner.l1LocationBlock) so they\n * can never be reordered by accident. Every other row is a hand-down or an exemption, i.e. \"L1 has no\n * objection\" — which is why only rows 3 and 5 carry a blockId.\n */\nexport const L1_ROWS: readonly L1Row[] = [\n new L1Row(1, 'f', '-', '-', '-', '-', ACT_EXEMPT, 'different git repo — hands off', null, null, [\n new L1UseCase(1,\n '`cd repositories/vendored && git commit` goes through untouched',\n '`f` / `y` / - — row 1',\n 'ALLOW_EXEMPT',\n 'none needed — jurisdiction is judged on the RESOLVED target, after the `cd`; a different git repo is hands-off',\n new L1Classification('f', false, false, true, false)),\n ]),\n new L1Row(2, 'o', '-', '-', '-', '-', ACT_DOWN, 'see \"Not done\" below', null, null, []),\n new L1Row(3, 'w', 'c', 'n', '-', '-', ACT_BLOCK,\n 'the coordinator\\'s guards do not follow its `cd` — delegate to a subagent bound to the worktree',\n new L1Cure('delegate to a subagent bound to the worktree', 'Spawn a subagent bound to that worktree', false),\n 'coordinator-in-worktree', [\n new L1UseCase(12,\n 'you are the **coordinator**, you ran `git worktree add ../wt`, and `cd ../wt && pnpm build` is blocked',\n '`w` / `c` / `n` — row 3',\n 'BLOCK',\n 'Option 1 (preferred): spawn a subagent bound to `<worktree>` — the Agent tool with worktree isolation, or have the subagent call `EnterWorktree` with `path: <worktree>` (it accepts a worktree you already created)<br>Do NOT: re-type the command, or conclude the harness ate your `cd` — it did not; your GUARDS did not follow it',\n new L1Classification('w', true, false, false, false)),\n ]),\n new L1Row(4, 'pw', '-', '-', 'n', '-', ACT_DOWN, 'force-to-root has no jurisdiction', null, null, [\n new L1UseCase(5,\n '`ls` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n '→ L2',\n 'none — force-to-root has no jurisdiction over non-git commands',\n new L1Classification('p', false, true, false, false)),\n new L1UseCase(6,\n '`pnpm test` from `packages/http/` runs normally',\n '`pw` / `n` / - — row 4',\n '→ L2',\n 'none — deliberately untouched, so package-local test runs stay natural',\n new L1Classification('p', false, false, false, false)),\n new L1UseCase(10,\n '`echo \"cd sub && git push\"` passes',\n '`pw` / `n` / `root` — row 4',\n '→ L2',\n 'none — the `cd` is inside quotes, so `ShellSegmentScan` never treats it as a scope escape',\n new L1Classification('p', false, false, false, true)),\n new L1UseCase(13,\n 'the same command from a **subagent** runs normally',\n '`w` / `s` — row 3 does not match',\n '→ L2',\n 'none — a subagent pinned to a worktree is the correct pattern',\n new L1Classification('w', false, false, false, false)),\n new L1UseCase(14,\n 'the coordinator\\'s `cd <worktree> && ls`/`cat`/`grep` still runs',\n '`w` / `c` / `y` — row 3 does not match',\n '→ L2',\n 'none — inspection is always open; so are the `Read` tool, `git -C <worktree> …` and `git show <branch>:<file>`, none of which move you',\n new L1Classification('w', true, true, false, false)),\n ]),\n new L1Row(5, 'pw', '-', '-', 'y', 'sub', ACT_BLOCK, '`cd <root> && <original>`',\n new L1Cure('`cd <root> && <original>`', 'Run git/gh commands from the repo root', true),\n 'force-to-root', [\n new L1UseCase(7,\n '`git status` from `packages/http/` is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK',\n 'Option 1 (preferred): `cd <root> && git status`',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(8,\n '`cd packages/http && git status` **typed from the root** is blocked',\n '`pw` / `y` / `sub` — row 5',\n 'BLOCK',\n 'Option 1 (preferred): `cd <root> && git status`<br>Do NOT: assume it is allowed because you started at the root — the predicate is `effectiveCwd === root`, i.e. the DESTINATION',\n new L1Classification('p', false, false, true, false)),\n new L1UseCase(11,\n '`cd <subdir> && git push` blocked with the force-to-root message, NOT the gated-flow one',\n '`pw` / `y` / `sub` — row 5; force-to-root runs first',\n 'BLOCK',\n 'Option 1 (preferred): `cd <root> && git push`, which then gets the push guard\\'s real answer ← costs one extra turn by design; still blocked',\n new L1Classification('p', false, false, true, false)),\n ]),\n new L1Row(6, 'pw', '-', '-', 'y', 'root', ACT_DOWN, '', null, null, [\n new L1UseCase(9,\n '`cd <root> && git status` passes from anywhere',\n '`pw` / `y` / `root` — row 6',\n '→ L2',\n 'none — this IS the prescribed cure',\n new L1Classification('p', false, false, true, true)),\n ]),\n];\n\n/**\n * The use cases that exercise something that is NOT a row: the excludePaths FILTER (2, 3, 4) and the L0\n * allowlist that runs ahead of L1 (15).\n *\n * They are use cases of L1 all the same — \"exempt\" is what emerges when the filter empties the rule\n * list, and case 15 is the invariant that a cure stays reachable from every tree — so they stay in the\n * doc's one numbered table. They carry no classification because no row classifies them.\n */\nexport const L1_UNROWED_USE_CASES: readonly L1UseCase[] = [\n new L1UseCase(2,\n 'Edit `repositories/vendored/foo.ts` allowed even on stale main',\n 'filter — the path is in `excludePaths`',\n 'ALLOW_EXEMPT',\n 'none needed'),\n new L1UseCase(3,\n 'Edit `packages/http/foo.ts` blocked on stale main',\n 'filter keeps the rules → L2 fires',\n 'BLOCK (at L2)',\n 'that is L2\\'s write-on-main verdict, not L1\\'s — follow the L2 message'),\n new L1UseCase(4,\n 'Edit `packages/http/foo.ts` judged even though the shell is in `/tmp`',\n 'filter, on the TARGET path',\n '→ L2',\n 'none — for file tools the cwd is irrelevant; do NOT `cd` anywhere to \"fix\" it'),\n new L1UseCase(15,\n 'the coordinator\\'s `cd <worktree> && pnpm install` still runs while row 3 is live',\n 'L0 allowlist, ahead of L1',\n 'ALLOW',\n 'none — a cure must stay reachable from every tree'),\n];\n\n/** Every use case, in the doc's numbering — the order the table is rendered and read in. */\n// webpieces-disable no-function-outside-class -- pure accessor over the two arrays above, beside them in this data module\nexport function allL1UseCases(): readonly L1UseCase[] {\n const all = [...L1_ROWS.flatMap((row: L1Row): readonly L1UseCase[] => row.useCases), ...L1_UNROWED_USE_CASES];\n return all.sort((a: L1UseCase, b: L1UseCase): number => a.num - b.num);\n}\n\n/**\n * FIRST MATCH WINS — the one lookup the guard and the tests share.\n *\n * Never null: rows 1, 2 and 4/5/6 between them cover every kind, and rows 4/5/6 partition G × P, so a\n * classification that matched nothing would be a hole in the matrix. The totality test asserts exactly\n * that, which is why this returns L1Row rather than L1Row | null.\n */\n// webpieces-disable no-function-outside-class -- the matcher over L1_ROWS, beside the array it reads\nexport function firstMatchingL1Row(c: L1Classification): L1Row {\n const row = L1_ROWS.find((r: L1Row): boolean => r.matches(c));\n if (row === undefined) throw new Error(`L1 matrix has a hole: no row matches ${JSON.stringify(c)}`);\n return row;\n}\n"]}
@@ -43,7 +43,11 @@ function logRejection(toolKind, input, result, cwd) {
43
43
  fs.writeFileSync(path.join(dayDir, detailFileName), detail);
44
44
  const logPath = path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE));
45
45
  rotateLogFile(logPath, path.join(logsDir, log_stream_1.logStream.fileName(LOG_FILE_PREV)));
46
- const logLine = `[${timestamp}]\t${toolKind}\t${relativePath}\t[${ruleNames.join(',')}]\t${detailRelPath}\n`;
46
+ // APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm
47
+ // previously landed here as a couple of lines attributed to whatever rule the report happened
48
+ // to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by
49
+ // the filename, which logStream prefixes with session/agent/hook — not by a column here.)
50
+ const logLine = `[${timestamp}]\t${toolKind}\t${relativePath}\t[${ruleNames.join(',')}]\t${detailRelPath}\tfault=${result.fault}\n`;
47
51
  fs.appendFileSync(logPath, logLine);
48
52
  rotateOldDays(hooksDir, MAX_AGE_DAYS);
49
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rejection-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/rejection-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, RepoRootFinder, HOOKS_STATE_DIR } from '@webpieces/rules-config';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\nimport { logStream } from './log-stream';\n\n// The rejection log SPLITS across the two state dirs on purpose: the `.log` index goes to `logs/`\n// with every other webpieces log, while the dated `hooks/<YYYY-MM-DD>/writeInfo-*.md` DETAIL files\n// are not logs and stay in `hooks/` (see LOGS_STATE_DIR).\nconst LOG_FILE = 'hook-rejection.log';\nconst LOG_FILE_PREV = 'hook-rejection.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded\nconst MAX_AGE_DAYS = 7;\n\nconst RULE_NAME_RE = /^\\[([^\\]]+)\\] \\(/gm;\n\nexport function logRejection(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n result: BlockedResult,\n cwd: string,\n): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while\n // the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const now = new Date();\n const timestamp = now.toISOString();\n const epochMs = String(now.getTime());\n const dateStr = timestamp.slice(0, 10);\n\n // LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one\n // writer, so its appends and its daily detail files cannot collide with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_STATE_DIR);\n const logsDir = dotWebpieces.logs(root);\n const dayDir = path.join(hooksDir, dateStr);\n fs.mkdirSync(dayDir, { recursive: true });\n fs.mkdirSync(logsDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `writeInfo-${epochMs}.md`;\n // Relative to the STATE DIR, not to the log file's own directory: the index now lives in\n // `logs/` while the detail lives in `hooks/<date>/`, so a bare `<date>/<file>` would no\n // longer resolve from where the reader found the line.\n const detailRelPath = `${HOOKS_STATE_DIR}/${dateStr}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(dayDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(LOG_FILE_PREV)));\n\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n rotateOldDays(hooksDir, MAX_AGE_DAYS);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction computeRelativePath(filePath: string, cwd: string): string {\n if (filePath.startsWith(cwd)) {\n const rel = filePath.slice(cwd.length);\n if (rel.startsWith('/')) return rel.slice(1);\n return rel;\n }\n return filePath;\n}\n\n/**\n * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because\n * two audit streams need the same answer from the same regex: this file's rejection index, and the\n * `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be\n * two answers to one question.\n */\n// webpieces-disable no-function-outside-class -- pure regex scraper beside this module's other module-scope helpers; exported so the invocation log and the rejection index scrape rule names with the SAME code.\nexport function extractRuleNames(report: string): string[] {\n const names: string[] = [];\n let match = RULE_NAME_RE.exec(report);\n while (match !== null) {\n names.push(match[1]);\n match = RULE_NAME_RE.exec(report);\n }\n RULE_NAME_RE.lastIndex = 0;\n return names;\n}\n\nfunction buildDetailContent(\n timestamp: string,\n toolKind: ToolKind,\n relativePath: string,\n ruleNames: string[],\n report: string,\n input: NormalizedToolInput,\n): string {\n const lines: string[] = [];\n lines.push('# Hook Rejection Detail');\n lines.push('');\n lines.push(`- **Timestamp:** ${timestamp}`);\n lines.push(`- **Tool:** ${toolKind}`);\n lines.push(`- **File:** ${relativePath}`);\n lines.push(`- **Rules violated:** ${ruleNames.join(', ')}`);\n lines.push('');\n lines.push('## Report');\n lines.push('');\n lines.push('```');\n lines.push(report.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('## Content Being Written');\n lines.push('');\n\n if (toolKind === 'Write') {\n const content = input.edits.length > 0 ? input.edits[0].newString : '';\n lines.push('```typescript');\n lines.push(content.trimEnd());\n lines.push('```');\n } else {\n for (let i = 0; i < input.edits.length; i += 1) {\n const edit = input.edits[i];\n lines.push(`### Edit ${String(i + 1)} of ${String(input.edits.length)}`);\n lines.push('');\n lines.push('**old_string:**');\n lines.push('```typescript');\n lines.push(edit.oldString.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('**new_string:**');\n lines.push('```typescript');\n lines.push(edit.newString.trimEnd());\n lines.push('```');\n lines.push('');\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction rotateOldDays(hooksDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(hooksDir);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return;\n }\n\n for (const entry of entries) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(entry)) continue;\n const dirDate = new Date(entry + 'T00:00:00Z');\n if (isNaN(dirDate.getTime())) continue;\n if (dirDate.getTime() < cutoff) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(path.join(hooksDir, entry), { recursive: true, force: true });\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"rejection-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/rejection-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, RepoRootFinder, HOOKS_STATE_DIR } from '@webpieces/rules-config';\n\nimport type { ToolKind, NormalizedToolInput, BlockedResult } from './types';\nimport { logStream } from './log-stream';\n\n// The rejection log SPLITS across the two state dirs on purpose: the `.log` index goes to `logs/`\n// with every other webpieces log, while the dated `hooks/<YYYY-MM-DD>/writeInfo-*.md` DETAIL files\n// are not logs and stay in `hooks/` (see LOGS_STATE_DIR).\nconst LOG_FILE = 'hook-rejection.log';\nconst LOG_FILE_PREV = 'hook-rejection.1.log';\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded\nconst MAX_AGE_DAYS = 7;\n\nconst RULE_NAME_RE = /^\\[([^\\]]+)\\] \\(/gm;\n\nexport function logRejection(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n result: BlockedResult,\n cwd: string,\n): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // `.webpieces/` lives at the repo root, NOT the AI's cwd — resolve it so a hook fired while\n // the AI is in a subdirectory never scatters a stray `<subdir>/.webpieces` tree.\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const now = new Date();\n const timestamp = now.toISOString();\n const epochMs = String(now.getTime());\n const dateStr = timestamp.slice(0, 10);\n\n // LOCAL scope — a rejection is this worktree's event, and a per-worktree log has exactly one\n // writer, so its appends and its daily detail files cannot collide with another agent's.\n const hooksDir = dotWebpieces.localFile(root, HOOKS_STATE_DIR);\n const logsDir = dotWebpieces.logs(root);\n const dayDir = path.join(hooksDir, dateStr);\n fs.mkdirSync(dayDir, { recursive: true });\n fs.mkdirSync(logsDir, { recursive: true });\n\n const relativePath = computeRelativePath(input.filePath, root);\n const ruleNames = extractRuleNames(result.report);\n const detailFileName = `writeInfo-${epochMs}.md`;\n // Relative to the STATE DIR, not to the log file's own directory: the index now lives in\n // `logs/` while the detail lives in `hooks/<date>/`, so a bare `<date>/<file>` would no\n // longer resolve from where the reader found the line.\n const detailRelPath = `${HOOKS_STATE_DIR}/${dateStr}/${detailFileName}`;\n\n const detail = buildDetailContent(timestamp, toolKind, relativePath, ruleNames, result.report, input);\n fs.writeFileSync(path.join(dayDir, detailFileName), detail);\n\n const logPath = path.join(logsDir, logStream.fileName(LOG_FILE));\n rotateLogFile(logPath, path.join(logsDir, logStream.fileName(LOG_FILE_PREV)));\n\n // APPEND-ONLY, and `fault=` is spelled exactly as the L0 sh log spells it. A fault-S storm\n // previously landed here as a couple of lines attributed to whatever rule the report happened\n // to cite, with nothing anywhere identifying L0 as the cause. (WHO made the call is answered by\n // the filename, which logStream prefixes with session/agent/hook — not by a column here.)\n const logLine = `[${timestamp}]\\t${toolKind}\\t${relativePath}\\t[${ruleNames.join(',')}]\\t${detailRelPath}\\tfault=${result.fault}\\n`;\n fs.appendFileSync(logPath, logLine);\n\n rotateOldDays(hooksDir, MAX_AGE_DAYS);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction computeRelativePath(filePath: string, cwd: string): string {\n if (filePath.startsWith(cwd)) {\n const rel = filePath.slice(cwd.length);\n if (rel.startsWith('/')) return rel.slice(1);\n return rel;\n }\n return filePath;\n}\n\n/**\n * The rule names a block report cites — every `[<rule-name>] (` header it opens with. Exported because\n * two audit streams need the same answer from the same regex: this file's rejection index, and the\n * `rule=` field guard-invocations.log now carries (see InvocationLog.finish). Two scrapers would be\n * two answers to one question.\n */\n// webpieces-disable no-function-outside-class -- pure regex scraper beside this module's other module-scope helpers; exported so the invocation log and the rejection index scrape rule names with the SAME code.\nexport function extractRuleNames(report: string): string[] {\n const names: string[] = [];\n let match = RULE_NAME_RE.exec(report);\n while (match !== null) {\n names.push(match[1]);\n match = RULE_NAME_RE.exec(report);\n }\n RULE_NAME_RE.lastIndex = 0;\n return names;\n}\n\nfunction buildDetailContent(\n timestamp: string,\n toolKind: ToolKind,\n relativePath: string,\n ruleNames: string[],\n report: string,\n input: NormalizedToolInput,\n): string {\n const lines: string[] = [];\n lines.push('# Hook Rejection Detail');\n lines.push('');\n lines.push(`- **Timestamp:** ${timestamp}`);\n lines.push(`- **Tool:** ${toolKind}`);\n lines.push(`- **File:** ${relativePath}`);\n lines.push(`- **Rules violated:** ${ruleNames.join(', ')}`);\n lines.push('');\n lines.push('## Report');\n lines.push('');\n lines.push('```');\n lines.push(report.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('## Content Being Written');\n lines.push('');\n\n if (toolKind === 'Write') {\n const content = input.edits.length > 0 ? input.edits[0].newString : '';\n lines.push('```typescript');\n lines.push(content.trimEnd());\n lines.push('```');\n } else {\n for (let i = 0; i < input.edits.length; i += 1) {\n const edit = input.edits[i];\n lines.push(`### Edit ${String(i + 1)} of ${String(input.edits.length)}`);\n lines.push('');\n lines.push('**old_string:**');\n lines.push('```typescript');\n lines.push(edit.oldString.trimEnd());\n lines.push('```');\n lines.push('');\n lines.push('**new_string:**');\n lines.push('```typescript');\n lines.push(edit.newString.trimEnd());\n lines.push('```');\n lines.push('');\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n}\n\nfunction rotateOldDays(hooksDir: string, maxAgeDays: number): void {\n const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(hooksDir);\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return;\n }\n\n for (const entry of entries) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(entry)) continue;\n const dirDate = new Date(entry + 'T00:00:00Z');\n if (isNaN(dirDate.getTime())) continue;\n if (dirDate.getTime() < cutoff) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(path.join(hooksDir, entry), { recursive: true, force: true });\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n }\n}\n"]}
@@ -21,7 +21,9 @@ const to_error_1 = require("./to-error");
21
21
  const report_1 = require("./report");
22
22
  const read_only_inspection_1 = require("./read-only-inspection");
23
23
  const shim_1 = require("../bin/shim");
24
+ const l0_fault_codes_1 = require("./l0-fault-codes");
24
25
  const l0_matrix_1 = require("./l0-matrix");
26
+ const l1_rows_1 = require("./l1-rows");
25
27
  const types_1 = require("./types");
26
28
  // Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a
27
29
  // disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the
@@ -77,7 +79,9 @@ function configMissingBlock(cwd, command = '') {
77
79
  if (command !== '' && l0FaultAllows(command))
78
80
  return null;
79
81
  const root = new rules_config_1.RepoRootFinder().resolveRepoRoot(cwd);
80
- return new types_1.BlockedResult(l0_matrix_1.CONFIG_MISSING_REPORT + (0, l0_matrix_1.guardMatrixPointer)((0, l0_matrix_1.writeGuardMatrixDoc)(root)));
82
+ // Stamped with its L0 letter so the block is greppable as fault C wherever it is recorded — the
83
+ // adapter carries it to the audit line rather than re-deriving it from the report text.
84
+ return new types_1.BlockedResult(l0_matrix_1.CONFIG_MISSING_REPORT + (0, l0_matrix_1.guardMatrixPointer)((0, l0_matrix_1.writeGuardMatrixDoc)(root)), l0_fault_codes_1.L0_FAULT_CONFIG_MISSING);
81
85
  }
82
86
  function run(toolKind, input, cwd, mode = 'all') {
83
87
  return runInternal(toolKind, input, cwd, mode);
@@ -283,14 +287,46 @@ function coordinatorInWorktreeBlock(command, tree, agent) {
283
287
  (0, decision_log_1.logGuardDecision)(tree.root, new decision_log_1.GuardDecision('coordinator-in-worktree', 'Bash', command, (0, decision_log_1.branchForLog)(tree.root), 'BLOCK', 'coordinator working inside a linked worktree'));
284
288
  return new types_1.BlockedResult(report);
285
289
  }
286
- // The structural L1 blocks, in order: coordinator-in-worktree (row 3), then force-to-root (row 5).
287
- // Neither is a configurable rule — they are decided from the resolved tree and the caller, so they run
288
- // as one step here rather than as two near-identical stanzas in runBashInternal.
290
+ // Where the command lands in L1's five dimensions (K/A/R/G/P). The ONE place the runner's view of the
291
+ // world is translated into the matrix's vocabulary see L1Classification.forEnforcement for why
292
+ // TreeKind 'outside' currently classifies as `p`.
293
+ // webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape
294
+ function l1Classify(command, tree, agent) {
295
+ return l1_rows_1.L1Classification.forEnforcement(tree.kind, agent.coordinator, new read_only_inspection_1.ReadOnlyInspectionScan().isReadOnlyInspection(command), isGitOrGhCommand(command), path.resolve(tree.effectiveCwd) === path.resolve(tree.root));
296
+ }
297
+ // The structural L1 blocks, in order: the misplaced-`cd` PRE-STAGE, then coordinator-in-worktree
298
+ // (row 3), then force-to-root (row 5). None is a configurable rule — they are decided from the command
299
+ // text, the resolved tree and the caller, so they run as one step here rather than as three
300
+ // near-identical stanzas in runBashInternal.
301
+ //
302
+ // For the two TREE-BASED blocks the ORDER and the CHOICE are not written here: they come from L1_ROWS
303
+ // (l1-rows.ts), the same array guards/L1-location.md is rendered from. Classify, take the first
304
+ // matching row, dispatch on its blockId — so a row deleted from the array is a block that stops firing,
305
+ // and the doc cannot describe a table the guard does not consult. The two report builders below still
306
+ // own their own predicates and their deny strings; l1-matrix.spec.ts asserts the row lookup and those
307
+ // predicates agree.
308
+ //
309
+ // misplacedCdBlock is deliberately OUTSIDE that lookup, and runs ahead of it, for the reason its own
310
+ // docblock gives: it decides from command TEXT, before a tree has been resolved, and the other two
311
+ // reason FROM the resolved tree. Classifying it would mean asking L1_ROWS a question whose answer the
312
+ // classification itself depends on. It is the same shape as L2's `bareCheckoutOfMain` pre-stage.
313
+ //
314
+ // KNOWN GAP, stated rather than discovered later: that makes it an L1 block with NO row in L1_ROWS, so
315
+ // guards/L1-location.md does not describe it — the exact drift this table exists to prevent, arriving
316
+ // while the table was being built. It needs a row (its "state" would be a command shape, the way L2
317
+ // row 2 carries one), which changes the rendered doc and so belongs in its own change, not in a merge
318
+ // resolution.
289
319
  // webpieces-disable no-function-outside-class -- sibling of gitFromSubdirBlock() and the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape
290
320
  function l1LocationBlock(command, tree, agent) {
291
- return misplacedCdBlock(command, tree)
292
- ?? coordinatorInWorktreeBlock(command, tree, agent)
293
- ?? gitFromSubdirBlock(command, tree);
321
+ const misplacedCd = misplacedCdBlock(command, tree);
322
+ if (misplacedCd !== null)
323
+ return misplacedCd;
324
+ const row = (0, l1_rows_1.firstMatchingL1Row)(l1Classify(command, tree, agent));
325
+ if (row.blockId === null)
326
+ return null; // exempt (row 1) or hand down to L2 (rows 2, 4, 6)
327
+ if (row.blockId === 'coordinator-in-worktree')
328
+ return coordinatorInWorktreeBlock(command, tree, agent);
329
+ return gitFromSubdirBlock(command, tree);
294
330
  }
295
331
  /**
296
332
  * ONE legal shape for relocating a command: `cd <literal path> && <work>`. Anything else — a `cd` after
@@ -471,7 +507,8 @@ function checkConfigSync(rules, config) {
471
507
  lines.push(` "${rule.name}": ${JSON.stringify((0, rules_config_1.seedEntryForRule)(rule.name))}`);
472
508
  lines.push('');
473
509
  }
474
- return new types_1.BlockedResult(lines.join('\n'));
510
+ // Fault Y, stamped for the audit trail — see configMissingBlock for why the producer names it.
511
+ return new types_1.BlockedResult(lines.join('\n'), l0_fault_codes_1.L0_FAULT_CONFIG_OUT_OF_SYNC);
475
512
  }
476
513
  // N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may
477
514
  // EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees: