@webpieces/ai-hook-rules 0.4.636 → 0.4.638
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/README.md +11 -4
- package/package.json +2 -2
- package/src/adapters/hook-core.js +4 -3
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/setup.js +72 -6
- package/src/bin/setup.js.map +1 -1
- package/src/core/custom-rule-adapter.js +3 -1
- package/src/core/custom-rule-adapter.js.map +1 -1
- package/src/core/decision-log.d.ts +38 -5
- package/src/core/decision-log.js +45 -6
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l1-doc.js +6 -3
- package/src/core/l1-doc.js.map +1 -1
- package/src/core/l2-doc.d.ts +7 -0
- package/src/core/l2-doc.js +274 -0
- package/src/core/l2-doc.js.map +1 -0
- package/src/core/l2-rows.d.ts +97 -0
- package/src/core/l2-rows.js +229 -0
- package/src/core/l2-rows.js.map +1 -0
- package/src/core/load-rules.d.ts +8 -2
- package/src/core/load-rules.js +51 -31
- package/src/core/load-rules.js.map +1 -1
- package/src/core/main-sync-refresh.d.ts +8 -1
- package/src/core/main-sync-refresh.js +9 -2
- package/src/core/main-sync-refresh.js.map +1 -1
- package/src/core/main-sync-timeout.d.ts +32 -0
- package/src/core/main-sync-timeout.js +93 -0
- package/src/core/main-sync-timeout.js.map +1 -0
- package/src/core/rules/branch-creation-guard.js +1 -1
- package/src/core/rules/branch-creation-guard.js.map +1 -1
- package/src/core/rules/catch-error-pattern.js +1 -1
- package/src/core/rules/catch-error-pattern.js.map +1 -1
- package/src/core/rules/feature-branch-guard.d.ts +3 -3
- package/src/core/rules/feature-branch-guard.js +17 -4
- package/src/core/rules/feature-branch-guard.js.map +1 -1
- package/src/core/rules/index.d.ts +15 -1
- package/src/core/rules/index.js +18 -10
- package/src/core/rules/index.js.map +1 -1
- package/src/core/rules/match-rule.js +4 -1
- package/src/core/rules/match-rule.js.map +1 -1
- package/src/core/rules/max-file-lines.js +1 -1
- package/src/core/rules/max-file-lines.js.map +1 -1
- package/src/core/rules/merge-in-progress-guard.d.ts +9 -3
- package/src/core/rules/merge-in-progress-guard.js +9 -4
- package/src/core/rules/merge-in-progress-guard.js.map +1 -1
- package/src/core/rules/merged-branch-bash-guard.d.ts +3 -3
- package/src/core/rules/merged-branch-bash-guard.js +17 -6
- package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
- package/src/core/rules/no-any-unknown.js +1 -1
- package/src/core/rules/no-any-unknown.js.map +1 -1
- package/src/core/rules/no-custom-css.js +1 -1
- package/src/core/rules/no-custom-css.js.map +1 -1
- package/src/core/rules/no-destructure.js +1 -1
- package/src/core/rules/no-destructure.js.map +1 -1
- package/src/core/rules/no-implicit-any.js +1 -1
- package/src/core/rules/no-implicit-any.js.map +1 -1
- package/src/core/rules/no-js-files.js +1 -1
- package/src/core/rules/no-js-files.js.map +1 -1
- package/src/core/rules/no-process-exit-outside-main.js +1 -1
- package/src/core/rules/no-process-exit-outside-main.js.map +1 -1
- package/src/core/rules/no-symbol-di-tokens.js +1 -1
- package/src/core/rules/no-symbol-di-tokens.js.map +1 -1
- package/src/core/rules/no-unmanaged-exceptions.js +1 -1
- package/src/core/rules/no-unmanaged-exceptions.js.map +1 -1
- package/src/core/rules/pr-creation-or-push-guard.d.ts +10 -3
- package/src/core/rules/pr-creation-or-push-guard.js +10 -4
- package/src/core/rules/pr-creation-or-push-guard.js.map +1 -1
- package/src/core/rules/pr-merge-guard.d.ts +3 -3
- package/src/core/rules/pr-merge-guard.js +2 -1
- package/src/core/rules/pr-merge-guard.js.map +1 -1
- package/src/core/rules/read-stale-guard.d.ts +3 -3
- package/src/core/rules/read-stale-guard.js +15 -6
- package/src/core/rules/read-stale-guard.js.map +1 -1
- package/src/core/rules/redirect-how-to-merge-main.d.ts +3 -3
- package/src/core/rules/redirect-how-to-merge-main.js +1 -1
- package/src/core/rules/redirect-how-to-merge-main.js.map +1 -1
- package/src/core/rules/require-return-type.js +1 -1
- package/src/core/rules/require-return-type.js.map +1 -1
- package/src/core/rules/stale-main-bash-guard.d.ts +3 -3
- package/src/core/rules/stale-main-bash-guard.js +5 -4
- package/src/core/rules/stale-main-bash-guard.js.map +1 -1
- package/src/core/rules/throw-cause-required.js +1 -1
- package/src/core/rules/throw-cause-required.js.map +1 -1
- package/src/core/rules/validate-ts-in-src.js +1 -1
- package/src/core/rules/validate-ts-in-src.js.map +1 -1
- package/src/core/rules/whole-repo-build-guard.js +6 -2
- package/src/core/rules/whole-repo-build-guard.js.map +1 -1
- package/src/core/runner.js +41 -22
- package/src/core/runner.js.map +1 -1
- package/src/core/sync-main.d.ts +9 -0
- package/src/core/sync-main.js +17 -1
- package/src/core/sync-main.js.map +1 -1
- package/src/core/types.d.ts +11 -0
- package/src/core/types.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-stale-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/read-stale-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n ReadStaleGuardConfig,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n MainSyncStatus,\n} from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2 } from '../decision-log';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { MergedBranchMessage } from './merged-branch-message';\nimport { StaleMainMessage } from './stale-main-message';\nimport { TreeRecovery } from './tree-recovery';\n\n/**\n * Blocks READS while the checked-out branch is a stale place to read from. TWO states:\n *\n * A. on `main`, and local main is BEHIND origin/main\n * B. on a feature branch whose PR is ALREADY MERGED (a pre-merge snapshot; origin/main has moved\n * past it and a squash merge means its HEAD is not even an ancestor of main)\n *\n * WHY READ, of all tools: either state means the AI reads stale FILE CONTENT and then reasons,\n * plans and writes against code that no longer exists upstream. Blocking the write is too late —\n * the bad premise is already in context. So the block lands on the read. (feature-branch-guard\n * blocks the WRITE in state B; this guard is the read-side half of that same protection, and the\n * two share one recovery message via MergedBranchMessage.)\n *\n * THE DIRTY-TREE ASYMMETRY is deliberate. State A fails OPEN on a dirty tree because `git pull` is\n * then not a guaranteed fast-forward and the agent would be trapped away from the files it needs to\n * resolve the conflict. State B blocks ANYWAY, because its cure — `git checkout -b <new>\n * origin/main` — carries uncommitted changes onto the fresh branch, so there is nothing to resolve\n * and nothing to be trapped by.\n *\n * WHY THIS CANNOT WEDGE: the block is scoped to Read ONLY. Every cure — `git pull origin main`,\n * `pnpm install`, any webpieces upgrade — is a Bash command, and this guard never looks at Bash.\n * So there is no command allowlist to maintain and no way to lock the agent out of its own fix.\n * (`git pull origin main` is explicitly permitted on main by redirect-how-to-merge-main, which\n * returns null when the branch IS main — the two guards are complementary, not stacked.)\n *\n * That scoping is also this guard's HOLE, and it is closed elsewhere rather than here: leaving Bash\n * entirely alone let a session `cat`/`grep`/`ls` the same stale tree the Read block was rejecting,\n * for a whole session, while the logs read \"read-stale-guard handled\". stale-main-bash-guard is the\n * State-A Bash counterpart (as merged-branch-bash-guard is State B's) and blocks only CONTENT-reading\n * commands, never the cure — which is why this guard can stay simple and Read-only.\n *\n * Everything here is FAIL-OPEN. A guard that blocks reads on bad data is far worse than one that\n * misses; every unknown resolves to \"allow\". The four deliberate escape valves:\n *\n * 1. DIRTY TREE — uncommitted work on main means `git pull` is not a guaranteed fast-forward.\n * Blocking reads there would trap the agent: it could not read the files it\n * needs to resolve the very conflict blocking it. Allow. (State A ONLY — see\n * the dirty-tree asymmetry above.)\n * 2. CACHE LAG — we do NOT compare hashes for equality. The cached `originMain` is written by\n * the detached refresher and is arbitrarily old, so `local !== origin` stays\n * true for a while AFTER a successful pull, which would spin the agent forever.\n * Instead: is the cached origin/main an ANCESTOR of local main? If local main\n * already contains it, we are not behind. That flips the instant the pull lands,\n * with no refresher round-trip. This is the single most important line here.\n * 3. CONFIG READ — webpieces.config.json stays readable so the agent can always read-then-edit\n * it to set `mode: OFF`. Its EDIT is already bypassed in runner.ts + hook-core;\n * this closes the read half of that same escape hatch.\n * 4. NO DATA — no cache, cache for another branch, empty originMain (offline), or no local\n * main at all (fresh clone / worktree) → allow.\n *\n * Runs from the Read fast path in hook-core (Read is neither a file-edit nor a bash payload, so it\n * never reaches the runner's rule loop). Fires the detached refresher on every call, which is also\n * what makes reads keep the shared main-sync cache warm for feature-branch-guard.\n */\nexport class ReadStaleGuardRule extends FileRuleBase<ReadStaleGuardConfig> {\n constructor(config: ReadStaleGuardConfig) { super(config, 'read-stale-guard'); }\n\n readonly description = 'Block reads on a branch that is stale to read from — a `main` behind origin/main, or a feature branch whose PR is already merged.';\n override readonly files = ['**/*'];\n override readonly defaultOptions = {\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'This branch is stale to read from — reading it would give you pre-merge/out-of-date content.',\n 'Get onto current code before reading anything else:',\n [\n new Option('On main, behind origin/main → git pull origin main. On an already-merged branch → git fetch origin main && git checkout -b <new-branch> origin/main. Then retry the read.', true),\n new Option(\"If that pull dies with 'fatal: Cannot fast-forward to multiple branches', .git/FETCH_HEAD holds a duplicate line — run 'git fetch --prune origin main' to rewrite it cleanly, then retry the pull.\"),\n new Option('Still allowed right now: Bash that does not read repo files (installs, upgrades, builds, tests, the pull itself, git/gh metadata), all Write/Edit, and reading webpieces.config.json. Content-reading Bash (cat/grep/ls/…) is blocked too on a stale main — see stale-main-bash-guard.'),\n new Option('Disable in webpieces.config.json under hookGuards → read-stale-guard (mode OFF) if intentional.'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n // Outside the workspace root — no jurisdiction.\n if (ctx.relativePath.startsWith('..')) return [];\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n if (branch === null) return this.failOpen(ctx, branch, 'branch-undeterminable');\n\n // Keep the shared cache warm for the next call. Detached; never blocks this read. Fired for\n // BOTH states — the merged-branch signal comes out of that same cache.\n triggerMainSyncRefresh(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? DEFAULT_HANG_TIMEOUT_MINUTES);\n\n // Escape valve 3 — the read half of the config escape hatch. Ahead of BOTH states' blocks so\n // the agent can always read-then-edit the file that turns this guard off.\n if (this.isConfigFile(ctx.relativePath)) return this.allow(ctx, branch, 'webpieces-config-read (escape hatch)');\n\n return branch === 'main'\n ? this.checkStaleMain(ctx, branch)\n : this.checkMergedBranch(ctx, branch);\n }\n\n // State A — on main, possibly behind origin/main.\n private checkStaleMain(ctx: FileContext, branch: string): readonly Violation[] {\n const status = readMainSyncStatus(ctx.workspaceRoot, 'main');\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: we asked for the 'main' entry by key, so\n // a mismatch means the map's key and the entry's own `branch` disagree — a shape bug. Kept so\n // that degrades to an allow. Unreachable in normal operation.\n if (status.branch !== 'main') return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n // Offline / origin unresolvable, or no local main to compare against.\n if (status.originMain === '') return this.failOpen(ctx, branch, 'origin-main-unknown', cache);\n\n // Escape valve 2 — ancestry, NOT equality. See the class comment.\n if (this.contains(ctx.workspaceRoot, status.originMain)) {\n return this.allow(ctx, branch, 'local-main-contains-origin (up to date)', cache);\n }\n\n // Escape valve 1 — a dirty tree means the pull is not a clean fast-forward; do not trap\n // the agent away from the files it needs to resolve it.\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.failOpen(ctx, branch, 'dirty-tree-on-main', cache);\n }\n\n return this.block(ctx, branch, 'on-stale-main', this.staleMainMessage(ctx.workspaceRoot), cache);\n }\n\n /**\n * State B — a feature branch whose PR is already merged. Reads a PRE-MERGE snapshot, so every\n * plan built from it is built on code origin/main has moved past.\n *\n * `branchAlreadyMerged` comes straight from the shared cache (the refresher's `gh pr list --state\n * merged`), so this path spawns nothing. No `gh` / offline → `mergedPr` is '' → not merged → allow,\n * which is the fail-open direction for free.\n *\n * The DIRTY-TREE escape valve is the same one state A has, for the same reason: uncommitted work\n * on a merged branch is work that exists nowhere else, and rescuing it means READING the files it\n * touches. `git checkout -b <new> origin/main` usually carries those changes across — but when it\n * does not (an overlapping change landed in main), a blocked read is an agent that cannot even\n * see what it is about to lose. feature-branch-guard still blocks the EDITS, so the state is\n * surfaced loudly either way; we just refuse to cut off the rescue path.\n */\n private checkMergedBranch(ctx: FileContext, branch: string): readonly Violation[] {\n const status = readMainSyncStatus(ctx.workspaceRoot, branch);\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: the entry was looked up BY `branch`, so\n // a mismatch is a shape bug rather than the old \"cache is for another branch\" state. Kept so\n // such a bug degrades to an allow. Unreachable in normal operation. (A branch the refresh has\n // not seen yet is the `status === null` case above — still fail-open.)\n if (status.branch !== branch) return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n if (!status.branchAlreadyMerged) return this.allow(ctx, branch, 'clean-feature-branch', cache);\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.failOpen(ctx, branch, 'dirty-merged-branch', cache);\n }\n\n const pr = status.mergedPr !== '' ? status.mergedPr : '?';\n return this.block(\n ctx,\n branch,\n `already-merged PR#${pr}`,\n this.mergedMessage(ctx.workspaceRoot, branch, status.mergedPr),\n cache,\n );\n }\n\n // The merged-branch text, told in the flavour of the tree we are standing in: a linked worktree\n // is told to open a NEW worktree off origin/main and reap this dead one; the primary clone is\n // told to branch off origin/main. Neither is ever told to `git checkout main` (fatal in a\n // worktree). Detection is one statSync — see WorktreeService.isLinkedWorktree.\n private mergedMessage(workspaceRoot: string, branch: string, mergedPr: string): string {\n const recovery = new TreeRecovery();\n return new MergedBranchMessage(workspaceRoot).forReads(\n branch, mergedPr, recovery.kindOf(workspaceRoot), workspaceRoot,\n );\n }\n\n // Is `commit` an ancestor of (i.e. already contained in) HEAD? Local-only and fast — no network.\n //\n // spawnSync, not execSync, precisely because the EXIT CODE is the answer and we must tell three\n // outcomes apart: 0 = ancestor (up to date), 1 = cleanly NOT an ancestor (genuinely behind),\n // anything else = git could not answer (bad/pruned object, not a repo) which must fail OPEN.\n // execSync collapses 1 and \"git broke\" into the same thrown Error, so it cannot make that call.\n // Arg-array form also means the commit hash is never parsed by a shell.\n private contains(workspaceRoot: string, commit: string): boolean {\n const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n });\n if (result.status === 0) return true;\n if (result.status === 1) return false;\n return true; // unknown/failed → treat as \"contained\" so the guard allows\n }\n\n private isDirty(workspaceRoot: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git status --porcelain', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return out.trim().length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // Cannot tell → assume dirty, which is the fail-OPEN direction for this guard.\n return true;\n }\n }\n\n private isConfigFile(relativePath: string): boolean {\n return relativePath === 'webpieces.config.json';\n }\n\n // How far behind we are, for the message. Best-effort — a bare \"behind\" reads fine without it.\n private behindCount(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git rev-list --count HEAD..origin/main', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return /^\\d+$/.test(out) ? out : '?';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '?';\n }\n }\n\n // Shared with stale-main-bash-guard (StaleMainMessage) so the two halves of the State-A block can\n // never prescribe different cures. Its \"still allowed\" tail no longer promises EVERY Bash command:\n // content-reading Bash is now blocked too, which is the whole point of the Bash counterpart.\n private staleMainMessage(workspaceRoot: string): string {\n return new StaleMainMessage(workspaceRoot).forReads(this.behindCount(workspaceRoot));\n }\n\n private cacheSummary(status: MainSyncStatus): string {\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `cache=${status.branch} localMain=${status.localMain.slice(0, 8)} originMain=${status.originMain.slice(0, 8)} merged=${merged} ts=${status.timestamp}`;\n }\n\n /**\n * The guard could not ESTABLISH the state it judges on, so it judged nothing.\n *\n * A sibling of allow() rather than a reason string passed to it, because the difference has to\n * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`\n * suffix on the free-text reason, which meant an abstention and a real approval were the same\n * verdict and the abstentions could not be counted — so nobody could tell whether these guards\n * were protecting anything or quietly standing down. Never block on data you could not\n * establish; but say out loud, in a field, that you did not establish it.\n */\n private failOpen(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW_FAIL_OPEN', reason, cache);\n return [];\n }\n\n private allow(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: FileContext, branch: string, reason: string, message: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK_AI_CURE', reason, cache);\n return [new V(1, ctx.relativePath, message)];\n }\n\n private logDecision(ctx: FileContext, branch: string | null, verdict: Verdict, reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('read-stale-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache, L0_FAULT_NONE, MATRIX_L2),\n );\n }\n\n /**\n * The current branch, WITHOUT spawning git on the common path.\n *\n * This runs on EVERY read, so it is the one call whose cost actually matters. Spawning\n * `git rev-parse --abbrev-ref HEAD` measures ~12ms — essentially all process-spawn overhead —\n * whereas `.git/HEAD` is a single tiny file whose read is microseconds. On a feature branch\n * (the overwhelmingly common case) that file read is the ONLY work this guard does before\n * short-circuiting, so reads stay effectively free.\n *\n * Falls back to spawning git whenever `.git/HEAD` cannot answer authoritatively:\n * - `.git` is a FILE, not a dir → we are in a worktree and HEAD lives elsewhere\n * - detached HEAD → the file holds a raw sha, not a `ref:` line\n * - anything unreadable/unexpected\n * The fallback is correct in all those cases; it is just slower, and they are rare.\n */\n private currentBranch(workspaceRoot: string): string | null {\n const fromHead = this.branchFromGitHead(workspaceRoot);\n if (fromHead !== null) return fromHead;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n 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 // Parse `.git/HEAD` (\"ref: refs/heads/<branch>\"). null = cannot answer, caller must fall back.\n private branchFromGitHead(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const gitPath = path.join(workspaceRoot, '.git');\n // A worktree/submodule has `.git` as a file pointing at the real gitdir — HEAD is not here.\n if (!fs.statSync(gitPath).isDirectory()) return null;\n const head = fs.readFileSync(path.join(gitPath, 'HEAD'), 'utf8').trim();\n const match = /^ref:\\s*refs\\/heads\\/(.+)$/.exec(head);\n return match ? match[1] : null; // no match = detached HEAD → fall back\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read-stale-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/read-stale-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport {\n BranchStateGuardConfig,\n BRANCH_STATE_GUARD_KEY,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n MainSyncStatus,\n} from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { hangTimeoutOf } from '../main-sync-timeout';\nimport { logGuardDecision, GuardDecision, Verdict, matrixL2Row } from '../decision-log';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { MergedBranchMessage } from './merged-branch-message';\nimport { StaleMainMessage } from './stale-main-message';\nimport { TreeRecovery } from './tree-recovery';\n\n/**\n * Blocks READS while the checked-out branch is a stale place to read from. TWO states:\n *\n * A. on `main`, and local main is BEHIND origin/main\n * B. on a feature branch whose PR is ALREADY MERGED (a pre-merge snapshot; origin/main has moved\n * past it and a squash merge means its HEAD is not even an ancestor of main)\n *\n * WHY READ, of all tools: either state means the AI reads stale FILE CONTENT and then reasons,\n * plans and writes against code that no longer exists upstream. Blocking the write is too late —\n * the bad premise is already in context. So the block lands on the read. (feature-branch-guard\n * blocks the WRITE in state B; this guard is the read-side half of that same protection, and the\n * two share one recovery message via MergedBranchMessage.)\n *\n * THE DIRTY-TREE ASYMMETRY is deliberate. State A fails OPEN on a dirty tree because `git pull` is\n * then not a guaranteed fast-forward and the agent would be trapped away from the files it needs to\n * resolve the conflict. State B blocks ANYWAY, because its cure — `git checkout -b <new>\n * origin/main` — carries uncommitted changes onto the fresh branch, so there is nothing to resolve\n * and nothing to be trapped by.\n *\n * WHY THIS CANNOT WEDGE: the block is scoped to Read ONLY. Every cure — `git pull origin main`,\n * `pnpm install`, any webpieces upgrade — is a Bash command, and this guard never looks at Bash.\n * So there is no command allowlist to maintain and no way to lock the agent out of its own fix.\n * (`git pull origin main` is explicitly permitted on main by redirect-how-to-merge-main, which\n * returns null when the branch IS main — the two guards are complementary, not stacked.)\n *\n * That scoping is also this guard's HOLE, and it is closed elsewhere rather than here: leaving Bash\n * entirely alone let a session `cat`/`grep`/`ls` the same stale tree the Read block was rejecting,\n * for a whole session, while the logs read \"read-stale-guard handled\". stale-main-bash-guard is the\n * State-A Bash counterpart (as merged-branch-bash-guard is State B's) and blocks only CONTENT-reading\n * commands, never the cure — which is why this guard can stay simple and Read-only.\n *\n * Everything here is FAIL-OPEN. A guard that blocks reads on bad data is far worse than one that\n * misses; every unknown resolves to \"allow\". The four deliberate escape valves:\n *\n * 1. DIRTY TREE — uncommitted work on main means `git pull` is not a guaranteed fast-forward.\n * Blocking reads there would trap the agent: it could not read the files it\n * needs to resolve the very conflict blocking it. Allow. (State A ONLY — see\n * the dirty-tree asymmetry above.)\n * 2. CACHE LAG — we do NOT compare hashes for equality. The cached `originMain` is written by\n * the detached refresher and is arbitrarily old, so `local !== origin` stays\n * true for a while AFTER a successful pull, which would spin the agent forever.\n * Instead: is the cached origin/main an ANCESTOR of local main? If local main\n * already contains it, we are not behind. That flips the instant the pull lands,\n * with no refresher round-trip. This is the single most important line here.\n * 3. CONFIG READ — webpieces.config.json stays readable so the agent can always read-then-edit\n * it to set `mode: OFF`. Its EDIT is already bypassed in runner.ts + hook-core;\n * this closes the read half of that same escape hatch.\n * 4. NO DATA — no cache, cache for another branch, empty originMain (offline), or no local\n * main at all (fresh clone / worktree) → allow.\n *\n * Runs from the Read fast path in hook-core (Read is neither a file-edit nor a bash payload, so it\n * never reaches the runner's rule loop). Fires the detached refresher on every call, which is also\n * what makes reads keep the shared main-sync cache warm for feature-branch-guard.\n */\nexport class ReadStaleGuardRule extends FileRuleBase<BranchStateGuardConfig> {\n constructor(config: BranchStateGuardConfig) { super(config, 'read-stale-guard', BRANCH_STATE_GUARD_KEY); }\n\n readonly description = 'Block reads on a branch that is stale to read from — a `main` behind origin/main, or a feature branch whose PR is already merged.';\n override readonly files = ['**/*'];\n override readonly defaultOptions = {\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'This branch is stale to read from — reading it would give you pre-merge/out-of-date content.',\n 'Get onto current code before reading anything else:',\n [\n new Option('On main, behind origin/main → git pull origin main. On an already-merged branch → git fetch origin main && git checkout -b <new-branch> origin/main. Then retry the read.', true),\n new Option(\"If that pull dies with 'fatal: Cannot fast-forward to multiple branches', .git/FETCH_HEAD holds a duplicate line — run 'git fetch --prune origin main' to rewrite it cleanly, then retry the pull.\"),\n new Option('Still allowed right now: Bash that does not read repo files (installs, upgrades, builds, tests, the pull itself, git/gh metadata), all Write/Edit, and reading webpieces.config.json. Content-reading Bash (cat/grep/ls/…) is blocked too on a stale main — see stale-main-bash-guard.'),\n new Option('Disable in webpieces.config.json under hookGuards → branch-state-guard (mode OFF) if intentional — that one key governs the Write, Read and Bash halves of this policy together.'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n // Outside the workspace root — no jurisdiction.\n if (ctx.relativePath.startsWith('..')) return [];\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n if (branch === null) return this.failOpen(ctx, branch, 'branch-undeterminable');\n\n // Keep the shared cache warm for the next call. Detached; never blocks this read. Fired for\n // BOTH states — the merged-branch signal comes out of that same cache.\n triggerMainSyncRefresh(ctx.workspaceRoot, hangTimeoutOf(this.config));\n\n // Escape valve 3 — the read half of the config escape hatch. Ahead of BOTH states' blocks so\n // the agent can always read-then-edit the file that turns this guard off.\n if (this.isConfigFile(ctx.relativePath)) return this.allow(ctx, branch, 'webpieces-config-read (escape hatch)');\n\n return branch === 'main'\n ? this.checkStaleMain(ctx, branch)\n : this.checkMergedBranch(ctx, branch);\n }\n\n // State A — on main, possibly behind origin/main.\n private checkStaleMain(ctx: FileContext, branch: string): readonly Violation[] {\n const status = readMainSyncStatus(ctx.workspaceRoot, 'main');\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: we asked for the 'main' entry by key, so\n // a mismatch means the map's key and the entry's own `branch` disagree — a shape bug. Kept so\n // that degrades to an allow. Unreachable in normal operation.\n if (status.branch !== 'main') return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n // Offline / origin unresolvable, or no local main to compare against.\n if (status.originMain === '') return this.failOpen(ctx, branch, 'origin-main-unknown', cache);\n\n // Escape valve 2 — ancestry, NOT equality. See the class comment.\n if (this.contains(ctx.workspaceRoot, status.originMain)) {\n return this.allow(ctx, branch, 'local-main-contains-origin (up to date)', cache);\n }\n\n // Escape valve 1 — a dirty tree means the pull is not a clean fast-forward; do not trap\n // the agent away from the files it needs to resolve it.\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.failOpen(ctx, branch, 'dirty-tree-on-main', cache);\n }\n\n return this.block(ctx, branch, 'on-stale-main', this.staleMainMessage(ctx.workspaceRoot), cache);\n }\n\n /**\n * State B — a feature branch whose PR is already merged. Reads a PRE-MERGE snapshot, so every\n * plan built from it is built on code origin/main has moved past.\n *\n * `branchAlreadyMerged` comes straight from the shared cache (the refresher's `gh pr list --state\n * merged`), so this path spawns nothing. No `gh` / offline → `mergedPr` is '' → not merged → allow,\n * which is the fail-open direction for free.\n *\n * The DIRTY-TREE escape valve is the same one state A has, for the same reason: uncommitted work\n * on a merged branch is work that exists nowhere else, and rescuing it means READING the files it\n * touches. `git checkout -b <new> origin/main` usually carries those changes across — but when it\n * does not (an overlapping change landed in main), a blocked read is an agent that cannot even\n * see what it is about to lose. feature-branch-guard still blocks the EDITS, so the state is\n * surfaced loudly either way; we just refuse to cut off the rescue path.\n */\n private checkMergedBranch(ctx: FileContext, branch: string): readonly Violation[] {\n const status = readMainSyncStatus(ctx.workspaceRoot, branch);\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: the entry was looked up BY `branch`, so\n // a mismatch is a shape bug rather than the old \"cache is for another branch\" state. Kept so\n // such a bug degrades to an allow. Unreachable in normal operation. (A branch the refresh has\n // not seen yet is the `status === null` case above — still fail-open.)\n if (status.branch !== branch) return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n // NOT-MERGED, or NOT-ASKED? `branchAlreadyMerged: false` is produced both by \"this branch has\n // no merged PR\" and by \"the forge could not be reached\" (`gh` missing, unauthenticated,\n // rate-limited, offline). Same allow either way — never block on data you could not establish\n // — but the LOG must not call the second one an approval, or the trail cannot tell a policy\n // that is protecting something from one that is quietly standing down.\n if (!status.branchAlreadyMerged) {\n return status.forgeReachable\n ? this.allow(ctx, branch, 'clean-feature-branch', cache)\n : this.failOpen(ctx, branch, 'no-forge', cache);\n }\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.failOpen(ctx, branch, 'dirty-merged-branch', cache);\n }\n\n const pr = status.mergedPr !== '' ? status.mergedPr : '?';\n return this.block(\n ctx,\n branch,\n `already-merged PR#${pr}`,\n this.mergedMessage(ctx.workspaceRoot, branch, status.mergedPr),\n cache,\n );\n }\n\n // The merged-branch text, told in the flavour of the tree we are standing in: a linked worktree\n // is told to open a NEW worktree off origin/main and reap this dead one; the primary clone is\n // told to branch off origin/main. Neither is ever told to `git checkout main` (fatal in a\n // worktree). Detection is one statSync — see WorktreeService.isLinkedWorktree.\n private mergedMessage(workspaceRoot: string, branch: string, mergedPr: string): string {\n const recovery = new TreeRecovery();\n return new MergedBranchMessage(workspaceRoot).forReads(\n branch, mergedPr, recovery.kindOf(workspaceRoot), workspaceRoot,\n );\n }\n\n // Is `commit` an ancestor of (i.e. already contained in) HEAD? Local-only and fast — no network.\n //\n // spawnSync, not execSync, precisely because the EXIT CODE is the answer and we must tell three\n // outcomes apart: 0 = ancestor (up to date), 1 = cleanly NOT an ancestor (genuinely behind),\n // anything else = git could not answer (bad/pruned object, not a repo) which must fail OPEN.\n // execSync collapses 1 and \"git broke\" into the same thrown Error, so it cannot make that call.\n // Arg-array form also means the commit hash is never parsed by a shell.\n private contains(workspaceRoot: string, commit: string): boolean {\n const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n });\n if (result.status === 0) return true;\n if (result.status === 1) return false;\n return true; // unknown/failed → treat as \"contained\" so the guard allows\n }\n\n private isDirty(workspaceRoot: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git status --porcelain', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n return out.trim().length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // Cannot tell → assume dirty, which is the fail-OPEN direction for this guard.\n return true;\n }\n }\n\n private isConfigFile(relativePath: string): boolean {\n return relativePath === 'webpieces.config.json';\n }\n\n // How far behind we are, for the message. Best-effort — a bare \"behind\" reads fine without it.\n private behindCount(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git rev-list --count HEAD..origin/main', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return /^\\d+$/.test(out) ? out : '?';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '?';\n }\n }\n\n // Shared with stale-main-bash-guard (StaleMainMessage) so the two halves of the State-A block can\n // never prescribe different cures. Its \"still allowed\" tail no longer promises EVERY Bash command:\n // content-reading Bash is now blocked too, which is the whole point of the Bash counterpart.\n private staleMainMessage(workspaceRoot: string): string {\n return new StaleMainMessage(workspaceRoot).forReads(this.behindCount(workspaceRoot));\n }\n\n private cacheSummary(status: MainSyncStatus): string {\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `cache=${status.branch} localMain=${status.localMain.slice(0, 8)} originMain=${status.originMain.slice(0, 8)} merged=${merged} ts=${status.timestamp}`;\n }\n\n /**\n * The guard could not ESTABLISH the state it judges on, so it judged nothing.\n *\n * A sibling of allow() rather than a reason string passed to it, because the difference has to\n * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`\n * suffix on the free-text reason, which meant an abstention and a real approval were the same\n * verdict and the abstentions could not be counted — so nobody could tell whether these guards\n * were protecting anything or quietly standing down. Never block on data you could not\n * establish; but say out loud, in a field, that you did not establish it.\n */\n private failOpen(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW_FAIL_OPEN', reason, cache);\n return [];\n }\n\n private allow(ctx: FileContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: FileContext, branch: string, reason: string, message: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK_AI_CURE', reason, cache);\n return [new V(1, ctx.relativePath, message)];\n }\n\n private logDecision(ctx: FileContext, branch: string | null, verdict: Verdict, reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('read-stale-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache, L0_FAULT_NONE, matrixL2Row(reason)),\n );\n }\n\n /**\n * The current branch, WITHOUT spawning git on the common path.\n *\n * This runs on EVERY read, so it is the one call whose cost actually matters. Spawning\n * `git rev-parse --abbrev-ref HEAD` measures ~12ms — essentially all process-spawn overhead —\n * whereas `.git/HEAD` is a single tiny file whose read is microseconds. On a feature branch\n * (the overwhelmingly common case) that file read is the ONLY work this guard does before\n * short-circuiting, so reads stay effectively free.\n *\n * Falls back to spawning git whenever `.git/HEAD` cannot answer authoritatively:\n * - `.git` is a FILE, not a dir → we are in a worktree and HEAD lives elsewhere\n * - detached HEAD → the file holds a raw sha, not a `ref:` line\n * - anything unreadable/unexpected\n * The fallback is correct in all those cases; it is just slower, and they are rare.\n */\n private currentBranch(workspaceRoot: string): string | null {\n const fromHead = this.branchFromGitHead(workspaceRoot);\n if (fromHead !== null) return fromHead;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: workspaceRoot,\n encoding: 'utf8',\n 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 // Parse `.git/HEAD` (\"ref: refs/heads/<branch>\"). null = cannot answer, caller must fall back.\n private branchFromGitHead(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const gitPath = path.join(workspaceRoot, '.git');\n // A worktree/submodule has `.git` as a file pointing at the real gitdir — HEAD is not here.\n if (!fs.statSync(gitPath).isDirectory()) return null;\n const head = fs.readFileSync(path.join(gitPath, 'HEAD'), 'utf8').trim();\n const match = /^ref:\\s*refs\\/heads\\/(.+)$/.exec(head);\n return match ? match[1] : null; // no match = detached HEAD → fall back\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrLifecycleGuardConfig } from '@webpieces/rules-config';
|
|
2
2
|
import type { BashContext, Violation } from '../types';
|
|
3
3
|
import { BashRuleBase } from '../rule-base';
|
|
4
4
|
import { FixHint } from '../fix-hint';
|
|
5
|
-
export declare class RedirectHowToMergeMainRule extends BashRuleBase<
|
|
5
|
+
export declare class RedirectHowToMergeMainRule extends BashRuleBase<PrLifecycleGuardConfig> {
|
|
6
6
|
private readonly scanner;
|
|
7
7
|
private readonly recovery;
|
|
8
8
|
private readonly switches;
|
|
9
|
-
constructor(config:
|
|
9
|
+
constructor(config: PrLifecycleGuardConfig);
|
|
10
10
|
readonly description = "Block ALL `git merge`/`git rebase` (any branch, any form) and `git pull origin main` on a feature branch. Use the squash-update process instead.";
|
|
11
11
|
readonly fixHint: FixHint;
|
|
12
12
|
check(ctx: BashContext): readonly Violation[];
|
|
@@ -62,7 +62,7 @@ class RedirectHowToMergeMainRule extends rule_base_1.BashRuleBase {
|
|
|
62
62
|
// feature-switch block, so this guard rejected the very command stale-main-bash-guard prescribes,
|
|
63
63
|
// with a reason ("switches to a feature branch") that was the opposite of the truth.
|
|
64
64
|
switches = new branch_switch_scan_1.BranchSwitchScan(this.scanner);
|
|
65
|
-
constructor(config) { super(config, 'redirect-how-to-merge-main'); }
|
|
65
|
+
constructor(config) { super(config, 'redirect-how-to-merge-main', rules_config_1.PR_LIFECYCLE_GUARD_KEY); }
|
|
66
66
|
description = 'Block ALL `git merge`/`git rebase` (any branch, any form) and `git pull origin main` on a feature branch. Use the squash-update process instead.';
|
|
67
67
|
fixHint = FIX_HINT;
|
|
68
68
|
check(ctx) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-how-to-merge-main.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/redirect-how-to-merge-main.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport { RedirectHowToMergeMainConfig, RepoRootFinder, SyncFlowGuidance, writeTemplate } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { CommandScanner } from '../command-scan';\nimport { TreeRecovery } from './tree-recovery';\nimport { BranchSwitch, BranchSwitchScan } from './branch-switch-scan';\n\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\nconst GUIDANCE = new SyncFlowGuidance();\n\nconst FIX_HINT = new FixHint(\n '`git merge` / `git rebase` are never run by AI — on any branch, in any form.',\n 'To bring main\\'s changes into your feature branch:\\n'\n + '\\n'\n + GUIDANCE.flows().join('\\n') + '\\n'\n + '\\n'\n // The fork-point DERIVATION (why the invariant has two halves, and the three consumers a polluted\n // fork point corrupts) and the WORD-FOR-WORD warning to hand the human are NOT here on purpose.\n // Both are stated in full in the git-workflow doc this hint links by absolute path, and a message\n // that re-derives what the linked doc already says is a second copy to keep in step. What survives\n // here is only what changes what the AI types next.\n + 'Either flow does a 3-point merge, which keeps the fork point a pure main commit — what the build\\n'\n + 'gate\\'s --base and the PR review diff are computed from; a raw merge/rebase destroys it. The\\n'\n + 'gated commands merge internally as child processes this hook never sees.\\n'\n + '\\n'\n + GUIDANCE.readOnlyChecks().join('\\n') + '\\n'\n + '\\n'\n // The \"do not run it, ask the human, tell them to push back\" instruction is NOT repeated here: the\n // violation line the AI reads first already says it. This paragraph's job is only the pointer.\n // The liveness phrase is kept on ONE line deliberately: it is asserted in full, and a wrap would\n // split the assertion's substring across two chunks. The retired wording (\"a branch you do not own\")\n // was pinned only by a `never sync a branch` PREFIX, which passed under both framings — which is how\n // this hint kept teaching the removed rule after the doc it links had replaced it.\n + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above — the full\\n'\n + 'flow (incl. worktrees), the fork-point invariant, why you must\\n'\n + 'never sync a branch something is ACTIVELY holding (liveness, not authorship — a finished agent\\n'\n + 'holds nothing), and the exact words to warn a human with when you ask them to run a merge.\\n'\n // NOT \"add that info to memory\": \"that info\" was the whole git-workflow doc, which is REGENERATED\n // per repo and per version — memorizing it guarantees a stale recall in the next session. Only the\n // invariant is stable enough to persist; the doc must be re-read, never remembered.\n + 'Add to memory: never run a raw `git merge`/`git rebase` — use this repo\\'s gated sync flow and\\n'\n + 're-read its git-workflow doc each time rather than recalling it (regenerated per repo/version).',\n);\n\n// `git merge --abort` / `git rebase --abort|--quit` UNDO an in-progress operation — they cannot create\n// a merge commit or rewrite history, so they cannot violate the fork-point invariant this rule\n// protects. They stay allowed so a repo left mid-operation (e.g. by a human-run rebase) can still be\n// cleaned up. `--continue` is deliberately NOT here: it COMPLETES the operation.\nconst UNDO_FLAG = /--(?:abort|quit)\\b/;\n\n// Typed as a query (\"would this fast-forward?\"), but a successful --ff-only IS the merge.\nconst FF_ONLY = /--ff-only\\b/;\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class RedirectHowToMergeMainRule extends BashRuleBase<RedirectHowToMergeMainConfig> {\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n // \"Which branch does this land on?\" — see branch-switch-scan.ts. It replaced a pair of regexes\n // here (`/git\\s+(?:checkout|switch)\\s+main\\b/` and its negative-lookahead twin) that assumed the\n // branch name is the token immediately after the subcommand. One flag broke both halves at once:\n // `git checkout -q main && git pull -q origin main` missed the main EXEMPTION and then tripped the\n // feature-switch block, so this guard rejected the very command stale-main-bash-guard prescribes,\n // with a reason (\"switches to a feature branch\") that was the opposite of the truth.\n private readonly switches = new BranchSwitchScan(this.scanner);\n\n constructor(config: RedirectHowToMergeMainConfig) { super(config, 'redirect-how-to-merge-main'); }\n\n readonly description = 'Block ALL `git merge`/`git rebase` (any branch, any form) and `git pull origin main` on a feature branch. Use the squash-update process instead.';\n readonly fixHint = FIX_HINT;\n\n check(ctx: BashContext): readonly Violation[] {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n const violation = this.checkSegment(ctx, segment);\n if (violation !== null) return [violation];\n }\n return [];\n }\n\n private checkSegment(ctx: BashContext, segment: string): Violation | null {\n // 1. merge/rebase: unconditional block. Deliberately NO branch lookup.\n //\n // This rule used to read hook-time HEAD and bail out when it was `main`. But a PreToolUse hook\n // runs BEFORE the command, so HEAD-at-hook-time is a value the command itself is about to\n // change: `git checkout feat && git rebase main`, issued while HEAD was still `main` from a\n // prior cleanup, read as \"we're on main, this is fine\" and was waved through. That is the\n // incident this rule exists to prevent. Since merge/rebase have no legitimate AI-run form on\n // ANY branch, there is no branch to consult — and so no HEAD to spoof.\n if (this.scanner.invokesGit(segment, 'merge') || this.scanner.invokesGit(segment, 'rebase')) {\n if (UNDO_FLAG.test(segment)) return null;\n // `--ff-only` reads like a probe (\"can I fast-forward?\") but it MUTATES whenever the answer\n // is yes, so say that here — the AI that typed it was usually only trying to look.\n const probe = FF_ONLY.test(segment)\n ? ' `--ff-only` is NOT a read-only check — it moves your branch whenever it succeeds; see the read-only checks below.'\n : '';\n return this.block(ctx, segment, 'Direct `git merge`/`git rebase` is blocked — AI never runs it, on any branch.' + probe);\n }\n\n // 2. pull: unlike merge/rebase this DOES retain a legitimate on-main form\n // (`git checkout main && git pull origin main`), so it must consult the branch — which is\n // exactly why it also needs the branch-switch check that (1) no longer requires.\n if (this.scanner.invokesGit(segment, 'pull') && /\\borigin\\s+main\\b/.test(segment)) {\n return this.checkPull(ctx, segment);\n }\n\n return null;\n }\n\n private checkPull(ctx: BashContext, segment: string): Violation | null {\n // The two branch questions are COMPLEMENTS of one parse, so they are asked of one parse —\n // that is what keeps them from disagreeing. `git branch -D <x>` is not a checkout so it does\n // not trip this, and `git checkout -` (previous branch, unknowable at hook time) lands\n // nowhere we can name, so it counts as neither.\n const switches = this.switches.switchesIn(ctx.command);\n if (switches.some((s: BranchSwitch): boolean => !this.switches.isExistingMain(s))) {\n return this.block(ctx, segment, 'Blocked: this command switches to a feature branch and then pulls main into it.');\n }\n // The recommended `git checkout main && git pull origin main` — but ONLY in the primary\n // clone. Inside a linked worktree that checkout FATALS (\"'main' is already checked out at\n // <primary>\"), so waving it through here hands the AI a command that cannot work and costs\n // it a turn to discover. Steer to the fetch, which is all a worktree needs.\n if (switches.length > 0) {\n if (this.recovery.kindOf(ctx.workspaceRoot) !== 'worktree') return null;\n // block() appends updateMainSteps for the tree we are in — don't say it twice here.\n return this.block(ctx, segment, 'Blocked.');\n }\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n if (currentBranch === 'main') return null;\n\n return this.block(ctx, segment, `Pulling main into feature branch '${currentBranch}' is blocked.`);\n }\n\n private block(ctx: BashContext, segment: string, what: string): Violation {\n // Materialize the doc we are about to send the AI to. This guard fires long before any `wp-*`\n // command runs (those are what normally write it), so the linked path could easily not exist —\n // and a STALE copy from an older @webpieces is just as misleading, hence overwrite.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n // \"How do I get MAIN itself current?\" is a different question from \"sync my feature branch\",\n // and it used to have no answer anywhere on this path — the flows cover feature branches and\n // the read-only checks cover looking. An AI on main with no third option improvises, and what\n // it improvises is `git reset --hard origin/main`. Answer the question instead, shaped to the\n // tree we are actually in (in a worktree `git checkout main` fatals).\n const updateMain = [\n '',\n 'On main and just wanted to bring MAIN itself up to date? That is a different question from',\n 'syncing a feature branch, and merge/reset is not the answer to it:',\n ...this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)),\n ].join('\\n');\n return new V(\n 1,\n truncate(segment),\n `${what} Use the gated 3-point flow instead: 'pnpm wp-start-update' → 'pnpm wp-finish-update' when NO PR is open, or 'pnpm wp-start-upsert-pr' → 'pnpm wp-finish-upsert-pr' when a PR IS open (required then — the merge rewrites the branch and the PR must be re-pointed in the same run). If you truly need a raw merge/rebase, ask the HUMAN to run it — and warn them to push back. Full flow: READ ${docPath}.${updateMain}`,\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"redirect-how-to-merge-main.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/redirect-how-to-merge-main.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport { PrLifecycleGuardConfig, PR_LIFECYCLE_GUARD_KEY, RepoRootFinder, SyncFlowGuidance, writeTemplate } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { CommandScanner } from '../command-scan';\nimport { TreeRecovery } from './tree-recovery';\nimport { BranchSwitch, BranchSwitchScan } from './branch-switch-scan';\n\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\nconst GUIDANCE = new SyncFlowGuidance();\n\nconst FIX_HINT = new FixHint(\n '`git merge` / `git rebase` are never run by AI — on any branch, in any form.',\n 'To bring main\\'s changes into your feature branch:\\n'\n + '\\n'\n + GUIDANCE.flows().join('\\n') + '\\n'\n + '\\n'\n // The fork-point DERIVATION (why the invariant has two halves, and the three consumers a polluted\n // fork point corrupts) and the WORD-FOR-WORD warning to hand the human are NOT here on purpose.\n // Both are stated in full in the git-workflow doc this hint links by absolute path, and a message\n // that re-derives what the linked doc already says is a second copy to keep in step. What survives\n // here is only what changes what the AI types next.\n + 'Either flow does a 3-point merge, which keeps the fork point a pure main commit — what the build\\n'\n + 'gate\\'s --base and the PR review diff are computed from; a raw merge/rebase destroys it. The\\n'\n + 'gated commands merge internally as child processes this hook never sees.\\n'\n + '\\n'\n + GUIDANCE.readOnlyChecks().join('\\n') + '\\n'\n + '\\n'\n // The \"do not run it, ask the human, tell them to push back\" instruction is NOT repeated here: the\n // violation line the AI reads first already says it. This paragraph's job is only the pointer.\n // The liveness phrase is kept on ONE line deliberately: it is asserted in full, and a wrap would\n // split the assertion's substring across two chunks. The retired wording (\"a branch you do not own\")\n // was pinned only by a `never sync a branch` PREFIX, which passed under both framings — which is how\n // this hint kept teaching the removed rule after the doc it links had replaced it.\n + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above — the full\\n'\n + 'flow (incl. worktrees), the fork-point invariant, why you must\\n'\n + 'never sync a branch something is ACTIVELY holding (liveness, not authorship — a finished agent\\n'\n + 'holds nothing), and the exact words to warn a human with when you ask them to run a merge.\\n'\n // NOT \"add that info to memory\": \"that info\" was the whole git-workflow doc, which is REGENERATED\n // per repo and per version — memorizing it guarantees a stale recall in the next session. Only the\n // invariant is stable enough to persist; the doc must be re-read, never remembered.\n + 'Add to memory: never run a raw `git merge`/`git rebase` — use this repo\\'s gated sync flow and\\n'\n + 're-read its git-workflow doc each time rather than recalling it (regenerated per repo/version).',\n);\n\n// `git merge --abort` / `git rebase --abort|--quit` UNDO an in-progress operation — they cannot create\n// a merge commit or rewrite history, so they cannot violate the fork-point invariant this rule\n// protects. They stay allowed so a repo left mid-operation (e.g. by a human-run rebase) can still be\n// cleaned up. `--continue` is deliberately NOT here: it COMPLETES the operation.\nconst UNDO_FLAG = /--(?:abort|quit)\\b/;\n\n// Typed as a query (\"would this fast-forward?\"), but a successful --ff-only IS the merge.\nconst FF_ONLY = /--ff-only\\b/;\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class RedirectHowToMergeMainRule extends BashRuleBase<PrLifecycleGuardConfig> {\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n // \"Which branch does this land on?\" — see branch-switch-scan.ts. It replaced a pair of regexes\n // here (`/git\\s+(?:checkout|switch)\\s+main\\b/` and its negative-lookahead twin) that assumed the\n // branch name is the token immediately after the subcommand. One flag broke both halves at once:\n // `git checkout -q main && git pull -q origin main` missed the main EXEMPTION and then tripped the\n // feature-switch block, so this guard rejected the very command stale-main-bash-guard prescribes,\n // with a reason (\"switches to a feature branch\") that was the opposite of the truth.\n private readonly switches = new BranchSwitchScan(this.scanner);\n\n constructor(config: PrLifecycleGuardConfig) { super(config, 'redirect-how-to-merge-main', PR_LIFECYCLE_GUARD_KEY); }\n\n readonly description = 'Block ALL `git merge`/`git rebase` (any branch, any form) and `git pull origin main` on a feature branch. Use the squash-update process instead.';\n readonly fixHint = FIX_HINT;\n\n check(ctx: BashContext): readonly Violation[] {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n const violation = this.checkSegment(ctx, segment);\n if (violation !== null) return [violation];\n }\n return [];\n }\n\n private checkSegment(ctx: BashContext, segment: string): Violation | null {\n // 1. merge/rebase: unconditional block. Deliberately NO branch lookup.\n //\n // This rule used to read hook-time HEAD and bail out when it was `main`. But a PreToolUse hook\n // runs BEFORE the command, so HEAD-at-hook-time is a value the command itself is about to\n // change: `git checkout feat && git rebase main`, issued while HEAD was still `main` from a\n // prior cleanup, read as \"we're on main, this is fine\" and was waved through. That is the\n // incident this rule exists to prevent. Since merge/rebase have no legitimate AI-run form on\n // ANY branch, there is no branch to consult — and so no HEAD to spoof.\n if (this.scanner.invokesGit(segment, 'merge') || this.scanner.invokesGit(segment, 'rebase')) {\n if (UNDO_FLAG.test(segment)) return null;\n // `--ff-only` reads like a probe (\"can I fast-forward?\") but it MUTATES whenever the answer\n // is yes, so say that here — the AI that typed it was usually only trying to look.\n const probe = FF_ONLY.test(segment)\n ? ' `--ff-only` is NOT a read-only check — it moves your branch whenever it succeeds; see the read-only checks below.'\n : '';\n return this.block(ctx, segment, 'Direct `git merge`/`git rebase` is blocked — AI never runs it, on any branch.' + probe);\n }\n\n // 2. pull: unlike merge/rebase this DOES retain a legitimate on-main form\n // (`git checkout main && git pull origin main`), so it must consult the branch — which is\n // exactly why it also needs the branch-switch check that (1) no longer requires.\n if (this.scanner.invokesGit(segment, 'pull') && /\\borigin\\s+main\\b/.test(segment)) {\n return this.checkPull(ctx, segment);\n }\n\n return null;\n }\n\n private checkPull(ctx: BashContext, segment: string): Violation | null {\n // The two branch questions are COMPLEMENTS of one parse, so they are asked of one parse —\n // that is what keeps them from disagreeing. `git branch -D <x>` is not a checkout so it does\n // not trip this, and `git checkout -` (previous branch, unknowable at hook time) lands\n // nowhere we can name, so it counts as neither.\n const switches = this.switches.switchesIn(ctx.command);\n if (switches.some((s: BranchSwitch): boolean => !this.switches.isExistingMain(s))) {\n return this.block(ctx, segment, 'Blocked: this command switches to a feature branch and then pulls main into it.');\n }\n // The recommended `git checkout main && git pull origin main` — but ONLY in the primary\n // clone. Inside a linked worktree that checkout FATALS (\"'main' is already checked out at\n // <primary>\"), so waving it through here hands the AI a command that cannot work and costs\n // it a turn to discover. Steer to the fetch, which is all a worktree needs.\n if (switches.length > 0) {\n if (this.recovery.kindOf(ctx.workspaceRoot) !== 'worktree') return null;\n // block() appends updateMainSteps for the tree we are in — don't say it twice here.\n return this.block(ctx, segment, 'Blocked.');\n }\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n if (currentBranch === 'main') return null;\n\n return this.block(ctx, segment, `Pulling main into feature branch '${currentBranch}' is blocked.`);\n }\n\n private block(ctx: BashContext, segment: string, what: string): Violation {\n // Materialize the doc we are about to send the AI to. This guard fires long before any `wp-*`\n // command runs (those are what normally write it), so the linked path could easily not exist —\n // and a STALE copy from an older @webpieces is just as misleading, hence overwrite.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n // \"How do I get MAIN itself current?\" is a different question from \"sync my feature branch\",\n // and it used to have no answer anywhere on this path — the flows cover feature branches and\n // the read-only checks cover looking. An AI on main with no third option improvises, and what\n // it improvises is `git reset --hard origin/main`. Answer the question instead, shaped to the\n // tree we are actually in (in a worktree `git checkout main` fatals).\n const updateMain = [\n '',\n 'On main and just wanted to bring MAIN itself up to date? That is a different question from',\n 'syncing a feature branch, and merge/reset is not the answer to it:',\n ...this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)),\n ].join('\\n');\n return new V(\n 1,\n truncate(segment),\n `${what} Use the gated 3-point flow instead: 'pnpm wp-start-update' → 'pnpm wp-finish-update' when NO PR is open, or 'pnpm wp-start-upsert-pr' → 'pnpm wp-finish-upsert-pr' when a PR IS open (required then — the merge rewrites the branch and the PR must be re-pointed in the same run). If you truly need a raw merge/rebase, ask the HUMAN to run it — and warn them to push back. Full flow: READ ${docPath}.${updateMain}`,\n );\n }\n}\n"]}
|
|
@@ -29,7 +29,7 @@ function isMissingReturnType(line) {
|
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
31
|
class RequireReturnTypeRule extends rule_base_1.EditRuleBase {
|
|
32
|
-
constructor(config) { super(config, 'require-return-type'); }
|
|
32
|
+
constructor(config) { super(config, 'require-return-type', 'require-return-type'); }
|
|
33
33
|
description = 'Every function and method must declare its return type.';
|
|
34
34
|
files = ['**/*.ts', '**/*.tsx'];
|
|
35
35
|
get fixHint() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"require-return-type.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/require-return-type.ts"],"names":[],"mappings":";;;AAAA,0DAA8E;AAG9E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAqD;AAErD,gGAAgG;AAChG,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,kDAAkD,CAAC;AAE7E,8EAA8E;AAC9E,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAEjF,qFAAqF;AACrF,MAAM,aAAa,GAAG,gEAAgE,CAAC;AAEvF,qEAAqE;AACrE,MAAM,eAAe,GAAG,aAAa,CAAC;AAEtC,iEAAiE;AACjE,MAAM,YAAY,GAAG,kFAAkF,CAAC;AAExG,SAAS,mBAAmB,CAAC,IAAY;IACrC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,UAAU,GACZ,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAa,qBAAsB,SAAQ,wBAAqC;IAC5E,YAAY,MAA+B,IAAI,KAAK,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"require-return-type.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/require-return-type.ts"],"names":[],"mappings":";;;AAAA,0DAA8E;AAG9E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAqD;AAErD,gGAAgG;AAChG,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,kDAAkD,CAAC;AAE7E,8EAA8E;AAC9E,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAEjF,qFAAqF;AACrF,MAAM,aAAa,GAAG,gEAAgE,CAAC;AAEvF,qEAAqE;AACrE,MAAM,eAAe,GAAG,aAAa,CAAC;AAEtC,iEAAiE;AACjE,MAAM,YAAY,GAAG,kFAAkF,CAAC;AAExG,SAAS,mBAAmB,CAAC,IAAY;IACrC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,UAAU,GACZ,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAa,qBAAsB,SAAQ,wBAAqC;IAC5E,YAAY,MAA+B,IAAI,KAAK,CAAC,MAAM,EAAE,qBAAqB,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAEpG,WAAW,GAAG,yDAAyD,CAAC;IAC/D,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClD,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,iCAAiC,EACjC,4DAA4D,EAC5D,EAAE,EACF,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,sDAAsD,CAAC,CAChH,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC7C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,mBAAmB,CAAC;gBAAE,SAAS;YAC5F,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AA1BD,sDA0BC","sourcesContent":["import { RequireReturnTypeConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, DisableEscape } from '../fix-hint';\n\n// Matches function/method signatures that don't have `: ReturnType` before the `{` body opener.\n// Pattern: function name(<params>) { — missing `: Type` between `)` and `{`\nconst FUNC_DECL_MISSING = /\\bfunction\\s+\\w+\\s*(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*\\{/;\n\n// Matches class method signatures: indented, optional async, name(<params>) {\nconst METHOD_MISSING = /^\\s{2,}(?:async\\s+)?\\w+\\s*(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*\\{/;\n\n// Arrow function: const name = (async)? (<params>) => — missing `: Type` before `=>`\nconst ARROW_MISSING = /\\bconst\\s+\\w+\\s*=\\s*(?:async\\s+)?(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*=>/;\n\n// Lines that have ): ReturnType before { or => — these are COMPLIANT\nconst HAS_RETURN_TYPE = /\\)\\s*:\\s*\\S/;\n\n// Skip constructors, getters, setters, and control flow keywords\nconst SKIP_PATTERN = /\\b(?:constructor|get\\s+\\w+|set\\s+\\w+|if|else|while|for|switch|catch|return)\\s*\\(/;\n\nfunction isMissingReturnType(line: string): boolean {\n if (SKIP_PATTERN.test(line)) return false;\n const isFuncLike =\n FUNC_DECL_MISSING.test(line) ||\n METHOD_MISSING.test(line) ||\n ARROW_MISSING.test(line);\n if (!isFuncLike) return false;\n if (HAS_RETURN_TYPE.test(line)) return false;\n return true;\n}\n\nexport class RequireReturnTypeRule extends EditRuleBase<RequireReturnTypeConfig> {\n constructor(config: RequireReturnTypeConfig) { super(config, 'require-return-type', 'require-return-type'); }\n\n readonly description = 'Every function and method must declare its return type.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n get fixHint(): FixHint {\n return new FixHint(\n 'Missing return type annotation.',\n 'Add a return type: function foo(x: T): ReturnType { ... }.',\n [],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable require-return-type -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!isMissingReturnType(stripped)) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.REQUIRE_RETURN_TYPE)) continue;\n violations.push(new V(lineNum, ctx.lines[i].trim()));\n }\n return violations;\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BranchStateGuardConfig } from '@webpieces/rules-config';
|
|
2
2
|
import type { BashContext, Violation } from '../types';
|
|
3
3
|
import { BashRuleBase } from '../rule-base';
|
|
4
4
|
import { FixHint } from '../fix-hint';
|
|
@@ -69,8 +69,8 @@ import { FixHint } from '../fix-hint';
|
|
|
69
69
|
* very files in conflict. Never trap the agent away from its own rescue.
|
|
70
70
|
* - reading `webpieces.config.json` (the mode-OFF escape hatch) and `.webpieces/**` → allow
|
|
71
71
|
*/
|
|
72
|
-
export declare class StaleMainBashGuardRule extends BashRuleBase<
|
|
73
|
-
constructor(config:
|
|
72
|
+
export declare class StaleMainBashGuardRule extends BashRuleBase<BranchStateGuardConfig> {
|
|
73
|
+
constructor(config: BranchStateGuardConfig);
|
|
74
74
|
private readonly scanner;
|
|
75
75
|
private readonly recovery;
|
|
76
76
|
private readonly switches;
|
|
@@ -8,6 +8,7 @@ const rule_base_1 = require("../rule-base");
|
|
|
8
8
|
const fix_hint_1 = require("../fix-hint");
|
|
9
9
|
const to_error_1 = require("../to-error");
|
|
10
10
|
const main_sync_refresh_1 = require("../main-sync-refresh");
|
|
11
|
+
const main_sync_timeout_1 = require("../main-sync-timeout");
|
|
11
12
|
const decision_log_1 = require("../decision-log");
|
|
12
13
|
const l0_fault_codes_1 = require("../l0-fault-codes");
|
|
13
14
|
const command_scan_1 = require("../command-scan");
|
|
@@ -83,7 +84,7 @@ const branch_switch_scan_1 = require("./branch-switch-scan");
|
|
|
83
84
|
* - reading `webpieces.config.json` (the mode-OFF escape hatch) and `.webpieces/**` → allow
|
|
84
85
|
*/
|
|
85
86
|
class StaleMainBashGuardRule extends rule_base_1.BashRuleBase {
|
|
86
|
-
constructor(config) { super(config, 'stale-main-bash-guard'); }
|
|
87
|
+
constructor(config) { super(config, 'stale-main-bash-guard', rules_config_1.BRANCH_STATE_GUARD_KEY); }
|
|
87
88
|
scanner = new command_scan_1.CommandScanner();
|
|
88
89
|
recovery = new tree_recovery_1.TreeRecovery();
|
|
89
90
|
switches = new branch_switch_scan_1.BranchSwitchScan(this.scanner);
|
|
@@ -105,7 +106,7 @@ class StaleMainBashGuardRule extends rule_base_1.BashRuleBase {
|
|
|
105
106
|
+ '\nWhichever form applies, the pull must be in the SAME command as the checkout.', true),
|
|
106
107
|
new fix_hint_1.Option('Already on main: git pull --ff-only origin main (then re-run). If that fatals with "Cannot fast-forward to multiple branches", .git/FETCH_HEAD has a duplicate line — run git fetch --prune origin main first.'),
|
|
107
108
|
new fix_hint_1.Option('NOT blocked: `git checkout <sha>`, `git checkout -b <x> origin/main`, `git checkout -- <file>`, any other branch. Also still allowed: builds, tests, installs, the pull itself, all git/gh METADATA (status|log|diff|show|branch), every Write/Edit, and reading webpieces.config.json.'),
|
|
108
|
-
new fix_hint_1.Option('Disable in webpieces.config.json under hookGuards →
|
|
109
|
+
new fix_hint_1.Option('Disable in webpieces.config.json under hookGuards → branch-state-guard (mode OFF) if intentional — that one key governs the Write, Read and Bash halves of this policy together.'),
|
|
109
110
|
]);
|
|
110
111
|
check(ctx) {
|
|
111
112
|
// PREVENTIVE half, FIRST and unconditional. Deliberately ahead of every fail-open bailout
|
|
@@ -119,7 +120,7 @@ class StaleMainBashGuardRule extends rule_base_1.BashRuleBase {
|
|
|
119
120
|
if (branch === null)
|
|
120
121
|
return this.failOpen(ctx, branch, 'branch-undeterminable');
|
|
121
122
|
// Keep the shared cache warm for the next call. Detached; never blocks this command.
|
|
122
|
-
(0, main_sync_refresh_1.triggerMainSyncRefresh)(ctx.workspaceRoot, this.config
|
|
123
|
+
(0, main_sync_refresh_1.triggerMainSyncRefresh)(ctx.workspaceRoot, (0, main_sync_timeout_1.hangTimeoutOf)(this.config));
|
|
123
124
|
// State A is on `main` only. A merged feature branch is merged-branch-bash-guard's job.
|
|
124
125
|
if (branch !== 'main')
|
|
125
126
|
return this.allow(ctx, branch, 'not-on-main (state B is another guard)');
|
|
@@ -262,7 +263,7 @@ class StaleMainBashGuardRule extends rule_base_1.BashRuleBase {
|
|
|
262
263
|
return s.length <= MAX ? s : s.slice(0, MAX) + '…';
|
|
263
264
|
}
|
|
264
265
|
logDecision(ctx, branch, verdict, reason, cache) {
|
|
265
|
-
(0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('stale-main-bash-guard', 'Bash', ctx.command, branch ?? 'unknown', verdict, reason, cache, l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.
|
|
266
|
+
(0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('stale-main-bash-guard', 'Bash', ctx.command, branch ?? 'unknown', verdict, reason, cache, l0_fault_codes_1.L0_FAULT_NONE, (0, decision_log_1.matrixL2Row)(reason)));
|
|
266
267
|
}
|
|
267
268
|
currentBranch(workspaceRoot) {
|
|
268
269
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale-main-bash-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/stale-main-bash-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\n\nimport {\n StaleMainBashGuardConfig,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n MainSyncStatus,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { logGuardDecision, GuardDecision, Verdict, MATRIX_L2 } from '../decision-log';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { CommandScanner } from '../command-scan';\nimport { StaleMainMessage } from './stale-main-message';\nimport { ContentReadScan } from './content-read-scan';\nimport { TreeRecovery } from './tree-recovery';\nimport { BranchSwitchScan } from './branch-switch-scan';\n\n/**\n * The BASH half of the STALE-MAIN protection (read-stale-guard's State A), in two halves of its own:\n * a PREVENTIVE check that stops a session landing on a stale `main`, and the REACTIVE check that\n * contains the damage once it is already there.\n *\n * ── PREVENTIVE: a bare `git checkout main` is blocked; the pull must ride along ──────────────────\n *\n * Everything below this paragraph fires only once the session is ALREADY sitting on a stale `main`.\n * Nothing stopped it ARRIVING there, and arriving is one keystroke. In the incident that added this\n * half, an agent ran `git checkout main` after a merge, in a clone whose local `main` was **157\n * commits behind** origin. That checkout did not merely produce stale files — it reverted:\n *\n * 1. `package.json`'s `@webpieces` pin, to a version OLDER than the installed `node_modules`;\n * 2. `.claude/webpieces/ai-hook.sh` — the version-drift guard ITSELF — to a 157-commit-old copy\n * whose message stated the drift BACKWARDS (\"your installed webpieces is older than required\")\n * and named a single cure, `pnpm install`;\n * 3. and so the agent's judgment: it ran that `pnpm install`, DOWNGRADING `node_modules` to match\n * the stale pin, and had to undo it with the `git pull` that should have come first.\n *\n * The shim on current main already diagnoses drift correctly — it distinguishes \"the pin is newer\"\n * from \"the pin is stale, and `pnpm install` would downgrade you\". None of that helped, because the\n * checkout had replaced the shim with the version that could not say it. **A guard a stale checkout\n * can revert cannot be relied on to catch a stale checkout**, which is why this check is preventive\n * and why it lives here rather than in a second rule: same failure, one step earlier, one switch.\n *\n * It matches on command TEXT alone and asks git nothing. That is not laziness — this runs BEFORE the\n * checkout, so the only `main` it could measure is the one it is about to leave. The interesting\n * `main` does not exist yet, and consulting HEAD-at-hook-time is the exact trap\n * `redirect-how-to-merge-main` documents at length. Pairing is unconditionally correct instead: when\n * `main` is already current the chained pull is a sub-second no-op, so no exception is worth carving.\n *\n * BLOCKED `git checkout main`, `git switch main` — with or without flags — when no `git pull`\n * appears anywhere in the SAME command.\n * ALLOWED `git checkout main && git pull origin main`, the pairing this forces, which is the\n * exact line the post-merge cleanup flow already prescribes.\n * ALLOWED `git checkout -b <x> origin/main` (current by construction), `git checkout <sha>`,\n * `git checkout -- <file>`, and any other branch.\n *\n * ── REACTIVE: content-reading Bash on a stale `main` ─────────────────────────────────────────────\n *\n * read-stale-guard blocks the Read tool when local `main` is behind origin/main — but it looks at\n * nothing else, deliberately: \"every cure is a Bash command, so Bash is the escape hatch — never\n * wedge it.\" That reasoning is right about the CURE and wrong about `cat`/`grep`/`ls`. In the\n * incident this closes, an agent sat on a `main` 18 commits behind origin/main (108 files, +8069\n * −3692 upstream), had its Read tool blocked exactly as designed, and then spent the whole session\n * `ls`-ing, `grep`-ing and `cat`-ing the same stale tree through the side door — describing a CI\n * workflow set that was missing a 186-line workflow which existed upstream. The logs read\n * \"read-stale-guard handled\", which is worse than no guard: it looks covered.\n *\n * So this guard blocks CONTENT-READING Bash only, never the whole shell. Builds, tests, installs,\n * `git pull`, git METADATA (log/diff/show/status) — all still run. What is blocked is a command that\n * would put stale FILE CONTENT into context: `cat`/`head`/`grep`/`rg`/`sed`/`awk`/`ls`/`find`/… of a\n * path inside this workspace, and `git grep` / `git show <rev>:<path>` against a local rev. The same\n * line merged-branch-bash-guard already draws for State B, scoped tighter because State A's cure is\n * one command away and there is no reason to stop anything else.\n *\n * A piped consumer reads stdin, not the tree: `git log --oneline | grep fix` is allowed, because the\n * bytes came from git metadata, not from a stale file. That is why the scan needs the pipe flag.\n *\n * FAIL-OPEN, with read-stale-guard's own escape valves, so it can never wedge a session:\n * - branch undeterminable / not on `main` / no cache / cache for another branch → allow\n * - `originMain` unknown (offline) → allow\n * - origin/main already an ancestor of HEAD (ancestry, NOT equality) → allow the instant the pull lands\n * - DIRTY tree → allow: the pull is not a clean fast-forward, and resolving that means reading the\n * very files in conflict. Never trap the agent away from its own rescue.\n * - reading `webpieces.config.json` (the mode-OFF escape hatch) and `.webpieces/**` → allow\n */\nexport class StaleMainBashGuardRule extends BashRuleBase<StaleMainBashGuardConfig> {\n constructor(config: StaleMainBashGuardConfig) { super(config, 'stale-main-bash-guard'); }\n\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n private readonly switches = new BranchSwitchScan(this.scanner);\n\n readonly description =\n 'Block a bare `git checkout main` (chain the pull into the same command), and block ' +\n 'content-reading Bash (cat/grep/ls/…) while local main is behind origin/main — so a session ' +\n 'neither lands on a stale main nor reasons over one through the side door the Read block leaves.';\n override readonly defaultOptions = {\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'Landing on `main` without pulling, or reading files while main is behind origin/main, both give you stale content.',\n 'Pair the checkout with the pull, or update main and re-run:',\n [\n // TREE-SHAPED, from the one source of tree-shaped cures. A static rule-level hint has no\n // workspace root, so it renders the 'unknown' kind — TreeRecovery's deliberate answer for\n // \"we cannot detect the tree\": both forms, each labelled. That matters here because the\n // primary-clone form (`git checkout main && …`) is BLOCKED by redirect-how-to-merge-main\n // inside a linked worktree, so a preferred option naming it unconditionally hands the AI a\n // cure a sibling guard denies. The per-block message (pairingMessage) is detected and\n // prints exactly one form; this is the fallback for the hint that cannot look.\n new Option(this.recovery.updateMainSteps('unknown').join('\\n')\n + '\\nWhichever form applies, the pull must be in the SAME command as the checkout.', true),\n new Option('Already on main: git pull --ff-only origin main (then re-run). If that fatals with \"Cannot fast-forward to multiple branches\", .git/FETCH_HEAD has a duplicate line — run git fetch --prune origin main first.'),\n new Option('NOT blocked: `git checkout <sha>`, `git checkout -b <x> origin/main`, `git checkout -- <file>`, any other branch. Also still allowed: builds, tests, installs, the pull itself, all git/gh METADATA (status|log|diff|show|branch), every Write/Edit, and reading webpieces.config.json.'),\n new Option('Disable in webpieces.config.json under hookGuards → stale-main-bash-guard (mode OFF) if intentional.'),\n ],\n );\n\n check(ctx: BashContext): readonly Violation[] {\n // PREVENTIVE half, FIRST and unconditional. Deliberately ahead of every fail-open bailout\n // below: those all ask \"is the main we are ON stale?\", and this asks about the main we are\n // about to MOVE TO — a different branch, and one no cache can describe yet.\n const bare = this.bareCheckoutOfMain(ctx);\n if (bare !== null) {\n return this.block(ctx, 'any', `bare checkout of main (${bare})`, this.pairingMessage(ctx), '-');\n }\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n if (branch === null) return this.failOpen(ctx, branch, 'branch-undeterminable');\n\n // Keep the shared cache warm for the next call. Detached; never blocks this command.\n triggerMainSyncRefresh(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? DEFAULT_HANG_TIMEOUT_MINUTES);\n\n // State A is on `main` only. A merged feature branch is merged-branch-bash-guard's job.\n if (branch !== 'main') return this.allow(ctx, branch, 'not-on-main (state B is another guard)');\n\n const status = readMainSyncStatus(ctx.workspaceRoot, 'main');\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: we asked for the 'main' entry by key, so\n // a mismatch means the map's key and the entry's own `branch` disagree — a shape bug. Kept so\n // that degrades to an allow. Unreachable in normal operation.\n if (status.branch !== 'main') return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n // Offline / origin unresolvable — we have nothing to be stale RELATIVE TO.\n if (status.originMain === '') return this.failOpen(ctx, branch, 'origin-main-unknown', cache);\n\n // Ancestry, not equality: the moment the pull lands (or we are simply ahead), we are current.\n if (this.contains(ctx.workspaceRoot, status.originMain)) {\n return this.allow(ctx, branch, 'local-main-contains-origin (up to date)', cache);\n }\n\n // A dirty tree means the pull is not a clean fast-forward. Do not cut the agent off from the\n // files it must read to resolve that — the same valve read-stale-guard opens.\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.failOpen(ctx, branch, 'dirty-tree-on-main', cache);\n }\n\n const reader = this.staleContentRead(ctx);\n if (reader === null) return this.allow(ctx, branch, 'not-a-content-read (cure/build/metadata)', cache);\n\n return this.block(ctx, branch, `stale-main content read (${reader})`, this.staleMessage(ctx.workspaceRoot), cache);\n }\n\n /**\n * The first segment that switches to the `main` BRANCH with no `git pull` anywhere in the same\n * command, or null. The pull is looked for across the WHOLE command, not the matched segment,\n * because `git checkout main && git pull origin main` splits into two segments and the pairing is\n * the point.\n */\n private bareCheckoutOfMain(ctx: BashContext): string | null {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n // BranchSwitchScan answers \"which branch does this land on\" for both guards, flag-tolerantly:\n // `git checkout -q main` lands on main exactly as the bare form does, while\n // `git checkout -b x origin/main` (creates), `git checkout -- main` (pathspec) and\n // `git checkout <sha>` do not. See branch-switch-scan.ts for why that lives in one place.\n if (!this.switches.landsOnExistingMain(segment)) continue;\n return this.scanner.commandInvokesAnyGit(ctx.command, ['pull']) ? null : segment;\n }\n return null;\n }\n\n private pairingMessage(ctx: BashContext): string {\n const steps = this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)).join('\\n');\n // Deliberately SHORT. The incident that bought this guard (a main 157 commits behind; the\n // downgrade the reverted shim then prescribed) is maintainer material and lives in the class\n // docblock above — the reader of THIS text needs only what changes what they type.\n return 'Blocked: a bare `git checkout main` lands you on whatever local `main` you last had — '\n + 'stale files, plus a reverted @webpieces pin and guard shim, so the drift guard then '\n + 'reports the drift BACKWARDS. Chain the pull into the same command:\\n' + steps;\n }\n\n // The first segment that would read stale workspace content, or null when none does. The RAW\n // command is scanned, not commandCode: this is a blocklist-shaped guard, so stripping quoted\n // prose can only ever block LESS (see BashContext.commandCode).\n private staleContentRead(ctx: BashContext): string | null {\n const scan = new ContentReadScan(this.scanner, ctx.workspaceRoot, ctx.effectiveCwd);\n for (const segment of this.scanner.segmentsWithPipes(ctx.command)) {\n const hit = scan.readsStaleContent(segment);\n if (hit !== null) return hit;\n }\n return null;\n }\n\n // Is `commit` already contained in HEAD? Exit code IS the answer, so spawnSync: 0 = ancestor,\n // 1 = genuinely behind, anything else = git could not tell → fail OPEN. (Mirrors read-stale-guard.)\n private contains(workspaceRoot: string, commit: string): boolean {\n const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n });\n if (result.status === 0) return true;\n if (result.status === 1) return false;\n return true;\n }\n\n private isDirty(workspaceRoot: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git status --porcelain', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n });\n return out.trim().length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return true; // cannot tell → assume dirty, the fail-OPEN direction here\n }\n }\n\n private staleMessage(workspaceRoot: string): string {\n return new StaleMainMessage(workspaceRoot).forBash(this.behindCount(workspaceRoot));\n }\n\n private behindCount(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git rev-list --count HEAD..origin/main', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return /^\\d+$/.test(out) ? out : '?';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '?';\n }\n }\n\n private cacheSummary(status: MainSyncStatus): string {\n return `cache=${status.branch} localMain=${status.localMain.slice(0, 8)} originMain=${status.originMain.slice(0, 8)} ts=${status.timestamp}`;\n }\n\n /**\n * The guard could not ESTABLISH the state it judges on, so it judged nothing.\n *\n * A sibling of allow() rather than a reason string passed to it, because the difference has to\n * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`\n * suffix on the free-text reason, which meant an abstention and a real approval were the same\n * verdict and the abstentions could not be counted — so nobody could tell whether these guards\n * were protecting anything or quietly standing down. Never block on data you could not\n * establish; but say out loud, in a field, that you did not establish it.\n */\n private failOpen(ctx: BashContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW_FAIL_OPEN', reason, cache);\n return [];\n }\n\n private allow(ctx: BashContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: BashContext, branch: string, reason: string, message: string, cache: string): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK_AI_CURE', reason, cache);\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, branch: string | null, verdict: Verdict, reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('stale-main-bash-guard', 'Bash', ctx.command, branch ?? 'unknown', verdict, reason, cache, L0_FAULT_NONE, MATRIX_L2),\n );\n }\n\n private currentBranch(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\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"]}
|
|
1
|
+
{"version":3,"file":"stale-main-bash-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/stale-main-bash-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAoD;AAEpD,0DAMiC;AAGjC,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA8C;AAC9C,0CAAsC;AACtC,4DAA8D;AAC9D,4DAAqD;AACrD,kDAAwF;AACxF,sDAAkD;AAClD,kDAAiD;AACjD,6DAAwD;AACxD,2DAAsD;AACtD,mDAA+C;AAC/C,6DAAwD;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,MAAa,sBAAuB,SAAQ,wBAAoC;IAC5E,YAAY,MAA8B,IAAI,KAAK,CAAC,MAAM,EAAE,uBAAuB,EAAE,qCAAsB,CAAC,CAAC,CAAC,CAAC;IAE9F,OAAO,GAAG,IAAI,6BAAc,EAAE,CAAC;IAC/B,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAC9B,QAAQ,GAAG,IAAI,qCAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAEtD,WAAW,GAChB,qFAAqF;QACrF,6FAA6F;QAC7F,iGAAiG,CAAC;IACpF,cAAc,GAAG;QAC/B,kBAAkB,EAAE,2CAA4B;KACnD,CAAC;IACO,OAAO,GAAG,IAAI,kBAAO,CAC1B,oHAAoH,EACpH,6DAA6D,EAC7D;QACI,yFAAyF;QACzF,0FAA0F;QAC1F,wFAAwF;QACxF,yFAAyF;QACzF,2FAA2F;QAC3F,sFAAsF;QACtF,+EAA+E;QAC/E,IAAI,iBAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;cACxD,iFAAiF,EAAE,IAAI,CAAC;QAC9F,IAAI,iBAAM,CAAC,gNAAgN,CAAC;QAC5N,IAAI,iBAAM,CAAC,yRAAyR,CAAC;QACrS,IAAI,iBAAM,CAAC,kLAAkL,CAAC;KACjM,CACJ,CAAC;IAEF,KAAK,CAAC,GAAgB;QAClB,0FAA0F;QAC1F,2FAA2F;QAC3F,4EAA4E;QAC5E,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,0BAA0B,IAAI,GAAG,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QACpG,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACrD,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,uBAAuB,CAAC,CAAC;QAEhF,qFAAqF;QACrF,IAAA,0CAAsB,EAAC,GAAG,CAAC,aAAa,EAAE,IAAA,iCAAa,EAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAEtE,wFAAwF;QACxF,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,wCAAwC,CAAC,CAAC;QAEhG,MAAM,MAAM,GAAG,IAAA,iCAAkB,EAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QAC7D,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,YAAY,CAAC,CAAC;QAEtF,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACxC,gGAAgG;QAChG,8FAA8F;QAC9F,8DAA8D;QAC9D,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,0BAA0B,EAAE,KAAK,CAAC,CAAC;QACnG,2EAA2E;QAC3E,IAAI,MAAM,CAAC,UAAU,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,qBAAqB,EAAE,KAAK,CAAC,CAAC;QAE9F,8FAA8F;QAC9F,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YACtD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,yCAAyC,EAAE,KAAK,CAAC,CAAC;QACrF,CAAC;QAED,6FAA6F;QAC7F,8EAA8E;QAC9E,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,oBAAoB,EAAE,KAAK,CAAC,CAAC;QACnE,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,0CAA0C,EAAE,KAAK,CAAC,CAAC;QAEvG,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,4BAA4B,MAAM,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC;IACvH,CAAC;IAED;;;;;OAKG;IACK,kBAAkB,CAAC,GAAgB;QACvC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,8FAA8F;YAC9F,4EAA4E;YAC5E,mFAAmF;YACnF,0FAA0F;YAC1F,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC;gBAAE,SAAS;YAC1D,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;QACrF,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,cAAc,CAAC,GAAgB;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChG,0FAA0F;QAC1F,6FAA6F;QAC7F,mFAAmF;QACnF,OAAO,wFAAwF;cACzF,sFAAsF;cACtF,sEAAsE,GAAG,KAAK,CAAC;IACzF,CAAC;IAED,6FAA6F;IAC7F,6FAA6F;IAC7F,gEAAgE;IACxD,gBAAgB,CAAC,GAAgB;QACrC,MAAM,IAAI,GAAG,IAAI,mCAAe,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;QACpF,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAChE,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;YAC5C,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,GAAG,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,8FAA8F;IAC9F,oGAAoG;IAC5F,QAAQ,CAAC,aAAqB,EAAE,MAAc;QAClD,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,YAAY,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;YAC7E,GAAG,EAAE,aAAa;YAClB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACrC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACtC,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,OAAO,CAAC,aAAqB;QACjC,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,IAAA,wBAAQ,EAAC,wBAAwB,EAAE;gBAC3C,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aACxE,CAAC,CAAC;YACH,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,IAAI,CAAC,CAAE,2DAA2D;QAC7E,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,aAAqB;QACtC,OAAO,IAAI,qCAAgB,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC;IACxF,CAAC;IAEO,WAAW,CAAC,aAAqB;QACrC,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,IAAA,wBAAQ,EAAC,wCAAwC,EAAE;gBAC3D,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aACxE,CAAC,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QACzC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,GAAG,CAAC;QACf,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,MAAsB;QACvC,OAAO,SAAS,MAAM,CAAC,MAAM,cAAc,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,eAAe,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,CAAC;IACjJ,CAAC;IAED;;;;;;;;;OASG;IACK,QAAQ,CAAC,GAAgB,EAAE,MAAqB,EAAE,MAAc,EAAE,QAAgB,GAAG;QACzF,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QAChE,OAAO,EAAE,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,GAAgB,EAAE,MAAqB,EAAE,MAAc,EAAE,QAAgB,GAAG;QACtF,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QACtD,OAAO,EAAE,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,GAAgB,EAAE,MAAc,EAAE,MAAc,EAAE,OAAe,EAAE,KAAa;QAC1F,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QAC9D,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,CAAC;IAEO,QAAQ,CAAC,CAAS;QACtB,MAAM,GAAG,GAAG,GAAG,CAAC;QAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;IACvD,CAAC;IAEO,WAAW,CAAC,GAAgB,EAAE,MAAqB,EAAE,OAAgB,EAAE,MAAc,EAAE,KAAa;QACxG,IAAA,+BAAgB,EACZ,GAAG,CAAC,aAAa,EACjB,IAAI,4BAAa,CAAC,uBAAuB,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,8BAAa,EAAE,IAAA,0BAAW,EAAC,MAAM,CAAC,CAAC,CACnJ,CAAC;IACN,CAAC;IAEO,aAAa,CAAC,aAAqB;QACvC,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;gBAC/C,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aACxE,CAAC,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;CACJ;AAxND,wDAwNC","sourcesContent":["import { execSync, spawnSync } from 'child_process';\n\nimport {\n BranchStateGuardConfig,\n BRANCH_STATE_GUARD_KEY,\n DEFAULT_HANG_TIMEOUT_MINUTES,\n readMainSyncStatus,\n MainSyncStatus,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\nimport { toError } from '../to-error';\nimport { triggerMainSyncRefresh } from '../main-sync-refresh';\nimport { hangTimeoutOf } from '../main-sync-timeout';\nimport { logGuardDecision, GuardDecision, Verdict, matrixL2Row } from '../decision-log';\nimport { L0_FAULT_NONE } from '../l0-fault-codes';\nimport { CommandScanner } from '../command-scan';\nimport { StaleMainMessage } from './stale-main-message';\nimport { ContentReadScan } from './content-read-scan';\nimport { TreeRecovery } from './tree-recovery';\nimport { BranchSwitchScan } from './branch-switch-scan';\n\n/**\n * The BASH half of the STALE-MAIN protection (read-stale-guard's State A), in two halves of its own:\n * a PREVENTIVE check that stops a session landing on a stale `main`, and the REACTIVE check that\n * contains the damage once it is already there.\n *\n * ── PREVENTIVE: a bare `git checkout main` is blocked; the pull must ride along ──────────────────\n *\n * Everything below this paragraph fires only once the session is ALREADY sitting on a stale `main`.\n * Nothing stopped it ARRIVING there, and arriving is one keystroke. In the incident that added this\n * half, an agent ran `git checkout main` after a merge, in a clone whose local `main` was **157\n * commits behind** origin. That checkout did not merely produce stale files — it reverted:\n *\n * 1. `package.json`'s `@webpieces` pin, to a version OLDER than the installed `node_modules`;\n * 2. `.claude/webpieces/ai-hook.sh` — the version-drift guard ITSELF — to a 157-commit-old copy\n * whose message stated the drift BACKWARDS (\"your installed webpieces is older than required\")\n * and named a single cure, `pnpm install`;\n * 3. and so the agent's judgment: it ran that `pnpm install`, DOWNGRADING `node_modules` to match\n * the stale pin, and had to undo it with the `git pull` that should have come first.\n *\n * The shim on current main already diagnoses drift correctly — it distinguishes \"the pin is newer\"\n * from \"the pin is stale, and `pnpm install` would downgrade you\". None of that helped, because the\n * checkout had replaced the shim with the version that could not say it. **A guard a stale checkout\n * can revert cannot be relied on to catch a stale checkout**, which is why this check is preventive\n * and why it lives here rather than in a second rule: same failure, one step earlier, one switch.\n *\n * It matches on command TEXT alone and asks git nothing. That is not laziness — this runs BEFORE the\n * checkout, so the only `main` it could measure is the one it is about to leave. The interesting\n * `main` does not exist yet, and consulting HEAD-at-hook-time is the exact trap\n * `redirect-how-to-merge-main` documents at length. Pairing is unconditionally correct instead: when\n * `main` is already current the chained pull is a sub-second no-op, so no exception is worth carving.\n *\n * BLOCKED `git checkout main`, `git switch main` — with or without flags — when no `git pull`\n * appears anywhere in the SAME command.\n * ALLOWED `git checkout main && git pull origin main`, the pairing this forces, which is the\n * exact line the post-merge cleanup flow already prescribes.\n * ALLOWED `git checkout -b <x> origin/main` (current by construction), `git checkout <sha>`,\n * `git checkout -- <file>`, and any other branch.\n *\n * ── REACTIVE: content-reading Bash on a stale `main` ─────────────────────────────────────────────\n *\n * read-stale-guard blocks the Read tool when local `main` is behind origin/main — but it looks at\n * nothing else, deliberately: \"every cure is a Bash command, so Bash is the escape hatch — never\n * wedge it.\" That reasoning is right about the CURE and wrong about `cat`/`grep`/`ls`. In the\n * incident this closes, an agent sat on a `main` 18 commits behind origin/main (108 files, +8069\n * −3692 upstream), had its Read tool blocked exactly as designed, and then spent the whole session\n * `ls`-ing, `grep`-ing and `cat`-ing the same stale tree through the side door — describing a CI\n * workflow set that was missing a 186-line workflow which existed upstream. The logs read\n * \"read-stale-guard handled\", which is worse than no guard: it looks covered.\n *\n * So this guard blocks CONTENT-READING Bash only, never the whole shell. Builds, tests, installs,\n * `git pull`, git METADATA (log/diff/show/status) — all still run. What is blocked is a command that\n * would put stale FILE CONTENT into context: `cat`/`head`/`grep`/`rg`/`sed`/`awk`/`ls`/`find`/… of a\n * path inside this workspace, and `git grep` / `git show <rev>:<path>` against a local rev. The same\n * line merged-branch-bash-guard already draws for State B, scoped tighter because State A's cure is\n * one command away and there is no reason to stop anything else.\n *\n * A piped consumer reads stdin, not the tree: `git log --oneline | grep fix` is allowed, because the\n * bytes came from git metadata, not from a stale file. That is why the scan needs the pipe flag.\n *\n * FAIL-OPEN, with read-stale-guard's own escape valves, so it can never wedge a session:\n * - branch undeterminable / not on `main` / no cache / cache for another branch → allow\n * - `originMain` unknown (offline) → allow\n * - origin/main already an ancestor of HEAD (ancestry, NOT equality) → allow the instant the pull lands\n * - DIRTY tree → allow: the pull is not a clean fast-forward, and resolving that means reading the\n * very files in conflict. Never trap the agent away from its own rescue.\n * - reading `webpieces.config.json` (the mode-OFF escape hatch) and `.webpieces/**` → allow\n */\nexport class StaleMainBashGuardRule extends BashRuleBase<BranchStateGuardConfig> {\n constructor(config: BranchStateGuardConfig) { super(config, 'stale-main-bash-guard', BRANCH_STATE_GUARD_KEY); }\n\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n private readonly switches = new BranchSwitchScan(this.scanner);\n\n readonly description =\n 'Block a bare `git checkout main` (chain the pull into the same command), and block ' +\n 'content-reading Bash (cat/grep/ls/…) while local main is behind origin/main — so a session ' +\n 'neither lands on a stale main nor reasons over one through the side door the Read block leaves.';\n override readonly defaultOptions = {\n hangTimeoutMinutes: DEFAULT_HANG_TIMEOUT_MINUTES,\n };\n readonly fixHint = new FixHint(\n 'Landing on `main` without pulling, or reading files while main is behind origin/main, both give you stale content.',\n 'Pair the checkout with the pull, or update main and re-run:',\n [\n // TREE-SHAPED, from the one source of tree-shaped cures. A static rule-level hint has no\n // workspace root, so it renders the 'unknown' kind — TreeRecovery's deliberate answer for\n // \"we cannot detect the tree\": both forms, each labelled. That matters here because the\n // primary-clone form (`git checkout main && …`) is BLOCKED by redirect-how-to-merge-main\n // inside a linked worktree, so a preferred option naming it unconditionally hands the AI a\n // cure a sibling guard denies. The per-block message (pairingMessage) is detected and\n // prints exactly one form; this is the fallback for the hint that cannot look.\n new Option(this.recovery.updateMainSteps('unknown').join('\\n')\n + '\\nWhichever form applies, the pull must be in the SAME command as the checkout.', true),\n new Option('Already on main: git pull --ff-only origin main (then re-run). If that fatals with \"Cannot fast-forward to multiple branches\", .git/FETCH_HEAD has a duplicate line — run git fetch --prune origin main first.'),\n new Option('NOT blocked: `git checkout <sha>`, `git checkout -b <x> origin/main`, `git checkout -- <file>`, any other branch. Also still allowed: builds, tests, installs, the pull itself, all git/gh METADATA (status|log|diff|show|branch), every Write/Edit, and reading webpieces.config.json.'),\n new Option('Disable in webpieces.config.json under hookGuards → branch-state-guard (mode OFF) if intentional — that one key governs the Write, Read and Bash halves of this policy together.'),\n ],\n );\n\n check(ctx: BashContext): readonly Violation[] {\n // PREVENTIVE half, FIRST and unconditional. Deliberately ahead of every fail-open bailout\n // below: those all ask \"is the main we are ON stale?\", and this asks about the main we are\n // about to MOVE TO — a different branch, and one no cache can describe yet.\n const bare = this.bareCheckoutOfMain(ctx);\n if (bare !== null) {\n return this.block(ctx, 'any', `bare checkout of main (${bare})`, this.pairingMessage(ctx), '-');\n }\n\n const branch = this.currentBranch(ctx.workspaceRoot);\n if (branch === null) return this.failOpen(ctx, branch, 'branch-undeterminable');\n\n // Keep the shared cache warm for the next call. Detached; never blocks this command.\n triggerMainSyncRefresh(ctx.workspaceRoot, hangTimeoutOf(this.config));\n\n // State A is on `main` only. A merged feature branch is merged-branch-bash-guard's job.\n if (branch !== 'main') return this.allow(ctx, branch, 'not-on-main (state B is another guard)');\n\n const status = readMainSyncStatus(ctx.workspaceRoot, 'main');\n if (status === null) return this.failOpen(ctx, branch, 'no-sync-cache', 'cache=none');\n\n const cache = this.cacheSummary(status);\n // BELT-AND-BRACES since the cache became branch-keyed: we asked for the 'main' entry by key, so\n // a mismatch means the map's key and the entry's own `branch` disagree — a shape bug. Kept so\n // that degrades to an allow. Unreachable in normal operation.\n if (status.branch !== 'main') return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);\n // Offline / origin unresolvable — we have nothing to be stale RELATIVE TO.\n if (status.originMain === '') return this.failOpen(ctx, branch, 'origin-main-unknown', cache);\n\n // Ancestry, not equality: the moment the pull lands (or we are simply ahead), we are current.\n if (this.contains(ctx.workspaceRoot, status.originMain)) {\n return this.allow(ctx, branch, 'local-main-contains-origin (up to date)', cache);\n }\n\n // A dirty tree means the pull is not a clean fast-forward. Do not cut the agent off from the\n // files it must read to resolve that — the same valve read-stale-guard opens.\n if (this.isDirty(ctx.workspaceRoot)) {\n return this.failOpen(ctx, branch, 'dirty-tree-on-main', cache);\n }\n\n const reader = this.staleContentRead(ctx);\n if (reader === null) return this.allow(ctx, branch, 'not-a-content-read (cure/build/metadata)', cache);\n\n return this.block(ctx, branch, `stale-main content read (${reader})`, this.staleMessage(ctx.workspaceRoot), cache);\n }\n\n /**\n * The first segment that switches to the `main` BRANCH with no `git pull` anywhere in the same\n * command, or null. The pull is looked for across the WHOLE command, not the matched segment,\n * because `git checkout main && git pull origin main` splits into two segments and the pairing is\n * the point.\n */\n private bareCheckoutOfMain(ctx: BashContext): string | null {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n // BranchSwitchScan answers \"which branch does this land on\" for both guards, flag-tolerantly:\n // `git checkout -q main` lands on main exactly as the bare form does, while\n // `git checkout -b x origin/main` (creates), `git checkout -- main` (pathspec) and\n // `git checkout <sha>` do not. See branch-switch-scan.ts for why that lives in one place.\n if (!this.switches.landsOnExistingMain(segment)) continue;\n return this.scanner.commandInvokesAnyGit(ctx.command, ['pull']) ? null : segment;\n }\n return null;\n }\n\n private pairingMessage(ctx: BashContext): string {\n const steps = this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)).join('\\n');\n // Deliberately SHORT. The incident that bought this guard (a main 157 commits behind; the\n // downgrade the reverted shim then prescribed) is maintainer material and lives in the class\n // docblock above — the reader of THIS text needs only what changes what they type.\n return 'Blocked: a bare `git checkout main` lands you on whatever local `main` you last had — '\n + 'stale files, plus a reverted @webpieces pin and guard shim, so the drift guard then '\n + 'reports the drift BACKWARDS. Chain the pull into the same command:\\n' + steps;\n }\n\n // The first segment that would read stale workspace content, or null when none does. The RAW\n // command is scanned, not commandCode: this is a blocklist-shaped guard, so stripping quoted\n // prose can only ever block LESS (see BashContext.commandCode).\n private staleContentRead(ctx: BashContext): string | null {\n const scan = new ContentReadScan(this.scanner, ctx.workspaceRoot, ctx.effectiveCwd);\n for (const segment of this.scanner.segmentsWithPipes(ctx.command)) {\n const hit = scan.readsStaleContent(segment);\n if (hit !== null) return hit;\n }\n return null;\n }\n\n // Is `commit` already contained in HEAD? Exit code IS the answer, so spawnSync: 0 = ancestor,\n // 1 = genuinely behind, anything else = git could not tell → fail OPEN. (Mirrors read-stale-guard.)\n private contains(workspaceRoot: string, commit: string): boolean {\n const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n });\n if (result.status === 0) return true;\n if (result.status === 1) return false;\n return true;\n }\n\n private isDirty(workspaceRoot: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git status --porcelain', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n });\n return out.trim().length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return true; // cannot tell → assume dirty, the fail-OPEN direction here\n }\n }\n\n private staleMessage(workspaceRoot: string): string {\n return new StaleMainMessage(workspaceRoot).forBash(this.behindCount(workspaceRoot));\n }\n\n private behindCount(workspaceRoot: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const out = execSync('git rev-list --count HEAD..origin/main', {\n cwd: workspaceRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return /^\\d+$/.test(out) ? out : '?';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '?';\n }\n }\n\n private cacheSummary(status: MainSyncStatus): string {\n return `cache=${status.branch} localMain=${status.localMain.slice(0, 8)} originMain=${status.originMain.slice(0, 8)} ts=${status.timestamp}`;\n }\n\n /**\n * The guard could not ESTABLISH the state it judges on, so it judged nothing.\n *\n * A sibling of allow() rather than a reason string passed to it, because the difference has to\n * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'`\n * suffix on the free-text reason, which meant an abstention and a real approval were the same\n * verdict and the abstentions could not be counted — so nobody could tell whether these guards\n * were protecting anything or quietly standing down. Never block on data you could not\n * establish; but say out loud, in a field, that you did not establish it.\n */\n private failOpen(ctx: BashContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW_FAIL_OPEN', reason, cache);\n return [];\n }\n\n private allow(ctx: BashContext, branch: string | null, reason: string, cache: string = '-'): readonly Violation[] {\n this.logDecision(ctx, branch, 'ALLOW', reason, cache);\n return [];\n }\n\n private block(ctx: BashContext, branch: string, reason: string, message: string, cache: string): readonly Violation[] {\n this.logDecision(ctx, branch, 'BLOCK_AI_CURE', reason, cache);\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, branch: string | null, verdict: Verdict, reason: string, cache: string): void {\n logGuardDecision(\n ctx.workspaceRoot,\n new GuardDecision('stale-main-bash-guard', 'Bash', ctx.command, branch ?? 'unknown', verdict, reason, cache, L0_FAULT_NONE, matrixL2Row(reason)),\n );\n }\n\n private currentBranch(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\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"]}
|
|
@@ -19,7 +19,7 @@ const ERROR_MESSAGE_PATTERN = /\berror\d*\.message\b/;
|
|
|
19
19
|
*/
|
|
20
20
|
const CAUSE_PATTERN = /\bcause\s*:\s*error\d*\b/;
|
|
21
21
|
class ThrowCauseRequiredRule extends rule_base_1.EditRuleBase {
|
|
22
|
-
constructor(config) { super(config, 'throw-cause-required'); }
|
|
22
|
+
constructor(config) { super(config, 'throw-cause-required', 'throw-cause-required'); }
|
|
23
23
|
description = 'When rethrowing with added context, chain the original exception: throw new Error("msg", { cause: error })';
|
|
24
24
|
files = ['**/*.ts', '**/*.tsx'];
|
|
25
25
|
get fixHint() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"throw-cause-required.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/throw-cause-required.ts"],"names":[],"mappings":";;;AAAA,0DAA+E;AAG/E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAC7D,8DAA+D;AAE/D;;GAEG;AACH,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAErD;;GAEG;AACH,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AAEtD;;GAEG;AACH,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAEjD,MAAa,sBAAuB,SAAQ,wBAAsC;IAC9E,YAAY,MAAgC,IAAI,KAAK,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"throw-cause-required.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/throw-cause-required.ts"],"names":[],"mappings":";;;AAAA,0DAA+E;AAG/E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAC7D,8DAA+D;AAE/D;;GAEG;AACH,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAErD;;GAEG;AACH,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AAEtD;;GAEG;AACH,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAEjD,MAAa,sBAAuB,SAAQ,wBAAsC;IAC9E,YAAY,MAAgC,IAAI,KAAK,CAAC,MAAM,EAAE,sBAAsB,EAAE,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAEvG,WAAW,GAAG,4GAA4G,CAAC;IAClH,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClD,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,iGAAiG,EACjG,WAAW,EACX;YACI,IAAI,iBAAM,CAAC,kGAAkG,EAAE,IAAI,CAAC;YACpH,IAAI,iBAAM,CAAC,+GAA+G,CAAC;YAC3H,IAAI,iBAAM,CAAC,+HAA+H,CAAC;SAC9I,EACD,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,uDAAuD,CAAC,CACjH,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC;QAEhC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAC1B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAChD,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACpD,IAAI,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAE3C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,oBAAoB,CAAC;gBAAE,SAAS;YAE7F,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC;QAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,IAAA,2CAAsB,EAAC,GAAG,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAC;QAChG,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AAtCD,wDAsCC","sourcesContent":["import { ThrowCauseRequiredConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\nimport { writeTemplateIfMissing } from '../instruct-ai-writer';\n\n/**\n * Matches: throw new SomeClass(\n */\nconst THROW_NEW_PATTERN = /\\bthrow\\s+new\\s+\\w+\\s*\\(/;\n\n/**\n * Matches: error.message or error2.message or error3.message\n */\nconst ERROR_MESSAGE_PATTERN = /\\berror\\d*\\.message\\b/;\n\n/**\n * Matches: cause: error or cause: error2 etc. (the good pattern)\n */\nconst CAUSE_PATTERN = /\\bcause\\s*:\\s*error\\d*\\b/;\n\nexport class ThrowCauseRequiredRule extends EditRuleBase<ThrowCauseRequiredConfig> {\n constructor(config: ThrowCauseRequiredConfig) { super(config, 'throw-cause-required', 'throw-cause-required'); }\n\n readonly description = 'When rethrowing with added context, chain the original exception: throw new Error(\"msg\", { cause: error })';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n get fixHint(): FixHint {\n return new FixHint(\n 'Rethrowing with error.message loses the original stack trace. Use { cause: error } to chain it.',\n 'Pick one:',\n [\n new Option('Remove the try-catch entirely. Letting the original exception bubble is usually the best option.', true),\n new Option('throw new Error(\"add more info here\", { cause: error }); chains the error and preserves the full stack trace.'),\n new Option('throw new SpecificError(\"add info here\", { cause: error }); e.g. new InformAiError(\"what happened for AI\", { cause: error }).'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable throw-cause-required -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n const lines = ctx.strippedLines;\n\n for (let i = 0; i < lines.length; i += 1) {\n const stripped = lines[i];\n if (!THROW_NEW_PATTERN.test(stripped)) continue;\n if (!ERROR_MESSAGE_PATTERN.test(stripped)) continue;\n if (CAUSE_PATTERN.test(stripped)) continue;\n\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.THROW_CAUSE_REQUIRED)) continue;\n\n violations.push(new V(lineNum, ctx.lines[i].trim()));\n }\n\n if (violations.length > 0) writeTemplateIfMissing(ctx.workspaceRoot, 'webpieces.exceptions.md');\n return violations;\n }\n}\n"]}
|
|
@@ -23,7 +23,7 @@ function findProjectRoot(filePath, workspaceRoot) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
class ValidateTsInSrcRule extends rule_base_1.FileRuleBase {
|
|
26
|
-
constructor(config) { super(config, 'validate-ts-in-src'); }
|
|
26
|
+
constructor(config) { super(config, 'validate-ts-in-src', 'validate-ts-in-src'); }
|
|
27
27
|
description = 'Every .ts file must belong to a project\'s src/ directory.';
|
|
28
28
|
files = ['**/*.ts', '**/*.tsx'];
|
|
29
29
|
defaultOptions = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-ts-in-src.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/validate-ts-in-src.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAgF;AAGhF,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA8C;AAE9C,MAAM,qBAAqB,GAAG;IAC1B,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IACrC,WAAW,EAAE,mBAAmB;CACnC,CAAC;AACF,MAAM,0BAA0B,GAAG,CAAC,eAAe,CAAC,CAAC;AAErD,SAAS,eAAe,CAAC,QAAgB,EAAE,aAAqB;IAC5D,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjC,OAAO,GAAG,KAAK,aAAa,IAAI,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC5D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QAC9D,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAa,mBAAoB,SAAQ,wBAAmC;IACxE,YAAY,MAA6B,IAAI,KAAK,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-ts-in-src.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/validate-ts-in-src.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { ValidateTsInSrcConfig, isPathExcluded } from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\n\nconst DEFAULT_EXCLUDE_PATHS = [\n 'node_modules', 'dist', '.nx', '.git',\n '**/*.d.ts', '**/jest.config.ts',\n];\nconst DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];\n\nfunction findProjectRoot(filePath: string, workspaceRoot: string): string | null {\n let dir = path.dirname(filePath);\n while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {\n if (fs.existsSync(path.join(dir, 'project.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nexport class ValidateTsInSrcRule extends FileRuleBase<ValidateTsInSrcConfig> {\n constructor(config: ValidateTsInSrcConfig) { super(config, 'validate-ts-in-src', 'validate-ts-in-src'); }\n\n readonly description = 'Every .ts file must belong to a project\\'s src/ directory.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = {\n excludePaths: DEFAULT_EXCLUDE_PATHS,\n allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,\n };\n readonly fixHint = new FixHint(\n 'TypeScript file is outside a project src/ directory.',\n 'Fix by one of:',\n [\n new Option('Move the file into an existing project\\'s src/ directory, or create a new project with project.json that owns the directory.', true),\n new Option('Add a dir or glob (e.g. \"**/codegen.ts\") to validate-ts-in-src.excludePaths in webpieces.config.json'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const excludePaths = this.config.excludePaths ?? DEFAULT_EXCLUDE_PATHS;\n const allowedRootFiles = this.config.allowedRootFiles ?? DEFAULT_ALLOWED_ROOT_FILES;\n\n // Holistic exclusion (Layer 1 + Layer 2): bare dir names + globs.\n if (isPathExcluded(ctx.relativePath, excludePaths)) return [];\n\n const relParts = ctx.relativePath.split(path.sep);\n if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0] ?? '') >= 0) return [];\n\n const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);\n\n if (!projectRoot) {\n return [new V(\n 1,\n ctx.relativePath,\n 'File is not inside any Nx project. Move it into a project\\'s src/ directory.',\n )];\n }\n\n const relToProject = path.relative(projectRoot, ctx.filePath);\n if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {\n const projectName = path.relative(ctx.workspaceRoot, projectRoot);\n return [new V(\n 1,\n ctx.relativePath,\n `File is inside project \\`${projectName}\\` but outside its src/ directory. Move it into src/.`,\n )];\n }\n\n return [];\n }\n}\n"]}
|
|
@@ -79,7 +79,11 @@ class WholeRepoBuildGuardRule extends rule_base_1.BashRuleBase {
|
|
|
79
79
|
* because this guard has no config entry to read one from — and that is the point.
|
|
80
80
|
*/
|
|
81
81
|
constructor(affectedBuildCommand) {
|
|
82
|
-
|
|
82
|
+
// configKey === name and is DELIBERATELY not a real key: this guard has no
|
|
83
|
+
// webpieces.config.json entry at all (see RETIRED_CONFIG_KEYS), and it is loaded outside the
|
|
84
|
+
// config-driven set so the fault-Y sync check never sees it. Naming it here keeps the
|
|
85
|
+
// AbstractRule contract honest without putting the string in HOOK_GUARD_NAMES.
|
|
86
|
+
super(new rule_base_1.EmptyRuleConfig(), 'whole-repo-build-guard', 'whole-repo-build-guard');
|
|
83
87
|
this.affectedBuildCommand = affectedBuildCommand;
|
|
84
88
|
}
|
|
85
89
|
scanner = new command_scan_1.CommandScanner();
|
|
@@ -225,7 +229,7 @@ class WholeRepoBuildGuardRule extends rule_base_1.BashRuleBase {
|
|
|
225
229
|
return s.length <= MAX ? s : s.slice(0, MAX) + '…';
|
|
226
230
|
}
|
|
227
231
|
logDecision(ctx, verdict, reason) {
|
|
228
|
-
(0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('whole-repo-build-guard', 'Bash', ctx.command, '-', verdict, reason, '-', l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.
|
|
232
|
+
(0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('whole-repo-build-guard', 'Bash', ctx.command, '-', verdict, reason, '-', l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.MATRIX_L2_UNROWED));
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
exports.WholeRepoBuildGuardRule = WholeRepoBuildGuardRule;
|