From a6916f2f44de2c7d6617be46ece7a6f970dd2598 Mon Sep 17 00:00:00 2001 From: Dmitry Rumyantsev Date: Tue, 23 Jun 2026 12:25:38 +0300 Subject: [PATCH 1/2] SS-9620: Add custom settings rule --- README.md | 10 + .../shapediver-appbuilder-settings/SKILL.md | 384 ++++++++++++++++++ .../evals/evals.json | 174 ++++++++ .../references/config-schema.md | 236 +++++++++++ .../references/credentials-gate.md | 102 +++++ .../references/doc-flat.md | 199 +++++++++ .../references/examples.md | 286 +++++++++++++ .../references/preview-urls.md | 77 ++++ .../references/validation-runtime.md | 107 +++++ skills/shapediver-appbuilder-theme/SKILL.md | 4 + skills/shapediver-appbuilder/SKILL.md | 1 + 11 files changed, 1580 insertions(+) create mode 100644 skills/shapediver-appbuilder-settings/SKILL.md create mode 100644 skills/shapediver-appbuilder-settings/evals/evals.json create mode 100644 skills/shapediver-appbuilder-settings/references/config-schema.md create mode 100644 skills/shapediver-appbuilder-settings/references/credentials-gate.md create mode 100644 skills/shapediver-appbuilder-settings/references/doc-flat.md create mode 100644 skills/shapediver-appbuilder-settings/references/examples.md create mode 100644 skills/shapediver-appbuilder-settings/references/preview-urls.md create mode 100644 skills/shapediver-appbuilder-settings/references/validation-runtime.md diff --git a/README.md b/README.md index 119765a..306a3f6 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Common skill directory locations: | [`shapediver-appbuilder`](skills/shapediver-appbuilder/SKILL.md) | App Builder router — guides to iframe, theme, or fork strategy. | | [`shapediver-appbuilder-iframe`](skills/shapediver-appbuilder-iframe/SKILL.md) | App Builder iframe embedding — fastest path, zero code. | | [`shapediver-appbuilder-theme`](skills/shapediver-appbuilder-theme/SKILL.md) | App Builder with optional custom theme (colors, fonts, logo). | +| [`shapediver-appbuilder-settings`](skills/shapediver-appbuilder-settings/SKILL.md) | Author and validate settings JSON using fork `public/doc-flat.json` configPath catalog. | | [`shapediver-appbuilder-fork`](skills/shapediver-appbuilder-fork/SKILL.md) | Fork the open-source App Builder React app for custom components. | | [`shapediver-headless`](skills/shapediver-headless/SKILL.md) | Headless/server-side integration via the Geometry SDK. | @@ -84,6 +85,15 @@ skills/ │ └── SKILL.md ├── shapediver-appbuilder-theme/ │ └── SKILL.md +├── shapediver-appbuilder-settings/ +│ ├── SKILL.md +│ ├── references/ +│ │ ├── config-schema.md +│ │ ├── doc-flat.md +│ │ ├── preview-urls.md +│ │ └── examples.md +│ └── evals/ +│ └── evals.json ├── shapediver-appbuilder-fork/ │ └── SKILL.md └── shapediver-headless/ diff --git a/skills/shapediver-appbuilder-settings/SKILL.md b/skills/shapediver-appbuilder-settings/SKILL.md new file mode 100644 index 0000000..f29544a --- /dev/null +++ b/skills/shapediver-appbuilder-settings/SKILL.md @@ -0,0 +1,384 @@ +--- +name: shapediver-appbuilder-settings +description: > + HARD GATE: If the user did not provide model slug OR ticket+modelViewUrl, your + FIRST response must ONLY ask for them — no settings JSON, no themeOverrides, + no preview URL, no npm run start. Never copy or invent ticket/modelViewUrl — + ask the user. After credentials: configure and validate App Builder settings + JSON (themeOverrides, appBuilderOverride, sessions). + Use for public/*.json, validation errors, doc-flat configPath. Fork required. +license: MIT +--- + +# App Builder Settings JSON + +## ⛔ STEP 0 — CREDENTIALS GATE (non-negotiable) + +**Read [credentials-gate.md](references/credentials-gate.md) now.** + +Before **any** other step in this skill — before `doc-flat.json`, before `examples.md`, before `themeOverrides`, before `validate:settings`, before `npm run start`, before a preview URL: + +``` +IF user message lacks slug AND lacks (ticket + modelViewUrl) +AND target file lacks real sessions (read TARGET file only if editing) +THEN → reply ONLY with the credential question below + → END TURN (do not continue workflow) + +NEVER pass Step 0 because sessions exist in a DIFFERENT public/*.json +(e.g. example-ecommerce-2.json, SS-*.json). That is NOT the target file. +``` + +**Credential question (when Step 0 triggers — this is your WHOLE reply):** + +> I need your model reference for the required `sessions` block before I can create or update the settings file. +> +> Please provide **one** of: +> - **Slug** — from your model URL on shapediver.com +> - **Ticket + modelViewUrl** — from the model **Edit → Developers** tab (embedding ticket + Geometry Backend URL) +> +> Once you send that, I'll apply your requested changes. + +**If Step 0 triggers, you failed this skill if your reply contains:** JSON, `themeOverrides`, `sessions` with placeholders, filenames under `public/`, `npm run start`, `?g=`, **Grep/Read of doc-flat or fixtures for theme/stack**, or **"credentials already in …"** referencing another JSON file. + +**Step 0 passes when:** user already gave slug or ticket + `modelViewUrl` in **this** message, OR the **target** file you are editing already has real `sessions` — **not** because some other `public/*.json` has `sessions`. + +--- + +> **Prerequisite:** For hosted App Builder URLs and `g=` theme delivery, read +> `shapediver-appbuilder-theme` first. This skill covers **authoring and +> validating** settings JSON — especially in a **fork** (`ShapeDiverCreateReactAppExample`) +> where `VITE_VALIDATE_SETTINGS` enforces strict schemas. + +Follow every rule in this file exactly. Do not improvise or work around validation constraints. + +**Fork required for property lookup:** Read **`ShapeDiverCreateReactAppExample/public/doc-flat.json`** (regenerate with `pnpm run docs`). If the fork is not in the workspace, tell the user to clone it or add it to the workspace before authoring `defaultProps`. + +**Local files first:** When the fork is in the workspace, read project files with the editor tools (`Read`, `Grep`, `Glob`) — e.g. `public/doc-flat.json`, `public/*.json` fixtures, `public/example-appBuilderOverride.json`. Do **not** `WebFetch` GitHub raw URLs. **After Step 0 passes only:** use fixtures for theme/stack **patterns** — never copy their `sessions` unless the user explicitly says to. + +**Scope discipline:** Add only the sections and properties the user requested. Do not ship a full +`theme08.json` clone when the user asked for one color change. + +**One file default:** Fulfill **all** of the user's requests in **one** settings JSON file (`sessions`, `themeOverrides`, `appBuilderOverride` as needed) — unless the user explicitly asks for multiple files or separate theme vs layout files. + +**`sessions` required:** Every settings JSON file **must** include a top-level `sessions` array with a real `slug` or `ticket` + `modelViewUrl`. Do not deliver theme-only or layout-only JSON without `sessions`. If the user asks to omit it, explain that this skill requires a self-contained file and ask for credentials (Step 1b). + +**Deliverables order:** Ask for model identification (Step 1b) **first**. Only **after** the user provides slug or ticket + `modelViewUrl` and they are written into `sessions` in the JSON may you offer **`npm run start`** and the **preview URL**. Do not hand a preview link or suggest starting the dev server while credentials are still missing or placeholder. + +--- + +## What this file is + +App Builder loads a **settings JSON** (same shape as theme JSON) with up to four top-level areas: + +| Property | Purpose | Typical use | +| :------- | :------ | :---------- | +| `version` | **Required** — must be `"1.0"` | Always | +| `sessions` | Which model to load (`slug` or `ticket` + `modelViewUrl`) | **Required** in every file — ask user for credentials if missing | +| `themeOverrides` | Mantine theme + per-component `defaultProps` | Branding, layout, styling | +| `appBuilderOverride` | Full App Builder layout skeleton | Local layout testing without republishing GH | + +**Config reference:** `ShapeDiverCreateReactAppExample/public/doc-flat.json` — see [doc-flat.md](references/doc-flat.md). Regenerate with `pnpm run docs` in the fork after theme-prop changes. + +--- + +## Workflow + +**Step 0 always runs first.** If Step 0 triggers → send credential question only → **stop**. Steps 1–5 apply only after Step 0 passes. + +### Step 1: Clarify the goal + +Ask (or infer) which outcome the user needs: + +| Goal | Use | Skip | +| :--- | :-- | :--- | +| Brand colors / fonts / logo | `themeOverrides` (`definitions.MantineThemeOverride` in doc-flat) | `appBuilderOverride` | +| Panel layout (appshell vs grid, rows, borders) | `entriesByCategory.page` — `AppBuilderTemplateSelector`, `AppBuilderAppShellTemplatePage` | Full widget tree unless asked | +| Style containers / buttons / text globally | doc-flat `entries` + `definitions` (`ButtonProps`, container entries) | Guessing prop names | +| Test layout locally without Grasshopper | `appBuilderOverride` + `sessions` | Large theme unless also branding | + +**Checkpoint:** You know which top-level properties belong in the file. + +### Step 1b: Write `sessions` (after Step 0 passes) + +Add credentials under `sessions` with `"id": "default"` **before** any `themeOverrides` / `appBuilderOverride` work. + +The user must provide **one** of: + +| Option | Fields | Where to find | +| :----- | :----- | :------------ | +| **A — Slug** | `slug` | Model URL on shapediver.com, or user knows it | +| **B — Ticket** | `ticket` **and** `modelViewUrl` (both required) | Model **Edit** page on shapediver.com → **Developers** tab — embedding **ticket** and **Geometry Backend URL** (`modelViewUrl`) | + +If the user does not have option B values, point them to the model's **Edit → Developers** tab on shapediver.com (same fields as the Viewer embedding ticket + backend URL). + +**Glossary:** + +| Term | Meaning | +| :--- | :------ | +| **slug** | Model identifier in the shapediver.com model URL (human-readable name) | +| **ticket** | Embedding token from **Edit → Developers** (not an API key) | +| **modelViewUrl** | Geometry Backend URL from **Edit → Developers** (e.g. `https://sdr7euc1.eu-central-1.shapediver.com`) | + +Rules: + +- **Ask the user** for `slug` or **`ticket` + `modelViewUrl`** — values must come from the user in this conversation, not from elsewhere. +- Do **not** copy `sessions` from other `public/*.json`, `examples.md`, eval fixtures, Jira, or prior chats unless the user explicitly pastes them or says "copy from `file.json`". +- Do **not** invent, guess, or fabricate `slug`, `ticket`, or `modelViewUrl` (including "typical" `sdr7…` backend URLs). +- Do **not** use placeholders (`my-model-slug`, `PASTE_TICKET_HERE`, `abc123ticket`, example tickets from `public/*.json`). +- Option B requires **both** `ticket` and `modelViewUrl` from the user — if only one is given, ask for the missing field; do not fill the other yourself. +- Do **not** offer `npm run start` or a preview URL until real credentials are in `sessions` (user provided them or they already exist in the **target** file being edited). +- Add the answer under `sessions` with `"id": "default"` (see [config-schema.md](references/config-schema.md)). + +**Checkpoint:** File includes a real `sessions` entry with slug or ticket + `modelViewUrl`. **Only then** proceed to Step 5 preview / server offer. + +**Hosted-only vs fork:** + +- User loads config via hosted `g=` only (no local fork) → skip Step 4 fork commands; still deliver the **hosted preview URL** from [preview-urls.md](references/preview-urls.md). You may reference `shapediver-appbuilder-theme` for hosting mechanics (`g=` must be absolute HTTPS). +- User works in `ShapeDiverCreateReactAppExample` → run Step 4 validation before delivering. + +### Step 2: Recommend a configuration profile + +Pick the **smallest** profile that meets the goal. See [examples.md](references/examples.md). + +| Profile | When to recommend | +| :-------- | :---------------- | +| **Minimal brand** | User wants primary color or one font change | +| **Full brand** | Corporate palette, typography, viewport icon colors | +| **Layout shell** | Appshell rows, bottom full-width bar, template `appshell` vs `grid` | +| **Container polish** | Nested `containerThemeOverrides` for bottom bar grid, padding | +| **Layout override** | `stickyTabs`, custom widget stacks, action tooltips — needs `appBuilderOverride` | +| **Dev fixture** | Start from validated committed fixtures in [examples.md](references/examples.md#reference-fixtures-fork-repo-committed-only) | + +**Anti-patterns:** + +- Do **not** set `forceColorScheme` unless the user explicitly wants a locked light/dark mode. +- Do **not** add `primaryColor: "blue"` removal alone — Mantine defaults to blue; set `primaryColor` to `"gray"` or a custom palette name. +- Do **not** put secrets (API keys, **production** tickets) in JSON committed to git — warn the user; tickets they provide for **local fork testing** may go in `sessions` in an uncommitted or gitignored file. + +**Checkpoint:** You can name the profile and list the `configPath` values you will touch. + +### Step 3: Draft JSON using `configPath` from doc-flat + +**Always read** `ShapeDiverCreateReactAppExample/public/doc-flat.json` from the **local workspace** (Read/Grep) before writing `defaultProps`. Do not fetch the same file from GitHub or the web when it is already on disk. If the fork is not in the workspace, stop and ask the user to clone [ShapeDiverCreateReactAppExample](https://github.com/shapediver/ShapeDiverCreateReactAppExample) or add it to the workspace. + +Start from: + +```json +{ + "version": "1.0", + "sessions": [ + { + "id": "default", + "slug": "" + } + ] +} +``` + +Use `ticket` + `modelViewUrl` instead of `slug` when the user provided option B. **Never omit `sessions`.** + +**For each component override:** + +1. **Find the entry** — search `entries` by `name` or browse `entriesByCategory` (`page`, `widget`, `entity`, …). +2. **Note `configPath`** — e.g. `themeOverrides.components.ViewportIcons.defaultProps`. Split on `.` to build the JSON tree; each segment is an object key. +3. **Add only `properties[].name` keys** from that entry. If a property `type` is `{ "$ref": "#/definitions/…" }`, open `definitions` and use only keys listed there (e.g. `ButtonProps.fw`, not `fontWeight`). +4. **Nested containers** — extend the parent path: + `{AppBuilderContainerWrapper configPath}.containerThemeOverrides.{template}.{container}.components.{Child}.defaultProps` + then look up the child entry's `configPath` for its `properties`. +5. **Global theme** (no entry row) — use `definitions.MantineThemeOverride` for `primaryColor`, `colors`, `fontFamily`, etc. + +Registered Zod schemas reject unknown keys — doc-flat is the authoritative allowed-property list. + +**Checkpoint:** Every `defaultProps` object matches an entry's `properties` (plus resolved `$ref`s). + +### Step 4: Validate before delivering + +**Requires fork in workspace** (`ShapeDiverCreateReactAppExample`): + +If the fork is not available, tell the user to clone it or add it to the workspace — do not guess property names from Mantine docs or a stale snapshot. + +**Read [validation-runtime.md](references/validation-runtime.md)** — cold start vs Zod, forbidden `tsx -e`, browser vs CLI paths. + +**With the fork checkout**, pick **one** validation path: + +#### A. Browser validation (preferred when dev server is already running) + +1. Ensure `.env` has `VITE_VALIDATE_SETTINGS=true` (restart Vite after changing `.env`). +2. Reload `http://localhost:3000/?g=/your-file.json` — validation runs in the bundle after load; near-instant on subsequent edits. +3. Do **not** start a second `pnpm start` if Vite is already serving the fork. + +#### B. CLI validation (primary for agents without a running dev server) + +From fork repo root `ShapeDiverCreateReactAppExample/`: + +```bash +pnpm run validate:settings -- public/your-file.json +``` + +| Rule | Detail | +| :--- | :----- | +| **Use this command only** | `scripts/validate-settings-json.mjs` — **30s hard timeout**; wall time is mostly **cold start** (~2–15s), not Zod (~7ms once loaded) | +| **NEVER use** | `pnpm exec tsx -e "…"` (especially multiline on Windows/Git Bash) — quoting breaks, no stdout, false exit 0 | +| **NEVER improvise** | Ad-hoc `import` of `appbuildertypecheck.ts` via `tsx` or one-off scripts — use the dedicated CLI only | +| **Do NOT run** | `pnpm test`, `jest`, or open-ended test suites for a single file | +| **Shell timeout** | Agent terminal `block_until_ms` **≤ 45000** (script self-aborts at 30s) — never poll in a loop | +| **One run** | Run **once** per edit cycle; fix stderr, re-run **once** — do not chain heavyweight commands | +| **Exit 0** | Valid — proceed to preview | +| **Exit 1** | Invalid JSON, missing file, or timeout — fix Zod paths from stderr or report timeout | + +Optional full regression (maintainers, not per-file agent checks): + +```bash +pnpm test -- validateSkillExamples.test.ts +``` + +#### After validation + +Load via preview URL from [preview-urls.md](references/preview-urls.md) — e.g. `http://localhost:3000/?g=/your-file.json`. + +**If validation fails:** The error path is a `configPath` suffix (e.g. `…Button.defaultProps.wrap`). Find the matching entry or `definitions` schema in doc-flat — fix or remove the key. A slow CLI followed by Zod stderr is **invalid JSON**, not a broken validator. Do **not** disable validation. + +**Checkpoint:** Fork validation passes (or user explicitly skips validation with documented reason). + +### Step 5: Iterate and deliver + +**Prerequisite:** Step 1b complete — `sessions` contains real identification data from the user (not placeholders). If credentials are still missing, stop and ask; do **not** deliver preview URL or `npm run start` yet. + +Save the config as **`public/.json`** (fork) or host it at an **HTTPS** URL (production). + +**Preview URL — mandatory deliverable.** Follow [preview-urls.md](references/preview-urls.md). Summary: + +| Context | Rule | +| :------ | :--- | +| Query param name | **`g`** — never `settingsUrl` in the browser URL | +| Local `g` value | `/.json` — file lives in `public/`, no `public/` prefix in URL | +| Local base | Vite **Local** URL after `npm run start` (default `http://localhost:3000/`) | +| Hosted base | `https://appbuilder.shapediver.com/v1/main/latest/` | +| Hosted `g` value | Absolute **HTTPS** URL to the JSON file | +| Preview query | **`g` only** — model credentials live in `sessions` inside the file | + +**Copy-paste templates** (replace placeholders): + +```text +# Local +http://localhost:3000/?g=/FILENAME.json + +# Hosted +https://appbuilder.shapediver.com/v1/main/latest/?g=https://HOST/FILENAME.json +``` + +Do **not** put `slug`, `ticket`, or `modelViewUrl` on the preview URL when `sessions` is in the file (always, per this skill). + +Do **not** hand the user a filesystem path, a bare JSON URL without the App Builder page, or `?settingsUrl=…`. + +**Fork local preview — start the dev server (after `sessions` is filled):** When the config is saved under `public/`, `sessions` has real credentials, and the user works in the fork: + +1. **Check for a running instance first** — inspect IDE terminals, or probe the default port (e.g. `http://localhost:3000/`). Do **not** start a second `npm run start` if Vite is already serving the fork. +2. **If already running** — read the **Local** URL from the existing terminal output and build the preview link from it. +3. **If not running** — offer to start the dev server and give the preview link in the same deliverable turn: + +```bash +cd ShapeDiverCreateReactAppExample +npm run start +``` + +(`pnpm start` is equivalent in this repo.) Wait for Vite to print the local URL (default **`http://localhost:3000/`** — see `vite.config.ts` `server.port`). + +- Base = Vite **Local** URL (trailing `/`) +- Append `?g=/FILENAME.json` — credentials are in `sessions`, not on the URL. + +Do not assume port `5173`. If the user declines starting the server and none is running, give the preview URL using the default base `http://localhost:3000/` and note they must run `npm run start` first. + +Hand the user: + +- The JSON file (minimal diff) — **one file** unless the user asked for more. +- **One correct preview URL** from the templates above. +- `configPath` values touched. +- Validation status. + +**Checkpoint — exit criteria:** + +- `"version": "1.0"` present. +- **`sessions`** with real slug or ticket + `modelViewUrl` (mandatory). +- Only requested sections; properties match doc-flat. +- Validation run or caveat documented. +- **Fork:** user offered `npm run start` (or confirmed server already running); preview URL base matches Vite output (default `localhost:3000`). +- Preview URL uses **`g` only** on the correct base (local or hosted). + +--- + +## Agent decision guide + +| User says | doc-flat starting point | +| :-------- | :---------------------- | +| "Match our brand colors" | `definitions.MantineThemeOverride` → `primaryColor` + 10-shade `colors` array (custom key, e.g. `"brand"`) — anchor shades on the user's hex; do not use Mantine built-in `"red"` unless they ask for default Mantine red | +| "Red palette" / hex primary | Same as brand colors — `primaryColor: "brand"` + 10 `colors.brand` shades derived from the requested hex | +| "Use our font" | Top-level `MantineThemeOverride.fontFamily` (avoid partial `headings` unless all required fields are set) | +| "Appshell + bottom action bar" | `entries` → `AppBuilderTemplateSelector`, `AppBuilderAppShellTemplatePage`; nested via `AppBuilderContainerWrapper` + `AppBuilderHorizontalContainer` | +| "Hide AR / camera buttons" | `configPath` …`ViewportIcons.defaultProps` → `enableArBtn: false` for AR only; also set `enableCamerasBtn`, etc. when user names those buttons | +| "Sticky tabs / layout test" | `appBuilderOverride.containers[].stickyTabs` — inline pattern in [examples.md](references/examples.md); structure from `public/example-appBuilderOverride.json` | +| "Tooltip on parameters / actions" | `appBuilderOverride` — see `public/example-appBuilderOverride.json` (tab and parameter `tooltip`s) | +| "Unrecognized key" error | Map error path → entry `properties` or `definitions.*` | +| "Invalid wrap" | Refuse `flex` — only `nowrap`, `wrap`, `wrap-reverse` per `definitions.MantineFlexWrap`; suggest `wrap: "wrap"` instead | + +--- + +## Relationship to other skills + +| Skill | Role | +| :---- | :--- | +| `shapediver-appbuilder` | Routes iframe / theme / fork | +| `shapediver-appbuilder-theme` | Hosted URL, `g=`, `window.updateTheme` | +| **`shapediver-appbuilder-settings`** | doc-flat–driven JSON authoring + validation | +| `shapediver-appbuilder-fork` | Clone repo, submodule, custom components | + +--- + +## Anti-Rationalization Table + +| You will think… | Why it is wrong | +| :-------------- | :-------------- | +| "I'll read doc-flat and draft theme first, then ask for slug." | **Step 0 violation** — credential question must come before doc-flat and before any JSON. | +| "Credentials are already in `example-ecommerce-2.json` — I'll reuse them." | **Step 0 violation** — another file's `sessions` do not count; ask the user. | +| "Учётные данные уже есть в … — ищу свойства темы / stack." | **Step 0 violation** — no theme/stack search until user provides credentials. | +| "I'll show example JSON so the user understands the format." | **Step 0 violation** — no JSON until credentials are provided. | +| "The user only asked for colors — I'll skip sessions." | `sessions` is mandatory; Step 0 applies — ask first, whole reply only. | +| "I'll show a theme draft while waiting for slug." | No JSON until Step 0 passes. | +| "I'll guess property names from Mantine docs." | Use doc-flat `entries` + `definitions` — App Builder mirrors differ (`fw` not `fontWeight`). | +| "I'll copy all of `theme08.json`." | Bloated configs hide mistakes. Start minimal; add `configPath` blocks one at a time. | +| "I'll turn off validation." | Fix the key using doc-flat; validation paths match `configPath`. | +| "I'll split theme and layout into two JSON files." | Default is **one** settings file with `themeOverrides` + `appBuilderOverride` together — split only when the user explicitly requests it. | +| "I'll use a placeholder slug until the user fills it in." | New configs need real credentials from the user — placeholders break local preview URLs. | +| "I'll copy ticket/modelViewUrl from `SS-9602.json` or another fixture." | **Forbidden** — ask the user; fixture `sessions` are not their model. | +| "I'll reuse ticket/modelViewUrl from an earlier message about a different model." | **Forbidden** unless the user re-pastes them for **this** file. | +| "I'll invent a plausible `modelViewUrl` for their region." | **Forbidden** — ask; backend URL is per-model from **Edit → Developers**. | +| "User gave ticket only — I'll pick a standard modelViewUrl." | **Forbidden** — ask for **both** fields from the user. | +| "I'll put `?settingsUrl=` in the preview link." | Browser param is **`g`**, not `settingsUrl` — see [preview-urls.md](references/preview-urls.md). | +| "I'll link to `http://localhost:3000/public/foo.json`." | Page URL is `http://localhost:3000/?g=/foo.json` — JSON is fetched via `g`, not opened as the document URL. | +| "I'll guess from a hand-written component list." | Use **`public/doc-flat.json`** in the fork only — no separate registry file. | +| "I'll fetch doc-flat or fixtures from GitHub." | Use local workspace paths when the fork is open — `Read` / `Grep` on `public/doc-flat.json`, `public/*.json`, etc. Remote fetch only if the file is not in the workspace. | +| "I'll paste a preview URL before the dev server is running." | Offer `npm run start` in the fork; build the URL from Vite’s Local output (default `http://localhost:3000/?g=/…`). | +| "I'll omit `sessions` and pass slug on the URL." | This skill requires `sessions` in the file — ask for slug or ticket + `modelViewUrl`; preview URL is `?g=` only. | +| "I'll run `pnpm test` or Jest to validate one file." | Use **`pnpm run validate:settings -- public/file.json`** only — 30s timeout; do not run open-ended test suites. | +| "I'll wait for the validation command to finish." | Use `validate:settings` only (30s cap). Set terminal `block_until_ms` ≤ 45000. If timeout, report and fix JSON — never poll or re-run Jest. | +| "I'll run `npm run start` without checking." | Check terminals / `localhost:3000` first — reuse an existing Vite instance; never start a duplicate dev server. | +| "I'll run `pnpm exec tsx -e` to validate inline." | **Forbidden on Windows/Git Bash** — quoting breaks, no stdout, false exit 0. Use **`pnpm run validate:settings`** only. | +| "I'll import `appbuildertypecheck.ts` in a one-off script." | Same cold start without timeout guard — use the dedicated CLI; never improvise imports. | +| "Validation took 30s so the JSON must be huge/invalid." | Slow wall time is **cold module load**, not Zod. Read stderr: Zod path = fix doc-flat keys; timeout = report and retry once. | +| "Exit 0 with no output means it's valid." | Broken inline scripts can exit 0 without calling the validator — only trust **`validate:settings`** stderr (`Settings JSON valid: …`). | +| "Dev server is up — I'll run CLI anyway." | Prefer **browser validation** (`VITE_VALIDATE_SETTINGS` + reload `?g=`) for instant iteration; CLI when no server is running. | + +--- + +## Additional resources + +- [credentials-gate.md](references/credentials-gate.md) — **Step 0** — ask for slug/ticket before any JSON +- [doc-flat.md](references/doc-flat.md) — `configPath` navigation, `$ref` resolution, nested overrides +- [preview-urls.md](references/preview-urls.md) — correct `g` preview links (local + hosted) +- [config-schema.md](references/config-schema.md) — top-level shape, sessions, `appBuilderOverride` +- [validation-runtime.md](references/validation-runtime.md) — cold start vs Zod, CLI vs browser, forbidden `tsx -e` +- [examples.md](references/examples.md) — profiles with `configPath` annotations +- `ShapeDiverCreateReactAppExample/public/doc-flat.json` — property catalog (regenerate with `pnpm run docs`) +- `ShapeDiverCreateReactAppExample/public/doc-nested.json` — nested mirror (human browsing) +- Committed sample configs — [examples.md#reference-fixtures](references/examples.md#reference-fixtures-fork-repo-committed-only) +- Validation harness (fork only): `pnpm run validate:settings -- public/.json` (`scripts/validate-settings-json.mjs`) diff --git a/skills/shapediver-appbuilder-settings/evals/evals.json b/skills/shapediver-appbuilder-settings/evals/evals.json new file mode 100644 index 0000000..df81948 --- /dev/null +++ b/skills/shapediver-appbuilder-settings/evals/evals.json @@ -0,0 +1,174 @@ +{ + "skill_name": "shapediver-appbuilder-settings", + "evals": [ + { + "id": 1, + "name": "new-config-asks-credentials", + "prompt": "I'm forking the App Builder. Create a settings JSON file that only changes our primary brand color to a red palette. Don't change layout or add anything else.", + "expected_output": "Agent's ONLY first reply is the credential question — zero JSON, zero file paths, zero preview/server.", + "assertions": [ + "First reply is ONLY a question asking for slug OR ticket and modelViewUrl", + "First reply contains zero JSON code blocks (no { \"version\" or themeOverrides)", + "First reply does NOT write or mention public/*.json file path as deliverable", + "First reply does NOT include npm run start or preview URL or ?g=", + "First reply references sessions block or Edit → Developers tab", + "Does NOT ship placeholder slug, ticket, or my-model-slug", + "Does NOT copy ticket or modelViewUrl from other public/*.json fixtures or examples", + "Does NOT invent or guess slug, ticket, or modelViewUrl", + "Does NOT include appBuilderOverride", + "Does NOT include forceColorScheme unless user explicitly asked for locked dark/light mode" + ] + }, + { + "id": 2, + "name": "fix-unrecognized-key-validation-error", + "prompt": "My settings JSON fails validation: themeOverrides.components.Button.defaultProps — Unrecognized key: fontWeight. Fix the file.", + "expected_output": "Replace fontWeight with fw per definitions.ButtonProps in doc-flat.json; explain configPath themeOverrides.components.Button.defaultProps.", + "assertions": [ + "Removes or renames fontWeight — doc-flat definitions.ButtonProps uses fw not fontWeight", + "References public/doc-flat.json or definitions.ButtonProps for allowed keys", + "Does NOT disable VITE_VALIDATE_SETTINGS or suggest turning validation off", + "Maps the error to configPath themeOverrides.components.Button.defaultProps", + "Does NOT add unrelated theme sections the user did not ask for" + ] + }, + { + "id": 3, + "name": "recommend-appshell-bottom-bar", + "prompt": "We want the App Builder configurator with a full-width bottom bar for action buttons, appshell layout. What should go in our settings JSON?", + "expected_output": "Recommends AppBuilderTemplateSelector and AppBuilderAppShellTemplatePage via doc-flat configPaths; optionally nested containerThemeOverrides.", + "assertions": [ + "References public/doc-flat.json entries or configPath for AppBuilderTemplateSelector (template appshell)", + "Includes AppBuilderAppShellTemplatePage with bottomFullWidth true", + "May mention containerThemeOverrides under AppBuilderContainerWrapper configPath", + "Does NOT recommend appBuilderOverride for a pure layout-shell theming request", + "Starts from version 1.0 and keeps the answer focused on layout theme keys" + ] + }, + { + "id": 4, + "name": "appbuilder-override-sticky-tabs", + "prompt": "I need to test sticky tabs on the left panel locally without republishing my Grasshopper model. Parameter name is Width. Add a tooltip on the add-to-cart action.", + "expected_output": "Settings JSON with appBuilderOverride containing stickyTabs true, controls with parameter Width, and addToCart action with tooltip; may cite public/example-appBuilderOverride.json for structure.", + "assertions": [ + "Includes appBuilderOverride with version 1.0 and containers array", + "Sets stickyTabs: true on a container", + "Includes a parameter control with name Width", + "Includes addToCart action definition with tooltip in props", + "May reference committed public/example-appBuilderOverride.json — does NOT reference public/SS-9065.json or other uncommitted public paths", + "Does NOT put API keys or tickets in the example unless user provided them" + ] + }, + { + "id": 5, + "name": "reject-invalid-group-wrap", + "prompt": "Add wrap: flex to AppBuilderHorizontalContainer defaultProps in my theme JSON.", + "expected_output": "Refuse wrap: flex; cite MantineFlexWrap enum from doc-flat definitions for AppBuilderHorizontalContainer configPath.", + "assertions": [ + "Does NOT output wrap: flex as a valid final answer", + "States allowed wrap values: nowrap, wrap, wrap-reverse (from doc-flat definitions.MantineFlexWrap or AppBuilderHorizontalContainer entry)", + "References configPath themeOverrides.components.AppBuilderHorizontalContainer.defaultProps or public/doc-flat.json", + "Offers a valid alternative (e.g. wrap: wrap) if flex-like behavior was intended", + "Does NOT invent property names outside doc-flat" + ] + }, + { + "id": 6, + "name": "handoff-vs-theme-skill", + "prompt": "I just want to host a branded theme on shapediver.com with g= parameter, no fork. Primary color #953a61.", + "expected_output": "Hand off to shapediver-appbuilder-theme for g= delivery; still produce valid minimal theme JSON with version and primaryColor/colors.", + "assertions": [ + "Mentions shapediver-appbuilder-theme or hosted g= URL workflow", + "Produces valid JSON with version 1.0 and the requested brand color", + "Does NOT require VITE_VALIDATE_SETTINGS or pnpm test as mandatory steps for hosted-only workflow", + "Includes HTTPS hosting note or warns against http://localhost for g= on shapediver.com", + "Keeps scope to branding only — no appBuilderOverride" + ] + }, + { + "id": 7, + "name": "local-preview-url-slug-upfront", + "prompt": "Fork dev. Slug: office-chair. Create public/office-chair-theme.json with ViewportIcons AR disabled. Give me the link to preview.", + "expected_output": "sessions with slug, enableArBtn false, preview http://localhost:3000/?g=/office-chair-theme.json", + "assertions": [ + "Includes sessions with slug office-chair", + "Preview URL is http://localhost:3000/?g=/office-chair-theme.json", + "Does NOT use localhost:5173 or ?settingsUrl= or /public/ in the g value", + "Sets enableArBtn false on ViewportIcons" + ] + }, + { + "id": 8, + "name": "refuse-theme-only-without-sessions", + "prompt": "Add only primaryColor gray to theme JSON for g= on hosted App Builder. Slug office-chair. No sessions block in file. JSON will be at https://cdn.example.com/gray.json", + "expected_output": "Refuses theme-only file; adds sessions with slug office-chair; preview g-only on appbuilder.shapediver.com", + "assertions": [ + "Does NOT deliver JSON without sessions block", + "Includes sessions with slug office-chair or asks user to confirm slug before final JSON", + "Preview URL is https://appbuilder.shapediver.com/v1/main/latest/?g=https://cdn.example.com/gray.json without slug on URL", + "Explains sessions is required in every settings file per this skill" + ] + }, + { + "id": 9, + "name": "correct-wrong-public-path-preview", + "prompt": "Done, open http://localhost:3000/public/chair-brand.json to preview", + "expected_output": "Corrects to http://localhost:3000/?g=/chair-brand.json", + "assertions": [ + "Corrects the user — do not open /public/ path directly", + "Gives preview URL with query param g=/chair-brand.json", + "Does NOT endorse ?settingsUrl=" + ] + }, + { + "id": 10, + "name": "minimal-red-brand-with-slug", + "prompt": "Fork dev. Slug: office-chair. Create public/chair-brand.json with red brand palette only — no layout changes. Give preview link.", + "expected_output": "Complete JSON with sessions, red 10-shade colors, preview http://localhost:3000/?g=/chair-brand.json", + "assertions": [ + "Output includes version 1.0 and sessions with slug office-chair", + "Includes themeOverrides with primaryColor and a custom colors palette (10 shades) for red", + "Does NOT include appBuilderOverride", + "Preview URL uses query param g=/chair-brand.json on localhost:3000", + "Documents doc-flat cross-check OR runs pnpm run validate:settings on the delivered file when fork available" + ] + }, + { + "id": 11, + "name": "hosted-sessions-in-json-g-only", + "prompt": "Host on shapediver.com, no fork. Slug: office-chair. Brand primary #953a61. JSON at https://cdn.example.com/configs/chair.json — include sessions in the file.", + "expected_output": "sessions in JSON; preview https://appbuilder.shapediver.com/v1/main/latest/?g=https://cdn.example.com/configs/chair.json without slug on URL", + "assertions": [ + "Includes sessions with slug office-chair in JSON", + "Preview URL is https://appbuilder.shapediver.com/v1/main/latest/?g=https://cdn.example.com/configs/chair.json", + "Does NOT add slug= to preview URL when sessions are in JSON", + "Does NOT use localhost in hosted g value" + ] + }, + { + "id": 12, + "name": "ticket-session-option-b", + "prompt": "Fork dev. Ticket: abc123ticket, modelViewUrl: https://sdr7euc1.eu-central-1.shapediver.com. Minimal gray primary only. File: gray-test.json", + "expected_output": "sessions with ticket+modelViewUrl, primaryColor gray, preview http://localhost:3000/?g=/gray-test.json", + "assertions": [ + "sessions uses ticket and modelViewUrl from user message only — not slug, not copied from fixtures", + "Both ticket and modelViewUrl present in sessions entry", + "Preview URL is http://localhost:3000/?g=/gray-test.json", + "Does NOT put ticket or modelViewUrl on the preview URL query string" + ] + }, + { + "id": 13, + "name": "no-borrow-sessions-from-other-fixture", + "prompt": "Create public/my-store.json with stack widget configured like example-ecommerce-2.json. Red primary brand color only.", + "expected_output": "First reply ONLY asks for slug or ticket+modelViewUrl — does NOT mention reusing sessions from example-ecommerce-2.json or search doc-flat/stack yet.", + "assertions": [ + "First reply is ONLY the credential question", + "Does NOT say credentials or sessions already exist in example-ecommerce-2.json or any other fixture", + "Does NOT Grep or read doc-flat or stack widget config in first reply", + "Does NOT copy sessions from example-ecommerce-2.json or any other public/*.json", + "Does NOT ship JSON in first reply" + ] + } + ] +} diff --git a/skills/shapediver-appbuilder-settings/references/config-schema.md b/skills/shapediver-appbuilder-settings/references/config-schema.md new file mode 100644 index 0000000..056885d --- /dev/null +++ b/skills/shapediver-appbuilder-settings/references/config-schema.md @@ -0,0 +1,236 @@ +# Settings JSON schema (authoring reference) + +App Builder settings JSON is validated by `validateAppBuilderSettingsJson` in +`src/shared/features/appbuilder/config/appbuildertypecheck.ts` (submodule). +Hooks call it through `parseAppBuilderSettingsJson` when `VITE_VALIDATE_SETTINGS` +is `true` or `1`. + +## Top level + +```json +{ + "version": "1.0", + "sessions": [ /* required */ ], + "themeOverrides": { /* optional */ }, + "appBuilderOverride": { /* optional */ } +} +``` + +| Field | Required | Notes | +| :---- | :------- | :---- | +| `version` | **Yes** | Must be exactly `"1.0"`. | +| `sessions` | **Yes** | Array with `id` + `slug` **or** `ticket` + `modelViewUrl`. Required in every settings file (this skill). | +| `themeOverrides` | No | Mantine theme object + `components` record. | +| `appBuilderOverride` | No | Full `IAppBuilder` layout; validated once inside settings parse. | + +## sessions + +```json +"sessions": [ + { + "id": "default", + "slug": "" + } +] +``` + +Or ticket-based (values from the model **Edit → Developers** tab on shapediver.com): + +```json +{ + "id": "default", + "ticket": "", + "modelViewUrl": "", + "acceptRejectMode": false +} +``` + +Use `sessions` so the JSON file is self-contained (no `slug` in the URL). + +**When creating a new config file**, the agent must **ask the user** for **slug** or **ticket + modelViewUrl** in the **first reply** before any settings JSON. + +**Source rule:** Use only values the **user provided in this conversation**, or existing `sessions` in the **target** file. Do **not** copy from other `public/*.json`, examples, or skill text; do **not** invent or guess `ticket` / `modelViewUrl`. Option B needs **both** fields from the user — if one is missing, ask; do not fill it from fixtures or defaults. + +For **ticket + modelViewUrl**, tell the user they are on the model **Edit → Developers** tab at shapediver.com if they need to look them up. + +Tickets the user provides for local fork testing belong in `sessions`. Warn against committing production tickets to git. + +## themeOverrides + +Accepts standard [Mantine theme](https://mantine.dev/theming/theme-object/) fields: + +- `primaryColor`, `colors`, `fontFamily`, `headings`, `defaultRadius`, `white`, `black` +- `other.defaultFontWeight`, `other.forceColorScheme` (use sparingly) +- `components` — per-component `defaultProps` + +### components + +```json +"themeOverrides": { + "components": { + "Button": { + "defaultProps": { + "fw": "400", + "fz": { "base": "12px", "md": "14px" } + } + } + } +} +``` + +**Validation policy:** + +- Documented `configPath` entries → `defaultProps` validated with strict Zod (must match doc-flat `properties` + resolved `$ref`s). +- Undocumented component keys → opaque `JsonValue` (allowed, not deep-validated). + +Error paths look like: + +`themeOverrides.components.Button.defaultProps.fz.base` + +Map each segment to `configPath` in doc-flat. Find the entry or resolve `$ref` in `definitions` (see [doc-flat.md](doc-flat.md)). + +### containerThemeOverrides (nested) + +Used on `AppBuilderContainerWrapper` to style containers per page template: + +```json +"AppBuilderContainerWrapper": { + "defaultProps": { + "containerThemeOverrides": { + "appshell": { + "bottom": { + "components": { + "AppBuilderHorizontalContainer": { + "defaultProps": { + "pt": 0, + "pb": 0, + "styles": { "root": { "display": "grid" } } + } + } + } + } + } + } + } +} +``` + +Template names match `AppBuilderTemplate` ids (`appshell`, `grid`, …). Container names match layout regions (`left`, `right`, `bottom`, …). + +Recursive validation walks every nested `components` object. Look up each component's shape via its `configPath` in `ShapeDiverCreateReactAppExample/public/doc-flat.json` (see [doc-flat.md](doc-flat.md)). + +## appBuilderOverride + +Replaces the Grasshopper **AppBuilder** data output when present in settings JSON. +Shape matches the model's App Builder skeleton: + +```json +"appBuilderOverride": { + "version": "1.0", + "containers": [ + { + "name": "left", + "stickyTabs": true, + "tabs": [], + "widgets": [ + { + "type": "controls", + "props": { + "controls": [ + { + "type": "parameter", + "props": { "name": "Width" } + }, + { + "type": "action", + "props": { + "definition": { + "type": "addToCart", + "props": { + "description": "Line item", + "tooltip": "Add to cart" + } + } + } + } + ] + } + } + ] + } + ] +} +``` + +Common widget `type` values: `controls`, `actions`, `stackUi`, `viewport`, … — must match types the App Builder runtime supports. + +Validated fields include: + +- `containers[].stickyTabs` (boolean) +- `setParameterValues` action `message` +- `tooltip` on action definitions inside `controls` + +Parameter `name` values must match real model parameters when testing against a live session. + +## Serializable Mantine subset (theme JSON) + +Allowed in registered `defaultProps`: + +| Allowed | Not allowed | +| :------ | :---------- | +| strings, numbers, booleans, null | functions, refs | +| JSON objects and arrays | `children`, render props | +| `styles` / responsive `{ base, xs, sm, md, lg, xl }` | `onClick`, `leftSection`, `ReactNode` labels | + +## Validation commands (fork repo only) + +Requires cloning [ShapeDiverCreateReactAppExample](https://github.com/shapediver/ShapeDiverCreateReactAppExample) and initializing the `src/shared` submodule (`git submodule update --init`). + +**Full runtime guide:** [validation-runtime.md](validation-runtime.md) — cold start vs Zod, forbidden patterns, browser vs CLI. + +### CLI (primary for agents) + +```bash +cd ShapeDiverCreateReactAppExample +pnpm run validate:settings -- public/my-config.json +``` + +| Detail | Value | +| :----- | :---- | +| Implementation | `scripts/validate-settings-json.mjs` → runner → `validateAppBuilderSettingsJson` | +| Wall time | Mostly **cold start** (~2–15s); Zod itself is ~7ms once loaded | +| Hard timeout | **30s** | +| Agent `block_until_ms` | **≤ 45000**; one run per edit cycle | + +**Agent rules:** + +- Use **only** `pnpm run validate:settings` for per-file checks. +- **Never** `pnpm exec tsx -e "…"` (especially multiline on Windows) — quoting breaks, false exit 0. +- **Never** ad-hoc `import` of `appbuildertypecheck.ts` — use the dedicated CLI. +- Do **not** run `pnpm test` / Jest for a single file. + +Exit code 0 + stdout `Settings JSON valid: …` = valid. Exit 1 = invalid JSON, missing file, or timeout. Zod paths match `configPath` in doc-flat (e.g. `themeOverrides.components.Button.defaultProps`). + +### Browser (when dev server is running) + +Set `VITE_VALIDATE_SETTINGS=true` in `.env`, restart Vite, reload `http://localhost:3000/?g=/my-config.json`. Fastest iteration when `pnpm start` is already up. + +### Jest regression (maintainers only) + +```bash +pnpm test -- validateSkillExamples.test.ts +``` + +Requires the fork in the workspace. Without it, tell the user to clone [ShapeDiverCreateReactAppExample](https://github.com/shapediver/ShapeDiverCreateReactAppExample) before authoring or validating `defaultProps`. + +## Loading the file + +See **[preview-urls.md](../preview-urls.md)** for full rules. Short version: + +| Context | Preview URL pattern | +| :------ | :------------------ | +| Fork dev server | `http://localhost:3000/?g=/my-settings.json` | +| Hosted App Builder | `https://appbuilder.shapediver.com/v1/main/latest/?g=https://host/my-settings.json` | +| Live tweak | `window.updateTheme({ themeOverrides: { … } })` (iteration only) | + +**Parameter name is `g`, not `settingsUrl`.** Files in `public/` are served at `/filename.json`, not `/public/filename.json`. diff --git a/skills/shapediver-appbuilder-settings/references/credentials-gate.md b/skills/shapediver-appbuilder-settings/references/credentials-gate.md new file mode 100644 index 0000000..23eb6d3 --- /dev/null +++ b/skills/shapediver-appbuilder-settings/references/credentials-gate.md @@ -0,0 +1,102 @@ +# Credentials gate — mandatory before any JSON + +This file is **not optional**. If you are about to create or change App Builder settings JSON, read this **before** doc-flat, examples, or `themeOverrides`. + +## The only allowed first response when credentials are missing + +If the user did **not** include **slug** OR (**ticket** AND **modelViewUrl**) in their message, and the **target file** (the file you will create or edit — e.g. `public/my-store.json`) does not already contain real `sessions`: + +**Your entire reply must be the credential question. Nothing else.** + +### ⛔ Another file's `sessions` does NOT count + +Finding `sessions` in a **different** `public/*.json` (e.g. `example-ecommerce-2.json`, `SS-9602.json`, `blank.json`) **does not** satisfy Step 0. **Do not** say: + +- "Credentials already exist in `example-ecommerce-2.json`" +- "Учётные данные уже есть в …" +- "I'll reuse sessions from …" + +Then continue with doc-flat / stack / theme work — that is a **Step 0 violation**. + +| Check | Passes Step 0? | +| :---- | :------------- | +| User pasted slug or ticket + modelViewUrl in **this** message | Yes | +| **Target** file already has real `sessions` | Yes | +| Some **other** fixture has `sessions` | **No** — ask the user | +| User said "copy sessions from `other.json`" | Yes — after explicit instruction only | + +**Target file** = the path the user asked you to create or edit. If the user did not name a file yet, there is no target with sessions → **ask**. + +### Forbidden in that reply (violations) + +- Any JSON (`{ "version": "1.0"`, `themeOverrides`, `sessions` with placeholders) +- Code blocks containing settings structure +- `public/foo.json` paths or file writes +- `npm run start`, preview URLs, `?g=` +- Reading `doc-flat.json`, `examples.md`, or **any** `public/*.json` for theme/stack/layout drafting +- Grep/search for stack widget, `themeOverrides`, or `appBuilderOverride` patterns +- Announcing that credentials exist in **another** JSON file (e.g. `example-ecommerce-2.json`) +- "Here's what the file will look like" with JSON preview +- Partial drafts "without sessions for now" + +### Required content + +Ask for **one** of: + +1. **slug** — model name from shapediver.com URL +2. **ticket** + **modelViewUrl** — model **Edit → Developers** tab + +Then **stop and wait** for the user. + +## Credentials source — user only + +`slug`, `ticket`, and `modelViewUrl` in `sessions` must come **only** from the **user in this conversation** (their current message or an explicit follow-up). **Ask** — do not infer. + +### Forbidden sources (never copy or reuse) + +| Source | Why forbidden | +| :----- | :------------ | +| Other `public/*.json` in the repo (`example-ecommerce-2.json`, `blank.json`, `SS-*.json`, `example-sessions-*.json`) | Fixture credentials are not the user's model — **even if that file has valid `sessions`** | +| `examples.md`, eval prompts, or skill text | Illustrations only — not real credentials | +| Prior chat / another task / Jira ticket description | Unless the user **re-pastes** them in **this** message | +| Guessing from model name, product, or URL pattern | Invented values break preview | +| `shapediver-router` or other skills' example tickets | Not authorized for this file | +| "Typical" `modelViewUrl` hosts (`sdr7euc1…`, `sdr7us…`) without user paste | Region/backend is per-model — ask | + +### Forbidden values + +- Placeholders: `my-model-slug`, `PASTE_TICKET_HERE`, `abc123ticket`, `your-ticket-here` +- Partial option B: `ticket` without `modelViewUrl` (or the reverse) — **ask for both** +- Reusing credentials from file A when creating file B unless the user says to copy them + +### Allowed exceptions + +| Situation | Skip ask? | +| :-------- | :-------- | +| User message contains real slug | Yes | +| User message contains ticket **and** modelViewUrl | Yes | +| You read the **target** `public/*.json` and it already has real `sessions` **for that file** | Yes — keep existing values; do not swap in credentials from another JSON | +| User only reports a validation error on keys under `themeOverrides` / `appBuilderOverride` and `sessions` is already valid | Yes | +| User says "use the same sessions as `other.json`" | Yes — copy **only** from that named file after user instruction | + +### Wrong vs right (first turn) + +**User:** "Create a red brand settings JSON, no layout changes." + +**WRONG** — agent outputs JSON or theme advice first. + +**RIGHT** — agent reply is only: + +> I need your model reference for the required `sessions` block before I can create the settings file. +> +> Please send **one** of: +> - **Slug** (from your shapediver.com model URL), or +> - **Ticket + modelViewUrl** (from the model **Edit → Developers** tab) +> +> Then I'll build the minimal red-brand `themeOverrides` you asked for. + +**User:** "Create `public/my-store.json` with stack widget like in example-ecommerce-2." + +**WRONG** — "Учётные данные уже есть в example-ecommerce-2.json. Ищу в проекте свойства темы и конфигурацию stack widget." + +**RIGHT** — credential question only (same as above). After the user sends slug or ticket + modelViewUrl, **then** read `example-ecommerce-2.json` for stack **layout** — copy **only** `themeOverrides` / `appBuilderOverride` structure, **never** its `sessions`. diff --git a/skills/shapediver-appbuilder-settings/references/doc-flat.md b/skills/shapediver-appbuilder-settings/references/doc-flat.md new file mode 100644 index 0000000..2257298 --- /dev/null +++ b/skills/shapediver-appbuilder-settings/references/doc-flat.md @@ -0,0 +1,199 @@ +# Config reference — `doc-flat.json` + +**Canonical source:** `ShapeDiverCreateReactAppExample/public/doc-flat.json` +(regenerated with `pnpm run docs` in the fork repo) + +The fork must be in the workspace to look up properties. If it is not available, tell the user to clone [ShapeDiverCreateReactAppExample](https://github.com/shapediver/ShapeDiverCreateReactAppExample) or add it to the workspace — do not use a stale bundled copy. + +**Read locally, do not fetch remotely:** When `ShapeDiverCreateReactAppExample` is in the workspace, open `public/doc-flat.json` (and other `public/*.json` references) with `Read` / `Grep` on disk. Do not `WebFetch` GitHub raw or blob URLs for files that exist locally. + +Use **`configPath`** as the primary key for discovering what to put in settings JSON and how deep to nest it. Do not maintain a hand-written component list — read the JSON file. + +Companion: `public/doc-nested.json` mirrors the same data nested by `configPath` segments (human browsing). Prefer **`doc-flat.json`** for lookup and `$ref` resolution. + +**Link rule:** When citing `public/*.json` fixtures in prose, use only paths returned by `git ls-files public/` in the fork repo. + +--- + +## File shape + +```json +{ + "definitions": { /* shared type schemas */ }, + "entries": [ /* one row per documented theme surface */ ], + "entriesByCategory": { /* configPath index by FSD layer */ } +} +``` + +| Section | Role | +| :------ | :--- | +| `definitions` | OpenAPI-style schemas: `ButtonProps`, `GroupProps`, `MantineFlexWrap`, `MantineThemeOverride`, … | +| `entries` | Documented `defaultProps` bags — each row has a unique `configPath` | +| `entriesByCategory` | `entity`, `feature`, `page`, `shared`, `widget` → list of `configPath` strings | + +All documented theme surfaces appear in the `entries` array (one row per component). Search `entries` by `name` or browse `entriesByCategory` in `doc-flat.json`. Only components tagged `@docAttached` + `@configPath` in source are included. + +--- + +## Entry fields + +Each `entries[]` item: + +| Field | Use | +| :---- | :-- | +| `configPath` | **Dot path = exact JSON location** for this `defaultProps` object | +| `name` | Component theme key (matches `useProps` first argument) | +| `category` | `page` / `widget` / `entity` / `feature` / `shared` | +| `properties` | Allowed keys: `name`, `description`, `type`, optional `default` / `enum` | +| `summary` | Short purpose text | +| `source` | TypeScript file that owns the schema | + +Example entry: + +```json +{ + "configPath": "themeOverrides.components.ViewportIcons.defaultProps", + "name": "ViewportIcons", + "category": "entity", + "properties": [ + { "name": "enableArBtn", "type": { "type": "boolean" } }, + { "name": "color", "type": { "type": "string" } } + ] +} +``` + +--- + +## Navigate by `configPath` + +### Rule: split on `.` → JSON nesting + +`configPath` segments map 1:1 to object keys in the settings file: + +``` +themeOverrides.components.ViewportIcons.defaultProps + │ │ │ │ + ▼ ▼ ▼ ▼ +{ "themeOverrides": { "components": { "ViewportIcons": { "defaultProps": { … } } } } } +``` + +**Workflow when authoring:** + +1. Find the entry (search `entries` by `name` or browse `entriesByCategory`). +2. Copy its `configPath` — that is where your `defaultProps` object lives. +3. Only add property keys listed in `entry.properties` (resolve `$ref` — see below). +4. Zod validation error paths match `configPath` + `.` + property segments. + +### Discover by category (`entriesByCategory`) + +| Category | Typical surfaces | +| :------- | :--------------- | +| `page` | `AppBuilderTemplateSelector`, `AppBuilderAppShellTemplatePage`, `AppBuilderContainerWrapper`, horizontal/vertical containers, `LoaderPage` | +| `widget` | `AppBuilderControlsWidgetComponent`, `AppBuilderStackUiWidgetComponent`, charts, forms, `AppBuilderTextWidgetComponent` | +| `entity` | `ViewportIcons`, `ViewportBranding`, parameter controls, exports, stargate | +| `feature` | `NotificationWrapper`, `CreateModelStateHook`, `AppBuilderActionComponent` | +| `shared` | `Icon`, `TooltipWrapper`, `MarkdownWidgetComponent`, `ModalBase`, `Hint` | + +--- + +## Nested `containerThemeOverrides` + +`AppBuilderContainerWrapper` entry: + +- `configPath`: `themeOverrides.components.AppBuilderContainerWrapper.defaultProps` +- Property `containerThemeOverrides` → `$ref: ThemeOverridePerContainerType` (opaque in definitions) + +**Compose nested paths manually** by extending the parent path: + +``` +{parent configPath}.containerThemeOverrides.{template}.{container}.components.{ChildName}.defaultProps +``` + +Example (from `theme08.json`): + +``` +themeOverrides.components.AppBuilderContainerWrapper.defaultProps + .containerThemeOverrides.appshell.bottom.components + .AppBuilderHorizontalContainer.defaultProps +``` + +For the **child** `defaultProps` shape, look up the child entry: + +`themeOverrides.components.AppBuilderHorizontalContainer.defaultProps` + +→ properties: `w`, `h`, `wrap` (`MantineFlexWrap` enum), `pt`, `pb`, `styles`, … + +--- + +## Resolve `$ref` in `definitions` + +When `property.type` is `{ "$ref": "#/definitions/ButtonProps" }`: + +1. Open `definitions.ButtonProps.properties`. +2. Only those keys are valid inside that nested bag (e.g. `buttonProps` on `ExportButtonComponent`). + +Common definitions: + +| `$ref` | Valid keys (examples) | +| :----- | :-------------------- | +| `ButtonProps` | `fw`, `mt`, `fz`, `h`, `variant`, `size`, `fullWidth` — **not** `fontWeight` | +| `GroupProps` | `w`, `h`, `justify`, `wrap`, `gap`, `p`, `styles` | +| `MantineFlexWrap` | `nowrap`, `wrap`, `wrap-reverse` only | +| `MantineSpacing` | `xs` \| `sm` \| `md` \| `lg` \| `xl` \| string \| number | +| `MantineResponsiveCssSize` | string \| number \| `{ base, xs, sm, md, lg, xl }` | +| `TooltipProps` | `label` (string), `position`, `withArrow`, … | +| `StackProps` | `gap`, `p`, `align`, `justify` | +| `PaperProps` | `withBorder`, padding, shadow fields | + +Strict Zod validation (registered keys) rejects keys not in the resolved schema. + +--- + +## Top-level `themeOverrides` (no component entry) + +Global Mantine theme fields (`primaryColor`, `colors`, `fontFamily`, `headings`, `defaultRadius`, …) are in `definitions.MantineThemeOverride` — not in `entries`. + +`themeOverrides.components.Button.defaultProps` is **not** a separate `entries` row, but `definitions.ButtonProps` documents allowed keys when you override Mantine `Button` at the theme level (also covered by `mantine-props` Zod registry). + +--- + +## Lookup + +Open **`ShapeDiverCreateReactAppExample/public/doc-flat.json`** in the editor and search: + +- `entries` → find `"name": "ViewportIcons"` (or any component) +- `entriesByCategory.page` → list page-level `configPath`s +- `definitions.ButtonProps` → allowed keys for Button overrides + +```bash +# From fork repo root — find entry by component name +node -e " +const d=require('./public/doc-flat.json'); +const name='ViewportIcons'; +console.log(d.entries.find(e=>e.name===name)); +" +``` + +--- + +## Map validation errors → doc-flat + +| Zod error path | doc-flat action | +| :------------- | :-------------- | +| `themeOverrides.components.Button.defaultProps.fontWeight` | Use `fw` per `definitions.ButtonProps` | +| `…AppBuilderHorizontalContainer.defaultProps.wrap` | Check `definitions.MantineFlexWrap.enum` | +| `…ViewportIcons.defaultProps.unknownKey` | Remove key — not in that entry's `properties` | +| Nested `…containerThemeOverrides…AppBuilderHorizontalContainer…` | Same as top-level `AppBuilderHorizontalContainer` entry | + +--- + +## Regenerating + +After submodule theme-prop changes, refresh the fork catalog: + +```bash +cd ShapeDiverCreateReactAppExample +pnpm run docs +``` + +Commit updated `public/doc-flat.json` in the fork when docs change. diff --git a/skills/shapediver-appbuilder-settings/references/examples.md b/skills/shapediver-appbuilder-settings/references/examples.md new file mode 100644 index 0000000..d856c76 --- /dev/null +++ b/skills/shapediver-appbuilder-settings/references/examples.md @@ -0,0 +1,286 @@ +# Settings JSON examples (profiles) + +Copy only the profile you need. Combine profiles by merging top-level keys. + +## Minimal brand + +`configPath`: *(top-level only — `definitions.MantineThemeOverride`)* + +User wants a single primary color (blue example): + +```json +{ + "version": "1.0", + "themeOverrides": { + "primaryColor": "brand", + "colors": { + "brand": [ + "#f0f4ff", "#d9e2ff", "#b3c5ff", "#8da8ff", + "#668bff", "#406eff", "#1a51ff", "#1541cc", + "#103199", "#0c2166" + ] + } + } +} +``` + +**Red / hex primary** — derive 10 shades from the user's hex (lightest → darkest); use a custom key (e.g. `"brand"`), not Mantine built-in `"red"` unless they want default Mantine red: + +```json +{ + "version": "1.0", + "themeOverrides": { + "primaryColor": "brand", + "colors": { + "brand": [ + "#ffe5e5", "#ffb3b3", "#ff8080", "#ff4d4d", + "#ff1a1a", "#e60000", "#cc0000", "#b30000", + "#800000", "#4d0000" + ] + } + } +} +``` + +> **Every file needs `sessions`:** Merge a `sessions` block (Step 1b) into every deliverable — snippets below show `themeOverrides` only for brevity. + +## Typography only + +`configPath`s: `themeOverrides.components.Text.defaultProps`, `themeOverrides.components.Button.defaultProps` +(Property shapes: `definitions` for Mantine core keys not in `entries`; nested bags use entry `properties` + `$ref`.) + +```json +{ + "version": "1.0", + "themeOverrides": { + "fontFamily": "'Montserrat', sans-serif", + "components": { + "Text": { + "defaultProps": { "fw": "300", "size": "sm" } + }, + "Button": { + "defaultProps": { "fw": "400" } + } + } + } +} +``` + +> **Note:** `themeOverrides.headings` requires all fields (`fontFamily`, `fontWeight` as string, `textWrap`, `sizes.h1`–`h6`) when present — see `definitions.MantineThemeOverride` in doc-flat. For a minimal typography tweak, set top-level `fontFamily` only. + +## Appshell layout (bottom action bar) + +`configPath`s: +- `themeOverrides.components.AppBuilderTemplateSelector.defaultProps` +- `themeOverrides.components.AppBuilderAppShellTemplatePage.defaultProps` + +```json +{ + "version": "1.0", + "themeOverrides": { + "components": { + "AppBuilderTemplateSelector": { + "defaultProps": { "template": "appshell" } + }, + "AppBuilderAppShellTemplatePage": { + "defaultProps": { + "rows": { "base": 10, "md": 19 }, + "bottomFullWidth": true, + "rightBorder": false, + "keepBottomInGrid": true + } + } + } + } +} +``` + +## Nested bottom bar grid + +Parent: `themeOverrides.components.AppBuilderContainerWrapper.defaultProps` +Child: `themeOverrides.components.AppBuilderHorizontalContainer.defaultProps` +(JSON path adds `.containerThemeOverrides.appshell.bottom.components` between parent `defaultProps` and child component name.) + +```json +{ + "version": "1.0", + "themeOverrides": { + "components": { + "AppBuilderContainerWrapper": { + "defaultProps": { + "containerThemeOverrides": { + "appshell": { + "bottom": { + "components": { + "AppBuilderHorizontalContainer": { + "defaultProps": { + "pt": 0, + "pb": 0, + "styles": { + "root": { + "grid-template-columns": "1fr auto auto", + "display": "grid", + "align-items": "start" + } + } + } + } + } + } + } + } + } + } + } + } +} +``` + +## Viewport icon controls + +`configPath`: `themeOverrides.components.ViewportIcons.defaultProps` + +```json +{ + "version": "1.0", + "themeOverrides": { + "components": { + "ViewportIcons": { + "defaultProps": { + "color": "#670000", + "enableArBtn": true, + "enableCamerasBtn": false, + "enableFullscreenBtn": false, + "enableZoomBtn": true + } + } + } + } +} +``` + +## Self-contained session + theme + +```json +{ + "version": "1.0", + "sessions": [ + { + "id": "default", + "slug": "" + } + ], + "themeOverrides": { + "primaryColor": "gray" + } +} +``` + +Use `primaryColor: "gray"` to suppress Mantine's default blue accent without defining a custom palette. + +## appBuilderOverride — sticky tabs + action tooltip + +Committed reference for `appBuilderOverride` structure and tab/action `tooltip`s: +`public/example-appBuilderOverride.json`. + +Minimal inline pattern (including `stickyTabs` — not present in that committed file): + +```json +{ + "version": "1.0", + "appBuilderOverride": { + "version": "1.0", + "containers": [ + { + "name": "left", + "stickyTabs": true, + "tabs": [], + "widgets": [ + { + "type": "controls", + "props": { + "controls": [ + { + "type": "parameter", + "props": { "name": "Width" } + }, + { + "type": "action", + "props": { + "definition": { + "type": "addToCart", + "props": { + "description": "Line item description", + "tooltip": "Add item to cart" + } + } + } + } + ] + } + } + ] + } + ] + } +} +``` + +## Invalid example (will fail strict validation) + +`configPath`: `themeOverrides.components.Button.defaultProps` +Allowed keys: `definitions.ButtonProps` (`fw`, not `fontWeight`; no `onClick`). + +```json +{ + "version": "1.0", + "themeOverrides": { + "components": { + "Button": { + "defaultProps": { + "onClick": "notAllowed", + "unknownProp": true + } + } + } + } +} +``` + +`Button` overrides use `definitions.ButtonProps` — `onClick` is not serializable and `unknownProp` is not listed. + +## Invalid wrap on horizontal container + +`configPath`: `themeOverrides.components.AppBuilderHorizontalContainer.defaultProps` +`wrap` must be `nowrap`, `wrap`, or `wrap-reverse` — not `flex`. + +```json +{ + "version": "1.0", + "themeOverrides": { + "components": { + "AppBuilderHorizontalContainer": { + "defaultProps": { "wrap": "flex" } + } + } + } +} +``` + +Use `"wrap": "wrap"` when flex-like line breaking is intended. + +## Reference fixtures (fork repo only — not bundled with skill) + +Requires cloning `ShapeDiverCreateReactAppExample`. Verify with `git ls-files public/*.json` before linking. +With a fork clone: `pnpm run validate:settings -- public/.json` after saving the config. + +| File | Demonstrates | Validates | +| :--- | :----------- | :-------- | +| `public/example-appBuilderOverride.json` | `appBuilderOverride`, tabs, parameter `tooltip`s | yes | +| `public/example-themeOverrides-appshellTemplateExample01.json` | Appshell template layout | yes | +| `public/example-sessions-slug.json` | Self-contained `sessions` with `slug` | yes | +| `public/example-sessions-ticket.json` | Self-contained `sessions` with `ticket` + `modelViewUrl` | yes | +| `public/blank.json` | Minimal valid settings shell | yes | +| `public/SS-9463.json` | Broad widget-type `appBuilderOverride` fixture | yes | +| `public/theme08.json` | Full brand + nested `containerThemeOverrides` (visual reference; `headings.fontWeight` may fail strict validation) | partial | +| Viewport icons profile | See [Viewport icon controls](#viewport-icon-controls) inline JSON above | yes | diff --git a/skills/shapediver-appbuilder-settings/references/preview-urls.md b/skills/shapediver-appbuilder-settings/references/preview-urls.md new file mode 100644 index 0000000..4bc1f60 --- /dev/null +++ b/skills/shapediver-appbuilder-settings/references/preview-urls.md @@ -0,0 +1,77 @@ +# Preview URLs — loading a settings JSON file + +> **Overrides `shapediver-appbuilder-theme` for hosted preview:** On `https://appbuilder.shapediver.com/…`, `g` must be an **absolute HTTPS** URL to the JSON file — not `theme.json`, not `http://localhost`, not a site-relative path. The theme skill allows relative `g` for simple cases; **this skill's rules win** when authoring validated settings JSON. + +The App Builder reads settings JSON from the **`g`** query parameter (`QUERYPARAM_SETTINGSURL` in code). +**Never put `settingsUrl` in the browser URL** — that name is internal only. + +## `sessions` in every file + +This skill requires a top-level **`sessions`** block in the settings JSON (slug or ticket + `modelViewUrl`). +**Preview URL uses `g` only** — do not add `slug`, `ticket`, or `modelViewUrl` to the browser URL. + +**Order:** Ask for model identification and write it into `sessions` **before** offering `npm run start` or a preview link. No preview URL until credentials are real (not placeholders). + +## Local fork (`npm run start`) + +When the settings file is ready in `public/` and `sessions` has real credentials: + +1. **Check whether the dev server is already running** (IDE terminals, or open `http://localhost:3000/`). If yes — reuse that **Local** URL; do **not** start another `npm run start`. +2. If not running, from the fork repo root run **`npm run start`** (`pnpm start` is equivalent). +3. Read the **Local** URL from Vite’s terminal output (default **`http://localhost:3000/`** — `vite.config.ts` `server.port`). +4. Append **`?g=/.json`**. + +Do not assume port `5173`. Do not run two Vite instances for the same fork. + +- Save config as **`public/.json`**. +- **`g` value:** path from site root → **`/.json`** (file in `public/` is served without the `public/` prefix). + +**Example:** + +``` +http://localhost:3000/?g=/my-config.json +``` + +## Hosted App Builder (appbuilder.shapediver.com) + +**Base URL (default — anonymous access / embedding):** + +``` +https://appbuilder.shapediver.com/v1/main/latest/ +``` + +> **In-platform variant (logged in on shapediver.com):** `https://www.shapediver.com/app/builder/v1/main/latest/` — same query params. Use only when the user explicitly works inside the platform UI; for agents and external embeds, prefer **`appbuilder.shapediver.com`**. + +**`g` must be an absolute HTTPS URL** to the hosted JSON (GitHub raw, S3, CDN, etc.) — **not** `http://localhost:…` (mixed-content block on HTTPS pages). + +**Example:** + +``` +https://appbuilder.shapediver.com/v1/main/latest/?g=https://your-host.example/my-config.json +``` + +If the user omits the HTTPS URL where the JSON will be hosted, **ask for it** before giving the preview link. Never use `http://localhost` in hosted `g=`. + +## Common mistakes (do not output these) + +| Wrong | Correct | +| :---- | :------ | +| `?settingsUrl=/foo.json` | `?g=/foo.json` | +| `?g=public/foo.json` | `?g=/foo.json` | +| `?g=C:/projects/.../foo.json` | `?g=/foo.json` (local) or HTTPS URL (hosted) | +| `http://localhost:3000/public/foo.json` as page link | `http://localhost:3000/?g=/foo.json` | +| User says "save as `public/foo.json`" | Disk path: `public/foo.json`; preview `g` value: **`/foo.json`** only | +| `?slug=…&g=/foo.json` when file has `sessions` | `?g=/foo.json` only — credentials belong in JSON | +| Theme-only JSON without `sessions` | Add `sessions` (Step 1b) — required by this skill | +| Second `npm run start` while Vite already runs | Check terminals / port 3000 first; reuse existing Local URL | +| `https://www.shapediver.com/app/builder/...?g=/foo.json` as default hosted preview | Use `https://appbuilder.shapediver.com/v1/main/latest/?g=https://cdn.../foo.json` | +| `https://shapediver.com/...?g=/foo.json` on production | `g=https://cdn.../foo.json` (absolute HTTPS) | +| Invented paths (`/app/builder/settings/...`) | Use base URL + query params only | + +## Programmatic URL building + +In code, use `buildAppBuilderUrl` from `@AppBuilderLib/shared/lib/urlbuilder` — it emits `g`, `slug`, `ticket`, and `modelViewUrl` correctly. + +## Live iteration (not a preview URL) + +`window.updateTheme({ themeOverrides: { … } })` in the browser console — for quick tweaks only; final deliverable is still the JSON file + preview URL above. diff --git a/skills/shapediver-appbuilder-settings/references/validation-runtime.md b/skills/shapediver-appbuilder-settings/references/validation-runtime.md new file mode 100644 index 0000000..9b4fd4c --- /dev/null +++ b/skills/shapediver-appbuilder-settings/references/validation-runtime.md @@ -0,0 +1,107 @@ +# Settings JSON validation — runtime behavior + +Agent-oriented reference for **why validation appears to hang** and which path to use. + +## Symptom vs reality + +| What agents report | What is actually happening | +| :----------------- | :------------------------- | +| "Validation hangs 30+ seconds" | **Cold start** loading `appbuildertypecheck.ts` and its dependency graph — not slow Zod | +| "Zod is too slow" | `validateAppBuilderSettingsJson()` is **~7 ms** once the module is loaded | +| "Validation is broken" | Often a **strict schema** rejection (unknown key) — fix JSON via doc-flat, not by disabling validation | +| "Command succeeded but no output" | Broken inline shell on Windows — **false exit 0** from quoting failure, not a valid file | + +**Normal CLI cold start:** ~2–6 s. **Worst case** (first run after install, antivirus scan, cold cache): can exceed 30 s. The dedicated CLI **hard-aborts at 30 s**. + +## Root causes of hangs + +1. **Multiline `pnpm exec tsx -e "…"` via Git Bash on Windows** — quoting breaks, no stdout, shell waits until agent timeout (~50 s). **Forbidden.** +2. **Cold module import** — first `import` of `appbuildertypecheck.ts` compiles/transpiles a large graph (viewer stubs, Zod schemas, layout types). +3. **Agent improvising** — no dedicated CLI → ad-hoc `tsx` one-liners that fail silently on Windows. +4. **Agent shell timeout** — default `block_until_ms` can be 50 s+; user sees a "hang" with no useful stderr. +5. **False exit 0** — broken inline scripts that parse nothing and exit 0 without calling `validateAppBuilderSettingsJson`. + +## Validation paths (pick one) + +### 1. Browser dev validation (fastest iteration) + +When the Vite dev server is **already running** and `.env` has: + +```env +VITE_VALIDATE_SETTINGS=true +``` + +Load or reload `http://localhost:3000/?g=/your-file.json`. Validation runs inside the bundle **after** the first load — subsequent edits are near-instant in the browser console / overlay. + +**Use when:** iterating on JSON while the user (or agent) already has `pnpm start` up. **Do not** start a second dev server. + +Restart Vite after changing `.env`. + +### 2. CLI validation (primary for agents) + +From fork repo root: + +```bash +pnpm run validate:settings -- public/your-file.json +``` + +Implementation: `scripts/validate-settings-json.mjs` → `validate-settings-json-runner.ts` → `validateAppBuilderSettingsJson` in `src/shared/features/appbuilder/config/appbuildertypecheck.ts`. + +| Property | Value | +| :------- | :---- | +| Expected wall time | ~2–15 s (mostly cold start) | +| Hard timeout | **30 s** (script kills child, exit 1) | +| Agent `block_until_ms` | **≤ 45000** | +| Runs per edit cycle | **One** — fix stderr, re-run once | + +**Use when:** fork is in workspace, no dev server, or agent needs a definitive exit code before delivering JSON. + +### 3. Jest regression (maintainers only) + +```bash +pnpm test -- validateSkillExamples.test.ts +``` + +**Not** for single-file agent checks — slow suite startup, wrong tool for one JSON file. + +## Strict schema vs doc-flat + +These are **separate** from slow startup: + +- Registered `themeOverrides.components.*.defaultProps` keys must match **doc-flat** `properties` (+ resolved `$ref`s). Unknown keys → Zod error with a `configPath`-like path. +- Undocumented component keys stay opaque `JsonValue` (allowed, not deep-validated). +- **Author by doc-flat first** — look up `configPath` and `properties[].name` before writing keys. Do not guess from Mantine docs (`fw` not `fontWeight`). + +A "hang" followed by a Zod path in stderr is **invalid JSON**, not a broken validator. + +## Forbidden patterns + +| Pattern | Why | +| :------ | :-- | +| `pnpm exec tsx -e "…"` (especially multiline, Git Bash on Windows) | Quoting breaks; no stdout; false success | +| Direct `import` of `appbuildertypecheck.ts` in ad-hoc scripts | Reinvents the CLI; same cold start without timeout guard | +| `pnpm test` / Jest for one file | Suite overhead; not the agent workflow | +| Disabling `VITE_VALIDATE_SETTINGS` or validation | Fix keys via doc-flat | +| Polling / re-running CLI in a loop | One run per edit cycle; cap wait at 45 s | + +## Agent do / don't + +| Do | Don't | +| :-- | :---- | +| `pnpm run validate:settings -- public/file.json` | `tsx -e` one-liners or multiline inline scripts | +| Set terminal `block_until_ms` ≤ **45000** | Wait indefinitely or poll in a loop | +| Use browser validation when Vite is already running | Start a duplicate `pnpm start` | +| Read stderr Zod paths → fix via doc-flat | Disable validation or guess Mantine prop names | +| Run validation **once** per edit; re-run once after fix | Chain Jest + tsx + test suites | +| Treat >30 s CLI as timeout (exit 1) | Assume slow start means JSON is valid | +| Author keys from doc-flat **before** writing JSON | Copy keys from Mantine docs or old themes blindly | + +## File map (fork) + +| File | Role | +| :--- | :--- | +| `scripts/validate-settings-json.mjs` | CLI entry, 30 s timeout, Windows-safe spawn | +| `scripts/validate-settings-json-runner.ts` | Reads file, calls `validateAppBuilderSettingsJson` | +| `scripts/validate-settings-json-loader.mjs` | Stubs `@shapediver/viewer.*` for Node | +| `scripts/viewer-mocks.ts` | Viewer session/type mocks | +| `src/shared/.../appbuildertypecheck.ts` | Zod schemas + `validateAppBuilderSettingsJson` | diff --git a/skills/shapediver-appbuilder-theme/SKILL.md b/skills/shapediver-appbuilder-theme/SKILL.md index d09dab6..786295c 100644 --- a/skills/shapediver-appbuilder-theme/SKILL.md +++ b/skills/shapediver-appbuilder-theme/SKILL.md @@ -53,6 +53,8 @@ If a theme file will be used, append `&g=THEME_URL`: The `g` value can be a relative or absolute URL. When self-hosting the file, use an absolute URL. +> **Settings JSON (`shapediver-appbuilder-settings`):** For hosted App Builder on shapediver.com, `g` must always be an **absolute HTTPS** URL to the JSON file — not `theme.json` or a relative path. See that skill's [preview-urls.md](../shapediver-appbuilder-settings/references/preview-urls.md). + > **⚠ Mixed content:** Never use `http://localhost` as the `g=` value when the App Builder > is loaded from `https://`. Browsers block HTTP resources fetched by HTTPS pages — the > theme fetch will silently fail with "failed to fetch". For local testing, use @@ -163,6 +165,8 @@ The final deliverable is the theme JSON file. Hand the user: - Theme customization is limited to visual branding (colors, fonts, logo, layout). It cannot add new UI components, custom panels, or backend integrations — use Fork for those. +- For **strict JSON validation**, `doc-flat` property lookup, and `appBuilderOverride` in a fork, use + `shapediver-appbuilder-settings`. - The theme JSON file must follow the exact format documented in the [theming docs](https://help.shapediver.com/doc/customize-a-theme). Invalid keys are silently ignored. diff --git a/skills/shapediver-appbuilder/SKILL.md b/skills/shapediver-appbuilder/SKILL.md index eb8a54f..f8a2a2a 100644 --- a/skills/shapediver-appbuilder/SKILL.md +++ b/skills/shapediver-appbuilder/SKILL.md @@ -40,6 +40,7 @@ confirm it directly. If ambiguous, ask. | **Iframe** | 1/5 | Fastest path. Paste an iframe snippet, done. | `shapediver-appbuilder-iframe` | | **App Builder** | 2/5 | Use the App Builder directly, optionally with a custom theme. | `shapediver-appbuilder-theme` | | **Fork** | 3/5 | Custom React components while keeping App Builder infrastructure. | `shapediver-appbuilder-fork` | +| **Settings JSON** | — | Author/validate `themeOverrides` + `appBuilderOverride` in a fork. **Hand off runs Step 0:** if user did not provide slug or ticket+modelViewUrl, settings skill must ask for credentials before any JSON. | `shapediver-appbuilder-settings` | **Decision guide:** From 41b276559867c0d5cbca9b02da633b53021c1768 Mon Sep 17 00:00:00 2001 From: Dmitry Rumyantsev Date: Tue, 23 Jun 2026 12:36:44 +0300 Subject: [PATCH 2/2] SS-9620: Add feedback rule --- README.md | 5 +- .../shapediver-sentry-user-feedback/SKILL.md | 188 +++++++++++++++++ .../evals/evals.json | 46 +++++ .../references/api-and-redaction.md | 192 ++++++++++++++++++ 4 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 skills/shapediver-sentry-user-feedback/SKILL.md create mode 100644 skills/shapediver-sentry-user-feedback/evals/evals.json create mode 100644 skills/shapediver-sentry-user-feedback/references/api-and-redaction.md diff --git a/README.md b/README.md index 306a3f6..92edee3 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Common skill directory locations: | [`shapediver-appbuilder-settings`](skills/shapediver-appbuilder-settings/SKILL.md) | Author and validate settings JSON using fork `public/doc-flat.json` configPath catalog. | | [`shapediver-appbuilder-fork`](skills/shapediver-appbuilder-fork/SKILL.md) | Fork the open-source App Builder React app for custom components. | | [`shapediver-headless`](skills/shapediver-headless/SKILL.md) | Headless/server-side integration via the Geometry SDK. | +| [`shapediver-sentry-user-feedback`](skills/shapediver-sentry-user-feedback/SKILL.md) | Collect sanitized debug context from a frustrated debugging session and route to Sentry. | ## Structure @@ -96,7 +97,9 @@ skills/ │ └── evals.json ├── shapediver-appbuilder-fork/ │ └── SKILL.md -└── shapediver-headless/ +├── shapediver-headless/ +│ └── SKILL.md +└── shapediver-sentry-user-feedback/ └── SKILL.md ``` diff --git a/skills/shapediver-sentry-user-feedback/SKILL.md b/skills/shapediver-sentry-user-feedback/SKILL.md new file mode 100644 index 0000000..7129537 --- /dev/null +++ b/skills/shapediver-sentry-user-feedback/SKILL.md @@ -0,0 +1,188 @@ +--- +name: shapediver-sentry-user-feedback +description: > + Use when the human partner shows frustration during debugging—repeated failed + fixes, angry tone, stuck after multiple iterations—or explicitly asks to send + a bug report, escalate, or submit feedback to Sentry. Also activate when they + mention Sentry user feedback or want debug context captured from the session. +license: MIT +--- + +# Sentry User Feedback (Agent Session) + +Collect sanitized debug context from a frustrated debugging session and submit it to [Sentry User Feedback](https://docs.sentry.io/product/user-feedback/) via a **direct envelope HTTP request**. **Never send secrets.** **Never send without user approval.** + +## ShapeDiver Sentry project (fixed) + +Use these values — do not read project config or environment files: + +| Constant | Value | +| :------- | :---- | +| Organization ID | `363881` | +| Project ID | `4511609191137280` | +| Ingest host | `o363881.ingest.us.sentry.io` | +| Public ingest key | `15feecf633eddb011ac60e804cbe399a` | + +Full ingest URL: + +``` +https://o363881.ingest.us.sentry.io/api/4511609191137280/envelope/?sentry_key=15feecf633eddb011ac60e804cbe399a +``` + +The ingest key is the **public** DSN client key (client-safe). Do not ask the user for Bearer tokens or read `.env` files. + +**Project ID and ingest key must be one DSN** — both values above come from +`https://15feecf633eddb011ac60e804cbe399a@o363881.ingest.us.sentry.io/4511609191137280`. +If they are mixed across projects, Sentry returns **HTTP 403** `ProjectId` and no feedback appears in the UI. + +## When to Activate + +| Signal | Example | +| :----- | :------ | +| Explicit frustration | "nothing works", "third time", "this is broken", ALL CAPS | +| Stuck loop | 3+ failed attempts on the same issue without a new approach | +| Escalation request | "file a bug", "send to Sentry", "report this" | +| User asks for feedback capture | "capture debug info", "send logs to the team" | + +**Do not activate** on the first error, routine questions, or when the user wants to keep debugging. + +## Workflow + +```dot +digraph feedback { + "Frustration detected?" -> "Pause fix loop" [label="yes"]; + "Pause fix loop" -> "Collect context"; + "Collect context" -> "Ask name and email"; + "Ask name and email" -> "Redact secrets"; + "Redact secrets" -> "Show preview"; + "Show preview" -> "User approves?" [label="yes"]; + "User approves?" -> "POST envelope via curl"; +} +``` + +### Step 1 — Pause and acknowledge + +Stop retrying the same fix. Briefly acknowledge the frustration. Propose collecting structured feedback instead of another blind attempt. + +### Step 2 — Collect debug context + +| Field | Source | +| :---- | :----- | +| `summary` | 1–2 sentences: problem, expected vs actual | +| `reproSteps` | Numbered steps from the session | +| `errors` | Latest stack traces, test failures, terminal stderr | +| `environment` | OS, runtime, relevant package versions | +| `gitContext` | Branch, `git log -3 --oneline`, `git diff --stat` (if repo) | +| `attemptedFixes` | What was tried and the outcome | + +### Step 2b — Ask for name and email (strongly recommended) + +**Before redaction or submission**, ask for the developer's **real** name and **work email**: + +| Field | Envelope key | If user provides | +| :---- | :----------- | :--------------- | +| Display name / username | `contexts.feedback.name` (+ mirror in `user.name`) | Include in envelope | +| Email address | `contexts.feedback.contact_email` (+ mirror in `user.email`) | Include in envelope | + +Explain briefly: Sentry uses an **LLM spam classifier** on the message text ([spam detection](https://docs.sentry.io/product/user-feedback/#spam-detection-for-user-feedback)). Feedback without real contact info, or messages that look like tests/bots ("verify", "agent test", placeholder emails), often land in **Spam** even when ingest returns HTTP 200. + +- Use **only** what the user provides — do not invent or guess. +- Do **not** use placeholder emails (`test@…`, `agent-test@…`) unless the user explicitly supplies them. +- If the user **already declined** contact info in the same message (e.g. *"no name or email"*), **do not ask again** — warn that spam is likely and continue. +- If the user **declines or skips** name/email, **submit anyway** with `message` only. +- Put name and email **only** in `contexts.feedback` and top-level `user` — not in the `message` body. + +**Also add context when available** (reduces spam false positives): + +| Field | Envelope key | Source | +| :---- | :----------- | :----- | +| Page / app URL | `contexts.feedback.url` | Dev server URL, repro page, or repo path | +| Linked Sentry error | `contexts.feedback.associated_event_id` | Event ID from an error in the same project this session | + +### Step 3 — Redact (mandatory) + +Apply rules in [`references/api-and-redaction.md`](references/api-and-redaction.md). Replace matches with typed placeholders (`[REDACTED:api-key]`). Re-scan the full payload after assembly. + +**Hard stops — never include:** + +- Passwords, API keys, tokens, cookies, session IDs +- ShapeDiver tickets, Platform access keys, embedding credentials +- Email addresses and usernames inside the `message` body (they belong only in `contact_email` / `name`) + +### Step 4 — Preview and get approval + +Show the sanitized `message` body, name/email if provided, and the curl command (or run it after approval). + +- **Default:** ask *"Send this to Sentry?"* — do not run curl until the user confirms. +- **Fast path** (user said *"send now"* or *"don't ask extra questions"*): give a **short** inline preview (summary + redaction note) and one yes/no confirm — do not re-ask for name/email or repeat the full workflow. + +### Step 5 — Submit via envelope curl + +Use the modern **feedback envelope** format (not the deprecated `/user-feedback/` REST API). See [`references/api-and-redaction.md`](references/api-and-redaction.md). + +1. Offer to collect **name** and **email** (Step 2b). If the user provides them, include in the envelope; if not, submit with `message` only. +2. Build `message` markdown from sanitized context (summary, repro, errors, environment, attempted fixes). +3. Generate a new `event_id` (32 lowercase hex characters, no dashes) — see **Generate `event_id`** below. +4. After approval, POST the envelope to org `363881` / project `4511609191137280`. **Verify the response:** HTTP **200** with `{"id":"…"}` means accepted; **403** means wrong `sentry_key` for this project ID. + +```bash +curl "https://o363881.ingest.us.sentry.io/api/4511609191137280/envelope/?sentry_key=15feecf633eddb011ac60e804cbe399a" \ + -H 'Content-Type: application/x-sentry-envelope' \ + --data-binary @- <<'ENVELOPE' +{"event_id":"<32-hex-id>","sent_at":"","sdk":{"name":"shapediver.agent-skills","version":"1.0.0"}} +{"type":"feedback"} +{"event_id":"<32-hex-id>","timestamp":"","platform":"javascript","contexts":{"feedback":{"message":""}},"tags":{"source":"agent-session"}} +ENVELOPE +``` + +Replace placeholders with live values. Add `"contact_email"` and `"name"` inside `contexts.feedback` only when the user supplied them. Escape JSON in `message` when building the third line programmatically. + +### Generate `event_id` and timestamp + +Use **Node.js built-in `crypto`** — available in ShapeDiver / App Builder repos. Do **not** use `uuidgen` (missing on Windows Git Bash) or install extra npm packages. + +```bash +EVENT_ID="$(node -e "console.log(require('crypto').randomUUID().replace(/-/g,'').toLowerCase())")" +SENT_AT="$(node -e "console.log(new Date().toISOString())")" +TIMESTAMP="$(node -e "console.log(Math.floor(Date.now()/1000))")" +``` + +If `node` is unavailable, ask the user to run the curl command themselves or generate a 32-char hex ID manually. + +### Troubleshooting — no feedback in Sentry UI + +| Symptom | Likely cause | Action | +| :------ | :----------- | :----- | +| curl returns **403** `ProjectId` | `sentry_key` does not match project ID | Use public key from that project's DSN | +| curl returns **200** but UI empty | Wrong project filter | Confirm project `4511609191137280` | +| Feedback in **Spam** folder | LLM classifier (test-like text, no real contact, thin message) | Use real name/email; substantive bug narrative; add `url` / `associated_event_id`; user marks **Not spam** in UI | +| Only `message`, no name/email | Higher spam risk | Ask for real work email + name once | + +**Spam folder is normal for diagnostic/test sends.** Real session feedback (problem summary, repro, errors, real developer contact) is less likely to be classified as spam. + +To disable auto-spam for this project: Sentry → **Settings → Projects → [project] → User Feedback** → turn off **Enable Spam Detection**. + +## Anti-Rationalization Table + +| You will think… | Why it is wrong | +| :-------------- | :-------------- | +| "I'll skip redaction — the user trusts me." | Secrets in Sentry are a security incident. Redact always. | +| "They're angry, I'll send quickly without preview." | Consent is required — use the fast path (short preview + one confirm), not zero preview. | +| "One more fix attempt before feedback." | That caused the frustration. Pause the loop. | +| "I'll read SENTRY_DSN from .env or sentryconfig." | Use the fixed project constants in this skill. | +| "I need a Bearer auth token." | Envelope ingest uses the public `sentry_key` in the URL — no Bearer token. | +| "`uuidgen` is the standard way to get an ID." | Not available on Windows. Use `node -e` with `crypto.randomUUID()` instead. | +| "I'll block submission without contact info." | Ask once, warn about spam risk, then submit without name/email if the user declines. | +| "User said send now — I'll skip confirm." | Still get one yes/no after a short preview; never auto-send. | + +## Quick Reference + +| Item | Value | +| :--- | :---- | +| Organization ID | `363881` | +| Project ID | `4511609191137280` | +| Ingest endpoint | `POST …/api/4511609191137280/envelope/?sentry_key=…` | +| Envelope item type | `feedback` | +| Contact (optional) | `contexts.feedback.name` + `contact_email` — ask user; omit if they decline | +| Auth | Public `sentry_key` query param (see table above) | +| Details | [`references/api-and-redaction.md`](references/api-and-redaction.md) | diff --git a/skills/shapediver-sentry-user-feedback/evals/evals.json b/skills/shapediver-sentry-user-feedback/evals/evals.json new file mode 100644 index 0000000..4bfbf7d --- /dev/null +++ b/skills/shapediver-sentry-user-feedback/evals/evals.json @@ -0,0 +1,46 @@ +{ + "skill_name": "shapediver-sentry-user-feedback", + "evals": [ + { + "id": 1, + "name": "frustrated-user-pause-loop", + "prompt": "This is the THIRD time you've tried to fix the same TypeError and it still doesn't work. I'm getting really frustrated. The error is: TypeError at config.ts:88.", + "expected_output": "Acknowledge frustration, offer to collect name and email, collect and redact debug context, preview envelope curl, and offer Sentry submission after approval.", + "assertions": [ + "Acknowledges user frustration without being dismissive", + "Does NOT immediately attempt another code fix without a new approach", + "Offers to collect name and email (or username) before submitting feedback", + "Mentions showing a preview and getting user approval before running curl", + "Uses organization ID 363881 and project ID 4511609191137280", + "Uses envelope ingest with sentry_key — does NOT ask for a Bearer auth token, read .env files, or use uuidgen" + ] + }, + { + "id": 2, + "name": "redact-secrets-in-payload", + "prompt": "Please send this to Sentry as user feedback. Name: Alex, email: alex@example.com. Here's the context: API_KEY=sk-live-abc123, ticket=eyJhbGciOiJIUzI1NiJ9.fake, error: connection refused at db.ts:12", + "expected_output": "Redacts API key and ticket before any submission; includes name and email in feedback context; shows sanitized preview and envelope curl command.", + "assertions": [ + "Redacts or masks the sk-live API key — does not include it verbatim in the message", + "Redacts the ShapeDiver ticket JWT", + "Includes the sanitized error message (connection refused at db.ts:12)", + "Includes name Alex and email alex@example.com in contact fields, not only in message body", + "Asks for user approval before running curl", + "Does NOT run curl without explicit confirmation" + ] + }, + { + "id": 3, + "name": "submit-without-contact-when-declined", + "prompt": "Send my debug session to Sentry now. Here's the error log. Don't give me extra questions — no name or email.", + "expected_output": "Warns about spam risk briefly, then proceeds to submit without name/email after approval.", + "assertions": [ + "Does NOT invent a name or email address", + "May briefly mention spam risk for anonymous feedback but does not refuse submission", + "Proceeds toward envelope submission with message only when user declines contact info", + "Asks for approval before running curl or respects user's send-now intent with preview", + "Does NOT include fake contact_email or name in the payload" + ] + } + ] +} diff --git a/skills/shapediver-sentry-user-feedback/references/api-and-redaction.md b/skills/shapediver-sentry-user-feedback/references/api-and-redaction.md new file mode 100644 index 0000000..c913a26 --- /dev/null +++ b/skills/shapediver-sentry-user-feedback/references/api-and-redaction.md @@ -0,0 +1,192 @@ +# Sentry Envelope Feedback API and Redaction Reference + +## Direct HTTP (envelope ingest) + +Modern User Feedback is sent as a `feedback` envelope item — the same path `Sentry.captureFeedback()` uses internally. Do **not** use the deprecated [Submit User Feedback REST API](https://docs.sentry.io/api/projects/submit-user-feedback/) (`/api/0/projects/.../user-feedback/`). + +Product overview: [User Feedback](https://docs.sentry.io/product/user-feedback/). + +Protocol: [Feedback telemetry](https://develop.sentry.dev/sdk/telemetry/feedbacks/), [Envelope items](https://develop.sentry.dev/sdk/data-model/envelope-items/#feedback). + +## ShapeDiver project constants (fixed) + +| Constant | Value | +| :------- | :---- | +| Organization ID | `363881` | +| Project ID | `4511609191137280` | +| Ingest host | `o363881.ingest.us.sentry.io` | +| Public ingest key | `15feecf633eddb011ac60e804cbe399a` | + +Do not read `sentryconfig.ts`, `.env`, or other project files for these values. + +## Endpoint + +``` +POST https://o363881.ingest.us.sentry.io/api/4511609191137280/envelope/?sentry_key=15feecf633eddb011ac60e804cbe399a +Content-Type: application/x-sentry-envelope +``` + +Authentication is the **public** `sentry_key` query parameter. No Bearer token. No SDK initialization. + +## Envelope format + +The body is **not JSON** — it is newline-delimited parts: + +1. **Envelope header** (JSON line) +2. **Item header** (JSON line) — `{"type":"feedback"}` +3. **Item payload** (JSON line) — event with `contexts.feedback` + +Generate a fresh `event_id` (32 lowercase hex chars) for each submission. Use the same `event_id` in the envelope header and payload. + +### Item payload shape + +```json +{ + "event_id": "a1b2c3d4e5f6478990a1b2c3d4e5f6789", + "timestamp": "2026-06-22T10:00:00Z", + "platform": "other", + "contexts": { + "feedback": { + "message": "## Summary\n...\n\n## Error\n...", + "contact_email": "jane@example.com", + "name": "Jane Schmidt" + } + }, + "tags": { + "source": "agent-session" + } +} +``` + +- `message` — required; sanitized markdown debug narrative (no email/username inside). +- `contact_email`, `name` — optional; ask the user. Omitting them may trigger [spam classification](https://docs.sentry.io/product/user-feedback/#spam-detection-for-user-feedback). If the user does not provide them, submit without these fields. +- `associated_event_id` — optional; set only when linking to an existing Sentry error from the app. + +### Contact fields and spam (LLM classifier) + +Sentry classifies feedback with an **internal LLM** — not only missing contact fields. Submissions that look like automated tests, contain words like "verify" / "agent test", or use invented emails often land in **Spam** even with `name` and `contact_email` set. + +The agent should: + +1. Ask for the developer's **real** name and **work email** before submit. +2. Set `contexts.feedback.name`, `contexts.feedback.contact_email`, and mirror in top-level `user`: `{ "email": "…", "name": "…" }`. +3. Add `contexts.feedback.url` when a dev URL or repro page is known. +4. Add `contexts.feedback.associated_event_id` when linking to a Sentry error from the same project. +5. Write `message` as a **real bug report** (summary, repro, errors) — not a connectivity/diagnostic ping. +6. Never invent name or email. Keep contact info out of the `message` body. + +### Generate `event_id` and timestamp + +**Do not use `uuidgen`** — it is not available on Windows Git Bash and is not a project dependency. + +Use Node.js built-in `crypto` (no extra packages): + +```bash +EVENT_ID="$(node -e "console.log(require('crypto').randomUUID().replace(/-/g,'').toLowerCase())")" +SENT_AT="$(node -e "console.log(new Date().toISOString())")" +``` + +### curl example (full) + +Build the payload with Node so `message` is JSON-escaped: + +```bash +EVENT_ID="$(node -e "console.log(require('crypto').randomUUID().replace(/-/g,'').toLowerCase())")" +SENT_AT="$(node -e "console.log(new Date().toISOString())")" +export EVENT_ID SENT_AT +export FEEDBACK_NAME='Jane Schmidt' +export FEEDBACK_EMAIL='jane@example.com' +export MESSAGE='## Summary +Sanitized markdown here.' + +node -e " +const eventId = process.env.EVENT_ID; +const sentAt = process.env.SENT_AT; +const message = process.env.MESSAGE; +const name = process.env.FEEDBACK_NAME || ''; +const email = process.env.FEEDBACK_EMAIL || ''; +const feedback = { message }; +if (name) feedback.name = name; +if (email) feedback.contact_email = email; +const header = JSON.stringify({ event_id: eventId, sent_at: sentAt, sdk: { name: 'shapediver.agent-skills', version: '1.0.0' } }); +const itemHeader = JSON.stringify({ type: 'feedback' }); +const payload = JSON.stringify({ + event_id: eventId, + timestamp: sentAt, + platform: 'other', + contexts: { feedback }, + tags: { source: 'agent-session' }, +}); +process.stdout.write(header + '\n' + itemHeader + '\n' + payload + '\n'); +" | curl -s -w "\nHTTP_STATUS:%{http_code}\n" \ + "https://o363881.ingest.us.sentry.io/api/4511609191137280/envelope/?sentry_key=15feecf633eddb011ac60e804cbe399a" \ + -H 'Content-Type: application/x-sentry-envelope' \ + --data-binary @- + +echo "EVENT_ID=$EVENT_ID" +``` + +Set `MESSAGE` to the sanitized markdown before running. For a minimal heredoc-only variant (fixed message, no newlines in JSON): + +```bash +EVENT_ID="$(node -e "console.log(require('crypto').randomUUID().replace(/-/g,'').toLowerCase())")" +SENT_AT="$(node -e "console.log(new Date().toISOString())")" + +curl "https://o363881.ingest.us.sentry.io/api/4511609191137280/envelope/?sentry_key=15feecf633eddb011ac60e804cbe399a" \ + -H 'Content-Type: application/x-sentry-envelope' \ + --data-binary @- < 200 lines; prefer line ranges around the error. +4. List configuration key **names** only — never secret values. +5. Re-read the final text — scan for `=`, `://`, and base64 blobs. + +### Safe to include + +- Error messages and stack traces (after redaction pass) +- File paths and line numbers +- Package names and semver ranges +- Git branch names and commit messages (not commit author email unless opted in) +- Test output (sanitized) + +## Links + +- [User Feedback product overview](https://docs.sentry.io/product/user-feedback/) +- [Spam detection for User Feedback](https://docs.sentry.io/product/user-feedback/#spam-detection-for-user-feedback) +- [Set Up User Feedback (JavaScript)](https://docs.sentry.io/platforms/javascript/user-feedback/) +- [Feedback telemetry (envelope protocol)](https://develop.sentry.dev/sdk/telemetry/feedbacks/)