fix(pm): stop T1 reading a typed parameter, and a re-spelled closed set, as a new key - #17760
Merged
Merged
Conversation
…et, as a new key `check-widening-tells`' T1 tell fires on any added line shaped `identifier: z.Something` on the contract source surface. Two live pairs raised it against diffs that add no key at all: - PR #17616 — `ctx: z.RefinementCtx`, the second parameter of an exported object-level refinement. That signature is the repo's prescribed one, so every diff adding a cross-field refusal raised a widening tell for the refusal itself. - PR #17638 — `strategy: z.enum(['eager', 'lazy'], {`, an in-shape key the same block removed with one member MORE. Exit 4 against a correct `Clause-②: no`, holding a reviewed retirement PR out of the queue. The second was the net-delta budget being EARNED and refused at the spend: "a line that DECLARES a closed set is never spent" was written about an opener, which `memberTellKind` already answers `null` for, so the only lines it caught were keys whose value opens a closed set. It is replaced by the thing it protected — an inline set widened in place is visible on the T1 row alone — so such a key spends only on three facts the block carries: a removed line naming the same key, both member lists readable inline, and the added list a subset of the removed one. The parameter half reads which bracket is innermost over the line's own hunk; a shape body is brace-delimited by construction, and every state the reader cannot carry honestly answers "keep firing". Measured over the 233 commits touching these surfaces (`e9efc403`): of 20,193 rows the previous reading raises, 23 decline and 20,170 stand — all 23 T1, fifteen parameters and eight existing keys re-spelled around an identical enum. Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK Co-authored-by: Claude <noreply@anthropic.com>
os-sales
marked this pull request as ready for review
September 12, 2026 03:09
This was referenced Sep 12, 2026
This was referenced Sep 12, 2026
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 #17618
What was wrong
check-widening-tells' T1 tell reads an added line shapedidentifier: z.Somethingon the contract source surface as "a new key on a Zod object schema — the accept set gains a spelling an author may now write". Two live pairs raised it against diffs that spell nothing new. Both were reproduced from the PRs' own pushed bytes before anything was written.Instance 1 — PR #17616 (
packages/spec/src/ui/dashboard.zod.ts:470). The row fires onctx: z.RefinementCtx,, the second parameter of an exported object-level refinement.z.RefinementCtxis a type; nothing constructs a shape there, and the diff it appears on refuses metadata that parses today. That signature is this repo's own prescribed one (the#16489convention —checkListViewPageMount,checkPageSourceCompleteness,checkGlobalFilterDateDefaultValue), so every diff adding a cross-field refusal raised a widening tell for the refusal itself.Instance 2 — PR #17638 (
packages/spec/src/system/cache.zod.ts:197). The row fires onstrategy: z.enum(['eager', 'lazy'], {, an in-shape key the same change block removed asstrategy: z.enum(['eager', 'lazy', 'scheduled']).default('lazy'). Same key, one member fewer. That pair declaresClause-②: no, so it exited 4 and held a reviewed, green retirement PR out of the queue, where the only sanctioned clear is the falseyesthe file already refuses to ask an author for.The measurement that picked the fix
Instance 2 is not the #16943 net-delta budget being too thin. The budget was earned — the removed
strategy:line is itself T1-shaped and bought one T1 unit — and then refused at the spend, by!CLOSED_SET_OPENER.test(text): "a line that DECLARES a closed set is never spent against the budget". That clause was written about an opener, and an opener never reaches it:memberTellKindalready answersnullfor an opener-only line. So the only lines it ever caught were keys whose value opensz.enum(/z.union(/z.discriminatedUnion(/z.literal(. Measured on this tree with the pre-change reader:The asymmetry was accidental.
What changed (
scripts/pm/check-widening-tells.mjs, one file)The spend. What the blanket refusal was protecting is real and is kept: an inline set has no per-member line for T2 to read, so a set widened in place is visible on the T1 row and nowhere else. A closed-set-valued key may now spend the budget only on three facts the block carries — a removed line naming the same key, both member lists readable on their own line, and the added list a subset of the removed one.
z.enum(['a', 'b'])toz.enum(['a', 'b', 'c'])still fires; a list that opens on a later line is unreadable and still fires; another key's removal pays nothing.The parameter. The claim is deliberately smaller than the depth-aware
z.object({ … })reader T1's own comment refuses — the one whose cheap version fails GREEN by truncating. A Zod shape body is{-delimited by construction, so the question is never "which shape is this line in" but "which bracket is innermost", read over the line's own hunk, plus a declaration head left of that paren. Every state the reader cannot carry honestly answersnull— keep the tell firing: a closer arriving on an empty stack (the hunk began inside something it was never shown), a string literal that does not close on its line, a declaration head it does not recognise (a method shorthand, for one). Nothing it returns ever means "no longer inside a shape", which is why it has no truncating failure mode. The same reading is applied on the old side too, the way #16822's fragment rule is: a deleted parameter must not buy an added key the right to go unreported.Price of the change, measured
Over the 233 commits touching these surfaces in this tree's history, A/B'd row-for-row between the pre-change reader (
origin/maine9efc403) and this one:Of the 23: 15 are parameters (twelve
ctx: z.RefinementCtx/z.core.$RefinementCtx, threeinput: z.input[typeof SomeSchema]) and 8 are existing keys re-spelled to carry.meta({ title })or a rewritten.describe()around an identical enum (e.g.d2badf723,f502898a4). Not one is a key or a member its diff added. On the tree itself, 16 of 8,974 T1-shaped lines underpackages/spec/src/**sit inside a parameter list, 10 of them annotatedz.RefinementCtx.The file's header prices this in the register it already uses, and states both quiet directions rather than leaving them to be discovered:
api-surface-signatures.jsoncarries 27define*helpers, none of them one of these checks). What is not lost is the function itself — a newly exported check adds its own T3 row, which is why fix(spec)!: refusedashboard.widgets[].options.stageOrderon every widget type that does not read it #17616 still reports one..optional()first among them) now declines on a closed-set-valued key. Not a new class: [finding] check-clause2-carriers T2 fires on a replaced string property value as "a new member of a closed set", and the C5 row it raises cannot be cleared except by declaring Clause-② yes on a change that does not widen #16943 already declines it for every key whose value is not a closed set; this removes the accidental exception rather than adding one. In the measured population all eight re-spellings are.meta/.describerewrites and none adds.optional().Acceptance (grading comment 5642818369)
dashboard.zod.ts:470cache.zod.ts:197manifest.zod.ts:44manifest.zod.ts:44#16489signature as a named fixtureFILE_REFINEMENT_SIGNATUREin--self-test--pair 17638with itsClause-②: nounchanged--pair 17616cannot form a pair: PR #17616 merged at 2026-09-11T07:41Z, and the carriers script only judges an open PR — it exits 2 ("PR #17616 is not open"), both before and after this change, which is not a T1 reading in either direction. Instance 1 is therefore reproduced and pinned where the reading actually lives:tellsInFileover that PR's real pushed patch (the bytes are the--self-testfixture), and the merged commit1f0b5659eis one of the 23 declines in the history A/B above.Verification
node scripts/pm/check-widening-tells.mjs --self-test— 269 cases pass (245 before; +24 in the new battery, registered in the roster so the floor cannot be silenced by deleting it).node scripts/pm/check-clause2-carriers.mjs --self-test— 465 cases pass (it imports this file).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the final commit and reconciled with--ran; every derived command run, exit codes in the report.Gate record — run on the final commit
923aed38anode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 32 families for the one changed path; all 32 were run and all 32 exited 0, then reconciled with--ran(each line recorded ascommand :: exit code, captured before any pipe):Beyond the derived families, the two this diff owes by kind:
pnpm check:pm-widening-tells(this file's own--self-test, which is whatlint.ymlruns) andnode scripts/pm/check-clause2-carriers.mjs --self-test(the importing sibling) — both exit 0 and both are inside the 32.git grepfinds no*.test.tsnaming this script; its suite IS the self-test.Lint is delivered as a proven narrowing, not skipped: (i) the population is read from eslint's own config — the base entry's
filesglob is the whole tree (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}), which covers this path; (ii)npx eslint --no-inline-config --format json scripts/pm/check-widening-tells.mjsreports 1 file, 0 errors, 0 warnings (count read from the JSON, exit 0); (iii) invariance — this repo runs oneeslint.config.mjswhich never enables type-aware linting for any file (noparserOptions.project, no typed@typescript-eslintrules; the config says so ateslint.config.mjs:326), so a one-file diff cannot move the verdict on any file it does not touch. The whole-repoeslint . --no-inline-configremains CI's run.The one judgment call, on the four axes
The card offered two shapes and this PR takes the smaller one. 实际业务需求: the population is measured, not speculative — 233 commits, 23 rows, and one PR currently blocked by the instrument. 项目长远合理性: the reading adds one bracket fact a hunk genuinely carries and refuses the depth-aware shape reader whose cheap version truncates;
nullmeans "keep firing" everywhere, so it cannot degrade silently (Route & surface ownership §3). 防 AI 写错: the defect pushed a seat away from tightening a contract — it reported the narrowing direction as the widening one — which is the inverse of what clause ② exists to catch; correcting it makes the strict direction the cheap one. 创业阶段不扩散: one file, no new flag, no new label, no new claim-line syntax, and no exit code moves.Acceptance notes
check-clause2-carriers.mjs,packages/spec/**and.claude/**are untouched; PR feat(spec)!: retire thescheduledcache-warmup strategy — the cron it selected left in this same major (ADR-0049) #17638 is read-only evidence.scripts/pm/**publishes nothing from any package'sfiles[]— this is theskip-changesetcase theCheck Changesetgate's own text calls textbook ("this PR edits a CI-internal script"). The label is applied on the PR.Authored by the skills-lane dev seat, session
session_01MCLBsUgfykL74aU716rzVK, on claim comment 5642827410.Generated by Claude Code