Skip to content

fix(tooling): resolve the changeset LEVEL axis package from its manifest, so the 51 nested packages are candidates it can refuse - #16969

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-16713-nested-package-level-axis
Sep 8, 2026
Merged

fix(tooling): resolve the changeset LEVEL axis package from its manifest, so the 51 nested packages are candidates it can refuse#16969
baozhoutao merged 2 commits into
mainfrom
claude/issue-16713-nested-package-level-axis

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16713

Clause-②: no — scripts/** only. No published surface moves: the files[] union across every workspace manifest is dist, README.md, CHANGELOG.md, api-surface, json-schema, liveness, llms.txt, prompts, spec-changes.json, src/**/*.zod.ts — all package-relative, none of which can reach a repo-root scripts/ file — and zero package/app/example sources import this gate. Hence skip-changeset, not a missing changeset.

What moved

scripts/check-changeset-no-major.mjs, one file, +294 / -36.

The LEVEL axis decided which package a diff "grew" through a path pattern whose package half was exactly one path segment wide. This workspace is not flat: 51 of its 74 packages sit at packages/GROUP/NAME/, so the segment after packages/ is the GROUP and no /src/ follows it. For all 51 the axis recorded no package, and a needs:contract-review carrier paired with a patch printed a tick.

That tick was not a weaker verdict — it was no verdict, rendered identically to a pass. And it escaped the one instrument built to catch exactly this: unreadable exists so a package the reading cannot NAME is never mistaken for a package the diff did not TOUCH (#4690), and it stayed empty here, because a nested package was never a candidate. A gate cannot report a limb it never grew.

The reading is now the manifest set instead of a depth:

  • publishedSourceOwners(path) enumerates, shape only and at any depth, every ancestor D under packages/ for which the path reads D/src/**, ordered shallowest first.
  • packagesTouched walks that list and takes the first candidate whose package.json is readable in the HEAD tree, caching one manifest read per directory rather than per changed file.
  • A path that matches the shape and has no nameable candidate lands in unreadable. That is the new failure mode the widening owes, and it is asserted as an invariant: a matching path lands in packages or in unreadable, never in neither.

bin/** is deliberately still unread. WHICH roots ship is #16692's axis, and a control in the new battery pins it as unread so this change cannot silently answer a different card.

Why shallowest-first, and why not option B

Both were settled by measurement, not preference.

Shallowest-first — this repo really contains a two-candidate path. packages/create-objectstack/src/templates/blank/src/objects/note.object.ts is D/src/** for BOTH packages/create-objectstack and the scaffold template dir under it, which carries its own manifest (objectstack-blank, private: true, template CONTENT that create-objectstack ships, not a workspace member). Resolving to the NEAREST manifest would name the private template and drop the real package — a regression against the one-segment reading being replaced. Shallowest-first returns the old answer on every path the old pattern matched.

Not option B (one optional group segment). Ablation below shows option B satisfies every nested-package assertion — it looks repaired — and fails only the three-levels-deep case and the two-candidate ordering. That is the recurrence the triage seat predicted, made mechanical instead of prose.

Measured over the whole tree

dirs matched lost gained
before 22
after 72 0 50

Population: 23 flat + 51 nested = 74 workspace packages, counted by path depth over git ls-tree -r origin/main -- packages/ on package.json rows. A naive packages/**/package.json count gives 75; the extra is packages/create-objectstack/src/templates/blank/package.json, the scaffold template above — private: true, not a workspace member. The 74-package count is the one used here.

Before/after pair, with both controls

Driven through the real exported scan / packagesTouched / judgeLevel / renderLevel of BOTH file versions (the pre-change blob at 07c56b5388 and this branch's HEAD) over identical temp git repositories. Every exit code captured before any pipe.

leg BEFORE AFTER
A nested packages/drivers/driver-sql/src/**, patch, declared yes exit 0 clean, touched {packages:[],unreadable:[]} exit 1 enforce, touched {packages:["@objectstack/driver-sql"]}
B flat control packages/cli/src/**, patch, declared yes exit 1 enforce exit 1 enforce
C nonsense control, content/docs/** only, patch, declared yes exit 0 clean exit 0 clean
D level control, same nested diff graded minor exit 0 clean, touched [] exit 0 clean, touched ["@objectstack/driver-sql"]
E new failure mode, nested dir with no manifest exit 0, in neither set exit 0, unreadable:["packages/newgroup/newpkg"]

Leg B is the load-bearing one: the nested leg turning red proves nothing unless the flat leg still reds in the same harness. Leg D separates "the axis was widened" from "the gate now refuses everything" — the package IS newly seen and the verdict still passes, one word apart. Leg C would be the only failure of an implementation that simply always enforced.

Blast radius, measured BEFORE the shape was chosen

Window: the 150 most recently merged PRs, each replayed at its merge commit against that commit's parent, driving both file versions' own judgeLevel. 150 measured, 0 skipped.

7 verdicts move exit 0 → exit 1. 0 move 1 → 0. 0 unreadable entries anywhere in the window.

PR before → after declaration packages named
#16650 cleanenforce yes, a durable body line @objectstack/driver-sql, @objectstack/driver-turso
#16759 not-measured-mootnot-measured-material none readable @objectstack/service-automation
#16730 not-measured-mootnot-measured-material none readable @objectstack/plugin-auth
#16739 not-measured-mootnot-measured-material none readable @objectstack/plugin-approvals
#16722 not-measured-mootnot-measured-material none readable @objectstack/plugin-security
#16700 not-measured-mootnot-measured-material none readable @objectstack/plugin-webhooks
#16680 not-measured-mootnot-measured-material none readable @objectstack/hono

Read the seven as two different facts:

No tolerance, no allowlist, no grandfathering is proposed, and none is in the diff. The number is a reading for the maintainer and the routing is the PM's.

⚠️ One honest limit on the window: the needs:contract-review carrier is stripped at review pass, so the labels read today understate how many of those PRs declared yes at the time. The bias runs toward the not-measured-* lanes rather than toward enforce; #16650's reading is unaffected because its declaration is a body line.

Ablation — the new battery can actually fail

Both legs: commit first, mutate, prove the mutation reached disk (injected-marker count and removed-text count, plus a git hash-object differing from the HEAD blob), run, restore via git checkout HEAD -- PATH, prove restoration by git diff HEAD being empty and the blob hash matching HEAD again. Both restored byte-identical to 3b0273c4e035fd07087f93379d89408a1bf35eae.

  1. Restore the pre-change one-segment reading → self-test exit 1, 11 failures, every one of them in the new battery, including the nested end-to-end leg reporting got clean. That is the card's finding reproduced inside the self-test. All other batteries stayed green.
  2. Implement option B (one optional group segment) → self-test exit 1, exactly 2 failures: the three-levels-deep assertion and the shallowest-first ordering. Option B passes every other new assertion, which is the point — it is the repair that would look done.

Batteries

battery before after
The LEVEL axis: #16044's two heads, one word apart (#16055) 56 56
The GRAIN: a PR-scoped declaration judged at PR scope (#16361) 25 25
The wiring: these fixtures must actually run on every PR 22 22
THE DEPTH: a nested package is a candidate the axis can refuse (#16713) 21 (new)
roster size / SELF_TEST_BATTERY_FLOOR 15 / 15 16 / 16
total assertions 204 225

No existing battery was disturbed. The roster floor moves 15 → 16 because the file's own invariant requires it: "DELETING an entry silences that battery's floor exactly as effectively as zeroing it, so the roster's own size is pinned too." Leaving it at 15 beside 16 declared batteries would let any one of them be deleted without redding.

Gates

All exit codes captured before any pipe.

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 33 commands; all 33 run, all exit 0. Reconciled with --ran: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.
  • node scripts/check-changeset-no-major.mjs --self-test → exit 0, 225 assertions (204 on the branch point).
  • pnpm lint (the whole repo, eslint . --no-inline-config) → exit 0. Run in full, so no narrowing claim is needed.
  • The 7 artifact-roster families the derivation flagged as keeping their roster under scripts/ were run rather than read as cleared: 5 exit 0; check:dts-closure and check:published-readme-exports exit 3 = PREREQUISITE NOT MET (no package has a dist/; they read a closure build). NOT MEASURED, not a finding, and declared to CI — this diff moves no package, so there is no affected-package closure to build for them.
  • Control-character self-scan over the changed file: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' → no matches (pnpm check:nul-bytes also exit 0).

Gate readings quoted from the gates' own verdict lines, at final commit a6c3fb8c5b.

验收备注

The triage seat's acceptance criteria, quoted verbatim and answered one by one (the ruling is left in its original language deliberately — rewriting a quoted ruling rewrites the ruling):

  1. 取选项 A(从工作区 manifest 集合解析被碰包,而不是手写路径正则)。⛔ 不取 B

    Taken. The package is resolved by locating its manifest in the HEAD tree, at any depth; what remains shape-based is only D/src/**, which is the definition of published source and [finding] judgeLevel counts a package as "grown" only through packages/*/src/**, so a shipped bin/ target is a published surface the changeset level axis cannot see #16692's axis, not this card's. Option B was implemented as an ablation and reds against the new battery, so B is now mechanically excluded rather than argued against.

  2. 两腿对照都必须在自测里 ... 扁平腿(packages/cli/src/**)继续 REFUSED —— 现有 :2028 断言保留,⛔ 不得改写;嵌套腿 ... REFUSED

    Both are in the self-test. The pre-existing flat assertion is untouched (its battery still registers 56), and the flat leg is additionally re-driven inside the new battery so a nested red is readable as a widening rather than as a gate that stopped discriminating.

  3. 阴性对照必测:一个碰任何已发布包源码的 diff ... 必须仍然通过

    Pinned as leg C, plus leg D (the same nested diff graded minor stays green while the package IS seen).

  4. judgeLevel 的其他消费者是否共用这条正则。修 PR 里给出一次读数

    Reading: not shared. git grep -n 'PUBLISHED_SOURCE_PATH|packagesTouched|judgeLevel' over the whole tree returns hits in scripts/check-changeset-no-major.mjs and nowhere else. The workflows (pr-automation.yml, cut-rc.yml, release.yml) invoke the script as a command; no file imports these symbols.

  5. 收口后在 [finding] judgeLevel counts a package as "grown" only through packages/*/src/**, so a shipped bin/ target is a published surface the changeset level axis cannot see #16692 上回帖说明 DEPTH 轴已并入,或反之 —— 两张卡的最终状态里 ⛔ 不得有一张停在"以为对方修了"

    Not done by this seat, and this is the half left open — hence Part of rather than a closing keyword. The DEPTH axis is landed here; the ROOT axis ([finding] judgeLevel counts a package as "grown" only through packages/*/src/**, so a shipped bin/ target is a published surface the changeset level axis cannot see #16692) is untouched by design and is pinned as untouched (packages/cli/bin/os.mjs owns nothing). This PR makes no claim on [finding] judgeLevel counts a package as "grown" only through packages/*/src/**, so a shipped bin/ target is a published surface the changeset level axis cannot see #16692 and does not alter its state. Routing that cross-post, and routing the seven-PR blast-radius reading above, are the PM's.

Out-of-scope observations — noted, not filed

  • packages/spec/liveness/README.md already records the same class of defect in a different tool ("A pathspec glob packages/*/src never matched the nested …"), and scripts/check-duration-unit-keys.ts carries a 'packages/*/src/**' scan surface of its own. Whether that gate is blind the same way is not measured here and is outside this card. Carrier: any future audit of one-segment packages/* globs across the gate farm; there is no PR in flight that touches those files.
  • The gate's own author-facing prose was rewritten from packages/*/src/** to packages/**/src/** in the same file, because after this change the old glob understates the axis's reach and would tell a nested-package author their package is not judged. One historical comment that quotes a since-removed message is left at the old spelling on purpose.

Generated by Claude Code

…t any depth

wip

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…ure mode

wip

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT in substance — PR #16969, reviewed against the tree, ⛔ not against the report

Flip + arm withheld until the six running legs report. Both open questions are answered below.

What I re-drove on the head

probe want got
publishedSourceOwners defined 1 1
the one-segment regex /^packages\/([^/]+)\/src\// is gone 0 0
staleness control — that regex was on origin/main 1 1
new battery THE DEPTH … (#16713) registered at 21 1 1
SELF_TEST_BATTERY_FLOOR = 16 1 1 · old 15 gone: 0
firing control — LEVEL(#16055) battery still 56 56 56
firing control — GRAIN(#16361) battery still 25 25 25
firing control — not-measured-material / discharged still present >0 8 / 5
nonsense control 0 0

⚠️ Two of my probe patterns produced false zeros and the tree was right both times — the roster key is double-quoted, not single-quoted, and unreadable is a Set (unreadable.add, not .push). ⇒ Re-read with corrected patterns and both came back correct. Recording it because it is the same trap this seat has now hit seven times in a shift: an unvalidated pattern's zero is not a reading.

⭐ Three things that make this the right fix rather than a wider regex

1. Shallowest-first is load-bearing, and the reason is the trap my own brief warned about — from the other side. packages/create-objectstack/src/templates/blank/src/objects/note.object.ts is D/src/** for both create-objectstack and the scaffold template dir that carries its own private: true manifest. Nearest-manifest resolution would name the private template and DROP the real package — a regression against the very reading being replaced. I flagged that template as a counting hazard; it turned out to be an algorithm hazard. ⭐ Good catch, and it is asserted, not just commented.

2. The new failure mode the widening owes is closed and pinned as an invariant, verbatim from the self-test:

'the invariant the widening owes: a path that MATCHES the shape lands in exactly one of the two sets, never in neither — landing in neither is the …'

⇒ A nested dir with no readable manifest is now reported in unreadable rather than vanishing. That is #4690's distinction — "a name it could not read must not look like a package the diff did not touch" — extended to the case the card showed it did not cover.

3. ⛔ #16692 cannot be silently closed, and that is mechanical. The battery carries a control asserting bin/** is still NOT read:

'control: \bin/**` is still NOT read — WHICH roots ship is #16692's axis and this card must not silently close it; a fix that reddened here would be …'`

⭐ And the second ablation is the one I most wanted: implementing option B (one optional group segment) — the repair that looks done — fails exactly 2 assertions (three-levels-deep, and shallowest-first) while passing every other new one. ⇒ the near-miss is mechanically excluded rather than argued against.

The pair, and the two extra legs

Leg A nested driver-sql patch + yes: exit 0 clean → exit 1 enforce. Leg B flat control packages/cli/src/**: exit 1 both sides — ⭐ the nested red is only readable because the flat leg still reds in the same harness. Leg C nonsense (content/docs/** only): 0 both sides, ruling out an always-enforce implementation. Leg D level control — the same nested diff graded minor stays green while touched goes [] → ['@objectstack/driver-sql']: ⭐ the package is newly seen and still passes, one word apart. Leg E: the no-manifest case, neither-set → unreadable.

Whole-tree effect: 22 package dirs matched before, 72 after, 0 lost, 50 gained.

⛔ Open question 1 — blast radius. Answer: A and B. ⛔ Explicitly not C.

7 of 150 replayed merges flip 0 → 1, 0 flip the other way. Six are not-measured-moot → not-measured-material with no readable declaration; the gate's own printed remedy for those is one body line, and it clears with no push (which #16776 made true this morning).

The seventh is the real one, and I verified it myself rather than taking it:

PR #16650, merged 2026-09-07T21:13:32Z — body carries a line-start Clause-②: yes (1 match; a durable line, ⛔ not a strippable carrier label), changeset .changeset/driver-raw-statement-declared-fault.md grades "@objectstack/driver-sql": patch and "@objectstack/driver-turso": patch, and the diff moves packages/drivers/driver-sql/src/** and packages/drivers/driver-turso/src/**both nested.

⇒ Declared yes, graded patch on packages it grew, merged green solely because the axis could not see them. ⭐ That is this gate's own rule, caught on a merged PR. Filing it as its own finding — ⛔ it is a maintainer-ruled level question on an already-released bump and ⛔ not mine to adjudicate.

And I ran option B's sweep myself, over the open board: all 23 open PRs (control: 23 of 23 returned a readable file list), cross-referencing every workspace manifest's directory against each PR's moved <dir>/src/** paths and its +"pkg": patch changeset lines, against a line-start Clause-②: yes or the needs:contract-review carrier.

Result: ZERO open PRs carry the shape this widening will newly refuse.

⚠️ One flagged and then cleared on inspection: PR #16883 matched my predicate, but its two packages (@objectstack/cli, @objectstack/platform-objects) are flat — the gate already sees them, and that PR is already red on Check Changeset (failure at 13:48:43Z and 14:08:43Z). ⇒ an author already meeting an old red, ⛔ not one about to meet a new one. Reporting it as a casualty of this change would have been wrong.

Nobody on the open board is surprised by this landing. That is the answer B asked for, and it is a measurement.

⛔ Open question 2 — who posts on #16692. Answer: A, this seat posts it.

Correct reading of the brief: #16692 was read-only for you, its ROOT axis is untouched and pinned as untouched, so this does not close it. ⇒ Acting on a card that is not yours would have been the wrong move; I will cross-post there that the DEPTH axis landed and the ROOT axis is still open.

The prose rewrite, accepted

Nine occurrences of packages/*/src/** in author-facing text now read packages/**/src/**. ⭐ Right call and correctly not filed separately: after this change the old spelling understates the axis's reach and would tell a nested-package author their package is not judged — that is part of this change, not a neighbour. One historical comment quoting a since-removed message is deliberately left at the old spelling.

Two gates NOT MEASURED, declared

check:dts-closure and check:published-readme-exports both exited 3 PREREQUISITE NOT MET (no dist/) — ⛔ not failures, ⛔ not passes. 33 of 33 derived families run, 0 UNRUN, and pnpm lint was run whole-repo so no narrowing claim is owed.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 21:31
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit c9a3deb Sep 8, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-16713-nested-package-level-axis branch September 8, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants