diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 8b50b918..af31dd6a 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -47,7 +47,7 @@ jobs: - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 - uses: hyperpolymath/a2ml-validate-action@0f8081cdfa293663ec6b204274b4272da302e564 # main (exit-127 comment fix) + uses: hyperpolymath/a2ml-ecosystem/validate-action@aa4b836bd969df2bc58128cb8e3d20bbc88d5e79 # main (exit-127 comment fix) with: path: '.' strict: 'false' @@ -109,7 +109,7 @@ jobs: - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-validate-action@91d662ecd1ac1e00875a7d77dff41933e3afe884 # main + uses: hyperpolymath/k9-ecosystem/validate-action@89f3c2702f4f650a92aa7411502f38da06abd562 # main with: path: '.' strict: 'false' diff --git a/lib/rules/structural_drift.ex b/lib/rules/structural_drift.ex index ed870854..59c984cb 100644 --- a/lib/rules/structural_drift.ex +++ b/lib/rules/structural_drift.ex @@ -976,7 +976,11 @@ defmodule Hypatia.Rules.StructuralDrift do "test/fixtures/", "tests/fixtures/", "scripts/fix-scripts/", - "third_party/" + "third_party/", + # Archived session notes record the tree AS IT WAS. A path that was + # valid when written is not rename drift; "fixing" it would falsify + # the record. + "docs/archive/" ] find_files_by_ext(repo_path, [ @@ -989,13 +993,16 @@ defmodule Hypatia.Rules.StructuralDrift do ".twasm" ]) |> Enum.reject(fn rel -> - rel == "CHANGELOG.md" or Enum.any?(corpus_prefixes, &String.starts_with?(rel, &1)) + rel == "CHANGELOG.md" or Enum.any?(corpus_prefixes, &String.starts_with?(rel, &1)) or + nested_project_doc?(repo_path, rel) end) |> Enum.flat_map(fn rel -> path = Path.join(repo_path, rel) case File.read(path) do - {:ok, content} -> + {:ok, raw} -> + content = strip_full_line_comments(raw, Path.extname(rel)) + # Negative lookbehind: only a *repo-root-relative* `src//` # is rename-drift. A `src/` preceded by another path segment # (`vcl-ut/src/bridges/`, `cli/src/commands/`, `echidna/src/rust/`, @@ -1035,6 +1042,69 @@ defmodule Hypatia.Rules.StructuralDrift do end end + # Drop whole-line comments before matching. A commented-out example is not a + # live claim about the tree. + # + # Measured 2026-07-29 on gitbot-fleet's .machine_readable/INTENT.contractile, + # where SD022 fired on + # + # ; "src/abi/ - formal proofs, changes require re-verification" + # + # inside a `; *REMINDER: List areas where LLMs should check...*` block -- + # template boilerplate, entirely commented out. Same family as the known + # unwrap-rule-matches-comments defect. + # + # Deliberately conservative: only `;` (scheme-shaped contractile/twasm) and + # `#` in TOML-shaped files. Markdown/AsciiDoc are left alone, since `#` there + # is a heading and may legitimately carry a path. + defp strip_full_line_comments(content, ext) do + leaders = + case ext do + e when e in [".contractile", ".twasm"] -> [";"] + e when e in [".toml", ".a2ml"] -> ["#", ";"] + _ -> [] + end + + if leaders == [] do + content + else + content + |> String.split("\n") + |> Enum.reject(fn line -> + t = String.trim_leading(line) + Enum.any?(leaders, &String.starts_with?(t, &1)) + end) + |> Enum.join("\n") + end + end + + # A doc inside a NESTED PROJECT ROOT (a subdirectory carrying its own build + # manifest) describes THAT project, whose layout is governed by its own + # repository -- not by this one. Reporting its paths as this repo's rename + # drift is noise the mirror cannot act on. + # + # Measured 2026-07-29 on gitbot-fleet: `bots/echidnabot/` is a vendored copy + # of the standalone echidnabot repo. Its CANONICAL_SOURCE.md documents + # `src/abi/` and states in the same table that the ABI namespace is + # "owner-managed in standalone; fleet does not need it for deploy". `src/abi/` + # exists upstream and is deliberately absent here, so SD022 fired on a doc + # that is correct. 10 of 11 findings in that repo were this shape. + # + # Only subtrees BELOW the repo root count -- a root manifest means the repo + # itself is the project, and its drift is in scope. + defp nested_project_doc?(repo_path, rel) do + manifests = ["Cargo.toml", "mix.exs", "package.json", "go.mod", "pyproject.toml"] + + rel + |> Path.dirname() + |> Path.split() + |> Enum.scan(fn seg, acc -> Path.join(acc, seg) end) + |> Enum.reject(&(&1 in [".", ""])) + |> Enum.any?(fn dir -> + Enum.any?(manifests, &File.exists?(Path.join([repo_path, dir, &1]))) + end) + end + # Index every `**/src/` directory in the tracked tree (via the file # list, so it respects .gitignore and skips _build/deps), returning the set # of basenames plus the set of real top-level directory names. diff --git a/lib/rules/workflow_audit.ex b/lib/rules/workflow_audit.ex index bad71ce1..dbc5dd77 100644 --- a/lib/rules/workflow_audit.ex +++ b/lib/rules/workflow_audit.ex @@ -522,8 +522,18 @@ defmodule Hypatia.Rules.WorkflowAudit do # Actor-controllable expressions. Anchored loosely so we catch the # expression wherever it appears in a run block — the previous full # `run: |` block-shape match missed inline `run:` single-liners. + # `[\s\S]*?` previously spanned step boundaries: a `run:` in one step and a + # context expression in a LATER step's `env:` block matched as though the + # value were interpolated into the script. Measured 2026-07-29 on + # gitbot-fleet's repo-integrity-guard.yml -- flagged :critical although the + # untrusted values are bound via `env:` and consumed as quoted shell vars + # through `grep -F`, i.e. exactly the mitigation this rule asks for. + # + # `[^\n]` per line, and no line may start a new step (`- name:`/`- uses:`) + # or open a sibling mapping key at step indent, so the match stays inside + # one run block. run_context_re = - ~r/run:[\s\S]*?\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)[^}]*\}\}/m + ~r/run:(?:[^\n]*\n(?!\s*-\s+(?:name|uses|id):|\s{0,10}(?:env|with|if|name|uses):\s))*?[^\n]*\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)[^}]*\}\}/m unsafe_json_payload_re = ~r/-d\s*".*\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)/s @@ -989,7 +999,30 @@ defmodule Hypatia.Rules.WorkflowAudit do Regex.match?(~r/if:[^\n]*steps\.[A-Za-z0-9_-]+\.outputs\./, step_block) and String.contains?(stripped, "GITHUB_OUTPUT") - if Regex.match?(gate_re, step_block) or env_output_gate? do + # Third valid idiom, previously unrecognised: a job-level + # `env:` binds a presence boolean from the secret and steps gate + # on it. Measured 2026-07-29 on gitbot-fleet's instant-sync.yml, + # which does exactly this on every step plus an else branch, and + # was still reported :high. + # + # env: + # HAS_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN != '' }} + # steps: + # - if: env.HAS_TOKEN == 'true' + env_boolean_gate? = + case Regex.run( + ~r/(\w+):\s*\$\{\{\s*secrets\.#{Regex.escape(secret_name)}\s*!=\s*['"]['"]?\s*\}\}/, + stripped + ) do + [_, env_name] -> + Regex.match?(~r/if:[^\n]*env\.#{Regex.escape(env_name)}\b/, stripped) + + _ -> + false + end + + if Regex.match?(gate_re, step_block) or env_output_gate? or + env_boolean_gate? do [] else [