Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/vibe-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ internal/vibe-tests/
├── setup-test/ # Setup evaluation over the canonical fixture matrix
│ ├── PLAN.md # controls, pilot stages, measures, decision rule
│ ├── matrix.json # fixtures × controls × prompts × bundles × reps
│ ├── conditions.json # controls plus the opt-in established-app strategies
│ ├── guidance/ # one document per patch, including both strategies
│ ├── run-setup.mjs # sandbox + task + provenance preparation only
│ ├── setup-interactions.mjs # marker-driven dialog and nested-overlay opener
│ ├── setup-measure.mjs # build + exact style, geometry, and overlay probes
Expand Down
370 changes: 355 additions & 15 deletions internal/vibe-tests/setup-test/PLAN.md

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions internal/vibe-tests/setup-test/conditions.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,87 @@
"role": "direct-control",
"patches": ["patch:pointer", "patch:existing-app", "patch:directed"],
"hypothesis": "Precise setup instructions remove discovery ambiguity. Any remaining visible host damage is a product gap and blocks instruction shipping."
},
{
"id": "host-aligned",
"label": "Host-aligned strategy (pilot)",
"role": "strategy-pilot",
"optIn": true,
"patches": ["patch:pointer", "patch:host-aligned"],
"hypothesis": "When the established host stays authoritative over typography and color, an app-owned theme that extends neutral and matches host fonts, semantic colors, and mode pairs avoids host damage that a stock theme stylesheet would cause."
},
{
"id": "guest-contained",
"label": "Guest-contained strategy (pilot)",
"role": "strategy-pilot",
"optIn": true,
"patches": ["patch:pointer", "patch:guest-contained"],
"hypothesis": "When the host owns global preflight and tokens, mounting the provider around only the new subtree and withholding the reset avoids host damage, at a portal-scope cost that must be measured rather than assumed."
},
{
"id": "guest-contained-r2",
"label": "Guest-contained strategy (pilot, revision 2)",
"role": "strategy-pilot",
"optIn": true,
"patches": ["patch:pointer", "patch:guest-contained"],
"supersedes": "guest-contained",
"revisionOf": "guest-contained",
"changedSincePriorRevision": "measurement only — the guest-contained document is byte-identical to the revision this supersedes. Two measurement faults it exposed were corrected: host container text now counts only text the browser renders, and probes are read after running CSS transitions settle. Both faults reported an unchanged host as damaged.",
"hypothesis": "The two guest-contained cells that failed the pilot failed on how the host was measured rather than on what the strategy did to the host, so the same document under a corrected measurement produces no host damage."
},
{
"id": "host-aligned-r2",
"label": "Host-aligned strategy (pilot, revision 2)",
"role": "strategy-pilot",
"optIn": true,
"patches": ["patch:pointer", "patch:host-aligned"],
"supersedes": "host-aligned",
"revisionOf": "host-aligned",
"changedSincePriorRevision": "document and measurement. The document states its non-negotiable rules before the reasoning and adds a section on host markup composed inside a design-system overlay. The rule against ignoring generated theme output was already written but is not present in any sandbox the prior revision produced, so that rule has never actually been tested.",
"hypothesis": "Told before the reasoning that generated theme output is committed, and told that relocated host markup must carry the host's own token-scoping hook, the executor produces no integrity failure and no host-surface style change."
},
{
"id": "guest-contained-r3",
"label": "Guest-contained strategy (pilot, revision 3)",
"role": "strategy-pilot",
"optIn": true,
"patches": ["patch:pointer", "patch:guest-contained"],
"supersedes": "guest-contained-r2",
"revisionOf": "guest-contained-r2",
"changedSincePriorRevision": "document only. The guest-contained document now names the supported pnpm 11 mechanism for approving a dependency's build script. The r2 run failed its build before any measurement could happen: the executor wrote the approval into package.json, which pnpm 11 does not read, so the install aborted with ERR_PNPM_IGNORED_BUILDS. Nothing about the strategy itself was measured on that cell.",
"hypothesis": "Told the mechanism pnpm 11 actually reads, the executor installs and builds, and the cell can finally be measured on what the strategy does to the host rather than on how it configured a package manager."
},
{
"id": "host-aligned-r3",
"label": "Host-aligned strategy (pilot, revision 3)",
"role": "strategy-pilot",
"optIn": true,
"patches": ["patch:pointer", "patch:host-aligned"],
"supersedes": "host-aligned-r2",
"revisionOf": "host-aligned-r2",
"changedSincePriorRevision": "document and measurement. The relocation rule is now one of the document's stated non-negotiables rather than a section far down it, and it carries a procedure for finding the boundary rather than an instruction to find one. The measurement now records which host token scopes a surface sits inside and reports a lost boundary as its own failure, so the r2 outcome is diagnosable instead of appearing only as a list of colours.",
"hypothesis": "The r2 executor carried the host's mode hooks onto the relocated menu but not the attribute scoping its palette, which is the specific mistake the procedure now addresses. Told to carry the boundary and how to find it, the executor keeps the relocated host surface inside its own token region and matching its reference exactly."
}
],
"strategyPilot": {
"conditions": [
"host-aligned",
"guest-contained",
"host-aligned-r2",
"guest-contained-r2",
"guest-contained-r3",
"host-aligned-r3"
],
"revisions": {
"$comment": "A revision is a new condition id, never an edit to an existing one. Earlier cells keep their ids, their measurements, and their verdicts exactly as recorded; a revision reruns a named subset beside them under its own identity so the two can be compared and neither can overwrite the other. Revisions chain: r3 supersedes r2, which supersedes the original, and each names the one it revises.",
"host-aligned-r2": "host-aligned",
"guest-contained-r2": "guest-contained",
"guest-contained-r3": "guest-contained-r2",
"host-aligned-r3": "host-aligned-r2"
},
"notes": "Opt-in established-app strategies explored additively. They do not replace floor, current, candidate, or direct, and they change no default or core behavior: greenfield adoption is unaffected. Each strategy condition carries the discovery pointer plus its own self-contained strategy document. Neither strategy is presented as universally correct.",
"supersedes": "A strategy document is self-contained and is not combined with patch:existing-app, because the two strategies deliberately disagree with parts of that recipe — host-aligned replaces the stock theme stylesheet, and guest-contained withholds the reset. Combining them would hand the executor contradictory instructions and make the measurement unreadable."
},
"measurement": {
"reference": "A pristine copied fixture, measured by the same code before setup.",
"schemes": ["light", "dark"],
Expand Down
148 changes: 148 additions & 0 deletions internal/vibe-tests/setup-test/guidance/guest-contained.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
## Strategy: guest-contained adoption

This app already owns its global stylesheet, its CSS reset, and its token
vocabulary. Adopt this strategy when the design system must live inside one new
or migrated region and leave the rest of the page untouched.

This is the exceptional strategy, not the established-app default. The normal
objective for an app like this one is host-aligned adoption — the host stays
authoritative over typography and color, and the design system is themed to it
through supported public APIs. Containment is for the narrower case where
leaving the rest of the page alone is a hard requirement of the work: a nested
or sub-scoped region inside an application whose global ownership cannot move.

It is also the strategy with no fully supported path today. Scoping the design
system to a subtree means suppressing the root attribute sync described below,
and there is no public API for that; portaled content rendered outside the
subtree then falls outside every scope root and loses its tokens. Both are
product gaps rather than choices this document can make well, and following this
strategy exercises them deliberately — what it produces is evidence about those
gaps as much as it is a recipe.

### The host keeps global ownership

- Do **not** import `@astryxdesign/core/reset.css`. The host already owns
preflight and element defaults; a second reset changes host elements.
- Do **not** import `@astryxdesign/core/tailwind-theme.css`, and do not change
the host's existing token definitions.
- Keep `@import 'tailwindcss';` exactly where it is. Declare the layer order
above it so the design system's layers sit between the host's base and the
host's components:

```css
@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;
```

- Verify the layer order in the **emitted** CSS rather than in source order.

### Installing the packages — approving the build script

`@astryxdesign/core` ships a `postinstall` script, and pnpm 11 does not run a
dependency's install scripts until that dependency is approved. It fails the
install rather than skipping quietly:

```
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @astryxdesign/core@0.5.2
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
```

Do what the error says:

```bash
pnpm install # fails with ERR_PNPM_IGNORED_BUILDS
pnpm approve-builds # --all to accept every pending package without prompting
```

That writes the approval into **`pnpm-workspace.yaml`**, as an `allowBuilds` map:

```yaml
allowBuilds:
'@astryxdesign/core': true
```

Two things about this are worth knowing before you hand-write anything:

- **pnpm 11 reads `allowBuilds` from `pnpm-workspace.yaml` only.** The same key
under a `pnpm` field in `package.json` is ignored, and the install fails
exactly as it did before — a confusing failure, because the setting is right
there in the file you edited. If you write it by hand rather than running
`approve-builds`, write it in `pnpm-workspace.yaml`, and create that file if
the project does not have one.
- **`ignoredBuiltDependencies` and `onlyBuiltDependencies` are not the fix
here.** Neither key clears this error in pnpm 11 in either file. Reach for
`approve-builds`, or the `allowBuilds` map it writes.

Setting a package to `false` also satisfies pnpm — it records a decision not to
build — but `@astryxdesign/core` expects its `postinstall` to run, so approve it
rather than suppressing it.

Prefer `pnpm-workspace.yaml` over `package.json` for another reason too: it is
pnpm's own file and npm and yarn ignore it, so approving a pnpm build leaves no
pnpm-specific configuration in the manifest those tools read.

### Mount the provider around the guest subtree only

Wrap only the new or migrated region:

```tsx
<Theme theme={neutralTheme} mode={hostMode}>
<NewRegion />
</Theme>
```

Do not wrap the application root, and do not wrap any pre-existing host UI.
`mode` follows the host's own light/dark state; the host keeps its mode control.

### Containing the root attribute sync — the honest state of this

A provider with no parent provider above it is a **root** provider, and a root
provider synchronizes `data-theme` and `data-astryx-theme` onto the document
element. Theme CSS is emitted as `@scope ([data-astryx-theme="<name>"]) to
(...)`, so that attribute on the document element makes the whole document a
scope root — which is exactly what defeats containment.

**There is no supported public API to disable that sync.** The provider's public
props are `theme`, `mode`, and `children`; none of them opt out, and there is no
nested-mode or scoped-root escape hatch. Do not invent a prop or reach into
internals.

The only mechanism available today is to remove the attribute from the document
element after mount and keep it removed with a `MutationObserver`:

```tsx
useEffect(() => {
const root = document.documentElement;
const strip = () => root.removeAttribute('data-astryx-theme');
strip();
const observer = new MutationObserver(strip);
observer.observe(root, {
attributes: true,
attributeFilter: ['data-astryx-theme'],
});
return () => observer.disconnect();
}, []);
```

State plainly in the code comment that this is a workaround for a missing
capability, not a supported configuration. Leave `data-theme` alone if the host
relies on it for `color-scheme`; strip only the theme-name attribute.

### The portal tradeoff — test it and write it down

`data-astryx-theme` on the document element is what lets `@scope`'d theme CSS
reach content rendered **outside** the provider's DOM subtree: portaled
overlays, dialogs, popovers, tooltips, and fallback toast viewports. Removing it
is therefore not free:

- content inside the provider's subtree stays themed;
- portaled design-system content rendered to `document.body` falls outside every
scope root and loses its theme tokens.

You must exercise this directly: open a portaled design-system overlay from
inside the guest subtree in both modes, observe the result, and record what
happens in a comment next to the workaround. If the region needs portaled
overlays, say so explicitly — for that region, containment and themed portals
cannot both hold today, and the choice belongs to the app.

Do not resolve this by re-adding a global attribute under another name, by
copying theme variables onto `body`, or by adding `!important` overrides.
Loading
Loading