Gate the workspace manifest dependency graph against cycles - #14524
Merged
Conversation
Adds check:workspace-manifest-cycles, a new check family that walks every workspace: edge in dependencies, devDependencies, peerDependencies and optionalDependencies across all 78 workspace packages and refuses on any cycle, naming each edge with its declaration class. peerDependencies is walked deliberately: a scan omitting it reports 0 cycles on the pre-fix #13513 tree, honestly and wrongly, since that cycle's first edge was a peer edge. Wired into lint.yml next to check:turbo-task-graph, the repo's existing collector idiom for this kind of manifest sweep. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…rkspace-cycle-gate
This was referenced Sep 2, 2026
Closed
… COMPOUND_ANCHOR_LEDGER The self-test declaration this PR adds in scripts/check-workspace-manifest-cycles.mjs (runSelfTest) is a compound name the anchor matches, and the pm-dispatch-gates census requires every such name be classified. It is a genuine self-test battery, so accidental: false, placed in file-path order among the other scripts/check-*.mjs accidental:false rows. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…rkspace-cycle-gate
baozhoutao
marked this pull request as ready for review
September 2, 2026 11:33
baozhoutao
enabled auto-merge
September 2, 2026 11:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14195
What
Adds
check:workspace-manifest-cycles— a new gate family that walks everyworkspace:edge independencies,devDependencies,peerDependenciesandoptionalDependenciesacross all 78 workspace packages and refuses on anycycle, naming each edge with its declaration class
(
A --devDependencies--> B). Before this, nothing in the repo read themanifest graph for cycles — the only signal was
pnpm install'sWARN There are cyclic workspace dependencies: …on stderr, exit 0, which hadbeen printing for the entire life of the #13513 defect before anyone read it.
peerDependenciesis walked deliberately, not as an afterthought: a scanomitting it reports 0 cycles on the pre-fix #13513 tree, honestly and
wrongly — that cycle's first edge was a
peerDependenciesedge (#13513comment
5473800149, resolved in5479206575). The--self-testbatterydrives that exact adversarial shape (a cycle whose ONLY closing edge is
peerDependencies, and the same foroptionalDependencies) so a future editthat narrows the walked classes back to three reddens the self-test.
Wired into
lint.ymlimmediately aftercheck:turbo-task-graph, the repo'sexisting collector idiom for this kind of manifest sweep.
File surface (exactly three files, per the claim)
scripts/check-workspace-manifest-cycles.mjs— the gate. Resolves edgesby package NAME (never directory) via the shared
scripts/workspace-enumerator.mjs. Aworkspace:spec naming a package nomember declares is recorded as an informational note, never a refusal (a
separate defect this gate does not judge). A self-edge counts as a cycle of
length 1. Cycle detection is Tarjan SCCs + a deterministic DFS walk per
cyclic component, so every disjoint cycle is reported, not just the first
one found. Exit 0 green, 1 cycle found, 2 usage/read error.
package.json— one newscriptskey,"check:workspace-manifest-cycles": "node scripts/check-workspace-manifest-cycles.mjs --self-test && node scripts/check-workspace-manifest-cycles.mjs",the
check:turbo-task-graphidiom..github/workflows/lint.yml— one new step in the same job, immediatelyafter
check:turbo-task-graph, with a header comment stating what itjudges, why, and that
--self-testruns first.No other file touched: no existing
scripts/check-*.mjsedited, nothingunder
scripts/pm/**(held by #14233 in flight), no workspacepackage.json,turbo.jsonorpnpm-workspace.yamledits.Proof on the real tree
Green on
origin/mainbefore this change (nothing reads the graph forcycles):
Green with the new gate:
Red-proof (temporarily added
packages/spec'speerDependenciesaworkspace:*edge back ontoapps/docs, which already has adevDependenciesedge ontopackages/spec— the two form a 2-cycle whoseonly closing edge is a peer, the #13513 shape):
Reverted with
git checkout HEAD -- packages/spec/package.json, confirmedclean with
git diff HEAD(0 lines) before committing.Self-test:
node scripts/check-workspace-manifest-cycles.mjs --self-test—acyclic green; a 2-cycle via
devDependenciesred with both edges named;the peer-only and optional-only false-zero controls both red; a 3-node cycle
red with all three edges in cycle order; a self-edge as a length-1 cycle; two
disjoint cycles both reported; a non-
workspace:spec is not an edge; allfour
workspace:*/^/~/<range>spellings count; an unresolvedworkspace:spec is recorded, not a refusal; both read-error refusals fire.All cases pass, on the base commit and again on the merged tree.
Gates
Derivation:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands scripts/check-workspace-manifest-cycles.mjs package.json .github/workflows/lint.yml→ 37 commands. All ran green except three that need a full
pnpm buildfirst(
check-test-completeness.mjs,check:dual-build-cjs-loads,check:type-check-debt— all exit 3, "Runpnpm buildfirst ... nothing wasmeasured", unrelated to this diff's surface).
check:pm-dispatch-gatesrandetached (~11 min):
dispatch-gates self-test: 1240 cases pass.check:declared-population-live --listshows the new family LIVE:live check:workspace-manifest-cycles -- pnpm-workspace.yaml/**, packages/**/package.json, apps/**/package.json, examples/**/package.json.node scripts/pm/bare-root-worklist.mjs --self-test—OK self-test: 57 live row(s), 49 unreachable as spelled, 49 recorded verdict(s) — none stale, none missing, none contradicted...— the new gate entered no unjudged row.node scripts/check-nul-bytes.mjs— clean. Re-ran the full union aftergit fetch origin main && git merge origin/main(clean, no conflicts) on85e8fc3f5— same result.Changeset
None — scripts + workflow + a root
scriptskey only, no package publishesanything here.
skip-changesetlabel applied on open, confirmed by readingthe PR's labels back.
Generated by Claude Code