Repository navigation
Commit 40bc51f
Fixes #22860
Clause-②: no
## What changed
`objectstack build` lowers an inline handler to a metadata `body`, and
`os lint` calls the same extractor. That extractor found the body by
counting `{` / `}` and `(` / `)` through `String(fn)`. It was blind to
strings, templates, regexes and comments, and it took the first
parenthesised `=>` anywhere as the handler's header.
`peelToBlockBody` in `packages/cli/src/utils/extract-hook-body.ts` now
reads the body off the handler's TypeScript parse. The node is
`parseFunction`'s, the one the parameter rebind and
`detectFreeIdentifiers` already read.
- A block body is the text between the block's own braces.
- An implicit-return arrow is its expression, taken from the end of `=>`
with leading whitespace trimmed and wrapped as `return (…);`, as before.
The parse runs once per callable and feeds both the peel and the rebind.
`findTopLevelArrow`, `matchBalancedIndex` and `sliceBalanced` are
deleted. All three were module-private, and `extractLoweredBody` was the
peel's only caller.
**Landing point:** `extract-hook-body.ts` only, plus tests and a
changeset.
- `detect-free-identifiers.ts` and `hook-body.ts` are untouched.
`parseFunction` was already exported and its node carries its source
file, so nothing had to be exposed.
- `compile.ts` and `lower-callables.ts` (#22853's) are untouched.
- No `hook-body/*` message is reworded.
## The PM's mechanism assumptions, measured
1. **Confirmed on `69d4218058`.** The three functions count brackets
with no string, template, regex or comment skipping.
2. **Reachable, same text.** `extractLoweredBody` hands the same `raw =
String(fn)` to the peel, the rebind and the free-identifier pass, so the
parse node reads exactly the text the peel received.
- A block's node span INCLUDES its braces: `getStart` is the `{` and
`end` is one past the `}`. The peel takes the text between them, which
is what the scan returned.
- For an expression body, `body.pos` is the end of the `=>` token, so a
leading comment survives the way the scan kept it.
3. **Reproduced at the real build before the fix**, table below.
## Before and after, at the real build
Scratch app, `tsx packages/cli/bin/run-dev.js build`. Each shipped body
was run under the L2 wrapper `(async (ctx) => { SOURCE })`.
| handler | BASE `69d4218058`, build exit 0 | after, build exit 0 |
|---|---|---|
| `'a}b'` in a string | SyntaxError | runs, `a}b` |
| `'a{b'` in a string | bundled: "could not parse the body" | lowered,
runs, `a{b` |
| template `t:${…}!` | runs | runs, same bytes |
| template with a literal `}` | SyntaxError | runs |
| regex `/}/g` | SyntaxError | runs |
| `async function` with a statement-level inner arrow | SyntaxError |
runs |
| `async function ({ input })` | ships `var { input } = ctx; input`:
runs, stores nothing | runs, stores `fn:alpha` |
| control | runs | runs, same bytes |
On BASE, `hook-body-source-unparseable` fired 4 times and "could not be
lowered" once. After the fix, neither fires.
The balanced template already lowered correctly on BASE. The failing
template member is one whose literal text carries an unbalanced brace,
so both are pinned.
The `function` handler with a pattern parameter is a member the card did
not name, and it is the silent one. Same mechanism (the scan took the
parameter pattern's `{` for the body), same fix, pinned beside the
others.
## Pins
- **`packages/cli/test/extract-hook-body.test.ts`: six defect pins.**
- Covered: a `}` in a string, a template (balanced, and with a `}`), a
regex and a comment, the inner arrow (expression-bodied, block-bodied,
method), the pattern parameter, and the opening `{`. The opening-`{` pin
goes through `lowerCallables` with zero `bodyExtractionWarnings`.
- Each pin runs the in-process handler and its lowered body and compares
them on a concrete value, so "both did nothing" cannot pass.
- **The same file: a CONTROL table of 12 shapes.** Each expected
`source` was MEASURED from BASE's scan over the exact text. `String(fn)`
equals that text, which the test asserts.
- **`packages/qa/dogfood/test/lowered-body-door.dogfood.test.ts`:**
- Hooks: `lbd_brace_string` and `lbd_inner_arrow` (the two shapes the
card measured), plus `lbd_fn_pattern`.
- Each is driven in-process and lowered, through `hooks.run` and over
REST.
- An anti-vacuity pin asserts each body is the handler's own block.
## Reverse verification (one-off; nothing left in the tree)
1. **The BASE peel restored**, tree only, by a trap-armed script.
- Proof of mutation: blob `a47af067c3`, equal to BASE's. Proof of
restore: blob `f78625e024`, equal to HEAD's, and `git diff HEAD` is
empty.
- Unit file: `6 failed | 50 passed (56)`. The 6 are exactly the new
defect pins, so all 12 CONTROL cases are green on BASE.
- Dogfood door: `7 failed | 13 passed (20)`.
- `lbd_brace_string`: SandboxError "SyntaxError: unexpected end of
string".
- `lbd_inner_arrow`: SyntaxError "expecting ')'". REST answered 500
where the source boot answered 201.
- `lbd_fn_pattern`: stored `null` where in-process stored
`pattern:alpha`, with no error at all.
2. **The CONTROL can fail.** Mutations went through
`scripts/ablation-replace.mjs`; each anchor hit 1 → 0 and was restored
to the HEAD blob.
- Block arm, `slice(open + 1, …)` changed to `slice(open, …)`: `15
failed`. That is all 9 block-bodied CONTROL cases plus the 6 defect
pins; the 3 expression cases stay green.
- Expression arm, `body.pos … trimStart()` changed to
`body.getStart(sf)`: exactly `1 failed`, the leading-comment case.
The unit tests import `../src/utils/extract-hook-body.js`, and the
dogfood door imports `../../../cli/src/utils/lower-callables.js`. So no
`dist/` sat between a mutation and its run.
## CONTROL over a corpus (one-off scratch instrument, not committed)
Both peels were run over every function text in the esbuild-transformed
(`es2022`, `esm`) `.ts` / `.mts` sources of `packages/cli`, `examples`
and `packages/qa`. The tree was `b4acbe447f` (BASE plus the fix): 1180
files, 21,262 functions.
- **20,739 identical.**
- **0** that BASE read and the new peel refuses.
- **121** that BASE refused (an unbalanced `{` in a string) now lower.
- **402 differ, and every one is a BASE misread:**
- 185: an inner block taken as the body (inner arrow, parameter pattern
or default);
- 9: a block outside the body (a pattern or default parameter);
- 98: an inner arrow read as the handler;
- 110: a body cut short at a brace in a string, template, regex or
comment.
- **10 corrected bodies fail my probe for reasons outside the peel.**
The probe compiles under `(async (ctx) => …)`: 6 of them declare their
own `const ctx`, and 4 use `import.meta`. In each one BASE had peeled an
inner function instead.
**Example apps**, with configs loaded through `loadConfig` as `os build`
loads them:
- `app-crm`: 2 of 2 callables identical.
- `app-todo`: 3 of 3 identical.
- `app-multi-package`: no inline callables.
- `app-showcase`: **NOT MEASURED** (its connector dists were not built
at that point). Its source text is covered by the corpus above.
## Tests and gates, at `ba8ac397a6` (after merging `origin/main` at
`0253192f76`)
**Tests and typechecks:**
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2`: 283 files, 4260 tests passed, `VERDICT command-exit 0`.
- The `integration` tier is declared to CI. The diff touches no
integration-tier file and no spawn entry point.
- `pnpm --filter @objectstack/cli typecheck`: exit 0,
`check:test-typecheck: OK`.
- `tsc --listFilesOnly` puts `test/extract-hook-body.test.ts` in
`tsconfig.test.json`'s program and `src/utils/extract-hook-body.ts` in
`tsconfig.json`'s.
- `pnpm --filter @objectstack/dogfood typecheck`: exit 0. Its program
includes the door file.
- `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2
test/lowered-body-door.dogfood.test.ts`: 20 passed.
**Gates:**
- The dispatch's 70-command gate list: every command exit 0.
- `check:dual-build-cjs-loads` first exited 3 with PREREQUISITE NOT MET:
8 packages outside this card's closure had no `dist/`.
- Re-run after building those 8: exit 0, `✓ check:dual-build-cjs-loads —
107 published require entry point(s) across 66 package(s) load`.
- `dispatch-gates --ran`, given a record with an exit code on every
line: `✓ dispatch-gates --ran: 69 derived famil(ies) accounted for — 69
run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and
none of them is 3)`.
- Re-deriving on the final tree added no command. It dropped `pnpm
check:dispatcher-error-vocabulary`, which was run anyway: exit 0.
**Lint, as a proven narrowing rather than a skipped run:**
- `eslint --no-inline-config --format json` over the 3 touched TS files
at `ba8ac397a6`: 3 files in the output, 0 errors, 0 warnings, none
reported as ignored.
- **Population, read from the config.** `eslint.config.mjs`'s
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` object matches all three. The
changeset `.md` matches no `files` glob.
- **Invariance.** The config enables no type-aware linting (no
`parserOptions.project`, no `projectService`) and loads no plugin with
cross-file rules. Its only file reads are its own baseline JSONs, which
this diff does not touch. So this diff cannot move any untouched file's
verdict.
**Size:** 4 files, +362 / −96.
## Acceptance notes
- **One refusal is folded into another; none is reworded.** The hook
face's separate "could not read the parameter list of …" refusal is
gone. The body and the parameter list now come from one parse, so a
handler that parse cannot read meets the existing "could not parse the
body of …" refusal instead: same `unparseable` kind, same remedy clause.
No measured function reached the removed refusal (0 of 21,262).
- **An observation, not filed.** A zero-parameter hook handler that
declares its own `let` / `const ctx` lowers to a body that redeclares
the wrapper's `ctx`, which is a SyntaxError: the rebind's shadowing
check runs only when there is a parameter. I read this off the
differential and did not measure it at a door, and I found no real
producer.
- **The sibling.** #22853 had not landed when this was opened, and none
of its files are touched.
## Out of scope, reported to the seat (not addressed here)
An action `target` whose parameters are not named `(input, ctx)`, for
example `async (params, context) => …`, lowers at `objectstack build`
exit 0 to a body that throws `ReferenceError: params is not defined`.
That is under the runtime's action wrapper, `(async (input, ctx) => { …
})` in `packages/runtime/src/sandbox/quickjs-runner.ts`. The `(input,
ctx)` control runs. This is the action-face sibling of the hook-face
parameter rebind; nothing here changes it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72b26ed commit 40bc51f
4 files changed
Lines changed: 362 additions & 96 deletions
File tree
- .changeset
- packages
- cli
- src/utils
- test
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
112 | 119 | | |
113 | 120 | | |
114 | 121 | | |
| |||
580 | 587 | | |
581 | 588 | | |
582 | 589 | | |
583 | | - | |
584 | | - | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
585 | 595 | | |
586 | 596 | | |
587 | 597 | | |
| |||
596 | 606 | | |
597 | 607 | | |
598 | 608 | | |
599 | | - | |
| 609 | + | |
600 | 610 | | |
601 | 611 | | |
602 | 612 | | |
| |||
769 | 779 | | |
770 | 780 | | |
771 | 781 | | |
772 | | - | |
773 | | - | |
774 | | - | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
775 | 787 | | |
776 | 788 | | |
777 | 789 | | |
| |||
788 | 800 | | |
789 | 801 | | |
790 | 802 | | |
791 | | - | |
792 | | - | |
793 | | - | |
794 | | - | |
795 | | - | |
796 | | - | |
797 | | - | |
798 | | - | |
799 | | - | |
800 | | - | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
801 | 808 | | |
802 | 809 | | |
803 | 810 | | |
| |||
977 | 984 | | |
978 | 985 | | |
979 | 986 | | |
980 | | - | |
981 | | - | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
982 | 1020 | | |
983 | | - | |
984 | | - | |
985 | | - | |
986 | | - | |
987 | | - | |
988 | | - | |
989 | | - | |
990 | | - | |
991 | | - | |
992 | | - | |
993 | | - | |
994 | | - | |
995 | | - | |
996 | | - | |
997 | | - | |
998 | | - | |
999 | | - | |
1000 | | - | |
1001 | | - | |
1002 | | - | |
1003 | | - | |
1004 | | - | |
1005 | | - | |
1006 | | - | |
1007 | | - | |
1008 | | - | |
1009 | | - | |
1010 | | - | |
1011 | | - | |
1012 | | - | |
1013 | | - | |
1014 | | - | |
1015 | | - | |
1016 | | - | |
1017 | | - | |
1018 | | - | |
1019 | | - | |
1020 | | - | |
1021 | | - | |
1022 | | - | |
1023 | | - | |
1024 | | - | |
1025 | | - | |
1026 | | - | |
1027 | | - | |
1028 | | - | |
1029 | | - | |
1030 | | - | |
1031 | | - | |
1032 | | - | |
1033 | | - | |
1034 | | - | |
1035 | | - | |
1036 | | - | |
1037 | | - | |
1038 | | - | |
1039 | | - | |
1040 | | - | |
1041 | | - | |
1042 | | - | |
1043 | | - | |
1044 | | - | |
1045 | | - | |
1046 | | - | |
1047 | | - | |
1048 | | - | |
1049 | | - | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
1050 | 1030 | | |
1051 | | - | |
1052 | | - | |
1053 | | - | |
1054 | | - | |
1055 | | - | |
1056 | | - | |
1057 | | - | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
1058 | 1034 | | |
0 commit comments