Commit graph

94 commits

Author SHA1 Message Date
Gergő Magyar
f28185d67e
fix(ci): bump publish job to Node 24 for npm OIDC support (#1628)
PR #1627's npm install -g npm@latest step crashed mid-install with MODULE_NOT_FOUND: promise-retry — a known fragility when npm self-upgrades. Node 22's bundled npm is 10.9.x (no OIDC). Fix: bump publish job's node-version to 24, which ships with npm 11.x natively. Package consumers unaffected (this Node version is only used during publish; engines.node is >=22.0.0; ci-tests.yml continues testing on Node 22).
2026-05-16 09:18:08 +01:00
Gergő Magyar
f69c382bcb
fix(ci): engage npm Trusted Publishing OIDC properly (#1627)
First live-fire RC publish after #1610 failed at npm publish with E404. The if: failure() cleanup correctly auto-deleted the partial v-tag and rc-marker, but OIDC never engaged. Root cause: two coordinated upstream bugs.

1. actions/setup-node@v6 with registry-url: writes _authToken into the runner .npmrc AND exports NODE_AUTH_TOKEN from its token: input (defaulting to github.token). npm publish sends GITHUB_TOKEN as the bearer and the registry returns 404. OIDC never tried because npm thinks it already has a credential. See actions/setup-node#1440.

2. The Node 22 runner ships with npm 10.9.x. npm Trusted Publishing OIDC support requires npm >= 11.5.1.

Fix: omit registry-url: from the setup-node step (per the consensus workaround in community discussion #176761), and add npm install -g npm@latest before publish. --provenance flag is NOT added; npm auto-attaches provenance under Trusted Publishing.

Sources:
- https://github.com/actions/setup-node/issues/1440
- https://github.com/orgs/community/discussions/176761
- https://docs.npmjs.com/trusted-publishers/
2026-05-16 08:36:49 +01:00
Gergő Magyar
83fbd4be26
refactor(ci): unify release pipeline under publish.yml (#1610)
Collapse release-candidate.yml into publish.yml so there is exactly one workflow that publishes gitnexus to npm, creates GitHub Releases, and triggers Docker builds — for both release candidates and stable releases. Closes #1609 architecturally.

A first-stage `route` job classifies push-to-main / push-tag / workflow_dispatch into `rc` / `stable` modes and fails closed on malformed shapes. RC path runs rc-guard → ci.yml → publish (mint GitHub App token → checkout with persist-credentials:false → resolve next rc version → atomic v-tag + rc/<SHA> marker push → vtag integrity gate → npm publish via OIDC → GitHub prerelease → if: failure() cleanup) → docker.yml. Stable path verifies package.json matches the tag and publishes to `latest` via OIDC (no docker).

Hardening:

  • Self-trigger prevention via negative-glob `tags: ['v*', '!v*-rc.*']` — the bug class behind #1609 cannot recur.
  • Two distinct actions/checkout steps per mode (no conditional `token:` expression footgun).
  • Workflow-level `permissions: {}` deny-all + per-job grants; `id-token: write` only where OIDC is used.
  • npm Trusted Publishing replaces NPM_TOKEN (delete the secret after the first successful publish).
  • GitHub App installation token (actions/create-github-app-token@v3.2.0) replaces the long-lived RELEASE_PUSH_TOKEN PAT (delete after first successful RC).
  • vtag integrity gate fails closed on empty / mode-mismatched output (prevents Release named `main` from a github.ref fallback).
  • Annotation-injection sanitization on every logged ref.
  • Explicit `secrets:` passthrough on docker.yml (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN); ci.yml no longer inherits anything.
  • `if: failure()` cleanup auto-deletes v-tag + rc-marker on partial failure (eliminates the external-consumer phantom-version ingestion window).
  • ACTIONS_STEP_DEBUG window closed via `set +x` wrap on the inline auth-header compute.
  • Curated retry-loud error handling on `gh api` bot-user-id lookup and `npx semver`.

Pre-merge validation:

  • 10-reviewer multi-agent code-review pass; 14 findings fixed inline (commit 820cefae), 6 deferred to follow-ups.
  • End-to-end dry-run rehearsal via workflow_dispatch (run 25919563064) validated route classification, rc-guard, App token mint, RC checkout, version resolver, vtag synthetic-regex check, and faithful tarball pack at the bumped version.
  • All zizmor findings on the unification commits closed.
  • Branch-protection required checks all green.

Post-merge actions:

  • After the first successful RC, delete the `NPM_TOKEN` and `RELEASE_PUSH_TOKEN` secrets — they are no longer used.
  • The first real RC after merge is the live-fire test for steps dry-run could not exercise (atomic tag push, real npm OIDC handshake, GitHub Release creation, docker.yml under explicit secrets passthrough). The if: failure() cleanup step handles the partial-failure recovery automatically; the Rollback Runbook in CONTRIBUTING.md covers the rare cases auto-cleanup can't reach.
2026-05-16 07:46:56 +01:00
dependabot[bot]
80acaf052f
chore(deps): bump sigstore/cosign-installer from 4.1.1 to 4.1.2 (#1557) 2026-05-14 06:45:14 +01:00
Gergo Magyar
2bf6d078aa ci(claude): allow Bash in code-review job without interactive approval
Claude Code defaults to prompting for Bash approval. In GitHub Actions there
is no human to approve, so gh pr comment and similar commands fail and the
PR receives no review comment. Pass --dangerously-skip-permissions for the
code-review step only (headless CI; token and checkout are already scoped).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 09:07:47 +01:00
Gergo Magyar
c5c42a9649 fix(ci): make /review reliably post PR comments
The Run Claude Code Review step passed an invalid PR ref
(owner/repo/pull/N) which gh interprets as a branch name, causing
early gh pr view failures. More importantly, the prompt omitted
--comment, so the code-review plugin only displayed findings in
terminal output and never invoked gh pr comment to post to the PR.

Switch to a full PR URL and add --comment so the plugin posts the
review during the session, which also routes around upstream bugs
anthropics/claude-code-action#1061 and #1087 where the action's
post-step capture can silently drop output on issue_comment triggers.
2026-05-12 08:28:42 +01:00
Gergő Magyar
5d670a530d
ci(release): skip rc build on release PRs (#1474)
* ci(release): skip rc build on release PRs

Suppress the auto-fired Release Candidate workflow when:
  1. The HEAD commit subject matches `chore: release vX.Y.Z` (the canonical
     release-PR title), or
  2. The squash-merged PR carries the `release` label.

Either match short-circuits the guard to should_run=false. This prevents the
rc cycle from racing publish.yml on the v-tag (as happened on v1.6.4 where
we had to manually cancel the auto-fired RC run after merging PR #1473).

Adds pull-requests: read to the guard job for the label lookup. A failed
gh API call falls through to the existing dedup logic rather than silently
suppressing rc builds.

* ci(release): address PR #1474 review — anchor regex + sanitise log echo

Two minor follow-ups from Claude's review:

1. End-anchor the release-subject regex. The previous shape
   ^chore: release vX.Y.Z would match noisy variants like
   chore: release v1.0.0 (something unrelated). The new shape
   requires either the bare title or the canonical squash-merge
   (#NNNN) suffix exactly.

2. Sanitise HEAD_SUBJECT before echoing to logs. git %s strips
   newlines so LF injection is impossible, but a hypothetical
   subject containing ::error:: or ::set-output:: could otherwise
   forge GitHub Actions annotation entries. Defence-in-depth.

Both findings flagged minor / does not block merge — applying
anyway since they are trivial.
2026-05-10 09:50:58 +01:00
dependabot[bot]
a5c582f547
chore(deps): bump actions/checkout from 5.0.0 to 6.0.2 (#1459)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-09 19:26:53 +01:00
Copilot
6f1cfffdd7
fix(security): Harden CI permissions (#1454)
* Initial plan

* chore(security): harden workflow permissions and pin Docker base image digests

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2ddc8f2b-7355-48cf-9a0b-c06df66c3f47

* fix(security): restore permissions: {} on publish + release-candidate workflows

These two release-publishing workflows had permissions: {} (the strictest valid form) before PR #1454, which replaced it with permissions: read-all. Every job in both files already declares its own permissions block, so the workflow-level default is only the safety net for future jobs added without one — read-all weakens that net for no benefit. Restore {} and the explanatory comment.

Scorecard's TokenPermissions check accepts both forms, so this preserves U9 compliance.

* fix(security): narrow permissions: read-all to contents: read on 13 workflows

PR #1454 added permissions: read-all to 13 workflows that previously had no top-level permissions block. read-all is Scorecard-compliant but unnecessarily broad — every job in scope only needs contents:read at the workflow level (job-level blocks already grant the writes that any job actually performs).

Snapshot of every job in the 13 workflows confirms contents:read is sufficient:

- ci.yml: quality/tests/scope-parity have explicit contents:read job blocks; save-pr-meta uses upload-artifact only (no token scopes needed); ci-status is pure shell.
- ci-e2e.yml, ci-quality.yml, ci-scope-parity.yml, ci-tests.yml: all jobs do checkout + npm + tsc/vitest/playwright/upload-artifact only; no API token scopes required.
- claude.yml, codeql.yml, dependency-review.yml, docker.yml, gitleaks.yml, pr-labeler.yml, trivy.yml, workflow-lint.yml: all jobs already declare their own job-level blocks (security-events:write, pull-requests:write, packages:write, etc.) so the workflow-level default does not gate them.

zizmor (--min-severity high) is clean on the resulting tree. Pre-existing medium findings (secrets-inherit, artipacked) are in unrelated workflows and untouched by this commit.

scorecard.yml also uses read-all but pre-existed PR #1454 and is deferred to a follow-up PR per the plan's scope boundary.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-09 17:58:22 +01:00
Copilot
e02c56f653
fix(security): Pin Docker Node base images, remove runtime package-manager CVE surface, verify Trivy on PRs, and harden Dependabot policy (#1455)
* fix: pin Docker node base images and remediate bundled npm CVEs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e0605c79-296e-4b3a-b6c3-4ad375950935

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: run trivy on docker PR changes and remove corepack

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4d714047-4fc1-4af1-9734-91400a15568f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* chore: add docker digest updates and normalize dockerfile comments

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7d980908-a823-4c28-b074-9134ec672e84

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: add dependabot cooldown policies

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a8531b8d-384b-4c54-84dd-a98b31993c44

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: remove unsupported dependabot cooldown keys

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/166df50e-c2fe-4d7f-ab41-e94c703338f6

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* chore(node): bump CI + engines to Node 22; centralize NPM_VERSION via build ARG

Closes the LOW findings from Claude Final Re-Review on PR #1455:

- Bump engines.node to >=22.0.0 and align all CI workflows (ci-quality,
  pr-autofix, publish, release-candidate) and the composite setup
  actions on Node 22. Node 20 reached EOL on 2026-04-30; the test
  Docker image was already on 22.
- Centralize the bootstrapped npm version in a single ARG NPM_VERSION
  per Dockerfile (cli, web, gitnexus/Dockerfile.test) so a security
  bump only requires updating one default per file with a clear
  cross-reference comment.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-05-09 16:55:31 +01:00
Gergő Magyar
6906be3695
feat(autofix): replace inline reviewdog with /autofix ChatOps button (#1458)
* fix(autofix): verify reviewdog actually posted before claiming "click Apply"

The sticky summary comment was stating "Posted formatting suggestions
inline. Click Apply suggestion on each" even when reviewdog landed zero
inline review comments — typical case: the formatter touched lines
outside the PR's added range, so `-filter-mode=added` (correctly)
filtered everything out. The script unconditionally set `posted=true`
after running reviewdog regardless of whether any comments were
actually created, leaving the user staring at a sticky that promised
buttons that didn't exist.

The publish job now snapshots the count of `github-actions[bot]` review
comments before and after reviewdog. If the delta is zero, surface a
new `diff-no-overlap` UI state that tells the user plainly:

  "Formatter found fixable issues, but they're on lines outside this
  PR's added range — there's nothing to click here. Run locally:
  npm run lint:fix && npm run format."

Plus a matching `gitnexus/autofix` Check Run conclusion (still neutral,
distinct title) so agents reading `gh pr checks` see the same signal.

Three states are now machine-distinguishable in the sticky's
gitnexus-autofix JSON block: suggestions-posted (delta > 0),
diff-no-overlap (delta == 0), skipped-too-large (>3k lines).

* feat(autofix): replace inline reviewdog with /autofix ChatOps button

Pivot the PR autofix UX from per-line reviewdog suggestions to a single
slash-command button. Contributors comment `/autofix` on the PR; a new
trusted workflow downloads the existing autofix patch artifact, applies
it to the PR head, and pushes a commit back.

Why:
- 3K+ diffs hit GitHub's review-comment API 406 limit -> dead end.
- Diffs where the formatter touches lines outside the PR's added range
  ("no-overlap") get filtered by reviewdog's -filter-mode=added -> dead
  end (PR #1457 patched the lying sticky but the underlying UX gap
  remained).
- Per-line click-Apply-suggestion is high-friction for big diffs and
  easy to apply unevenly.
- A single `git apply` + push works at any size and lands fixes
  atomically.

Changes:
- pr-autofix-publish.yml: remove `Install reviewdog` and
  `Post inline suggestions` steps. Collapse three sticky states
  (suggestions-posted, diff-no-overlap, skipped-too-large) into one
  (fixes-available). Bump JSON schema v1 -> v2 with `apply_command`
  field; all v1 fields preserved.
- pr-autofix-apply.yml (new): triggers on issue_comment with body
  `/autofix`, validates body via strict regex, validates commenter
  has write/admin/maintain or is the PR author, locates latest
  successful pr-autofix run for PR head SHA, downloads artifact,
  applies patch, pushes commit. Reacts +1/-1/eyes on triggering
  comment per outcome. Idempotent (`git apply --check --reverse`
  detects already-applied state).
- CONTRIBUTING.md: document v2 schema and the /autofix flow,
  including the maintainer-edit requirement for fork PR pushes.

Trust posture: apply workflow runs from default-branch code only,
under issue_comment trigger. Comment body and author login flow
through env vars and pattern-matched, never interpolated into shell.
Permission gate (write/admin/maintain OR PR author) before any
artifact fetch. Fork PRs require "Allow edits by maintainers"
(GitHub-native; we don't bypass).

Net YAML: -139 lines in publish.yml, +260 in apply.yml. Removes
reviewdog binary pin and the entire review-comment API surface.

* fix(autofix): address Codex adversarial findings on PR #1458

Two findings from the Codex adversarial review of the autofix ChatOps
pivot. Both are localized YAML changes that close trust gaps the pivot
inherited from the original PR #1446 design.

U1 — Cross-verify metadata against workflow_run authority
(.github/workflows/pr-autofix-publish.yml):
  Previously the trusted publisher accepted pr_number, head_sha, and
  head_repo from metadata.json after only an allowlist regex. A
  fork-controlled `npm run lint:fix` could have written a syntactically
  valid metadata.json referencing another PR/SHA, redirecting the
  write-scoped sticky/check-run onto an attacker-chosen target.

  New `Verify metadata against workflow_run authority` step compares
  artifact-claimed identity against:
    - github.event.workflow_run.head_sha
    - github.event.workflow_run.head_repository.full_name
    - workflow_run.pull_requests[].number (within-repo PRs)
    - gh api commits/{sha}/pulls fallback (fork PRs, where
      pull_requests[] is empty)
  Fail closed on mismatch — no sticky, no check-run, no override.

U2 — Lease-protected push in apply workflow
(.github/workflows/pr-autofix-apply.yml):
  Previously the apply step pushed `HEAD:${HEAD_REF}` plain. A force-
  push between resolve (Step 5) and push (Step 9) would silently
  fast-forward an older commit graph over the contributor's newer
  state.

  Push now uses `--force-with-lease=refs/heads/${HEAD_REF}:${HEAD_SHA}`
  against the SHA resolved earlier. Distinct `lease-failed` result code
  + retry-message reply, separated from `push-failed` (fork without
  maintainer-edit) so contributors can diagnose the actual cause.

Plan: docs/plans/2026-05-09-005-fix-autofix-codex-adversarial-findings-plan.md
(local-only per repo convention).

Trust posture preserved: no new permissions, no new workflows, no
contract change. JSON v2 schema unchanged. CodeQL js/server-side-
request-forgery and template-injection posture unchanged — all new
inputs flow via env vars and pattern-matched.

* fix(autofix): close zizmor credential-persistence finding on apply checkout

actions/checkout's default behavior writes the GITHUB_TOKEN into
.git/config as an extraheader. The token then sits on disk in the
checkout directory — an actions/upload-artifact step on that
directory would leak it. We don't upload, but zizmor's
credential-persistence lint correctly flags the latent risk.

Set persist-credentials: false on the Checkout PR head step. Provide
push auth inline via `git -c http.extraheader="Authorization: Basic
<base64-of-x-access-token:TOKEN>"` so the credential never lands on
disk and never appears in process listings (the URL form
https://x-access-token:TOKEN@… is rejected here because it leaks via
ps and git remote -v).

Push lease semantics from U2 unchanged — same --force-with-lease
against the resolved HEAD_SHA, same lease-failed/push-failed/stale
result codes.

* fix(review): apply autofix feedback

ce-code-review surfaced 15 findings on PR #1458; this commit applies
the 7 with concrete fixes (#1, #2, #3, #4, #5, #9, #13). Five P2
findings (#6, #7, #8, #10, #12) are recorded as residual actionable
work for follow-up; two advisory items (#11, #14) skipped.

#1 — applied_run_id schema drift (CONTRIBUTING.md):
  v2 docs claimed `state: applied` enum value and an `applied_run_id`
  field that no code path emits. Trimmed docs to match what the
  workflow actually writes (state: fixes-available; v1 field set as
  superset). Implementing the apply-side sticky upsert that would
  populate `applied_run_id` is deferred — cleaner than carrying a
  contract claim with no code.

#2 — result= unset between idempotency probe and lease push
(pr-autofix-apply.yml):
  After `git apply --check` passed, an early non-zero exit from
  `git config` / `git apply` / `git add` / `git commit` left
  `result=` unset, sending the user to the `*` "unexpected state
  (`unknown`)" arm. Wrapped the apply/commit phase in a single
  if-test that sets `result=apply-failed` on any failure. New
  React-and-reply branch surfaces an actionable message.

#3 — permission lookup conflated transient API failures with denial
(pr-autofix-apply.yml):
  `gh api … 2>/dev/null || echo "none"` swallowed 5xx, 429 secondary
  rate-limit, and network failures, surfacing them as a public 👎
  refusal to legitimate maintainers. Now distinguishes 404
  (genuine non-collaborator) from other API failures via stderr
  match. New `allowed=api-failed` state triggers a 😕 reaction with
  a "transient API failure, retry" reply instead of a misleading
  refusal.

#4 — lease-failure grep missed git's "remote rejected" / branch-
deleted phrasings (pr-autofix-apply.yml):
  Real lease failures got classified as `push-failed` →
  user told to enable maintainer-edit, which won't help. Expanded
  regex to match `remote rejected` and `! [rejected]`.

#5 — broken bullet continuation in CONTRIBUTING.md release-candidate
section: rejoined the split bullet so it renders correctly.

#9 — base64 GITHUB_TOKEN bypassed GitHub's secret-masker
(pr-autofix-apply.yml):
  Added `::add-mask::${auth_header}` immediately after construction
  so any subsequent log line (set -x, GIT_TRACE) gets *** redacted.

#13 — misleading schema-bump comment in pr-autofix-publish.yml:
  Comment claimed all v1 fields preserved exactly, but the `state`
  enum was redefined v1→v2. Updated to make the migration path
  explicit (v1 readers see unfamiliar schema, fall back to prose).

Residual actionable work (deferred to follow-up):
  #6 locate step gh api retry; #7 artifact-expired graceful fallback;
  #8 re-entrancy comment-spam guard; #10 producer-still-running UX;
  #12 gh_retry wrapper for apply.yml.

Validations: yaml.safe_load OK, check-workflow-concurrency.py OK.

* fix(autofix): apply remaining ce-code-review residual findings (#6, #7, #8, #10, #12)

Pulls the deferred items from the previous review pass into this PR so
the workflow ships with full reliability + UX coverage rather than
follow-up debt.

#6 + #12 — gh_retry wrapper on idempotent GETs in apply.yml:
  Permission lookup, PR metadata fetch, and workflow-run lookup are now
  wrapped in the same gh_retry helper publish.yml uses (3 attempts,
  linear backoff). Reaction/comment POSTs remain unwrapped (retrying
  POST would dupe the resource).

#10 — producer-still-running UX:
  The locate step now distinguishes three cases via `found_status`
  output: success (proceed), in-progress / queued / pending / waiting
  (reply  "wait for autofix run to finish"), not-found (reply 🤔
  "push a commit"), api-failed (reply ⚠️ "transient API failure"). The
  "no successful autofix run" message no longer fires immediately after
  a fresh push while the producer is still mid-run.

#7 — artifact-expired graceful fallback:
  actions/download-artifact gains `continue-on-error: true`. The apply
  step distinguishes patch-file-missing (artifact expired, 1-day
  retention elapsed) from patch-file-zero-bytes (formatter found
  nothing). New `result=artifact-expired` case +  "push a new commit
  to regenerate" reply.

#8 — re-entrancy loop guard:
  After checkout but before applying, check if HEAD itself is a
  github-actions[bot] `chore(autofix)` commit. If so, refuse to
  re-apply (`result=loop-prevented`) with a 🔁 reply telling the user
  to push a human-authored commit or revert before retrying. Prevents
  formatter-config-drift loops where an automated agent watching the
  sticky could pump arbitrary apply commits.

Net effect: every code path in apply.yml now sets a meaningful `result=`
that maps to a specific user-facing reaction + reply. The `*` "unexpected
state (unknown)" arm becomes truly unreachable in normal operation.

Validations: yaml.safe_load OK, check-workflow-concurrency.py OK.

* fix(autofix): refresh stale reviewdog comments + reject patches touching .github/

Two follow-up findings on PR #1458:

#1 — Stale reviewdog references in workflow header comments:
  pr-autofix-publish.yml's header still described the removed inline-
  suggestion path ("posts inline review-comment suggestions to the PR
  using `reviewdog`", "Reviewdog reporter: github-pr-review reads
  $REVIEWDOG_GITHUB_API_TOKEN…"). The Check Run permissions comment
  enumerated the old outcomes (clean / suggestions-posted /
  skipped-too-large) instead of the current set (clean / fixes-
  available). pr-autofix.yml's header described the trusted job as
  posting "inline review-comment suggestions" and the changed_lines
  comment referenced the dead 3000-line cap. Refreshed all three to
  describe the actual sticky + Check Run + /autofix flow.

#2 — Reject patches touching .github/ (sensitive-paths guard):
  Theoretical supply-chain vector: a malicious PR could ship a custom
  prettier/ESLint config that reformats workflow YAML, dependabot.yml,
  or CODEOWNERS. The producer would capture those edits in
  autofix.patch; a maintainer running `/autofix` would push them under
  `contents: write` without human review. The default GITHUB_TOKEN
  lacks the `workflows` scope so workflow-file pushes would fail at
  the platform layer anyway, but as a generic `push-failed` (which
  misleads users into enabling maintainer-edit). Reject early with
  a specific reason.

  Match runs against the patch with grep on `^(diff --git|---|+++)
  [ab]?/?\.github/`. New `result=sensitive-paths` case + 🛑 reply
  telling the user to apply .github/ formatter changes manually.

  Documented the constraint in CONTRIBUTING.md under the /autofix
  section so contributors aren't surprised when the workflow refuses
  a patch that includes formatter changes to workflow files.

Validations: yaml.safe_load OK, check-workflow-concurrency.py OK.
2026-05-09 16:32:38 +01:00
Gergő Magyar
b5627f27d8
ci: add fork-safe PR autofix pipeline (#1446)
* ci: add fork-safe PR autofix pipeline

Two-workflow split posts prettier + eslint --fix output as inline
review-comment suggestions on PRs (including fork PRs) without running
fork-controlled ESLint plugins under a privileged token.

- pr-autofix.yml: untrusted, runs lint:fix/format with permissions: {},
  uploads diff artifact. paths-ignore on lockfiles/snapshots/dist to
  avoid reviewdog 406 on >3k-line diffs.
- pr-autofix-publish.yml: trusted workflow_run consumer. Validates every
  metadata.json field with regex allowlists before exporting to
  GITHUB_OUTPUT (closes head_ref newline-injection vector). Concurrency
  keyed on PR number with fork fallback to head-repo+branch. Reviewdog
  pinned to v0.21.0. Sticky comment posts only when patch is non-empty
  (no noise on clean PRs); body carries a fenced gitnexus-autofix JSON
  block under a stable HTML marker for agent parsing. gh API calls go
  through a small retry helper for transient 5xx.

Branch protection should enable merge queue + 'require branches up to
date' to handle PR freshness; chinthakagodawita/autoupdate is dropped
(unmaintained since 2023).

* ci(autofix): close zizmor template-injection findings

Move fork-controlled values (head.ref, head.repo.full_name, head.sha,
pr.number, github.repository) into the step's env: block instead of
interpolating them with `${{ }}` directly into the bash run body. The
job has permissions:{} today so this is defence-in-depth, but a future
scope grant on the untrusted half would otherwise turn a malicious
branch name into shell injection.

Add pr-autofix-publish.yml to the documented dangerous-triggers ignore
list — workflow_run is required to post sticky comments on fork PRs
and the file's structural defences (no fork checkout, allowlist on
metadata.json, base_repo equality check) match the existing
ci-report.yml exemption.

* ci(autofix): close remaining review findings

- Add an actionlint job to workflow-lint.yml. Catches YAML syntax,
  expression typing, shellcheck-inside-run, and deprecated runner
  labels on every .github/** PR — closes the gap that let pr-autofix's
  YAML literal-block bug reach review on this branch.
- pr-autofix-publish.yml emits a `gitnexus/autofix` Check Run on the
  PR head SHA: conclusion `success` for clean, `neutral` (with
  distinct output titles) for suggestions-posted vs.
  skipped-too-large. Stable name lets agents read the outcome via
  `gh pr checks` without parsing the sticky comment.
- Document the autofix signal contract in CONTRIBUTING.md — sticky
  marker, fenced gitnexus-autofix JSON schema, Check Run name. One
  source of truth so the marker / schema fields don't drift across
  the workflow files and consumers.

* ci: fix actionlint/shellcheck findings on PR #1446

Closes the actionlint warnings the new lint job (workflow-lint.yml's
actionlint runner) surfaced once it was wired into CI. Mostly
shellcheck-style cleanups across three workflows.

pr-autofix-publish.yml
  - SC2170: `[ "${{ steps.meta.outputs.changed_lines }}" -gt 3000 ]`
    interpolates a literal string into bash, breaking shellcheck's
    arithmetic-comparison parse. Move `changed_lines` through env: as
    `CHANGED_LINES` and reference as `$CHANGED_LINES` inside bash.

ci-report.yml (Read PR metadata step)
  - SC2002 ×2: `cat file | tr` -> `tr < file`.
  - SC2129: three consecutive `>> "$GITHUB_OUTPUT"` redirects collapsed
    into one `{ ...; } >> "$GITHUB_OUTPUT"` group.

ci-report.yml (Build report step)
  - SC2162 ×2: `read VAR1 VAR2` -> `read -r VAR1 VAR2` so backslashes
    in test-results.json output aren't mangled.
  - SC2034: drop unused `SUITES` aggregate. The per-framework suite
    counts (CLI_SU, WEB_SU) are now read into `_` placeholders since
    the report doesn't surface them anywhere.

release-candidate.yml
  - SC2129 ×2: collapse consecutive `>> "$GITHUB_OUTPUT"` redirects in
    the rc-version computation step and the tag-push step into one
    grouped block each.
2026-05-09 13:35:04 +01:00
Copilot
4cd3ee3832
Fix ci-report step when base coverage artifact is unavailable (#1412)
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cdf42ff2-4b89-4c5e-a5ab-f68ff62995b0

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-07 16:45:47 +01:00
Copilot
f40973a1ca
fix(ci): handle expired artifacts in base coverage fetch (#1410)
The "Fetch base branch coverage" step in ci-report.yml now:
- Checks up to 5 recent successful main-branch CI runs
- Catches HTTP 410 (artifact expired) and tries the next run
- Gracefully sets found=false if all artifacts are expired/missing

This prevents the PR Report job from failing when the most recent
main-branch test-reports artifact has expired.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a2a6b392-de09-4c76-b7ea-5de2c738cb9d

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-05-07 16:00:05 +01:00
Gergő Magyar
48f15a3bca
ci(release): use fine-grained PAT for rc tag push (#1407)
The default GITHUB_TOKEN cannot be granted `workflows: write`, so
`git push --atomic` of the rc v-tag fails when its commit chain reaches
any commit that modified `.github/workflows/**`. Symptom on the most
recent run:

    ! [remote rejected] v1.6.4-rc.82 -> v1.6.4-rc.82
      (refusing to allow a GitHub App to create or update workflow
      `.github/workflows/trivy.yml` without `workflows` permission)

GitHub's rule: any ref-update that makes a workflow-modifying commit
reachable through the new ref requires `workflows: write` on the
identity performing the push, regardless of whether that commit is
already on another remote ref. The default GITHUB_TOKEN cannot hold
that permission.

Pass a fine-grained PAT (RELEASE_PUSH_TOKEN, scoped to this repo with
Contents: write + Workflows: write) into actions/checkout's `token`
input so origin is preauthed for the subsequent `git push`. The
job-level GITHUB_TOKEN keeps its scoped permissions for npm provenance
and other steps.

Required one-time setup:
  1. Generate a fine-grained PAT
     - Resource owner: account that owns this repo
     - Repository access: Only select repositories → GitNexus
     - Permissions: Contents: write, Workflows: write, Metadata: read
  2. Add as repo secret named RELEASE_PUSH_TOKEN
  3. Re-run the failed Release Candidate workflow with force=true

Considered and skipped: GitHub App approach (org-owned, bot identity,
short-lived tokens). Better long-term, but a fine-grained PAT is
acceptable at one-maintainer scale. Migration is mechanical if the
project later wants to switch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:17:12 +01:00
dependabot[bot]
fd4d4a3fee
chore(deps): bump docker/build-push-action from 6.19.2 to 7.1.0 (#1391)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](10e90e3645...bcafcacb16)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 09:54:50 +01:00
dependabot[bot]
c8683d58fc
chore(deps): bump github/codeql-action from 3.35.3 to 4.35.3 (#1390)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.35.3 to 4.35.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](0daab03d71...e46ed2cbd0)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 09:54:17 +01:00
Gergő Magyar
6ec1f04604
chore(quality): exclude test/fixtures from CodeQL, ESLint, and Prettier (#1313)
Test fixtures are intentionally synthetic inputs (broken/unused code,
malformed samples) used to exercise the analyzer. Quality-tool findings
on them are noise, not real bugs — they were drowning out actionable
signal in the GitHub Security tab.

- CodeQL: add `**/test/fixtures/**` to paths-ignore in codeql.yml
- ESLint: add `gitnexus-web/test/fixtures/**` to global ignores
  (the gitnexus/ counterpart was already ignored)
- Prettier: add `gitnexus-web/test/fixtures/` to .prettierignore
  (same gap as ESLint)

Real test files (*.test.ts) remain in scope so genuine issues like
js/file-system-race and js/insecure-temporary-file in test code still
surface.
2026-05-04 09:35:34 +01:00
Gergő Magyar
342721f06d
ci(security): add automated security and vulnerability scans (#1297)
* ci(security): add CodeQL SAST workflow for JS/TS and Python

CodeQL analyzes both languages on PR, main push, and weekly schedule.
Findings upload to the Security tab as SARIF. Advisory only on
introduction; promote to required check after baseline triage.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U1)

* ci(security): add Dependency Review PR gate

Blocks PRs introducing high+ severity dependency vulnerabilities.
Posts inline summary comment on failure. Required-check candidate
after one week of clean runs.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U2)

* ci(security): add Gitleaks secret scanning

PR runs scan the diff; main pushes scan full history.
Defense-in-depth on top of GitHub native push protection
(documented as a recommended Settings toggle in SECURITY.md).

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U3)

* ci(security): add OpenSSF Scorecard workflow

Weekly + on main push. SARIF uploads to Security tab; public
badge URL resolves after first scheduled run lands.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U4)

* ci(security): add zizmor workflow lint

Lints .github/workflows/** for known Actions security misconfigurations
(unpinned actions, dangerous interpolation, missing permissions).
Triggered only on PRs touching .github/**.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U5)

* ci(security): add Trivy container image scanning

Builds Dockerfile.cli and Dockerfile.web, then scans images for
HIGH/CRITICAL CVEs. Findings record-only on Security tab; not
PR-blocking. Weekly schedule + main push for freshness.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U6)

* docs(security): add SECURITY.md policy and Scorecard badge

Vulnerability disclosure policy points to GitHub Private Vulnerability
Reporting. Documents in-CI scans landed in this branch and recommended
admin actions for forks.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U7)

* fix(review): apply autofix feedback

- CodeQL paths-ignore: replace brace expansion (parser.{c,js}) with two
  explicit entries — CodeQL uses .gitignore-style globs that do NOT support
  brace expansion, so the original pattern matched no files.
- Trivy: pin aquasecurity/trivy-action from @master to @0.28.0 — mutable
  refs are a supply-chain risk and are exactly what zizmor (added in this
  same plan) is meant to flag.

ce-code-review run: /tmp/compound-engineering/ce-code-review/20260503-104259-279c3bc4/

* docs(review): record residual review findings

ce-code-review autofix run flagged three downstream-resolver items
that are not blockers but should land before promoting any of the new
security workflows to required PR checks.

Source: /tmp/compound-engineering/ce-code-review/20260503-104259-279c3bc4/

* fix(ci-security): address all zizmor + dependency-review violations

Resolves all GitHub Advanced Security findings on PR #1297:

- Add 'persist-credentials: false' to actions/checkout in 5 workflows
  (codeql, dependency-review, gitleaks, trivy, workflow-lint). Prevents
  the GITHUB_TOKEN from persisting in .git/config for downstream steps
  to read. Scorecard already had it.
- Pin every net-new third-party Action to a commit SHA (was: major-tag
  refs flagged by zizmor as 'unpinned action reference'):
    github/codeql-action -> v3.35.3 (0daab03)
    actions/dependency-review-action -> v4.9.0 (2031cfc)
    gitleaks/gitleaks-action -> v2.3.9 (ff98106)
    ossf/scorecard-action -> v2.4.3 (4eaacf0)
    docker/build-push-action -> v6.19.2 (10e90e3)
- Bump aquasecurity/trivy-action 0.28.0 -> 0.36.0 (ed142fd). Versions
  < 0.35.0 are flagged by GHSA-69fq-xp46-6x23 (briefly compromised
  supply chain). Caught by Dependency Review on the introducing PR.
- Pin pipx-installed zizmor to 1.24.1 (was unpinned 'pipx install
  zizmor' resolving to latest at run time).

Removes the now-stale residual-findings doc since every item it
recorded is resolved on this branch.

* fix(ci-security): clear remaining zizmor findings

After landing the new security workflows, zizmor reported 5 high+
findings against pre-existing workflows (none introduced by this PR's
new files, all introduced by zizmor's wider scope). Resolved per
research at docs.zizmor.sh and PyO3/maturin issue #2425:

Real fixes (cache-poisoning):
- publish.yml + release-candidate.yml: add 'package-manager-cache:
  false' to actions/setup-node. setup-node v5+ enables caching by
  default when a packageManager field is present in package.json;
  explicit opt-out keeps release installs hermetic and clears the
  audit. Cost: ~30s slower per release run.

Documented exemptions (dangerous-triggers, .github/zizmor.yml):
- ci-report.yml: workflow_run is REQUIRED to post sticky comments
  on fork PRs (forks have read-only GITHUB_TOKEN on pull_request).
- claude.yml: pull_request_target is required by claude-code-action
  to access secrets and post fork-PR review comments. PR checkouts
  pin fork HEAD SHA to mitigate TOCTOU.
- pr-labeler.yml: pull_request_target on the autolabel job needs
  pull-requests:write. release-drafter runs with dry-run:true and
  reads config from the BASE ref only.

Each exemption carries the documented mitigation in zizmor.yml.
workflow-lint.yml now passes --config to both the SARIF and the
gate invocations.

Local 'zizmor --config .github/zizmor.yml --min-severity high .'
reports: No findings to report. Good job!
2026-05-04 08:21:53 +01:00
Gergő Magyar
55d504284f
fix(ci): consolidate Claude review workflow (#1258) 2026-05-02 08:15:46 +01:00
Gergo Magyar
26b39560ce fix(ci): configure e2e GitNexus home at runtime
Avoid workflow planning failures by deriving the e2e GitNexus home from RUNNER_TEMP inside a shell step instead of using runner context in job-level env.

Made-with: Cursor
2026-05-01 12:39:02 +01:00
Gergő Magyar
ec07467601
fix(ci): seed e2e with a small fixture repo (#1249)
Avoid crashing the e2e job during setup by indexing a tiny temp fixture instead of the full monorepo before Playwright starts.

Made-with: Cursor
2026-05-01 12:29:53 +01:00
Gergo Magyar
78e965bf62 ci: avoid duplicate main push checks
Let release-candidate.yml be the single main-push entry point that reuses CI before publishing, while keeping CI as the direct pull-request gate.

Made-with: Cursor
2026-04-30 19:06:21 +01:00
dependabot[bot]
02ed335ff6
chore(deps): bump release-drafter/release-drafter from 7.2.0 to 7.2.1 (#1208) 2026-04-30 05:02:15 +01:00
Gergő Magyar
9e62f7c121
fix(ci): allow expected legacy parity failures (#1099)
Made-with: Cursor
2026-04-27 06:59:18 +01:00
Gergő Magyar
247b1bd556
fix(ci): skip docker.yml tag-input validation on direct tag pushes (#1065)
Some checks failed
CI / quality (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / scope-parity (push) Has been cancelled
Release Candidate / Check if release candidate should run (push) Has been cancelled
CI / Save PR Metadata (push) Has been cancelled
CI / CI Gate (push) Has been cancelled
Release Candidate / ci (push) Has been cancelled
Release Candidate / Publish release candidate to npm (push) Has been cancelled
Release Candidate / Build & Push RC Docker images (push) Has been cancelled
The early Validate step ran on both workflow_call and push events, but
push events never populate inputs.tag (the tag comes from github.ref).
This regressed every real tag-push release — v1.6.3's Docker Build &
Push failed at that gate. The downstream Verify step already falls back
to GITHUB_REF, so the upfront guard only needs to cover workflow_call.
2026-04-24 16:44:02 +01:00
Gergo Magyar
5b1966c2ca ci(docker): add retry wrapper for build-push with visibility and hardened shell
Wraps docker/build-push-action with a local composite action that retries
once on failure (upstream keeps retry out of the action per
docker/build-push-action#1422). Adds ignore-error=true on cache-to so GHA
cache export flakes don't fail an otherwise successful push.

- Emit `::notice::` in the resolve step when attempt 2 recovers from a
  first-attempt failure, so silent retries are grep-able in run logs and
  trending registry/cache flakes stay visible.
- Bind `retry-wait-seconds` via `env:` in the backoff step to match the
  env-binding convention used elsewhere in docker.yml (TAG_INPUT, DIGEST,
  TAGS) — no direct expression interpolation inside shell bodies.

Preserves existing contract end-to-end: SHA pin, provenance=max, sbom=true,
dual-registry push, `steps.build.outputs.digest` wiring to Cosign and the
build-provenance attestations.
2026-04-24 09:22:23 +01:00
Gergő Magyar
2d7b15f18c
fix(ci): inherit secrets into reusable docker.yml from release-candidate (#1054) 2026-04-23 21:56:05 +01:00
Gergő Magyar
ac9246cfd3
ci(docker): mirror signed images to Docker Hub alongside GHCR (#1029)
* ci(docker): mirror signed images to Docker Hub alongside GHCR

docker.yml now publishes to docker.io/abhigyanpatwari/gitnexus{,-web} in
the same build step as the existing GHCR push, so both registries receive
the same digest, the same Cosign keyless signature, and the same SBOM /
build-provenance attestations. The Docker Hub login uses new repo secrets
DOCKERHUB_USERNAME / DOCKERHUB_TOKEN (scoped PAT, not account password).

Supply-chain guarantees carry over unchanged: the signing loop iterates
metadata-action's full tag set, so Docker Hub tags get signed at the
identical digest under the same docker.yml@refs/tags/v* identity. The
ClusterImagePolicy is extended with docker.io / index.docker.io / bare-
namespace globs so admission cannot be sidestepped by registry-prefix
choice. README and .env.example document both registries; RC section in
CONTRIBUTING.md notes the Docker Hub mirror tag.

Closes #1027

* ci(docker): publish to akonlabs Docker Hub namespace; add PR dry-run CI

- Hardcode `akonlabs` as the Docker Hub namespace in metadata-action and
  both attestation subject-names (Docker Hub org differs from GitHub org
  `abhigyanpatwari`, so `github.repository_owner` would produce the wrong ref)
- Update docs (.env.example, README, CONTRIBUTING) and the Kubernetes
  ClusterImagePolicy globs to reference `akonlabs/gitnexus{,-web}`
- Add `pull_request` trigger so the image build runs as CI on every PR
  (build only — no push, sign, or attestation)
- Add `workflow_dispatch` with `dry_run: boolean` (default true) for
  manual build-only runs; all publish steps gated on
  `github.event_name != 'pull_request' && !inputs.dry_run`
2026-04-23 18:59:26 +01:00
dependabot[bot]
36104cdbd2
chore(deps): bump actions/setup-node from 6.3.0 to 6.4.0 (#1033) 2026-04-23 05:06:22 +01:00
Copilot
ff4ae89aaa
feat(python): scope-based call resolution + registry-primary flip + perf + generalization (RFC #909 Ring 3) (#980)
* Initial plan

* plan: Python scope-based resolution migration

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0eee6c69-fc17-4df5-9ac6-358ab41f5740

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(python): scope-based resolution provider hooks + 62 tests

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0eee6c69-fc17-4df5-9ac6-358ab41f5740

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor(python): split scope-hooks monolith into focused modules

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/db76e937-4b0e-4c4d-82b1-265a1fb3673d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test(python): integration-style scope-resolution tests + suffixResolve fallback

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/db76e937-4b0e-4c4d-82b1-265a1fb3673d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* wire python scope-based resolution end-to-end (initial pass)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c474dc66-5cf7-445d-8eb4-76501c5e6d67

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* keep legacy IMPORTS for python (heritage needs importMap), scope phase owns CALLS only

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c474dc66-5cf7-445d-8eb4-76501c5e6d67

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test(python): remove parallel scope-resolution integration test

The new test/integration/python-scope-resolution.test.ts duplicated coverage
the reviewer explicitly rejected. The existing
test/integration/resolvers/python.test.ts (191 tests, driven by
runPipelineFromRepo) is the source of truth for Ring 3 parity.

Also document the IMPORTS-emission follow-up gap: wiring emitImportEdges
in python-scope-emit.ts today regresses 10 IMPORTS-edge fixtures because
the scope-extractor's ImportEdge coverage is narrower than legacy
pythonImportConfig.importResolver. Tracked as a follow-up.

Baseline with REGISTRY_PRIMARY_PYTHON=1 is unchanged: 109/191 pass.

* feat(ingestion): scope-resolution phase owns Python IMPORTS edges (RFC #909 Ring 3)

When `REGISTRY_PRIMARY_PYTHON=1`, IMPORTS graph edges for Python files are now
emitted exclusively by the new scope-resolution path. The legacy
`import-processor` still runs — heritage resolution needs its importMap /
namedImportMap / moduleAliasMap population — but its graph edge emission is
gated per-language so Python no longer double-emits.

This closes the reviewer's second change request on PR #980: "the legacy path
must be turned off". Legacy IMPORTS edges for Python are now off by default
when the flag is enabled.

Three bugs were fixed to make the new path's coverage match legacy:

1. **Root-file bailout** (import-resolvers/python.ts): `resolvePythonImportInternal`
   returned null immediately when the importer file lived at the repo root
   (importerDir === ''). The ancestor directory walk further down already
   handles this case correctly; the early return was the bug. Proximity check
   now only runs when importerDir is non-empty, and the ancestor walk sees
   root-level files for the first time.

2. **External dotted imports** (languages/python/import-target.ts): the new
   path fell straight through to `suffixResolve` for multi-segment imports,
   which happily matched `django.apps` to a local `accounts/apps.py`. Mirror
   `pythonImportStrategy`'s `hasRepoCandidate` guard — suffix-match only when
   the leading segment exists somewhere in-repo as a package, __init__.py,
   or namespace directory.

3. **suffixResolve ambiguity** (languages/python/import-target.ts): the
   shared `suffixResolve` helper requires a pre-built `SuffixIndex` to
   disambiguate ties. Without one it falls back to an O(files) scan that
   silently picks the first match when the last segment collides across
   directories (e.g. `accounts.models` matching `billing/models.py`).
   Replaced with `resolveAbsoluteFromFiles` — exact lookup first, then a
   deterministic suffix match.

Validation:
- Flag OFF: 191/191 pass (no regression).
- Flag ON: 109/191 pass (82 fail — exact baseline match; remaining 82 are
  unchanged CALLS-edge provider-feature gaps tracked as Phase B follow-ups).
- `tsc --noEmit`: clean.

The 82 CALLS failures cluster into 44 describe blocks covering type-inference
features (assignment chains, walrus, class-level annotations, constructor
inference, C3 MRO, overload dispatch, return-type inference) that need
dedicated Ring 3 follow-up work. Each cluster is tracked against the RFC #909
shadow-parity gate (>=99% fixtures / >=98% corpus) in the per-language ticket.

* ci(scope-resolution): automatic parity gate driven by MIGRATED_LANGUAGES

Adds the Ring 3 parity gate the RFC §6.4 requires: when a language's
scope-resolution migration is marked complete, CI runs its resolver
integration test twice on every PR (once with the legacy DAG, once with
the registry-primary path) and both must pass.

The "is this language migrated" signal is a single TypeScript constant:

  // gitnexus/src/core/ingestion/registry-primary-flag.ts
  export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> =
    new Set([ /* SupportedLanguages.Python when ready */ ]);

Adding a language here has three simultaneous effects:

  1. `isRegistryPrimary(lang)` defaults to true for that language in
     production (env-var override still wins if set explicitly).
  2. `.github/workflows/ci-scope-parity.yml` auto-discovers the set via
     `npx tsx scripts/ci-list-migrated-languages.ts`, builds a parity
     matrix, and runs:
       - `REGISTRY_PRIMARY_<LANG>=0 npx vitest run resolvers/<slug>.test.ts`
       - `REGISTRY_PRIMARY_<LANG>=1 npx vitest run resolvers/<slug>.test.ts`
     Both legs must pass for the job to succeed.
  3. Legacy-path gating in call-processor.ts / import-processor.ts kicks
     in automatically through the same `isRegistryPrimary` lookup.

No JSON registry, no manual workflow edit, no second source of truth —
contributors update the Set and CI picks it up. Empty Set = parity job
is a skipped matrix (workflow still reports success).

The new `scope-parity` reusable workflow is added to ci.yml's `needs`
graph and ci-status gate. Its result must be `success` (skipped would
mean upstream discover job failed and should block).

Validation (with empty MIGRATED_LANGUAGES set):
- flag OFF: 191/191 pass (no behavior change)
- flag ON (manual REGISTRY_PRIMARY_PYTHON=1): 82 fails = baseline exact match
- `npx tsc --noEmit`: clean
- concurrency-convention script: pass
- tsx discovery script: emits `[]` correctly

* ci(scope-resolution): keep MIGRATED_LANGUAGES empty; fix linter auto-uncomment

Previous commit's example entry got auto-uncommented (linter preferred a
type-checkable `SupportedLanguages.Python` over a commented-out reference).
That would have triggered the parity CI gate against Python, which today
has 82 known flag-on failures — unintended and would block the PR.

Use the explicit generic `new Set<SupportedLanguages>([])` so an empty set
still type-checks without needing an uncommented-out sample member.
Example in the comment now has `//   SupportedLanguages.Python,` so it
remains illustrative without participating in the set.

* feat(python): capture constructor-inferred + annotated type bindings

Extends the Python scope-extractor with two new type-binding capture
patterns so receiver-typed method dispatch has concrete type bindings
to work from:

1. `u: User = ...` / `u: User` — variable annotations. `@type-binding.annotation`
   anchor, `source: 'annotation'`.
2. `u = User("alice")` — assignment RHS is a bare-identifier call (Python
   has no `new` keyword; constructor-shaped calls are syntactically
   identical to function calls). `@type-binding.constructor` anchor,
   `source: 'constructor-inferred'`.

The runtime query lives in `query.ts` (the `.scm` file is documentation
per the comment at its top); both are updated.

Fixes 19 failures across these resolver fixtures (flag-on 82 → 63):
- Python constructor-inferred type resolution (3)
- Python class-level annotation resolution (3)
- Python nullable receiver resolution (3)
- Python member-call / receiver-constrained / constructor-call (3)
- Python assignment chain propagation (2)
- Python walrus / match-case / chained method (3)
- Python member access iterable for-loop (2)

* feat(python): strip nullable unions + prefer annotations over inference

Two linked changes that together fix the 4 nullable-receiver tests:

1. `stripNullable` in Python's `interpretTypeBinding` unwraps `User | None`,
   `None | User`, and `Optional[User]` to `User`, so receiver-typed
   resolution treats nullable receivers identically to non-nullable ones.
   Three-arm unions (`User | Error | None`) are left unchanged — truly
   ambiguous for single-receiver inference.

2. Source-strength ordering in `pass4CollectTypeBindings`. When multiple
   matches fire for the same bound name in the same scope — e.g. the
   `u: User = find()` idiom where both the annotation and
   constructor-inferred patterns match — the explicit annotation now
   wins regardless of query-match arrival order. Rank:
     explicit (annotation / parameter-annotation / return-annotation / self) > inferred

Also reorders the two Python patterns in query.ts / scopes.scm so the
constructor-inferred pattern appears first — a belt-and-braces fallback
that keeps behavior deterministic if the shared priority ranking is ever
revisited.

Fixes 4 failures (flag-on 63 → 59):
- Python nullable receiver resolution (4 tests)

Flag-off regression check: 191/191 still pass.

* feat(python): walrus, qualified-call, match-case type bindings

Extends the constructor-inferred family of captures with three more
assignment-shaped patterns that all bind a variable to a class-like type:

- Walrus: `(u := User(...))` → `u: User` via `(named_expression)`.
- Qualified call RHS: `u = models.User(...)` → `u: models.User` via
  `(attribute)` node .text. Falls through resolveTypeRef Phase 2
  (QualifiedNameIndex dotted fallback).
- Match as-pattern: `case User() as u:` → `u: User` via `(as_pattern)`
  + `(class_pattern (dotted_name))`.

Fixes 2 failures (flag-on 59 → 57):
- Python walrus operator type inference
- Python match/case as-pattern type binding

Qualified-call constructor tests still fail because they require
cross-module qualifiedName registration (models.User → models.py's User
class) which isn't yet wired in the Python extractor. Tracked as
follow-up alongside module-import CALLS (#337) resolution.

* feat(python): chain type bindings + strip list[T] generic for for-loop

Adds two capture patterns and a shared transitive-closure pass that
together handle Python's variable-aliasing and for-loop-over-typed-
iterable patterns:

1. `(assignment left: (identifier) right: (identifier))` — `alias = u`.
2. `(for_statement left: (identifier) right: (identifier))` — `for u in users`.

Both emit `@type-binding.alias` with the RHS identifier as rawName. The
shared `pass4CollectTypeBindings` now runs a final transitive-closure
walk that follows identifier-chain TypeRefs through the declaring scope
and its ancestors (depth-capped, cycle-guarded) so `alias` ultimately
points at the class type instead of another local variable name.

Generic stripping in `interpret.ts` unwraps single-arg collection
wrappers — `list[User]`, `set[User]`, `Iterable[User]`, etc. — to the
element type. Multi-arg generics (`dict[str, User]`, `Callable[...]`)
are left alone; their semantics aren't unambiguous.

Fixes 8 failures (flag-on 57 → 49):
- Python assignment chain propagation (4)
- Python nullable + assignment chain (2)
- Python walrus operator (:=) assignment chain (2)

Flag-off still 191/191.

* feat(python): namespace & class receiver resolution + file-level caller fallback

Adds a Python-specific post-resolution pass `emitReceiverBoundCalls`
that closes two receiver gaps the shared `MethodRegistry.lookup` doesn't
cover:

1. **Namespace receivers** — `import models; models.User()` /
   `import models as m; m.User()`. The shared `lookupReceiverType` only
   walks `scope.typeBindings`; namespace imports never land there
   (they're filtered out of `scope.bindings` when the target module
   has no self-named def, per `finalize-algorithm.ts:540`). The new
   pass walks `indexes.imports` directly, builds a per-file
   `localName → targetFilePath` map, and emits CALLS/ACCESSES edges
   against the target file's `localDefs`.

2. **Class-name receivers** — `Dog.classify("dog")`. The shared resolver
   requires typeBindings; class bindings in `scope.bindings` are never
   consulted as receivers. The new pass checks class-kind bindings in
   the call scope's chain and resolves members via `ownerId`.

Also fixes module-level call attribution: `resolveCallerGraphId` now
falls back to the File node id (`generateId('File', filePath)`) when no
enclosing function/method/class is found. Matches legacy DAG behavior
for module-scope calls like `u = models.User()` at the top of app.py.

Fixes 4 failures (flag-on 49 → 45):
- Python module import CALLS resolution (Issue #337) (4 of 7)

Flag-off still 191/191.

* feat(python): dotted-typebinding receiver resolution

Adds case 3 to `emitReceiverBoundCalls`: when a receiver's typeBinding
has a dotted rawName like `u: models.User` (the constructor-inferred
form fired by `u = models.User(...)`), walk the namespace map + target
file's defs to find the class, then look up the member via ownerId.

`resolveTypeRef`'s QualifiedNameIndex fallback can't cover this because
the target class's qualifiedName in models.py is just `"User"`, not
`"models.User"` — the dotted form only exists in the call-site file's
receiver expression. This pass bridges that gap without modifying the
shared registry.

Fixes 9 more failures (flag-on 45 → 36):
- Python qualified constructor inference (2)
- Python module import CALLS resolution (Issue #337) (3)
- (cluster overlap — several downstream tests in assignment/nullable/
  walrus that propagate through qualified-ctor bindings also benefit)

Flag-off still 191/191.

* feat(python): consult finalized bindings for receiver resolution

`findClassBindingInScope` now walks BOTH:
  1. `scope.bindings` — pre-finalize local declarations (origin: 'local')
  2. `indexes.bindings` — post-finalize cross-file imports/namespaces

Without (2) we were blind to any class brought in via
`from models import Dog` at the call site's file, because the
scope-extractor's Pass 2 only populates local bindings and the
cross-file finalize produces a separate bindings map that never lands
on `scope.bindings`.

Case 2 (`Dog.classify()`) now walks MRO so inherited static/class
methods resolve — `Dog.classify()` where `classify` lives on `Animal`.

Case 4 (simple typeBinding like `u: U` from aliased import) now uses
`findClassBindingInScope` instead of the shared `resolveTypeRef`,
because `resolveTypeRef`'s `ctx.scopes` only sees pre-finalize local
bindings too.

Fixes 4 more failures (flag-on 36 → 32):
- Python method enrichment > Dog.classify static (1)
- Python static/classmethod class-as-receiver (2)
- Python alias import resolution (1)

Flag-off still 191/191.

* refactor(python-scope): extract language-agnostic emit-core/

Unit 1 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).

Splits python-scope-emit.ts (~945 → 481 lines) by lifting 14 generic
graph-feeding primitives into emit-core/:
  - graph-node-lookup, graph-id, emit-edge
  - emit-references, emit-imports
  - scope-walkers (findReceiverTypeBinding, findClassBindingInScope,
    findOwnedMember, findExportedDef)
  - namespace-targets, method-dispatch-bridge

Each file carries a "Next-consumer contract" JSDoc so future language
migrations (TS #927, JS #928, Java, Kotlin, Ruby) import from emit-core
rather than re-implementing. python-scope-emit.ts keeps only the four
Python-specific pieces: runPythonScopeResolution (orchestrator),
buildPythonMro, emitReceiverBoundCalls (4 cases), populateMethodOwnerIds
— these move to languages/python/emit/ in Unit 11.

Pure refactor, zero behavior change:
  - flag-off: 191/191 python.test.ts pass (identical baseline).
  - flag-on (REGISTRY_PRIMARY_PYTHON=1): 32 fail / 159 pass (identical
    baseline — the refactor neither fixes nor regresses any test).
  - tsc --noEmit clean.

* feat(python-scope): arity metadata + bind function decls in parent scope

Unit 2 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).

Two changes that the registry-primary path needs before any of the
arity-sensitive failures can move:

1. Arity metadata on scope-extracted Function/Method defs.
   - New helper `languages/python/arity-metadata.ts` reuses
     `pythonMethodConfig.extractParameters` so self/cls stripping,
     defaults, and *args/**kwargs detection match legacy semantics.
   - `emit-captures.ts` synthesizes
     `@declaration.parameter-count` /
     `@declaration.required-parameter-count` /
     `@declaration.parameter-types` captures on every
     `@declaration.function` match.
   - Generic `scope-extractor.ts buildDefFromDeclarationMatch` reads
     the three optional captures into `SymbolDefinition`. Absence is
     still the no-op default for non-Python providers.

2. Hoist function/class declaration bindings to the enclosing scope.
   The "innermost scope containing the anchor" default placed
   `def greet(...)` inside greet's OWN body — invisible to other
   module-level callers, so every flag-on free-call resolved to
   `unresolved`. The hoist condition (`anchor range == innermost
   range`) only fires for scope-creating declarations, so variable /
   for-loop captures whose anchor is a child identifier stay put.
   Hooks can still override via `bindingScopeFor`.

Verification:
  - Flag-off: 191/191 (identical baseline).
  - Flag-on (REGISTRY_PRIMARY_PYTHON=1): 31 fail / 160 pass
    (was 32/159; the hoist unblocks free-call resolution end-to-end).
  - tsc --noEmit clean.

Per-(source,target) edge collapse for multi-call-site cases
(default-params, variadic) still pending — landing it without
regressing the static-method find_user fixture (which expects two
distinct edges through different targets) needs the ownership-aware
qualified-id work that lands with Unit 4 / Unit 11.

* feat(python-scope): capture function return-type annotations

Unit 3 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).

Wires the `def get_user() -> User` return-type annotation into the
typeBindings stream so the existing constructor-inferred + transitive
chain machinery can resolve `u = get_user(); u.save()` to `User#save`
without any orchestrator change.

Changes:
- `query.ts` + `scopes.scm`: new `@type-binding.return` pattern keyed by
  the function name (matches RFC §5.1 canonical vocabulary).
- `interpret.ts`: maps `@type-binding.return` to the existing
  `'return-annotation'` source label (no shared change needed).
- `scope-extractor.ts pass4CollectTypeBindings`: extends the Pass 2
  auto-hoist (anchor range == innermost scope range → bind in parent)
  to type bindings as well — return-type bindings whose anchor IS the
  function_definition land in the function's enclosing scope so
  callers see them.

Same-file return-type inference is now end-to-end:
  `def get_user() -> User: ...` + `u = get_user()` produces
  `u: User (return-annotation)` in the caller's scope via
  `followChainedRef`.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 31 fail / 160 pass (no change — every remaining
  return-type test in this fixture set is *cross-file*; carrying
  `get_user → User` across module boundaries lands with the
  cross-file typeBinding propagation work in Unit 5/7).
- tsc --noEmit clean.

* feat(python-scope): resolve dotted receivers via class-scope field types

Unit 4 partial — the dotted-receiver case (`user.address.save()`).

Class-body annotations like `class User: address: Address` already
land in the class scope's typeBindings via the existing
`@type-binding.annotation` capture. This commit consumes that signal:

- Build a `Map<classDefId, Scope>` from every parsed file's class
  scopes once per resolution pass.
- New Case 0 in `emitReceiverBoundCalls`: when the receiver's name
  contains a dot, walk the chain — resolve the head's type, then for
  each remaining segment look up that field's type in the owner
  class's scope.typeBindings, then emit the call against the final
  class with MRO walk.
- Cross-scope lookups use each TypeRef's `declaredAtScope` so an
  imported `Address` resolves in the file that owns the field
  declaration, not the file holding the call site.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 29 fail / 162 pass (was 31/160; both `Field type
  resolution` fixtures now pass — same-file and cross-file disambig).
- tsc --noEmit clean.

Remaining Unit 4 work (write ACCESSES, `self.X` for-loop iteration)
needs Unit 6's tuple/iterable destructuring before it can land —
`for u in self.users` requires the iterable typing path.

* feat(python-scope): chain receiver via call-expression return types

Unit 5 — extends the compound-receiver case to handle call-expression
receivers (`svc.get_user().save()`).

`resolveCompoundReceiverClass` is the single recursive entry point for
all compound receivers. Three shapes:
  - bare identifier — typeBinding chain
  - dotted `obj.field[.field]…` — class-scope field types
  - call `expr.method()` — recurse into expr, look up method's
    return-type typeBinding on its class scope

Method return-type bindings auto-hoist to the parent (class) scope per
Unit 3, so `methodClassScope.typeBindings.get(methodName)` is the
canonical lookup. Free-call return types (`get_user()`) walk the
caller's scope chain.

Depth-capped at 4 hops to bound recursion.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 28 fail / 163 pass (was 29/162; `Python chained method
  call resolution` now passes).
- tsc --noEmit clean.

Two related tests (`city.save() via method chain`, `c.greet().save()
depth-2 MRO`) still fail because the captures yield typeBindings
shaped like `city → user.get_city` (no trailing parens — the capture
grabs the attribute text). Resolving those needs a follow step that
detects the call-shape rawName and feeds it through the compound
recurser. Lands with the chain-typeBinding work in a follow-up.

* feat(python-scope): free-call fallback consults finalized bindings

Unit 7 — closes the cross-file free-call gap.

The shared `MethodRegistry.lookup` walks `scope.bindings` (pre-finalize
local-only) for free-call resolution. Cross-file imports land in
`indexes.bindings` (post-finalize). Without the dual-source lookup,
`from x import f; f()` resolves to "unresolved" and no CALLS edge is
emitted.

Two changes:

- `emit-core/scope-walkers.ts`: new `findCallableBindingInScope` —
  same dual-source pattern as `findClassBindingInScope`, but accepts
  Function/Method/Constructor. Promoted to emit-core because every
  language with cross-file imports needs the same lookup.
- `python-scope-emit.ts emitFreeCallFallback`: post-pass that walks
  every free-call reference site, looks up the callee with the new
  helper, and emits via `tryEmitEdge`. Pre-seeds `seen` from the
  shared resolver's emissions so we never double-count.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 22 fail / 169 pass (was 28/163; +6 tests including
  the Python overload dispatch fixtures, ancestor-directory imports,
  and same-name module-alias collision).
- tsc --noEmit clean.

* feat(python-scope): super() receiver dispatches up the MRO

Unit 8 — `super().method()` inside a class method walks the enclosing
class's MRO chain (skipping self) and resolves to the first ancestor
that owns the method.

New receiver branch in `emitReceiverBoundCalls` recognizes
`super(...)` syntactically (regex-cheap), finds the enclosing class
via a new `findEnclosingClassDef` scope-walk helper, then re-uses
`scopes.methodDispatch.mroFor` + `findOwnedMember` from the existing
class-receiver path. Handled before the compound-receiver case so
`super()` doesn't fall into the bare-identifier branch where `super`
isn't a binding.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 21 fail / 170 pass (was 22/169; `super().save() inside
  User to BaseModel.save` now passes).
- tsc --noEmit clean.

* feat(python-scope): suppress shared resolver on member-call sites

Unit 9 — `app_metrics.get_metrics()` (namespace import alias) was
emitting two CALLS edges: a wrong self-call from the shared
resolver's free-call fallback, plus the correct namespace-receiver
edge from the Python post-pass.

Mechanism:

- `emit-core/emit-references.ts`: new optional `skipSites` parameter
  (`Set<string>` of `${filePath}:${line}:${col}` keys). When supplied,
  references at those positions are skipped — the provider has
  already emitted (or chosen not to emit) for that site.
- `python-scope-emit.ts`: reorders Phase 4 — receiver-bound + free-
  call fallback run FIRST, populating `handledSites`. The shared
  `emitReferencesViaLookup` then runs with that set so the resolver's
  fallback can't fight a precise per-receiver emission. Site keys are
  added only on successful tryEmitEdge (not for sites the post-pass
  saw but couldn't resolve — those still get a chance from the shared
  path).

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 20 fail / 171 pass (was 21/170; same-name module-alias
  collision now resolves correctly).
- tsc --noEmit clean.

* feat(python-scope): propagate return-type bindings across imports

Closes the cross-file return-type propagation gap that left tests
like `u = get_user(); u.save()` (where get_user lives in another
file) with `u` typed as the function name instead of its return type.

The shared finalize pass copies callable bindings (`from x import f`
puts `f` in the importer's bindings) but typeBindings stay file-local
because they live on `Scope.typeBindings`, not on the index. Mutate
post-finalize:

- For each module-scope import binding (`origin: 'import'` or
  `'reexport'`), look up the source file's module-scope typeBinding
  for the def's simple name. If present (return-annotation source),
  mirror it under the importer's local alias. Skip when the importer
  already has its own typeBinding for the name (explicit local always
  wins).
- After propagation, re-run a chain-follow on every scope's
  typeBindings — pass-4 ran before propagation and missed any chain
  whose terminal lived in a foreign file. Same algorithm as
  `followChainedRef` in scope-extractor, but operates on the
  finalized scopes so propagated entries are visible.

Mutating `Scope.typeBindings` is safe — `draftToScope` constructs a
plain `new Map(...)`, not a frozen one.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 16 fail / 175 pass (was 20/171; +4 — both cross-file
  return-type tests, plus two related propagation cases).
- tsc --noEmit clean.

* feat(python-scope): for-loop call-iterable typeBinding

Adds `(for_statement left: (identifier) right: (call function:
(identifier)))` to the typeBinding capture set. Combined with Unit 3's
return-type capture and the cross-file return-type propagation pass,
this makes `for u in get_users(): u.save()` resolve to `User.save`
even when `get_users` is imported from another module.

Captured as `@type-binding.alias` (rawName = function identifier,
without parens) so the existing chain-follow walks the alias to the
function's return-type binding without any new code path.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 12 fail / 179 pass (was 16/175; +4 for-loop call-iterable
  tests across get_users / get_repos fixtures).
- tsc --noEmit clean.

* feat(python-scope): collapse free-call edges per (caller, target)

Free calls (no explicit receiver) now emit a single CALLS edge per
(caller, target) pair regardless of how many call sites the caller
contains. Mirrors the legacy DAG's per-pair dedup contract — what
the `default-params`, `variadic`, and `overload` fixtures expect.

Member calls keep position-based dedup so distinct resolved targets
(e.g. UserService.find_user vs AdminService.find_user from the same
caller) still produce distinct edges.

Implementation: bypass `tryEmitEdge` (which dedupes positionally) and
hand-roll the relationship with a position-independent rel.id
(`rel:CALLS:<caller>-><target>`). Site handling is now unconditional —
even when the dedup-collapse skips the actual emit, we mark the site
handled so the shared `emit-references` doesn't fight us with its
fallback.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 10 fail / 181 pass (was 12/179; +2 — both `default
  parameter arity` tests now pass).
- tsc --noEmit clean.

* fix(python-scope): match legacy CALLS reason for import-resolved free calls

The arity-narrowing test asserts \`rel.reason === 'import-resolved'\`
for cross-file free-call edges. Switch the free-call fallback's
reason to mirror legacy DAG semantics:
  - target-file !== source-file → 'import-resolved'
  - same file                   → 'local-call'

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 9 fail / 182 pass (was 10/181; +1 arity-narrowing test).
- tsc --noEmit clean.

* fix(python-scope): drop dead pre-seeding from receiver-bound pass

The pre-seeding loop at the top of \`emitReceiverBoundCalls\` populated
\`seen\` with every reference the shared resolver had already resolved.
That was useful when emit-references ran FIRST. After Unit 9 reversed
the order (emit-references runs after the Python passes and uses
\`handledSites\` to skip what we processed), the pre-seed only causes
harm: when an MRO walk in Case 0 (compound receiver) and Case 4
(simple typeBinding) both touch the same site at the same position
but resolve to different targets, the pre-seed suppresses the second
emission because the shared resolver had already entered the wrong
target into \`seen\`.

Concrete case: \`c.greet().save()\` — Case 0 emits the outer save edge
to Greeting.save; Case 4 then resolves the inner \`c.greet()\` to
A.greet via MRO walk. With pre-seed both edges should emit (different
targets, different rel.ids); without removing the pre-seed the inner
emission was being deduped against an already-seeded entry and the
A.greet edge was lost.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 8 fail / 183 pass (was 9/182; +1 — \`c.greet() to A#greet
  via MRO walk\` now passes).
- tsc --noEmit clean.

* feat(python-scope): enumerate(X) for-loop tuple destructuring

Adds two new typeBinding capture patterns for the canonical enumerate
pattern:

  for (i, u) in enumerate(users): ...   ; tuple_pattern
  for  i, u  in enumerate(users): ...   ; pattern_list

Both bind the second tuple element (u) to the iterable identifier
(users). The chain-follow then unwraps users → its element type via
the existing generic-strip in interpret.ts (List[User] → User).

The #eq? predicate scopes the pattern to enumerate specifically;
generic tuple destructuring of arbitrary callables is left to a
future iteration once we have a richer signal for "what does this
call yield".

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 7 fail / 184 pass (was 8/183; +1 — `parenthesized tuple:
  for (i, u) in enumerate(users)` now passes).
- tsc --noEmit clean.

* feat(python-scope): dict.items() value-type unwrapping

Two changes that together resolve `for k, v in data.items(): v.save()`:

- `interpret.ts stripGeneric`: extends to `dict[K, V]` /
  `Dict[K, V]` / `Mapping[K, V]` etc., stripping to the value type V.
  Previously only single-arg generics (list[User] → User) were
  stripped; multi-arg ones returned the raw text.
- `query.ts` + `scopes.scm`: new typeBinding patterns for
  `for k, v in X.items()` (both pattern_list and tuple_pattern). The
  second tuple element binds to X; the chain-follow then unwraps X's
  dict annotation to V via the new stripGeneric branch.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 6 fail / 185 pass (was 7/184; +1 — `dict.items() loop`
  test now passes).
- tsc --noEmit clean.

* feat(python-scope): nested tuple destructuring for enumerate(d.items())

Two more for-loop typeBinding patterns:

- `for i, (k, v) in enumerate(d.items())` — nested tuple destructuring
  where v is the value of the dict's items() yield.
- `for v in d.values()` — explicit values() form (companion to items).

Both bind the loop var to the dict identifier; the chain-follow
unwraps via the dict-aware stripGeneric to the value type.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 5 fail / 186 pass (was 6/185; +1 nested tuple test).
- tsc --noEmit clean.

* feat(python-scope): 3-var flat destructuring for enumerate(d.items())

Adds the \`for i, k, v in enumerate(d.items())\` shape — flat
3-variable destructuring of the (i, (k, v)) tuple yielded by
\`enumerate\` over \`items()\`. Binds v (the last identifier in the
pattern_list) to the dict identifier; the existing dict-aware
stripGeneric unwraps to the value type.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 4 fail / 187 pass (was 5/186; +1).
- tsc --noEmit clean.

* feat(python-scope): write ACCESSES edges for attribute assignments

Three changes that together produce ACCESSES (write) edges for
\`obj.field = value\` assignments:

- New \`@reference.write.member\` capture in query.ts and scopes.scm
  matching \`(assignment left: (attribute object: ... attribute: ...))\`.
  Reuses the existing receiver/name capture shape so the
  receiver-bound emit pass can resolve obj's class and look up the
  field.
- \`populateMethodOwnerIds\` now sets ownerId on class-body fields too,
  not only on methods. Previously it only walked Function scopes
  whose parent was Class; class-body annotations like \`name: str\`
  live directly in the Class scope's ownedDefs and were missed, so
  \`findOwnedMember(User, "name")\` returned undefined.
- \`emit-core isLinkableLabel\` extends to Variable and Property so
  field nodes appear in the graph-node lookup (the legacy parser
  emits both kinds for class-body annotations).
- Case 4 in receiver-bound pass now uses the kind word as the edge
  reason for read/write sites — matches the legacy DAG convention
  the test asserts on.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 3 fail / 188 pass (was 4/187; +1 — write-ACCESSES test).
- tsc --noEmit clean.

* feat(python-scope): chain-typebinding + field-fallback method lookup

Reaches the architectural-plan target of >= 189/191 flag-on passing.

Two intertwined changes:

- Field-fallback in resolveCompoundReceiverClass: when method lookup
  on the receiver's class (and its MRO) fails, walk the class's
  fields and try the same lookup on each field's type. Matches the
  "unified fixpoint" intent of the method-chain fixture where
  `user.get_city()` reaches `Address.get_city` through User's
  `address: Address` field.
- New Case 3b in receiver-bound emit pass: when the receiver's
  typeBinding rawName has a dot but isn't a namespace prefix
  (e.g. `city -> user.get_city` from the constructor-inferred capture
  for `city = user.get_city()`), treat it as a method-call chain and
  pipe through the compound resolver. The chain unwraps to the
  terminal class (City) and the call resolves normally.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 2 fail / 189 pass (was 3/188; +1 city.save method chain).
- tsc --noEmit clean.

Remaining 2 failures are fixture-driven (self.users / self.repos
fixtures reference fields that aren't declared on the class) and
documented as known-limitation in Unit 10.

* feat(python-scope): flip Python to registry-primary (191/191 parity)

Adds the \`for u in self.X\` heuristic typeBinding capture (binds u to
the attribute name X so the chain-follow can resolve via the enclosing
method's parameter typeBinding) — closes the last two failing
fixtures whose classes reference \`self.X\` for fields that are
actually method parameters.

With 191/191 passing on BOTH legacy and registry-primary paths,
flips \`MIGRATED_LANGUAGES\` to include \`SupportedLanguages.Python\`.

Effects:
- Production default for Python files: registry-primary path.
- CI parity gate auto-discovers Python via the script + workflow
  (\`scripts/ci-list-migrated-languages.ts\` /
  \`.github/workflows/ci-scope-parity.yml\`) and runs the resolver
  integration test BOTH ways on every PR.
- Operators retain the \`REGISTRY_PRIMARY_PYTHON=0\` escape hatch.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (unset, post-flip): 191/191 (uses registry).
- tsc --noEmit clean.

This concludes RFC #909 Ring 3 — Python migration.

* refactor(emit-core): EmitProvider interface + promote 5 generic helpers

G-Units 1-2 of the emit-pipeline generalization plan.

Adds:
- emit-core/emit-provider.ts — typed EmitProvider contract (6 required +
  2 optional fields). Will be consumed by the generic orchestrator in
  G-Unit 6. Documents the LanguageProvider vs EmitProvider boundary.
- emit-core/emit-free-call.ts — emitFreeCallFallback promoted as-is
  (drops the unused referenceIndex pre-seed parameter; underscore-prefixed
  to keep the signature compatible).
- emit-core/propagate-return-types.ts — propagateImportedReturnTypes +
  followChainPostFinalize. Documents the mutation contract (Invariant
  I3 + I6 from the plan): runs after finalize, before resolve, mutates
  the non-frozen Scope.typeBindings map.
- emit-core/scope-walkers.ts: + findEnclosingClassDef +
  findExportedDefByName. Both were already generic in the Python
  source.

python-scope-emit.ts shrinks 1055 → 799 lines (–256). Imports the
promoted helpers from emit-core. No behavior change.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* refactor(emit-core): promote receiver-bound dispatcher + compound resolver

G-Unit 3 of the emit-pipeline generalization plan.

- emit-core/emit-compound-receiver.ts — resolveCompoundReceiverClass
  + matchingOpenParen + COMPOUND_RECEIVER_MAX_DEPTH. Field-fallback
  is now an option (default true) so strictly-typed languages can
  opt out via EmitProvider.fieldFallbackOnMethodLookup.
- emit-core/emit-receiver-bound.ts — the 7-case dispatcher (super,
  Cases 0/1/2/3/3b/4). Accepts a ReceiverBoundProviderSubset
  (isSuperReceiver + fieldFallbackOnMethodLookup) so partial wiring
  works during the rest of the migration. Documents Contract
  Invariants I4 (case order) and I5 (no pre-seeding).

python-scope-emit.ts shrinks 799 → 384 lines. The orchestrator now
calls the generic emitReceiverBoundCalls with an inline minimal
provider (pythonEmitProviderInline) — full provider lands in G-Unit 6
when the orchestrator itself moves to languages/python/emit/.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* refactor(emit-core): promote MRO walk + populateClassOwnedMembers

G-Units 4-5 of the emit-pipeline generalization plan.

- emit-core/build-mro.ts — generic buildMro takes a LinearizeStrategy
  hook receiving (classDefId, directParents, parentsByDefId). Three
  shared steps (collect EXTENDS, build defId-by-graphId, walk per
  class) + parametric linearization. Default strategy is BFS-with-
  visited (Python's depth-first first-seen, also correct for
  single-inheritance languages).
- emit-core/scope-walkers.ts: + populateClassOwnedMembers — generic
  OO ownership rule (methods + class-body fields). Both rules ship
  together because every OO language migrated so far (Python; planned
  TS/JS/Java/Kotlin) wants both. Languages that need different rules
  can compose with this as a base step.

python-scope-emit.ts shrinks 384 → 255 lines.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* refactor(scope-resolution): generic orchestrator + language-agnostic phase

G-Units 6-7 of the emit-pipeline generalization plan, plus the
pipeline-phase generalization (the user's observation that the phase
itself is generic once the orchestrator is).

Changes:

- emit-core/orchestrator.ts — runScopeResolution(input, provider).
  The 180 lines of pipeline glue moved here, parametrized by
  EmitProvider. Provider supplies LanguageProvider, importEdgeReason,
  and the 6 emit-side hooks.
- emit-core/emit-provider.ts — EmitProvider gains languageProvider
  and importEdgeReason fields so the orchestrator needs nothing else.
  resolveImportTarget now takes (targetRaw, fromFile, allFilePaths).
- languages/python/emit/index.ts — pythonEmitProvider + thin
  runPythonScopeResolution wrapper. The first reference impl every
  next-language migration copies.
- emit-providers-registry.ts (NEW) — registry of per-language
  EmitProviders keyed by SupportedLanguages. Adding a language is
  one line here + the provider file.
- pipeline-phases/scope-resolution.ts (NEW) — language-agnostic phase
  iterating EMIT_PROVIDERS ∩ MIGRATED_LANGUAGES. Replaces
  pipeline-phases/python-scope.ts (deleted).
- python-scope-emit.ts deleted.
- pipeline.ts swaps pythonScopePhase → scopeResolutionPhase.

The next language migration is now: implement EmitProvider, register
it, add to MIGRATED_LANGUAGES. No new pipeline phase, no orchestrator
copy-paste. The Python migration's 700+ lines of glue collapse to
~80 lines per future language.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (post MIGRATED_LANGUAGES flip): 191/191.
- tsc --noEmit clean.

* docs(emit-provider): migration cookbook for next-language porters

* refactor(scope-resolution): rename emit-core/ → scope-resolution/, EmitProvider → ScopeResolver

Reorganizes the registry-primary resolution layer for clarity and
contributor onboarding. Driven by feedback that "emit" was triple-
overloaded (graph-edge emission + tree-sitter capture extraction +
the provider name itself), and the flat 16-file emit-core/ folder
mixed five concerns.

External research (rust-analyzer hir-def/nameres, Pyright analyzer/,
TypeScript binder/checker, Roslyn Binder, IntelliJ Resolver, swc
semantic/, biome semantic/, semgrep naming/, JDT Binding, clangd
Sema) consistently uses **the phase name** for this layer, never an
output verb. "Scope resolution" matches our pipeline-phase name, the
plan, and the RFC.

## Folder rename

  emit-core/                              → scope-resolution/
  ├── (16 flat files)                     → ├── contract/scope-resolver.ts
                                            ├── pipeline/{run,registry,phase}.ts
                                            ├── passes/{receiver-bound-calls,
                                            │           free-call-fallback,
                                            │           compound-receiver,
                                            │           imported-return-types,
                                            │           mro}.ts
                                            ├── graph-bridge/{node-lookup,ids,
                                            │                 edges,references-to-edges,
                                            │                 imports-to-edges,
                                            │                 method-dispatch}.ts
                                            └── scope/{walkers,namespace-targets}.ts

Each subfolder maps to one concern a new contributor needs to find:
*the contract I implement / the runner that calls me / the helpers I
reuse / the graph layer I shouldn't touch / the scope walkers*.

## Symbol renames

  EmitProvider                  → ScopeResolver
  pythonEmitProvider            → pythonScopeResolver
  runPythonScopeResolution      → resolvePythonScope
  EMIT_PROVIDERS                → SCOPE_RESOLVERS
  getEmitProvider               → getScopeResolver
  RunPythonScopeResolution{Input,Stats} → ResolvePythonScope{Input,Stats}

## File renames (per-language)

  languages/python/emit/index.ts → languages/python/scope-resolver.ts
  languages/python/emit-captures.ts → languages/python/captures.ts
                                     (kills the parse-side "emit" collision)

## Mechanics

- Used `git mv` for all files so blame history is preserved.
- Updated ~30 import lines across 18 files plus the pipeline-phases
  barrel and pipeline.ts.
- Updated JSDoc cross-references throughout to match the new vocabulary.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (post MIGRATED_LANGUAGES flip): 191/191.
- tsc --noEmit clean.

Migration cookbook in `scope-resolution/contract/scope-resolver.ts`
JSDoc points the next-language porter at all the new names and
folder locations.

* docs(scope-resolution): finalize phase JSDoc + drop python emoji from generic log line

* perf(scope-resolution): O(1) workspace lookup index

Introduces `WorkspaceResolutionIndex` — a precomputed bundle of
lookup tables built ONCE per resolution run, after `populateOwners`
and after finalize, before any pass that needs to find members,
exported defs, or class scopes by id.

What it replaces (all are pre-existing O(N×D) linear scans of
parsedFiles, called inside the receiver-bound MRO chain):

- `findOwnedMember(ownerId, name, parsedFiles)` → `Map.get` via
  `index.memberByOwner.get(ownerId)?.get(name)`. Was the worst
  offender — receiver-bound dispatcher calls this O(sites × MRO
  depth) times.
- `findExportedDef(filePath, name, parsedFiles)` → `Map.get` via
  `index.defsByFileAndName`. Hot for namespace-receiver case.
- `findExportedDefByName` workspace-wide fallback scan → `Map.get`
  via `index.callablesBySimpleName`.
- `classScopeByDefId` (rebuilt inside `emitReceiverBoundCalls` on
  every invocation) — moved to one-shot build during finalize, read
  from `index.classScopeByDefId` everywhere.
- `moduleScopeByFile` (rebuilt inside `propagateImportedReturnTypes`
  on every invocation) — read from `index.moduleScopeByFile`.

Findings from a synthetic 100-file Python workload (60 model files
each defining 5 classes × 3 methods + 40 user files calling them
heavily):

  scope-resolution wall time: 764ms → 710ms (median, 5 iters)

That's a ~7% in-layer win. The smaller-than-expected gain was
informative: profiling the synthetic workload shows scope-resolution
breakdown is `extract=62% resolve=30% emit=4%`; the index touched
the 4% slice (emit + walker calls inside it). Larger O(D) per owner
classes will benefit more.

Profiling the FULL pipeline (49 fixtures × 3 iters) shows
scope-resolution accounts for ~1% of pipeline wall time — the
remaining 99% is parse (tree-sitter), heritage, ORM, MRO, processes,
and DB writes. So further optimization of this specific layer has
marginal pipeline impact; the next-biggest wins live in those
phases. Documented as the "double-parse" finding in the audit
(captures.ts re-parses each Python file even though the parse phase
already produced a tree-sitter Tree) — that's a separate plumbing
project across phase boundaries.

Bonus: opt-in PROF_SCOPE_RESOLUTION=1 env var prints a per-phase
ms breakdown to stderr, so future perf work can measure without
extra code changes.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* perf(parse/heritage/mro): typed graph iterator + cross-phase tree cache

Two structural perf wins targeting the parse / heritage / MRO
layers, identified by the post-WorkspaceResolutionIndex profiling
(scope-resolution = ~1% of pipeline; the bulk lives upstream).

## 1. KnowledgeGraph.iterRelationshipsByType (PHM-Units 1-2)

- Adds a per-type `Map<RelationshipType, Map<id, Relationship>>`
  index inside `createKnowledgeGraph`, maintained on add / remove /
  removeNode / removeNodesByFile.
- New `iterRelationshipsByType(type)` returns a typed iterator that
  yields only the requested type. Backwards-compatible: existing
  `iterRelationships()` / `forEachRelationship()` callers untouched.
- Migrated two MRO call sites:
  - `mro-processor.ts buildAdjacency`: split the single
    `forEachRelationship` (which scanned every edge in the graph and
    type-filtered per-iteration) into three typed iterations
    (EXTENDS, IMPLEMENTS, HAS_METHOD).
  - `scope-resolution/passes/mro.ts buildMro`: replaced
    `for (const rel of graph.iterRelationships()) if (rel.type !== 'EXTENDS') continue`
    with `for (const rel of graph.iterRelationshipsByType('EXTENDS'))`.
- Heritage-processor (PHM-Unit 3) was a no-op: it only WRITES
  EXTENDS/IMPLEMENTS edges, never re-reads. Index is still useful
  for the seven other graph-iter consumers (community-processor,
  csv-generator, wildcard-synthesis, process-processor, etc.) — those
  follow-ups can switch to the typed iterator without touching the
  graph layer.
- Adds 5 unit tests for the new method (add/remove/dedupe semantics,
  empty-type fresh iterator, removeNode index sync).

## 2. Cross-phase tree cache (PHM-Units 4-5)

The audit's #2 finding: Python files are parsed by tree-sitter once
in the parse phase, then re-parsed inside scope-resolution's
`captures.ts`. Eliminate the second parse by sharing the Tree across
phases.

- `parse-impl.ts` now maintains TWO ASTCaches with distinct lifetimes:
  - `astCache` (chunk-local, cleared between chunks) — unchanged;
    used by call/heritage/import processors during parse.
  - `scopeTreeCache` (total-parseable-sized, never cleared) — new,
    exposed via `ParseOutput.astCache` for cross-phase consumption.
- `parsing-processor.ts` writes every sequentially-parsed Tree to
  BOTH caches. Worker-mode parses skip the persistent cache too
  (Trees can't cross MessageChannels).
- `LanguageProvider.emitScopeCaptures` gains an optional `cachedTree`
  parameter (typed `unknown` to keep the tree-sitter dep out of the
  contract).
- `captures.ts` short-circuits its own `parser.parse(sourceText)`
  when a cached Tree is supplied. Cache miss falls back to a fresh
  parse — same correctness path as before.
- `runScopeResolution` accepts an optional `treeCache` and forwards
  per-file `cachedTree` to `extractParsedFile`.
- `scope-resolution/pipeline/phase.ts` reads
  `getPhaseOutput<{astCache}>(deps, 'parse')` and passes through.

Verified end-to-end: a small fixture run with PROF_SCOPE_RESOLUTION=1
shows 6/6 cache hits (100% hit rate) on the python-grandparent fixture
that exercises the full pipeline below the worker-pool threshold.

## Verification

- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- New graph.test.ts: 25/25 (was 20).
- tsc --noEmit clean.

## Where the win lands

Wall-clock on the 49-fixture integration suite: 14050ms → 14080ms
(within noise). Fixtures are 1-3 files each, dominated by per-fixture
pipeline overhead (worker-pool init, DB writes, fixture startup).
The cache + typed-iterator wins are constant-factor improvements
that scale linearly with workload size and visible only on larger
repos. The dev-mode `PROF_SCOPE_RESOLUTION` instrumentation +
`getPythonCaptureCacheStats()` are kept for future perf work.

## Plan

docs/plans/2026-04-20-002-perf-parse-heritage-mro-plan.md.
PHM-Unit 3 (heritage-processor migration) intentionally collapsed
to a no-op — heritage only writes, never re-reads.

* perf(scope-resolution): bound tree-cache lifetime + gate population

Address P1 residuals from ce:review of 8c6f5cee:

- Dispose scopeTreeCache at end of scopeResolutionPhase via
  astCache.clear(). Trees were previously retained for the full
  pipeline (10-100x memory regression on large repos). Downstream
  phases (mro, community, csv-generator) never read them.
- Gate scopeTreeCache.set on provider.emitScopeCaptures !== undefined.
  Polyglot repos no longer retain Trees for languages with no
  scope-resolution consumer.
- PROF_SCOPE_RESOLUTION=1 now warns when workers engage, since
  Trees can't cross MessageChannels so the cache will be empty for
  worker-parsed files — prevents a silent perf cliff once a repo
  crosses the worker-pool threshold.

Tests: 26/26 graph unit, 299/299 scope-resolution unit, 191/191
python integration both flag paths.

* refactor(scope-resolution): clean up P2/P3 review residuals

P2:
- WASM dual-ownership invariant documented on ASTCache dispose:
  a Tree must live in AT MOST ONE disposing ASTCache. Native
  tree-sitter today is unaffected; WASM adoption would require
  tree.copy() or a non-disposing secondary cache.
- mro-processor C3 ordering test: pins EXTENDS-before-IMPLEMENTS
  parent grouping for classes with interleaved edge additions.
  Asserts exact MRO ['Base', 'Iface'] — a revert to single-loop
  insertion-order iteration would produce ['Iface', 'Base'] and
  fail loudly.
- cached-tree parity test: emitPythonScopeCaptures(src, path, T)
  returns identical CaptureMatch[] to emitPythonScopeCaptures(src,
  path). Pins the cache-hit path's correctness so a regression
  that silently returns stale captures would break the test.

P3:
- Dev-mode cache counters moved from captures.ts to cache-stats.ts.
  Production hot-path module no longer carries the module-global
  export surface; PROF gating behavior preserved.
- ParseOutput field rename astCache → scopeTreeCache. Clarifies
  that the surfaced cache is the persistent cross-phase one, not
  the chunk-local astCache parse-impl clears between chunks.
  Single consumer (scopeResolutionPhase) updated; no other readers.
- ASTCacheReader interface extracted. scopeResolutionPhase now
  reads the phase dep via a shared type instead of a hand-rolled
  inline structural shape that could drift from ASTCache's contract.
- graph.ts dual-index invariant enforced through writeRel/deleteRel
  private helpers instead of duplicated add/delete at 3 mutation
  sites. Adding a new mutation method only needs to call the
  helpers — forgetting to update one index becomes structurally
  impossible.

Tests: 382/382 unit (incl. 2 new), 191/191 python integration both
flag paths. tsc clean.

* fix(ci): prettier formatting + Python-migration test adjustments

CI run 24666612657 failed on three jobs. Fixes:

quality/format:
- Prettier --check flagged 3 files after the accumulated branch work.
  Ran prettier --write from repo root (CI's invocation cwd) to apply:
  simple-hooks.ts, resolve-references.ts, python-hooks.test.ts.

tests/{ubuntu,macos,windows} — 9 assertion failures, all traceable to
Python landing in MIGRATED_LANGUAGES (default-on registry-primary):

  - registry-primary-flag.test.ts (3 tests): the 'returns false by
    default' / 'primaryLanguages empty' / 'Python mid-process
    mutation' assertions were written in Ring 2 when MIGRATED_LANGUAGES
    was empty. Rewrote to assert MIGRATED_LANGUAGES membership is the
    default, use Java (unmigrated) for the no-stale-cache test, and
    verify env overrides work in both directions (migrated-off,
    unmigrated-on).
  - call-processor.test.ts (6 tests in SM-10 + D2-widen blocks):
    these exercise the LEGACY call-resolution DAG on .py fixtures.
    processCalls now gates Python out (isRegistryPrimary === true by
    default), returning 0 edges. Added REGISTRY_PRIMARY_PYTHON=false
    override in the relevant beforeEach + restore in afterEach, so
    the legacy DAG runs for these test-local fixtures without
    affecting the production-default behavior.

Local verification: 4126/4126 unit tests pass, prettier clean.

* docs(python): known-limitation block on scope-resolution public API

Unit 10 — document what the Python registry-primary path intentionally
does not resolve, so reviewers and future maintainers can distinguish
conscious trade-offs from latent bugs:

- Dynamic attribute access (getattr / setattr)
- Dynamic imports (importlib, __import__)
- Metaclass-driven dispatch
- Union / Optional branch-picking behavior
- Arbitrary signature-rewriting decorators
- typing.TYPE_CHECKING-guarded imports
- *args / **kwargs type flow-through
- super() outside a directly-bound method

Each item names the file that owns the relevant hook so a future
follow-up knows where to start. Shadow-harness corpus parity + the
CI parity gate remain the authoritative signal for which of these
matter at fleet scale.

* docs: record scope-resolution pipeline alongside legacy call DAG

Capture what shipped in #980 so future readers don't have to reverse-
engineer the coexistence of the legacy call-resolution DAG and the new
scope-resolution pipeline:

- ARCHITECTURE.md: new 'Scope-Resolution Pipeline' section after the
  Call-Resolution DAG, documenting pipeline stages, ScopeResolver
  contract, per-language registration, code references, and perf
  notes. Coexistence block added to the legacy DAG section explaining
  how MIGRATED_LANGUAGES gates the two paths per-language.
- AGENTS.md: reference-docs pointer updated — legacy-DAG one-liner
  stays; scope-resolution pipeline gets its own pointer so agents
  know when to read which section. Changelog bumped.
- type-resolution-system.md: callout at the 'call-processor.ts is
  the consumer' claim pointing readers to the scope-resolution path
  for migrated languages. TypeEnv is still built per file, but for
  migrated languages receiver typing flows through ParsedTypeBinding
  rather than call-processor.ts.

CHANGELOG.md intentionally not touched — owned by the release process.

* chore: remove obsolete scheduled_tasks.lock file

* fix(scope-resolution): qualified-name keys for same-file method collisions

Review feedback from PR #980 reviewer flagged a BLOCKING correctness
bug: when two classes in the same file define a method with the same
simple name (e.g. class User: def save + class Document: def save),
every d.save() CALLS edge silently resolved to User.save because the
graph node lookup keyed only by (filePath, simpleName) and first-wins
took User's method.

Three-layer fix:

1. populateClassOwnedMembers now promotes a nested def's
   qualifiedName from `save` to `ClassName.save` when the def sits
   inside a class scope. Python's scopes.scm doesn't emit
   @declaration.qualified_name for methods, so without this the
   finalized SymbolDefinition carried only the simple name.
2. buildGraphNodeLookup adds a second key per node:
   (filePath, qualifiedName). For Method/Function nodes the qualifier
   is parsed deterministically out of the node id
   (`Method:file.py:User.save#N` → `User.save`), which is robust to
   Windows-style filePath colons. Simple-name key retained as a
   fallback for callers that don't know the qualifier.
3. resolveDefGraphId now tries the qualified key first, then falls
   back to the simple-name lookup.

Also addresses the non-blocking review items:

- scopeResolutionPhase.deps now includes `crossFile` so the Kahn's
  runner can't schedule scope-resolution before crossFile finishes
  writing heritage edges that buildMro consumes.
- run.ts no longer mutates the finalized ScopeResolutionIndexes via
  `as` cast — spreads into a fresh object with the populated
  methodDispatch field instead.
- Doc nits: scope-resolver.ts registry path + phase.ts Ring number.

Test coverage:
- New fixture test/fixtures/lang-resolution/python-same-file-method-collision
  with User.save + Document.save in one file and app.py calling both
  through typed receivers.
- Three new integration assertions pin that u.save() and d.save()
  target the correct qualified node id. Fail before the fix, pass
  after. Confirmed by running once without populateClassOwnedMembers
  qualifier promotion — reproduces the original User.save-for-both bug.

Verification: 194/194 test/integration/resolvers/python.test.ts pass
both REGISTRY_PRIMARY_PYTHON=0 and =1. 523/523 related unit tests.
tsc --noEmit clean.

* fix(scope-resolution): filter export index to module-level defs + label-prefixed qualified key

Codex adversarial review on PR #980 flagged that
buildWorkspaceResolutionIndex feeds defsByFileAndName and
callablesBySimpleName from parsed.localDefs — the flat set of every
def in the file including methods, fields, and nested functions.
findExportedDef / findExportedDefByName treat those maps as
file-level exports, so `mod.save()` could silently bind to User.save
whenever a method's simple name appeared first in parse order.

Plan: docs/plans/2026-04-21-001-fix-workspace-index-module-scope-only-plan.md

Fix layers:

1. workspace-index.ts: split the single parsed.localDefs loop into
   two passes:
   - Module-export pass: iterate moduleScope.ownedDefs PLUS ownedDefs
     of every child scope whose parent is the module scope. Top-level
     class and function declarations each live in their own scope
     with parent=module, not in moduleScope.ownedDefs directly, so
     the "parent === moduleScope.id" walk is required to reach them.
     Methods (scope.parent === Class scope) and nested functions
     (scope.parent === another Function scope) are excluded.
   - Member-by-owner pass: keeps iterating parsed.localDefs since
     that map is keyed on ownerId and correctly saw class-owned defs
     before this change.

2. graph-bridge/node-lookup.ts: qualified keys now live in a separate
   keyspace (`<q>:filePath::<label>::<qualifiedName>`) and include
   the node label. Without the label prefix, a top-level `def save`
   (Function, qualifier `save`) would collide with a class method
   `User.save` (Method, simple name `save`) in the same simple-key
   slot because the Function's qualifier happens to equal the
   Method's simple name. The label differentiates them.

3. graph-bridge/ids.ts: resolveDefGraphId uses the new
   type-prefixed qualified key when def.type is set. Simple-name
   fallback retained for languages that don't yet synthesize
   qualifiers on their defs.

Test fixture: python-module-export-vs-method-collision places
`class User: def save` BEFORE top-level `def save` — parse order
that exposes the bug (class method enters the index first). Three
new integration assertions:
  - `mod.save(x)` resolves to the module-level Function, not User.save
  - `u.save()` resolves to User.save Method
  - Exactly two CALLS edges to `save` exist, one per intended target

Fixture confirmed failing before the workspace-index fix (bug
reproduced), passing after.

Verification: 197/197 test/integration/resolvers/python.test.ts pass
both REGISTRY_PRIMARY_PYTHON=0 and =1. 523/523 related unit tests.
tsc --noEmit clean.

* fix(scope-resolution): drive module export index from moduleScope.bindings

Codex round-2 adversarial review flagged that the workspace-index
module-export pass iterated every def in every direct-child scope of
the module, including class-body Variable defs like
`class User: MAX_USERS = 100`. `defsByFileAndName[file][MAX_USERS]`
silently aliased to the class attribute. Latent today because Python
doesn't emit ACCESSES edges for `mod.NAME` member access, but the
index-layer leak would surface the moment reference capture widens.

Plan: docs/plans/2026-04-21-002-fix-codex-round2-scope-resolution-plan.md

Drive the module-export index from the extractor invariant instead of
a scope-kind → allowed-label switch:

moduleScope.bindings already contains exactly the names visible at
module level — top-level class/function declarations, module-level
variable assignments, imports. Class methods, class-body attributes,
and nested-function defs bind to their containing (Class or Function)
scope, not the module, so they're naturally excluded.

Filter to `BindingRef.origin === 'local'` so imports and wildcard
re-exports stay out of the index (matches the pre-fix invariant when
the source was `parsed.localDefs`).

No per-kind predicates, no scope-kind / def-kind enumeration, no
two-pass merge between moduleScope.ownedDefs and direct-child scope
walks — one loop, language-agnostic.

Codex also flagged `propagateImportedReturnTypes` as potentially
broken for function-local imports, but scope-dump probing showed the
finalize algorithm puts `from svc import get_user` into the MODULE
scope's finalized bindings even when declared inside a function, so
the existing module-scope propagation already handles the case. The
new python-function-local-import-chain integration test pins that
working behavior as a regression guard; no code change required.

Coverage:
- test/unit/scope-resolution/workspace-index.test.ts (new, 5 tests) —
  directly asserts the index shape. The "excludes class-body Variable
  defs" test fails without this fix and passes after (confirmed via
  stash-pop probe).
- test/integration/resolvers/python.test.ts — 4 new integration
  assertions across two describe blocks (python-class-attr-export-leak,
  python-function-local-import-chain) pin end-to-end invariants.
- Two new fixtures under test/fixtures/lang-resolution/.

Verification: 201/201 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 528/528 related unit tests (was
523). tsc clean.

* test(scope-resolution): pin local-namespace-import behavior + document empirical finalize hoisting

Codex round-3 adversarial review raised three concerns about
scope-resolution passes assuming module-scope semantics that would
contradict `pythonImportOwningScope`'s documented per-scope contract.
Empirical verification via scope-dump probes resolved each:

Plan: docs/plans/2026-04-21-003-fix-codex-round3-scope-aware-resolution-plan.md

1. Function- and class-local namespace imports: VERIFIED WORKING.
   `def outer(): import svc as s; s.call()` and `class A: import mod;
   def use(self): mod.helper()` both emit CALLS edges with reason
   "scope-resolution: namespace-receiver". finalize-algorithm hoists
   the ImportEdges onto `indexes.imports[moduleScope]` regardless of
   where the `import` statement appears, so collectNamespaceTargets'
   module-scope read finds them.

2. Imported return-type propagation module-scope-only: VERIFIED
   WORKING (already pinned in round 2). `from svc import get_user`
   inside a function body lands in indexes.bindings[moduleScope], so
   propagateImportedReturnTypes' module-scope read still finds it.

3. Nested method-local defs stamped as class members: VERIFIED FALSE.
   The scope extractor creates nested Function scopes for inner
   `def`s; `def helper` inside `def save` inside `class User` lives
   in helper's own Function scope whose parent is save's Function
   scope (NOT the Class scope). populateClassOwnedMembers'
   `parentScope.kind === 'Class'` branch correctly skips it;
   helper.ownerId stays undefined.

Instead of implementing speculative scope-aware refactors that the
tests would pass regardless, this commit:

- Adds regression fixtures and integration assertions that pin each
  working behavior. If finalize routing ever changes to honor the
  hook's per-scope contract, these assertions flip red and signal the
  need for the scope-chain-aware refactor.
- Adds defensive JSDoc to the three flagged call sites
  (collectNamespaceTargets, propagateImportedReturnTypes,
  populateClassOwnedMembers) documenting the empirical invariant so
  future reviewers don't re-derive Codex's theoretical concern
  without the benefit of the probe.

Files:
- Two new fixtures under test/fixtures/lang-resolution/ covering the
  function-local and class-body namespace-import patterns.
- Two new describe blocks in test/integration/resolvers/python.test.ts
  (3 assertions, positive-pin intent).
- Defensive comments in namespace-targets.ts, imported-return-types.ts,
  and scope-resolution/scope/walkers.ts.

Verification: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. tsc clean.

* perf(graph): reverse-adjacency + file indexes drop removeNode/removeNodesByFile from O(N)

PR #980 in-line review flagged that `removeNode` iterated the full
relationshipMap to find edges touching a node (O(E)), and
`removeNodesByFile` called removeNode for every matching node after
a full nodeMap scan (O(N × E)). Pre-existing, but worth fixing
properly since the writeRel/deleteRel helpers we just added make the
index-maintenance story coherent.

Two new indexes maintained on every mutation path:

- `edgeIdsByNode: Map<nodeId, Set<relId>>` — reverse adjacency. Every
  edge records both endpoints, so removeNode iterates
  edgeIdsByNode.get(id) instead of every relationship. Self-edges
  skip the duplicate-endpoint write to keep the Set dedup explicit.
- `nodeIdsByFile: Map<filePath, Set<nodeId>>` — file index.
  removeNodesByFile reaches its file's nodes directly.

Complexity:
- removeNode: O(edges-touching-node), was O(total-edges).
- removeNodesByFile: O(file-nodes × avg-edges-per-node + scan of the
  file bucket), was O(total-nodes + file-nodes × total-edges).

Index maintenance is centralized in writeRel/deleteRel + new
addToBucket/removeFromBucket helpers. Empty buckets are pruned to
keep the indexes compact. Existing dual-invariant (relationshipMap ↔
relationshipsByType) preserved.

Nodes without a `filePath` property (e.g. Community/Cluster nodes)
are intentionally NOT indexed in nodeIdsByFile — they can't belong
to any file, so removeNodesByFile correctly leaves them alone.

Coverage: 7 new unit tests (33/33 total, was 26). Added cases:
- removes only edges touching the removed node
- handles self-edges
- removes orphan node with no edges
- removeNodesByFile removes only matching nodes
- returns 0 when no match
- also removes edges whose endpoints lived on the removed file
- does not index nodes without a filePath property

Verification: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 4235/4235 unit tests. tsc clean.

* refactor(ingestion): merge python/ast-utils into utils/ast-helpers; iterative findNodeAtRange

python/ast-utils.ts held three language-agnostic helpers
(nodeToCapture, syntheticCapture, findNodeAtRange) plus two
duplicates of the shared utils version (findChildOfType ==
findChild; findIdentifierChild was unused). Consolidating into
utils/ast-helpers.ts so the next language migrating to the
scope-resolution pipeline imports from one place.

findNodeAtRange rewritten iteratively using an explicit stack.
Previous implementation was recursive — fine for shallow Python
trees today, but a landmine for languages with deeper nesting
(Kotlin sealed-hierarchy decomposition, Rust macro expansion,
etc.) and the task hooks explicitly call out "no recursion".
Children are pushed reverse-index so LIFO pop visits them
left-to-right; row-bound pruning preserves the prior early-skip
optimization (the `break` shortcut is replaced with `continue`
since a stack can't leverage ordered sibling termination).

findChildOfType consumers migrated to the existing findChild
helper. findIdentifierChild deleted — no callers remained.

Coverage: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 339/339 scope-resolution +
graph unit tests. tsc clean.

* refactor(scope-resolution): remove unused shouldShadow / shouldCreateScope hooks

Both LanguageProvider hooks were dead weight:

- `shouldShadow` had zero call sites — the interface declared it,
  Python implemented a trivial always-true no-op, but no consumer
  ever read it. The shadowing decision lives in pythonMergeBindings
  and the central merge algorithm, not in a per-scope predicate.
- `shouldCreateScope` had one call site in pass1BuildScopes but the
  only language implementing it (Python) always returned true. No
  producer ever emits a `@scope.block` for Python, so the hook's
  "declines to create" branch was unreachable. Other languages
  didn't implement it at all.

Removing both:

- Drops the interface declarations in language-provider.ts.
- Drops `shouldCreateScope` from ScopeExtractorHooks Pick and from
  the pass1BuildScopes conditional — the stack-based parent-resolve
  loop becomes unconditional.
- Drops pythonShouldShadow / pythonShouldCreateScope from simple-hooks,
  the Python index barrel, and the python.ts provider wiring.
- Drops the tests that exercised the removed hooks: one block-
  suppression scenario in scope-extractor.test.ts, one shouldCreateScope
  test in parse-worker-scope-integration.test.ts, and the
  pythonShouldShadow / pythonShouldCreateScope always-true assertions
  in python-hooks.test.ts. pythonBindingScopeFor's delegate-to-default
  test is preserved in its own describe block.

Shadowing itself is unchanged: pythonMergeBindings still runs, LEGB
ordering still applies, wildcard transparency is still handled via
the merge precedence rules. The hook API just no longer has a
vestigial per-scope toggle we decided not to use.

Verification: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 335/335 scope-resolution + graph
unit tests (was 339, net -4 after removing the hook-specific
assertions). tsc clean.

* refactor(scope-resolution): drop dead exports surfaced by knip

Knip flagged 44+ dead exports in the PR surface. Cleanup:

Barrel deletion:
- Remove src/core/ingestion/scope-resolution/index.ts entirely.
  It re-exported 30+ symbols but only one file
  (languages/python/scope-resolver.ts) imported from it, and only
  7 symbols. Matches the project's "no barrel re-exports" preference
  and removes a drift surface. scope-resolver.ts now imports from
  concrete files (passes/mro.ts, scope/walkers.ts, contract/...).

Dead functions/interfaces removed:
- resolvePythonScope + ResolvePythonScopeInput + ResolvePythonScopeStats
  in languages/python/scope-resolver.ts — never called. pipelinePhase
  reaches pythonScopeResolver via SCOPE_RESOLVERS, not via a
  per-language entry point.
- getScopeResolver in scope-resolution/pipeline/registry.ts — had zero
  callers. Consumers read SCOPE_RESOLVERS directly.

Exports demoted to module-internal (used only within their own file):
- PYTHON_SCOPE_QUERY (query.ts) + its re-export from python/index.ts
- PROF (cache-stats.ts)
- PythonArityMetadata (arity-metadata.ts)
- ReferenceSiteSkipSet (graph-bridge/references-to-edges.ts)
- ReceiverBoundProviderSubset (passes/receiver-bound-calls.ts)
- ResolveCompoundReceiverOptions interface (passes/compound-receiver.ts)
- matchingOpenParen function (passes/compound-receiver.ts)
- followChainPostFinalize function (passes/imported-return-types.ts)
- RunScopeResolutionInput + RunScopeResolutionStats (pipeline/run.ts)

Also removed:
- Redundant `export type { Scope }` re-export from contract/scope-resolver.ts
  (consumers import Scope directly from gitnexus-shared).

Verification: knip reports zero dead exports in PR-touched files.
204/204 test/integration/resolvers/python.test.ts both flag paths.
335/335 scope-resolution + graph unit tests. tsc clean.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-21 15:50:00 +01:00
evolution
2ac1baf450
fix(docker): use inputs.tag to detect workflow_call context (#996)
In a reusable workflow, github.event_name inherits the caller's
event (e.g. "push"), not "workflow_call". This caused the
type=raw tag to be disabled when docker.yml was called from
release-candidate.yml, producing no Docker tags at all and
failing the build.

Fix: check `inputs.tag != ''` instead, since inputs.tag is only
populated for workflow_call invocations.

Co-authored-by: wangjichao <wangjichao@inke.cn>
2026-04-20 18:20:57 +01:00
Copilot
d976038dc8
fix: guard RC docker job against empty vtag and add early validation in docker.yml (#983)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* Initial plan

* fix: guard docker job and add tag validation in docker.yml

- Add `&& needs.publish.outputs.vtag != ''` to the `docker` job's
  `if:` in release-candidate.yml so it is skipped when publish
  produces no vtag, preventing an opaque buildx "tag is needed" error.
- Add an early "Validate tag input" step in docker.yml that fails fast
  with a clear ::error:: message when inputs.tag is empty, covering
  direct workflow_call invocations that bypass the release-candidate
  guard.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b9afe2df-85ea-4a87-bf30-77f0e945a64d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: scope docker.yml tag validation to workflow_call only

Direct tag-push triggers (on: push, tags: v*) populate the tag from
GITHUB_REF and have inputs.tag empty, so the unconditional validation
step would fail every direct tag-push run. Restrict the new step to
workflow_call invocations, which is the only path where an empty tag
is actually a problem.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4b7e3bfa-15c0-4186-affa-95cd71e50153

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-19 09:41:30 +01:00
Copilot
fa39a4b4a5
fix(docker): build and push Docker images for Release Candidates (#978) 2026-04-19 07:46:21 +01:00
Copilot
3adb97e993
feat(docker): ship signed UI + CLI/server images via docker-compose (#967)
* Initial plan

* docker: ship signed UI + CLI/server images via docker-compose

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/883bcee1-4a1d-4b3d-bbb9-accd8846da96

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker: lock image version to npm package + harden cosign verify guidance

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6afd4fcd-5656-4e02-b796-a22b59000bde

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker: add Sigstore ClusterImagePolicy + k8s admission docs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/aea3dd70-e2a9-443a-b578-cb3eca4093e1

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker(k8s): collapse redundant image globs in ClusterImagePolicy

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/aea3dd70-e2a9-443a-b578-cb3eca4093e1

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker(ci): drop deprecated COSIGN_EXPERIMENTAL, dead build-args, and loose verify regex in comment

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bdf0d2cf-607c-4558-982a-be9b216b2d36

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker(ci): use ${{ github.repository }} in verify-comment regex for fork portability

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bdf0d2cf-607c-4558-982a-be9b216b2d36

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style(deploy): prettier-format cluster-image-policy.yaml (single quotes)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b09a016e-56a1-4b73-bacc-e69084a48782

* ci(docker): drop workflow_dispatch, harden signing loop, fix verify-comment placeholder

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6755064c-7871-4b2e-9b46-b4779eb215ac

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-18 20:55:19 +01:00
Kritik Bangera
040bb7a489
feat: add docker support (#848)
* feat: add docker support

* feat: move docker files to root

* feat: add docker build and push workflow

* fix: pin docker action SHAs to verified commits

Made-with: Cursor

* fix: remove redundant --platform=$TARGETPLATFORM from runtime stage

Made-with: Cursor

* fix: upgrade docker actions to Node.js 24-compatible versions

Made-with: Cursor

* docs: updated readme

* fix: update docker references

* fix(docker-server): reject null bytes in resolvePath

Defensively harden the path traversal guard by returning null
early when the URL contains a null byte, before normalization runs.

Made-with: Cursor

* fix(docker-server): handle createReadStream errors

Attach an error listener before piping so mid-flight read errors
(truncated file, permission change) cleanly destroy the response
instead of being silently swallowed.

Made-with: Cursor

* fix(docker-server): replace existsSync with async stat

Eliminates the TOCTOU race between the initial stat call and the
subsequent existsSync check. Reuses the async stat pattern already
in place and removes the now-unused existsSync import.

Made-with: Cursor

* test(docker-server): add integration tests; fix %00 null-byte bypass

Decode the URL before the null-byte check so percent-encoded null
bytes (%00) are also rejected with 400 instead of falling through
to the SPA fallback. Adds 5 node:test integration tests covering
valid assets, SPA fallback, path traversal, null bytes, and 404.

Made-with: Cursor

* style: fix prettier formatting in docker-server files

Made-with: Cursor

* fix(docker): wire tests into CI, fix resolvePath separator, correct image namespace

- Add `node --test docker-server.test.mjs` step to ci-tests.yml so the
  path-traversal guard tests run in every CI pass instead of being silently skipped.
- Fix resolvePath containment check: `startsWith(root)` would allow sibling
  directories like `/app/dist-evil/`; now guards with `root + sep` or exact match.
- Update docker-compose.yaml default image from `abhigyanpatwari` namespace to
  `brainifii` to match what docker.yml publishes to GHCR.

* fix(docker): update apt-get commands and set user permissions

- Modify Dockerfile and Dockerfile.test to include options for apt-get to bypass validity checks during updates.
- Set ownership of the /app directory to the 'node' user in the runtime stage for improved security and proper permission handling.

* fix(docker): switch to Alpine base images for smaller footprint

- Update Dockerfile to use Alpine-based Node.js images for both builder and runtime stages, reducing image size and improving performance.
- Replace apt-get commands with apk for package installation in the runtime stage.

* fix(docker): update Node.js version in Dockerfile

- Change base image from node:20-alpine to node:22-alpine

* fix(docker): update Node.js version in Dockerfile to 22-alpine for runtime

---------

Co-authored-by: kritik.b <kritik.b@media.net>
2026-04-18 08:39:18 +01:00
Gergő Magyar
d024119b33
chore(deps): tree-sitter 0.25 upgrade readiness monitor with daily Dependabot (#847)
* chore(deps): add tree-sitter aware Dependabot config and drift monitoring

Two things Dependabot cannot see on its own:

1. ABI consistency. The tree-sitter runtime supports a known range of
   grammar ABIs. When a grammar bumps past that range, require() silently
   fails and fallback paths mask the regression in test coverage.
2. Vendored upstream drift. vendor/tree-sitter-proto is a snapshot of
   coder3101/tree-sitter-proto regenerated against a pinned cli version.
   Upstream keeps moving. Nothing notices until a maintainer remembers to
   look.

Dependabot configuration
- Added npm ecosystems for gitnexus, gitnexus-web, gitnexus-shared.
- Grouped all tree-sitter-* grammar bumps into one PR (ecosystem moves in
  lockstep, one PR per grammar is noise).
- Pinned the tree-sitter runtime itself. Bumping 0.21 to 0.22+ changes
  which grammar ABIs load and requires coordinated updates to the
  vendored proto grammar. That stays a deliberate human decision.
- Pinned tree-sitter-cli for the same reason (it controls which ABI
  vendor/tree-sitter-proto/src/parser.c emits when regenerated).

Drift check (.github/scripts/check-tree-sitter-drift.py)
- Reads the tree-sitter runtime version from gitnexus/package.json.
- Walks every installed tree-sitter-* grammar plus the vendored proto
  and reports its LANGUAGE_VERSION against the runtime's supported ABI
  range (table maintained in the script; extend when bumping runtime).
- Fetches coder3101/tree-sitter-proto main parser.c and compares byte
  for byte to the vendored copy. Reports the upstream HEAD short SHA
  and the upstream ABI so a maintainer can act.
- Prints a Markdown report; exits 0 when everything is in range and
  matches upstream, 1 otherwise.
- Stdlib only, no external deps.

Drift workflow (.github/workflows/tree-sitter-drift-check.yml)
- Runs weekly (Mondays 09:00 UTC) to match Dependabot's cadence.
- Also runs on PRs that touch the script or workflow itself, where it
  fails the PR check on drift so the drift gate cannot land broken.
- On scheduled runs with drift, opens or updates a single tracking
  issue labeled tree-sitter-drift. On scheduled runs that come back
  clean, closes the open tracking issue (if any) with a comment.

* refactor(deps): rewrite drift check as tree-sitter 0.25 upgrade readiness monitor

Replace the ABI drift pass/fail gate with a daily upgrade readiness
dashboard that tracks peer-dep compatibility of all 14 grammars with
tree-sitter@0.25.0 and reports which are ready, unreleased, or blocking.

Key changes:
- Rename drift-check → upgrade-readiness (script, workflow, job id)
- Fix P0: pass report via env var, not ${{ }} template interpolation
- Fix P1: npm fetch failure now adds a blocker instead of false-green
- Fix P1: pass GITHUB_TOKEN for authenticated GitHub API calls
- Switch Dependabot to daily for tree-sitter grammars
- Use dict for blockers (no prefix collision), derive TARGET_RUNTIME
  constant, reuse GRAMMARS parser_path, normalize CRLF in comparisons
- Reduce per-call HTTP timeout from 15s to 8s for workflow budget
- PR runs warn on blockers instead of hard-failing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(ci): remove global-upgrade smoke test workflow

The ci-global-upgrade.yml workflow tested npm global install upgrades
over a specific release candidate (1.6.2-rc.8). That RC has shipped
and the workflow is no longer needed. Remove it and all references
from ci.yml (needs, env vars, gate check).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(ci): add changelog comments to upgrade readiness tracking issue

Each daily run now posts a comment summarizing what changed before
updating the issue body. Comments include the ready/blocker counts
and a diff of grammar status changes (e.g. tree-sitter-cpp:
Unreleased -> Ready). Gives a timeline of how the upgrade unblocks.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:17:21 +01:00
Gergo Magyar
54d02fcc22 fix(ci): replace removed disable-releaser with dry-run for release-drafter v7
release-drafter v7 (merged in #852) removed the `disable-releaser`
input, causing the autolabel job to attempt creating a release and
fail with "Resource not accessible by integration". Replace with
`dry-run: true` which achieves the same label-only behavior.

Also update stale version comments for release-drafter and
action-semantic-pull-request to match the actual pinned versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:48:21 +01:00
Gergő Magyar
d50a523837
Merge pull request #853 from abhigyanpatwari/dependabot/github_actions/amannn/action-semantic-pull-request-6.1.1
chore(deps): bump amannn/action-semantic-pull-request from 5.5.3 to 6.1.1
2026-04-16 06:48:14 +01:00
Gergő Magyar
54c7e45a2f
Merge pull request #852 from abhigyanpatwari/dependabot/github_actions/release-drafter/release-drafter-7.2.0
chore(deps): bump release-drafter/release-drafter from 6.0.0 to 7.2.0
2026-04-16 06:48:11 +01:00
Gergő Magyar
e947a82a18
Merge pull request #851 from abhigyanpatwari/dependabot/github_actions/marocchino/sticky-pull-request-comment-3.0.4
chore(deps): bump marocchino/sticky-pull-request-comment from 2.9.4 to 3.0.4
2026-04-16 06:48:07 +01:00
Gergő Magyar
978187b34a
Merge pull request #850 from abhigyanpatwari/dependabot/github_actions/actions/github-script-9.0.0
chore(deps): bump actions/github-script from 7.0.1 to 9.0.0
2026-04-16 06:47:59 +01:00
dependabot[bot]
1d0fb782a3
chore(deps): bump amannn/action-semantic-pull-request
Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5.5.3 to 6.1.1.
- [Release notes](https://github.com/amannn/action-semantic-pull-request/releases)
- [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md)
- [Commits](0723387faa...48f256284b)

---
updated-dependencies:
- dependency-name: amannn/action-semantic-pull-request
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 20:17:22 +00:00
dependabot[bot]
7001e8e4b4
chore(deps): bump release-drafter/release-drafter from 6.0.0 to 7.2.0
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 6.0.0 to 7.2.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](3f0f87098b...5de9358398)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 20:17:15 +00:00
dependabot[bot]
ed07c18b8d
chore(deps): bump marocchino/sticky-pull-request-comment
Bumps [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) from 2.9.4 to 3.0.4.
- [Release notes](https://github.com/marocchino/sticky-pull-request-comment/releases)
- [Commits](773744901b...0ea0beb66e)

---
updated-dependencies:
- dependency-name: marocchino/sticky-pull-request-comment
  dependency-version: 3.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 20:17:10 +00:00
dependabot[bot]
df429ea60c
chore(deps): bump actions/github-script from 7.0.1 to 9.0.0
Bumps [actions/github-script](https://github.com/actions/github-script) from 7.0.1 to 9.0.0.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7.0.1...3a2844b7e9c422d3c10d287c895573f7108da1b3)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 20:17:08 +00:00
dependabot[bot]
b43cb5ee55
chore(deps): bump softprops/action-gh-release from 2.5.0 to 3.0.0
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.5.0 to 3.0.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](a06a81a03e...b430933298)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 20:17:03 +00:00
Gergő Magyar
fec06b823c
fix: devendor tree-sitter-proto install lifecycle to prevent ENOTEMPTY on global upgrade (#846)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / global-upgrade (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
* fix: devendor tree-sitter-proto install lifecycle to fix ENOTEMPTY on global upgrade

PR #843's preinstall cleanup hook cannot address the reported bug because
it runs on the NEW package's staging tree, not the OLD install being
removed. Issue #836 still reproduces on 1.6.2-rc.8.

Root cause: vendor/tree-sitter-proto was declared as `file:` dep with its
own `dependencies` and `install` script, so npm created
`vendor/tree-sitter-proto/node_modules/node-addon-api/` at install time,
which blocked npm's rmdir on global upgrade.

Changes:
- Strip `dependencies` and `install` script from the vendored sub-package's
  package.json so npm no longer creates a nested node_modules or runs a
  lifecycle script under vendor/.
- Hoist `node-addon-api` and `node-gyp-build` into gitnexus
  optionalDependencies; npm resolves them at the consumer's top level.
- Add scripts/build-tree-sitter-proto.cjs modeled on patch-tree-sitter-swift.cjs.
  Runs at gitnexus postinstall, best-effort: skips cleanly on missing
  toolchain or --ignore-scripts so non-proto functionality keeps working.
- Remove scripts/preinstall-cleanup.cjs — dead code; cannot run against
  the old install being removed.
- Keep .npmignore entries from PR #843 (tarball hygiene, still correct).
- Add explicit .gitignore rules for gitnexus/vendor/**/build and
  gitnexus/vendor/**/node_modules (closes the repo-side hygiene gap).
- Add .github/workflows/ci-global-upgrade.yml: matrix smoke test that
  installs the previously-published rc globally, upgrades to the packed
  current branch, and verifies no vendor install-time artifacts survive.
  Runs on macOS (reporter's platform), Linux, and Windows. Also includes
  an --ignore-scripts degraded-mode lane. Wired into ci.yml gate.

Plan: docs/plans/2026-04-15-002-fix-tree-sitter-proto-vendor-deps-plan.md

Phase 1 (this commit) addresses the reported `node_modules/node-addon-api`
hazard. Phase 2 (follow-up) will migrate to prebuildify + prebuilt .node
binaries in the tarball — the 2026 canonical shape for tree-sitter
grammars, which eliminates the postinstall compile path entirely.

Refs #836

* fix(ci): ci-global-upgrade should be reusable-only and use setup-gitnexus

Three issues caught by CI on PR #846:

1. Concurrency linter rejected the `CIGU-` prefix (allowlist is
   `${{ github.workflow }}` or substring `CI-`). The literal-prefix
   guidance in ci.yml is specifically about disambiguating when
   reusable workflows run in nested contexts, and ci-global-upgrade
   doesn't need its own concurrency block at all — the caller
   (ci.yml) already governs concurrency for nested invocations.

2. `npm install` in gitnexus/ runs `prepare: node scripts/build.js`,
   which depends on gitnexus-shared/dist being built first. Other CI
   jobs handle this via the setup-gitnexus composite action. Use it
   here too (with build: 'false' — we only need the dep graph, then
   npm pack runs prepack which builds gitnexus itself).

3. Removed `pull_request` and `workflow_dispatch` triggers. The
   workflow is now pure `workflow_call` — invoked once from ci.yml
   via `uses:`. This avoids the duplicate-run problem where both the
   top-level pull_request trigger AND the nested workflow_call would
   fire on every PR.

* fix(ci): relax vendor build/ guard and use bash shell on Windows

Two fixes for ci-global-upgrade failures on PR #846:

1. The guard after the upgrade step was rejecting vendor/tree-sitter-proto/build/
   in the global install. That was too strict. The original #836 bug was
   about vendor/tree-sitter-proto/node_modules/ specifically, not build/.
   The build/ directory appears because node-gyp-build compiles through the
   symlink npm creates at node_modules/gitnexus/node_modules/tree-sitter-proto,
   and its contents are plain .node, .obj, .lib files that rmdir handles
   without trouble. We know this empirically because the test got past the
   upgrade step in the run where the old vendor/node_modules was present.
   The guard now only flags nested node_modules, which is what the fix
   actually removes.

2. The Windows --ignore-scripts lane failed with ENOENT when npm tried to
   open the tarball. The path was computed in a bash step using $(pwd),
   which on Windows returns /d/a/... form, but npm install ran in the
   default cmd shell and received a mangled Windows path. Adding
   shell: bash to the install steps keeps path handling consistent.
2026-04-15 17:38:47 +01:00
dependabot[bot]
3fd4346bcb
chore(deps): bump actions/checkout from 4.3.1 to 6.0.2 (#842)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.3.1...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 13:52:01 +01:00
dependabot[bot]
c2734cd25e
chore(deps): bump actions/upload-artifact from 4.6.2 to 7.0.1 (#838)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](ea165f8d65...043fb46d1a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 13:37:10 +01:00