@webpieces/ai-hook-rules 0.4.635 → 0.4.637

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +11 -4
  2. package/package.json +2 -2
  3. package/src/adapters/hook-core.js +4 -3
  4. package/src/adapters/hook-core.js.map +1 -1
  5. package/src/bin/setup.js +72 -6
  6. package/src/bin/setup.js.map +1 -1
  7. package/src/core/custom-rule-adapter.js +3 -1
  8. package/src/core/custom-rule-adapter.js.map +1 -1
  9. package/src/core/decision-log.d.ts +38 -5
  10. package/src/core/decision-log.js +45 -6
  11. package/src/core/decision-log.js.map +1 -1
  12. package/src/core/l1-doc.js +6 -3
  13. package/src/core/l1-doc.js.map +1 -1
  14. package/src/core/l2-doc.d.ts +7 -0
  15. package/src/core/l2-doc.js +274 -0
  16. package/src/core/l2-doc.js.map +1 -0
  17. package/src/core/l2-rows.d.ts +97 -0
  18. package/src/core/l2-rows.js +229 -0
  19. package/src/core/l2-rows.js.map +1 -0
  20. package/src/core/load-rules.d.ts +8 -2
  21. package/src/core/load-rules.js +51 -31
  22. package/src/core/load-rules.js.map +1 -1
  23. package/src/core/main-sync-refresh.d.ts +8 -1
  24. package/src/core/main-sync-refresh.js +9 -2
  25. package/src/core/main-sync-refresh.js.map +1 -1
  26. package/src/core/main-sync-timeout.d.ts +32 -0
  27. package/src/core/main-sync-timeout.js +93 -0
  28. package/src/core/main-sync-timeout.js.map +1 -0
  29. package/src/core/rules/branch-creation-guard.js +1 -1
  30. package/src/core/rules/branch-creation-guard.js.map +1 -1
  31. package/src/core/rules/catch-error-pattern.js +1 -1
  32. package/src/core/rules/catch-error-pattern.js.map +1 -1
  33. package/src/core/rules/feature-branch-guard.d.ts +3 -3
  34. package/src/core/rules/feature-branch-guard.js +17 -4
  35. package/src/core/rules/feature-branch-guard.js.map +1 -1
  36. package/src/core/rules/index.d.ts +15 -1
  37. package/src/core/rules/index.js +18 -10
  38. package/src/core/rules/index.js.map +1 -1
  39. package/src/core/rules/match-rule.js +4 -1
  40. package/src/core/rules/match-rule.js.map +1 -1
  41. package/src/core/rules/max-file-lines.js +1 -1
  42. package/src/core/rules/max-file-lines.js.map +1 -1
  43. package/src/core/rules/merge-in-progress-guard.d.ts +9 -3
  44. package/src/core/rules/merge-in-progress-guard.js +9 -4
  45. package/src/core/rules/merge-in-progress-guard.js.map +1 -1
  46. package/src/core/rules/merged-branch-bash-guard.d.ts +3 -3
  47. package/src/core/rules/merged-branch-bash-guard.js +17 -6
  48. package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
  49. package/src/core/rules/no-any-unknown.js +1 -1
  50. package/src/core/rules/no-any-unknown.js.map +1 -1
  51. package/src/core/rules/no-custom-css.js +1 -1
  52. package/src/core/rules/no-custom-css.js.map +1 -1
  53. package/src/core/rules/no-destructure.js +1 -1
  54. package/src/core/rules/no-destructure.js.map +1 -1
  55. package/src/core/rules/no-implicit-any.js +1 -1
  56. package/src/core/rules/no-implicit-any.js.map +1 -1
  57. package/src/core/rules/no-js-files.js +1 -1
  58. package/src/core/rules/no-js-files.js.map +1 -1
  59. package/src/core/rules/no-process-exit-outside-main.js +1 -1
  60. package/src/core/rules/no-process-exit-outside-main.js.map +1 -1
  61. package/src/core/rules/no-symbol-di-tokens.js +1 -1
  62. package/src/core/rules/no-symbol-di-tokens.js.map +1 -1
  63. package/src/core/rules/no-unmanaged-exceptions.js +1 -1
  64. package/src/core/rules/no-unmanaged-exceptions.js.map +1 -1
  65. package/src/core/rules/pr-creation-or-push-guard.d.ts +10 -3
  66. package/src/core/rules/pr-creation-or-push-guard.js +10 -4
  67. package/src/core/rules/pr-creation-or-push-guard.js.map +1 -1
  68. package/src/core/rules/pr-merge-guard.d.ts +3 -3
  69. package/src/core/rules/pr-merge-guard.js +2 -1
  70. package/src/core/rules/pr-merge-guard.js.map +1 -1
  71. package/src/core/rules/read-stale-guard.d.ts +3 -3
  72. package/src/core/rules/read-stale-guard.js +15 -6
  73. package/src/core/rules/read-stale-guard.js.map +1 -1
  74. package/src/core/rules/redirect-how-to-merge-main.d.ts +3 -3
  75. package/src/core/rules/redirect-how-to-merge-main.js +1 -1
  76. package/src/core/rules/redirect-how-to-merge-main.js.map +1 -1
  77. package/src/core/rules/require-return-type.js +1 -1
  78. package/src/core/rules/require-return-type.js.map +1 -1
  79. package/src/core/rules/stale-main-bash-guard.d.ts +3 -3
  80. package/src/core/rules/stale-main-bash-guard.js +5 -4
  81. package/src/core/rules/stale-main-bash-guard.js.map +1 -1
  82. package/src/core/rules/throw-cause-required.js +1 -1
  83. package/src/core/rules/throw-cause-required.js.map +1 -1
  84. package/src/core/rules/validate-ts-in-src.js +1 -1
  85. package/src/core/rules/validate-ts-in-src.js.map +1 -1
  86. package/src/core/rules/whole-repo-build-guard.js +6 -2
  87. package/src/core/rules/whole-repo-build-guard.js.map +1 -1
  88. package/src/core/runner.js +41 -22
  89. package/src/core/runner.js.map +1 -1
  90. package/src/core/sync-main.d.ts +9 -0
  91. package/src/core/sync-main.js +17 -1
  92. package/src/core/sync-main.js.map +1 -1
  93. package/src/core/types.d.ts +11 -0
  94. package/src/core/types.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sync-main.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/sync-main.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BranchReaper,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n MainSyncStatusFile,\n MergedBranchesCache,\n MergedBranchesService,\n ReapResult,\n loadAndValidate,\n computeAllMainSyncStatuses,\n writeMainSyncStatusFile,\n writeMainSyncLock,\n tryAcquireMainSyncLock,\n finishedLock,\n} from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\nimport { logSyncEvent, SyncLogEvent } from './main-sync-log';\nimport { logStream, StreamIdentity } from './log-stream';\n\n/**\n * The detached, fire-and-forget refresher spawned (by file path, not a bin) from\n * main-sync-refresh.ts. It does the SLOW work (merged-PR lookup + git fetch + merge-base +\n * same-file-overlap) and writes `.webpieces/main-sync-status.json` so the next hook call reads it\n * instantly. Nobody reads our exit code or output — we run after the spawning hook has returned.\n *\n * Concurrency: a lock file (`.webpieces/main-sync.lock.json`) holds `inprocess`/`finished` + a start\n * epoch + the holder's pid, taken with an ATOMIC exclusive create. If another refresher already holds\n * it and is alive, we exit immediately (don't pile up `git fetch`es). If the holder is finished, dead,\n * or older than hangTimeoutMinutes, the lock is reclaimed and we proceed.\n *\n * argv: [, , repoRoot, hangTimeoutMinutes, sessionId, agentId, hook]\n *\n * The last three are the SPAWNER's LogStream identity, and adopting them here is the whole reason\n * they are passed. This process has its own fresh module-level `logStream`; left unidentified it\n * named every line `unknown-coordinator-hook.log` — one shared file that every\n * agent's refresher child appended to concurrently (the PIPE_BUF tearing LogStream exists to remove),\n * and which held this run's START/FINISH while the parent's SPAWN_ATTEMPT sat in a different file\n * entirely. Adopting the parent's identity puts one refresh cycle back in one stream. Absent argv\n * (a hand-run of this file) leaves the default, which still prefixes.\n */\nexport function main(): void {\n logStream.identify(spawnerIdentity(process.argv));\n refreshMainSync(\n process.argv[2] ?? process.cwd(),\n Number(process.argv[3]) || DEFAULT_HANG_TIMEOUT_MINUTES,\n process.argv.slice(2).join(' '),\n );\n}\n\n/**\n * The spawner's LogStream identity as it arrives on this child's argv — positions 4/5/6, which is\n * where `triggerMainSyncRefresh` puts them (its own array is offset by two: node + script path).\n *\n * Exported so ONE test can pin BOTH ends of the contract at once. The bug this closes is precisely a\n * disagreement between two files about argv positions, and a test that only checks the reader would\n * have stayed green through it.\n */\n// webpieces-disable no-function-outside-class -- argv parse for this detached main(), matching the file's existing shape\nexport function spawnerIdentity(argv: string[]): StreamIdentity {\n return new StreamIdentity(argv[4] ?? '', argv[5] ?? '', argv[6] ?? 'hook');\n}\n\n/**\n * The refresh itself, argv-free so it can be driven by a test. `main()` is only the argv parse — a\n * test importing another module's `main` is what no-process-exit-outside-main exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-level entry point of this detached refresher, matching the file's existing shape\nexport function refreshMainSync(repoRoot: string, hangTimeoutMinutes: number, argvDetail: string = ''): void {\n const startedMs = Date.now();\n\n // First action: prove the detached child actually started. If the async-refresh stream has no START line\n // for a spawn, the child never launched (or died before this point).\n logSyncEvent(repoRoot, new SyncLogEvent('START', process.pid, '-', `argv=${argvDetail}`));\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // ONE atomic acquire — not a check followed by a write. The old check-then-write pair had a\n // gap in which two detached children could both pass the check and then both run `git fetch`.\n const lock = tryAcquireMainSyncLock(repoRoot, hangTimeoutMinutes);\n if (!lock) {\n logSyncEvent(repoRoot, new SyncLogEvent('SKIP_INPROGRESS', process.pid, '-', 'another refresh is in progress'));\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // EVERY worktree's branch, not just ours. The cache is shared by the whole repo but used\n // to describe only the branch of whichever tree won this lock, so every other worktree's\n // guards read a cross-branch cache and abstained. One fetch + one `gh` call now arms all\n // of them; the map is rebuilt from the live worktree set, so dead branches drop out.\n const file = computeAllMainSyncStatuses(repoRoot);\n writeMainSyncStatusFile(repoRoot, file);\n\n // Second slow signal, same lock, same detached run: which local branches are dead. One bulk\n // `gh pr list --state merged` call. The branch-creation-guard reads the result to enforce its\n // cap without ever touching the network itself. Deliberately allowed to go stale.\n const mergedBranches = new MergedBranchesService();\n const cache = mergedBranches.computeMergedBranches(repoRoot);\n mergedBranches.writeMergedBranches(repoRoot, cache);\n\n // Third step, same detached run: actually DELETE the dead branches. Reporting them was\n // never enough — the reap was only ever a `git branch -D` string in a fix hint, which an\n // agent reads as destructive and stalls on, so nothing was ever cleaned. Here nobody has\n // to be asked. Reuses the verdicts we JUST computed (no second `gh` call).\n const reaped = autoReap(repoRoot, cache);\n\n // FINISH after a successful write — START-without-FINISH means we were killed mid-run.\n const names = Object.keys(file.branches);\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'FINISH', process.pid, names.join(','),\n `branches=${String(names.length)} ${names.map(summarize(file)).join(' ')} deletableBranches=${String(cache.deletable.length)} reaped=${String(reaped)} ms=${String(Date.now() - startedMs)}`,\n ));\n } finally {\n // Always flip the lock off so a compute failure can't wedge the guard until the\n // staleness reclaim kicks in.\n writeMainSyncLock(repoRoot, finishedLock(lock.started));\n }\n } catch (err: unknown) {\n const error = toError(err);\n // Detached: swallow so a transient git/fs error never leaves poison state (the next hook call\n // spawns a fresh refresher) — but record WHY it died so the failure isn't invisible.\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `${error.message} | ${error.stack ?? ''}`));\n }\n}\n\n/**\n * One `branch(merged/forkPoint/conflict)` token per recorded branch, for the FINISH log line. The log\n * is the only evidence that a given worktree's branch was actually armed by a refresh, so it has to\n * name every branch written, not just the tree the refresher happened to run in.\n */\n// webpieces-disable no-function-outside-class -- module-level log formatter of this detached main(), matching the file's existing shape\nfunction summarize(file: MainSyncStatusFile): (branch: string) => string {\n return (branch: string): string => {\n const status = file.branches[branch];\n if (status === undefined) return `${branch}(?)`;\n return `${branch}(merged=${String(status.branchAlreadyMerged)}:${status.mergedPr} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)})`;\n };\n}\n\n/**\n * Delete the branches the verdicts just declared dead. Returns how many actually went.\n *\n * WHY it is safe to do this unattended: every candidate is provably dead (merged PR / squash backup\n * of a merged branch / zero commits of its own), `main` and any worktree-held branch are excluded\n * upstream, and each delete is logged with the branch's pre-delete SHA plus the exact command that\n * restores it. WHY it is safe to do it HERE: this refresher already recomputed those verdicts on\n * this very run, so it is acting on evidence seconds old, not on the deliberately-stale cache file.\n *\n * Swallows everything. We are detached and fire-and-forget: cleanup failing must never damage the\n * main-sync status this process exists to produce — but every failure is logged, because a silent\n * background deletion is exactly what nobody should have to trust.\n */\n// webpieces-disable no-function-outside-class -- module-level helper of this detached main(), matching the file's existing shape\nfunction autoReap(repoRoot: string, cache: MergedBranchesCache): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const config = loadAndValidate(repoRoot).rulesConfig['branch-creation-guard'];\n // Strictly opt-IN: only an explicit `true` reaps. `autoReapMergedBranches` is schema-required,\n // so every validated config states an answer — which means \"absent\" here is not a consumer\n // who wants the default, it is a config that never passed validation. Deleting branches on\n // that basis would be deleting on a preference nobody expressed.\n if (config?.mode === 'OFF' || config?.autoReapMergedBranches !== true) return 0;\n\n const result: ReapResult = new BranchReaper().reap(repoRoot, 'auto-reap', cache);\n for (const failure of result.failed) {\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'ERROR', process.pid, failure.branch, `reap failed: ${failure.error}`));\n }\n return result.reaped.length;\n } catch (err: unknown) {\n const error = toError(err);\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `autoReap: ${error.message}`));\n return 0;\n }\n}\n\nif (require.main === module) {\n main();\n}\n"]}
1
+ {"version":3,"file":"sync-main.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/sync-main.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BranchReaper,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n MainSyncStatusFile,\n MergedBranchesCache,\n MergedBranchesService,\n ReapResult,\n loadAndValidate,\n computeAllMainSyncStatuses,\n writeMainSyncStatusFile,\n writeMainSyncLock,\n tryAcquireMainSyncLock,\n finishedLock,\n} from '@webpieces/rules-config';\n\nimport { toError } from './to-error';\nimport { logSyncEvent, SyncLogEvent } from './main-sync-log';\nimport { logStream, StreamIdentity } from './log-stream';\n\n/**\n * The detached, fire-and-forget refresher spawned (by file path, not a bin) from\n * main-sync-refresh.ts. It does the SLOW work (merged-PR lookup + git fetch + merge-base +\n * same-file-overlap) and writes `.webpieces/main-sync-status.json` so the next hook call reads it\n * instantly. Nobody reads our exit code or output — we run after the spawning hook has returned.\n *\n * Concurrency: a lock file (`.webpieces/main-sync.lock.json`) holds `inprocess`/`finished` + a start\n * epoch + the holder's pid, taken with an ATOMIC exclusive create. If another refresher already holds\n * it and is alive, we exit immediately (don't pile up `git fetch`es). If the holder is finished, dead,\n * or older than hangTimeoutMinutes, the lock is reclaimed and we proceed.\n *\n * argv: [, , repoRoot, hangTimeoutMinutes, sessionId, agentId, hook]\n *\n * The last three are the SPAWNER's LogStream identity, and adopting them here is the whole reason\n * they are passed. This process has its own fresh module-level `logStream`; left unidentified it\n * named every line `unknown-coordinator-hook.log` — one shared file that every\n * agent's refresher child appended to concurrently (the PIPE_BUF tearing LogStream exists to remove),\n * and which held this run's START/FINISH while the parent's SPAWN_ATTEMPT sat in a different file\n * entirely. Adopting the parent's identity puts one refresh cycle back in one stream. Absent argv\n * (a hand-run of this file) leaves the default, which still prefixes.\n */\nexport function main(): void {\n logStream.identify(spawnerIdentity(process.argv));\n refreshMainSync(\n process.argv[2] ?? process.cwd(),\n hangTimeoutFromArgv(process.argv[3]),\n process.argv.slice(2).join(' '),\n );\n}\n\n/**\n * Parse the hang timeout off argv, WITHOUT `||`.\n *\n * `Number(argv[3]) || DEFAULT` silently rewrote a configured `0` — \"never treat a lock as stale\" — into\n * the 5-minute default, because `0` is falsy. The knob now has exactly one reader (the branch-state\n * policy entry), so a value that survives the config only to be discarded at the process boundary is\n * the whole knob being a lie. Only a MISSING or non-numeric argument falls back.\n */\n// webpieces-disable no-function-outside-class -- argv parse for this detached main(), matching the file's existing shape\nexport function hangTimeoutFromArgv(raw: string | undefined): number {\n if (raw === undefined || raw.trim() === '') return DEFAULT_HANG_TIMEOUT_MINUTES;\n const parsed = Number(raw);\n return Number.isFinite(parsed) ? parsed : DEFAULT_HANG_TIMEOUT_MINUTES;\n}\n\n/**\n * The spawner's LogStream identity as it arrives on this child's argv — positions 4/5/6, which is\n * where `triggerMainSyncRefresh` puts them (its own array is offset by two: node + script path).\n *\n * Exported so ONE test can pin BOTH ends of the contract at once. The bug this closes is precisely a\n * disagreement between two files about argv positions, and a test that only checks the reader would\n * have stayed green through it.\n */\n// webpieces-disable no-function-outside-class -- argv parse for this detached main(), matching the file's existing shape\nexport function spawnerIdentity(argv: string[]): StreamIdentity {\n return new StreamIdentity(argv[4] ?? '', argv[5] ?? '', argv[6] ?? 'hook');\n}\n\n/**\n * The refresh itself, argv-free so it can be driven by a test. `main()` is only the argv parse — a\n * test importing another module's `main` is what no-process-exit-outside-main exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-level entry point of this detached refresher, matching the file's existing shape\nexport function refreshMainSync(repoRoot: string, hangTimeoutMinutes: number, argvDetail: string = ''): void {\n const startedMs = Date.now();\n\n // First action: prove the detached child actually started. If the async-refresh stream has no START line\n // for a spawn, the child never launched (or died before this point).\n logSyncEvent(repoRoot, new SyncLogEvent('START', process.pid, '-', `argv=${argvDetail}`));\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // ONE atomic acquire — not a check followed by a write. The old check-then-write pair had a\n // gap in which two detached children could both pass the check and then both run `git fetch`.\n const lock = tryAcquireMainSyncLock(repoRoot, hangTimeoutMinutes);\n if (!lock) {\n logSyncEvent(repoRoot, new SyncLogEvent('SKIP_INPROGRESS', process.pid, '-', 'another refresh is in progress'));\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // EVERY worktree's branch, not just ours. The cache is shared by the whole repo but used\n // to describe only the branch of whichever tree won this lock, so every other worktree's\n // guards read a cross-branch cache and abstained. One fetch + one `gh` call now arms all\n // of them; the map is rebuilt from the live worktree set, so dead branches drop out.\n const file = computeAllMainSyncStatuses(repoRoot);\n writeMainSyncStatusFile(repoRoot, file);\n\n // Second slow signal, same lock, same detached run: which local branches are dead. One bulk\n // `gh pr list --state merged` call. The branch-creation-guard reads the result to enforce its\n // cap without ever touching the network itself. Deliberately allowed to go stale.\n const mergedBranches = new MergedBranchesService();\n const cache = mergedBranches.computeMergedBranches(repoRoot);\n mergedBranches.writeMergedBranches(repoRoot, cache);\n\n // Third step, same detached run: actually DELETE the dead branches. Reporting them was\n // never enough — the reap was only ever a `git branch -D` string in a fix hint, which an\n // agent reads as destructive and stalls on, so nothing was ever cleaned. Here nobody has\n // to be asked. Reuses the verdicts we JUST computed (no second `gh` call).\n const reaped = autoReap(repoRoot, cache);\n\n // FINISH after a successful write — START-without-FINISH means we were killed mid-run.\n const names = Object.keys(file.branches);\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'FINISH', process.pid, names.join(','),\n `branches=${String(names.length)} ${names.map(summarize(file)).join(' ')} deletableBranches=${String(cache.deletable.length)} reaped=${String(reaped)} ms=${String(Date.now() - startedMs)}`,\n ));\n } finally {\n // Always flip the lock off so a compute failure can't wedge the guard until the\n // staleness reclaim kicks in.\n writeMainSyncLock(repoRoot, finishedLock(lock.started));\n }\n } catch (err: unknown) {\n const error = toError(err);\n // Detached: swallow so a transient git/fs error never leaves poison state (the next hook call\n // spawns a fresh refresher) — but record WHY it died so the failure isn't invisible.\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `${error.message} | ${error.stack ?? ''}`));\n }\n}\n\n/**\n * One `branch(merged/forkPoint/conflict)` token per recorded branch, for the FINISH log line. The log\n * is the only evidence that a given worktree's branch was actually armed by a refresh, so it has to\n * name every branch written, not just the tree the refresher happened to run in.\n */\n// webpieces-disable no-function-outside-class -- module-level log formatter of this detached main(), matching the file's existing shape\nfunction summarize(file: MainSyncStatusFile): (branch: string) => string {\n return (branch: string): string => {\n const status = file.branches[branch];\n if (status === undefined) return `${branch}(?)`;\n return `${branch}(merged=${String(status.branchAlreadyMerged)}:${status.mergedPr} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)})`;\n };\n}\n\n/**\n * Delete the branches the verdicts just declared dead. Returns how many actually went.\n *\n * WHY it is safe to do this unattended: every candidate is provably dead (merged PR / squash backup\n * of a merged branch / zero commits of its own), `main` and any worktree-held branch are excluded\n * upstream, and each delete is logged with the branch's pre-delete SHA plus the exact command that\n * restores it. WHY it is safe to do it HERE: this refresher already recomputed those verdicts on\n * this very run, so it is acting on evidence seconds old, not on the deliberately-stale cache file.\n *\n * Swallows everything. We are detached and fire-and-forget: cleanup failing must never damage the\n * main-sync status this process exists to produce — but every failure is logged, because a silent\n * background deletion is exactly what nobody should have to trust.\n */\n// webpieces-disable no-function-outside-class -- module-level helper of this detached main(), matching the file's existing shape\nfunction autoReap(repoRoot: string, cache: MergedBranchesCache): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const config = loadAndValidate(repoRoot).rulesConfig['branch-creation-guard'];\n // Strictly opt-IN: only an explicit `true` reaps. `autoReapMergedBranches` is schema-required,\n // so every validated config states an answer — which means \"absent\" here is not a consumer\n // who wants the default, it is a config that never passed validation. Deleting branches on\n // that basis would be deleting on a preference nobody expressed.\n if (config?.mode === 'OFF' || config?.autoReapMergedBranches !== true) return 0;\n\n const result: ReapResult = new BranchReaper().reap(repoRoot, 'auto-reap', cache);\n for (const failure of result.failed) {\n logSyncEvent(repoRoot, new SyncLogEvent(\n 'ERROR', process.pid, failure.branch, `reap failed: ${failure.error}`));\n }\n return result.reaped.length;\n } catch (err: unknown) {\n const error = toError(err);\n logSyncEvent(repoRoot, new SyncLogEvent('ERROR', process.pid, '-', `autoReap: ${error.message}`));\n return 0;\n }\n}\n\nif (require.main === module) {\n main();\n}\n"]}
@@ -125,6 +125,17 @@ export interface PlainRule {
125
125
  * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.
126
126
  */
127
127
  export interface Rule extends PlainRule {
128
+ /**
129
+ * The webpieces.config.json key whose entry configures this rule — NOT necessarily `name`.
130
+ *
131
+ * `name` is the operator identity (what a decision-log line and a deny report carry); `configKey`
132
+ * is the switch. They differ wherever several classes implement one POLICY: all four branch-state
133
+ * guards read `branch-state-guard`, all four PR-lifecycle guards read `pr-lifecycle-guard`. Every
134
+ * consumer that asks "which section does this live in?" (filterByMode) or "does this have an
135
+ * entry?" (the fault-Y config-sync check) must read THIS field, because asking for `name` there
136
+ * demands entries under keys the validator rejects as retired.
137
+ */
138
+ readonly configKey: string;
128
139
  shouldRun(): boolean;
129
140
  }
130
141
  export declare class RuleGroup {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/types.ts"],"names":[],"mappings":"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","sourcesContent":["// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config\n// so ai-hooks and the Nx validate-code executor share one loader and one config file.\nimport { RuleOptions } from '@webpieces/rules-config';\nexport { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError, RuleFailError } from '@webpieces/rules-config';\nimport { FixHint } from './fix-hint';\nimport { L0_FAULT_NONE } from './l0-fault-codes';\n\n// 'Read' is a first-class member because read-stale-guard is a file-scoped guard that runs on the\n// Read fast path. It is deliberately NOT in HANDLED_FILE_TOOLS (hook-core), so normalizeToolKind()\n// still returns null for it and Read never enters the edit/file rule pipeline — only the one guard\n// that asks for it. Nothing switches exhaustively on this union; it is carried for logging.\nexport type ToolKind = 'Write' | 'Edit' | 'MultiEdit' | 'Read';\nexport type RuleScope = 'edit' | 'file' | 'bash';\n// Which category of built-in rules a hook invocation runs: code-style 'rules', git/PR/branch\n// 'guards' (the hookGuards section), or 'all' (both categories — used by the openclaw plugin adapter,\n// which is a single before_tool_call hook rather than two split PreToolUse hooks).\nexport type HookMode = 'rules' | 'guards' | 'all';\nexport type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;\n\nexport class Violation {\n readonly line: number;\n readonly snippet: string;\n // Optional per-occurrence override for the `→` line. When omitted, the report falls back\n // to the rule's `FixHint.violation`. Dynamic rules (param name, line count, marker path,\n // branch/PR) pass a specific message here; static rules omit it.\n readonly message: string | undefined;\n editIndex: number | undefined;\n editCount: number | undefined;\n\n constructor(line: number, snippet: string, message?: string) {\n this.line = line;\n this.snippet = snippet;\n this.message = message;\n this.editIndex = undefined;\n this.editCount = undefined;\n }\n}\n\nexport class NormalizedEdit {\n readonly oldString: string;\n readonly newString: string;\n\n constructor(oldString: string, newString: string) {\n this.oldString = oldString;\n this.newString = newString;\n }\n}\n\nexport class NormalizedToolInput {\n readonly filePath: string;\n readonly edits: readonly NormalizedEdit[];\n\n constructor(filePath: string, edits: readonly NormalizedEdit[]) {\n this.filePath = filePath;\n this.edits = edits;\n }\n}\n\nexport class NormalizedBashInput {\n readonly command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n}\n\nexport class EditContext {\n readonly tool: ToolKind;\n readonly editIndex: number;\n readonly editCount: number;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly addedContent: string;\n readonly strippedContent: string;\n readonly lines: readonly string[];\n readonly strippedLines: readonly string[];\n readonly removedContent: string;\n readonly isLineDisabled: IsLineDisabled;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n editIndex: number,\n editCount: number,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n addedContent: string,\n strippedContent: string,\n lines: readonly string[],\n strippedLines: readonly string[],\n removedContent: string,\n isLineDisabled: IsLineDisabled,\n ) {\n this.tool = tool;\n this.editIndex = editIndex;\n this.editCount = editCount;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.addedContent = addedContent;\n this.strippedContent = strippedContent;\n this.lines = lines;\n this.strippedLines = strippedLines;\n this.removedContent = removedContent;\n this.isLineDisabled = isLineDisabled;\n this.options = {};\n }\n}\n\n// A heredoc and its body: `<<EOF` / `<<'EOF'` / `<<-EOF` through the terminator line.\nconst HEREDOC_BODY = /<<-?\\s*(['\"]?)(\\w+)\\1[\\s\\S]*?^\\t*\\2\\s*$/gm;\n\n// A single- or double-quoted span.\nconst QUOTED_SPAN = /'([^']*)'|\"([^\"]*)\"/g;\n\n// Shell syntax that a quoted span is explicitly NOT: separators, pipes, subshells, redirection,\n// expansion. Content carrying any of these is data (a jq filter, a regex, a glob), and unquoting it\n// would hand the segment scanner syntax the shell never sees. See stripProse.\nconst SHELL_METACHARACTER = /[|;&()<>$`]/;\n\nexport class BashContext {\n readonly tool: 'Bash';\n readonly command: string;\n /**\n * `command` with heredoc bodies and prose-in-quotes removed — what a guard should MATCH on, while\n * `command` stays raw for the violation message (a human needs to see what they actually typed).\n *\n * A commit message that merely MENTIONS a blocked command is not that command. This repo's whole\n * subject matter is the git workflow, so its commit messages, docs and the guards' own fix-hint\n * text are full of `git push` / `gh pr …` strings; matching raw text blocks writing about the\n * tooling. A guard that cries wolf is a guard someone eventually turns OFF, which costs far more\n * than the theoretical bypass this opens (`sh -c '<blocked command>'` stops matching). That trade\n * is deliberate: these guards exist to catch a FORGETFUL agent, not an adversarial one.\n *\n * ONLY for blocklist-shaped guards (\"if the command matches X, block\"), where stripping can only\n * ever block LESS. Never use it in an allowlist-shaped guard such as merged-branch-bash-guard,\n * where stripping could turn a permitted recovery command into a blocked one and deadlock the\n * session — there, match on the raw `command`.\n */\n readonly commandCode: string;\n /**\n * The tree this command is JUDGED against — the root of the worktree it actually acts on, which\n * is NOT the shell's cwd whenever the command carries a leading `cd <path> &&` (see\n * EffectiveTreeResolver). Guards read git state, the main-sync cache and the decision log from\n * here, so a command aimed at a linked worktree is judged on that worktree's branch, never the\n * primary clone's.\n */\n readonly workspaceRoot: string;\n /** The directory the command really runs in (post-`cd`). Relative paths resolve against THIS. */\n readonly effectiveCwd: string;\n /** The root owning webpieces.config.json. Equals workspaceRoot except in a linked worktree. */\n readonly governedRoot: string;\n options: RuleOptions;\n\n constructor(command: string, workspaceRoot: string, effectiveCwd?: string, governedRoot?: string) {\n this.tool = 'Bash';\n this.command = command;\n this.commandCode = this.stripProse(command);\n this.workspaceRoot = workspaceRoot;\n this.effectiveCwd = effectiveCwd ?? workspaceRoot;\n this.governedRoot = governedRoot ?? workspaceRoot;\n this.options = {};\n }\n\n /**\n * A quoted span WITHOUT whitespace is kept: `git checkout \"main\"` is a real command with a quoted\n * argument, not prose. One containing whitespace is a sentence, and becomes a single space.\n *\n * ONE exception, and it is a false positive that actually fired: a whitespace-free span carrying\n * SHELL METACHARACTERS is dropped too. Keeping the content strips only the quotes, so a jq filter —\n * `--jq '[.[]|select(.title|test(\"x\"))]|length'` — was handed to the guards as bare shell syntax.\n * Every guard then split it on its `|`, `(` and `;` and saw a segment reading exactly `test`, which\n * whole-repo-build-guard classified as the workspace-wide test script and BLOCKED. There was no\n * build anywhere in that command; it polled npm and GitHub.\n *\n * Quoting is precisely how a shell says \"this is DATA, not syntax\", so honouring the quotes is the\n * correct reading. And because commandCode only ever feeds blocklist-shaped guards, dropping more\n * can only ever block LESS — never turn an allowed command into a refused one.\n */\n private stripProse(command: string): string {\n const withoutHeredocs = command.replace(HEREDOC_BODY, ' ');\n return withoutHeredocs.replace(QUOTED_SPAN, (match: string, single?: string, double?: string): string => {\n const content = single ?? double ?? '';\n if (/\\s/.test(content)) return ' ';\n return SHELL_METACHARACTER.test(content) ? ' ' : content;\n });\n }\n}\n\nexport class FileContext {\n readonly tool: ToolKind;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly currentFileLines: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly projectedFileLines: number;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n currentFileLines: number,\n linesAdded: number,\n linesRemoved: number,\n projectedFileLines: number,\n ) {\n this.tool = tool;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.currentFileLines = currentFileLines;\n this.linesAdded = linesAdded;\n this.linesRemoved = linesRemoved;\n this.projectedFileLines = projectedFileLines;\n this.options = {};\n }\n}\n\n/**\n * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).\n * It carries only metadata + a `check` method — no on/off logic of its own.\n */\nexport interface PlainRule {\n readonly name: string;\n readonly description: string;\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly defaultOptions: RuleOptions;\n readonly fixHint: FixHint;\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[];\n}\n\n/**\n * The runtime contract the runner iterates. Both the built-in rule classes (which extend\n * EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add\n * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.\n */\nexport interface Rule extends PlainRule {\n shouldRun(): boolean;\n}\n\nexport class RuleGroup {\n readonly ruleName: string;\n readonly ruleDescription: string;\n readonly fixHint: FixHint;\n readonly violations: readonly Violation[];\n\n constructor(\n ruleName: string,\n ruleDescription: string,\n fixHint: FixHint,\n violations: readonly Violation[],\n ) {\n this.ruleName = ruleName;\n this.ruleDescription = ruleDescription;\n this.fixHint = fixHint;\n this.violations = violations;\n }\n}\n\nexport class BlockedResult {\n readonly report: string;\n /**\n * The L0 fault this block IS, in the codebook's own letter (see core/l0-fault-codes.ts), or `-` for\n * an ordinary rule block.\n *\n * It rides on the result rather than being re-derived from the report text because the block is\n * decided deep in the runner (fault C in configMissingBlock, fault Y in checkConfigSync) and\n * STAMPED by the adapter at the terminal boundary, several frames up. Scraping the report for a\n * fault would be a second answer to a question the producer already knows.\n */\n readonly fault: string;\n\n constructor(report: string, fault: string = L0_FAULT_NONE) {\n this.report = report;\n this.fault = fault;\n }\n}\n\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/types.ts"],"names":[],"mappings":"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","sourcesContent":["// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config\n// so ai-hooks and the Nx validate-code executor share one loader and one config file.\nimport { RuleOptions } from '@webpieces/rules-config';\nexport { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError, RuleFailError } from '@webpieces/rules-config';\nimport { FixHint } from './fix-hint';\nimport { L0_FAULT_NONE } from './l0-fault-codes';\n\n// 'Read' is a first-class member because read-stale-guard is a file-scoped guard that runs on the\n// Read fast path. It is deliberately NOT in HANDLED_FILE_TOOLS (hook-core), so normalizeToolKind()\n// still returns null for it and Read never enters the edit/file rule pipeline — only the one guard\n// that asks for it. Nothing switches exhaustively on this union; it is carried for logging.\nexport type ToolKind = 'Write' | 'Edit' | 'MultiEdit' | 'Read';\nexport type RuleScope = 'edit' | 'file' | 'bash';\n// Which category of built-in rules a hook invocation runs: code-style 'rules', git/PR/branch\n// 'guards' (the hookGuards section), or 'all' (both categories — used by the openclaw plugin adapter,\n// which is a single before_tool_call hook rather than two split PreToolUse hooks).\nexport type HookMode = 'rules' | 'guards' | 'all';\nexport type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;\n\nexport class Violation {\n readonly line: number;\n readonly snippet: string;\n // Optional per-occurrence override for the `→` line. When omitted, the report falls back\n // to the rule's `FixHint.violation`. Dynamic rules (param name, line count, marker path,\n // branch/PR) pass a specific message here; static rules omit it.\n readonly message: string | undefined;\n editIndex: number | undefined;\n editCount: number | undefined;\n\n constructor(line: number, snippet: string, message?: string) {\n this.line = line;\n this.snippet = snippet;\n this.message = message;\n this.editIndex = undefined;\n this.editCount = undefined;\n }\n}\n\nexport class NormalizedEdit {\n readonly oldString: string;\n readonly newString: string;\n\n constructor(oldString: string, newString: string) {\n this.oldString = oldString;\n this.newString = newString;\n }\n}\n\nexport class NormalizedToolInput {\n readonly filePath: string;\n readonly edits: readonly NormalizedEdit[];\n\n constructor(filePath: string, edits: readonly NormalizedEdit[]) {\n this.filePath = filePath;\n this.edits = edits;\n }\n}\n\nexport class NormalizedBashInput {\n readonly command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n}\n\nexport class EditContext {\n readonly tool: ToolKind;\n readonly editIndex: number;\n readonly editCount: number;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly addedContent: string;\n readonly strippedContent: string;\n readonly lines: readonly string[];\n readonly strippedLines: readonly string[];\n readonly removedContent: string;\n readonly isLineDisabled: IsLineDisabled;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n editIndex: number,\n editCount: number,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n addedContent: string,\n strippedContent: string,\n lines: readonly string[],\n strippedLines: readonly string[],\n removedContent: string,\n isLineDisabled: IsLineDisabled,\n ) {\n this.tool = tool;\n this.editIndex = editIndex;\n this.editCount = editCount;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.addedContent = addedContent;\n this.strippedContent = strippedContent;\n this.lines = lines;\n this.strippedLines = strippedLines;\n this.removedContent = removedContent;\n this.isLineDisabled = isLineDisabled;\n this.options = {};\n }\n}\n\n// A heredoc and its body: `<<EOF` / `<<'EOF'` / `<<-EOF` through the terminator line.\nconst HEREDOC_BODY = /<<-?\\s*(['\"]?)(\\w+)\\1[\\s\\S]*?^\\t*\\2\\s*$/gm;\n\n// A single- or double-quoted span.\nconst QUOTED_SPAN = /'([^']*)'|\"([^\"]*)\"/g;\n\n// Shell syntax that a quoted span is explicitly NOT: separators, pipes, subshells, redirection,\n// expansion. Content carrying any of these is data (a jq filter, a regex, a glob), and unquoting it\n// would hand the segment scanner syntax the shell never sees. See stripProse.\nconst SHELL_METACHARACTER = /[|;&()<>$`]/;\n\nexport class BashContext {\n readonly tool: 'Bash';\n readonly command: string;\n /**\n * `command` with heredoc bodies and prose-in-quotes removed — what a guard should MATCH on, while\n * `command` stays raw for the violation message (a human needs to see what they actually typed).\n *\n * A commit message that merely MENTIONS a blocked command is not that command. This repo's whole\n * subject matter is the git workflow, so its commit messages, docs and the guards' own fix-hint\n * text are full of `git push` / `gh pr …` strings; matching raw text blocks writing about the\n * tooling. A guard that cries wolf is a guard someone eventually turns OFF, which costs far more\n * than the theoretical bypass this opens (`sh -c '<blocked command>'` stops matching). That trade\n * is deliberate: these guards exist to catch a FORGETFUL agent, not an adversarial one.\n *\n * ONLY for blocklist-shaped guards (\"if the command matches X, block\"), where stripping can only\n * ever block LESS. Never use it in an allowlist-shaped guard such as merged-branch-bash-guard,\n * where stripping could turn a permitted recovery command into a blocked one and deadlock the\n * session — there, match on the raw `command`.\n */\n readonly commandCode: string;\n /**\n * The tree this command is JUDGED against — the root of the worktree it actually acts on, which\n * is NOT the shell's cwd whenever the command carries a leading `cd <path> &&` (see\n * EffectiveTreeResolver). Guards read git state, the main-sync cache and the decision log from\n * here, so a command aimed at a linked worktree is judged on that worktree's branch, never the\n * primary clone's.\n */\n readonly workspaceRoot: string;\n /** The directory the command really runs in (post-`cd`). Relative paths resolve against THIS. */\n readonly effectiveCwd: string;\n /** The root owning webpieces.config.json. Equals workspaceRoot except in a linked worktree. */\n readonly governedRoot: string;\n options: RuleOptions;\n\n constructor(command: string, workspaceRoot: string, effectiveCwd?: string, governedRoot?: string) {\n this.tool = 'Bash';\n this.command = command;\n this.commandCode = this.stripProse(command);\n this.workspaceRoot = workspaceRoot;\n this.effectiveCwd = effectiveCwd ?? workspaceRoot;\n this.governedRoot = governedRoot ?? workspaceRoot;\n this.options = {};\n }\n\n /**\n * A quoted span WITHOUT whitespace is kept: `git checkout \"main\"` is a real command with a quoted\n * argument, not prose. One containing whitespace is a sentence, and becomes a single space.\n *\n * ONE exception, and it is a false positive that actually fired: a whitespace-free span carrying\n * SHELL METACHARACTERS is dropped too. Keeping the content strips only the quotes, so a jq filter —\n * `--jq '[.[]|select(.title|test(\"x\"))]|length'` — was handed to the guards as bare shell syntax.\n * Every guard then split it on its `|`, `(` and `;` and saw a segment reading exactly `test`, which\n * whole-repo-build-guard classified as the workspace-wide test script and BLOCKED. There was no\n * build anywhere in that command; it polled npm and GitHub.\n *\n * Quoting is precisely how a shell says \"this is DATA, not syntax\", so honouring the quotes is the\n * correct reading. And because commandCode only ever feeds blocklist-shaped guards, dropping more\n * can only ever block LESS — never turn an allowed command into a refused one.\n */\n private stripProse(command: string): string {\n const withoutHeredocs = command.replace(HEREDOC_BODY, ' ');\n return withoutHeredocs.replace(QUOTED_SPAN, (match: string, single?: string, double?: string): string => {\n const content = single ?? double ?? '';\n if (/\\s/.test(content)) return ' ';\n return SHELL_METACHARACTER.test(content) ? ' ' : content;\n });\n }\n}\n\nexport class FileContext {\n readonly tool: ToolKind;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly currentFileLines: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly projectedFileLines: number;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n currentFileLines: number,\n linesAdded: number,\n linesRemoved: number,\n projectedFileLines: number,\n ) {\n this.tool = tool;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.currentFileLines = currentFileLines;\n this.linesAdded = linesAdded;\n this.linesRemoved = linesRemoved;\n this.projectedFileLines = projectedFileLines;\n this.options = {};\n }\n}\n\n/**\n * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).\n * It carries only metadata + a `check` method — no on/off logic of its own.\n */\nexport interface PlainRule {\n readonly name: string;\n readonly description: string;\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly defaultOptions: RuleOptions;\n readonly fixHint: FixHint;\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[];\n}\n\n/**\n * The runtime contract the runner iterates. Both the built-in rule classes (which extend\n * EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add\n * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.\n */\nexport interface Rule extends PlainRule {\n /**\n * The webpieces.config.json key whose entry configures this rule — NOT necessarily `name`.\n *\n * `name` is the operator identity (what a decision-log line and a deny report carry); `configKey`\n * is the switch. They differ wherever several classes implement one POLICY: all four branch-state\n * guards read `branch-state-guard`, all four PR-lifecycle guards read `pr-lifecycle-guard`. Every\n * consumer that asks \"which section does this live in?\" (filterByMode) or \"does this have an\n * entry?\" (the fault-Y config-sync check) must read THIS field, because asking for `name` there\n * demands entries under keys the validator rejects as retired.\n */\n readonly configKey: string;\n shouldRun(): boolean;\n}\n\nexport class RuleGroup {\n readonly ruleName: string;\n readonly ruleDescription: string;\n readonly fixHint: FixHint;\n readonly violations: readonly Violation[];\n\n constructor(\n ruleName: string,\n ruleDescription: string,\n fixHint: FixHint,\n violations: readonly Violation[],\n ) {\n this.ruleName = ruleName;\n this.ruleDescription = ruleDescription;\n this.fixHint = fixHint;\n this.violations = violations;\n }\n}\n\nexport class BlockedResult {\n readonly report: string;\n /**\n * The L0 fault this block IS, in the codebook's own letter (see core/l0-fault-codes.ts), or `-` for\n * an ordinary rule block.\n *\n * It rides on the result rather than being re-derived from the report text because the block is\n * decided deep in the runner (fault C in configMissingBlock, fault Y in checkConfigSync) and\n * STAMPED by the adapter at the terminal boundary, several frames up. Scraping the report for a\n * fault would be a second answer to a question the producer already knows.\n */\n readonly fault: string;\n\n constructor(report: string, fault: string = L0_FAULT_NONE) {\n this.report = report;\n this.fault = fault;\n }\n}\n\n"]}