Skip to content

fix(core): bind the in-transaction capacity gate — one shared pool-id convention (NOT user-visible yet — see R2) - #2488

Merged
gsxdsm merged 4 commits into
mainfrom
feature/workflow-e2e-sweep-table
Jul 28, 2026
Merged

fix(core): bind the in-transaction capacity gate — one shared pool-id convention (NOT user-visible yet — see R2)#2488
gsxdsm merged 4 commits into
mainfrom
feature/workflow-e2e-sweep-table

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

The bug

moves.ts asked countActiveInCapacitySlotAsync for occupants of pool "builtin:coding", while the counter buckets selection-less rows under DEFAULT_WORKFLOW_POOL_ID ("__default-workflow__"). Nothing ever landed in the pool being asked about, so the count came back 0 and a finite limit could never bind.

Root fix, not a literal swap

A shared constant would not have prevented this: DEFAULT_WORKFLOW_ID was already imported in moves.ts and the code still wrote a literal. So both sides now call a shared function, resolveCapacityPoolId — "which pool does a selection-less task belong to" has exactly one answer and no call site is in a position to disagree with it.

The one variable serving two masters is split: a capacity pool key (a bucketing sentinel that must not collide with a workflow id) and a workflow id (telemetry, must stay a real id). The emitted TaskTransitioned payload is byte-identical.

Checked, not assumed: no second copy

scheduler.ts:2514 and :2536 do carry ?? "builtin:coding" — but as an IR resolution key (resolveWorkflowIrById), where a real workflow id is required and the pool sentinel would not resolve at all. Same literal, different concept, correctly used. A blanket replace would have broken it.

Something did depend on the gate being dead — exactly one thing

move-path-equivalence.pg.test.ts"UNPROVEN: in-transaction column capacity did NOT reject on EITHER path in this fixture". It left the cause open —

something further in (resolveColumnCapacity's limit resolution, or what countActiveInCapacitySlotAsync counts as an occupant — a task with no session/agent may not count) keeps the check from firing … This suite does not establish which.

— and predicted its own obsolescence ("if a future change makes this reject, that is the capacity gate coming alive"). Neither guess was right; it was the pool id. Updated to assert the divergence with the answer recorded — not weakened. Its fixture also had to start each phase from an empty wip column: once the gate binds, the inline phase's leftovers trip the cap on the holder move before the contended move under test runs.

schema-applier.test.ts failed only in the full-suite run and passes in isolation both with and without the fix — cross-file contamination, not mine.

Before / after — measured, both directions

maxConcurrent: 1, real PG store, real moveTask:

flagOFF / no selection flagOFF / selection flagON / no selection flagON / selection
before ADMITTED ADMITTED ADMITTED ← the bug REJECTED
after ADMITTED ADMITTED REJECTED REJECTED

The E2E acceptance row asserts held at cap 1 and admitted at cap 2 on the same fixture, so it cannot pass by simply never admitting anything. With the fix reverted that row fails; the admitted case still passes, as it should. The Phase A3 ratchet's two flipped assertions also fail with the fix reverted.

Ratchet flipped exactly as its author specified: DEFECT (R1) becomes a rejection, and it.fails on the invariant becomes a plain it.

⚠️ This is NOT user-visible yet — please read before merging

The premise this was approved on ("once it binds, cards that currently slip through will start being held") does not hold for this change alone. The whole capacity block sits inside if (useWorkflow && workflowIr && fromColumn !== toColumn), and useWorkflow is experimentalFeatures.workflowColumns === true — absent from DEFAULT_GLOBAL_SETTINGS, with no writer anywhere outside tests. That is Phase A3's R2, still live and now retitled DEFECT (R2, STILL LIVE) with the measured matrix recorded in it.

So on merge: nothing changes for any real project. Making it actually bind means also removing the useWorkflow condition — a materially larger, genuinely user-visible change that I have not made unilaterally. Escalated for a decision; if that lands, the changeset here should be re-categorised.

Review follow-up (48e79ff): the convention was still duplicated — swept and ratcheted

The first pass added the resolver and routed the transactional gate + counters, but hold-release still derived the pool independently. Swept the repo: six sites name the sentinel, five derive the convention and now call resolveCapacityPoolId (hold-release.ts:116/118/442/576, task-store-helpers.ts:290). The sixth, scheduler.ts:1558, names the default pool as a literal in a capacity diagnostic — no selection input, nothing to disagree with — so it keeps the constant.

Does this change hold-release behavior? No, and it was never releasing against the wrong pool. hold-release computed x ?? DEFAULT_WORKFLOW_POOL_ID, which is exactly what the counter buckets under; moves.ts (?? "builtin:coding") was the sole disagreeing site, and the first commit moved it into agreement with hold-release, not the reverse. resolveCapacityPoolId(x) is x ?? DEFAULT_WORKFLOW_POOL_ID, so every routed site computes an identical value for every input. No second user-visible change rides along with this PR — the only behavior delta remains the gate binding on the flag-ON path, which per R2 is still not the path production takes. Evidence: hold-release + capacity suites 43/43 identical before and after.

The resolver is now the only way to compute a pool id, not merely the newest way. scripts/check-capacity-pool-id.mjs fails on any inline ?? DEFAULT_WORKFLOW_POOL_ID outside workflow-capacity.ts, wired into both pretest and the blocking test:gate. A review note would not have sufficed: the original defect landed in a file that already imported the canonical constant. Verified both ways — clean run scans 1124 files and passes; reintroducing the old hold-release expression exits 1 and names the line.

Review follow-up (a5b6755): the ratchet was rebuilt because it would not have caught the bug

The first ratchet matched one spelling (?? DEFAULT_WORKFLOW_POOL_ID) and the real defect used another (?? "builtin:coding"). Verified: reintroducing the original defect and running the old checker exits 0. A guard that reports success without checking is worse than no guard — it stops anyone looking.

Rebuilt on the TypeScript AST with two rules. Rule 1 (sink): a value reaching a capacity counter's workflowId must come from resolveCapacityPoolId, or a local initialized from it — so it fires on the original defect regardless of which literal was used, on one line or twenty. Rule 2 (sentinel): no ?? onto the sentinel at any qualification depth or as its raw value; multiline is one AST node and caught by construction. ?? "builtin:coding" is deliberately not banned outright — it is the legitimate default for a workflow id in ~8 places, and is only a bug when it reaches a capacity pool.

Fails closed three ways that previously reported success without inspecting: unreadable file, unparseable file, and an empty file listing (the old script would have printed a green tick off a broken glob).

Acceptance was not "passes on main". Each form was reintroduced into the real source and confirmed to fail: the original defect in moves.ts, a multiline fallback, and a deeply qualified sentinel. All are pinned in capacity-pool-id-check.test.ts (12 cases: 7 must-catch starting with the reduced actual pre-fix moves.ts, 4 must-not-flag, 1 fail-closed) so the guard cannot silently narrow again.

Also added to pretest:full, which had omitted it.

Follow-up (0be8df6): a dead rule found by fixing a test title

Splitting the mislabelled fail-closed test surfaced more than a mislabel: ts.createSourceFile is error-tolerant and does not throw on malformed syntax, so the try/catch behind the unparseable rule was unreachable and that rule could never fire. The earlier "fails closed three ways" claim was overstated — the guard advertised a capability it did not have. Detection now reads sf.parseDiagnostics; a partial AST can silently lack the ?? nodes and sink calls the rules look for, so "did not parse" must not read as "inspected and clean". Mutation-verified: reverting the detection fails that case and only that case.

Test-file exclusion also moved to the repo's {test,spec}.{ts,tsx} guideline shape — a .spec.ts under packages/<pkg>/src/ was being scanned as production source. Verified both ways: the .spec.ts is skipped, and the identical content in a non-test file is still caught, so the exclusion is scoped rather than a hole.

Verification

  • engine + core tsc --noEmit clean
  • pnpm test:gate green (299 + 10 + 71)
  • E2E 20/20; capacity + move-path suites 14/14
  • full core PG: 1037 passed / 3 failed — all three reproduce with the fix stashed (pre-existing)
  • engine-default: 279 failed vs 280 at baseline with the fix stashed — pre-existing red lane, no regression
  • hold-release + capacity suites: 43/43 identical before and after the resolver routing
  • check-capacity-pool-id ratchet: 14/14 regression cases; clean over 1124 files; exits 1 on the original defect, a multiline fallback, and a deeply qualified sentinel reintroduced into real source

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed capacity-limit accounting when workflow selection is missing by consistently deriving the correct capacity pool id.
    • Made capacity enforcement align across move and hold/release paths, rejecting over-limit moves with capacity-exhausted.
  • Tests
    • Updated PostgreSQL and added an E2E scenario to verify the corrected in-transaction gating behavior at maxConcurrent limits of 1 and 2.
  • Chores
    • Added an automated guard to detect inconsistent capacity pool id fallback patterns in code.
  • Public API
    • Exposed resolveCapacityPoolId for consistent capacity pool id derivation.

… convention

`moves.ts` asked the counter for occupants of pool "builtin:coding" while
`countActiveInCapacitySlotAsync` buckets selection-less rows under
DEFAULT_WORKFLOW_POOL_ID. Nothing ever landed in the pool being asked about, so
the count came back 0 and a finite limit could never bind.

ROOT FIX, not a literal swap. A shared constant would not have prevented this —
`DEFAULT_WORKFLOW_ID` was ALREADY imported in moves.ts and the code still wrote a
literal. Both sides now call `resolveCapacityPoolId`, so "which pool does a
selection-less task belong to" has one answer and no call site is in a position
to disagree. The one variable that served two masters is split: a capacity POOL
key (bucketing sentinel) and a WORKFLOW id (telemetry, must stay a real id). The
emitted event payload is unchanged.

CHECKED, NOT ASSUMED — no other site carries the same bug. `scheduler.ts:2514`
and `:2536` do use `?? "builtin:coding"`, but as an IR RESOLUTION key
(resolveWorkflowIrById), where a real workflow id is required and the pool
sentinel would not resolve. Same literal, different concept, correctly used.

ONE TEST DEPENDED ON THE GATE BEING DEAD: move-path-equivalence's "UNPROVEN:
in-transaction column capacity did NOT reject on EITHER path". It left the cause
open ("`resolveColumnCapacity`'s limit resolution, or what the counter counts as
an occupant — this suite does not establish which") and predicted its own
obsolescence. Neither guess was right; it was the pool id. Updated to assert the
divergence with the answer recorded — not weakened.

Also flips the Phase A3 ratchet as its author specified: the R1 DEFECT
expectation becomes a rejection and `it.fails` on the invariant becomes a plain
`it`. R2 stays and is retitled STILL LIVE.

MEASURED, both directions (maxConcurrent=1, real PG, real moveTask):
  before  flagOFF/no-sel ADMIT  flagOFF/sel ADMIT  flagON/no-sel ADMIT   flagON/sel REJECT
  after   flagOFF/no-sel ADMIT  flagOFF/sel ADMIT  flagON/no-sel REJECT  flagON/sel REJECT

E2E acceptance row asserts held-at-cap AND admitted-at-cap+1 on the same fixture,
so it cannot pass by never admitting. With the fix reverted that row fails; the
admitted case still passes, as it should.

NOT USER-VISIBLE YET: the capacity block is still inside `if (useWorkflow && …)`
and `useWorkflow` reads `experimentalFeatures.workflowColumns`, absent from
defaults with no production writer. Escalated separately.

Verified: engine + core tsc clean; test:gate green (299+10+71); E2E 20/20; full
core PG 1037 passed / 3 failed, all three reproducing with the fix stashed;
engine-default 279 failed vs 280 at baseline (pre-existing red lane, no
regression).

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

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change centralizes capacity-pool ID resolution, updates move, counting, helper, and hold-release logic to use the sentinel for selection-less tasks, and adds regression, lifecycle, and source-validation coverage.

Changes

Capacity pool sentinel correction

Layer / File(s) Summary
Centralize capacity pool resolution
packages/core/src/workflow-capacity.ts, packages/core/src/index*.ts, packages/core/src/task-store/project-store-ops.ts, packages/core/src/task-store/task-store-helpers.ts
Adds and exports resolveCapacityPoolId, then uses it for active-capacity counting and effective workflow resolution.
Wire pool resolution into moves
packages/core/src/task-store/moves.ts, .changeset/capacity-pool-sentinel.md
Separates workflow identity from capacity-pool identity and passes the resolved pool ID to capacity counting.
Align hold-release capacity pooling
packages/engine/src/hold-release.ts
Uses the shared resolver when deriving pool values, filtering occupants, and releasing capacity-held tasks.
Validate and verify capacity pooling
scripts/lib/capacity-pool-id-check.mjs, scripts/check-capacity-pool-id.mjs, packages/engine/src/__tests__/capacity-pool-id-check.test.ts, package.json
Adds AST-based resolver enforcement, fail-closed source scanning, guard tests, and pretest integration.
Update capacity regression coverage
packages/core/src/__tests__/postgres/*capacity*.pg.test.ts, packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts, packages/engine/src/__tests__/workflow-lifecycle-live-e2e.pg.test.ts
Verifies sentinel-based rejection, preserves inline-path divergence expectations, and tests lifecycle capacity limits of 1 and 2.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: fixing in-transaction capacity gating by unifying pool-id handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/workflow-e2e-sweep-table

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR centralizes capacity-pool ID derivation and restores enforcement for selection-less tasks on the workflow-enabled path.

  • Adds resolveCapacityPoolId and routes transactional counting, hold-release, and store helper paths through it.
  • Separates capacity pool keys from real workflow IDs used by transition telemetry.
  • Adds an AST-based repository check and regression tests to prevent duplicated fallback logic.
  • Updates PostgreSQL and lifecycle tests to verify rejection at capacity and admission below capacity.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains, and the previously reported duplicated capacity-pool derivation has been routed through the canonical resolver across the relevant hold-release and store paths.

Important Files Changed

Filename Overview
packages/core/src/workflow-capacity.ts Adds the canonical capacity-pool resolver while preserving the distinct default workflow identity.
packages/core/src/task-store/moves.ts Uses the canonical pool key for transactional occupancy checks while retaining a real workflow ID for telemetry.
packages/engine/src/hold-release.ts Routes selection, fallback, counting, and release pool derivations through the shared resolver.
scripts/lib/capacity-pool-id-check.mjs Adds AST checks for underived capacity-counter inputs and duplicated sentinel fallback expressions.
packages/engine/src/tests/capacity-pool-id-check.test.ts Covers the original regression, alternate fallback syntax, valid workflow defaults, and fail-closed parsing behavior.
packages/core/src/tests/postgres/workflow-capacity-invariant.pg.test.ts Updates the capacity invariant to verify selection-less tasks are rejected on the workflow-enabled path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Selection[Task workflow selection] --> Resolver[resolveCapacityPoolId]
    Resolver --> MoveGate[Transactional move capacity gate]
    Resolver --> Counter[Active capacity counter]
    Resolver --> HoldRelease[Hold-release sweep]
    MoveGate --> Decision{Pool below capacity?}
    Counter --> Decision
    Decision -->|Yes| Admit[Admit transition]
    Decision -->|No| Reject[Reject with capacity-exhausted]
Loading

Reviews (4): Last reviewed commit: "fix(scripts): make the `unparseable` rul..." | Re-trigger Greptile

Comment thread packages/core/src/workflow-capacity.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts (1)

374-397: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Condense the FNXC test narratives to the current behavioral contract.

Keep only the observed capacity behavior and reason needed to maintain each assertion; remove historical investigation and operator-planning prose.

  • packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts#L374-L397: reduce this to the hooks rejection / inline ungated contract.
  • packages/core/src/__tests__/postgres/workflow-capacity-invariant.pg.test.ts#L115-L132: reduce this to the production-inline admission contract.

As per coding guidelines, “Add dated FNXC:Area-of-product comments …; keep them concise and current.” Based on learnings, test-source FNXC context should remain concise and behavioral.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts` around
lines 374 - 397, Condense the FNXC comments at
packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts lines
374-397 and
packages/core/src/__tests__/postgres/workflow-capacity-invariant.pg.test.ts
lines 115-132 to concise, current behavioral context: document that the hooks
path rejects at capacity while the inline path remains ungated, and separately
state the production-inline admission contract. Remove historical investigation,
root-cause narrative, and operator-planning discussion while preserving the
dated FNXC format.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts`:
- Around line 374-397: Condense the FNXC comments at
packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts lines
374-397 and
packages/core/src/__tests__/postgres/workflow-capacity-invariant.pg.test.ts
lines 115-132 to concise, current behavioral context: document that the hooks
path rejects at capacity while the inline path remains ungated, and separately
state the production-inline admission contract. Remove historical investigation,
root-cause narrative, and operator-planning discussion while preserving the
dated FNXC format.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 53e0be30-c446-48ce-b50f-0aca0010dabd

📥 Commits

Reviewing files that changed from the base of the PR and between b848a13 and c292255.

📒 Files selected for processing (7)
  • .changeset/capacity-pool-sentinel.md
  • packages/core/src/__tests__/postgres/move-path-equivalence.pg.test.ts
  • packages/core/src/__tests__/postgres/workflow-capacity-invariant.pg.test.ts
  • packages/core/src/task-store/moves.ts
  • packages/core/src/task-store/project-store-ops.ts
  • packages/core/src/workflow-capacity.ts
  • packages/engine/src/__tests__/workflow-lifecycle-live-e2e.pg.test.ts

…solver, and ratchet it

PR #2488 review: the resolver was added and the transactional gate + counters
used it, but the HOLD-RELEASE capacity path still applied
`workflowId ?? DEFAULT_WORKFLOW_POOL_ID` independently — so the convention was
still duplicated across enforcement surfaces and could drift again at the other
site. Correct, and the same failure mode the fix was meant to close.

Swept the whole repo. SIX sites named the sentinel; FIVE derived the convention
and now call `resolveCapacityPoolId`:
  - hold-release.ts:116  selection fallback
  - hold-release.ts:118  catch fallback (an unreadable selection is
                         indistinguishable from none for pooling)
  - hold-release.ts:442  map-miss fallback in the occupant counter
  - hold-release.ts:576  map-miss fallback at the release decision
  - task-store-helpers.ts:290  resolveEffectiveWorkflowIdSyncImpl

The sixth, scheduler.ts:1558, is NOT a derivation: it names the default pool as a
literal field in a capacity DIAGNOSTIC, with no selection input and therefore no
way to disagree with anything. Left as the constant.

NO BEHAVIOR CHANGE, and specifically NOT a second user-visible change.
hold-release was already computing `?? DEFAULT_WORKFLOW_POOL_ID` — the SAME value
the counter buckets under — so it was never releasing against the wrong pool. The
gate in moves.ts was the sole disagreeing site (`?? "builtin:coding"`), and the
previous commit moved IT into agreement with hold-release, not the reverse.
`resolveCapacityPoolId(x)` IS `x ?? DEFAULT_WORKFLOW_POOL_ID`, so every routed
call site computes an identical value for every input.

RATCHET so the resolver is the ONLY way to compute a pool id, not merely the
newest way: scripts/check-capacity-pool-id.mjs fails on any inline
`?? DEFAULT_WORKFLOW_POOL_ID` outside workflow-capacity.ts, wired into BOTH
pretest and the blocking test:gate. A review note would not have been enough —
the original defect landed in a file that already imported the canonical
constant, so availability of the right thing demonstrably does not prevent the
wrong thing. Verified in both directions: passes clean (1124 files), and
reintroducing the old hold-release expression makes it exit 1.

Verified: hold-release + capacity suites 43/43 IDENTICAL before and after the
routing; test:gate green (299+10+71) with the new check running in it; core +
engine tsc clean; full core PG 1037 passed / 3 failed, the same three that
reproduce with the fix stashed.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Around line 17-18: Update the pretest:full script to include
check-capacity-pool-id.mjs in its chained checker commands, matching the
existing pretest and test:gate coverage while preserving the current command
order and behavior.

In `@scripts/check-capacity-pool-id.mjs`:
- Around line 27-28: Update the BANNED check in the capacity-pool validation
script to enforce resolver-only pool derivation rather than matching only
same-line DEFAULT_WORKFLOW_POOL_ID expressions. Use AST/source-aware analysis,
or equivalent detection, that rejects literal fallbacks such as
"builtin:coding", multiline nullish fallbacks, and arbitrarily deep qualified
DEFAULT_WORKFLOW_POOL_ID references; add regression coverage for each form.
- Around line 42-47: Update the read failure handling in the file-scanning loop
around readFileSync so an unreadable tracked file records a violation or causes
the check to exit nonzero. Remove the silent continue behavior, while preserving
normal processing for files that are read successfully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f0cf9af-392c-4e55-b684-07665652a05a

📥 Commits

Reviewing files that changed from the base of the PR and between c292255 and 48e79ff.

📒 Files selected for processing (6)
  • package.json
  • packages/core/src/index.gate.ts
  • packages/core/src/index.ts
  • packages/core/src/task-store/task-store-helpers.ts
  • packages/engine/src/hold-release.ts
  • scripts/check-capacity-pool-id.mjs

Comment thread package.json Outdated
Comment thread scripts/check-capacity-pool-id.mjs Outdated
Comment thread scripts/check-capacity-pool-id.mjs Outdated
… regex missed the defect it guards

PR #2488 review, and the finding is the serious kind: the ratchet I added would
have passed green on the exact bug it exists to prevent. Verified, not assumed —
reintroducing the original `?? "builtin:coding"` into moves.ts and running the
old checker exits 0. It matched one spelling of the fallback
(`?? DEFAULT_WORKFLOW_POOL_ID`) and the real defect used another. A guard that
reports success without checking is worse than no guard, because it stops anyone
looking.

Rebuilt on the TypeScript AST around two complementary rules:

  RULE 1 (SINK) — the one that catches the original defect. A value reaching a
  capacity counter's `workflowId` must come from `resolveCapacityPoolId`, or a
  local initialized from it. The defect passed a local derived from a `??`
  literal, so this fires regardless of WHICH literal, on one line or twenty. The
  banned thing is not a spelling; it is an underived value reaching the counter.

  RULE 2 (SENTINEL) — no `??` onto the pool sentinel, at any qualification depth
  (`A.B.C.DEFAULT_WORKFLOW_POOL_ID`) or as its raw value `"__default-workflow__"`.
  Being AST-based, a fallback split across lines is one node and is caught.

`?? "builtin:coding"` is deliberately NOT banned outright: it is the legitimate
default for a WORKFLOW id in ~8 places (scheduler's IR-resolution key, task
creation, analytics). It is only a bug when it reaches a capacity pool, which is
what Rule 1 encodes. Banning the spelling would be suppressed until it rotted.

FAILS CLOSED, three ways that previously reported success without inspecting:
  - an unreadable file is a violation, not a skip (was a silent `continue`);
  - an unparseable file is a violation;
  - an empty file listing is a violation rather than "0 files, all clean".

Also wired into `pretest:full`, which omitted it — so the claim that both pretest
chains run it is now true rather than merely stated.

REGRESSION SUITE (capacity-pool-id-check.test.ts, 12 cases) pins every form the
guard must catch — starting with the reduced ACTUAL pre-fix moves.ts — plus the
negatives it must not flag, so the pattern cannot silently narrow again. The
acceptance test is not "passes on main": each form was also reintroduced into the
REAL source and confirmed to fail (original defect, multiline fallback, deeply
qualified sentinel).

Verified: 12/12 regression cases; checker clean over 1124 files and exit 1 on
each reintroduced form; test:gate green (299+10+71) with the check inside it;
core + engine tsc clean.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/engine/src/__tests__/capacity-pool-id-check.test.ts`:
- Around line 130-144: Correct the test description in the
“check-capacity-pool-id ratchet — it fails CLOSED” suite to describe the
simulated read failure as unreadable, matching the existing “unreadable”
assertion. Add a separate regression test that supplies malformed TypeScript to
findViolations and verifies the unparseable rule from the ts.createSourceFile
parsing failure.

In `@scripts/check-capacity-pool-id.mjs`:
- Around line 18-25: Update the file discovery command in the capacity-pool scan
to include both .ts and .tsx sources, and update its exclusion filter to omit
recognized .test.ts, .test.tsx, .spec.ts, and .spec.tsx files in addition to
__tests__ paths. Keep scanning the existing package source directories and
preserve the non-empty path filtering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 093638a0-8ab0-4954-b3c8-35a7017949f4

📥 Commits

Reviewing files that changed from the base of the PR and between 48e79ff and a5b6755.

📒 Files selected for processing (4)
  • package.json
  • packages/engine/src/__tests__/capacity-pool-id-check.test.ts
  • scripts/check-capacity-pool-id.mjs
  • scripts/lib/capacity-pool-id-check.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment thread packages/engine/src/__tests__/capacity-pool-id-check.test.ts
Comment thread scripts/check-capacity-pool-id.mjs Outdated
…lude .spec.ts too

PR #2488 review, two threads. The first one turned out to be more than a test-title
fix.

1) THE MISLABELLED TEST WAS HIDING A DEAD RULE. The case titled "unparseable"
   simulated a read() throw and asserted "unreadable" — it never reached the parse
   path. Splitting it as asked surfaced why that mattered:
   `ts.createSourceFile` is error-TOLERANT and does NOT throw on malformed syntax
   (verified: `function )( {` returns a source file with 4 parseDiagnostics), so
   the try/catch it was meant to cover was UNREACHABLE and the `unparseable` rule
   could never fire. My "fails closed three ways" claim was therefore overstated —
   the guard advertised a capability it did not have, which is this PR's own
   failure mode in miniature.

   Detection now reads `sf.parseDiagnostics`. A file that did not parse yields a
   partial AST whose `??` nodes and sink calls may simply be absent, so reporting
   it clean would report "not inspected" as "inspected".

   The suite now has three fail-closed cases, each exercising the path it names:
   unreadable, genuinely-malformed-source, and a negative that well-formed source
   is not flagged (a diagnostics check that fired on valid syntax would be noise,
   and noise gets suppressed). Mutation-verified: reverting the parseDiagnostics
   detection fails the new case and only that case.

2) TEST-FILE EXCLUSION now uses the repo's guideline shape (the
   `{test,spec}.{ts,tsx}` family) instead of a `.test.ts`-only suffix. A `.spec.ts`
   directly under `packages/<pkg>/src/` was being scanned as production source and
   would have flagged its fixtures as real violations. Verified BOTH directions:
   a `.spec.ts` carrying a violation is skipped, and the identical content in a
   non-test file is still caught — so the exclusion is scoped, not a hole.

Verified: 14/14 regression cases; checker clean over 1124 files; test:gate green
(299+10+71); core + engine tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gsxdsm
gsxdsm merged commit 7871b28 into main Jul 28, 2026
7 checks passed
@gsxdsm
gsxdsm deleted the feature/workflow-e2e-sweep-table branch July 28, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant