Repository navigation
Commit c32bd82
fix(cli): objectstack build ships each functions key under its own name and code; hook and action refs resolve to their own code (#22901)
Fixes #22886
Clause-②: no
## Summary
The runtime resolves a hook's `handler`, an action's `target` and a
`functions` key against one merged namespace, `bundle.functions`, which
the runtime module (`objectstack-runtime.*.mjs`) fills. Two sides used
to name that namespace's entries, and they disagreed:
- `lowerCallables` minted refs in walk order (hooks, then actions, then
`functions`), deduplicated them by function identity, and re-keyed each
`functions` entry by the ref it got.
- The runtime-module template re-derived names from the config. It keyed
hooks by `name` and `functions` entries by their authored key, and never
reached an action's `target` or a `packages[i].manifest` body.
This PR makes the build's ref assignment the one reading (the triage
direction in force):
- **`lower-callables.ts`**: every authored `functions` name (a map key,
or an array entry's `name`) is taken before any internal ref is minted,
and is always its own ref, bound to its own callable. A hook or action
ref is internal. On an equal name it is the one that takes the `__2`
suffix; when it binds the same function as a `functions` entry, it
re-uses that entry's name. Nothing is refused (no new build refusal).
- **`build-runtime.ts`**: the template no longer derives names.
`buildRuntimeBundle` takes `lowerCallables`' `functions` (ref →
callable) and the loaded config. `locateCallables` finds each callable
in the config by identity (breadth-first, shortest path). The template
then exports exactly those refs, each bound to the callable at its path.
A top-level key resolves the way `loadConfig` merged it: the default
export's own keys first, then named exports.
- **`compile.ts`**: the one call site passes `callables:
lowering.functions, loadedConfig: config` instead of `refs:
Object.keys(lowering.functions)`. See the deviation below.
## Measured through `objectstack build` and then `loadArtifactBundle`
Both trees were run through a scratch probe: every registration site ×
{a function shared with a `functions` entry, a name equal to a
`functions` key}. Each callable names a module-scope constant, so the
ref is the path that runs.
| fixture | `main` `2e11d9006c` | this branch |
|:--|:--|:--|
| hook, shared function (shape i) | `functions: {kc_hook}`, authored
`kc_fn` gone | `functions: {kc_fn}`, hook ref `kc_fn` answers its own
code |
| hook, equal name (shape ii) | `kc_same__2` bare string; hook ref
`kc_same` answers the `functions` entry's code | `kc_same` answers the
entry; hook ref `kc_same__2` answers the hook |
| object action, shared / equal | authored key gone / re-keyed; action
ref unresolved / answers the entry | each authored key and each action
ref answers its own code |
| global action, shared / equal | same as the object action | same as
the object action |
| two map keys on one function (bare, declared) | second key gone | both
keys ship, both answer |
| two array names on one function | second entry renamed to the first's
name | both names ship, both answer |
| CONTROL, no collision | artifact refs as pinned below; both action
refs **unresolved** | artifact byte-identical apart from the module's
content hash; every ref answers its own code |
| `functions` map exported by name beside the default export | ships
`kc_named` as a bare string | `kc_named` answers its own code |
The last two rows are shipped defects this change touches, so they are
fixed here: the template never carried an action's callable, and it read
the default export only. Neither involves a collision.
## Deviation: `compile.ts` is outside the claim's file surface
The claim's surface is `lower-callables.ts`, `build-runtime.ts`, their
tests and the changeset. The decision in force says the template reads
the ref assignment from `lowerCallables`' output. Measured:
`compile.ts:1070-1074` is the one path from `lowerCallables` to
`buildRuntimeBundle`, and it carried only
`Object.keys(lowering.functions)`, the ref names with no callable. That
falsifies the PM's mechanism assumption 2 ("whether a ref →
authored-name map already exists in its output or has to be added"). It
has to be added, and it has to cross that call site.
The edit is the call site's arguments only (+2/−1). There is no new
call, so `validate-build-gate-parity.test.ts`'s closed roster of
`compile.ts` calls is untouched. Following the role file's rule for a
falsified mechanism assumption (switch route by the decision's intent)
conflicts with the dispatch's "stop on any other breach". The report
names the conflict for the seat to ratify.
## Tests
All at `13401ef927`; the final head `9736807dde` differs from it only in
the changeset text (+2/−2).
- **Enumeration pin, new**:
`packages/cli/test/build-callable-names.test.ts`, 13/13 passed.
- The census reads every `register…(` call and every
`lowerActionCallable(` call site off `lower-callables.ts` with the
TypeScript AST. Its derived kinds are `hook`, `object-action`,
`global-action`, `functions-map`, `functions-map-declared`,
`functions-array-named` and `functions-array-nameless`, and it requires
one fixture per derived kind (closed both ways).
- Each fixture round-trips through `objectstack build` and
`loadArtifactBundle`.
- Plus the CONTROL, the named-export case and two `locateCallables` unit
cases.
- `test/lower-callables.test.ts`: the test that pinned shape (ii) (hook
kept `dup`, authored key became `dup__2`) is rewritten to the decided
invariant. Four unit cases were added: shared function, two keys on one
function, a pass-through key still owns its name, and the ADR-0130 D4
package re-walk agreeing.
- `test/build-runtime-bundle-bodyless.test.ts`: `hook-and-function`
(shape i itself) now asserts `functions: { pb_score: 'pb_score' }`
instead of "one key resolves".
- `pnpm --filter @objectstack/cli typecheck`: exit 0, test layer
included. The 3 touched test files are in `tsconfig.test.json`'s program
and the 3 touched sources in `tsconfig.json`'s, counted with
`--listFilesOnly`.
- `vitest run --project unit`: 283/283 files.
- 281 passed on the full run.
- 2 (`published-subpath-*.pin`) refused because `packages/cli` was not
built; after `pnpm --filter @objectstack/cli build` they pass, 29/29.
- `vitest run --project integration` (run locally, since this diff
touches integration-tier files and the build path): 112/112 files, 1020
passed, 2 skipped, `VERDICT command-exit 0`.
**Reverse verification** (one-time, no permanent file):
- The three `src` files were restored to `BASE` in the working tree only
(`git restore --source=2e11d9006c`), under an `EXIT INT TERM` trap that
restores from `HEAD`.
- Landed on disk: `registerAuthored` count 0, `locateCallables` count 0,
the old `refs:` line count 1, and all 3 tree hashes equal the `BASE`
blobs.
- The new pin went **9 failed / 0 passed**. Examples: `hook: expected [
'kc_shared_hook', 'kc_same__2' ] to deeply equal [ 'kc_shared_fn',
'kc_same' ]`; CONTROL `expected 'unresolved: undefined' to be
'action:kc'`; the census named `register(value)` unclassified.
- Restore proven: 3/3 hashes equal the `HEAD` blobs, `git diff HEAD`
empty, status clean.
- The CLI runs from source through `bin/run-dev.js` and tsx, so no
`dist/` rebuild was needed for either leg.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands` with no paths, at
`9736807dde`, equals the dispatch's list exactly (65 commands).
- All 65 ran at `9736807dde` and exited 0.
- `--ran` with recorded exit codes: `65 derived, 65 run, 0 NOT-MEASURED,
0 UNRUN`.
- `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3
(prerequisites: other packages unbuilt). They pass after `turbo run
build` of the named packages and the example closures
(`check-i18n-coverage: OK (13 config(s), 621 baselined untranslated
string(s), none new)`).
**Lint (proven narrowing, `pnpm lint` itself is CI's):**
1. Population, read from `eslint.config.mjs`: `**/*.{ts,…}` and
`packages/cli/src/**`. The 6 touched `.ts` files are all inside it (0
reported as ignored).
2. Count, read from `--format json`: 6 results, 0 errors, 0 warnings.
3. Invariance: the config never enables type-aware linting (no
`parserOptions.project`, no typed rules; its own comment at about line
327 says so), so this diff cannot move a verdict on any untouched file.
## Acceptance notes
- **The array form accepts two entries authoring one `name`** (measured:
`objectstack build` exit 0 on both trees).
- It is one name for two codes in one flat namespace, not a collision
between kinds, so the pin records it as having no equal-name fixture,
with that reason.
- On `main` the second entry shipped as an uncallable `kc_a__2`. Here it
ships callable, but still under the suffixed name.
- A real fix is a refusal at the schema (the `domain:spec` seat's). It
is reported in the dev report rather than worked around here.
- **Two `packages[i].manifest` bodies binding one authored `functions`
name to different code** cannot share one flat runtime-module namespace.
The later one keeps today's suffixed ref, consistently across walks. No
door producing that shape was measured.
- `LoweringResult.count` now counts refs, so two authored keys on one
function count 2. Its readers are only the build's "Bundling N handlers"
line and `--json`'s `handlersBundled`.
## Size
7 files, +738 / −65 = 803 lines against the merge base `45e206e498`:
| file | + | − |
|:--|--:|--:|
| `test/build-callable-names.test.ts` (new) | 400 | 0 |
| `src/utils/build-runtime.ts` | 128 | 42 |
| `src/utils/lower-callables.ts` | 127 | 15 |
| `test/lower-callables.test.ts` | 59 | 3 |
| `.changeset/22886-functions-key-keeps-authored-name.md` | 18 | 0 |
| `test/build-runtime-bundle-bodyless.test.ts` | 4 | 4 |
| `src/commands/compile.ts` | 2 | 1 |
Dev session `session_01VoSxBQujKLZKPwK2u5ehQ6` (`domain:cli` seat 2
dispatch, mode:subagent).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3f69471 commit c32bd82
7 files changed
Lines changed: 738 additions & 65 deletions
File tree
- .changeset
- packages/cli
- src
- commands
- utils
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1069 | 1069 | | |
1070 | 1070 | | |
1071 | 1071 | | |
1072 | | - | |
| 1072 | + | |
| 1073 | + | |
1073 | 1074 | | |
1074 | 1075 | | |
1075 | 1076 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
25 | | - | |
26 | | - | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
27 | 39 | | |
28 | 40 | | |
29 | 41 | | |
| |||
46 | 58 | | |
47 | 59 | | |
48 | 60 | | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
54 | 83 | | |
55 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
56 | 154 | | |
57 | 155 | | |
58 | 156 | | |
59 | 157 | | |
60 | 158 | | |
61 | | - | |
62 | 159 | | |
63 | 160 | | |
64 | | - | |
| 161 | + | |
65 | 162 | | |
66 | | - | |
| 163 | + | |
| 164 | + | |
67 | 165 | | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
78 | 170 | | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
95 | 181 | | |
96 | 182 | | |
97 | 183 | | |
98 | 184 | | |
99 | | - | |
100 | | - | |
| 185 | + | |
101 | 186 | | |
102 | 187 | | |
103 | | - | |
| 188 | + | |
104 | 189 | | |
105 | 190 | | |
106 | 191 | | |
107 | 192 | | |
108 | 193 | | |
109 | | - | |
| 194 | + | |
110 | 195 | | |
| 196 | + | |
111 | 197 | | |
112 | 198 | | |
113 | 199 | | |
| |||
116 | 202 | | |
117 | 203 | | |
118 | 204 | | |
119 | | - | |
| 205 | + | |
120 | 206 | | |
121 | 207 | | |
122 | 208 | | |
| |||
0 commit comments