@webpieces/ai-hook-rules 0.4.663 → 0.4.665
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.
- package/package.json +2 -2
- package/src/core/load-rules.d.ts +5 -3
- package/src/core/load-rules.js +5 -3
- package/src/core/load-rules.js.map +1 -1
- package/src/core/rules/whole-repo-build-guard.d.ts +25 -17
- package/src/core/rules/whole-repo-build-guard.js +33 -22
- package/src/core/rules/whole-repo-build-guard.js.map +1 -1
- package/src/core/runner.js +2 -2
- package/src/core/runner.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.665",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/ai-hook-rules"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.665"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
package/src/core/load-rules.d.ts
CHANGED
|
@@ -16,9 +16,11 @@ export declare function loadRules(config: WebpiecesRulesConfig, workspaceRoot: s
|
|
|
16
16
|
*
|
|
17
17
|
* Each rule here decides for ITSELF whether it acts, and the two do it differently on purpose:
|
|
18
18
|
*
|
|
19
|
-
* - `whole-repo-build-guard` is
|
|
20
|
-
* `~/.webpieces/config.json` opts
|
|
21
|
-
* legitimately want
|
|
19
|
+
* - `whole-repo-build-guard` is ON by default and goes inert when the optional machine-local
|
|
20
|
+
* `~/.webpieces/config.json` opts OUT. Building the whole workspace is a thing a person may
|
|
21
|
+
* legitimately want, so the escape hatch stays — but it takes no file and no key to be in the
|
|
22
|
+
* default state, which is the difference between this and the required-key release that blocked
|
|
23
|
+
* every upgrading consumer's shell.
|
|
22
24
|
* - `commit-message-substitution-guard` acts unconditionally. Nobody legitimately wants a backtick
|
|
23
25
|
* expanded inside a commit message, and its cure (`git commit -F <file>`) is available for every
|
|
24
26
|
* input and can never itself match the guard — so there is nothing for a switch to rescue.
|
package/src/core/load-rules.js
CHANGED
|
@@ -101,9 +101,11 @@ function loadRules(config, workspaceRoot, guardHints) {
|
|
|
101
101
|
*
|
|
102
102
|
* Each rule here decides for ITSELF whether it acts, and the two do it differently on purpose:
|
|
103
103
|
*
|
|
104
|
-
* - `whole-repo-build-guard` is
|
|
105
|
-
* `~/.webpieces/config.json` opts
|
|
106
|
-
* legitimately want
|
|
104
|
+
* - `whole-repo-build-guard` is ON by default and goes inert when the optional machine-local
|
|
105
|
+
* `~/.webpieces/config.json` opts OUT. Building the whole workspace is a thing a person may
|
|
106
|
+
* legitimately want, so the escape hatch stays — but it takes no file and no key to be in the
|
|
107
|
+
* default state, which is the difference between this and the required-key release that blocked
|
|
108
|
+
* every upgrading consumer's shell.
|
|
107
109
|
* - `commit-message-substitution-guard` acts unconditionally. Nobody legitimately wants a backtick
|
|
108
110
|
* expanded inside a commit message, and its cure (`git commit -F <file>`) is available for every
|
|
109
111
|
* input and can never itself match the guard — so there is nothing for a switch to rescue.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-rules.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/load-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n BaseRuleConfig, RuleOptions, WebpiecesRulesConfig,\n NoAnyUnknownConfig, NoImplicitAnyConfig, MaxFileLinesConfig, ValidateTsInSrcConfig,\n NoDestructureConfig, RequireReturnTypeConfig, NoUnmanagedExceptionsConfig,\n CatchErrorPatternConfig, ThrowCauseRequiredConfig,\n NoSymbolDiTokensConfig, NoCustomCssConfig, NoProcessExitOutsideMainConfig, BranchCreationGuardConfig,\n PrLifecycleGuardConfig, BranchStateGuardConfig,\n NoJsFilesConfig, MatchRuleConfig,\n} from '@webpieces/rules-config';\n\nimport type { Rule, PlainRule } from './types';\nimport { InformAiError } from './types';\nimport { toError } from './to-error';\nimport { EmptyRuleConfig } from './rule-base';\nimport { CustomRuleAdapter } from './custom-rule-adapter';\nimport { builtInConfigKeys } from './rules/index';\nimport { NoAnyUnknownRule } from './rules/no-any-unknown';\nimport { NoImplicitAnyRule } from './rules/no-implicit-any';\nimport { MaxFileLinesRule } from './rules/max-file-lines';\nimport { ValidateTsInSrcRule } from './rules/validate-ts-in-src';\nimport { NoDestructureRule } from './rules/no-destructure';\nimport { RequireReturnTypeRule } from './rules/require-return-type';\nimport { NoUnmanagedExceptionsRule } from './rules/no-unmanaged-exceptions';\nimport { CatchErrorPatternRule } from './rules/catch-error-pattern';\nimport { ThrowCauseRequiredRule } from './rules/throw-cause-required';\nimport { NoSymbolDiTokensRule } from './rules/no-symbol-di-tokens';\nimport { NoCustomCssRule } from './rules/no-custom-css';\nimport { NoProcessExitOutsideMainRule } from './rules/no-process-exit-outside-main';\nimport { BranchCreationGuardRule } from './rules/branch-creation-guard';\nimport { PrCreationOrPushGuardRule } from './rules/pr-creation-or-push-guard';\nimport { MergeInProgressGuardRule } from './rules/merge-in-progress-guard';\nimport { PrMergeGuardRule } from './rules/pr-merge-guard';\nimport { RedirectHowToMergeMainRule } from './rules/redirect-how-to-merge-main';\nimport { NoJsFilesRule } from './rules/no-js-files';\nimport { FeatureBranchGuardRule } from './rules/feature-branch-guard';\nimport { ReadStaleGuardRule } from './rules/read-stale-guard';\nimport { MergedBranchBashGuardRule } from './rules/merged-branch-bash-guard';\nimport { StaleMainBashGuardRule } from './rules/stale-main-bash-guard';\nimport { WholeRepoBuildGuardRule } from './rules/whole-repo-build-guard';\nimport { CommitMessageSubstitutionGuardRule } from './rules/commit-message-substitution-guard';\nimport { MatchRule } from './rules/match-rule';\n\nconst REQUIRED_FIELDS: readonly string[] = ['name', 'description', 'scope', 'files', 'check'];\nconst VALID_SCOPES = new Set(['edit', 'file', 'bash']);\n\n/**\n * ONE CONFIG KEY → N RULES.\n *\n * Each built-in rule is constructed from its typed *Config (the entry in webpieces.config.json). The\n * config arrives as a plain object structurally typed as the *Config class, so the `as` narrows the\n * shared BaseRuleConfig param back to the concrete config the rule consumes.\n *\n * The map is keyed by CONFIG KEY and each factory returns an ARRAY, because a hookGuards key names a\n * POLICY and a policy may be implemented by several classes: `branch-state-guard` builds all four\n * branch-state guards from one entry, `pr-lifecycle-guard` all four PR-lifecycle guards. It used to be\n * `Record<string, (c) => Rule>` — one factory per key — which is precisely why four classes could not\n * share a key and why the config had to carry nine switches for three decisions.\n *\n * `guardHints` are the resolved `commands.guardHints` strings, handed to the two rules that print a\n * gated command. They arrive as a constructor argument rather than a config field, so there is exactly\n * one spelling of each command in the config (see PrLifecycleGuardConfig).\n */\ntype RuleFactory = (config: BaseRuleConfig, guardHints: GuardHintCommands) => readonly Rule[];\n\n/** The two gated-command strings guards print, resolved from `commands.guardHints`. Data-only. */\nexport class GuardHintCommands {\n constructor(readonly upsertPr: string, readonly mergeComplete: string) {}\n}\n\nconst BUILT_IN_RULE_MAP: Record<string, RuleFactory> = {\n 'no-any-unknown': (c: BaseRuleConfig) => [new NoAnyUnknownRule(c as NoAnyUnknownConfig)],\n 'no-implicit-any': (c: BaseRuleConfig) => [new NoImplicitAnyRule(c as NoImplicitAnyConfig)],\n 'max-file-lines': (c: BaseRuleConfig) => [new MaxFileLinesRule(c as MaxFileLinesConfig)],\n 'validate-ts-in-src': (c: BaseRuleConfig) => [new ValidateTsInSrcRule(c as ValidateTsInSrcConfig)],\n 'no-destructure': (c: BaseRuleConfig) => [new NoDestructureRule(c as NoDestructureConfig)],\n 'require-return-type': (c: BaseRuleConfig) => [new RequireReturnTypeRule(c as RequireReturnTypeConfig)],\n 'no-unmanaged-exceptions': (c: BaseRuleConfig) => [new NoUnmanagedExceptionsRule(c as NoUnmanagedExceptionsConfig)],\n 'catch-error-pattern': (c: BaseRuleConfig) => [new CatchErrorPatternRule(c as CatchErrorPatternConfig)],\n 'throw-cause-required': (c: BaseRuleConfig) => [new ThrowCauseRequiredRule(c as ThrowCauseRequiredConfig)],\n 'no-symbol-di-tokens': (c: BaseRuleConfig) => [new NoSymbolDiTokensRule(c as NoSymbolDiTokensConfig)],\n 'no-custom-css': (c: BaseRuleConfig) => [new NoCustomCssRule(c as NoCustomCssConfig)],\n 'no-process-exit-outside-main': (c: BaseRuleConfig) => [new NoProcessExitOutsideMainRule(c as NoProcessExitOutsideMainConfig)],\n 'no-js-files': (c: BaseRuleConfig) => [new NoJsFilesRule(c as NoJsFilesConfig)],\n 'branch-creation-guard': (c: BaseRuleConfig) => [new BranchCreationGuardRule(c as BranchCreationGuardConfig)],\n // THE TWO COLLAPSED POLICIES. Order inside each array is the order the rules run in, and it is the\n // same order the previous per-key registry produced.\n 'pr-lifecycle-guard': (c: BaseRuleConfig, hints: GuardHintCommands) => [\n new PrCreationOrPushGuardRule(c as PrLifecycleGuardConfig, hints.upsertPr),\n new MergeInProgressGuardRule(c as PrLifecycleGuardConfig, hints.mergeComplete),\n new PrMergeGuardRule(c as PrLifecycleGuardConfig),\n new RedirectHowToMergeMainRule(c as PrLifecycleGuardConfig),\n ],\n 'branch-state-guard': (c: BaseRuleConfig) => [\n new FeatureBranchGuardRule(c as BranchStateGuardConfig),\n new ReadStaleGuardRule(c as BranchStateGuardConfig),\n new MergedBranchBashGuardRule(c as BranchStateGuardConfig),\n new StaleMainBashGuardRule(c as BranchStateGuardConfig),\n ],\n};\n\n// Index the typed config by rule name. Each value is the rule's *Config (a plain object from\n// JSON), or undefined when the rule has no entry yet (the sync check reports those).\nfunction asConfigMap(config: WebpiecesRulesConfig): Record<string, BaseRuleConfig | undefined> {\n // webpieces-disable no-any-unknown -- index the typed config by dynamic rule name\n return config as unknown as Record<string, BaseRuleConfig | undefined>;\n}\n\n// webpieces-disable no-function-outside-class -- the module's entry point, beside loadMatchRules/loadKeylessBashRules; this whole loader is module-scope functions and a lone class for one of them would break the file's shape\nexport function loadRules(\n config: WebpiecesRulesConfig,\n workspaceRoot: string,\n guardHints: GuardHintCommands,\n): readonly Rule[] {\n const builtIns = loadBuiltInRules(config, guardHints);\n const custom = loadCustomRules(config, workspaceRoot);\n return [...builtIns, ...custom];\n}\n\n/**\n * The KEYLESS bash guards: rules that have NO webpieces.config.json entry, and are therefore\n * deliberately kept out of `builtInConfigKeys`/`BUILT_IN_RULE_MAP` — so the config-sync check (fault Y,\n * \"every built-in rule needs an entry, or every Bash call is blocked\") can never see them. That\n * containment is the whole point: whole-repo-build-guard shipped inside the config-driven set once and\n * took every upgrading consumer's shell down with it.\n *\n * Each rule here decides for ITSELF whether it acts, and the two do it differently on purpose:\n *\n * - `whole-repo-build-guard` is EXPERIMENTAL and inert unless the optional machine-local\n * `~/.webpieces/config.json` opts in, because building the whole workspace is a thing a person may\n * legitimately want and a wrong block there costs them their shell.\n * - `commit-message-substitution-guard` acts unconditionally. Nobody legitimately wants a backtick\n * expanded inside a commit message, and its cure (`git commit -F <file>`) is available for every\n * input and can never itself match the guard — so there is nothing for a switch to rescue.\n *\n * `affectedBuildCommand` is the project's gate command, passed through so a refusal quotes what THIS\n * repo's gate actually runs.\n */\n// webpieces-disable no-function-outside-class -- sibling of loadRules/loadMatchRules in this module; the whole loader is module-scope functions and a lone class for this one would break the file's shape\nexport function loadKeylessBashRules(affectedBuildCommand: string): Rule[] {\n return [new WholeRepoBuildGuardRule(affectedBuildCommand), new CommitMessageSubstitutionGuardRule()];\n}\n\n// One MatchRule per entry of the `match-rules` array. Kept separate from loadRules (built-ins/custom)\n// because match-rules live in their own validated section — they must NOT flow through the\n// config-sync check, which compares rule names against the `rules`/`hookGuards` map.\nexport function loadMatchRules(matchRules: readonly MatchRuleConfig[]): Rule[] {\n return matchRules.map((c: MatchRuleConfig) => new MatchRule(c));\n}\n\n// Iterates CONFIG KEYS, not rule names — one entry can yield several rules (see BUILT_IN_RULE_MAP).\n// webpieces-disable no-function-outside-class -- the body of loadRules above, in the same module of loader functions\nfunction loadBuiltInRules(config: WebpiecesRulesConfig, guardHints: GuardHintCommands): Rule[] {\n const map = asConfigMap(config);\n const rules: Rule[] = [];\n for (const configKey of builtInConfigKeys) {\n const factory = BUILT_IN_RULE_MAP[configKey];\n if (!factory) {\n process.stderr.write(`[ai-hooks] unknown built-in config key: ${configKey}\\n`);\n continue;\n }\n const ruleConfig = map[configKey] ?? new EmptyRuleConfig();\n rules.push(...factory(ruleConfig, guardHints));\n }\n return rules;\n}\n\nfunction loadCustomRules(config: WebpiecesRulesConfig, workspaceRoot: string): Rule[] {\n const dirs = config.rulesDir ?? [];\n // webpieces-disable no-any-unknown -- index the typed config by dynamic custom-rule name\n const map = config as unknown as Record<string, RuleOptions | undefined>;\n const rules: Rule[] = [];\n for (const plain of loadCustomPlainRules(dirs, workspaceRoot)) {\n const rawConfig = map[plain.name] ?? {};\n rules.push(new CustomRuleAdapter(plain, rawConfig));\n }\n return rules;\n}\n\nfunction loadCustomPlainRules(rulesDirs: readonly string[], workspaceRoot: string): PlainRule[] {\n const modules: PlainRule[] = [];\n for (const dir of rulesDirs) {\n const absDir = path.isAbsolute(dir) ? dir : path.join(workspaceRoot, dir);\n if (!fs.existsSync(absDir)) {\n process.stderr.write(`[ai-hooks] rulesDir not found: ${absDir}\\n`);\n continue;\n }\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(absDir).filter((e: string) => e.endsWith('.js'));\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot read custom rules directory '${absDir}'`, { cause: error });\n }\n for (const entry of entries) {\n const full = path.join(absDir, entry);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const mod = require(full);\n const candidate = mod.default || mod;\n if (validateRule(candidate)) modules.push(candidate);\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot load custom rule '${full}'`, { cause: error });\n }\n }\n }\n return modules;\n}\n\n// webpieces-disable no-any-unknown -- validates untrusted require() output at system boundary\nfunction validateRule(rule: unknown): rule is PlainRule {\n if (!rule || typeof rule !== 'object') {\n process.stderr.write('[ai-hooks] rule is not an object, skipping\\n');\n return false;\n }\n // webpieces-disable no-any-unknown -- narrowing from unknown at system boundary\n const obj = rule as Record<string, unknown>;\n for (const field of REQUIRED_FIELDS) {\n if (obj[field] === undefined) {\n const name = typeof obj['name'] === 'string' ? obj['name'] : '<unnamed>';\n process.stderr.write(`[ai-hooks] rule \"${name}\" missing required field: ${field}\\n`);\n return false;\n }\n }\n if (!VALID_SCOPES.has(obj['scope'] as string)) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" has invalid scope: ${String(obj['scope'])}\\n`);\n return false;\n }\n if (!Array.isArray(obj['files'])) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" files must be an array\\n`);\n return false;\n }\n if (typeof obj['check'] !== 'function') {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" check must be a function\\n`);\n return false;\n }\n return true;\n}\n\nexport function globMatches(pattern: string, filePath: string): boolean {\n const regex = globToRegex(pattern);\n return regex.test(filePath);\n}\n\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"load-rules.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/load-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n BaseRuleConfig, RuleOptions, WebpiecesRulesConfig,\n NoAnyUnknownConfig, NoImplicitAnyConfig, MaxFileLinesConfig, ValidateTsInSrcConfig,\n NoDestructureConfig, RequireReturnTypeConfig, NoUnmanagedExceptionsConfig,\n CatchErrorPatternConfig, ThrowCauseRequiredConfig,\n NoSymbolDiTokensConfig, NoCustomCssConfig, NoProcessExitOutsideMainConfig, BranchCreationGuardConfig,\n PrLifecycleGuardConfig, BranchStateGuardConfig,\n NoJsFilesConfig, MatchRuleConfig,\n} from '@webpieces/rules-config';\n\nimport type { Rule, PlainRule } from './types';\nimport { InformAiError } from './types';\nimport { toError } from './to-error';\nimport { EmptyRuleConfig } from './rule-base';\nimport { CustomRuleAdapter } from './custom-rule-adapter';\nimport { builtInConfigKeys } from './rules/index';\nimport { NoAnyUnknownRule } from './rules/no-any-unknown';\nimport { NoImplicitAnyRule } from './rules/no-implicit-any';\nimport { MaxFileLinesRule } from './rules/max-file-lines';\nimport { ValidateTsInSrcRule } from './rules/validate-ts-in-src';\nimport { NoDestructureRule } from './rules/no-destructure';\nimport { RequireReturnTypeRule } from './rules/require-return-type';\nimport { NoUnmanagedExceptionsRule } from './rules/no-unmanaged-exceptions';\nimport { CatchErrorPatternRule } from './rules/catch-error-pattern';\nimport { ThrowCauseRequiredRule } from './rules/throw-cause-required';\nimport { NoSymbolDiTokensRule } from './rules/no-symbol-di-tokens';\nimport { NoCustomCssRule } from './rules/no-custom-css';\nimport { NoProcessExitOutsideMainRule } from './rules/no-process-exit-outside-main';\nimport { BranchCreationGuardRule } from './rules/branch-creation-guard';\nimport { PrCreationOrPushGuardRule } from './rules/pr-creation-or-push-guard';\nimport { MergeInProgressGuardRule } from './rules/merge-in-progress-guard';\nimport { PrMergeGuardRule } from './rules/pr-merge-guard';\nimport { RedirectHowToMergeMainRule } from './rules/redirect-how-to-merge-main';\nimport { NoJsFilesRule } from './rules/no-js-files';\nimport { FeatureBranchGuardRule } from './rules/feature-branch-guard';\nimport { ReadStaleGuardRule } from './rules/read-stale-guard';\nimport { MergedBranchBashGuardRule } from './rules/merged-branch-bash-guard';\nimport { StaleMainBashGuardRule } from './rules/stale-main-bash-guard';\nimport { WholeRepoBuildGuardRule } from './rules/whole-repo-build-guard';\nimport { CommitMessageSubstitutionGuardRule } from './rules/commit-message-substitution-guard';\nimport { MatchRule } from './rules/match-rule';\n\nconst REQUIRED_FIELDS: readonly string[] = ['name', 'description', 'scope', 'files', 'check'];\nconst VALID_SCOPES = new Set(['edit', 'file', 'bash']);\n\n/**\n * ONE CONFIG KEY → N RULES.\n *\n * Each built-in rule is constructed from its typed *Config (the entry in webpieces.config.json). The\n * config arrives as a plain object structurally typed as the *Config class, so the `as` narrows the\n * shared BaseRuleConfig param back to the concrete config the rule consumes.\n *\n * The map is keyed by CONFIG KEY and each factory returns an ARRAY, because a hookGuards key names a\n * POLICY and a policy may be implemented by several classes: `branch-state-guard` builds all four\n * branch-state guards from one entry, `pr-lifecycle-guard` all four PR-lifecycle guards. It used to be\n * `Record<string, (c) => Rule>` — one factory per key — which is precisely why four classes could not\n * share a key and why the config had to carry nine switches for three decisions.\n *\n * `guardHints` are the resolved `commands.guardHints` strings, handed to the two rules that print a\n * gated command. They arrive as a constructor argument rather than a config field, so there is exactly\n * one spelling of each command in the config (see PrLifecycleGuardConfig).\n */\ntype RuleFactory = (config: BaseRuleConfig, guardHints: GuardHintCommands) => readonly Rule[];\n\n/** The two gated-command strings guards print, resolved from `commands.guardHints`. Data-only. */\nexport class GuardHintCommands {\n constructor(readonly upsertPr: string, readonly mergeComplete: string) {}\n}\n\nconst BUILT_IN_RULE_MAP: Record<string, RuleFactory> = {\n 'no-any-unknown': (c: BaseRuleConfig) => [new NoAnyUnknownRule(c as NoAnyUnknownConfig)],\n 'no-implicit-any': (c: BaseRuleConfig) => [new NoImplicitAnyRule(c as NoImplicitAnyConfig)],\n 'max-file-lines': (c: BaseRuleConfig) => [new MaxFileLinesRule(c as MaxFileLinesConfig)],\n 'validate-ts-in-src': (c: BaseRuleConfig) => [new ValidateTsInSrcRule(c as ValidateTsInSrcConfig)],\n 'no-destructure': (c: BaseRuleConfig) => [new NoDestructureRule(c as NoDestructureConfig)],\n 'require-return-type': (c: BaseRuleConfig) => [new RequireReturnTypeRule(c as RequireReturnTypeConfig)],\n 'no-unmanaged-exceptions': (c: BaseRuleConfig) => [new NoUnmanagedExceptionsRule(c as NoUnmanagedExceptionsConfig)],\n 'catch-error-pattern': (c: BaseRuleConfig) => [new CatchErrorPatternRule(c as CatchErrorPatternConfig)],\n 'throw-cause-required': (c: BaseRuleConfig) => [new ThrowCauseRequiredRule(c as ThrowCauseRequiredConfig)],\n 'no-symbol-di-tokens': (c: BaseRuleConfig) => [new NoSymbolDiTokensRule(c as NoSymbolDiTokensConfig)],\n 'no-custom-css': (c: BaseRuleConfig) => [new NoCustomCssRule(c as NoCustomCssConfig)],\n 'no-process-exit-outside-main': (c: BaseRuleConfig) => [new NoProcessExitOutsideMainRule(c as NoProcessExitOutsideMainConfig)],\n 'no-js-files': (c: BaseRuleConfig) => [new NoJsFilesRule(c as NoJsFilesConfig)],\n 'branch-creation-guard': (c: BaseRuleConfig) => [new BranchCreationGuardRule(c as BranchCreationGuardConfig)],\n // THE TWO COLLAPSED POLICIES. Order inside each array is the order the rules run in, and it is the\n // same order the previous per-key registry produced.\n 'pr-lifecycle-guard': (c: BaseRuleConfig, hints: GuardHintCommands) => [\n new PrCreationOrPushGuardRule(c as PrLifecycleGuardConfig, hints.upsertPr),\n new MergeInProgressGuardRule(c as PrLifecycleGuardConfig, hints.mergeComplete),\n new PrMergeGuardRule(c as PrLifecycleGuardConfig),\n new RedirectHowToMergeMainRule(c as PrLifecycleGuardConfig),\n ],\n 'branch-state-guard': (c: BaseRuleConfig) => [\n new FeatureBranchGuardRule(c as BranchStateGuardConfig),\n new ReadStaleGuardRule(c as BranchStateGuardConfig),\n new MergedBranchBashGuardRule(c as BranchStateGuardConfig),\n new StaleMainBashGuardRule(c as BranchStateGuardConfig),\n ],\n};\n\n// Index the typed config by rule name. Each value is the rule's *Config (a plain object from\n// JSON), or undefined when the rule has no entry yet (the sync check reports those).\nfunction asConfigMap(config: WebpiecesRulesConfig): Record<string, BaseRuleConfig | undefined> {\n // webpieces-disable no-any-unknown -- index the typed config by dynamic rule name\n return config as unknown as Record<string, BaseRuleConfig | undefined>;\n}\n\n// webpieces-disable no-function-outside-class -- the module's entry point, beside loadMatchRules/loadKeylessBashRules; this whole loader is module-scope functions and a lone class for one of them would break the file's shape\nexport function loadRules(\n config: WebpiecesRulesConfig,\n workspaceRoot: string,\n guardHints: GuardHintCommands,\n): readonly Rule[] {\n const builtIns = loadBuiltInRules(config, guardHints);\n const custom = loadCustomRules(config, workspaceRoot);\n return [...builtIns, ...custom];\n}\n\n/**\n * The KEYLESS bash guards: rules that have NO webpieces.config.json entry, and are therefore\n * deliberately kept out of `builtInConfigKeys`/`BUILT_IN_RULE_MAP` — so the config-sync check (fault Y,\n * \"every built-in rule needs an entry, or every Bash call is blocked\") can never see them. That\n * containment is the whole point: whole-repo-build-guard shipped inside the config-driven set once and\n * took every upgrading consumer's shell down with it.\n *\n * Each rule here decides for ITSELF whether it acts, and the two do it differently on purpose:\n *\n * - `whole-repo-build-guard` is ON by default and goes inert when the optional machine-local\n * `~/.webpieces/config.json` opts OUT. Building the whole workspace is a thing a person may\n * legitimately want, so the escape hatch stays — but it takes no file and no key to be in the\n * default state, which is the difference between this and the required-key release that blocked\n * every upgrading consumer's shell.\n * - `commit-message-substitution-guard` acts unconditionally. Nobody legitimately wants a backtick\n * expanded inside a commit message, and its cure (`git commit -F <file>`) is available for every\n * input and can never itself match the guard — so there is nothing for a switch to rescue.\n *\n * `affectedBuildCommand` is the project's gate command, passed through so a refusal quotes what THIS\n * repo's gate actually runs.\n */\n// webpieces-disable no-function-outside-class -- sibling of loadRules/loadMatchRules in this module; the whole loader is module-scope functions and a lone class for this one would break the file's shape\nexport function loadKeylessBashRules(affectedBuildCommand: string): Rule[] {\n return [new WholeRepoBuildGuardRule(affectedBuildCommand), new CommitMessageSubstitutionGuardRule()];\n}\n\n// One MatchRule per entry of the `match-rules` array. Kept separate from loadRules (built-ins/custom)\n// because match-rules live in their own validated section — they must NOT flow through the\n// config-sync check, which compares rule names against the `rules`/`hookGuards` map.\nexport function loadMatchRules(matchRules: readonly MatchRuleConfig[]): Rule[] {\n return matchRules.map((c: MatchRuleConfig) => new MatchRule(c));\n}\n\n// Iterates CONFIG KEYS, not rule names — one entry can yield several rules (see BUILT_IN_RULE_MAP).\n// webpieces-disable no-function-outside-class -- the body of loadRules above, in the same module of loader functions\nfunction loadBuiltInRules(config: WebpiecesRulesConfig, guardHints: GuardHintCommands): Rule[] {\n const map = asConfigMap(config);\n const rules: Rule[] = [];\n for (const configKey of builtInConfigKeys) {\n const factory = BUILT_IN_RULE_MAP[configKey];\n if (!factory) {\n process.stderr.write(`[ai-hooks] unknown built-in config key: ${configKey}\\n`);\n continue;\n }\n const ruleConfig = map[configKey] ?? new EmptyRuleConfig();\n rules.push(...factory(ruleConfig, guardHints));\n }\n return rules;\n}\n\nfunction loadCustomRules(config: WebpiecesRulesConfig, workspaceRoot: string): Rule[] {\n const dirs = config.rulesDir ?? [];\n // webpieces-disable no-any-unknown -- index the typed config by dynamic custom-rule name\n const map = config as unknown as Record<string, RuleOptions | undefined>;\n const rules: Rule[] = [];\n for (const plain of loadCustomPlainRules(dirs, workspaceRoot)) {\n const rawConfig = map[plain.name] ?? {};\n rules.push(new CustomRuleAdapter(plain, rawConfig));\n }\n return rules;\n}\n\nfunction loadCustomPlainRules(rulesDirs: readonly string[], workspaceRoot: string): PlainRule[] {\n const modules: PlainRule[] = [];\n for (const dir of rulesDirs) {\n const absDir = path.isAbsolute(dir) ? dir : path.join(workspaceRoot, dir);\n if (!fs.existsSync(absDir)) {\n process.stderr.write(`[ai-hooks] rulesDir not found: ${absDir}\\n`);\n continue;\n }\n let entries: string[];\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n entries = fs.readdirSync(absDir).filter((e: string) => e.endsWith('.js'));\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot read custom rules directory '${absDir}'`, { cause: error });\n }\n for (const entry of entries) {\n const full = path.join(absDir, entry);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const mod = require(full);\n const candidate = mod.default || mod;\n if (validateRule(candidate)) modules.push(candidate);\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Cannot load custom rule '${full}'`, { cause: error });\n }\n }\n }\n return modules;\n}\n\n// webpieces-disable no-any-unknown -- validates untrusted require() output at system boundary\nfunction validateRule(rule: unknown): rule is PlainRule {\n if (!rule || typeof rule !== 'object') {\n process.stderr.write('[ai-hooks] rule is not an object, skipping\\n');\n return false;\n }\n // webpieces-disable no-any-unknown -- narrowing from unknown at system boundary\n const obj = rule as Record<string, unknown>;\n for (const field of REQUIRED_FIELDS) {\n if (obj[field] === undefined) {\n const name = typeof obj['name'] === 'string' ? obj['name'] : '<unnamed>';\n process.stderr.write(`[ai-hooks] rule \"${name}\" missing required field: ${field}\\n`);\n return false;\n }\n }\n if (!VALID_SCOPES.has(obj['scope'] as string)) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" has invalid scope: ${String(obj['scope'])}\\n`);\n return false;\n }\n if (!Array.isArray(obj['files'])) {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" files must be an array\\n`);\n return false;\n }\n if (typeof obj['check'] !== 'function') {\n process.stderr.write(`[ai-hooks] rule \"${obj['name']}\" check must be a function\\n`);\n return false;\n }\n return true;\n}\n\nexport function globMatches(pattern: string, filePath: string): boolean {\n const regex = globToRegex(pattern);\n return regex.test(filePath);\n}\n\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n"]}
|
|
@@ -31,22 +31,30 @@ import { FixHint } from '../fix-hint';
|
|
|
31
31
|
* that reads like the guard's advice was wrong. Only `$(git …)` is expanded, and only read-only git;
|
|
32
32
|
* anything else is left verbatim.
|
|
33
33
|
*
|
|
34
|
-
* ───
|
|
35
|
-
* `experimental.whole-repo-build-guard`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `hookGuards`, so every consumer that upgraded hit fault Y — EVERY Bash call blocked — for a feature
|
|
39
|
-
* they had never asked for. An experimental feature is opted into from a machine-local file whose absent
|
|
40
|
-
* state is byte-for-byte the old behaviour, or it is not experimental.
|
|
34
|
+
* ─── ON BY DEFAULT, with ONE machine-local opt-out ─────────────────────────────────────────────────
|
|
35
|
+
* `experimental.whole-repo-build-guard` in `~/.webpieces/config.json` is this guard's only switch, and
|
|
36
|
+
* it is an OPT-OUT: absent means ON (`WHOLE_REPO_BUILD_GUARD_DEFAULT`), `false` turns it off for this
|
|
37
|
+
* machine. Nothing has to be created, added or edited anywhere to be in the default state.
|
|
41
38
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
39
|
+
* It shipped OFF-by-default first, and that is why it never did its job. A guard nobody opts into never
|
|
40
|
+
* fires: a sibling repo's `ci:local` verify chain ran `prettier --check .`, `wp-ci` and
|
|
41
|
+
* `nx affected -t test` with NO `--base` — three whole-world passes on every inner loop — for months,
|
|
42
|
+
* while the correct scoped command sat unused in `commands.pr-gate.buildCommand`.
|
|
43
|
+
*
|
|
44
|
+
* There is still NO webpieces.config.json entry, and the reason is a live incident that is often
|
|
45
|
+
* misread as an argument against defaulting ON. This guard first shipped with `mode: 'ON'` by default
|
|
46
|
+
* AND a REQUIRED entry under `hookGuards`, and every consumer that upgraded hit fault Y — EVERY Bash
|
|
47
|
+
* call blocked — for a feature they had never asked for. The fault was the REQUIRED KEY: the failure
|
|
48
|
+
* was at config LOAD, before any command was judged, and the cure was "edit a file to get your shell
|
|
49
|
+
* back". Defaulting ON from an OPTIONAL machine-local file is the opposite shape. The only thing that
|
|
50
|
+
* can be refused here is a command that genuinely builds the world, and the refusal hands over the
|
|
51
|
+
* scoped command that replaces it.
|
|
52
|
+
*
|
|
53
|
+
* Three states, and only three:
|
|
54
|
+
* - the file does not exist (essentially every machine) → the guard is ON.
|
|
55
|
+
* - the file exists → the key's value decides when it is named, and ON when it is not. Every key in
|
|
56
|
+
* that file is optional, because it is MACHINE-GLOBAL and the repos on one machine pin different
|
|
57
|
+
* webpieces releases — a required key there is unsatisfiable (see home-config.ts).
|
|
50
58
|
* - the file exists and is unparseable, or a key it DOES define has the wrong type → HARD FAILURE
|
|
51
59
|
* naming the edit. That is a file somebody wrote wrongly, not one written for another release.
|
|
52
60
|
* Editing that file is an unconditional PASS in the guards, so the block is always self-curable.
|
|
@@ -88,8 +96,8 @@ export declare class WholeRepoBuildGuardRule extends BashRuleBase<EmptyRuleConfi
|
|
|
88
96
|
check(ctx: BashContext): readonly Violation[];
|
|
89
97
|
/**
|
|
90
98
|
* The home config, or the Error explaining why it is unusable. NOT a boolean: "absent" is already
|
|
91
|
-
* folded into the returned HomeConfig (
|
|
92
|
-
* distinguish is "present and wrong", which must be reported rather than swallowed.
|
|
99
|
+
* folded into the returned HomeConfig (each key's default — for this guard, ON), so the only thing
|
|
100
|
+
* left to distinguish is "present and wrong", which must be reported rather than swallowed.
|
|
93
101
|
*/
|
|
94
102
|
private loadHome;
|
|
95
103
|
private blockOnBrokenHomeConfig;
|
|
@@ -41,22 +41,30 @@ const whole_repo_build_scan_1 = require("./whole-repo-build-scan");
|
|
|
41
41
|
* that reads like the guard's advice was wrong. Only `$(git …)` is expanded, and only read-only git;
|
|
42
42
|
* anything else is left verbatim.
|
|
43
43
|
*
|
|
44
|
-
* ───
|
|
45
|
-
* `experimental.whole-repo-build-guard`
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `hookGuards`, so every consumer that upgraded hit fault Y — EVERY Bash call blocked — for a feature
|
|
49
|
-
* they had never asked for. An experimental feature is opted into from a machine-local file whose absent
|
|
50
|
-
* state is byte-for-byte the old behaviour, or it is not experimental.
|
|
44
|
+
* ─── ON BY DEFAULT, with ONE machine-local opt-out ─────────────────────────────────────────────────
|
|
45
|
+
* `experimental.whole-repo-build-guard` in `~/.webpieces/config.json` is this guard's only switch, and
|
|
46
|
+
* it is an OPT-OUT: absent means ON (`WHOLE_REPO_BUILD_GUARD_DEFAULT`), `false` turns it off for this
|
|
47
|
+
* machine. Nothing has to be created, added or edited anywhere to be in the default state.
|
|
51
48
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
49
|
+
* It shipped OFF-by-default first, and that is why it never did its job. A guard nobody opts into never
|
|
50
|
+
* fires: a sibling repo's `ci:local` verify chain ran `prettier --check .`, `wp-ci` and
|
|
51
|
+
* `nx affected -t test` with NO `--base` — three whole-world passes on every inner loop — for months,
|
|
52
|
+
* while the correct scoped command sat unused in `commands.pr-gate.buildCommand`.
|
|
53
|
+
*
|
|
54
|
+
* There is still NO webpieces.config.json entry, and the reason is a live incident that is often
|
|
55
|
+
* misread as an argument against defaulting ON. This guard first shipped with `mode: 'ON'` by default
|
|
56
|
+
* AND a REQUIRED entry under `hookGuards`, and every consumer that upgraded hit fault Y — EVERY Bash
|
|
57
|
+
* call blocked — for a feature they had never asked for. The fault was the REQUIRED KEY: the failure
|
|
58
|
+
* was at config LOAD, before any command was judged, and the cure was "edit a file to get your shell
|
|
59
|
+
* back". Defaulting ON from an OPTIONAL machine-local file is the opposite shape. The only thing that
|
|
60
|
+
* can be refused here is a command that genuinely builds the world, and the refusal hands over the
|
|
61
|
+
* scoped command that replaces it.
|
|
62
|
+
*
|
|
63
|
+
* Three states, and only three:
|
|
64
|
+
* - the file does not exist (essentially every machine) → the guard is ON.
|
|
65
|
+
* - the file exists → the key's value decides when it is named, and ON when it is not. Every key in
|
|
66
|
+
* that file is optional, because it is MACHINE-GLOBAL and the repos on one machine pin different
|
|
67
|
+
* webpieces releases — a required key there is unsatisfiable (see home-config.ts).
|
|
60
68
|
* - the file exists and is unparseable, or a key it DOES define has the wrong type → HARD FAILURE
|
|
61
69
|
* naming the edit. That is a file somebody wrote wrongly, not one written for another release.
|
|
62
70
|
* Editing that file is an unconditional PASS in the guards, so the block is always self-curable.
|
|
@@ -110,7 +118,7 @@ class WholeRepoBuildGuardRule extends rule_base_1.BashRuleBase {
|
|
|
110
118
|
}
|
|
111
119
|
const command = this.resolvedCommand !== '' ? this.resolvedCommand : this.buildCommandTemplate();
|
|
112
120
|
return new fix_hint_1.FixHint('That command builds the WHOLE monorepo. Build only what your change affects.', 'Build the affected projects, or one project, or one spec file — never the workspace:\n' +
|
|
113
|
-
`
|
|
121
|
+
` pnpm wp-build # the gate's own build; runs: ${command}\n` +
|
|
114
122
|
' pnpm nx run <project>:ci # one project\n' +
|
|
115
123
|
' pnpm exec vitest run <path> # one suite\n' +
|
|
116
124
|
'Turn this guard off for this machine by setting "experimental": ' +
|
|
@@ -123,9 +131,9 @@ class WholeRepoBuildGuardRule extends rule_base_1.BashRuleBase {
|
|
|
123
131
|
// whatever command happens to be running, because a broken opt-in file is not a build question.
|
|
124
132
|
if (home instanceof Error)
|
|
125
133
|
return this.blockOnBrokenHomeConfig(ctx, home);
|
|
126
|
-
// THE
|
|
127
|
-
//
|
|
128
|
-
// "this guard does not exist".
|
|
134
|
+
// THE OPT-OUT. True for every machine that has not said otherwise, including every machine with
|
|
135
|
+
// no `~/.webpieces/config.json` at all. Opting out is silent on purpose: no block, no log, no
|
|
136
|
+
// file touched — `false` must be indistinguishable from "this guard does not exist".
|
|
129
137
|
this.homeConfigError = '';
|
|
130
138
|
if (!home.wholeRepoBuildGuard)
|
|
131
139
|
return [];
|
|
@@ -141,8 +149,8 @@ class WholeRepoBuildGuardRule extends rule_base_1.BashRuleBase {
|
|
|
141
149
|
}
|
|
142
150
|
/**
|
|
143
151
|
* The home config, or the Error explaining why it is unusable. NOT a boolean: "absent" is already
|
|
144
|
-
* folded into the returned HomeConfig (
|
|
145
|
-
* distinguish is "present and wrong", which must be reported rather than swallowed.
|
|
152
|
+
* folded into the returned HomeConfig (each key's default — for this guard, ON), so the only thing
|
|
153
|
+
* left to distinguish is "present and wrong", which must be reported rather than swallowed.
|
|
146
154
|
*/
|
|
147
155
|
loadHome() {
|
|
148
156
|
// webpieces-disable no-unmanaged-exceptions -- chokepoint: a rejected home config is converted
|
|
@@ -176,8 +184,11 @@ class WholeRepoBuildGuardRule extends rule_base_1.BashRuleBase {
|
|
|
176
184
|
+ 'log file, and names that file if it fails. Read the file; do not rebuild.\n'
|
|
177
185
|
+ 'Need a check before then: pnpm exec vitest run <path>.';
|
|
178
186
|
}
|
|
187
|
+
// `pnpm wp-build` FIRST, with the command it resolves to shown beside it. One thing to type, and
|
|
188
|
+
// it runs `commands.pr-gate.buildCommand` through the gate's own resolver — so this refusal can
|
|
189
|
+
// never teach a command the gate does not run, however the project reconfigures it.
|
|
179
190
|
return 'Blocked: that builds the WHOLE monorepo. Build only what your change affects:\n\n'
|
|
180
|
-
+ ` ${this.resolvedCommand}\n\n`
|
|
191
|
+
+ ` pnpm wp-build # runs: ${this.resolvedCommand}\n\n`
|
|
181
192
|
+ 'Narrower still: pnpm nx run <project>:ci, or pnpm exec vitest run <path>.';
|
|
182
193
|
}
|
|
183
194
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whole-repo-build-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/whole-repo-build-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport { DEFAULT_BUILD_COMMAND, HomeConfig, HomeConfigService, InformAiError } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase, EmptyRuleConfig } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2_UNROWED } from '../decision-log';\nimport { CommandScanner } from '../command-scan';\nimport { WholeRepoBuildScan, WholeRepoBuildHit } from './whole-repo-build-scan';\n\n/**\n * Blocks a Bash command that would build or test the WHOLE monorepo, and hands back the narrow\n * command to run instead. Which shapes count is `WholeRepoBuildScan`'s job; this rule owns the\n * decision, the log line and the two refusal messages.\n *\n * ─── Why: SCOPE and AGREEMENT, not a speed claim ───────────────────────────────────────────────────\n * `nx affected --target=ci --base=<fork point>` is the command the PR gate itself runs\n * (`commands.pr-gate.buildCommand`), so a green local result is evidence about the gate. A whole-repo\n * build is a different, wider command whose green says nothing extra — it just also compiles projects\n * the change cannot reach.\n *\n * It is NOT automatically faster, and this guard deliberately does not claim it is. Measured on a\n * `core-util` change, `affected` selected the IDENTICAL 20 projects / 104 tasks as the whole-repo\n * build: a package at the BASE of the dependency graph prunes nothing. The pruning win is real for\n * LEAF projects and absent for base ones. The long builds people blamed on scope were caused by a cold\n * nx cache and by CPU contention between agents running full sweeps at once (measured: ~3.2x total\n * test time under contention) — neither of which a narrower target list fixes on its own.\n *\n * So what this guard buys is the scope being right by default. Building the world is never the\n * correct inner-loop move in a monorepo; the correct one has existed all along, and nothing stopped\n * the wide one.\n *\n * ─── The message is READ FROM CONFIG, and it is RESOLVED ───────────────────────────────────────────\n * The replacement command comes from `commands.pr-gate.buildCommand` (injected into this guard's\n * config by load-config), so the refusal follows the project when the gate command changes. The `$(…)`\n * in it is EXPANDED before printing: handing an agent `--base=$(git merge-base origin/main HEAD)` is\n * handing it a template, and a template pasted where no shell expands it produces a confusing failure\n * that reads like the guard's advice was wrong. Only `$(git …)` is expanded, and only read-only git;\n * anything else is left verbatim.\n *\n * ─── EXPERIMENTAL: the ONLY switch is ~/.webpieces/config.json ─────────────────────────────────────\n * `experimental.whole-repo-build-guard` (a boolean) decides whether this guard blocks anything at all.\n * There is NO webpieces.config.json entry — deliberately, and the reason is a live incident: this guard\n * first shipped as an ordinary validated guard with `mode: 'ON'` by default AND a required entry under\n * `hookGuards`, so every consumer that upgraded hit fault Y — EVERY Bash call blocked — for a feature\n * they had never asked for. An experimental feature is opted into from a machine-local file whose absent\n * state is byte-for-byte the old behaviour, or it is not experimental.\n *\n * Four states, and only four:\n * - the file does not exist (essentially every consumer) → this guard is INERT. It blocks nothing and\n * logs nothing, for every command, including the ones it would otherwise refuse.\n * - the file exists and defines the key → the key's value decides.\n * - the file exists but does NOT define the key → the guard is OFF, exactly as if there were no file.\n * Every key in that file is optional, because it is MACHINE-GLOBAL and the repos on one machine pin\n * different webpieces releases — a required key there is unsatisfiable (see home-config.ts). The\n * default can only fail towards \"never opted in\", which is the safe direction.\n * - the file exists and is unparseable, or a key it DOES define has the wrong type → HARD FAILURE\n * naming the edit. That is a file somebody wrote wrongly, not one written for another release.\n * Editing that file is an unconditional PASS in the guards, so the block is always self-curable.\n *\n * ─── Two messages, chosen by a DIFFERENT key ───────────────────────────────────────────────────────\n * `experimental.buildGateLogCapture` is a separate feature (the pr-gate captures its build's full output\n * to a log file and hands the agent that path instead of a rebuild instruction) and it is NOT this\n * guard's switch. It only picks WHICH refusal is printed once the guard is on: with capture ON the right\n * advice is not \"build smaller\", it is \"do not build; stage ② already builds and you can READ the\n * result\". `HomeConfigService` is the one reader of both keys.\n *\n * ─── Humans are not affected, by construction ──────────────────────────────────────────────────────\n * This is a PreToolUse hook. It sees the AI's Bash tool calls and nothing else — a human typing\n * `pnpm run build-all` in their own terminal never reaches a hook, and the `build-all` script itself is\n * deliberately left in package.json for exactly that reason. The guard is about what the AI does in a\n * loop, not about the command being wrong for a person who chooses to run it once.\n */\nexport class WholeRepoBuildGuardRule extends BashRuleBase<EmptyRuleConfig> {\n /**\n * `affectedBuildCommand` is the project's gate command (`commands.pr-gate.buildCommand`), handed in\n * by the runner off the loaded config. It is a CONSTRUCTOR ARGUMENT rather than a config field\n * because this guard has no config entry to read one from — and that is the point.\n */\n constructor(private readonly affectedBuildCommand: string) {\n // configKey === name and is DELIBERATELY not a real key: this guard has no\n // webpieces.config.json entry at all (see RETIRED_CONFIG_KEYS), and it is loaded outside the\n // config-driven set so the fault-Y sync check never sees it. Naming it here keeps the\n // AbstractRule contract honest without putting the string in HOOK_GUARD_NAMES.\n super(new EmptyRuleConfig(), 'whole-repo-build-guard', 'whole-repo-build-guard');\n }\n\n private readonly scanner = new CommandScanner();\n private readonly homeConfig = new HomeConfigService();\n\n /** Set by check() when the home config is unreadable, so the fix hint names the same repair. */\n private homeConfigError = '';\n\n readonly description =\n 'Block a whole-monorepo build (build-all, an unnarrowed nx run-many, nx affected with no ' +\n '--base, a bare vitest run) and name the affected-scoped command to run instead.';\n\n /**\n * The command this rule last printed, so the fix hint and the violation message are one string and\n * cannot disagree. Empty until check() runs (fixHint is also read without it), at which point the\n * getter falls back to the configured TEMPLATE — never to a second literal, which is exactly the\n * drift this guard's own docstring says a duplicated command string causes.\n */\n private resolvedCommand = '';\n\n get fixHint(): FixHint {\n if (this.homeConfigError !== '') {\n return new FixHint(\n '~/.webpieces/config.json exists but cannot be read.',\n 'Edit ~/.webpieces/config.json (editing it is always permitted, including right now), or ' +\n 'delete it outright — with no such file every webpieces command behaves exactly as it ' +\n 'does by default.',\n );\n }\n const command = this.resolvedCommand !== '' ? this.resolvedCommand : this.buildCommandTemplate();\n return new FixHint(\n 'That command builds the WHOLE monorepo. Build only what your change affects.',\n 'Build the affected projects, or one project, or one spec file — never the workspace:\\n' +\n ` ${command} # the gate's own build\\n` +\n ' pnpm nx run <project>:ci # one project\\n' +\n ' pnpm exec vitest run <path> # one suite\\n' +\n 'Turn this guard off for this machine by setting \"experimental\": ' +\n '{ \"whole-repo-build-guard\": false } in ~/.webpieces/config.json (there is no ' +\n 'webpieces.config.json entry for it).',\n );\n }\n\n check(ctx: BashContext): readonly Violation[] {\n const home = this.loadHome();\n // A file someone deliberately created must be correct — see the class docstring. Reported for\n // whatever command happens to be running, because a broken opt-in file is not a build question.\n if (home instanceof Error) return this.blockOnBrokenHomeConfig(ctx, home);\n\n // THE EXPERIMENTAL GATE, and the state of essentially every consumer. Silent on purpose: no\n // block, no log, no file touched — \"no ~/.webpieces/config.json\" must be indistinguishable from\n // \"this guard does not exist\".\n this.homeConfigError = '';\n if (!home.wholeRepoBuildGuard) return [];\n\n // Blocklist-shaped, so match on commandCode: stripping heredocs and quoted prose can only ever\n // block LESS, and this repo's own commit messages are full of the command names above.\n const hit = new WholeRepoBuildScan(this.scanner, ctx.effectiveCwd === ctx.workspaceRoot)\n .firstHit(ctx.commandCode);\n if (hit === null) return this.allow(ctx, 'not-a-whole-repo-build');\n\n // Resolved ONCE, here, and read by both the violation message and the fix hint.\n this.resolvedCommand = this.resolvedBuildCommand(ctx.workspaceRoot);\n return this.block(ctx, hit, this.message(home));\n }\n\n /**\n * The home config, or the Error explaining why it is unusable. NOT a boolean: \"absent\" is already\n * folded into the returned HomeConfig (all-defaults, guard off), so the only thing left to\n * distinguish is \"present and wrong\", which must be reported rather than swallowed.\n */\n private loadHome(): HomeConfig | Error {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a rejected home config is converted\n // into this guard's own block, which carries the loader's fix instruction verbatim\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return this.homeConfig.load();\n } catch (err: unknown) {\n const error = toError(err);\n return error;\n }\n }\n\n private blockOnBrokenHomeConfig(ctx: BashContext, error: Error): readonly Violation[] {\n this.homeConfigError = error.message;\n this.logDecision(ctx, 'BLOCK_AI_CURE', 'home-config-invalid');\n return [new V(1, this.truncate(ctx.command), this.brokenHomeConfigMessage(error))];\n }\n\n private brokenHomeConfigMessage(error: Error): string {\n const detail = error instanceof InformAiError\n ? error.message\n : `~/.webpieces/config.json could not be read: ${error.message}`;\n return `Blocked: ~/.webpieces/config.json is present but unusable.\\n\\n${detail}`;\n }\n\n // The whole refusal. Short on purpose: it is read mid-task by an agent that needs the ONE command\n // to run next, and a guard message long enough to skim is a guard message that gets skimmed.\n private message(home: HomeConfig): string {\n if (home.buildGateLogCapture) {\n return 'Blocked: that builds the WHOLE monorepo — and you should not be building at all.\\n'\n + '`pnpm wp-review-upsert-pr` (stage ②) runs the build for you, captures the full output to a\\n'\n + 'log file, and names that file if it fails. Read the file; do not rebuild.\\n'\n + 'Need a check before then: pnpm exec vitest run <path>.';\n }\n return 'Blocked: that builds the WHOLE monorepo. Build only what your change affects:\\n\\n'\n + ` ${this.resolvedCommand}\\n\\n`\n + 'Narrower still: pnpm nx run <project>:ci, or pnpm exec vitest run <path>.';\n }\n\n /**\n * The project's build command, unexpanded. ONE source: `commands.pr-gate.buildCommand`, handed to\n * the constructor by the runner, falling back to the same DEFAULT_BUILD_COMMAND the gate itself\n * falls back to. This guard never spells a build command of its own — a second copy is how a refusal\n * starts teaching a command the gate does not run.\n */\n private buildCommandTemplate(): string {\n return this.affectedBuildCommand.trim() === '' ? DEFAULT_BUILD_COMMAND : this.affectedBuildCommand;\n }\n\n /**\n * The configured build command with its `$(git …)` substitutions expanded, so what is printed is\n * runnable as-is. Expansion is limited to git, and any failure leaves the template untouched — a\n * guard may degrade its own message, never fail the tool call it is judging.\n */\n private resolvedBuildCommand(workspaceRoot: string): string {\n const template = this.buildCommandTemplate();\n return template.replace(/\\$\\(([^()]*)\\)/g, (match: string, inner: string): string => {\n const trimmed = inner.trim();\n if (!trimmed.startsWith('git ')) return match;\n const output = this.capture(trimmed, workspaceRoot);\n return output === null ? match : output;\n });\n }\n\n private capture(command: string, workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync(command, {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private allow(ctx: BashContext, reason: string): readonly Violation[] {\n this.logDecision(ctx, 'ALLOW', reason);\n return [];\n }\n\n private block(ctx: BashContext, hit: WholeRepoBuildHit, message: string): readonly Violation[] {\n // BLOCK_AI_CURE, not BLOCK_HUMAN: the cure is one command the agent runs itself, and the\n // refusal hands it over already resolved.\n this.logDecision(ctx, 'BLOCK_AI_CURE', hit.shape);\n return [new V(1, this.truncate(ctx.command), message)];\n }\n\n private truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n }\n\n private logDecision(ctx: BashContext, verdict: Verdict, reason: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('whole-repo-build-guard', 'Bash', ctx.command, '-', verdict, reason, '-', L0_FAULT_NONE, MATRIX_L2_UNROWED),\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"whole-repo-build-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/whole-repo-build-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport { DEFAULT_BUILD_COMMAND, HomeConfig, HomeConfigService, InformAiError } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase, EmptyRuleConfig } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2_UNROWED } from '../decision-log';\nimport { CommandScanner } from '../command-scan';\nimport { WholeRepoBuildScan, WholeRepoBuildHit } from './whole-repo-build-scan';\n\n/**\n * Blocks a Bash command that would build or test the WHOLE monorepo, and hands back the narrow\n * command to run instead. Which shapes count is `WholeRepoBuildScan`'s job; this rule owns the\n * decision, the log line and the two refusal messages.\n *\n * ─── Why: SCOPE and AGREEMENT, not a speed claim ───────────────────────────────────────────────────\n * `nx affected --target=ci --base=<fork point>` is the command the PR gate itself runs\n * (`commands.pr-gate.buildCommand`), so a green local result is evidence about the gate. A whole-repo\n * build is a different, wider command whose green says nothing extra — it just also compiles projects\n * the change cannot reach.\n *\n * It is NOT automatically faster, and this guard deliberately does not claim it is. Measured on a\n * `core-util` change, `affected` selected the IDENTICAL 20 projects / 104 tasks as the whole-repo\n * build: a package at the BASE of the dependency graph prunes nothing. The pruning win is real for\n * LEAF projects and absent for base ones. The long builds people blamed on scope were caused by a cold\n * nx cache and by CPU contention between agents running full sweeps at once (measured: ~3.2x total\n * test time under contention) — neither of which a narrower target list fixes on its own.\n *\n * So what this guard buys is the scope being right by default. Building the world is never the\n * correct inner-loop move in a monorepo; the correct one has existed all along, and nothing stopped\n * the wide one.\n *\n * ─── The message is READ FROM CONFIG, and it is RESOLVED ───────────────────────────────────────────\n * The replacement command comes from `commands.pr-gate.buildCommand` (injected into this guard's\n * config by load-config), so the refusal follows the project when the gate command changes. The `$(…)`\n * in it is EXPANDED before printing: handing an agent `--base=$(git merge-base origin/main HEAD)` is\n * handing it a template, and a template pasted where no shell expands it produces a confusing failure\n * that reads like the guard's advice was wrong. Only `$(git …)` is expanded, and only read-only git;\n * anything else is left verbatim.\n *\n * ─── ON BY DEFAULT, with ONE machine-local opt-out ─────────────────────────────────────────────────\n * `experimental.whole-repo-build-guard` in `~/.webpieces/config.json` is this guard's only switch, and\n * it is an OPT-OUT: absent means ON (`WHOLE_REPO_BUILD_GUARD_DEFAULT`), `false` turns it off for this\n * machine. Nothing has to be created, added or edited anywhere to be in the default state.\n *\n * It shipped OFF-by-default first, and that is why it never did its job. A guard nobody opts into never\n * fires: a sibling repo's `ci:local` verify chain ran `prettier --check .`, `wp-ci` and\n * `nx affected -t test` with NO `--base` — three whole-world passes on every inner loop — for months,\n * while the correct scoped command sat unused in `commands.pr-gate.buildCommand`.\n *\n * There is still NO webpieces.config.json entry, and the reason is a live incident that is often\n * misread as an argument against defaulting ON. This guard first shipped with `mode: 'ON'` by default\n * AND a REQUIRED entry under `hookGuards`, and every consumer that upgraded hit fault Y — EVERY Bash\n * call blocked — for a feature they had never asked for. The fault was the REQUIRED KEY: the failure\n * was at config LOAD, before any command was judged, and the cure was \"edit a file to get your shell\n * back\". Defaulting ON from an OPTIONAL machine-local file is the opposite shape. The only thing that\n * can be refused here is a command that genuinely builds the world, and the refusal hands over the\n * scoped command that replaces it.\n *\n * Three states, and only three:\n * - the file does not exist (essentially every machine) → the guard is ON.\n * - the file exists → the key's value decides when it is named, and ON when it is not. Every key in\n * that file is optional, because it is MACHINE-GLOBAL and the repos on one machine pin different\n * webpieces releases — a required key there is unsatisfiable (see home-config.ts).\n * - the file exists and is unparseable, or a key it DOES define has the wrong type → HARD FAILURE\n * naming the edit. That is a file somebody wrote wrongly, not one written for another release.\n * Editing that file is an unconditional PASS in the guards, so the block is always self-curable.\n *\n * ─── Two messages, chosen by a DIFFERENT key ───────────────────────────────────────────────────────\n * `experimental.buildGateLogCapture` is a separate feature (the pr-gate captures its build's full output\n * to a log file and hands the agent that path instead of a rebuild instruction) and it is NOT this\n * guard's switch. It only picks WHICH refusal is printed once the guard is on: with capture ON the right\n * advice is not \"build smaller\", it is \"do not build; stage ② already builds and you can READ the\n * result\". `HomeConfigService` is the one reader of both keys.\n *\n * ─── Humans are not affected, by construction ──────────────────────────────────────────────────────\n * This is a PreToolUse hook. It sees the AI's Bash tool calls and nothing else — a human typing\n * `pnpm run build-all` in their own terminal never reaches a hook, and the `build-all` script itself is\n * deliberately left in package.json for exactly that reason. The guard is about what the AI does in a\n * loop, not about the command being wrong for a person who chooses to run it once.\n */\nexport class WholeRepoBuildGuardRule extends BashRuleBase<EmptyRuleConfig> {\n /**\n * `affectedBuildCommand` is the project's gate command (`commands.pr-gate.buildCommand`), handed in\n * by the runner off the loaded config. It is a CONSTRUCTOR ARGUMENT rather than a config field\n * because this guard has no config entry to read one from — and that is the point.\n */\n constructor(private readonly affectedBuildCommand: string) {\n // configKey === name and is DELIBERATELY not a real key: this guard has no\n // webpieces.config.json entry at all (see RETIRED_CONFIG_KEYS), and it is loaded outside the\n // config-driven set so the fault-Y sync check never sees it. Naming it here keeps the\n // AbstractRule contract honest without putting the string in HOOK_GUARD_NAMES.\n super(new EmptyRuleConfig(), 'whole-repo-build-guard', 'whole-repo-build-guard');\n }\n\n private readonly scanner = new CommandScanner();\n private readonly homeConfig = new HomeConfigService();\n\n /** Set by check() when the home config is unreadable, so the fix hint names the same repair. */\n private homeConfigError = '';\n\n readonly description =\n 'Block a whole-monorepo build (build-all, an unnarrowed nx run-many, nx affected with no ' +\n '--base, a bare vitest run) and name the affected-scoped command to run instead.';\n\n /**\n * The command this rule last printed, so the fix hint and the violation message are one string and\n * cannot disagree. Empty until check() runs (fixHint is also read without it), at which point the\n * getter falls back to the configured TEMPLATE — never to a second literal, which is exactly the\n * drift this guard's own docstring says a duplicated command string causes.\n */\n private resolvedCommand = '';\n\n get fixHint(): FixHint {\n if (this.homeConfigError !== '') {\n return new FixHint(\n '~/.webpieces/config.json exists but cannot be read.',\n 'Edit ~/.webpieces/config.json (editing it is always permitted, including right now), or ' +\n 'delete it outright — with no such file every webpieces command behaves exactly as it ' +\n 'does by default.',\n );\n }\n const command = this.resolvedCommand !== '' ? this.resolvedCommand : this.buildCommandTemplate();\n return new FixHint(\n 'That command builds the WHOLE monorepo. Build only what your change affects.',\n 'Build the affected projects, or one project, or one spec file — never the workspace:\\n' +\n ` pnpm wp-build # the gate's own build; runs: ${command}\\n` +\n ' pnpm nx run <project>:ci # one project\\n' +\n ' pnpm exec vitest run <path> # one suite\\n' +\n 'Turn this guard off for this machine by setting \"experimental\": ' +\n '{ \"whole-repo-build-guard\": false } in ~/.webpieces/config.json (there is no ' +\n 'webpieces.config.json entry for it).',\n );\n }\n\n check(ctx: BashContext): readonly Violation[] {\n const home = this.loadHome();\n // A file someone deliberately created must be correct — see the class docstring. Reported for\n // whatever command happens to be running, because a broken opt-in file is not a build question.\n if (home instanceof Error) return this.blockOnBrokenHomeConfig(ctx, home);\n\n // THE OPT-OUT. True for every machine that has not said otherwise, including every machine with\n // no `~/.webpieces/config.json` at all. Opting out is silent on purpose: no block, no log, no\n // file touched — `false` must be indistinguishable from \"this guard does not exist\".\n this.homeConfigError = '';\n if (!home.wholeRepoBuildGuard) return [];\n\n // Blocklist-shaped, so match on commandCode: stripping heredocs and quoted prose can only ever\n // block LESS, and this repo's own commit messages are full of the command names above.\n const hit = new WholeRepoBuildScan(this.scanner, ctx.effectiveCwd === ctx.workspaceRoot)\n .firstHit(ctx.commandCode);\n if (hit === null) return this.allow(ctx, 'not-a-whole-repo-build');\n\n // Resolved ONCE, here, and read by both the violation message and the fix hint.\n this.resolvedCommand = this.resolvedBuildCommand(ctx.workspaceRoot);\n return this.block(ctx, hit, this.message(home));\n }\n\n /**\n * The home config, or the Error explaining why it is unusable. NOT a boolean: \"absent\" is already\n * folded into the returned HomeConfig (each key's default — for this guard, ON), so the only thing\n * left to distinguish is \"present and wrong\", which must be reported rather than swallowed.\n */\n private loadHome(): HomeConfig | Error {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a rejected home config is converted\n // into this guard's own block, which carries the loader's fix instruction verbatim\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return this.homeConfig.load();\n } catch (err: unknown) {\n const error = toError(err);\n return error;\n }\n }\n\n private blockOnBrokenHomeConfig(ctx: BashContext, error: Error): readonly Violation[] {\n this.homeConfigError = error.message;\n this.logDecision(ctx, 'BLOCK_AI_CURE', 'home-config-invalid');\n return [new V(1, this.truncate(ctx.command), this.brokenHomeConfigMessage(error))];\n }\n\n private brokenHomeConfigMessage(error: Error): string {\n const detail = error instanceof InformAiError\n ? error.message\n : `~/.webpieces/config.json could not be read: ${error.message}`;\n return `Blocked: ~/.webpieces/config.json is present but unusable.\\n\\n${detail}`;\n }\n\n // The whole refusal. Short on purpose: it is read mid-task by an agent that needs the ONE command\n // to run next, and a guard message long enough to skim is a guard message that gets skimmed.\n private message(home: HomeConfig): string {\n if (home.buildGateLogCapture) {\n return 'Blocked: that builds the WHOLE monorepo — and you should not be building at all.\\n'\n + '`pnpm wp-review-upsert-pr` (stage ②) runs the build for you, captures the full output to a\\n'\n + 'log file, and names that file if it fails. Read the file; do not rebuild.\\n'\n + 'Need a check before then: pnpm exec vitest run <path>.';\n }\n // `pnpm wp-build` FIRST, with the command it resolves to shown beside it. One thing to type, and\n // it runs `commands.pr-gate.buildCommand` through the gate's own resolver — so this refusal can\n // never teach a command the gate does not run, however the project reconfigures it.\n return 'Blocked: that builds the WHOLE monorepo. Build only what your change affects:\\n\\n'\n + ` pnpm wp-build # runs: ${this.resolvedCommand}\\n\\n`\n + 'Narrower still: pnpm nx run <project>:ci, or pnpm exec vitest run <path>.';\n }\n\n /**\n * The project's build command, unexpanded. ONE source: `commands.pr-gate.buildCommand`, handed to\n * the constructor by the runner, falling back to the same DEFAULT_BUILD_COMMAND the gate itself\n * falls back to. This guard never spells a build command of its own — a second copy is how a refusal\n * starts teaching a command the gate does not run.\n */\n private buildCommandTemplate(): string {\n return this.affectedBuildCommand.trim() === '' ? DEFAULT_BUILD_COMMAND : this.affectedBuildCommand;\n }\n\n /**\n * The configured build command with its `$(git …)` substitutions expanded, so what is printed is\n * runnable as-is. Expansion is limited to git, and any failure leaves the template untouched — a\n * guard may degrade its own message, never fail the tool call it is judging.\n */\n private resolvedBuildCommand(workspaceRoot: string): string {\n const template = this.buildCommandTemplate();\n return template.replace(/\\$\\(([^()]*)\\)/g, (match: string, inner: string): string => {\n const trimmed = inner.trim();\n if (!trimmed.startsWith('git ')) return match;\n const output = this.capture(trimmed, workspaceRoot);\n return output === null ? match : output;\n });\n }\n\n private capture(command: string, workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync(command, {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private allow(ctx: BashContext, reason: string): readonly Violation[] {\n this.logDecision(ctx, 'ALLOW', reason);\n return [];\n }\n\n private block(ctx: BashContext, hit: WholeRepoBuildHit, message: string): readonly Violation[] {\n // BLOCK_AI_CURE, not BLOCK_HUMAN: the cure is one command the agent runs itself, and the\n // refusal hands it over already resolved.\n this.logDecision(ctx, 'BLOCK_AI_CURE', hit.shape);\n return [new V(1, this.truncate(ctx.command), message)];\n }\n\n private truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n }\n\n private logDecision(ctx: BashContext, verdict: Verdict, reason: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('whole-repo-build-guard', 'Bash', ctx.command, '-', verdict, reason, '-', L0_FAULT_NONE, MATRIX_L2_UNROWED),\n );\n }\n}\n"]}
|
package/src/core/runner.js
CHANGED
|
@@ -369,8 +369,8 @@ function misplacedCdBlock(command, tree) {
|
|
|
369
369
|
* webpieces.config.json entry at all, so passing them through checkConfigSync would make every
|
|
370
370
|
* consumer's next Bash call a fault-Y block ("this rule has no entry") for a feature nobody opted into
|
|
371
371
|
* — which is exactly what whole-repo-build-guard did on its first release. Each one decides for itself
|
|
372
|
-
* whether it acts: whole-repo-build-guard reads ~/.webpieces/config.json
|
|
373
|
-
*
|
|
372
|
+
* whether it acts: whole-repo-build-guard reads ~/.webpieces/config.json for a machine-local OPT-OUT
|
|
373
|
+
* (absent means ON), commit-message-substitution-guard acts unconditionally (see loadKeylessBashRules).
|
|
374
374
|
*
|
|
375
375
|
* They still honour excludePaths, and they do not run in `rules` mode (code-style-only hook).
|
|
376
376
|
*/
|
package/src/core/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, ExcludePaths, isHookGuard, HomeConfigService, RepoRootFinder, seedEntryForRule, CONFIG_FILENAME, renderRuleFailForAi } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { VersionSyncGuard } from './version-sync';\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\nimport { gitFromSubdirBlock } from './force-to-root';\nimport { loadRules, loadMatchRules, loadKeylessBashRules, globMatches, GuardHintCommands } from './load-rules';\nimport { missingDirectoryBlock } from './missing-directory';\nimport { MatchRule } from './rules/match-rule';\nimport { branchStateHangTimeout, maybeRefreshMainSync } from './main-sync-timeout';\nimport { logGuardDecision, logL1Decision, GuardDecision, branchForLog, MatrixRef, Verdict, MATRIX_L0_ALLOW, MATRIX_L2_UNROWED } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_ALLOW_JS, L0_CURE_ALLOW_JS } from '../bin/shim';\nimport { L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC, L0_FAULT_NONE } from './l0-fault-codes';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport { L1Classification, firstMatchingL1Row, L1_PRESTAGE_ROW } from './l1-rows';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\n// isHookGuard is asked about the rule's CONFIG KEY, never its name. Since the collapse those differ\n// for every class behind a policy key — `feature-branch-guard` is a rule NAME whose key is\n// `branch-state-guard` — and asking about the name would classify all eight collapsed guards as\n// code-style rules, i.e. run them in the wrong hook and never in the guards hook at all.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.configKey));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.configKey));\n}\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\n// This is L1's FILTER (not a table row) — see guards/L1-location.md.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n if (ex.paths.some((p: string): boolean => globMatches(p, relativePath))) return [];\n return rules;\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// The resolved gated-command strings the PR-lifecycle guards print, straight off the loaded\n// `commands.guardHints`. Handed to the rules at construction rather than injected into their config\n// entries under guard-name literals — the injection this replaces could miss a rename silently.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction guardHintsOf(loaded: LoadedConfig): GuardHintCommands {\n return new GuardHintCommands(loaded.commands.upsertPr, loaded.commands.mergeComplete);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n//\n// `command` is '' on the file-tool path (there is no command to judge) and the Bash command otherwise:\n// a call on the L0 allowlist survives this block, which is how read-only orientation stays available\n// under C. You have to be able to see which tree you are standing in before you can decide what to\n// write into the config. Returns null when the call is allowed through.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string, command: string = ''): BlockedResult | null {\n if (command !== '' && l0FaultAllows(command)) return null;\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n // Stamped with its L0 letter so the block is greppable as fault C wherever it is recorded — the\n // adapter carries it to the audit line rather than re-deriving it from the report text.\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)), L0_FAULT_CONFIG_MISSING);\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n // …and the same unconditional PASS for the OPTIONAL machine-local `~/.webpieces/config.json`, for the\n // identical reason. That file is strictly validated when it exists (HomeConfigService), so a bad key\n // in it makes a `wp-*` command fail with an instruction to edit it — and a guard that then blocked\n // that edit would wedge the agent inside the failure. webpieces.config.json is immune to exactly this\n // because of the pass above; the home config gets the same immunity rather than a different answer.\n // Matches the absolute, `~/`, `$HOME/` and `${HOME}/` spellings alike (see isHomeConfigPath).\n if (new HomeConfigService().isHomeConfigPath(input.filePath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\n// SINGLETON, deliberately: WebpiecesVersions memoizes per root, and the same two roots are asked for on\n// every Bash call. A fresh instance per call would spawn `git worktree list` and re-read two manifests\n// on the hook's blocking path each time.\nconst VERSION_SYNC = new VersionSyncGuard();\n\n// There is deliberately NO agent parameter. It existed only for CoordinatorWorktreeGuard, which asked\n// WHO was calling; that guard is gone and its replacement asks WHICH TREE the command acts on. Agent\n// identity was measured untrustworthy for that question anyway — a worktree-isolated agent auto-reaped\n// at a turn boundary silently resumes with its cwd on the primary clone — so a guard must never infer\n// a tree from who is asking.\n// webpieces-disable no-function-outside-class -- sibling of run()/runRead() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n return runBashInternal(command, cwd, mode);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const relativePath = path.relative(workspaceRoot, filePath);\n const all = loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded));\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n relativePath,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n//\n// It tests the CURE subset (L0_CURE_ALLOW_JS), not the whole list, and the distinction matters here\n// and nowhere else: this call site runs BEFORE any fault has been established, so whatever it accepts\n// is waved past the L1 guards on a perfectly healthy repo too. A repair command has to be waved past —\n// it runs before the config can be loaded at all. Read-only ORIENTATION does not: it fixes nothing, and\n// letting `git status` through here would delete force-to-root as a side effect of adding `pwd`. Under\n// an actual fault, l0FaultAllows() below consults the WHOLE list.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_CURE_ALLOW_JS.test(command.trim());\n}\n\n// The FULL L0 list, asked only where this file has actually detected an L0 fault (C = config missing,\n// Y = config out of sync). Those two are the JS-side faults; D/X/K are decided in the shim, which greps\n// the same union. Without this the orientation entry would be honoured under four faults and silently\n// dropped under the other two — precisely the per-fault carve-out the L0 rewrite removed.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l0FaultAllows(command: string): boolean {\n return L0_ALLOW_JS.test(command.trim());\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)', '-', L0_FAULT_NONE, MATRIX_L0_ALLOW));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\n// Version skew: this worktree pins a different @webpieces than the MAIN tree that governs it. The guard\n// hooks are absolute, so the main tree's binary judges every tree — which is fine until the two trees\n// disagree about which release that should be. L1 row 8; guards/L1-location.md carries the table.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction versionSkewBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const report = VERSION_SYNC.block(command, tree);\n return report === null ? null : new BlockedResult(report);\n}\n\n// Where the command lands in L1's five dimensions (K/A/R/G/P). The ONE place the runner's view of the\n// world is translated into the matrix's vocabulary — see L1Classification.forEnforcement for why\n// TreeKind 'outside' currently classifies as `p`.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1Classify(command: string, tree: EffectiveTree): L1Classification {\n return L1Classification.forEnforcement(\n tree.kind,\n VERSION_SYNC.skewed(tree),\n new ReadOnlyInspectionScan().isReadOnlyInspection(command),\n isGitOrGhCommand(command),\n path.resolve(tree.effectiveCwd) === path.resolve(tree.root),\n );\n}\n\n// The structural L1 blocks, in order: the misplaced-`cd` PRE-STAGE, then version skew\n// (row 8), then force-to-root (row 5). None is a configurable rule — they are decided from the command\n// text, the resolved tree and the caller, so they run as one step here rather than as three\n// near-identical stanzas in runBashInternal.\n//\n// For the two TREE-BASED blocks the ORDER and the CHOICE are not written here: they come from L1_ROWS\n// (l1-rows.ts), the same array guards/L1-location.md is rendered from. Classify, take the first\n// matching row, dispatch on its blockId — so a row deleted from the array is a block that stops firing,\n// and the doc cannot describe a table the guard does not consult. The two report builders below still\n// own their own predicates and their deny strings; l1-matrix.spec.ts asserts the row lookup and those\n// predicates agree.\n//\n// misplacedCdBlock is deliberately OUTSIDE that lookup, and runs ahead of it, for the reason its own\n// docblock gives: it decides from command TEXT, before a tree has been resolved, and the other two\n// reason FROM the resolved tree. Classifying it would mean asking L1_ROWS a question whose answer the\n// classification itself depends on. It is the same shape as L2's `bareCheckoutOfMain` pre-stage.\n//\n// It is therefore ROW 0 — \"pre-stage, decided from command text\" — rather than a row among 1-6. That\n// keeps it IN the table (the drift this table exists to prevent) without pretending it is classified\n// over the same five dimensions, which is the thing that cannot be true. `L1_PRESTAGE_ROW` is the\n// number the doc prints and the number `row=` logs, so the two still join.\n//\n// THIS FUNCTION IS THE ONE PLACE L1 REPORTS. It is the only scope holding the classification, the\n// matched row, the resolved tree and the agent at once — so logging here, BEFORE the early return,\n// is what finally records the outcomes that were previously invisible: the exempt row and the three\n// hand-downs wrote nothing at all, which is why \"L1 had no objection\" could not be observed and\n// \"show me every L1 decision\" had no answer. The three block helpers no longer log for themselves.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1LocationBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const misplacedCd = misplacedCdBlock(command, tree);\n if (misplacedCd !== null) {\n logL1(tree, command, 'BLOCK_AI_CURE', L1_PRESTAGE_ROW, 'cd-must-be-first', 'cd not leading/literal');\n return misplacedCd;\n }\n\n const row = firstMatchingL1Row(l1Classify(command, tree));\n const rowNum = String(row.num);\n if (row.blockId === null) {\n // ALLOW_EXEMPT stops here; ALLOW means \"no objection, handed down to L2\". Recording the\n // difference is the point — see Verdict. Neither is a claim that the call actually ran: the\n // other parallel hook may still have denied it.\n logL1(tree, command, row.action.kind === 'exempt' ? 'ALLOW_EXEMPT' : 'ALLOW', rowNum, '-', row.why);\n return null;\n }\n logL1(tree, command, 'BLOCK_AI_CURE', rowNum, row.blockId, row.why);\n if (row.blockId === 'missing-directory') return missingDirectoryBlock(command, tree);\n if (row.blockId === 'trinary-version-skew') return versionSkewBlock(command, tree);\n return gitFromSubdirBlock(command, tree, isGitOrGhCommand(command));\n}\n\n// One L1 line, into L1's OWN stream. `row` is the number the generated doc prints, so a reader who\n// sees `row=5` can open guards/L1-location.md at row 5 and read the state, the cure and the use cases\n// that row is supposed to cover.\n// eslint-disable-next-line @typescript-eslint/max-params -- the six fields of one log line, and a class here would break this file's shape\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions\nfunction logL1(tree: EffectiveTree, command: string, verdict: Verdict, row: string, rule: string, why: string): void {\n logL1Decision(tree.root, new GuardDecision(\n rule, 'Bash', command, branchForLog(tree.root), verdict, why, '-', L0_FAULT_NONE, new MatrixRef('L1', row),\n ));\n}\n\n/**\n * ONE legal shape for relocating a command: `cd <literal path> && <work>`. Anything else — a `cd` after\n * another command or after a `VAR=…` assignment, or a `cd \"$DIR\"` the guard cannot expand — is refused\n * here instead of being silently judged from the shell cwd.\n *\n * This changes no verdict: every command it refuses was ALREADY judged from the shell cwd, so nothing\n * it blocks was previously being allowed on the strength of its `cd`. What changes is that the agent\n * finds out. Two PRs of increasingly precise near-miss wording (#596, #597) still left the fact in a\n * paragraph appended to an unrelated block; the rule is simpler stated as a rule.\n *\n * FIRST in the L1 chain deliberately. version skew and force-to-root both reason from the\n * resolved tree, and if the `cd` did not resolve, that tree is not the one the agent thinks they are\n * in — so their remedies would be steering from a location the command does not actually run in.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole runner is module-scope functions\nfunction misplacedCdBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const reason = new EffectiveTreeResolver().misplacedCd(command);\n if (reason === null) return null;\n const report =\n `❌ A \\`cd\\` must come FIRST in the command, with a LITERAL path.\\n` +\n ` Why this one was rejected: ${reason}.\\n` +\n ` \\`cd <literal path> && <work>\\` is the ONE shape that moves where webpieces judges a command.\\n` +\n ` Any other \\`cd\\` cannot be resolved, so the command would be judged against ${tree.root}\\n` +\n ` — not where you think it runs. That used to happen silently; this block is that silence, spoken.\\n` +\n ` Fix Option 1: put the \\`cd\\` first, literal path, SAME command: cd /abs/path && <the rest>\\n` +\n ` Fix Option 2: drop the \\`cd\\` — ${tree.root} is where the command is judged anyway.\\n` +\n ` Fix Option 3: if the SECOND \\`cd\\` was there to point a tool at a directory, use that tool's own\\n` +\n ` directory flag instead — \\`git -C <dir>\\`, \\`tar -C <dir>\\`, \\`npm --prefix <dir>\\`,\\n` +\n ` \\`npm pack --pack-destination <dir>\\`, \\`pnpm --dir <dir>\\`. One \\`cd\\`, same result.\\n` +\n ` Fix Option 4: split it — run the work in one call, the \\`cd\\` in another (a \\`cd\\` alone still\\n` +\n ` moves nothing the guards judge; see EffectiveTree on why cwd cannot be assumed).`;\n return new BlockedResult(report);\n}\n\n/**\n * The KEYLESS bash guards, deliberately kept OUT of the config-driven rule set. They have no\n * webpieces.config.json entry at all, so passing them through checkConfigSync would make every\n * consumer's next Bash call a fault-Y block (\"this rule has no entry\") for a feature nobody opted into\n * — which is exactly what whole-repo-build-guard did on its first release. Each one decides for itself\n * whether it acts: whole-repo-build-guard reads ~/.webpieces/config.json and does nothing at all\n * without it, commit-message-substitution-guard acts unconditionally (see loadKeylessBashRules).\n *\n * They still honour excludePaths, and they do not run in `rules` mode (code-style-only hook).\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction keylessBashRules(loaded: LoadedConfig, mode: HookMode, relativePath: string): readonly Rule[] {\n if (mode === 'rules') return [];\n return filterByExcludedPaths(\n loadKeylessBashRules(loaded.prGate.buildCommand), relativePath, loaded.excludePaths,\n );\n}\n\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nfunction runBashInternal(command: string, cwd: string, mode: HookMode): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return configMissingBlock(cwd, command); // fault C\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW_EXEMPT', 'foreign git repo (out of scope)', '-', L0_FAULT_NONE, new MatrixRef('L1', '1')));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const relativeCwd = path.relative(workspaceRoot, tree.effectiveCwd);\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), mode), relativeCwd, loaded.excludePaths,\n );\n const keyless = keylessBashRules(loaded, mode, relativeCwd);\n if (rules.length === 0 && keyless.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig); // fault Y — L0 list wins, as under C\n if (outOfSync) return l0FaultAllows(command) ? null : outOfSync;\n\n const locationBlock = l1LocationBlock(command, tree);\n if (locationBlock) return locationBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root, branchStateHangTimeout(loaded.rulesConfig));\n\n const ctx = buildBashContext(command, tree);\n const groups = runBashRules([...rules, ...keyless], ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK_AI_CURE', 'bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n const report = formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n//\n// The hint MUST state the precondition, not just the remedy. \"cd into it first\" is true only for a\n// LITERAL cd at the FRONT of the same command; `D=…; cd \"$D\"; git push` lands in the identical\n// directory and does NOT relocate the verdict. That used to be silent, then a near-miss paragraph\n// appended here; it is now misplacedCdBlock, which refuses the command outright — so by the time this\n// footer is reached, any `cd` in the command is already known to be leading and literal. The wording\n// still states the shape, because this is where an agent LEARNS it, before writing the next command.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees (${exemptGuards.join(', ')})? `\n + `Put a LITERAL \\`cd\\` at the FRONT of the SAME command — \\`cd /abs/path/to/repo && git push\\` — and git/gh `\n + `run normally there: the webpieces guards do NOT govern them (each is its own repo).`\n + `\\n \\`cd <literal path> && <work>\\` is the ONE shape that moves where a command is judged. A \\`cd\\` `\n + `anywhere else in the line, or a non-literal target like \\`cd \"$DIR\"\\`, is refused outright rather than `\n + `judged from somewhere you did not intend.`;\n}\n\n// The set of CONFIG KEYS explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\n/**\n * Fault Y — a loaded rule whose CONFIG KEY has no entry.\n *\n * Compared on `configKey`, not `name`. Eight of the loaded rules are classes behind two policy keys, so\n * comparing names here would demand entries named `feature-branch-guard`, `pr-merge-guard` and six\n * others — keys the validator then REJECTS as retired. That is the two-enforcement-paths deadlock this\n * repo has already hit once (see the narration in guards.spec.ts), and it blocks every Bash/Write/Edit.\n *\n * De-duplicated on the key too, so one missing policy entry reports ONE paste-ready snippet rather than\n * four copies of the same one under four different headings.\n */\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const seen = new Set<string>();\n const unconfiguredRules = rules.filter((r: Rule): boolean => {\n if (configured.has(r.configKey) || seen.has(r.configKey)) return false;\n seen.add(r.configKey);\n return true;\n });\n if (unconfiguredRules.length === 0) return null;\n\n // ONE action, no menu, no escalation — the config-validation invariant (guards/L0-tooling.md): every\n // config problem cures to \"make the file right\", and editing it is never denied. This message used\n // to tell the agent to interview the human about each rule; agents did not do it, so the block just\n // stalled. Each rule now ships a paste-ready entry at its recommended mode.\n //\n // Note this is the CONFIG-BEHIND-CODE direction. The opposite one — the config names a rule the\n // installed validator has no schema for — is unknownRuleError() in rules-config/validate-config.ts\n // and surfaces in the validation banner, not here.\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n `Add an entry for each rule below to ${CONFIG_FILENAME}. Editing that file is ALWAYS allowed through`,\n 'the guard — including right now, while this block is up — so paste the entries and retry.',\n '',\n 'Each entry below is ready to paste at its recommended mode; adjust the option values if your',\n 'project needs different ones.',\n '',\n `Do NOT delete a rule from ${CONFIG_FILENAME} to silence it — an entry is REQUIRED for every rule,`,\n 'and \"mode\": \"OFF\" is how a rule is turned off.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.configKey} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n // The SAME entry the installer would seed: recommended mode, both hatches, and every other\n // schema-required field — so pasting it satisfies the loader in one pass.\n lines.push(`Entry to add to ${CONFIG_FILENAME}:`);\n lines.push(` \"${rule.configKey}\": ${JSON.stringify(seedEntryForRule(rule.configKey))}`);\n lines.push('');\n }\n\n // Fault Y, stamped for the audit trail — see configMissingBlock for why the producer names it.\n return new BlockedResult(lines.join('\\n'), L0_FAULT_CONFIG_OUT_OF_SYNC);\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixOptions kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and cures. Fold the\n// cures into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value). The \"Fix Option N:\"/\"(preferred)\" labels come from the ONE\n// framework-owned renderer, exactly as report.ts renders a rule's static FixHint — a rule never\n// hand-numbers its own cures.\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n return new Violation(error.line ?? 0, error.snippet ?? '', renderRuleFailForAi(error));\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/runner.ts"],"names":[],"mappings":";;AA4CA,sDAGC;AAMD,4CAEC;AAYD,4CAEC;AAmBD,kBAOC;AAqED,0BAEC;AAmBD,0BA6BC;AA2YD,oCAWC;;AA5mBD,mDAA6B;AAE7B,0DAAoN;AAEpN,mDAAkE;AAClE,iDAAkD;AAClD,qDAAwE;AACxE,mDAAqD;AACrD,6CAA+G;AAC/G,2DAA4D;AAC5D,mDAA+C;AAC/C,2DAAmF;AACnF,iDAAsJ;AACtJ,yCAAqC;AACrC,qCAAoE;AACpE,iEAAgE;AAChE,sCAA4D;AAC5D,qDAAuG;AACvG,2CAAwH;AACxH,uCAAkF;AAClF,mCAIiB;AAEjB,mGAAmG;AACnG,oGAAoG;AACpG,oGAAoG;AACpG,2BAA2B;AAC3B,oGAAoG;AACpG,2FAA2F;AAC3F,gGAAgG;AAChG,yFAAyF;AACzF,SAAS,YAAY,CAAC,KAAsB,EAAE,IAAc;IACxD,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACjC,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAO,EAAW,EAAE,CAAC,IAAA,0BAAW,EAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAO,EAAW,EAAE,CAAC,CAAC,IAAA,0BAAW,EAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,uGAAuG;AACvG,wGAAwG;AACxG,4FAA4F;AAC5F,qEAAqE;AACrE,SAAgB,qBAAqB,CAAC,KAAsB,EAAE,YAAoB,EAAE,EAAgB;IAChG,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,IAAA,wBAAW,EAAC,CAAC,EAAE,YAAY,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACnF,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,mGAAmG;AACnG,mGAAmG;AACnG,qGAAqG;AACrG,qKAAqK;AACrK,SAAgB,gBAAgB,CAAC,OAAe,EAAE,GAAW;IACzD,OAAO,IAAI,sCAAqB,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAClE,CAAC;AAED,4FAA4F;AAC5F,oGAAoG;AACpG,gGAAgG;AAChG,qKAAqK;AACrK,SAAS,YAAY,CAAC,MAAoB;IACtC,OAAO,IAAI,8BAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;AAC1F,CAAC;AAED,gGAAgG;AAChG,MAAM,YAAY,GAAG,4BAA4B,CAAC;AAClD,SAAgB,gBAAgB,CAAC,OAAe;IAC5C,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;AAED,sGAAsG;AACtG,sGAAsG;AACtG,kGAAkG;AAClG,EAAE;AACF,uGAAuG;AACvG,qGAAqG;AACrG,mGAAmG;AACnG,wEAAwE;AACxE,qKAAqK;AACrK,SAAS,kBAAkB,CAAC,GAAW,EAAE,UAAkB,EAAE;IACzD,IAAI,OAAO,KAAK,EAAE,IAAI,aAAa,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACvD,gGAAgG;IAChG,wFAAwF;IACxF,OAAO,IAAI,qBAAa,CAAC,iCAAqB,GAAG,IAAA,8BAAkB,EAAC,IAAA,+BAAmB,EAAC,IAAI,CAAC,CAAC,EAAE,wCAAuB,CAAC,CAAC;AAC7H,CAAC;AAED,SAAgB,GAAG,CACf,QAAkB,EAClB,KAA0B,EAC1B,GAAW,EACX,OAAiB,KAAK;IAEtB,OAAO,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,WAAW,CAChB,QAAkB,EAClB,KAA0B,EAC1B,GAAW,EACX,IAAc;IAEd,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,GAAG,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAE/D,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAEtD,qFAAqF;IACrF,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACnE,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,sGAAsG;IACtG,qGAAqG;IACrG,mGAAmG;IACnG,sGAAsG;IACtG,oGAAoG;IACpG,8FAA8F;IAC9F,IAAI,IAAI,gCAAiB,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,oGAAoG;IACpG,uGAAuG;IACvG,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAA,sBAAS,EAAC,MAAM,CAAC,WAAW,EAAE,aAAa,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,IAAA,2BAAc,EAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAC/H,MAAM,SAAS,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAExC,+FAA+F;IAC/F,kGAAkG;IAClG,yFAAyF;IACzF,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,qBAAqB,CAAC,SAAS,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAClF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpC,kGAAkG;IAClG,mGAAmG;IACnG,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,sBAAS,CAAC,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5G,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,MAAM,QAAQ,GAAG,IAAA,6BAAa,EAAC,QAAQ,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;IAE/D,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC9D,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,CAAC,GAAG,UAAU,EAAE,GAAG,UAAU,CAAC,CAAC;IAEjD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAExC,MAAM,MAAM,GAAG,IAAA,qBAAY,EAAC,YAAY,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO,IAAI,qBAAa,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED,wGAAwG;AACxG,uGAAuG;AACvG,yCAAyC;AACzC,MAAM,YAAY,GAAG,IAAI,+BAAgB,EAAE,CAAC;AAE5C,sGAAsG;AACtG,qGAAqG;AACrG,uGAAuG;AACvG,sGAAsG;AACtG,6BAA6B;AAC7B,8MAA8M;AAC9M,SAAgB,OAAO,CAAC,OAAe,EAAE,GAAW,EAAE,OAAiB,KAAK;IACxE,OAAO,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,+FAA+F;AAC/F,iGAAiG;AACjG,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE9E;;;;;;;;;;;GAWG;AACH,8MAA8M;AAC9M,SAAgB,OAAO,CAAC,QAAgB,EAAE,GAAW,EAAE,OAAiB,KAAK;IACzE,mDAAmD;IACnD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,GAAG,CAAC,CAAC;IACpC,6FAA6F;IAC7F,0BAA0B;IAC1B,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE5C,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAEtD,kGAAkG;IAClG,4FAA4F;IAC5F,IAAI,IAAI,sCAAqB,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEhG,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,IAAA,sBAAS,EAAC,MAAM,CAAC,WAAW,EAAE,aAAa,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAG,qBAAqB,CAC/B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAO,EAAW,EAAE,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAChE,YAAY,EACZ,MAAM,CAAC,YAAY,CACtB,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpC,MAAM,GAAG,GAAG,IAAI,mBAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,OAAO,IAAI,qBAAa,CAAC,IAAA,qBAAY,EAAC,YAAY,EAAE,MAAM,EAAE,qBAAY,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,8FAA8F;AAC9F,gGAAgG;AAChG,kGAAkG;AAClG,iDAAiD;AACjD,EAAE;AACF,gGAAgG;AAChG,kGAAkG;AAClG,iGAAiG;AACjG,sGAAsG;AACtG,iEAAiE;AACjE,EAAE;AACF,oGAAoG;AACpG,sGAAsG;AACtG,uGAAuG;AACvG,wGAAwG;AACxG,uGAAuG;AACvG,kEAAkE;AAClE,qKAAqK;AACrK,SAAS,eAAe,CAAC,OAAe;IACpC,OAAO,uBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,sGAAsG;AACtG,wGAAwG;AACxG,sGAAsG;AACtG,0FAA0F;AAC1F,qKAAqK;AACrK,SAAS,aAAa,CAAC,OAAe;IAClC,OAAO,kBAAW,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,qGAAqG;AACrG,qGAAqG;AACrG,4FAA4F;AAC5F,6FAA6F;AAC7F,qKAAqK;AACrK,SAAS,eAAe,CAAC,OAAe,EAAE,GAAW;IACjD,MAAM,IAAI,GAAG,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACvD,IAAA,+BAAgB,EAAC,IAAI,EAAE,IAAI,4BAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAA,2BAAY,EAAC,IAAI,CAAC,EAAE,OAAO,EAAE,iCAAiC,EAAE,GAAG,EAAE,8BAAa,EAAE,8BAAe,CAAC,CAAC,CAAC;AACzK,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qKAAqK;AACrK,SAAS,2BAA2B,CAAC,OAAe,EAAE,GAAW;IAC7D,yHAAyH;IACzH,IAAI,CAAC;QACD,OAAO,IAAA,8BAAe,EAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,KAAK,YAAY,qBAAa,IAAI,IAAI,6CAAsB,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/F,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC;AAED,wGAAwG;AACxG,sGAAsG;AACtG,kGAAkG;AAClG,2KAA2K;AAC3K,SAAS,gBAAgB,CAAC,OAAe,EAAE,IAAmB;IAC1D,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACjD,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,qBAAa,CAAC,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED,sGAAsG;AACtG,iGAAiG;AACjG,kDAAkD;AAClD,2KAA2K;AAC3K,SAAS,UAAU,CAAC,OAAe,EAAE,IAAmB;IACpD,OAAO,0BAAgB,CAAC,cAAc,CAClC,IAAI,CAAC,IAAI,EACT,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,EACzB,IAAI,6CAAsB,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAC1D,gBAAgB,CAAC,OAAO,CAAC,EACzB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAC9D,CAAC;AACN,CAAC;AAED,sFAAsF;AACtF,uGAAuG;AACvG,4FAA4F;AAC5F,6CAA6C;AAC7C,EAAE;AACF,sGAAsG;AACtG,gGAAgG;AAChG,wGAAwG;AACxG,sGAAsG;AACtG,sGAAsG;AACtG,oBAAoB;AACpB,EAAE;AACF,qGAAqG;AACrG,mGAAmG;AACnG,sGAAsG;AACtG,iGAAiG;AACjG,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,kGAAkG;AAClG,2EAA2E;AAC3E,EAAE;AACF,kGAAkG;AAClG,mGAAmG;AACnG,oGAAoG;AACpG,gGAAgG;AAChG,mGAAmG;AACnG,2KAA2K;AAC3K,SAAS,eAAe,CAAC,OAAe,EAAE,IAAmB;IACzD,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACpD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,yBAAe,EAAE,kBAAkB,EAAE,wBAAwB,CAAC,CAAC;QACrG,OAAO,WAAW,CAAC;IACvB,CAAC;IAED,MAAM,GAAG,GAAG,IAAA,4BAAkB,EAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QACvB,wFAAwF;QACxF,4FAA4F;QAC5F,gDAAgD;QAChD,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACpG,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IACpE,IAAI,GAAG,CAAC,OAAO,KAAK,mBAAmB;QAAE,OAAO,IAAA,yCAAqB,EAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrF,IAAI,GAAG,CAAC,OAAO,KAAK,sBAAsB;QAAE,OAAO,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACnF,OAAO,IAAA,kCAAkB,EAAC,OAAO,EAAE,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,mGAAmG;AACnG,sGAAsG;AACtG,iCAAiC;AACjC,2IAA2I;AAC3I,yHAAyH;AACzH,SAAS,KAAK,CAAC,IAAmB,EAAE,OAAe,EAAE,OAAgB,EAAE,GAAW,EAAE,IAAY,EAAE,GAAW;IACzG,IAAA,4BAAa,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,4BAAa,CACtC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAA,2BAAY,EAAC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,8BAAa,EAAE,IAAI,wBAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAC7G,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,8IAA8I;AAC9I,SAAS,gBAAgB,CAAC,OAAe,EAAE,IAAmB;IAC1D,MAAM,MAAM,GAAG,IAAI,sCAAqB,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAChE,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,MAAM,GACR,mEAAmE;QACnE,iCAAiC,MAAM,KAAK;QAC5C,oGAAoG;QACpG,kFAAkF,IAAI,CAAC,IAAI,IAAI;QAC/F,uGAAuG;QACvG,kGAAkG;QAClG,sCAAsC,IAAI,CAAC,IAAI,2CAA2C;QAC1F,uGAAuG;QACvG,yGAAyG;QACzG,0GAA0G;QAC1G,qGAAqG;QACrG,mGAAmG,CAAC;IACxG,OAAO,IAAI,qBAAa,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;;GASG;AACH,qKAAqK;AACrK,SAAS,gBAAgB,CAAC,MAAoB,EAAE,IAAc,EAAE,YAAoB;IAChF,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO,qBAAqB,CACxB,IAAA,iCAAoB,EAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,CACtF,CAAC;AACN,CAAC;AAED,8MAA8M;AAC9M,SAAS,eAAe,CAAC,OAAe,EAAE,GAAW,EAAE,IAAc;IACjE,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,2BAA2B,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACzD,2FAA2F;IAC3F,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,UAAU;IAEnF,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAEtD,iGAAiG;IACjG,4FAA4F;IAC5F,6FAA6F;IAC7F,wCAAwC;IACxC,MAAM,IAAI,GAAG,IAAI,sCAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;IAE9E,uFAAuF;IACvF,gGAAgG;IAChG,kGAAkG;IAClG,+EAA+E;IAC/E,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC1B,IAAA,+BAAgB,EAAC,aAAa,EAAE,IAAI,4BAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAA,2BAAY,EAAC,aAAa,CAAC,EAAE,cAAc,EAAE,iCAAiC,EAAE,GAAG,EAAE,8BAAa,EAAE,IAAI,wBAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACvM,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,2FAA2F;IAC3F,kGAAkG;IAClG,oGAAoG;IACpG,gFAAgF;IAChF,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,qBAAqB,CAC/B,YAAY,CAAC,IAAA,sBAAS,EAAC,MAAM,CAAC,WAAW,EAAE,aAAa,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,YAAY,CAC3H,CAAC;IACF,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5D,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,qCAAqC;IACnG,IAAI,SAAS;QAAE,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAEhE,MAAM,aAAa,GAAG,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrD,IAAI,aAAa;QAAE,OAAO,aAAa,CAAC;IAExC,+FAA+F;IAC/F,8FAA8F;IAC9F,8FAA8F;IAC9F,gGAAgG;IAChG,gGAAgG;IAChG,IAAA,wCAAoB,EAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAA,0CAAsB,EAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IAEnF,MAAM,GAAG,GAAG,IAAA,gCAAgB,EAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,6FAA6F;QAC7F,2FAA2F;QAC3F,4FAA4F;QAC5F,gBAAgB;QAChB,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,IAAA,+BAAgB,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,4BAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAA,2BAAY,EAAC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,GAAG,EAAE,8BAAa,EAAE,gCAAiB,CAAC,CAAC,CAAC;QACzK,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAY,EAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7E,IAAA,+BAAgB,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,4BAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAA,2BAAY,EAAC,IAAI,CAAC,IAAI,CAAC,EAAE,eAAe,EAAE,kBAAkB,EAAE,GAAG,EAAE,8BAAa,EAAE,gCAAiB,CAAC,CAAC,CAAC;IAChL,MAAM,MAAM,GAAG,IAAA,qBAAY,EAAC,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,qBAAY,CAAC,GAAG,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC9H,OAAO,IAAI,qBAAa,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED,kGAAkG;AAClG,qGAAqG;AACrG,+FAA+F;AAC/F,qKAAqK;AACrK,SAAS,YAAY,CAAC,OAAe;IACjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACzE,CAAC;AAED,wGAAwG;AACxG,wGAAwG;AACxG,sGAAsG;AACtG,yGAAyG;AACzG,qDAAqD;AACrD,EAAE;AACF,mGAAmG;AACnG,+FAA+F;AAC/F,kGAAkG;AAClG,sGAAsG;AACtG,qGAAqG;AACrG,qGAAqG;AACrG,qKAAqK;AACrK,SAAS,eAAe,CAAC,MAA4B,EAAE,YAA+B;IAClF,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAY,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,2BAA2B,CAAC;QAAE,OAAO,EAAE,CAAC;IAEnG,OAAO,qEAAqE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;UAClG,4GAA4G;UAC5G,qFAAqF;UACrF,uGAAuG;UACvG,yGAAyG;UACzG,2CAA2C,CAAC;AACtD,CAAC;AAED,kGAAkG;AAClG,SAAS,mBAAmB,CAAC,MAA4B;IACrD,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,eAAe,CAAC,KAAsB,EAAE,MAA4B;IACzE,MAAM,UAAU,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,iBAAiB,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAO,EAAW,EAAE;QACxD,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;QACvE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC;IAChB,CAAC,CAAC,CAAC;IACH,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhD,qGAAqG;IACrG,mGAAmG;IACnG,oGAAoG;IACpG,4EAA4E;IAC5E,EAAE;IACF,gGAAgG;IAChG,mGAAmG;IACnG,mDAAmD;IACnD,MAAM,KAAK,GAAG;QACV,8FAA8F;QAC9F,iFAAiF;QACjF,qCAAyB;QACzB,EAAE;QACF,uCAAuC,8BAAe,+CAA+C;QACrG,2FAA2F;QAC3F,EAAE;QACF,8FAA8F;QAC9F,+BAA+B;QAC/B,EAAE;QACF,6BAA6B,8BAAe,uDAAuD;QACnG,gDAAgD;QAChD,EAAE;KACL,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,iBAAiB,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,MAAM,CAAC,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,gBAAgB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC;QACjC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;YAC5D,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;gBACxB,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;YACzD,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,KAAK,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;QACtD,CAAC;QACD,2FAA2F;QAC3F,0EAA0E;QAC1E,KAAK,CAAC,IAAI,CAAC,mBAAmB,8BAAe,GAAG,CAAC,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,CAAC,IAAA,+BAAgB,EAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;QACzF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IAED,+FAA+F;IAC/F,OAAO,IAAI,qBAAa,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,4CAA2B,CAAC,CAAC;AAC5E,CAAC;AAED,kGAAkG;AAClG,iGAAiG;AACjG,sGAAsG;AACtG,sFAAsF;AACtF,SAAgB,YAAY,CAAC,IAAU,EAAE,GAA4C;IACjF,8DAA8D;IAC9D,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,KAAK,YAAY,qBAAa,EAAE,CAAC;YACjC,OAAO,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,CAAC,IAAI,iBAAS,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,IAAI,CAAC,IAAI,cAAc,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACnF,CAAC;AACL,CAAC;AAED,mGAAmG;AACnG,oGAAoG;AACpG,sGAAsG;AACtG,gGAAgG;AAChG,8BAA8B;AAC9B,SAAS,qBAAqB,CAAC,KAAoB;IAC/C,OAAO,IAAI,iBAAS,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,IAAA,kCAAmB,EAAC,KAAK,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED,SAAS,eAAe,CAAC,IAAU,EAAE,YAAoB;IACrD,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,IAAA,wBAAW,EAAC,OAAO,EAAE,YAAY,CAAC;YAAE,OAAO,IAAI,CAAC;IACxD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CAAC,KAAsB,EAAE,WAAwB;IAClE,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,SAAS;QACpC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;YAAE,SAAS;QAChC,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAC3C,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,IAAI,iBAAS,CACrB,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,CACrD,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,YAAY,CAAC,KAAsB,EAAE,YAAoC;IAC9E,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,SAAS;QACpC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;YAAE,SAAS;QAChC,MAAM,aAAa,GAAgB,EAAE,CAAC;QACtC,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC7B,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC;gBAAE,SAAS;YACvD,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACnC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,IAAI,iBAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;gBACzD,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;gBAC/B,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;gBAC/B,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;QACL,CAAC;QACD,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,IAAI,iBAAS,CACrB,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,CAC3D,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,YAAY,CAAC,KAAsB,EAAE,WAAwB;IAClE,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,SAAS;QACpC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;YAAE,SAAS;QAChC,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,WAAW,CAAC,YAAY,CAAC;YAAE,SAAS;QAC/D,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAC3C,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,IAAI,iBAAS,CACrB,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,CACrD,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["import * as path from 'path';\n\nimport { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, ExcludePaths, isHookGuard, HomeConfigService, RepoRootFinder, seedEntryForRule, CONFIG_FILENAME, renderRuleFailForAi } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { VersionSyncGuard } from './version-sync';\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\nimport { gitFromSubdirBlock } from './force-to-root';\nimport { loadRules, loadMatchRules, loadKeylessBashRules, globMatches, GuardHintCommands } from './load-rules';\nimport { missingDirectoryBlock } from './missing-directory';\nimport { MatchRule } from './rules/match-rule';\nimport { branchStateHangTimeout, maybeRefreshMainSync } from './main-sync-timeout';\nimport { logGuardDecision, logL1Decision, GuardDecision, branchForLog, MatrixRef, Verdict, MATRIX_L0_ALLOW, MATRIX_L2_UNROWED } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_ALLOW_JS, L0_CURE_ALLOW_JS } from '../bin/shim';\nimport { L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC, L0_FAULT_NONE } from './l0-fault-codes';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport { L1Classification, firstMatchingL1Row, L1_PRESTAGE_ROW } from './l1-rows';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\n// isHookGuard is asked about the rule's CONFIG KEY, never its name. Since the collapse those differ\n// for every class behind a policy key — `feature-branch-guard` is a rule NAME whose key is\n// `branch-state-guard` — and asking about the name would classify all eight collapsed guards as\n// code-style rules, i.e. run them in the wrong hook and never in the guards hook at all.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.configKey));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.configKey));\n}\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\n// This is L1's FILTER (not a table row) — see guards/L1-location.md.\nexport function filterByExcludedPaths(rules: readonly Rule[], relativePath: string, ex: ExcludePaths): readonly Rule[] {\n if (ex.paths.some((p: string): boolean => globMatches(p, relativePath))) return [];\n return rules;\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// The resolved gated-command strings the PR-lifecycle guards print, straight off the loaded\n// `commands.guardHints`. Handed to the rules at construction rather than injected into their config\n// entries under guard-name literals — the injection this replaces could miss a rename silently.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction guardHintsOf(loaded: LoadedConfig): GuardHintCommands {\n return new GuardHintCommands(loaded.commands.upsertPr, loaded.commands.mergeComplete);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n//\n// `command` is '' on the file-tool path (there is no command to judge) and the Bash command otherwise:\n// a call on the L0 allowlist survives this block, which is how read-only orientation stays available\n// under C. You have to be able to see which tree you are standing in before you can decide what to\n// write into the config. Returns null when the call is allowed through.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string, command: string = ''): BlockedResult | null {\n if (command !== '' && l0FaultAllows(command)) return null;\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n // Stamped with its L0 letter so the block is greppable as fault C wherever it is recorded — the\n // adapter carries it to the audit line rather than re-deriving it from the report text.\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)), L0_FAULT_CONFIG_MISSING);\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Always allow edits to webpieces.config.json — it's the fix target when out of sync\n if (path.resolve(input.filePath) === path.resolve(loaded.configPath)) {\n return null;\n }\n\n // …and the same unconditional PASS for the OPTIONAL machine-local `~/.webpieces/config.json`, for the\n // identical reason. That file is strictly validated when it exists (HomeConfigService), so a bad key\n // in it makes a `wp-*` command fail with an instruction to edit it — and a guard that then blocked\n // that edit would wedge the agent inside the failure. webpieces.config.json is immune to exactly this\n // because of the pass above; the home config gets the same immunity rather than a different answer.\n // Matches the absolute, `~/`, `$HOME/` and `${HOME}/` spellings alike (see isHomeConfigPath).\n if (new HomeConfigService().isHomeConfigPath(input.filePath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const relativePath = path.relative(workspaceRoot, input.filePath);\n const rules = filterByExcludedPaths(modeRules, relativePath, loaded.excludePaths);\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\n// SINGLETON, deliberately: WebpiecesVersions memoizes per root, and the same two roots are asked for on\n// every Bash call. A fresh instance per call would spawn `git worktree list` and re-read two manifests\n// on the hook's blocking path each time.\nconst VERSION_SYNC = new VersionSyncGuard();\n\n// There is deliberately NO agent parameter. It existed only for CoordinatorWorktreeGuard, which asked\n// WHO was calling; that guard is gone and its replacement asks WHICH TREE the command acts on. Agent\n// identity was measured untrustworthy for that question anyway — a worktree-isolated agent auto-reaped\n// at a turn boundary silently resumes with its cwd on the primary clone — so a guard must never infer\n// a tree from who is asking.\n// webpieces-disable no-function-outside-class -- sibling of run()/runRead() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runBash(command: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n return runBashInternal(command, cwd, mode);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const relativePath = path.relative(workspaceRoot, filePath);\n const all = loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded));\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n relativePath,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n//\n// It tests the CURE subset (L0_CURE_ALLOW_JS), not the whole list, and the distinction matters here\n// and nowhere else: this call site runs BEFORE any fault has been established, so whatever it accepts\n// is waved past the L1 guards on a perfectly healthy repo too. A repair command has to be waved past —\n// it runs before the config can be loaded at all. Read-only ORIENTATION does not: it fixes nothing, and\n// letting `git status` through here would delete force-to-root as a side effect of adding `pwd`. Under\n// an actual fault, l0FaultAllows() below consults the WHOLE list.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_CURE_ALLOW_JS.test(command.trim());\n}\n\n// The FULL L0 list, asked only where this file has actually detected an L0 fault (C = config missing,\n// Y = config out of sync). Those two are the JS-side faults; D/X/K are decided in the shim, which greps\n// the same union. Without this the orientation entry would be honoured under four faults and silently\n// dropped under the other two — precisely the per-fault carve-out the L0 rewrite removed.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l0FaultAllows(command: string): boolean {\n return L0_ALLOW_JS.test(command.trim());\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)', '-', L0_FAULT_NONE, MATRIX_L0_ALLOW));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\n// Version skew: this worktree pins a different @webpieces than the MAIN tree that governs it. The guard\n// hooks are absolute, so the main tree's binary judges every tree — which is fine until the two trees\n// disagree about which release that should be. L1 row 8; guards/L1-location.md carries the table.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction versionSkewBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const report = VERSION_SYNC.block(command, tree);\n return report === null ? null : new BlockedResult(report);\n}\n\n// Where the command lands in L1's five dimensions (K/A/R/G/P). The ONE place the runner's view of the\n// world is translated into the matrix's vocabulary — see L1Classification.forEnforcement for why\n// TreeKind 'outside' currently classifies as `p`.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1Classify(command: string, tree: EffectiveTree): L1Classification {\n return L1Classification.forEnforcement(\n tree.kind,\n VERSION_SYNC.skewed(tree),\n new ReadOnlyInspectionScan().isReadOnlyInspection(command),\n isGitOrGhCommand(command),\n path.resolve(tree.effectiveCwd) === path.resolve(tree.root),\n );\n}\n\n// The structural L1 blocks, in order: the misplaced-`cd` PRE-STAGE, then version skew\n// (row 8), then force-to-root (row 5). None is a configurable rule — they are decided from the command\n// text, the resolved tree and the caller, so they run as one step here rather than as three\n// near-identical stanzas in runBashInternal.\n//\n// For the two TREE-BASED blocks the ORDER and the CHOICE are not written here: they come from L1_ROWS\n// (l1-rows.ts), the same array guards/L1-location.md is rendered from. Classify, take the first\n// matching row, dispatch on its blockId — so a row deleted from the array is a block that stops firing,\n// and the doc cannot describe a table the guard does not consult. The two report builders below still\n// own their own predicates and their deny strings; l1-matrix.spec.ts asserts the row lookup and those\n// predicates agree.\n//\n// misplacedCdBlock is deliberately OUTSIDE that lookup, and runs ahead of it, for the reason its own\n// docblock gives: it decides from command TEXT, before a tree has been resolved, and the other two\n// reason FROM the resolved tree. Classifying it would mean asking L1_ROWS a question whose answer the\n// classification itself depends on. It is the same shape as L2's `bareCheckoutOfMain` pre-stage.\n//\n// It is therefore ROW 0 — \"pre-stage, decided from command text\" — rather than a row among 1-6. That\n// keeps it IN the table (the drift this table exists to prevent) without pretending it is classified\n// over the same five dimensions, which is the thing that cannot be true. `L1_PRESTAGE_ROW` is the\n// number the doc prints and the number `row=` logs, so the two still join.\n//\n// THIS FUNCTION IS THE ONE PLACE L1 REPORTS. It is the only scope holding the classification, the\n// matched row, the resolved tree and the agent at once — so logging here, BEFORE the early return,\n// is what finally records the outcomes that were previously invisible: the exempt row and the three\n// hand-downs wrote nothing at all, which is why \"L1 had no objection\" could not be observed and\n// \"show me every L1 decision\" had no answer. The three block helpers no longer log for themselves.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1LocationBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const misplacedCd = misplacedCdBlock(command, tree);\n if (misplacedCd !== null) {\n logL1(tree, command, 'BLOCK_AI_CURE', L1_PRESTAGE_ROW, 'cd-must-be-first', 'cd not leading/literal');\n return misplacedCd;\n }\n\n const row = firstMatchingL1Row(l1Classify(command, tree));\n const rowNum = String(row.num);\n if (row.blockId === null) {\n // ALLOW_EXEMPT stops here; ALLOW means \"no objection, handed down to L2\". Recording the\n // difference is the point — see Verdict. Neither is a claim that the call actually ran: the\n // other parallel hook may still have denied it.\n logL1(tree, command, row.action.kind === 'exempt' ? 'ALLOW_EXEMPT' : 'ALLOW', rowNum, '-', row.why);\n return null;\n }\n logL1(tree, command, 'BLOCK_AI_CURE', rowNum, row.blockId, row.why);\n if (row.blockId === 'missing-directory') return missingDirectoryBlock(command, tree);\n if (row.blockId === 'trinary-version-skew') return versionSkewBlock(command, tree);\n return gitFromSubdirBlock(command, tree, isGitOrGhCommand(command));\n}\n\n// One L1 line, into L1's OWN stream. `row` is the number the generated doc prints, so a reader who\n// sees `row=5` can open guards/L1-location.md at row 5 and read the state, the cure and the use cases\n// that row is supposed to cover.\n// eslint-disable-next-line @typescript-eslint/max-params -- the six fields of one log line, and a class here would break this file's shape\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions\nfunction logL1(tree: EffectiveTree, command: string, verdict: Verdict, row: string, rule: string, why: string): void {\n logL1Decision(tree.root, new GuardDecision(\n rule, 'Bash', command, branchForLog(tree.root), verdict, why, '-', L0_FAULT_NONE, new MatrixRef('L1', row),\n ));\n}\n\n/**\n * ONE legal shape for relocating a command: `cd <literal path> && <work>`. Anything else — a `cd` after\n * another command or after a `VAR=…` assignment, or a `cd \"$DIR\"` the guard cannot expand — is refused\n * here instead of being silently judged from the shell cwd.\n *\n * This changes no verdict: every command it refuses was ALREADY judged from the shell cwd, so nothing\n * it blocks was previously being allowed on the strength of its `cd`. What changes is that the agent\n * finds out. Two PRs of increasingly precise near-miss wording (#596, #597) still left the fact in a\n * paragraph appended to an unrelated block; the rule is simpler stated as a rule.\n *\n * FIRST in the L1 chain deliberately. version skew and force-to-root both reason from the\n * resolved tree, and if the `cd` did not resolve, that tree is not the one the agent thinks they are\n * in — so their remedies would be steering from a location the command does not actually run in.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole runner is module-scope functions\nfunction misplacedCdBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const reason = new EffectiveTreeResolver().misplacedCd(command);\n if (reason === null) return null;\n const report =\n `❌ A \\`cd\\` must come FIRST in the command, with a LITERAL path.\\n` +\n ` Why this one was rejected: ${reason}.\\n` +\n ` \\`cd <literal path> && <work>\\` is the ONE shape that moves where webpieces judges a command.\\n` +\n ` Any other \\`cd\\` cannot be resolved, so the command would be judged against ${tree.root}\\n` +\n ` — not where you think it runs. That used to happen silently; this block is that silence, spoken.\\n` +\n ` Fix Option 1: put the \\`cd\\` first, literal path, SAME command: cd /abs/path && <the rest>\\n` +\n ` Fix Option 2: drop the \\`cd\\` — ${tree.root} is where the command is judged anyway.\\n` +\n ` Fix Option 3: if the SECOND \\`cd\\` was there to point a tool at a directory, use that tool's own\\n` +\n ` directory flag instead — \\`git -C <dir>\\`, \\`tar -C <dir>\\`, \\`npm --prefix <dir>\\`,\\n` +\n ` \\`npm pack --pack-destination <dir>\\`, \\`pnpm --dir <dir>\\`. One \\`cd\\`, same result.\\n` +\n ` Fix Option 4: split it — run the work in one call, the \\`cd\\` in another (a \\`cd\\` alone still\\n` +\n ` moves nothing the guards judge; see EffectiveTree on why cwd cannot be assumed).`;\n return new BlockedResult(report);\n}\n\n/**\n * The KEYLESS bash guards, deliberately kept OUT of the config-driven rule set. They have no\n * webpieces.config.json entry at all, so passing them through checkConfigSync would make every\n * consumer's next Bash call a fault-Y block (\"this rule has no entry\") for a feature nobody opted into\n * — which is exactly what whole-repo-build-guard did on its first release. Each one decides for itself\n * whether it acts: whole-repo-build-guard reads ~/.webpieces/config.json for a machine-local OPT-OUT\n * (absent means ON), commit-message-substitution-guard acts unconditionally (see loadKeylessBashRules).\n *\n * They still honour excludePaths, and they do not run in `rules` mode (code-style-only hook).\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction keylessBashRules(loaded: LoadedConfig, mode: HookMode, relativePath: string): readonly Rule[] {\n if (mode === 'rules') return [];\n return filterByExcludedPaths(\n loadKeylessBashRules(loaded.prGate.buildCommand), relativePath, loaded.excludePaths,\n );\n}\n\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nfunction runBashInternal(command: string, cwd: string, mode: HookMode): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return configMissingBlock(cwd, command); // fault C\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW_EXEMPT', 'foreign git repo (out of scope)', '-', L0_FAULT_NONE, new MatrixRef('L1', '1')));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const relativeCwd = path.relative(workspaceRoot, tree.effectiveCwd);\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), mode), relativeCwd, loaded.excludePaths,\n );\n const keyless = keylessBashRules(loaded, mode, relativeCwd);\n if (rules.length === 0 && keyless.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig); // fault Y — L0 list wins, as under C\n if (outOfSync) return l0FaultAllows(command) ? null : outOfSync;\n\n const locationBlock = l1LocationBlock(command, tree);\n if (locationBlock) return locationBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root, branchStateHangTimeout(loaded.rulesConfig));\n\n const ctx = buildBashContext(command, tree);\n const groups = runBashRules([...rules, ...keyless], ctx);\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK_AI_CURE', 'bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n const report = formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n//\n// The hint MUST state the precondition, not just the remedy. \"cd into it first\" is true only for a\n// LITERAL cd at the FRONT of the same command; `D=…; cd \"$D\"; git push` lands in the identical\n// directory and does NOT relocate the verdict. That used to be silent, then a near-miss paragraph\n// appended here; it is now misplacedCdBlock, which refuses the command outright — so by the time this\n// footer is reached, any `cd` in the command is already known to be leading and literal. The wording\n// still states the shape, because this is where an agent LEARNS it, before writing the next command.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees (${exemptGuards.join(', ')})? `\n + `Put a LITERAL \\`cd\\` at the FRONT of the SAME command — \\`cd /abs/path/to/repo && git push\\` — and git/gh `\n + `run normally there: the webpieces guards do NOT govern them (each is its own repo).`\n + `\\n \\`cd <literal path> && <work>\\` is the ONE shape that moves where a command is judged. A \\`cd\\` `\n + `anywhere else in the line, or a non-literal target like \\`cd \"$DIR\"\\`, is refused outright rather than `\n + `judged from somewhere you did not intend.`;\n}\n\n// The set of CONFIG KEYS explicitly present in webpieces.config.json (every key except rulesDir).\nfunction configuredRuleNames(config: WebpiecesRulesConfig): ReadonlySet<string> {\n return new Set(Object.keys(config).filter((k: string) => k !== 'rulesDir'));\n}\n\n/**\n * Fault Y — a loaded rule whose CONFIG KEY has no entry.\n *\n * Compared on `configKey`, not `name`. Eight of the loaded rules are classes behind two policy keys, so\n * comparing names here would demand entries named `feature-branch-guard`, `pr-merge-guard` and six\n * others — keys the validator then REJECTS as retired. That is the two-enforcement-paths deadlock this\n * repo has already hit once (see the narration in guards.spec.ts), and it blocks every Bash/Write/Edit.\n *\n * De-duplicated on the key too, so one missing policy entry reports ONE paste-ready snippet rather than\n * four copies of the same one under four different headings.\n */\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const seen = new Set<string>();\n const unconfiguredRules = rules.filter((r: Rule): boolean => {\n if (configured.has(r.configKey) || seen.has(r.configKey)) return false;\n seen.add(r.configKey);\n return true;\n });\n if (unconfiguredRules.length === 0) return null;\n\n // ONE action, no menu, no escalation — the config-validation invariant (guards/L0-tooling.md): every\n // config problem cures to \"make the file right\", and editing it is never denied. This message used\n // to tell the agent to interview the human about each rule; agents did not do it, so the block just\n // stalled. Each rule now ships a paste-ready entry at its recommended mode.\n //\n // Note this is the CONFIG-BEHIND-CODE direction. The opposite one — the config names a rule the\n // installed validator has no schema for — is unknownRuleError() in rules-config/validate-config.ts\n // and surfaces in the validation banner, not here.\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n `Add an entry for each rule below to ${CONFIG_FILENAME}. Editing that file is ALWAYS allowed through`,\n 'the guard — including right now, while this block is up — so paste the entries and retry.',\n '',\n 'Each entry below is ready to paste at its recommended mode; adjust the option values if your',\n 'project needs different ones.',\n '',\n `Do NOT delete a rule from ${CONFIG_FILENAME} to silence it — an entry is REQUIRED for every rule,`,\n 'and \"mode\": \"OFF\" is how a rule is turned off.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.configKey} ---`);\n lines.push(`Description: ${rule.description}`);\n const opts = rule.defaultOptions;\n const optKeys = Object.keys(opts);\n if (optKeys.length > 0) {\n lines.push(`Available options (suggested defaults shown):`);\n for (const key of optKeys) {\n lines.push(` ${key}: ${JSON.stringify(opts[key])}`);\n }\n } else {\n lines.push('Available options: none beyond mode');\n }\n // The SAME entry the installer would seed: recommended mode, both hatches, and every other\n // schema-required field — so pasting it satisfies the loader in one pass.\n lines.push(`Entry to add to ${CONFIG_FILENAME}:`);\n lines.push(` \"${rule.configKey}\": ${JSON.stringify(seedEntryForRule(rule.configKey))}`);\n lines.push('');\n }\n\n // Fault Y, stamped for the audit trail — see configMissingBlock for why the producer names it.\n return new BlockedResult(lines.join('\\n'), L0_FAULT_CONFIG_OUT_OF_SYNC);\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixOptions kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and cures. Fold the\n// cures into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value). The \"Fix Option N:\"/\"(preferred)\" labels come from the ONE\n// framework-owned renderer, exactly as report.ts renders a rule's static FixHint — a rule never\n// hand-numbers its own cures.\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n return new Violation(error.line ?? 0, error.snippet ?? '', renderRuleFailForAi(error));\n}\n\nfunction ruleMatchesFile(rule: Rule, relativePath: string): boolean {\n for (const pattern of rule.files) {\n if (globMatches(pattern, relativePath)) return true;\n }\n return false;\n}\n\nfunction runBashRules(rules: readonly Rule[], bashContext: BashContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'bash') continue;\n if (!rule.shouldRun()) continue;\n const vs = runRuleCheck(rule, bashContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n\nfunction runEditRules(rules: readonly Rule[], editContexts: readonly EditContext[]): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'edit') continue;\n if (!rule.shouldRun()) continue;\n const allViolations: Violation[] = [];\n for (const ctx of editContexts) {\n if (!ruleMatchesFile(rule, ctx.relativePath)) continue;\n const vs = runRuleCheck(rule, ctx);\n for (const v of vs) {\n const copy = new Violation(v.line, v.snippet, v.message);\n copy.editIndex = ctx.editIndex;\n copy.editCount = ctx.editCount;\n allViolations.push(copy);\n }\n }\n if (allViolations.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, allViolations,\n ));\n }\n }\n return groups;\n}\n\nfunction runFileRules(rules: readonly Rule[], fileContext: FileContext): readonly RuleGroup[] {\n const groups: RuleGroup[] = [];\n for (const rule of rules) {\n if (rule.scope !== 'file') continue;\n if (!rule.shouldRun()) continue;\n if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;\n const vs = runRuleCheck(rule, fileContext);\n if (vs.length > 0) {\n groups.push(new RuleGroup(\n rule.name, rule.description, rule.fixHint, [...vs],\n ));\n }\n }\n return groups;\n}\n"]}
|