@lifeaitools/rdc-skills 0.37.7 → 0.37.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,9 +56,14 @@ That is a **scoped grant**, not a readout: `permittedActions` and
56
56
  to "which checkout do I work in" — it already knows pooled repos want a lane.
57
57
 
58
58
  **Unresolved is a result, not an error.** `open regen-root` returns
59
- `weDoNotHold: ["product_class"]`; `open fsm` returns `unknown_slug`. Register the
60
- target, then open it again — never guess a default and never fall back to
61
- hand-rolled git.
59
+ `weDoNotHold: ["product_class"]`; `open fsm` returns `unknown_slug`. An
60
+ `unknown_slug` — nothing registered at all — means `rdc:new` next: it
61
+ scaffolds and registers a product of the declared class, then hands back to
62
+ this skill to confirm. A `weDoNotHold` entry on an otherwise-known slug is
63
+ narrower — `rdc-harness new <slug> --environment ...` (also wrapped by
64
+ `rdc:new`) fills in just the missing piece. Either way: register the target,
65
+ then open it again — never guess a default and never fall back to hand-rolled
66
+ git.
62
67
 
63
68
  **Two targets at once is two calls.** Each returns its own `bindingId` and its
64
69
  own `sourceBoundary`; nothing is exclusive. Working `clauth` and `rdc-harness`
@@ -156,14 +161,46 @@ Downstream verbs re-resolve from the registry by slug.
156
161
  2. **Target.** Read `productClass`, `workModel`, `delivery` and `weDoNotHold`
157
162
  from the same result. `monorepo_path` NULL means a standalone repo, not in
158
163
  this tree. Do not re-query the registry for facts the handle already carries.
159
- 3. **Harness.** Name the shape the target's class implies:
160
-
161
- | class | build | dev | prod |
162
- |---|---|---|---|
163
- | `apps/<name>` | `pnpm --filter @regen/<name> build` | PM2 @ Vultr | Coolify |
164
- | `packages/<name>` | `pnpm --filter @regen/<name> build` | imported | `npm publish` |
165
- | `sites/`, `models/` | static/vite | PM2 @ Vultr | Coolify |
166
- | standalone | its own tooling | — | Coolify |
164
+ 3. **Harness.** Name the shape the target's class implies. The routes come
165
+ from the RPMS kind registry (`rpms_kinds` → `rpms_usages`); this table is
166
+ **generated from `rpms_kinds`** and never hand-edited. A new kind is one
167
+ row via `register_rpms_kind`, then (in rdc-skills)
168
+ `node scripts/gen-rpms-kind-tables.mjs --from <dump.json>`. Full guide to
169
+ every level, not just product classes: `docs/architecture/KINDS.md` in
170
+ regen-root.
171
+
172
+ <!-- BEGIN GENERATED: rpms-kinds -->
173
+ _Generated from `rpms_kinds` + `rpms_usages` (schema `rpms.kinds.v1`) by `scripts/gen-rpms-kind-tables.mjs` — do not hand-edit. Product-level kinds only; every other level is in `select rpms_kinds_snapshot()`._
174
+
175
+ | kind | home | build · dev (`ship_route`) | runtime | prod (`deploy_route`) |
176
+ |---|---|---|---|---|
177
+ | `app` | `apps/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
178
+ | `artifact` | `artifacts/<slug>/` | `artifact-export` | — | — (no-production-target) |
179
+ | `bpmn-process` | `docs/systems/<system>/bpmn/` | `flowable-dev-release` | `java` | `flowable-release` |
180
+ | `city-tool` | `apps/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
181
+ | `corpus` | `$CORPUS_ROOT/<CO>/docs/` | `content-write` | — | — (content-not-code) |
182
+ | `dmn-decision` _(proposed)_ | `docs/systems/<system>/dmn/` | `dmn-test-cases` | `java` | `flowable-dmn-release` |
183
+ | `documentation` | `docs/<slug>/` | `artifact-export` | — | — (no-production-target) |
184
+ | `dsl` _(proposed)_ | `packages/<pkg>/src/dsl/` | `grammar-suite` | — | — (production-is-registry) |
185
+ | `education-piece` | `artifacts/<slug>/` | `artifact-export` | — | — (no-production-target) |
186
+ | `grammar` | `packages/<pkg>/src/grammar/` | `grammar-suite` | — | — (production-is-registry) |
187
+ | `mcp` | `mcp-servers/<slug>/` | `plugin-registration` | `ts-server` | `declared-service-route` |
188
+ | `model` | `models/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
189
+ | `ontology` | `packages/planetary-ontology/src/` | `grammar-suite` | — | — (production-is-registry) |
190
+ | `package` | `packages/<slug>/` | `registry-release` | — | — (production-is-registry) |
191
+ | `prototype` | `artifacts/<slug>/` | — | — | — (no-production-target) |
192
+ | `share-app` | `apps/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
193
+ | `site-html` | `sites/<slug>/` | `static-development` | `static-html` | `declared-promotion` |
194
+ | `site-ts` | `sites/<slug>/` | `static-development` | `static-html` | `declared-promotion` |
195
+ | `skill` | `skills/<slug>/` | `local-install` | — | `release-tag` |
196
+ | `theme` | — | `content-write` | — | — (content-not-code) |
197
+ | `vendor` | `vendor/<slug>/` | `registry-release` | — | — (production-is-registry) |
198
+ | `worker` | `workers/<slug>/` | `edge-dev` | — | `edge-deploy` |
199
+ <!-- END GENERATED -->
200
+
201
+ The concrete build command is the target's own (`pnpm --filter <pkg>
202
+ build` in a monorepo, its own tooling standalone); the route names what
203
+ kind of ship and deploy it owes.
167
204
 
168
205
  4. **Visual-test contract (for an app/site with UI work).** Read the target
169
206
  `package.json` and name its `test:ui` command and Playwright config. A
@@ -216,6 +253,8 @@ Downstream verbs re-resolve from the registry by slug.
216
253
 
217
254
  ## Related
218
255
 
256
+ - `rdc:new` — an `unknown_slug` or a `weDoNotHold` gap routes here: scaffold and
257
+ register a product, or just its deployment environment, then re-open.
219
258
  - `guides/work-contract.md` — the contract this skill opens: rows, tiers, proving,
220
259
  staleness, and what Stop checks.
221
260
  - `rdc:flow` — declares what KIND of work this is. `rdc-work start --type` sets it
@@ -36,38 +36,143 @@ codex login status
36
36
  codex features list
37
37
  ```
38
38
 
39
- Requirements for size to apply:
40
-
41
- 1. Use `--enable image_generation`.
42
- 2. Use `-c model=gpt-5.5`.
43
- 3. Instruct Codex: `use the built-in image_gen tool DIRECTLY; do not write API keys, scripts, or MCP`.
44
- 4. Name the target size in the instruction, such as `2048x1152`, `2048x2048`, or `3840x2160`.
39
+ ### Parameters are not interchangeable
40
+
41
+ Checked 2026-09-26 against official documentation, installed Codex 0.153.4,
42
+ the bundled imagegen CLI, and regen-media's `generateLocalSchema`.
43
+
44
+ | Input | Keyless Codex / `generate_local` | Bundled API script |
45
+ |---|---|---|
46
+ | Description | `prompt`; describe the requested change for edits | `--prompt` or `--prompt-file` |
47
+ | Style, tones, lighting | Named sections inside `prompt` | `--style`, `--palette`, `--lighting`; these augment the prompt |
48
+ | Corpus information | Read selected authorized sources first; include relevant facts and source references in the brief | Same; `--prompt-file` can contain the assembled brief. No automatic corpus retrieval flag |
49
+ | Image to update | `mode: "edit"`, `source_image`; direct Codex uses `-i` | `edit --image <path>`; repeat for additional references |
50
+ | Size | `size` currently becomes a prompt request, NOT a guaranteed native tool parameter | `--size 3840x2160` is sent as an API parameter |
51
+ | Quality / output format | Only pass native fields if the current built-in tool schema exposes them; do not invent flags | `--quality high --output-format png` |
52
+ | Output location | `generate_local` has `out_name`, not `output_dir`; direct Codex copies its generated artifact to the requested destination | `--out <file>` or `--out-dir <directory>` |
53
+ | Model | `model` / `-m` selects the Codex reasoning model, not an image-model parameter | `--model gpt-image-2` selects the image model |
54
+
55
+ The CLI's `--enable image_generation`, `-m`, `-i`, and `--cd` enable the tool,
56
+ select the reasoning model, attach input images, and choose the workspace.
57
+ They do not establish output dimensions. `--output-last-message` saves text,
58
+ not an image. Do not invent `codex exec --size`, `--quality`, or a TOML image-size
59
+ setting: use only fields exposed by the installed command/tool schema.
60
+
61
+ The former four-step recipe was not a size guarantee. On 2026-09-26, two local
62
+ media calls and a direct `gpt-5.5` call with those exact instructions requested
63
+ 3840x2160 but produced 1672x941. The available built-in schema exposed a prompt
64
+ and reference images, not a native `size` field. Treat this as observed behavior
65
+ of that environment, not a permanent limit of all Codex versions.
66
+
67
+ Retain the user's requested dimensions in the brief, inspect the current tool
68
+ schema, then measure the saved output. If a hard size requirement is missed,
69
+ report it as unmet; do not repeat identical requests, silently resample, or
70
+ silently switch to a paid API. A re-render can change composition and is not a
71
+ lossless upscale.
45
72
 
46
73
  Text to image:
47
74
 
48
75
  ```bash
49
- echo "Use the built-in image_gen tool DIRECTLY at size 3840x2160 (4K). No API keys/scripts/MCP. Copy the PNG to output/imagegen/<name>.png and reply with only that path + pixel dims. Prompt: <dense art-directed prompt>." | codex exec --enable image_generation --dangerously-bypass-approvals-and-sandbox --cd "$PWD" -c model=gpt-5.5
76
+ echo 'Use $imagegen and the built-in image_gen tool directly. Generate the described image, requested size 3840x2160. No API or MCP fallback. Use native size only if exposed by the tool schema. Copy the full-resolution output from this run to .rdc/tmp/imagegen/hero-v2.png. Measure and report actual dimensions; report any size mismatch without resizing. Brief: <description, style, palette, lighting, composition, constraints>.' | codex exec --enable image_generation --cd "$PWD" -m gpt-5.5 -
50
77
  ```
51
78
 
52
79
  Image edit, re-render, or upscale:
53
80
 
54
81
  ```bash
55
- echo "Use the built-in image_gen tool DIRECTLY to re-render the ATTACHED image at size 3840x2160 (4K), preserving its composition, subject, palette, and lighting. No API keys/scripts/MCP. Save to output/imagegen/<name>-4k.png and reply with only the path + dims." | codex exec -i "input/source.png" --enable image_generation --dangerously-bypass-approvals-and-sandbox --cd "$PWD" -c model=gpt-5.5
82
+ echo 'Use $imagegen and the built-in image_gen tool directly to edit the attached image. Change: <requested change>. Preserve: <invariants>. Request 3840x2160; do not claim that size unless measured. No API or MCP fallback. Copy the output to .rdc/tmp/imagegen/hero-v2.png; report path and actual dimensions, without resizing.' | codex exec -i "input/source.png" --enable image_generation --cd "$PWD" -m gpt-5.5 -
56
83
  ```
57
84
 
58
85
  Notes:
59
86
 
60
- - The keyless path uses the local ChatGPT Pro OAuth in the Codex install, not `OPENAI_API_KEY`.
61
- - Output may land both in the requested path and under `~/.codex/generated_images/<session-id>/ig_*.png`.
87
+ - These examples retain configured approval and sandbox settings. Do not disable safeguards merely to generate an image. `gpt-5.5` is an example previously used locally, not a documented requirement for all Codex versions.
88
+ - The keyless path uses the local ChatGPT login, not `OPENAI_API_KEY`, and consumes included Codex usage limits.
89
+ - Built-in output lands under `$CODEX_HOME/generated_images/`; copy the specific output belonging to this run. Never recover the newest image across unrelated sessions.
62
90
  - Use a long timeout or background session; generation commonly takes 2-5 minutes.
63
- - Valid `gpt-image-2` sizes include `1024x1024`, `1536x1024`, `1024x1536`, `2048x2048`, `2048x1152`, `3840x2160`, `2160x3840`, and `auto`, subject to model constraints.
91
+ - API-supported sizes do not prove that Codex exposes matching controls.
92
+
93
+ ## Calling regen-media-local
94
+
95
+ Use its MCP `generate_local` when the caller requested that service. Discover
96
+ its current tool schema rather than assuming local and remote servers expose
97
+ the same tools. The inspected implementation accepts this shape:
98
+
99
+ ```json
100
+ {
101
+ "prompt": "Purpose: RDC masthead. Change: replace the boy with a community square. Style: cinematic landscape. Palette: warm amber and deep forest green. Composition: elevated valley view, distant mountains, quiet upper-left copy area. Preserve: river and overall lighting. Context: selected authorized place facts from the brief; do not invent measured outcomes.",
102
+ "mode": "edit",
103
+ "source_image": "C:/path/to/original.png",
104
+ "size": "3840x2160",
105
+ "out_name": "rdc-masthead-town-square-v2",
106
+ "model": "gpt-5.5",
107
+ "auto_upload": false
108
+ }
109
+ ```
110
+
111
+ This is the tool's argument object, not a shell command. For a fresh image use
112
+ `mode: "text_to_image"` and omit `source_image`. The current service resolves
113
+ output beneath `<CODEX_CD>/output/imagegen/<prefix>/<out_name>.png`; it does not
114
+ accept a per-call output directory. Do not silently change shared `CODEX_CD`.
115
+ Copy a selected result to the caller's authorized project destination afterward.
116
+ Its returned `size` is the request; `width` and `height` are measurements. A
117
+ success message alone does not prove exact size. Keep `auto_upload: false` until
118
+ size and visual checks pass. Do not label a mismatched file as native 4K.
119
+
120
+ ## Explicit parameterized API script
121
+
122
+ Codex's installed imagegen skill includes `scripts/image_gen.py`. This is a real
123
+ script accepting a parameter set and an output directory, but it is a separate,
124
+ credentialed API path, not the keyless Codex image tool or `generate_local`.
125
+ Use only after the user chooses that path. Obtain credentials through the
126
+ environment's approved vault mechanism; never ask for keys in chat. A request
127
+ to research parameters or update this skill is not permission for a paid run.
128
+
129
+ Read the installed imagegen CLI reference and `--help` before using it. Example
130
+ PowerShell call, with `--dry-run` so no key, network or generation is needed:
131
+
132
+ ```powershell
133
+ $imageGenScript = Join-Path $env:USERPROFILE '.codex/skills/.system/imagegen/scripts/image_gen.py'
134
+ python $imageGenScript edit `
135
+ --model gpt-image-2 `
136
+ --image 'C:/path/to/original.png' `
137
+ --prompt-file 'C:/path/to/approved-brief.txt' `
138
+ --style 'cinematic landscape' `
139
+ --palette 'warm amber, deep forest green, blue mountains' `
140
+ --lighting 'sunset with warm community lights' `
141
+ --composition 'elevated valley, distant mountains, quiet upper-left copy area' `
142
+ --constraints 'preserve river; no boy, text or network lines' `
143
+ --size 3840x2160 --quality high --output-format png `
144
+ --out-dir 'C:/path/to/approved-output-directory' `
145
+ --dry-run
146
+ ```
147
+
148
+ For generation, replace `edit` with `generate` and omit `--image`. For a stable
149
+ filename use `--out` instead of `--out-dir`. Respect `CODEX_HOME` if configured;
150
+ the example uses the default Windows installation path. Do not modify the
151
+ bundled script or build another wrapper for parameters it already supports.
152
+
153
+ Corpus content is not an API parameter. Read only the requested sources, select
154
+ relevant authorized facts, retain source references alongside the brief, and
155
+ state which are conceptual rather than observed. Never bulk-send a corpus or
156
+ claim that mentioning a file path makes the generator read its contents.
157
+
158
+ For `gpt-image-2`, size is `auto` or constrained `WIDTHxHEIGHT`: edges divisible
159
+ by 16, maximum edge 3840, ratio at most 3:1, and 655360–8294400 total pixels.
160
+ Above 2560x1440 is experimental. API output still requires dimension and visual
161
+ checks. Style/palette/lighting are prompt augmentation, not separate model
162
+ rendering controls. `input_fidelity` is not supported for `gpt-image-2`.
163
+
164
+ Sources (checked 2026-09-26):
165
+ - [Codex image generation and reference-image guidance](https://learn.chatgpt.com/docs/image-generation)
166
+ - [Codex CLI flags](https://developers.openai.com/codex/cli/reference)
167
+ - [Responses image tool parameters](https://developers.openai.com/api/docs/guides/tools-image-generation)
168
+ - [Image API and resolution constraints](https://developers.openai.com/api/docs/guides/image-generation)
64
169
 
65
170
  ## Upload to Regen Media
66
171
 
67
172
  After generation, upload finished images through the project uploader when available:
68
173
 
69
174
  ```powershell
70
- powershell.exe -ExecutionPolicy Bypass -File "{PROJECT_ROOT}/scripts/upload-to-media.ps1" "output/imagegen/<name>.png"
175
+ powershell.exe -ExecutionPolicy Bypass -File "{PROJECT_ROOT}/scripts/upload-to-media.ps1" ".rdc/tmp/imagegen/<name>.png"
71
176
  ```
72
177
 
73
178
  The uploader obtains `regen-media-api` from clauth and posts to Regen Media. Never print credential values. If upload is not required, return the local artifact path and dimensions.
@@ -81,15 +186,15 @@ Use the deployed regen-media MCP/API generation path only when the local Codex i
81
186
  1. Classify the task: `generate`, `edit`, `upscale`, or `upload`.
82
187
  2. If `RDC_TEST=1`, print the selected path and planned commands without executing external calls.
83
188
  3. For generation/edit/upscale, attempt the local keyless Codex preflight.
84
- 4. Build the Codex `exec` command with the requested size and explicit built-in `image_gen` instruction.
85
- 5. Save to `output/imagegen/` or a caller-provided path.
86
- 6. Verify the file exists and record pixel dimensions.
189
+ 4. Use the requested service, keyless Codex command, or explicitly chosen API script; distinguish prompt fields from supported native parameters.
190
+ 5. Save/copy non-destructively to the caller-provided path, or `.rdc/tmp/imagegen/` for Regen Root scratch. Preserve the original.
191
+ 6. Verify this run's file exists, measure actual pixel dimensions, compare with requested size, and inspect composition. Stop and report unmet constraints on mismatch.
87
192
  7. If the artifact should be managed, upload it to Regen Media and return the media URL/slug plus local path.
88
- 8. If local Codex is unavailable, use the server-side regen-media generation fallback and clearly say that it is the fallback path.
193
+ 8. If local Codex is unavailable or cannot meet a hard requirement, explain the credentialed fallback and obtain the user's choice before invoking it.
89
194
 
90
195
  ## Boundaries
91
196
 
92
197
  - Do not write or expose API keys.
93
198
  - Do not silently downgrade from local keyless `gpt-image-2` to server-side MCP/API generation.
94
- - Do not use prompt-only size hints; always set the Codex command and instruction shape above.
199
+ - Do not present prompt-only size requests as native parameters or guarantee dimensions from CLI flags alone.
95
200
  - Do not run generation or upload in test mode.
@@ -0,0 +1,113 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { treesitterPlugin } from '../../scripts/lib/plugins/treesitter.mjs';
4
+ import { srp } from '../../scripts/lib/solid-scoring.mjs';
5
+
6
+ let fixture = 0;
7
+ function inspect(body, prefix = 'const registry = { a: A };') {
8
+ const [unit] = treesitterPlugin.extractUnits(`/jsx-reference-${fixture++}.tsx`,
9
+ `function A() { return null; } ${prefix} function B() { ${body} }`);
10
+ return { calls: unit.members.find(m => m.name === 'B').calls, score: srp(unit).score };
11
+ }
12
+
13
+ test('direct call and opening/self-closing JSX have the same local dependency', () => {
14
+ for (const body of ['return A();', 'return <A/>;', 'return <A></A>;']) {
15
+ const result = inspect(body);
16
+ assert.ok(result.calls.includes('A'));
17
+ assert.equal(result.score, 100);
18
+ assert.equal(result.calls.filter(c => c === 'A').length, 1);
19
+ }
20
+ });
21
+
22
+ test('member JSX and const aliases resolve a statically selected registry entry', () => {
23
+ for (const body of ['return <registry.a/>;', 'const C = registry.a; return <C/>;',
24
+ 'const C = registry["a"]; return C();', 'const C = registry.a; const D = C; return <D/>;']) {
25
+ assert.ok(inspect(body).calls.includes('A'), body);
26
+ }
27
+ });
28
+
29
+ test('external member JSX retains receiver; intrinsic tags are not component calls', () => {
30
+ const { calls } = inspect('return <><UI.A/><a/><div/></>;', '');
31
+ assert.deepEqual(calls, ['UI.A']);
32
+ });
33
+
34
+ test('dynamic lookup, spread, reassignment, parameter shadow and cycles stay unresolved', () => {
35
+ const cases = [
36
+ ['const C=registry[key]; return <C/>;', 'const registry=getRegistry();'],
37
+ ['const C=registry.a; return <C/>;', 'const registry={a:A,...other};'],
38
+ ['registry.a=other; const C=registry.a; return <C/>;', 'const registry={a:A};'],
39
+ ['const C=registry.a; return <C/>;', 'let registry={a:A};'],
40
+ ['const C=registry.a; return <C/>;', 'const registry={a:A,[key]:other};'],
41
+ ['const C=registry.a; return <C/>;', 'const registry={a:A,a:other};'],
42
+ ['consume(registry); const C=registry.a; return <C/>;', 'const registry={a:A};'],
43
+ ['const C=D; const D=C; return <C/>;', ''],
44
+ ['function nested(registry){ const C=registry.a; return <C/>; } return nested({});', 'const registry={a:A};'],
45
+ ['function nested(A){ const registry={A}; const C=registry.A; return <C/>; } return nested(other);', ''],
46
+ ['const {registry}=other; return <registry.a/>;', 'const registry={a:A};'],
47
+ ['consume({registry}); return <registry.a/>;', 'const registry={a:A};'],
48
+ ['(registry.a)=Other; return <registry.a/>;', 'const registry={a:A};'],
49
+ ['[registry.a]=[Other]; return <registry.a/>;', 'const registry={a:A};'],
50
+ ['for(const A of others){return <A/>;}', ''],
51
+ ['function A(){return null;} return <A/>;', ''],
52
+ ['const f=function A(){return <A/>;}; return f();', ''],
53
+ ['const f=class A { render(){return <A/>;} }; return f;', ''],
54
+ ];
55
+ for (const [body, prefix] of cases) assert.ok(!inspect(body, prefix).calls.includes('A'), body);
56
+ });
57
+
58
+ test('unused registry values do not create call edges', () => {
59
+ assert.equal(inspect('return null;').score, 70);
60
+ });
61
+
62
+ test('const assertions and shorthand registry entries retain the selected reference', () => {
63
+ assert.ok(inspect('return <registry.A/>;', 'const registry={A} as const;').calls.includes('A'));
64
+ });
65
+
66
+ test('JSX parameter shadows do not connect an unrelated module function', () => {
67
+ const [unit] = treesitterPlugin.extractUnits('/jsx-param-shadow.tsx', 'function A(){return null;} function B(A){return <A/>;}');
68
+ assert.deepEqual(unit.members.find(member => member.name === 'B').calls, []);
69
+ assert.equal(srp(unit).score, 70);
70
+ });
71
+
72
+ test('closed registry dispatch records possible call targets, as explicit branches do', () => {
73
+ const prefix = 'function A(){return null;} function B(){return null;} function Fallback(){return null;} const registry={a:A,b:B};';
74
+ for (const body of [
75
+ 'const C=registry[key] ?? Fallback; return <C/>;',
76
+ 'const C=registry[key] ?? Fallback; return C();',
77
+ ]) {
78
+ const [unit] = treesitterPlugin.extractUnits(`/closed-dispatch-${fixture++}.tsx`, `${prefix} function Render(key){${body}}`);
79
+ const calls = unit.members.find(member => member.name === 'Render').calls;
80
+ for (const target of ['A', 'B', 'Fallback']) assert.ok(calls.includes(target), `${body}: ${target}`);
81
+ assert.equal(srp(unit).score, 100);
82
+ }
83
+ });
84
+
85
+ test('literal selection does not connect unselected registry entries', () => {
86
+ const [unit] = treesitterPlugin.extractUnits('/literal-selection.tsx',
87
+ 'function A(){return null;} function B(){return null;} const registry={a:A,b:B}; function Render(){const C=registry["a"];return <C/>;}');
88
+ assert.deepEqual(unit.members.find(member => member.name === 'Render').calls, ['A']);
89
+ assert.equal(srp(unit).score, 70);
90
+ });
91
+
92
+ test('dynamic registry analysis preserves mutation, escape, shadow and open-object negatives', () => {
93
+ const negatives = [
94
+ ['const registry=getRegistry();', ''],
95
+ ['let registry={a:A};', ''],
96
+ ['const registry={a:A,...other};', ''],
97
+ ['const registry={a:A,[key]:Other};', ''],
98
+ ['const registry={a:A,a:Other};', ''],
99
+ ['export const registry={a:A};', ''],
100
+ ['const registry={a:A};', 'consume({registry});'],
101
+ ['const registry={a:A};', '(registry.a)=Other;'],
102
+ ['const registry={a:A};', '[registry.a]=[Other];'],
103
+ ['const registry={a:A};', 'const {registry}=other;'],
104
+ ['A=Other; const registry={a:A};', ''],
105
+ ['const registry={a:A};', 'for (registry.a of others) {}'],
106
+ ['const registry={a:A};', 'for (registry.a in other) {}'],
107
+ ];
108
+ for (const [prefix, before] of negatives) {
109
+ const result = inspect(`${before} const C=registry[key] ?? ExternalFallback; return <C/>;`, prefix);
110
+ assert.ok(!result.calls.includes('A'), `${prefix} ${before}`);
111
+ assert.ok(result.calls.includes('ExternalFallback'));
112
+ }
113
+ });