Skip to content

test(plugin-map,plugin-timeline): pin what these two contractEnvelope-6839 waits were standing on - #8713

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-8709-unpinned-wait-preconditions
Sep 9, 2026
Merged

test(plugin-map,plugin-timeline): pin what these two contractEnvelope-6839 waits were standing on#8713
os-justin merged 1 commit into
mainfrom
claude/issue-8709-unpinned-wait-preconditions

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8709

Test-only. No component changed. Final head 3df35c668.

Both diagnoses reproduced first — one of them needed a correction

The card asked me to re-derive at the component before repairing, because its parent card's list was half wrong. Both readings hold, and the timeline one holds more narrowly than stated.

map — CONFIRMED, and worse than the card said. The absence-shaped wait (queryByText('Loading map...') is null) is satisfied by a mount that never started. With ObjectMap mutated to return null unconditionally — "render nothing, ever", strictly worse than the bug — the refusal case still passed. And the panel is a settle signal, not a rows signal: it substitutes for "the rows are on screen" only because setData(capped.rows) and setLoading(false) sit in one await continuation and React 18 batches them. Nothing asserted that. With setData deferred by 50ms and the records arm restored to extractRecords, the refusal case read zero markers and passed while the settled map plotted two — the bug the file exists to catch went undetected.

timeline — CONFIRMED in substance, CORRECTED in reach. The data-item-count not-null clause is inert exactly as described: the renderer double writes the attribute unconditionally, so once getByTestId stops throwing the attribute is always a string, and the throw was the entire gate. Under the same deferral the wait was satisfied at data-item-count="0" while the settled timeline drew two rows. But the card's framing that this is a bare mount signal overshoots in one direction: getByTestId throwing is a real mount gate, so this pin does not pass "render nothing, ever" — under that mutation its refusal case went red pre-fix. Its exposure is narrower and specific: mounted-but-not-yet-populated, not never-rendered. The repair is aimed at that, and the mount-transition guard is added because a renderer mounted empty on the first paint would still slip through.

What changed

markersThrough (map) — chose both options the card offered, because the legs show neither alone closes it:

  1. asserts the loading panel is on screen before the absence wait (kills "never started" / "render nothing, ever");
  2. reads the marker count at the transition, holds a 50ms settle window, and requires the count to be unchanged — which pins the co-commit invariant the card flagged as real and defended by nothing.

Gate 1 alone still lets a split commit through; gate 2 alone still passes a component that renders nothing. Measured, both directions, below.

itemsThrough (timeline) — gates on the count the wait names, at the value the arm claims, then requires that value to survive the same 50ms window, with the loading skeleton observed first. The inert clause is replaced rather than deleted, per the card.

50ms rather than 0ms: RTL's asyncWrapper drains one macrotask before returning, so a setTimeout(..., 0) commit hides inside the window meant to detect it.

Evidence — refusal-arm classification, from vitest's JSON reporter

Read sites mutated, never a pin. M1 = the records arm restored ahead of data in extractRecords; M2 = the data commit deferred 50ms in the component; M3 = the component returns null unconditionally.

leg pin mutation map refusal arm timeline refusal arm
baseline pre-fix none PASS PASS
A pre-fix M1+M2 PASS (bug undetected) PASS (bug undetected)
B / G pre-fix M3 PASS (bug undetected) FAIL
clean post-fix none PASS PASS
C post-fix M1+M2 FAIL FAIL
D post-fix M3 FAIL FAIL
E post-fix M1 only FAIL FAIL

Leg E is the no-regression leg: the plain bug, with commits still batched, is caught before and after — so this is a strengthening, not a relocation.

The failing assertions in leg C carry the reading themselves:

AssertionError: markers must not arrive AFTER the loading panel clears — the count
read at the transition must already be the settled one: expected 2 to be +0
AssertionError: rows must not arrive AFTER the count was read — a count that moves
in the settle window was an intermediate state: expected '2' to be '0'

expected 2 to be 0 is the whole finding in one line: the old wait read 0 while the settled component held 2.

Control (the PR #8702 shape). Legs F and G restore the pre-fix pin from the base blob and run it under the identical mutation. Provenance by git hash-object, not by assertion:

packages/plugin-map/src/ObjectMap.contractEnvelope-6839.test.tsx
  base blob      : 829e417b5fb200705378cf7323d06f0b977e74e0
  on disk now    : 829e417b5fb200705378cf7323d06f0b977e74e0
  HEAD (post-fix): ea263b3e6e8313f0614358a7808d86a79f684d1e
packages/plugin-timeline/src/ObjectTimeline.contractEnvelope-6839.test.tsx
  base blob      : e5e244554e66ad1f7c3b7bfb2b72edba7351a151
  on disk now    : e5e244554e66ad1f7c3b7bfb2b72edba7351a151
  HEAD (post-fix): ec53eac846d208a209c05504fc553e7625aa0f45

Every ablation ran from a committed tree, proved its mutation on disk in both directions (anchor counts, git hash-object against the HEAD blob, and a line-total gate), and restored by state under an EXIT/INT/TERM trap with absolute paths — git diff HEAD empty and disk hash equal to the HEAD blob after each leg.

Harness hazards the card warned about, both carried. The react-map-gl/maplibre mock the pin owns is untouched, and every map measurement ran through the pin itself rather than a substitute. Container leakage was checked rather than assumed: three consecutive clean repetitions, all 2 / 2 / 0 — an accumulating count would have gone red on repetition two, since the pin asserts exact numbers.

Checks run locally

  • vitest run on both pin files, JSON reporter: 6/6 pass on the final head; stable over three repetitions.
  • pnpm --filter @object-ui/plugin-map --filter @object-ui/plugin-timeline run type-check: both Done, with the dependency closure built first (turbo run build --filter=@object-ui/plugin-map^... --filter=@object-ui/plugin-timeline^...), so this is measured and not a TS2307 precondition failure. It found a real one: toBeInTheDocument is TS2339 in plugin-timeline because that package's tsconfig.test.json does not name @testing-library/jest-dom in types (the plugin-map one does). Swapped for toBeTruthygetByTestId throwing is the assertion either way — and left a comment saying why. Reported as an out-of-scope finding.
  • eslint on the two changed files: 0 errors, 9 warnings — identical to the count on the base blobs, so no new finding.
  • node scripts/check-changeset-presence.mjs: green. Empty-frontmatter changeset, the real exemption; skip-changeset is a phantom label here and was not applied.

Repo-wide lint and the full suite are CI's, not narrowed here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…ing on

Both `contractEnvelope-6839` pins were green for reasons their wait
expressions did not state.

`ObjectMap` carries the family's only absence-shaped wait — `queryByText('Loading
map...')` is null. An absence nothing entered is satisfied by a mount that never
started: with the component mutated to `return null` unconditionally, an
implementation strictly worse than the bug, the refusal case still passed. And
the panel is a settle signal, not a rows signal — it stands in for "the rows are
on screen" only because `setData` and `setLoading(false)` land in one React
commit, which nothing asserted. With `setData` deferred by 50ms and the
`records` arm restored to `extractRecords`, the refusal case read zero markers
and passed while the settled map plotted two.

`ObjectTimeline` waited for `getByTestId('timeline-renderer')`'s
`data-item-count` to be not-null. That clause is inert: the renderer double
writes the attribute unconditionally, so once `getByTestId` stops throwing the
attribute is always a string, and the throw was the entire gate. It named a row
count and gated on a mount, so `"0"` satisfied it — under the same deferral the
wait was satisfied at `data-item-count="0"` while the settled timeline drew two
rows.

`markersThrough` now asserts the loading panel is on screen before waiting for
it to go, and that the marker count read at the transition survives a 50ms
settle window. `itemsThrough` takes the count its arm claims, gates the wait on
that value, requires it to survive the same window, and observes the loading
skeleton first so a renderer mounted empty on the first paint is not read as a
settled zero. 50ms rather than 0ms because RTL's `asyncWrapper` drains one
macrotask, so a 0ms deferral hides inside the window meant to detect it.

Test-only; no component was changed.

Refs objectui#8709

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

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.6 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BkOatu4S.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.36KB 61.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 251.25KB 65.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.18KB 56.62KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit a1ddb5b Sep 9, 2026
35 checks passed
@os-justin
os-justin deleted the claude/issue-8709-unpinned-wait-preconditions branch September 9, 2026 01:28
os-justin pushed a commit that referenced this pull request Sep 9, 2026
… file

The merge before this one took this branch's side of an add/add conflict and
proved that drops no BYTE main carries. It does not prove it drops no CLAIM:
the header main landed makes five, and each needed a verdict against the AST
matcher that now sits underneath it. Four survived the rewrite. One had not.

RESTORED — the two regex modes' strict buckets are NESTED on this tree.
The rewrite kept "the modes are incomparable by construction" (M1) and dropped
the measurement that says the union of the two is just `path`'s bucket. M1 is
about shapes; on this tree ident-only is EMPTY, so a reader could take M1 as a
reason to run both modes and union them, which buys nothing. Re-measured on
both trees rather than copied forward:

    at da5e4f6            ident 15 ⊂ path 18   ident-only EMPTY, 3 path-only
    with main @ a9bc029   ident 12 ⊂ path 15   ident-only EMPTY, the SAME 3

and the same three `PermissionMatrixEditor` sites are the path-only ones in
both. The companion claim — that the mode choice was never the largest source
of movement — is restored as arithmetic the file can be re-run to check: the
mode separates THREE sites, D1 and D2 separate SIXTEEN (−7/+9, both verified
by diffing the site lists at da5e4f6).

ADDED — R2', the half of D1 that did NOT get repaired.
The window is scoped to the enclosing FUNCTION body. That kills the runaway
half (it can never reach the next `it`) and leaves the truncation half alive,
pointed the other way: a wait hosted in a helper the test awaits covers none of
the CALLER's statements. Forced on three probes — the same cross-recorder read
draws a flag with the wait inline and ZERO with it moved into a helper. Not a
corner: 503 of this tree's 3945 `await waitFor(...)` sites have their window
owned by an inner function, 464 of those a helper. The regex matcher was blind
here too, differently, so this is a surviving blind spot rather than a
regression — the header says which, and says the one shape where the old
matcher's runaway window did reach the read and this one does not.

The banner the reader running the script actually sees said the three error
sources were removed. Two were, plus half of the third; it now says that.

Also recorded: the counts move with the TREE alone. Merging main moved the
population 2776 → 2786 and every strict bucket down (AST 20 → 18, path 18 → 15,
ident 15 → 12) with the matcher byte-identical, because #8707/#8711/#8713 each
anchored a wait. That is claim 4 demonstrating itself.

Unchanged and re-verified: the census stays OUT of CI. No workflow and no
package script runs it; the only caller is the pin test, over six fixtures.

objectui#8704

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 9, 2026
… fixtures a home (objectstack-ai#8719)

* test(plugin-designer): anchor the lookupKeying absence pin on the handler's reload

`expect(deletes).toEqual([])` was dated to the first PUT by `waitFor(puts)`.
That covers the delete scan only because the scan happens to run before the
save loop in `handleObjectsChange` — a property of the page, not one the file
asserts. Anchor it on `reload()`, the handler's last statement, so the
emptiness is read after every write the handler makes.

Measured: with a stray `reset` issued after the saves, the `puts` wait leaves
this assertion green while C0/H1/H2 in the same file — which already anchor on
the reload — go red.

objectui#8690

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

* chore(scripts): keep objectui#8690's recorder-wait census as a script

The detector answers where to look, never what is wrong, so it is a census
tool and NOT wired into CI: a gate on this list would institutionalise the
batch repair the card exists to prevent. Two recorder-matching modes, because
the choice moves the numbers, and the header records what each one measured on
the card's base commit rather than quoting the card's counts as reproduced.

Changeset declares an empty frontmatter: test-only plus one repo script,
nothing published moves.

objectui#8690

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

* chore(scripts): record what the recorder-wait census's two matchers disagree about

The census script's numbers move with an internal choice its header stated but
never measured. objectui#8703 measured it, and the answer is wider than the
mode choice:

- The two `--recorder-match` modes are incomparable by construction — each
  flags a shape the other cannot see (forced on fixtures, both directions).
- On da5e4f6 the buckets are nevertheless nested: ident's 15 is a subset of
  path's 18, ident-only is empty. "They do not contain each other" is an
  in-principle claim, false as a measurement of this tree.
- The header's stated `path` blind spot (pushed bare, read as `host.inits[0]`)
  is wrong: both modes miss that shape.
- Two mode-independent rules dominate the mode choice: the forward window ends
  at the next `await` in the FILE rather than at the end of the enclosing test,
  and any textual occurrence counts as a read. Seven of the eighteen strict
  flags on that tree point at a declaration, a destructuring, a reset or a
  function parameter — not at a read.

Header rewritten to carry that, plus a caveat printed next to the counts. The
detector's behaviour is unchanged: both modes print byte-identical site lists.
Script and changeset only.

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

* chore(scripts): give the recorder-wait census an AST matcher, and its fixtures a home

objectui#8704. The census resolved a recorder by the SPELLING of its `.push(`
site, ended its forward window at the next textual `await` IN THE FILE, and
counted every textual occurrence as a read. Seven of the eighteen strict flags
it reported at da5e4f6 were therefore a declaration, a destructuring, a reset
or a function parameter — and the same truncation went blind on a genuine
cross-recorder read one ordinary `await` further on.

The default matcher is now an AST pass:

  - identity over bindings and aliases, not names, one hop through a helper's
    parameter and a same-file factory's return value;
  - windows scoped to the enclosing test body, in statements, ending at the next
    awaited SETTLING anchor rather than at any `await`;
  - every occurrence classified read / write / declaration.

The original census is kept behind `--matcher=regex`, so objectui#8690's and
objectui#8703's published numbers stay reproducible from this file rather than
being claims about a deleted script. Verified: at da5e4f6 it still prints
159 / 15 strict for `ident` and 167 / 18 strict for `path`, exactly as published.

objectui#8703's five throwaway fixtures are committed as the script's test suite,
with the sixth objectui#8704 asked for — the absence read objectui#8690 repaired,
which must STAY flagged, because "flag nothing" is strictly worse than the bug
and passes every other case. Both matchers are pinned over the same six files,
so the repair is pinned as a direction.

The header's "no count printed here is a corpus fact" caveat STAYS, with its
residuals restated: no type checker, a window rule that is a judgement rather
than a fact, and a flag that is still a site to read and not a defect. The
census remains out of CI; the pin test runs the matcher over the fixtures only.

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

* chore(scripts): resolve the census header claim by claim, not file by file

The merge before this one took this branch's side of an add/add conflict and
proved that drops no BYTE main carries. It does not prove it drops no CLAIM:
the header main landed makes five, and each needed a verdict against the AST
matcher that now sits underneath it. Four survived the rewrite. One had not.

RESTORED — the two regex modes' strict buckets are NESTED on this tree.
The rewrite kept "the modes are incomparable by construction" (M1) and dropped
the measurement that says the union of the two is just `path`'s bucket. M1 is
about shapes; on this tree ident-only is EMPTY, so a reader could take M1 as a
reason to run both modes and union them, which buys nothing. Re-measured on
both trees rather than copied forward:

    at da5e4f6            ident 15 ⊂ path 18   ident-only EMPTY, 3 path-only
    with main @ a9bc029   ident 12 ⊂ path 15   ident-only EMPTY, the SAME 3

and the same three `PermissionMatrixEditor` sites are the path-only ones in
both. The companion claim — that the mode choice was never the largest source
of movement — is restored as arithmetic the file can be re-run to check: the
mode separates THREE sites, D1 and D2 separate SIXTEEN (−7/+9, both verified
by diffing the site lists at da5e4f6).

ADDED — R2', the half of D1 that did NOT get repaired.
The window is scoped to the enclosing FUNCTION body. That kills the runaway
half (it can never reach the next `it`) and leaves the truncation half alive,
pointed the other way: a wait hosted in a helper the test awaits covers none of
the CALLER's statements. Forced on three probes — the same cross-recorder read
draws a flag with the wait inline and ZERO with it moved into a helper. Not a
corner: 503 of this tree's 3945 `await waitFor(...)` sites have their window
owned by an inner function, 464 of those a helper. The regex matcher was blind
here too, differently, so this is a surviving blind spot rather than a
regression — the header says which, and says the one shape where the old
matcher's runaway window did reach the read and this one does not.

The banner the reader running the script actually sees said the three error
sources were removed. Two were, plus half of the third; it now says that.

Also recorded: the counts move with the TREE alone. Merging main moved the
population 2776 → 2786 and every strict bucket down (AST 20 → 18, path 18 → 15,
ident 15 → 12) with the matcher byte-identical, because objectstack-ai#8707/objectstack-ai#8711/objectstack-ai#8713 each
anchored a wait. That is claim 4 demonstrating itself.

Unchanged and re-verified: the census stays OUT of CI. No workflow and no
package script runs it; the only caller is the pin test, over six fixtures.

objectui#8704

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants