fix(scripts): enclosingDeclaration recognises a CLASS METHOD header, so a method code helper is no longer dropped in silence - #13428
Merged
os-project-manager merged 4 commits intoAug 30, 2026
Conversation
WIP: recognizer + call-site receiver. Ledger and pins follow.
os-project-manager
marked this pull request as ready for review
August 30, 2026 11:45
os-project-manager
enabled auto-merge
August 30, 2026 11:45
os-project-manager
deleted the
claude/issue-13226-codehelper-class-method
branch
August 30, 2026 12:12
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 #13226
enclosingDeclarationinscripts/check-dispatcher-error-vocabulary.mjsrecognised three declaration header forms —function NAME(,constructor(,const|let|var NAME = (— and no CLASS METHOD. A code-carrying helper that happens to be a method was therefore unreachable even in the assignment positioncodehelperalready implements: the identifier was never recognised as a parameter,helperCodesForreturnednull, and the site fell through toresolveConstant, which reduced nothing. No site, no unresolved, nothing reported — the gate's declared bound (a value it cannot reduce is REPORTED, never dropped) failing in the one way the bound cannot notice.The control table, reproduced through the real
deriveSitesSame body, same unregistered probe, one declaration form apart. K is the positive control that proves the instrument fires, and it is reproduced on both sides precisely because a fix that lights up I while K silently died reads identically in a summary.
function fail(code, message)e.code = codecodehelperprivate fail(code, message)in a classe.code = codecodehelperprivate fail(code, message)in a class{ severity, code, message }J is the boundary. The declaration form is now seen; the object-literal stamp POSITION still has no shape, and that half is the sibling card's. J is pinned as its own case so the split stays legible.
Two halves, because one alone delivers only noise
The recognizer. A method header is bare
NAME(, so the discriminator is the header's own shape: line start, optional modifier run, name, balanced parens, optional return-type annotation,{. The trailing{does the work — it separates a method from a line-initial CALL (;follows) and from a body-less interface orabstractSIGNATURE (no body, so no stamp can be inside it). A keyword lookahead removes the statement forms that share the shape (if (x) {,return (a) => {).The call scan.
helperCodesForbuilt its call regex with[^\w.$]before the name, which excludes a preceding dot on purpose. A method is reached through a receiver, so without widening that, every method helper would reduce to no literals and land as anunresolved— the declared bound technically restored, but the instrument permanently at the noise level and never resolving a single code.this.and onlythis.is admitted: the scan is in-file, so a method helper's callers here are its own siblings, and any other receiver stays unmatched, which is the safe direction.Why not a real parser, and why not brace-depth class tracking
The card frames the discriminator as being directly inside a class body, and says a textual scan has to earn that. It cannot, for measured reasons rather than squeamish ones, so the header shape above is what is implemented instead:
maskCommentsed source, which by contract leaves STRINGS, TEMPLATES and REGEX LITERALS standing — a brace inside a string flips the depth of everything after it. Masking literals too (scanSourceexposes the flags) fixes that half and leaves the other: finding where a class BODY opens means walking aclass X extends Ywhose TYPE ARGUMENT is an inline object type — a brace that opens inside the type argument, before the class body ever starts, and telling a type argument from a comparison textually is the problem TypeScript wrote a parser for.DECLARED widening: the header shape also admits an OBJECT-LITERAL method. That is the same helper genre, it costs nothing today (zero in
packages/**non-test source stamp a code in the assignment position), and it cannot produce a wrong site — anobj.fail(...)caller matches nothing, so it degrades tounresolved. Stated in the code rather than left to be discovered. Still NOT recognised: a class PROPERTY holding an arrow — a fourth form, filed separately, not smuggled in here.Blast radius on this tree: ZERO, measured
Derived sites and
unresolvedare byte-identical before and after, through the realderiveSitesoverpackages/**, comparing this branch against a worktree at the parent commitc09451bf1— same tree, only the script version differing.Two independent reasons, and the second is the one worth carrying forward:
.code = identmatches inpackages/**non-test source, 5 are true helpers (the identifier is a parameter) and every one is afunctionor aconstructor; 0 are methods.unresolvedthe card attributes to this blindness are NOT a cost this change pays — they are a cost the POSITION widening will pay when it lands. The card reads as though closing the declaration form surfaces them; it does not, and the ledger now says so explicitly.What this does buy is that the position widening will REACH those helpers.
enclosingDeclarationat theParser#errorstamp offset now answerserror(code, message, start, tag); before it answered the Parser CONSTRUCTOR. Helpers visible toenclosingDeclarationgo from 10 of 14 to 13 of 14.Re-derived numbers — the card's were claims, and one had drifted
unresolvedunresolvedThe whole-population drift is explained exactly and entirely by one helper that landed between the two readings (
blockinpackages/spec/scripts/check-yaml-examples.ts, added 2026-08-29 21:33 UTC, 10 call sites, none reducing): 106 + 10 = 116, and 4 + 1 = 5. The card's figures were right when taken. This is a census of a moving tree, and the ledger now says a reader re-runs it rather than quoting it.⭐ The number that grades the work — unregistered SCREAMING_SNAKE codes hiding behind the blindness — is still ZERO, so the p2 holds and this remains prevention rather than a live defect. It is a property of the TREE, not of the gate, so the ledger marks it as expiring.
Adjacent and re-checked, not assumed: the card records
enclosingDeclarationanswering the constructor whose parameters parsed as["readonly","readonly"]. That half was already repaired onmain— it reads["src","opts"]now. Both halves had to be true for the stamp to resolve; neither alone sufficed.Pins, and the two that ablation proved wrong
--self-testgoes from 194 to 202 assertions. The former zero on the class-method form is now an EQUIVALENCE against the free-function control, plus the receiver assertion, the constructor-precedence case, the two look-alike shapes, and the J boundary.Every load-bearing pin was ablated from the committed state — mutation proven on disk by anchored text counts and a
git hash-objectchange, restore proven by the blob matchingHEADwith an emptygit diff HEAD, each leg carrying atrapon absolute paths.this.receiverconstructorfrom the keyword lookaheadBoth wrong predictions are reported rather than quietly fixed, because each was a real defect:
isConstructoris read off the matched TEXT, not off which alternative fired, sopublic constructor(is still classified as a constructor even when the method alternative wins the position. The entry is kept, the comment now says it is defensive and that removing it leaves the self-test green, andisMethodis made mutually exclusive withisConstructorso the flags cannot both be true.function outer(, which wins as nearest-preceding whether or not the signature is recognised — so it passed with the body-follows check deleted. The pin is repaired to put the stamp where the signature would be the nearest header, and it now fails correctly under that ablation. The trap is written into the test as a comment, because it is silent.Local verification, at
9344aaecThe gate's site count and pending count are unchanged from the parent commit, which is the zero blast radius stated above.
Gate family derived on this branch's own final head with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(1 changed path). All green, exit codes captured before any pipe:check:dispatcher-error-vocabulary·check:agent-test-spelling·check:bash32-floor·check:cli-command-ids·check:cross-package-test-inputs·check:entry-guard·check:parse-guard·check:pnpm-filter-targets·check:watch-hint-literal·check:nul-bytes·check:pm-dispatch-gates·check:error-code-casing·pm/bare-root-worklist.mjs --self-test·check-self-test-wired.mjsand its--self-test·check-ci-filter-parity.mjs·check-cross-package-test-inputs.mjs·check-shard-attestation.mjs.The two convention-triggered obligations for editing a GATE SCRIPT (
bare-root-worklist --self-testandcheck:pm-dispatch-gates) are in that list and both pass; neither needed a new ledger row, since this change adds no population.Declarations
packages/specand the runtime doors are untouched.packages/orskills/is modified, so nothing is released.scripts/is CI-internal, theskip-changesetcase, so no changeset accompanies this PR — matching the sibling change to this same file.docs/adr/, no.claude/, noAGENTS.md, noCLAUDE.md, nocontent/docs/releases/.SHAPESis unchanged, and the boundary case J is pinned to stay at zero. The queued sibling cards on this same file are not addressed by this branch and remain open.helperCodesFor's call scans is built from the declaration's name, so it can produce neither a site nor anunresolved— admitting it to the header regex would buy nothing and cost the regex its anchor. Reaching it needs call-graph resolution this textual scan does not have. It remains the sole entry ininvisibleDeclarationForms, and that entry is pinned.Generated by Claude Code