@webpieces/ai-hook-rules 0.4.711 → 0.4.713

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.
Files changed (59) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/detect-ai.d.ts +5 -4
  3. package/src/adapters/detect-ai.js +5 -4
  4. package/src/adapters/detect-ai.js.map +1 -1
  5. package/src/adapters/hook-core.js +9 -3
  6. package/src/adapters/hook-core.js.map +1 -1
  7. package/src/bin/codex-guard-presence.d.ts +86 -0
  8. package/src/bin/codex-guard-presence.js +150 -0
  9. package/src/bin/codex-guard-presence.js.map +1 -0
  10. package/src/bin/codex-trust.d.ts +99 -0
  11. package/src/bin/codex-trust.js +199 -0
  12. package/src/bin/codex-trust.js.map +1 -0
  13. package/src/bin/hook-registration.d.ts +200 -43
  14. package/src/bin/hook-registration.js +218 -89
  15. package/src/bin/hook-registration.js.map +1 -1
  16. package/src/bin/l0-allowlist.d.ts +0 -30
  17. package/src/bin/l0-allowlist.js +9 -67
  18. package/src/bin/l0-allowlist.js.map +1 -1
  19. package/src/bin/l0-decide.d.ts +43 -0
  20. package/src/bin/l0-decide.js +90 -0
  21. package/src/bin/l0-decide.js.map +1 -0
  22. package/src/bin/l0-ignored-tools.d.ts +38 -0
  23. package/src/bin/l0-ignored-tools.js +44 -0
  24. package/src/bin/l0-ignored-tools.js.map +1 -0
  25. package/src/bin/setup-config.d.ts +19 -0
  26. package/src/bin/setup-config.js +370 -0
  27. package/src/bin/setup-config.js.map +1 -0
  28. package/src/bin/setup.d.ts +56 -21
  29. package/src/bin/setup.js +125 -370
  30. package/src/bin/setup.js.map +1 -1
  31. package/src/bin/shim-audit-log.js +10 -1
  32. package/src/bin/shim-audit-log.js.map +1 -1
  33. package/src/bin/shim-deny-reason.js +4 -4
  34. package/src/bin/shim-deny-reason.js.map +1 -1
  35. package/src/bin/shim.d.ts +2 -0
  36. package/src/bin/shim.js +21 -1
  37. package/src/bin/shim.js.map +1 -1
  38. package/src/bin/upgrade-shim.d.ts +1 -1
  39. package/src/bin/upgrade-shim.js +12 -8
  40. package/src/bin/upgrade-shim.js.map +1 -1
  41. package/src/core/agent-event.d.ts +16 -0
  42. package/src/core/agent-event.js +17 -1
  43. package/src/core/agent-event.js.map +1 -1
  44. package/src/core/ai-type-context.d.ts +36 -0
  45. package/src/core/ai-type-context.js +45 -0
  46. package/src/core/ai-type-context.js.map +1 -0
  47. package/src/core/decision-log.js +9 -0
  48. package/src/core/decision-log.js.map +1 -1
  49. package/src/core/l0-matrix.js +14 -6
  50. package/src/core/l0-matrix.js.map +1 -1
  51. package/src/core/l0-tooling-doc.d.ts +3 -2
  52. package/src/core/l0-tooling-doc.js +22 -10
  53. package/src/core/l0-tooling-doc.js.map +1 -1
  54. package/src/core/rejection-log.js +5 -1
  55. package/src/core/rejection-log.js.map +1 -1
  56. package/src/index.d.ts +1 -1
  57. package/src/index.js +4 -1
  58. package/src/index.js.map +1 -1
  59. package/templates/ai-hook.sh +15 -1
@@ -0,0 +1,38 @@
1
+ /**
2
+ * THE CODEX TOOLS L0 HAS NOTHING TO SAY ABOUT — its own module because BOTH halves of L0 read it and
3
+ * neither may reach the other: `l0-allowlist.ts` splices the sh alternation into the rendered shim, and
4
+ * `l0-decide.ts` asks the JS predicate. A set that lived in either one would make the other import it
5
+ * for a reason unrelated to that file's job.
6
+ *
7
+ * Dependency-free on purpose, like every other L0 module: it has to load on a tree too broken to build a
8
+ * DI container.
9
+ */
10
+ /**
11
+ * TOOLS WITH NOTHING TO JUDGE — the Codex tools that are neither a shell command nor a file edit.
12
+ *
13
+ * MEASURED in a live codex-cli 0.151.0 session: `webrun`, `collaborationspawn_agent`,
14
+ * `collaborationwait_agent`, `view_image`, `update_plan`. The CodexAdapter already maps them to the
15
+ * `Ignored` routing kind on a HEALTHY tree; this is the same answer one layer out, for the fail-CLOSED
16
+ * path, where the guard bin never runs and the shim decides alone.
17
+ *
18
+ * Without it, an L0 fault (D/X/U/K/S) turns every one of these into a DENY: `update_plan` is how a Codex
19
+ * agent records what it intends to do next, so the agent would be blocked from RECORDING the cure while
20
+ * being told to run it. That is the deadlock shape the whole allowlist exists to remove, and it is why
21
+ * this is a set and not a comment.
22
+ *
23
+ * AN EXPLICIT LIST, NOT "anything unrecognised" — and that asymmetry with CodexAdapter.kindOf() is
24
+ * deliberate. The adapter's default is safe because a healthy tree still has every guard behind it; L0's
25
+ * default must be DENY, or a future write-capable Codex tool would be waved straight past a fault the
26
+ * day it ships. So `apply_patch` is deliberately ABSENT: it is Codex's only WRITE, it is not a cure, and
27
+ * under a fault it must fail closed exactly as `Write`/`Edit` do.
28
+ *
29
+ * Keep in sync with L0_IGNORED_TOOLS_SH below (locked by the twin unit test).
30
+ */
31
+ export declare const L0_IGNORED_TOOLS: ReadonlySet<string>;
32
+ /**
33
+ * sh twin of L0_IGNORED_TOOLS — the alternation the rendered shim splices into a `case` pattern.
34
+ *
35
+ * BUILT from the set rather than retyped, for the reason every other twin in this file is: two hand-kept
36
+ * lists is two answers to one question, and the one that drifts is always the one nobody is reading.
37
+ */
38
+ export declare const L0_IGNORED_TOOLS_SH: string;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.L0_IGNORED_TOOLS_SH = exports.L0_IGNORED_TOOLS = void 0;
4
+ /**
5
+ * THE CODEX TOOLS L0 HAS NOTHING TO SAY ABOUT — its own module because BOTH halves of L0 read it and
6
+ * neither may reach the other: `l0-allowlist.ts` splices the sh alternation into the rendered shim, and
7
+ * `l0-decide.ts` asks the JS predicate. A set that lived in either one would make the other import it
8
+ * for a reason unrelated to that file's job.
9
+ *
10
+ * Dependency-free on purpose, like every other L0 module: it has to load on a tree too broken to build a
11
+ * DI container.
12
+ */
13
+ /**
14
+ * TOOLS WITH NOTHING TO JUDGE — the Codex tools that are neither a shell command nor a file edit.
15
+ *
16
+ * MEASURED in a live codex-cli 0.151.0 session: `webrun`, `collaborationspawn_agent`,
17
+ * `collaborationwait_agent`, `view_image`, `update_plan`. The CodexAdapter already maps them to the
18
+ * `Ignored` routing kind on a HEALTHY tree; this is the same answer one layer out, for the fail-CLOSED
19
+ * path, where the guard bin never runs and the shim decides alone.
20
+ *
21
+ * Without it, an L0 fault (D/X/U/K/S) turns every one of these into a DENY: `update_plan` is how a Codex
22
+ * agent records what it intends to do next, so the agent would be blocked from RECORDING the cure while
23
+ * being told to run it. That is the deadlock shape the whole allowlist exists to remove, and it is why
24
+ * this is a set and not a comment.
25
+ *
26
+ * AN EXPLICIT LIST, NOT "anything unrecognised" — and that asymmetry with CodexAdapter.kindOf() is
27
+ * deliberate. The adapter's default is safe because a healthy tree still has every guard behind it; L0's
28
+ * default must be DENY, or a future write-capable Codex tool would be waved straight past a fault the
29
+ * day it ships. So `apply_patch` is deliberately ABSENT: it is Codex's only WRITE, it is not a cure, and
30
+ * under a fault it must fail closed exactly as `Write`/`Edit` do.
31
+ *
32
+ * Keep in sync with L0_IGNORED_TOOLS_SH below (locked by the twin unit test).
33
+ */
34
+ exports.L0_IGNORED_TOOLS = new Set([
35
+ 'webrun', 'collaborationspawn_agent', 'collaborationwait_agent', 'view_image', 'update_plan',
36
+ ]);
37
+ /**
38
+ * sh twin of L0_IGNORED_TOOLS — the alternation the rendered shim splices into a `case` pattern.
39
+ *
40
+ * BUILT from the set rather than retyped, for the reason every other twin in this file is: two hand-kept
41
+ * lists is two answers to one question, and the one that drifts is always the one nobody is reading.
42
+ */
43
+ exports.L0_IGNORED_TOOLS_SH = [...exports.L0_IGNORED_TOOLS].join('|');
44
+ //# sourceMappingURL=l0-ignored-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l0-ignored-tools.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/l0-ignored-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACU,QAAA,gBAAgB,GAAwB,IAAI,GAAG,CAAC;IACzD,QAAQ,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,YAAY,EAAE,aAAa;CAC/F,CAAC,CAAC;AAEH;;;;;GAKG;AACU,QAAA,mBAAmB,GAAG,CAAC,GAAG,wBAAgB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC","sourcesContent":["/**\n * THE CODEX TOOLS L0 HAS NOTHING TO SAY ABOUT — its own module because BOTH halves of L0 read it and\n * neither may reach the other: `l0-allowlist.ts` splices the sh alternation into the rendered shim, and\n * `l0-decide.ts` asks the JS predicate. A set that lived in either one would make the other import it\n * for a reason unrelated to that file's job.\n *\n * Dependency-free on purpose, like every other L0 module: it has to load on a tree too broken to build a\n * DI container.\n */\n/**\n * TOOLS WITH NOTHING TO JUDGE — the Codex tools that are neither a shell command nor a file edit.\n *\n * MEASURED in a live codex-cli 0.151.0 session: `webrun`, `collaborationspawn_agent`,\n * `collaborationwait_agent`, `view_image`, `update_plan`. The CodexAdapter already maps them to the\n * `Ignored` routing kind on a HEALTHY tree; this is the same answer one layer out, for the fail-CLOSED\n * path, where the guard bin never runs and the shim decides alone.\n *\n * Without it, an L0 fault (D/X/U/K/S) turns every one of these into a DENY: `update_plan` is how a Codex\n * agent records what it intends to do next, so the agent would be blocked from RECORDING the cure while\n * being told to run it. That is the deadlock shape the whole allowlist exists to remove, and it is why\n * this is a set and not a comment.\n *\n * AN EXPLICIT LIST, NOT \"anything unrecognised\" — and that asymmetry with CodexAdapter.kindOf() is\n * deliberate. The adapter's default is safe because a healthy tree still has every guard behind it; L0's\n * default must be DENY, or a future write-capable Codex tool would be waved straight past a fault the\n * day it ships. So `apply_patch` is deliberately ABSENT: it is Codex's only WRITE, it is not a cure, and\n * under a fault it must fail closed exactly as `Write`/`Edit` do.\n *\n * Keep in sync with L0_IGNORED_TOOLS_SH below (locked by the twin unit test).\n */\nexport const L0_IGNORED_TOOLS: ReadonlySet<string> = new Set([\n 'webrun', 'collaborationspawn_agent', 'collaborationwait_agent', 'view_image', 'update_plan',\n]);\n\n/**\n * sh twin of L0_IGNORED_TOOLS — the alternation the rendered shim splices into a `case` pattern.\n *\n * BUILT from the set rather than retyped, for the reason every other twin in this file is: two hand-kept\n * lists is two answers to one question, and the one that drifts is always the one nobody is reading.\n */\nexport const L0_IGNORED_TOOLS_SH = [...L0_IGNORED_TOOLS].join('|');\n"]}
@@ -0,0 +1,19 @@
1
+ type Json = Record<string, unknown>;
2
+ type RuleEntry = Json;
3
+ type Section = Record<string, RuleEntry>;
4
+ interface ConfigFile {
5
+ extends?: string;
6
+ rules: Section;
7
+ hookGuards: Section;
8
+ commands: Json;
9
+ excludePaths: string[];
10
+ 'match-rules': Json[];
11
+ rulesDir: string[];
12
+ }
13
+ interface MigrateResult {
14
+ config: ConfigFile;
15
+ changes: string[];
16
+ }
17
+ export declare function migrate(existing: Json): MigrateResult;
18
+ export declare function seedOrSyncConfig(projectRoot: string): void;
19
+ export {};
@@ -0,0 +1,370 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migrate = migrate;
4
+ exports.seedOrSyncConfig = seedOrSyncConfig;
5
+ const tslib_1 = require("tslib");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const rules_config_1 = require("@webpieces/rules-config");
9
+ const to_error_1 = require("../core/to-error");
10
+ /**
11
+ * SEEDING AND MIGRATING `webpieces.config.json` — the installer's OTHER job.
12
+ *
13
+ * Split out of ./setup.ts purely for size, and along the seam that was already there: setup.ts is now
14
+ * hook WIRING (which settings file gets which matcher and which shim command) and this module is CONFIG
15
+ * SHAPE (which rules exist, where a retired key moves to). They share only `main()`, which calls this
16
+ * once and then wires the hooks.
17
+ *
18
+ * Like setup.ts it is deliberately DI-FREE: `wp-install-ai-hooks` has to run on a half-written
19
+ * node_modules (see install-entry.ts), so every function here is module-scope and imports nothing that
20
+ * needs a container.
21
+ */
22
+ const CONFIG_FILENAME = 'webpieces.config.json';
23
+ // The seeded buildCommand comes from @webpieces/rules-config, NOT from a copy here. This file used to
24
+ // hold its own — with `--base=origin/main`, a DIFFERENT base from the one the gate documents — so a
25
+ // freshly set-up repo was seeded with a command that rebuilds projects touched by other people's
26
+ // merged PRs, and whole-repo-build-guard then quoted that command back in its refusals.
27
+ const DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';
28
+ const DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';
29
+ // webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design
30
+ function seedRule(ruleName) {
31
+ // Both escape hatches are seeded (and REQUIRED) so every rule block shows them: 0 = active,
32
+ // null = no branch scoping. A human/AI edits these to time-box or branch-scope a rule off.
33
+ //
34
+ // The ENTIRE entry comes from rules-config's seedEntryForRule() — the same module that owns the
35
+ // schema the loader validates against, so the installer can never emit an entry the loader
36
+ // rejects. It supplies: the recommended mode (the SAME recommendation the validator prints in its
37
+ // copy-paste snippet, so seed and advice cannot disagree), both hatches, and a default for every
38
+ // other schema-REQUIRED field. Seeding used to be a flat 'OFF' plus the two hatches, which was
39
+ // wrong twice over: adopters got nothing enforced, AND the entry was missing required fields
40
+ // (e.g. branch-creation-guard.autoReapMergedBranches), so the config failed to load on first run.
41
+ return (0, rules_config_1.seedEntryForRule)(ruleName);
42
+ }
43
+ // The guard-hint command strings live under `guardHints`. The flat `upsertPr`/`mergeComplete` keys this
44
+ // used to seed are RETIRED and now fail validation — seeding them meant every freshly installed repo was
45
+ // born on a shape the validator rejects.
46
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
47
+ function seedCommands() {
48
+ return {
49
+ 'pr-gate': { mode: 'OFF', buildCommand: rules_config_1.DEFAULT_BUILD_COMMAND, gates: [] },
50
+ guardHints: { prCreationOrPush: DEFAULT_UPSERT_PR, mergeInProgress: DEFAULT_MERGE_COMPLETE },
51
+ };
52
+ }
53
+ // Required excludePaths block: ONE glob list suppressing hook enforcement per file path. Seeded empty
54
+ // (enforce everywhere) — a client adds paths (e.g. "repositories/**") to exempt vendored trees.
55
+ //
56
+ // Deliberately NOT seeded with webpieces' own `.webpieces/` state dir. That exemption lives in CODE
57
+ // (`isWebpiecesStateDir`, consulted by `filterByExcludedPaths` ahead of this list and regardless of it),
58
+ // and a glob here would be a second, weaker spelling of it — weaker because `.webpieces/**` compiles to
59
+ // an anchored regex that misses the bare directory the predicate matches, and because a config entry
60
+ // invites a consumer to delete it and believe the exemption went with it.
61
+ // webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design
62
+ function seedExcludePaths() {
63
+ return [];
64
+ }
65
+ // Bring an existing `excludePaths` forward to the single-list shape. Already a list → untouched.
66
+ // Legacy `{ rules, guards }` → unioned (order preserved, duplicates dropped) and recorded as a change
67
+ // so `wp-install-ai-hooks` is the migration path rather than a hand-edit. Anything else → seeded [].
68
+ // webpieces-disable no-any-unknown -- `raw` is opaque consumer JSON until narrowed here
69
+ // webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design
70
+ function migrateExcludePaths(raw, changes) {
71
+ if (Array.isArray(raw))
72
+ return raw.filter(p => typeof p === 'string');
73
+ if (typeof raw === 'object' && raw !== null) {
74
+ // webpieces-disable no-any-unknown -- narrowing the opaque legacy block from consumer JSON
75
+ const legacy = raw;
76
+ const rules = Array.isArray(legacy['rules']) ? legacy['rules'] : [];
77
+ const guards = Array.isArray(legacy['guards']) ? legacy['guards'] : [];
78
+ const merged = [...new Set([...rules, ...guards].filter(p => typeof p === 'string'))];
79
+ changes.push(`migrated excludePaths {rules,guards} -> one list (${merged.length} path(s))`);
80
+ return merged;
81
+ }
82
+ changes.push('added excludePaths ([])');
83
+ return seedExcludePaths();
84
+ }
85
+ /** One retired flat command string and the guardHints field it becomes. Data-only (per CLAUDE.md). */
86
+ class GuardHintMove {
87
+ retiredKey;
88
+ hintKey;
89
+ fallback;
90
+ constructor(retiredKey, hintKey, fallback) {
91
+ this.retiredKey = retiredKey;
92
+ this.hintKey = hintKey;
93
+ this.fallback = fallback;
94
+ }
95
+ }
96
+ /**
97
+ * Bring `commands` forward to the `guardHints` shape, moving the RETIRED flat `upsertPr`/`mergeComplete`
98
+ * strings and DELETING them. Deleting is the point: the validator now rejects them, so leaving them behind
99
+ * would keep the config failing after a "successful" sync.
100
+ *
101
+ * The consumer's own value wins over the default — a repo that renamed its gated command keeps that name.
102
+ */
103
+ // webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design
104
+ function migrateGuardHints(commands, changes) {
105
+ const hints = (typeof commands['guardHints'] === 'object' && commands['guardHints'] !== null)
106
+ ? commands['guardHints'] : {};
107
+ const moves = [
108
+ new GuardHintMove('upsertPr', 'prCreationOrPush', DEFAULT_UPSERT_PR),
109
+ new GuardHintMove('mergeComplete', 'mergeInProgress', DEFAULT_MERGE_COMPLETE),
110
+ ];
111
+ for (const move of moves) {
112
+ const retiredKey = move.retiredKey;
113
+ const hintKey = move.hintKey;
114
+ const fallback = move.fallback;
115
+ const carried = commands[retiredKey];
116
+ if (carried !== undefined) {
117
+ delete commands[retiredKey];
118
+ if (hints[hintKey] === undefined)
119
+ hints[hintKey] = carried;
120
+ changes.push(`moved retired commands.${retiredKey} -> commands.guardHints.${hintKey}`);
121
+ }
122
+ if (hints[hintKey] === undefined) {
123
+ hints[hintKey] = fallback;
124
+ changes.push(`added commands.guardHints.${hintKey}`);
125
+ }
126
+ }
127
+ commands['guardHints'] = hints;
128
+ }
129
+ /**
130
+ * Apply the RETIRED rule/guard retirements in place. These used to be rewritten silently at load time, so
131
+ * a consumer's file kept the dead name forever; the loader now rejects it, which makes this the one
132
+ * command that can fix the file. Skips a rename when the new name is already configured, so an explicit
133
+ * entry is never clobbered by a stale one.
134
+ *
135
+ * NOT EVERY RETIREMENT IS A RENAME, and treating them all as one produced garbage. `whole-repo-build-guard`
136
+ * moved OUT of webpieces.config.json entirely — its `movedTo` is the PROSE destination
137
+ * `~/.webpieces/config.json → experimental.whole-repo-build-guard`, not a sibling key — so the rename
138
+ * branch below would have created a hookGuards entry literally named that whole sentence, which no
139
+ * validator knows and which the next run reports as another unknown rule. `prunable` is the discriminator:
140
+ * when the entry says deleting is the whole fix, DELETE it, exactly as `ConfigPruner` does.
141
+ */
142
+ // webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design
143
+ function migrateRetiredRuleNames(section, changes) {
144
+ for (const entry of rules_config_1.RETIRED_CONFIG_KEYS) {
145
+ if (entry.scope !== rules_config_1.RETIRED_SCOPE_RULE)
146
+ continue;
147
+ if (!(entry.key in section))
148
+ continue;
149
+ if (entry.prunable) {
150
+ delete section[entry.key];
151
+ changes.push(`deleted retired "${entry.key}" (it moved to ${entry.movedTo})`);
152
+ continue;
153
+ }
154
+ mergeIntoDestination(section, entry.key, entry.movedTo, changes);
155
+ }
156
+ fillRequiredFields(section, changes);
157
+ }
158
+ /**
159
+ * Fold one retired key's entry into its destination, whether the destination exists yet or not.
160
+ *
161
+ * THIS IS N→1, NOT 1:1, and the difference is the whole reason this helper exists. Four retired keys
162
+ * now point at ONE destination (`branch-state-guard`, `pr-lifecycle-guard`). The previous code renamed
163
+ * the first key it met and then, finding the destination already present, DELETED each of the other
164
+ * three outright — so which guard's settings survived depended on RETIRED_CONFIG_KEYS declaration
165
+ * order rather than on the consumer's file, and the survivor carried only that one guard's fields, so
166
+ * it was missing required fields of the merged schema. `wp-install-ai-hooks` is the command advertised
167
+ * as the migration path; half-migrating every consumer into an invalid config is not an option.
168
+ *
169
+ * UNION, first writer wins per field. Earlier-declared keys are the more specific ones (only
170
+ * feature-branch-guard carries `branchNamingConvention`), and a field already present on the
171
+ * destination — because the consumer wrote it, or an earlier key contributed it — is never overwritten.
172
+ * Fields the merged schema does not know are dropped by the same pass, since carrying a deleted field
173
+ * across (`upsertPrCommand`) would produce a config the validator immediately rejects.
174
+ */
175
+ // webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design
176
+ function mergeIntoDestination(section, key, destination, changes) {
177
+ const source = asSection(section[key]);
178
+ delete section[key];
179
+ const fields = (0, rules_config_1.schemaFieldNames)(destination);
180
+ const target = asSection(section[destination]);
181
+ const existed = destination in section;
182
+ const carried = [];
183
+ const dropped = [];
184
+ for (const field of Object.keys(source)) {
185
+ if (fields !== null && !fields.includes(field)) {
186
+ dropped.push(field);
187
+ continue;
188
+ }
189
+ if (field in target)
190
+ continue;
191
+ target[field] = source[field];
192
+ carried.push(field);
193
+ }
194
+ section[destination] = target;
195
+ const verb = existed ? 'merged' : 'renamed';
196
+ const droppedNote = dropped.length > 0 ? `; dropped deleted field(s) ${dropped.join(', ')}` : '';
197
+ changes.push(`${verb} retired "${key}" -> "${destination}" (carried ${carried.join(', ') || 'nothing new'}${droppedNote})`);
198
+ }
199
+ /**
200
+ * Fill any schema-REQUIRED field a migrated entry ended up without.
201
+ *
202
+ * A union of four partial entries is not guaranteed to satisfy the destination's schema — the merged
203
+ * `branch-state-guard` needs `mode` and both escape hatches, and a consumer whose four old entries
204
+ * predate one of them would land short. Seeding the gap from the SAME source the installer and the
205
+ * validator use (seedEntryForRule) is what makes the install command a complete instruction
206
+ * rather than a first step. Only ever ADDS; a value the consumer stated is never touched.
207
+ */
208
+ // webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design
209
+ function fillRequiredFields(section, changes) {
210
+ for (const name of Object.keys(section)) {
211
+ if ((0, rules_config_1.schemaFieldNames)(name) === null)
212
+ continue;
213
+ // A rule ENTRY is a flat bag of scalars, so it is read as Json here rather than through
214
+ // asSection (whose values are whole entries). Same object either way; only the view differs.
215
+ const entry = asSection(section[name]);
216
+ const seed = (0, rules_config_1.seedEntryForRule)(name);
217
+ const added = [];
218
+ for (const field of Object.keys(seed)) {
219
+ if (field in entry)
220
+ continue;
221
+ entry[field] = seed[field];
222
+ added.push(field);
223
+ }
224
+ if (added.length === 0)
225
+ continue;
226
+ section[name] = entry;
227
+ changes.push(`filled required field(s) on "${name}": ${added.join(', ')}`);
228
+ }
229
+ }
230
+ // Deep-copy the framework's default match-rules (the no-fetch guard) into plain JSON for the config
231
+ // file. Round-tripping through JSON turns the MatchRuleConfig instances into plain objects.
232
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
233
+ function seedMatchRules() {
234
+ return JSON.parse(JSON.stringify(rules_config_1.DEFAULT_MATCH_RULES));
235
+ }
236
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
237
+ function buildSeedConfig() {
238
+ const rules = {};
239
+ const hookGuards = {};
240
+ for (const name of (0, rules_config_1.allRuleNames)()) {
241
+ if ((0, rules_config_1.sectionForRule)(name) === 'hookGuards')
242
+ hookGuards[name] = seedRule(name);
243
+ else
244
+ rules[name] = seedRule(name);
245
+ }
246
+ return {
247
+ rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(),
248
+ // Seed the required match-rules array with the framework's default no-fetch guard. A fresh
249
+ // project gets contract-first enforcement out of the box; clients edit it and add more entries.
250
+ 'match-rules': seedMatchRules(),
251
+ rulesDir: [],
252
+ };
253
+ }
254
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
255
+ function writeConfig(configPath, config) {
256
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n');
257
+ }
258
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
259
+ function readConfig(configPath) {
260
+ const raw = fs.readFileSync(configPath, 'utf8');
261
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
262
+ try {
263
+ return JSON.parse(raw);
264
+ }
265
+ catch (err) {
266
+ const error = (0, to_error_1.toError)(err);
267
+ throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });
268
+ }
269
+ }
270
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
271
+ function asSection(value) {
272
+ return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? value : {};
273
+ }
274
+ // Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.
275
+ // Returns a human-readable list of what changed (empty = already up to date).
276
+ // webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method
277
+ function migrate(existing) {
278
+ const changes = [];
279
+ const rules = asSection(existing['rules']);
280
+ const hookGuards = asSection(existing['hookGuards']);
281
+ const commands = (typeof existing['commands'] === 'object' && existing['commands'] !== null)
282
+ ? existing['commands'] : {};
283
+ // Move a deprecated top-level pr-gate block under commands.
284
+ if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {
285
+ commands['pr-gate'] = existing['pr-gate'];
286
+ changes.push('moved top-level "pr-gate" → commands["pr-gate"]');
287
+ }
288
+ // Apply retired RENAMES first, so a renamed guard is placed and presence-checked under its new name
289
+ // rather than being treated as unknown and re-added alongside its own stale entry.
290
+ migrateRetiredRuleNames(rules, changes);
291
+ migrateRetiredRuleNames(hookGuards, changes);
292
+ // Move guards mistakenly left in rules into hookGuards.
293
+ for (const name of Object.keys(rules)) {
294
+ if ((0, rules_config_1.isHookGuard)(name)) {
295
+ hookGuards[name] = rules[name];
296
+ delete rules[name];
297
+ changes.push(`moved "${name}" from rules → hookGuards`);
298
+ }
299
+ }
300
+ // Move code rules mistakenly placed in hookGuards back into rules.
301
+ for (const name of Object.keys(hookGuards)) {
302
+ if (!(0, rules_config_1.isHookGuard)(name) && (0, rules_config_1.allRuleNames)().includes(name)) {
303
+ rules[name] = hookGuards[name];
304
+ delete hookGuards[name];
305
+ changes.push(`moved "${name}" from hookGuards → rules`);
306
+ }
307
+ }
308
+ // Add any missing built-in into its correct section, ENFORCING at its recommended mode (not OFF).
309
+ for (const name of (0, rules_config_1.allRuleNames)()) {
310
+ const target = (0, rules_config_1.sectionForRule)(name) === 'hookGuards' ? hookGuards : rules;
311
+ if (!(name in target)) {
312
+ const entry = seedRule(name);
313
+ target[name] = entry;
314
+ changes.push(`added "${name}" (${String(entry['mode'])}) to ${(0, rules_config_1.sectionForRule)(name)}`);
315
+ }
316
+ }
317
+ // Fill command defaults.
318
+ if (commands['pr-gate'] === undefined) {
319
+ commands['pr-gate'] = { mode: 'OFF', buildCommand: rules_config_1.DEFAULT_BUILD_COMMAND, gates: [] };
320
+ changes.push('added commands["pr-gate"] (OFF)');
321
+ }
322
+ migrateGuardHints(commands, changes);
323
+ // Seed the now-required excludePaths list (empty = enforce everywhere) if the config predates it,
324
+ // and MIGRATE the legacy `{ rules: [], guards: [] }` object to the single list by unioning them.
325
+ // The union is behaviour-preserving for every config we have seen (both lists set identically), and
326
+ // widening is the safe direction anyway: a path either side excluded stays excluded.
327
+ const excludePaths = migrateExcludePaths(existing['excludePaths'], changes);
328
+ // Seed the now-required match-rules array (with the default no-fetch guard) if the config predates
329
+ // it. A client that has already customized it keeps their array untouched.
330
+ let matchRules;
331
+ if (Array.isArray(existing['match-rules'])) {
332
+ matchRules = existing['match-rules'];
333
+ }
334
+ else {
335
+ matchRules = seedMatchRules();
336
+ changes.push('added "match-rules" (seeded with the no-fetch guard)');
337
+ }
338
+ const rulesDir = Array.isArray(existing['rulesDir']) ? existing['rulesDir'] : [];
339
+ const config = { rules, hookGuards, commands, excludePaths, 'match-rules': matchRules, rulesDir };
340
+ if (typeof existing['extends'] === 'string')
341
+ config.extends = existing['extends'];
342
+ return { config, changes };
343
+ }
344
+ // Seed the config when it is missing, migrate it when it is not. ONE behaviour, always — there is no
345
+ // "migrate but stop here" mode any more. The flag that used to select it was never NECESSARY (the validator prints
346
+ // the exact edit for every error at once, and editing webpieces.config.json is always allowed through the
347
+ // guard — the documented primary cure), it REFUSED to act when the config was missing (useless in the one
348
+ // case automation would have helped), and it gave deny messages a second competing path when they are
349
+ // supposed to end in exactly one action. Readers also mistook it for the shim-repair command, which it
350
+ // never was — `wp-upgrade-shim` is that.
351
+ // webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope
352
+ function seedOrSyncConfig(projectRoot) {
353
+ const configPath = path.join(projectRoot, CONFIG_FILENAME);
354
+ if (!fs.existsSync(configPath)) {
355
+ writeConfig(configPath, buildSeedConfig());
356
+ console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands); each rule seeded at its recommended mode — gradual where supported, so only code you change is enforced.`);
357
+ console.log(' Enable the ones you want by changing "mode".');
358
+ return;
359
+ }
360
+ const result = migrate(readConfig(configPath));
361
+ if (result.changes.length === 0) {
362
+ console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);
363
+ return;
364
+ }
365
+ writeConfig(configPath, result.config);
366
+ console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);
367
+ for (const change of result.changes)
368
+ console.log(` - ${change}`);
369
+ }
370
+ //# sourceMappingURL=setup-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup-config.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n allRuleNames, seedEntryForRule, schemaFieldNames, sectionForRule, isHookGuard, DEFAULT_MATCH_RULES,\n DEFAULT_BUILD_COMMAND, RETIRED_CONFIG_KEYS, RETIRED_SCOPE_RULE,\n} from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\n\n/**\n * SEEDING AND MIGRATING `webpieces.config.json` — the installer's OTHER job.\n *\n * Split out of ./setup.ts purely for size, and along the seam that was already there: setup.ts is now\n * hook WIRING (which settings file gets which matcher and which shim command) and this module is CONFIG\n * SHAPE (which rules exist, where a retired key moves to). They share only `main()`, which calls this\n * once and then wires the hooks.\n *\n * Like setup.ts it is deliberately DI-FREE: `wp-install-ai-hooks` has to run on a half-written\n * node_modules (see install-entry.ts), so every function here is module-scope and imports nothing that\n * needs a container.\n */\nconst CONFIG_FILENAME = 'webpieces.config.json';\n// The seeded buildCommand comes from @webpieces/rules-config, NOT from a copy here. This file used to\n// hold its own — with `--base=origin/main`, a DIFFERENT base from the one the gate documents — so a\n// freshly set-up repo was seeded with a command that rebuilds projects touched by other people's\n// merged PRs, and whole-repo-build-guard then quoted that command back in its refusals.\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: string[];\n 'match-rules': Json[];\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedRule(ruleName: string): RuleEntry {\n // Both escape hatches are seeded (and REQUIRED) so every rule block shows them: 0 = active,\n // null = no branch scoping. A human/AI edits these to time-box or branch-scope a rule off.\n //\n // The ENTIRE entry comes from rules-config's seedEntryForRule() — the same module that owns the\n // schema the loader validates against, so the installer can never emit an entry the loader\n // rejects. It supplies: the recommended mode (the SAME recommendation the validator prints in its\n // copy-paste snippet, so seed and advice cannot disagree), both hatches, and a default for every\n // other schema-REQUIRED field. Seeding used to be a flat 'OFF' plus the two hatches, which was\n // wrong twice over: adopters got nothing enforced, AND the entry was missing required fields\n // (e.g. branch-creation-guard.autoReapMergedBranches), so the config failed to load on first run.\n return seedEntryForRule(ruleName);\n}\n\n// The guard-hint command strings live under `guardHints`. The flat `upsertPr`/`mergeComplete` keys this\n// used to seed are RETIRED and now fail validation — seeding them meant every freshly installed repo was\n// born on a shape the validator rejects.\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n guardHints: { prCreationOrPush: DEFAULT_UPSERT_PR, mergeInProgress: DEFAULT_MERGE_COMPLETE },\n };\n}\n\n// Required excludePaths block: ONE glob list suppressing hook enforcement per file path. Seeded empty\n// (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\n//\n// Deliberately NOT seeded with webpieces' own `.webpieces/` state dir. That exemption lives in CODE\n// (`isWebpiecesStateDir`, consulted by `filterByExcludedPaths` ahead of this list and regardless of it),\n// and a glob here would be a second, weaker spelling of it — weaker because `.webpieces/**` compiles to\n// an anchored regex that misses the bare directory the predicate matches, and because a config entry\n// invites a consumer to delete it and believe the exemption went with it.\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedExcludePaths(): string[] {\n return [];\n}\n\n// Bring an existing `excludePaths` forward to the single-list shape. Already a list → untouched.\n// Legacy `{ rules, guards }` → unioned (order preserved, duplicates dropped) and recorded as a change\n// so `wp-install-ai-hooks` is the migration path rather than a hand-edit. Anything else → seeded [].\n// webpieces-disable no-any-unknown -- `raw` is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateExcludePaths(raw: unknown, changes: string[]): string[] {\n if (Array.isArray(raw)) return (raw as string[]).filter(p => typeof p === 'string');\n if (typeof raw === 'object' && raw !== null) {\n // webpieces-disable no-any-unknown -- narrowing the opaque legacy block from consumer JSON\n const legacy = raw as Record<string, unknown>;\n const rules = Array.isArray(legacy['rules']) ? (legacy['rules'] as string[]) : [];\n const guards = Array.isArray(legacy['guards']) ? (legacy['guards'] as string[]) : [];\n const merged = [...new Set([...rules, ...guards].filter(p => typeof p === 'string'))];\n changes.push(`migrated excludePaths {rules,guards} -> one list (${merged.length} path(s))`);\n return merged;\n }\n changes.push('added excludePaths ([])');\n return seedExcludePaths();\n}\n\n/** One retired flat command string and the guardHints field it becomes. Data-only (per CLAUDE.md). */\nclass GuardHintMove {\n retiredKey: string;\n hintKey: string;\n fallback: string;\n\n constructor(retiredKey: string, hintKey: string, fallback: string) {\n this.retiredKey = retiredKey;\n this.hintKey = hintKey;\n this.fallback = fallback;\n }\n}\n\n/**\n * Bring `commands` forward to the `guardHints` shape, moving the RETIRED flat `upsertPr`/`mergeComplete`\n * strings and DELETING them. Deleting is the point: the validator now rejects them, so leaving them behind\n * would keep the config failing after a \"successful\" sync.\n *\n * The consumer's own value wins over the default — a repo that renamed its gated command keeps that name.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateGuardHints(commands: Json, changes: string[]): void {\n const hints: Json = (typeof commands['guardHints'] === 'object' && commands['guardHints'] !== null)\n ? (commands['guardHints'] as Json) : {};\n const moves: readonly GuardHintMove[] = [\n new GuardHintMove('upsertPr', 'prCreationOrPush', DEFAULT_UPSERT_PR),\n new GuardHintMove('mergeComplete', 'mergeInProgress', DEFAULT_MERGE_COMPLETE),\n ];\n for (const move of moves) {\n const retiredKey = move.retiredKey;\n const hintKey = move.hintKey;\n const fallback = move.fallback;\n const carried = commands[retiredKey];\n if (carried !== undefined) {\n delete commands[retiredKey];\n if (hints[hintKey] === undefined) hints[hintKey] = carried;\n changes.push(`moved retired commands.${retiredKey} -> commands.guardHints.${hintKey}`);\n }\n if (hints[hintKey] === undefined) {\n hints[hintKey] = fallback;\n changes.push(`added commands.guardHints.${hintKey}`);\n }\n }\n commands['guardHints'] = hints;\n}\n\n/**\n * Apply the RETIRED rule/guard retirements in place. These used to be rewritten silently at load time, so\n * a consumer's file kept the dead name forever; the loader now rejects it, which makes this the one\n * command that can fix the file. Skips a rename when the new name is already configured, so an explicit\n * entry is never clobbered by a stale one.\n *\n * NOT EVERY RETIREMENT IS A RENAME, and treating them all as one produced garbage. `whole-repo-build-guard`\n * moved OUT of webpieces.config.json entirely — its `movedTo` is the PROSE destination\n * `~/.webpieces/config.json → experimental.whole-repo-build-guard`, not a sibling key — so the rename\n * branch below would have created a hookGuards entry literally named that whole sentence, which no\n * validator knows and which the next run reports as another unknown rule. `prunable` is the discriminator:\n * when the entry says deleting is the whole fix, DELETE it, exactly as `ConfigPruner` does.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateRetiredRuleNames(section: Section, changes: string[]): void {\n for (const entry of RETIRED_CONFIG_KEYS) {\n if (entry.scope !== RETIRED_SCOPE_RULE) continue;\n if (!(entry.key in section)) continue;\n if (entry.prunable) {\n delete section[entry.key];\n changes.push(`deleted retired \"${entry.key}\" (it moved to ${entry.movedTo})`);\n continue;\n }\n mergeIntoDestination(section, entry.key, entry.movedTo, changes);\n }\n fillRequiredFields(section, changes);\n}\n\n/**\n * Fold one retired key's entry into its destination, whether the destination exists yet or not.\n *\n * THIS IS N→1, NOT 1:1, and the difference is the whole reason this helper exists. Four retired keys\n * now point at ONE destination (`branch-state-guard`, `pr-lifecycle-guard`). The previous code renamed\n * the first key it met and then, finding the destination already present, DELETED each of the other\n * three outright — so which guard's settings survived depended on RETIRED_CONFIG_KEYS declaration\n * order rather than on the consumer's file, and the survivor carried only that one guard's fields, so\n * it was missing required fields of the merged schema. `wp-install-ai-hooks` is the command advertised\n * as the migration path; half-migrating every consumer into an invalid config is not an option.\n *\n * UNION, first writer wins per field. Earlier-declared keys are the more specific ones (only\n * feature-branch-guard carries `branchNamingConvention`), and a field already present on the\n * destination — because the consumer wrote it, or an earlier key contributed it — is never overwritten.\n * Fields the merged schema does not know are dropped by the same pass, since carrying a deleted field\n * across (`upsertPrCommand`) would produce a config the validator immediately rejects.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction mergeIntoDestination(section: Section, key: string, destination: string, changes: string[]): void {\n const source = asSection(section[key]);\n delete section[key];\n const fields = schemaFieldNames(destination);\n const target = asSection(section[destination]);\n const existed = destination in section;\n const carried: string[] = [];\n const dropped: string[] = [];\n for (const field of Object.keys(source)) {\n if (fields !== null && !fields.includes(field)) { dropped.push(field); continue; }\n if (field in target) continue;\n target[field] = source[field];\n carried.push(field);\n }\n section[destination] = target;\n const verb = existed ? 'merged' : 'renamed';\n const droppedNote = dropped.length > 0 ? `; dropped deleted field(s) ${dropped.join(', ')}` : '';\n changes.push(`${verb} retired \"${key}\" -> \"${destination}\" (carried ${carried.join(', ') || 'nothing new'}${droppedNote})`);\n}\n\n/**\n * Fill any schema-REQUIRED field a migrated entry ended up without.\n *\n * A union of four partial entries is not guaranteed to satisfy the destination's schema — the merged\n * `branch-state-guard` needs `mode` and both escape hatches, and a consumer whose four old entries\n * predate one of them would land short. Seeding the gap from the SAME source the installer and the\n * validator use (seedEntryForRule) is what makes the install command a complete instruction\n * rather than a first step. Only ever ADDS; a value the consumer stated is never touched.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction fillRequiredFields(section: Section, changes: string[]): void {\n for (const name of Object.keys(section)) {\n if (schemaFieldNames(name) === null) continue;\n // A rule ENTRY is a flat bag of scalars, so it is read as Json here rather than through\n // asSection (whose values are whole entries). Same object either way; only the view differs.\n const entry: Json = asSection(section[name]);\n const seed = seedEntryForRule(name);\n const added: string[] = [];\n for (const field of Object.keys(seed)) {\n if (field in entry) continue;\n entry[field] = seed[field];\n added.push(field);\n }\n if (added.length === 0) continue;\n section[name] = entry;\n changes.push(`filled required field(s) on \"${name}\": ${added.join(', ')}`);\n }\n}\n\n// Deep-copy the framework's default match-rules (the no-fetch guard) into plain JSON for the config\n// file. Round-tripping through JSON turns the MatchRuleConfig instances into plain objects.\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nfunction seedMatchRules(): Json[] {\n return JSON.parse(JSON.stringify(DEFAULT_MATCH_RULES)) as Json[];\n}\n\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule(name);\n else rules[name] = seedRule(name);\n }\n return {\n rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(),\n // Seed the required match-rules array with the framework's default no-fetch guard. A fresh\n // project gets contract-first enforcement out of the box; clients edit it and add more entries.\n 'match-rules': seedMatchRules(),\n rulesDir: [],\n };\n}\n\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\n// webpieces-disable no-function-outside-class -- this module is deliberately DI-FREE: `wp-install-ai-hooks` must run on a half-written node_modules (see install-entry.ts), so it cannot build a container to hold a method\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Apply retired RENAMES first, so a renamed guard is placed and presence-checked under its new name\n // rather than being treated as unknown and re-added alongside its own stale entry.\n migrateRetiredRuleNames(rules, changes);\n migrateRetiredRuleNames(hookGuards, changes);\n\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section, ENFORCING at its recommended mode (not OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n const entry = seedRule(name);\n target[name] = entry;\n changes.push(`added \"${name}\" (${String(entry['mode'])}) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n migrateGuardHints(commands, changes);\n\n // Seed the now-required excludePaths list (empty = enforce everywhere) if the config predates it,\n // and MIGRATE the legacy `{ rules: [], guards: [] }` object to the single list by unioning them.\n // The union is behaviour-preserving for every config we have seen (both lists set identically), and\n // widening is the safe direction anyway: a path either side excluded stays excluded.\n const excludePaths: string[] = migrateExcludePaths(existing['excludePaths'], changes);\n\n // Seed the now-required match-rules array (with the default no-fetch guard) if the config predates\n // it. A client that has already customized it keeps their array untouched.\n let matchRules: Json[];\n if (Array.isArray(existing['match-rules'])) {\n matchRules = existing['match-rules'] as Json[];\n } else {\n matchRules = seedMatchRules();\n changes.push('added \"match-rules\" (seeded with the no-fetch guard)');\n }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, 'match-rules': matchRules, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\n// Seed the config when it is missing, migrate it when it is not. ONE behaviour, always — there is no\n// \"migrate but stop here\" mode any more. The flag that used to select it was never NECESSARY (the validator prints\n// the exact edit for every error at once, and editing webpieces.config.json is always allowed through the\n// guard — the documented primary cure), it REFUSED to act when the config was missing (useless in the one\n// case automation would have helped), and it gave deny messages a second competing path when they are\n// supposed to end in exactly one action. Readers also mistook it for the shim-repair command, which it\n// never was — `wp-upgrade-shim` is that.\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nexport function seedOrSyncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands); each rule seeded at its recommended mode — gradual where supported, so only code you change is enforced.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n"]}