From b04792a126ca49bef7389b7e595444ced66bfd48 Mon Sep 17 00:00:00 2001 From: Eli Pinkerton Date: Sat, 10 Oct 2026 04:51:34 +0000 Subject: [PATCH] Format Markdown at a 180 column width Users asked for the 180 line-length concept to cover Markdown too, CHANGELOG in particular, whose entries wrapped at 80. Add Prettier (pinned 3.9.9, matching unity-helpers) with printWidth 180 and proseWrap always, reflow 19 tracked Markdown files, and gate the check in the llm-lint workflow on both OSes with a Markdown trigger path. The generated skills index and SKILL.md frontmatter contracts are excluded through .prettierignore: the harness owns their format and requires single-line frontmatter. Escaping the pipes in code spans inside one forbidden-patterns table row fixes a pre-existing rendering defect the formatter exposed; a per-table pipe-count sweep found no other instance. Validation: format check, llm lints, file lengths, line endings, strict mkdocs build, npm pack, and 23/23 harness self-tests pass. Markdown rendering is content-identical (verified by whitespace- normalized comparison of all files against HEAD); word count moves by one re-wrapped blockquote marker. No C# changed, so Unity suites carry over from 73b8beb (357/357, 15/15). --- .devcontainer/README.md | 145 ++++------- .github/workflows/llm-lint.yml | 14 + .gitignore | 1 + .llm/context.md | 327 +++++++++--------------- .llm/mcp/README.md | 274 ++++++++------------ .llm/references/RELEASING.md | 142 +++++------ .llm/references/forbidden-patterns.md | 72 +++--- .llm/references/product-decisions.md | 51 ++-- .llm/references/roadmap.md | 351 +++++++++----------------- .llm/skills/ship-changes/SKILL.md | 3 + .prettierignore | 15 ++ .prettierrc.json | 6 + CHANGELOG.md | 43 ++-- README.md | 119 ++++++--- docs/extending.md | 119 +++------ docs/getting-started.md | 110 +++----- docs/index.md | 34 +-- docs/managing-assets.md | 96 +++---- docs/organizing.md | 135 ++++------ docs/search-and-filter.md | 117 +++------ docs/troubleshooting.md | 153 ++++------- package.json | 44 +--- 22 files changed, 906 insertions(+), 1465 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 15a620a..736e9b3 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,112 +1,73 @@ # Data Visualizer dev container -A fast, reliable VS Code devcontainer for working on this Unity UPM package, -with every agentic coding frontend pre-installed and wired to a shared MCP -server fleet. +A fast, reliable VS Code devcontainer for working on this Unity UPM package, with every agentic coding frontend pre-installed and wired to a shared MCP server fleet. ## What's inside -| Layer | Tools | -| --- | --- | -| Runtimes | .NET SDK (base image), Node.js LTS via NVM, Python 3 + uv/uvx | -| Coding CLIs | `@nanocollective/nanocoder`, `@opencode/cli` (OpenCode v2), `@openai/codex`, `@anthropic-ai/claude-code` (npm `latest`, refreshed on create and every start) | -| Gateway backends | `codex-zai`, `claude-zai` (Z.ai GLM Coding Plan) and `codex-openrouter`, `claude-openrouter` (OpenRouter) launchers | -| Repo tooling | PowerShell (lint scripts), CSharpier via `dotnet tool restore` | -| MCP servers | unity (official Unity CLI, ~140 tools), github (all toolsets), context7, fetch, git, zai-vision, zai-web-search, zai-image — auto-configured into Claude Code, Codex, OpenCode, Nanocoder, VS Code, and Cursor | -| CLI comfort | ripgrep, fd, bat, fzf, zoxide, jq, tree, htop, ncdu | +| Layer | Tools | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Runtimes | .NET SDK (base image), Node.js LTS via NVM, Python 3 + uv/uvx | +| Coding CLIs | `@nanocollective/nanocoder`, `@opencode/cli` (OpenCode v2), `@openai/codex`, `@anthropic-ai/claude-code` (npm `latest`, refreshed on create and every start) | +| Gateway backends | `codex-zai`, `claude-zai` (Z.ai GLM Coding Plan) and `codex-openrouter`, `claude-openrouter` (OpenRouter) launchers | +| Repo tooling | PowerShell (lint scripts), CSharpier via `dotnet tool restore` | +| MCP servers | unity (official Unity CLI, ~140 tools), github (all toolsets), context7, fetch, git, zai-vision, zai-web-search, zai-image — auto-configured into Claude Code, Codex, OpenCode, Nanocoder, VS Code, and Cursor | +| CLI comfort | ripgrep, fd, bat, fzf, zoxide, jq, tree, htop, ncdu | ## Why `npm install` never needs sudo -The npm coding CLIs are installed **as the `vscode` user** during the image -build. The base image's NVM directory is group-writable through the stable -`nvm` group, and the global npm prefix is `chgrp nvm` + `chmod g+rws` so the -tree survives Dev Containers' UID remapping. `install-npm-tools.sh` verifies -every required path is writable before doing anything and **fails with a -remediation message instead of invoking sudo** or creating mixed-ownership -installs. If npm is temporarily unreachable at container start, the -image-baked versions keep working (bounded offline checks with -`--offline-ok`). +The npm coding CLIs are installed **as the `vscode` user** during the image build. The base image's NVM directory is group-writable through the stable `nvm` group, and the global +npm prefix is `chgrp nvm` + `chmod g+rws` so the tree survives Dev Containers' UID remapping. `install-npm-tools.sh` verifies every required path is writable before doing anything +and **fails with a remediation message instead of invoking sudo** or creating mixed-ownership installs. If npm is temporarily unreachable at container start, the image-baked +versions keep working (bounded offline checks with `--offline-ok`). ## Open speed and rebuilds -- The Dockerfile builds with Docker layer cache **enabled** (unlike NovaSharp's - `--no-cache` setup): cold builds take a few minutes, rebuilds are seconds. -- The moving npm `latest` dist-tags are resolved at container create/start by - `install-npm-tools.sh`, which only installs tools whose installed version - differs — a no-op when everything is current. -- Named volumes persist npm + uv caches and every agent's auth home - (`~/.codex`, `~/.claude`, `~/.claude-zai`, `~/.claude-openrouter`, - `~/.local/share/opencode`, `~/.config/nanocoder`) across **full rebuilds**: - log in once. The image pre-creates those mount targets with `vscode` - ownership so fresh volumes start writable (Docker seeds a new volume from the - image path when it exists, and from `root:root` when it does not); - `post-start.sh` and the launchers additionally self-heal any root-owned - volume in place via passwordless sudo. +- The Dockerfile builds with Docker layer cache **enabled** (unlike NovaSharp's `--no-cache` setup): cold builds take a few minutes, rebuilds are seconds. +- The moving npm `latest` dist-tags are resolved at container create/start by `install-npm-tools.sh`, which only installs tools whose installed version differs — a no-op when + everything is current. +- Named volumes persist npm + uv caches and every agent's auth home (`~/.codex`, `~/.claude`, `~/.claude-zai`, `~/.claude-openrouter`, `~/.local/share/opencode`, + `~/.config/nanocoder`) across **full rebuilds**: log in once. The image pre-creates those mount targets with `vscode` ownership so fresh volumes start writable (Docker seeds a + new volume from the image path when it exists, and from `root:root` when it does not); `post-start.sh` and the launchers additionally self-heal any root-owned volume in place via + passwordless sudo. ## Credentials: `.env.local` first -Secrets live in a gitignored `.env.local` at the workspace root (template: -`.env.local.example`). `on-create.sh` bootstraps it (auto-generating -`UNITY_MCP_TOKEN`) and sources it into every interactive shell via a marker- -delimited `~/.bashrc` block. The gateway launchers additionally **self-load -`.env.local`** (workspace root beside the script, then the current directory, -or `AI_BACKENDS_ENV_FILE`), so agent Bash tools, tasks, and CI — which skip rc -files — resolve credentials too. Variables already exported in the -environment always win over the file. `remoteEnv` additionally forwards -`GITHUB_PERSONAL_ACCESS_TOKEN`, `GITHUB_TOKEN`, `ZAI_API_KEY`, and -`OPENROUTER_API_KEY` from the host shell as fallbacks. Prefer `.env.local`; -never commit it. +Secrets live in a gitignored `.env.local` at the workspace root (template: `.env.local.example`). `on-create.sh` bootstraps it (auto-generating `UNITY_MCP_TOKEN`) and sources it +into every interactive shell via a marker- delimited `~/.bashrc` block. The gateway launchers additionally **self-load `.env.local`** (workspace root beside the script, then the +current directory, or `AI_BACKENDS_ENV_FILE`), so agent Bash tools, tasks, and CI — which skip rc files — resolve credentials too. Variables already exported in the environment +always win over the file. `remoteEnv` additionally forwards `GITHUB_PERSONAL_ACCESS_TOKEN`, `GITHUB_TOKEN`, `ZAI_API_KEY`, and `OPENROUTER_API_KEY` from the host shell as +fallbacks. Prefer `.env.local`; never commit it. Regenerate the machine-local MCP configs any time with `npm run mcp:sync`. ## Native and gateway backends -`codex` and `claude` keep their native OpenAI/Anthropic backends. The gateway -launchers are per-process opt-ins installed by -`bash .devcontainer/ai-backends.sh install` (run at create and start): - -- `codex-zai` uses a `devcontainer-zai` profile against Z.ai's Responses API - with a pinned glm-5.3 model catalog. -- `claude-zai` uses Z.ai's Anthropic-compatible endpoint with a separate - `~/.claude-zai` state directory, so native credentials and sessions stay - isolated. The launcher keeps `ZAI_API_KEY` (and the `Z_AI_API_KEY` alias) - exported — the same secret already present as `ANTHROPIC_AUTH_TOKEN` — so - project `.mcp.json` `${ZAI_API_KEY}` expansion reaches MCP servers; - `zai-web-search` sends an empty bearer and `zai-vision` refuses to start - without it. -- `codex-openrouter` uses a `devcontainer-openrouter` profile against - OpenRouter's OpenAI-compatible Responses API (`https://openrouter.ai/api/v1`) - with command-backed auth, which unlocks OpenRouter's live model catalog. -- `claude-openrouter` uses OpenRouter's Anthropic skin - (`ANTHROPIC_BASE_URL=https://openrouter.ai/api`, `ANTHROPIC_AUTH_TOKEN`, - explicitly empty `ANTHROPIC_API_KEY`) with an isolated `~/.claude-openrouter` - state directory and gateway model discovery enabled. -- Credentials come from `ZAI_API_KEY`/`Z_AI_API_KEY` and - `OPENROUTER_API_KEY`/`OPEN_ROUTER_API_KEY` (`.env.local` is self-loaded) and - are never written to a repository or generated profile. -- Z.ai overrides: `CODEX_ZAI_MODEL`, `CODEX_ZAI_REASONING_EFFORT` - (`low|high|max`), `ZAI_API_TIMEOUT_MS`, `CLAUDE_ZAI_CONFIG_DIR`, - `CLAUDE_ZAI_{SONNET,OPUS,HAIKU}_MODEL`. -- OpenRouter overrides: `CODEX_OPENROUTER_MODEL`, - `CODEX_OPENROUTER_REASONING_EFFORT` (`low|medium|high|xhigh|max`), - `OPENROUTER_API_TIMEOUT_MS`, `CLAUDE_OPENROUTER_CONFIG_DIR`, - `CLAUDE_OPENROUTER_{FABLE,SONNET,OPUS,HAIKU}_MODEL`, - `CLAUDE_OPENROUTER_SUBAGENT_MODEL`, `CLAUDE_OPENROUTER_GATEWAY_DISCOVERY`. -- Contract tests live in `.devcontainer/tests/test-ai-backends.sh` (stubs, no - network) and `.devcontainer/tests/test-mcp-sync.sh` (syncs into a throwaway - workspace; asserts fleet parity across frontends and that no secrets land on - disk). Both run via `scripts/tests/run-all.ps1` in CI. - -Inside the container Claude's experimental Linux subprocess scrubber is -disabled by default (`AI_BACKENDS_CLAUDE_SUBPROCESS_ENV_SCRUB=0`; legacy -`CLAUDE_ZAI_SUBPROCESS_ENV_SCRUB` stays honored): Docker's seccomp -profile blocks bubblewrap's namespaces, and the outer devcontainer is already -the isolation boundary. Outside a container the stronger scrubber is retained. +`codex` and `claude` keep their native OpenAI/Anthropic backends. The gateway launchers are per-process opt-ins installed by `bash .devcontainer/ai-backends.sh install` (run at +create and start): + +- `codex-zai` uses a `devcontainer-zai` profile against Z.ai's Responses API with a pinned glm-5.3 model catalog. +- `claude-zai` uses Z.ai's Anthropic-compatible endpoint with a separate `~/.claude-zai` state directory, so native credentials and sessions stay isolated. The launcher keeps + `ZAI_API_KEY` (and the `Z_AI_API_KEY` alias) exported — the same secret already present as `ANTHROPIC_AUTH_TOKEN` — so project `.mcp.json` `${ZAI_API_KEY}` expansion reaches MCP + servers; `zai-web-search` sends an empty bearer and `zai-vision` refuses to start without it. +- `codex-openrouter` uses a `devcontainer-openrouter` profile against OpenRouter's OpenAI-compatible Responses API (`https://openrouter.ai/api/v1`) with command-backed auth, which + unlocks OpenRouter's live model catalog. +- `claude-openrouter` uses OpenRouter's Anthropic skin (`ANTHROPIC_BASE_URL=https://openrouter.ai/api`, `ANTHROPIC_AUTH_TOKEN`, explicitly empty `ANTHROPIC_API_KEY`) with an + isolated `~/.claude-openrouter` state directory and gateway model discovery enabled. +- Credentials come from `ZAI_API_KEY`/`Z_AI_API_KEY` and `OPENROUTER_API_KEY`/`OPEN_ROUTER_API_KEY` (`.env.local` is self-loaded) and are never written to a repository or generated + profile. +- Z.ai overrides: `CODEX_ZAI_MODEL`, `CODEX_ZAI_REASONING_EFFORT` (`low|high|max`), `ZAI_API_TIMEOUT_MS`, `CLAUDE_ZAI_CONFIG_DIR`, `CLAUDE_ZAI_{SONNET,OPUS,HAIKU}_MODEL`. +- OpenRouter overrides: `CODEX_OPENROUTER_MODEL`, `CODEX_OPENROUTER_REASONING_EFFORT` (`low|medium|high|xhigh|max`), `OPENROUTER_API_TIMEOUT_MS`, `CLAUDE_OPENROUTER_CONFIG_DIR`, + `CLAUDE_OPENROUTER_{FABLE,SONNET,OPUS,HAIKU}_MODEL`, `CLAUDE_OPENROUTER_SUBAGENT_MODEL`, `CLAUDE_OPENROUTER_GATEWAY_DISCOVERY`. +- Contract tests live in `.devcontainer/tests/test-ai-backends.sh` (stubs, no network) and `.devcontainer/tests/test-mcp-sync.sh` (syncs into a throwaway workspace; asserts fleet + parity across frontends and that no secrets land on disk). Both run via `scripts/tests/run-all.ps1` in CI. + +Inside the container Claude's experimental Linux subprocess scrubber is disabled by default (`AI_BACKENDS_CLAUDE_SUBPROCESS_ENV_SCRUB=0`; legacy `CLAUDE_ZAI_SUBPROCESS_ENV_SCRUB` +stays honored): Docker's seccomp profile blocks bubblewrap's namespaces, and the outer devcontainer is already the isolation boundary. Outside a container the stronger scrubber is +retained. ## Unity workflow (host + container) -Unity's official CLI MCP server (`unity mcp`, ~140 tools) runs on the -**host**; agents in the container reach it over `host.docker.internal`: +Unity's official CLI MCP server (`unity mcp`, ~140 tools) runs on the **host**; agents in the container reach it over `host.docker.internal`: ```bash # On the host (once per project): @@ -119,13 +80,9 @@ npm run unity:mcp:host # bridges `unity mcp` on http://0.0.0.0:9020/ unity mcp configure ``` -Container agents connect to `http://host.docker.internal:9020/mcp` -(configured automatically by `npm run mcp:sync`). Full details: -`.llm/mcp/README.md`. +Container agents connect to `http://host.docker.internal:9020/mcp` (configured automatically by `npm run mcp:sync`). Full details: `.llm/mcp/README.md`. ## Host networking note (Linux) -On Linux hosts the container reaches host services via the `host-gateway` -add-host in `runArgs`. Keep wired Ethernet and Wi-Fi from being active -simultaneously on the same IPv4 subnet (weak-host ARP ambiguity breaks -long-lived model streams). macOS/Docker Desktop is unaffected. +On Linux hosts the container reaches host services via the `host-gateway` add-host in `runArgs`. Keep wired Ethernet and Wi-Fi from being active simultaneously on the same IPv4 +subnet (weak-host ARP ambiguity breaks long-lived model streams). macOS/Docker Desktop is unaffected. diff --git a/.github/workflows/llm-lint.yml b/.github/workflows/llm-lint.yml index d434537..e67be30 100644 --- a/.github/workflows/llm-lint.yml +++ b/.github/workflows/llm-lint.yml @@ -18,6 +18,10 @@ on: - ".pre-commit-config.yaml" - ".editorconfig" - ".gitattributes" + - ".prettierrc.json" + - ".prettierignore" + - ".csharpierrc" + - "**/*.md" - ".devcontainer/**" - "Editor/**/*.cs" - "Runtime/**/*.cs" @@ -41,6 +45,10 @@ on: - ".pre-commit-config.yaml" - ".editorconfig" - ".gitattributes" + - ".prettierrc.json" + - ".prettierignore" + - ".csharpierrc" + - "**/*.md" - ".devcontainer/**" - "Editor/**/*.cs" - "Runtime/**/*.cs" @@ -74,6 +82,12 @@ jobs: with: persist-credentials: false + - name: Install npm dependencies + run: npm install --no-audit --no-fund + + - name: Check Markdown formatting + run: npm run format:md:check + - name: Lint LLM instructions run: pwsh -NoProfile -File scripts/lint-llm-instructions.ps1 -VerboseOutput diff --git a/.gitignore b/.gitignore index 434e0a3..ee8b6e7 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ PLAN.md* # npm writes this stub (no runtime deps); not used by the Unity package package-lock.json +node_modules/ diff --git a/.llm/context.md b/.llm/context.md index 629cba3..a3fb84d 100644 --- a/.llm/context.md +++ b/.llm/context.md @@ -1,9 +1,7 @@ # LLM Agent Instructions -Procedural skills are in the [skills/](./skills/) directory. Frontend entrypoints -(`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, `.windsurfrules`, -`.github/copilot-instructions.md`) are thin pointers to this file. Skills are -standard [Agent Skills](https://agentskills.io) (`SKILL.md` + frontmatter). +Procedural skills are in the [skills/](./skills/) directory. Frontend entrypoints (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md`) +are thin pointers to this file. Skills are standard [Agent Skills](https://agentskills.io) (`SKILL.md` + frontmatter). ## Repository Overview @@ -13,11 +11,9 @@ standard [Agent Skills](https://agentskills.io) (`SKILL.md` + frontmatter). - **Root namespace**: `WallstopStudios.DataVisualizer` (editor: `WallstopStudios.DataVisualizer.Editor`, tests: `WallstopStudios.DataVisualizer.Tests.Editor`) - **License**: MIT - **Repository**: https://github.com/wallstop/DataVisualizer -- A Unity UPM package providing a UI Toolkit editor window for managing, inspecting, - and batch-editing ScriptableObject assets, plus a small runtime API - (`BaseDataObject`, lifecycle interfaces, display attributes). -- This folder is the git repo root (nested under the Unity project's `Packages/`). - Open agent frontends with this folder as the workspace root. +- A Unity UPM package providing a UI Toolkit editor window for managing, inspecting, and batch-editing ScriptableObject assets, plus a small runtime API (`BaseDataObject`, + lifecycle interfaces, display attributes). +- This folder is the git repo root (nested under the Unity project's `Packages/`). Open agent frontends with this folder as the workspace root. ## Project Structure @@ -48,169 +44,95 @@ scripts/ PowerShell automation for this .llm harness ## Skills Reference -See the generated [Skills Index](./skills/index.md). Regenerate it after adding or -editing any skill with `pwsh -NoProfile -File scripts/generate-skills-index.ps1` -(validated by `scripts/lint-llm-instructions.ps1`). +See the generated [Skills Index](./skills/index.md). Regenerate it after adding or editing any skill with `pwsh -NoProfile -File scripts/generate-skills-index.ps1` (validated by +`scripts/lint-llm-instructions.ps1`). ## Critical Rules ### C# and Unity -1. Target C# 10 with 4-space indentation and block-scoped namespaces; place `using` - directives inside the namespace, matching existing files. `npm run lint:csharp-usings` - enforces this; file-level usings are sanctioned only for namespaceless - assembly-attribute files and `[assembly: ...]` preambles such as `InternalsVisibleTo` - (#124). -2. Format every modified `.cs` file with CSharpier 1.1.2 (print width 180 in - `.csharpierrc`; `dotnet tool run csharpier -- format `); gate on it. +1. Target C# 10 with 4-space indentation and block-scoped namespaces; place `using` directives inside the namespace, matching existing files. `npm run lint:csharp-usings` enforces + this; file-level usings are sanctioned only for namespaceless assembly-attribute files and `[assembly: ...]` preambles such as `InternalsVisibleTo` (#124). +2. Format every modified `.cs` file with CSharpier 1.1.2 (print width 180 in `.csharpierrc`; `dotnet tool run csharpier -- format `) and every modified `.md` file with + Prettier (print width 180 in `.prettierrc.json`; `npm run format:md`); treat both as gates. Generated and skill Markdown is exempt (`.prettierignore`), and table cells align to + their widest cell, so table lines may exceed the width. 3. Resolve analyzer warnings before review. 4. Prefer explicit namespaces so the window's namespace/type tree stays predictable. -5. Avoid runtime reflection and stringly-typed lookups; wire menu items, property - paths, and analytics IDs with `nameof`. -6. ScriptableObjects end in `Data`, `Settings`, or `Profile`; editor windows end in - `Window`. -7. Private serialized fields are camelCase with `[SerializeField]`; add - `[FormerlySerializedAs]` when renaming anything already saved to disk. -8. Do not rename serialized fields, asset paths, or EditorPrefs keys without a - persistence story (user state JSON and settings assets carry saved data). -9. `InternalsVisibleTo` is not honored for the editor->tests assembly pair in - Unity's compilation; helpers that tests must exercise are `public` (see - `ObjectIdExtensions`, `LabelFilterEvaluator`). -10. Guard editor-only APIs under `Runtime/` with `#if UNITY_EDITOR`; the runtime - assembly must compile without editor references. -11. Unity 6000.5+ compatibility: prefer `EntityId` over the retired - `GetInstanceID` flow where the migration applies. -12. Custom `Try*` methods return `bool` and expose successful values through `out` - parameters. Use names such as `*OrNull`, `*OrDefault`, or `*IfAvailable` when a - method instead returns a sentinel or performs a best-effort action. -13. Assign every custom `out` parameter just in time, directly before each return; - do not initialize outputs at method entry and rely on later branches to replace - them. -14. Collection mutation helpers return a changed flag or affected-item count when - callers can use it for persistence decisions, diagnostics, or observability. -15. Before replacing `RemoveAt` with swap-back or `RemoveAll`, check multiplicity - and order semantics. A single stable removal is linear, not quadratic; swap-back - is valid only for unordered collections, and `RemoveAll` is for predicate-based - bulk removal. -16. Use read-only collection interfaces for consumers. Mutation helpers that resize - a collection should accept the concrete supported mutable type when a broader - interface permits invalid inputs or adds measured dispatch cost; specialize only - for collection shapes used by production callers. -17. Unity generates `.meta` files for every non-dot path; commit them alongside new - files. Dot-folders (`.llm`, `.github`) never get `.meta` files. -18. Keep tests, docs imagery, and agent/dev files out of the npm tarball: - `package.json` `files` whitelists only `Editor`, `Runtime`, and their `.meta` - companions (docs imagery ships via GitHub, not npm). Static - analyzer binaries, Unity metadata/labels, notices, and `-analyzer` response-file - arguments must not exist in this package repository; install development-only - analyzers in the host Unity project instead. Install or verify the pinned - analyzer sets with `scripts/install-host-errorprone-analyzers.ps1 -HostProject ` - (`-AnalyzerSet SonarAnalyzer|All` selects sets; add `-VerifyOnly` for an - offline integrity check). -19. Write explicit ordered comparisons with `<` or `<=`, reversing operands instead - of using `>` or `>=`. Relational patterns retain the operator required by C# - syntax. Keep `!=` when expressing genuine inequality or null checks; do not wrap - `==` in a negation merely to avoid it. Before reversing user-defined operators, - verify the swapped operands and paired operator preserve behavior. -20. Prefer one declared class, struct, interface, or enum per `.cs` file. Unity object - types must use a matching filename so Unity can associate the script reliably. - Keep any deliberately nonconforming regression fixture isolated and document why - it must model an external project's unsupported layout. -21. Use `foreach` for arrays and concrete collections with value-type enumerators when - the loop does not need an index. For variables typed as `IReadOnlyList` or - `IList`, use a counted loop to avoid interface-enumerator allocation. Retain - `foreach` when the input is a stream or only exposes `IEnumerable`. The C# source - lint flags `for (int i = 0; i < x.Length; ...)` loops whose index is used only to read - `x[i]`; `.Count` loops stay review-enforced because the declared type is not visible to - a source check. Owner-enforced in review (PR #98): sweep new and touched loops for this - rule before requesting review. -22. Never use bitwise `|`, `&`, `|=`, or `&=` to aggregate Boolean results. Evaluate - every side-effectful operation into its own named Boolean, then combine results - with `||` or `&&`. Bitwise operators remain correct for flags and numeric values. -23. Give mutable lifecycle/state machines one transition owner. Helpers may gather or - process data, but they must not scatter direct state writes or scheduler ownership - across methods; route transitions through one named runner/transition method. -24. Every enum member has an explicit numeric value. Reserve zero for an invalid - `Unknown`, `None`, or compatibility sentinel marked `[Obsolete("Please use a valid - value")]`; valid values must be nonzero. Preserve or explicitly migrate existing - serialized ordinals rather than renumbering them accidentally. -25. Mutable services are instantiable sealed classes. Put state, events, and lifecycle - callbacks on the instance, and expose only one explicit static shared instance when - production needs global coordination. Static classes remain appropriate for pure - functions, extension methods, constants, and platform interop. Static shared state - must document why it is static (cross-instance coordination), key by lifecycle - owner with weak references where the owner is disposable, unregister on every - removal path, and be reset by the owning window's cleanup - (`AssetGuidTypeIndex.Shared` and the theme-selection ownership registry are the - precedents). -26. Within each C# type, order members as: constants, events, delegates, static - properties, static fields, properties, fields, constructors, static methods, - methods, then nested types. Within every tier use public, protected, internal, - private. Do not reorder across conditional-compilation boundaries or static - initialization dependencies. Run `npm run lint:csharp-member-order`; use its - `:fix` variant for safe source-slice permutations, then resolve reported barriers - manually. -27. Name classes, structs, enums, records, and delegates in PascalCase; interfaces - use PascalCase with an `I` prefix. Do not carry all-caps native typedef spellings - into managed type names. Use a descriptive prefix such as `Native` when the - idiomatic name would collide with a Unity or framework type. `.editorconfig` - enforces these declarations as warnings. -28. Name every C# method in PascalCase without underscores, including test methods. - The C# source lint enforces this convention independently of IDE diagnostics. -29. Use `//` for standalone single-line comments and tool control directives only. - Write ordinary multi-line explanations as one indented `/* ... */` block. Keep - `///` XML documentation line-oriented. The C# source lint enforces this rule. -30. Prefer public Unity Editor geometry and display APIs before adding platform-native - bindings. If a native boundary remains necessary, verify its ABI with primary - platform documentation, represent pointer-sized native values with pointer-sized - managed types, and document every supported platform branch that was not executed - during validation. -31. Represent reversible state, resource cleanup, and mandatory exit work with a named - `IDisposable` scope consumed directly by `using`, not a hand-written `try/finally`. - Register cleanup before the first side effect. Production hot paths prefer copy-safe - readonly struct leases backed by reusable owner storage; leases are default-safe and - idempotent, and `Dispose()` never throws. Retain `try/catch` where the purpose is - exception isolation or translation rather than lifetime management. -32. Keep the shipped `Runtime/` assembly, persisted editor-state models under - `Editor/DataVisualizer/Data/`, global-search models under - `Editor/DataVisualizer/Search/`, asset postprocessing under - `Editor/DataVisualizer/Unity/`, and the namespace/type catalog controller - `Editor/DataVisualizer/NamespaceController.cs` free of `System.Linq`. Use direct - collection operations and streaming loops so player-facing helpers, state - transfers, per-result search aggregation, per-import asset-change handling, and - namespace/type catalog construction do not introduce avoidable iterator/delegate - allocations. Other editor and test code may retain LINQ when clarity outweighs - measured cost. -33. Before caching or pooling a temporary collection, first try eliminating the - scratch state. Introduce pooling only after measured repeated reuse, with a defined - lifetime, invalidation owner, retention bound, and overlapping-call safety. Do not - create a type-specific pool speculatively; generalize only when multiple proven - call sites share the same comparer, reset, and retention semantics. Prefer direct - scans for genuinely bounded small inputs and per-operation sets for unbounded - membership work. -34. Compare and sort package-owned identifiers (asset labels, namespace keys, type - names, paths, GUIDs) with `StringComparison.Ordinal` or `StringComparer.Ordinal`; - use `OrdinalIgnoreCase` when casing is intentionally ignored. Reserve - culture-sensitive comparisons for user-authored prose. Bare `Sort()`, - `string.Compare(a, b)`, `StartsWith(prefix)`, and `OrderBy(key)` on identifiers - sort or match with the current culture and reorder across machines. Prefer static - `string.Equals(a, b, StringComparison)` over instance `a.Equals(...)` so a null - operand compares as a value instead of throwing. -35. Never use `Assert.IsNull`/`Assert.IsNotNull`, the `Is.Null`/`Is.Not.Null` - constraints, `?.`, `??`, or an implicit bool test on a `UnityEngine.Object`. Unity - overloads the equality operators, so a destroyed-object wrapper compares null only - through `== null` / `!= null`; the other forms use reference equality and miss that - lifetime. Write `Assert.That(value == null)` / `Assert.That(value != null)`. The C# - source lint enforces the assertion and constraint forms; the `?.`/`??`/bool shapes - are review-enforced (#117). +5. Avoid runtime reflection and stringly-typed lookups; wire menu items, property paths, and analytics IDs with `nameof`. +6. ScriptableObjects end in `Data`, `Settings`, or `Profile`; editor windows end in `Window`. +7. Private serialized fields are camelCase with `[SerializeField]`; add `[FormerlySerializedAs]` when renaming anything already saved to disk. +8. Do not rename serialized fields, asset paths, or EditorPrefs keys without a persistence story (user state JSON and settings assets carry saved data). +9. `InternalsVisibleTo` is not honored for the editor->tests assembly pair in Unity's compilation; helpers that tests must exercise are `public` (see `ObjectIdExtensions`, + `LabelFilterEvaluator`). +10. Guard editor-only APIs under `Runtime/` with `#if UNITY_EDITOR`; the runtime assembly must compile without editor references. +11. Unity 6000.5+ compatibility: prefer `EntityId` over the retired `GetInstanceID` flow where the migration applies. +12. Custom `Try*` methods return `bool` and expose successful values through `out` parameters. Use names such as `*OrNull`, `*OrDefault`, or `*IfAvailable` when a method instead + returns a sentinel or performs a best-effort action. +13. Assign every custom `out` parameter just in time, directly before each return; do not initialize outputs at method entry and rely on later branches to replace them. +14. Collection mutation helpers return a changed flag or affected-item count when callers can use it for persistence decisions, diagnostics, or observability. +15. Before replacing `RemoveAt` with swap-back or `RemoveAll`, check multiplicity and order semantics. A single stable removal is linear, not quadratic; swap-back is valid only for + unordered collections, and `RemoveAll` is for predicate-based bulk removal. +16. Use read-only collection interfaces for consumers. Mutation helpers that resize a collection should accept the concrete supported mutable type when a broader interface permits + invalid inputs or adds measured dispatch cost; specialize only for collection shapes used by production callers. +17. Unity generates `.meta` files for every non-dot path; commit them alongside new files. Dot-folders (`.llm`, `.github`) never get `.meta` files. +18. Keep tests, docs imagery, and agent/dev files out of the npm tarball: `package.json` `files` whitelists only `Editor`, `Runtime`, and their `.meta` companions (docs imagery + ships via GitHub, not npm). Static analyzer binaries, Unity metadata/labels, notices, and `-analyzer` response-file arguments must not exist in this package repository; install + development-only analyzers in the host Unity project instead. Install or verify the pinned analyzer sets with + `scripts/install-host-errorprone-analyzers.ps1 -HostProject ` (`-AnalyzerSet SonarAnalyzer|All` selects sets; add `-VerifyOnly` for an offline integrity check). +19. Write explicit ordered comparisons with `<` or `<=`, reversing operands instead of using `>` or `>=`. Relational patterns retain the operator required by C# syntax. Keep `!=` + when expressing genuine inequality or null checks; do not wrap `==` in a negation merely to avoid it. Before reversing user-defined operators, verify the swapped operands and + paired operator preserve behavior. +20. Prefer one declared class, struct, interface, or enum per `.cs` file. Unity object types must use a matching filename so Unity can associate the script reliably. Keep any + deliberately nonconforming regression fixture isolated and document why it must model an external project's unsupported layout. +21. Use `foreach` for arrays and concrete collections with value-type enumerators when the loop does not need an index. For variables typed as `IReadOnlyList` or `IList`, use + a counted loop to avoid interface-enumerator allocation. Retain `foreach` when the input is a stream or only exposes `IEnumerable`. The C# source lint flags + `for (int i = 0; i < x.Length; ...)` loops whose index is used only to read `x[i]`; `.Count` loops stay review-enforced because the declared type is not visible to a source + check. Owner-enforced in review (PR #98): sweep new and touched loops for this rule before requesting review. +22. Never use bitwise `|`, `&`, `|=`, or `&=` to aggregate Boolean results. Evaluate every side-effectful operation into its own named Boolean, then combine results with `||` or + `&&`. Bitwise operators remain correct for flags and numeric values. +23. Give mutable lifecycle/state machines one transition owner. Helpers may gather or process data, but they must not scatter direct state writes or scheduler ownership across + methods; route transitions through one named runner/transition method. +24. Every enum member has an explicit numeric value. Reserve zero for an invalid `Unknown`, `None`, or compatibility sentinel marked `[Obsolete("Please use a valid value")]`; valid + values must be nonzero. Preserve or explicitly migrate existing serialized ordinals rather than renumbering them accidentally. +25. Mutable services are instantiable sealed classes. Put state, events, and lifecycle callbacks on the instance, and expose only one explicit static shared instance when + production needs global coordination. Static classes remain appropriate for pure functions, extension methods, constants, and platform interop. Static shared state must + document why it is static (cross-instance coordination), key by lifecycle owner with weak references where the owner is disposable, unregister on every removal path, and be + reset by the owning window's cleanup (`AssetGuidTypeIndex.Shared` and the theme-selection ownership registry are the precedents). +26. Within each C# type, order members as: constants, events, delegates, static properties, static fields, properties, fields, constructors, static methods, methods, then nested + types. Within every tier use public, protected, internal, private. Do not reorder across conditional-compilation boundaries or static initialization dependencies. Run + `npm run lint:csharp-member-order`; use its `:fix` variant for safe source-slice permutations, then resolve reported barriers manually. +27. Name classes, structs, enums, records, and delegates in PascalCase; interfaces use PascalCase with an `I` prefix. Do not carry all-caps native typedef spellings into managed + type names. Use a descriptive prefix such as `Native` when the idiomatic name would collide with a Unity or framework type. `.editorconfig` enforces these declarations as + warnings. +28. Name every C# method in PascalCase without underscores, including test methods. The C# source lint enforces this convention independently of IDE diagnostics. +29. Use `//` for standalone single-line comments and tool control directives only. Write ordinary multi-line explanations as one indented `/* ... */` block. Keep `///` XML + documentation line-oriented. The C# source lint enforces this rule. +30. Prefer public Unity Editor geometry and display APIs before adding platform-native bindings. If a native boundary remains necessary, verify its ABI with primary platform + documentation, represent pointer-sized native values with pointer-sized managed types, and document every supported platform branch that was not executed during validation. +31. Represent reversible state, resource cleanup, and mandatory exit work with a named `IDisposable` scope consumed directly by `using`, not a hand-written `try/finally`. Register + cleanup before the first side effect. Production hot paths prefer copy-safe readonly struct leases backed by reusable owner storage; leases are default-safe and idempotent, and + `Dispose()` never throws. Retain `try/catch` where the purpose is exception isolation or translation rather than lifetime management. +32. Keep the shipped `Runtime/` assembly, persisted editor-state models under `Editor/DataVisualizer/Data/`, global-search models under `Editor/DataVisualizer/Search/`, asset + postprocessing under `Editor/DataVisualizer/Unity/`, and the namespace/type catalog controller `Editor/DataVisualizer/NamespaceController.cs` free of `System.Linq`. Use direct + collection operations and streaming loops so player-facing helpers, state transfers, per-result search aggregation, per-import asset-change handling, and namespace/type catalog + construction do not introduce avoidable iterator/delegate allocations. Other editor and test code may retain LINQ when clarity outweighs measured cost. +33. Before caching or pooling a temporary collection, first try eliminating the scratch state. Introduce pooling only after measured repeated reuse, with a defined lifetime, + invalidation owner, retention bound, and overlapping-call safety. Do not create a type-specific pool speculatively; generalize only when multiple proven call sites share the + same comparer, reset, and retention semantics. Prefer direct scans for genuinely bounded small inputs and per-operation sets for unbounded membership work. +34. Compare and sort package-owned identifiers (asset labels, namespace keys, type names, paths, GUIDs) with `StringComparison.Ordinal` or `StringComparer.Ordinal`; use + `OrdinalIgnoreCase` when casing is intentionally ignored. Reserve culture-sensitive comparisons for user-authored prose. Bare `Sort()`, `string.Compare(a, b)`, + `StartsWith(prefix)`, and `OrderBy(key)` on identifiers sort or match with the current culture and reorder across machines. Prefer static + `string.Equals(a, b, StringComparison)` over instance `a.Equals(...)` so a null operand compares as a value instead of throwing. +35. Never use `Assert.IsNull`/`Assert.IsNotNull`, the `Is.Null`/`Is.Not.Null` constraints, `?.`, `??`, or an implicit bool test on a `UnityEngine.Object`. Unity overloads the + equality operators, so a destroyed-object wrapper compares null only through `== null` / `!= null`; the other forms use reference equality and miss that lifetime. Write + `Assert.That(value == null)` / `Assert.That(value != null)`. The C# source lint enforces the assertion and constraint forms; the `?.`/`??`/bool shapes are review-enforced + (#117). ### Skills Discipline -- Skills live at `.llm/skills//SKILL.md` (standard Agent Skills format); - see [manage-skills](./skills/manage-skills/SKILL.md) before creating or editing one. -- Every `.md` file under `.llm/` must stay within the 300-line hard limit - (enforced by `scripts/lint-file-lengths.ps1`); keep SKILL.md bodies lean and move - bulk material to `.llm/code-samples/` or `.llm/references/`. +- Skills live at `.llm/skills//SKILL.md` (standard Agent Skills format); see [manage-skills](./skills/manage-skills/SKILL.md) before creating or editing one. +- Every `.md` file under `.llm/` must stay within the 300-line hard limit (enforced by `scripts/lint-file-lengths.ps1`); keep SKILL.md bodies lean and move bulk material to + `.llm/code-samples/` or `.llm/references/`. - After editing any skill: regenerate the index, then run `npm run lint:llm`. ### Validation Ladder (Run After Each Change) @@ -219,6 +141,8 @@ editing any skill with `pwsh -NoProfile -File scripts/generate-skills-index.ps1` dotnet tool restore dotnet tool run csharpier -- format Editor Runtime Tests dotnet tool run csharpier -- check Editor Runtime Tests +npm run format:md +npm run format:md:check npm run lint:csharp-member-order npm run lint:csharp-null-assertions npm run lint:csharp-usings @@ -228,73 +152,48 @@ pwsh -NoProfile -File scripts/lint-llm-instructions.ps1 -VerboseOutput pwsh -NoProfile -File scripts/lint-file-lengths.ps1 -VerboseOutput pwsh -NoProfile -File scripts/tests/run-all.ps1 npm run lint:llm -npm run lint:llm:fix npm pack unity -projectPath -batchmode -quit -runTests -testPlatform editmode unity -projectPath -batchmode -quit -runTests -testPlatform playmode ``` -Layers: the pre-commit framework hook runs CSharpier, C# member-order, null-assertions, -and the fast `.llm` checks on staged files; -`npm run lint:llm` is the local gate; CI (`.github/workflows/llm-lint.yml`) runs -everything on ubuntu and windows. CSharpier and member ordering run via the -pre-commit config on staged `.cs` files. +Layers: the pre-commit framework hook runs CSharpier, C# member-order, null-assertions, and the fast `.llm` checks on staged files; `npm run lint:llm` is the local gate; CI +(`.github/workflows/llm-lint.yml`) runs everything, including the Prettier Markdown check, on ubuntu and windows. CSharpier and member ordering run via the pre-commit config on +staged `.cs` files. ### Testing -- Unity Test Framework; EditMode specs live in `Tests/Editor`, grouped by feature - (`NamespaceOrderingTests`, `SelectionPersistenceTests`); name methods +- Unity Test Framework; EditMode specs live in `Tests/Editor`, grouped by feature (`NamespaceOrderingTests`, `SelectionPersistenceTests`); name methods `ShouldExpectationWhenCondition`. -- Gate pull requests on both EditMode and PlayMode suites; aim for coverage on - ordering, filtering, and cloning paths before tagging a release. -- Add PlayMode coverage for `BaseDataObject` lifecycle callbacks and asset-state - persistence in a sibling `Tests/Runtime` folder mirroring Unity's layout. +- Gate pull requests on both EditMode and PlayMode suites; aim for coverage on ordering, filtering, and cloning paths before tagging a release. +- Add PlayMode coverage for `BaseDataObject` lifecycle callbacks and asset-state persistence in a sibling `Tests/Runtime` folder mirroring Unity's layout. ### Commit & Pull Requests -- Write all agent-to-user copy (PR titles and bodies, commit messages, code - comments, handoffs, CHANGELOG entries) in Simplified Technical English: - short, plain, active sentences answering why, how, and what, in that order. -- Agent-written PR and issue bodies start with `DISCLOSURE: LLM-GENERATED - TEXT`. CHANGELOG entries: one or two sentences, 300 rendered characters max - (issue links do not count), verb-first, user effect first, no mechanism or - narration; never modify released sections. CHANGELOG.md stays user-facing - only (tests, CI, tooling, and release mechanics belong elsewhere). -- PR titles: 50 characters max, imperative, the user-visible effect. PR - bodies: `**Why:**` one sentence naming the problem, `**What:**` two to five - one-line bullets of at most 12 words each, then `Fixes`/`Refs`. Validation - evidence lives in commits, tests, and issues, never in the body. -- Commit subjects: imperative, 72 characters max, subsystem up front ("Fix - settings persistence dirty state"); the body explains why first and - references related issue IDs. Confirm CSharpier, `npm pack`, and both Unity - test suites before requesting review. -- Version bumps: change `package.json`, then run `npm run lint:llm:fix` so the - version line above is synced, and commit both together - (see [bump-version-release](./skills/bump-version-release/SKILL.md)). +- Write all agent-to-user copy (PR titles and bodies, commit messages, code comments, handoffs, CHANGELOG entries) in Simplified Technical English: short, plain, active sentences + answering why, how, and what, in that order. +- Agent-written PR and issue bodies start with `DISCLOSURE: LLM-GENERATED TEXT`. CHANGELOG entries: one or two sentences, 300 rendered characters max (issue links do not count), + verb-first, user effect first, no mechanism or narration; never modify released sections. CHANGELOG.md stays user-facing only (tests, CI, tooling, and release mechanics belong + elsewhere). +- PR titles: 50 characters max, imperative, the user-visible effect. PR bodies: `**Why:**` one sentence naming the problem, `**What:**` two to five one-line bullets of at most 12 + words each, then `Fixes`/`Refs`. Validation evidence lives in commits, tests, and issues, never in the body. +- Commit subjects: imperative, 72 characters max, subsystem up front ("Fix settings persistence dirty state"); the body explains why first and references related issue IDs. Confirm + CSharpier, `npm pack`, and both Unity test suites before requesting review. +- Version bumps: change `package.json`, then run `npm run lint:llm:fix` so the version line above is synced, and commit both together (see + [bump-version-release](./skills/bump-version-release/SKILL.md)). ### Shared References -- [forbidden patterns](./references/forbidden-patterns.md) - patterns that must not - appear in this codebase, with the compliant alternative for each. +- [forbidden patterns](./references/forbidden-patterns.md) - patterns that must not appear in this codebase, with the compliant alternative for each. ### Dev Container, MCP, and Agent Tooling -- The devcontainer (`.devcontainer/`) installs the four coding CLIs as the - non-root user and refreshes them on create/start; **never** suggest `sudo - npm` — rerun `bash .devcontainer/install-npm-tools.sh` or rebuild instead. -- Gateway launchers (`codex-zai`, `claude-zai`, `codex-openrouter`, - `claude-openrouter`) come from `.devcontainer/ai-backends.sh`. They - self-load the gitignored `.env.local` for keys not already exported - (preexisting environment variables win), so they work in non-interactive - shells too; contract tests live in `.devcontainer/tests/`. -- `npm run mcp:sync` regenerates machine-local MCP configs for Claude Code, - Codex, OpenCode, Nanocoder, VS Code, and Cursor from - `.llm/mcp/configure.mjs`. Those outputs are gitignored and must never be - committed. -- Secrets live in gitignored `.env.local` (template: `.env.local.example`); - config files reference env vars instead of embedding credentials. -- Unity MCP: the **official Unity CLI** MCP server (`unity mcp`, ~140 tools). - Container agents reach it through the host HTTP bridge - (`npm run unity:mcp:host` → `host.docker.internal:`, default 9020 via - `UNITY_MCP_HTTP_PORT`); host GUI clients use `unity mcp configure ` - directly. See [mcp/README.md](./mcp/README.md). +- The devcontainer (`.devcontainer/`) installs the four coding CLIs as the non-root user and refreshes them on create/start; **never** suggest `sudo npm` — rerun + `bash .devcontainer/install-npm-tools.sh` or rebuild instead. +- Gateway launchers (`codex-zai`, `claude-zai`, `codex-openrouter`, `claude-openrouter`) come from `.devcontainer/ai-backends.sh`. They self-load the gitignored `.env.local` for + keys not already exported (preexisting environment variables win), so they work in non-interactive shells too; contract tests live in `.devcontainer/tests/`. +- `npm run mcp:sync` regenerates machine-local MCP configs for Claude Code, Codex, OpenCode, Nanocoder, VS Code, and Cursor from `.llm/mcp/configure.mjs`. Those outputs are + gitignored and must never be committed. +- Secrets live in gitignored `.env.local` (template: `.env.local.example`); config files reference env vars instead of embedding credentials. +- Unity MCP: the **official Unity CLI** MCP server (`unity mcp`, ~140 tools). Container agents reach it through the host HTTP bridge (`npm run unity:mcp:host` → + `host.docker.internal:`, default 9020 via `UNITY_MCP_HTTP_PORT`); host GUI clients use `unity mcp configure ` directly. See [mcp/README.md](./mcp/README.md). diff --git a/.llm/mcp/README.md b/.llm/mcp/README.md index 3b7dccd..688394d 100644 --- a/.llm/mcp/README.md +++ b/.llm/mcp/README.md @@ -1,80 +1,57 @@ # MCP tooling (`.llm/mcp/`) -Shared MCP infrastructure for this package: the official Unity CLI MCP server -(exposed over HTTP for container agents) plus a universal configurator that -registers every MCP server into all supported agentic frontends (Claude Code, -OpenAI Codex, OpenCode, Nanocoder, VS Code, Cursor). +Shared MCP infrastructure for this package: the official Unity CLI MCP server (exposed over HTTP for container agents) plus a universal configurator that registers every MCP server +into all supported agentic frontends (Claude Code, OpenAI Codex, OpenCode, Nanocoder, VS Code, Cursor). ## Server fleet -| Name | Kind | Purpose | Auth | -| --- | --- | --- | --- | -| `unity` | HTTP (host bridge → official `unity mcp`) | ~140 Unity editor tools: scenes, GameObjects, prefabs, materials, animation, packages, tests, builds, C# eval | `UNITY_MCP_TOKEN` (bearer) | -| `github` | remote HTTP | GitHub platform: repos, PRs, issues, actions, security (hosted MCP, **all toolsets** ≈ 95 tools) | `GITHUB_PERSONAL_ACCESS_TOKEN` (`GITHUB_MCP_PAT` is reconciled into it — see [Credential pairs](#credential-pairs-github_personal_access_token--github_mcp_pat)) | -| `context7` | stdio (npx) | Up-to-date library documentation | `CONTEXT7_API_KEY` (optional) | -| `fetch` | stdio (uvx) | Fetch web pages as markdown | none | -| `git` | stdio (uvx) | Local git operations | none | -| `zai-vision` | stdio (npx) | Official Z.ai MCP: screenshot/diagram/image/video **analysis** | `ZAI_API_KEY` | -| `zai-web-search` | remote HTTP | Z.ai web-search-prime | `ZAI_API_KEY` | -| `zai-image` | stdio (local script) | Z.ai image **generation** (glm-image) → `.artifacts/zai-images/` | `ZAI_API_KEY` | - -All credentials come from the environment with a gitignored `.env.local` -fallback (preferred). Config files contain env-var *references* — no secrets -on disk outside `.env.local`. +| Name | Kind | Purpose | Auth | +| ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `unity` | HTTP (host bridge → official `unity mcp`) | ~140 Unity editor tools: scenes, GameObjects, prefabs, materials, animation, packages, tests, builds, C# eval | `UNITY_MCP_TOKEN` (bearer) | +| `github` | remote HTTP | GitHub platform: repos, PRs, issues, actions, security (hosted MCP, **all toolsets** ≈ 95 tools) | `GITHUB_PERSONAL_ACCESS_TOKEN` (`GITHUB_MCP_PAT` is reconciled into it — see [Credential pairs](#credential-pairs-github_personal_access_token--github_mcp_pat)) | +| `context7` | stdio (npx) | Up-to-date library documentation | `CONTEXT7_API_KEY` (optional) | +| `fetch` | stdio (uvx) | Fetch web pages as markdown | none | +| `git` | stdio (uvx) | Local git operations | none | +| `zai-vision` | stdio (npx) | Official Z.ai MCP: screenshot/diagram/image/video **analysis** | `ZAI_API_KEY` | +| `zai-web-search` | remote HTTP | Z.ai web-search-prime | `ZAI_API_KEY` | +| `zai-image` | stdio (local script) | Z.ai image **generation** (glm-image) → `.artifacts/zai-images/` | `ZAI_API_KEY` | + +All credentials come from the environment with a gitignored `.env.local` fallback (preferred). Config files contain env-var _references_ — no secrets on disk outside `.env.local`. ## Credential pairs (`GITHUB_PERSONAL_ACCESS_TOKEN` / `GITHUB_MCP_PAT`) -One GitHub PAT circulates under two names: generated configs reference -`GITHUB_PERSONAL_ACCESS_TOKEN`, while some machines only define -`GITHUB_MCP_PAT`. `ZAI_API_KEY` / `Z_AI_API_KEY` is the same shape. +One GitHub PAT circulates under two names: generated configs reference `GITHUB_PERSONAL_ACCESS_TOKEN`, while some machines only define `GITHUB_MCP_PAT`. `ZAI_API_KEY` / +`Z_AI_API_KEY` is the same shape. -The incident this section exists for: `.env.local` ended up with **both** names -set to **different** values — a revoked PAT under the canonical name and a valid -one under the alternate. Every generated config referenced the stale name, so the -hosted GitHub MCP returned `401` while a perfectly good token sat unused one line -away. Both `configure.mjs` and `mcp-doctor.mjs` reported success because they -only ever checked *presence*, never *agreement*. +The incident this section exists for: `.env.local` ended up with **both** names set to **different** values — a revoked PAT under the canonical name and a valid one under the +alternate. Every generated config referenced the stale name, so the hosted GitHub MCP returned `401` while a perfectly good token sat unused one line away. Both `configure.mjs` and +`mcp-doctor.mjs` reported success because they only ever checked _presence_, never _agreement_. Current behavior: -- Both set and **equal** → `configured`. Both set and **different** → - `divergent`, reported by `mcp:sync --check` and as a failure by - `mcp:doctor`. Only one set → still mirrored into the other as before. -- `npm run mcp:repair` reconciles a divergent pair: it probes each name against - the real endpoint and copies the one that authenticates over the other, in - place (never appending a duplicate key). It **refuses to write** when neither - name authenticates rather than guessing, and never prints a credential. -- `post-start.sh` runs the repair automatically on container start, before - `mcp:sync`, so a divergence is resolved on the next start instead of - producing an opaque 401. - -A credential rotated in `.env.local` also has to reach already-running agents. -Two separate layers cache the old value, and both need handling: - -1. **Your shell.** `{env:VAR}` expands once at agent startup. - `env-local.sh` is sourced with `DATAVIZ_ENV_LOCAL_PRECEDENCE=file`, which - lets credential names take the `.env.local` value over a stale exported copy. - A new shell picks the change up with no rebuild. -2. **OpenCode's background service.** Every `opencode` session attaches to ONE - shared server (`opencode serve --service`), discovered via - `~/.local/state/opencode/service.json`. It inherits the environment of - whichever client happened to start it and holds it for its entire life, so - **new sessions stay broken too** — the session is fine, the singleton behind - it is not. `.devcontainer/recycle-stale-opencode-service.sh` compares the - running service's credentials against `.env.local` and, on a mismatch, - recycles it so the next session respawns a healthy one. It is a no-op when - the service matches, when there is no `.env.local`, or when no service is - running. `post-start.sh` runs it on every container start. +- Both set and **equal** → `configured`. Both set and **different** → `divergent`, reported by `mcp:sync --check` and as a failure by `mcp:doctor`. Only one set → still mirrored + into the other as before. +- `npm run mcp:repair` reconciles a divergent pair: it probes each name against the real endpoint and copies the one that authenticates over the other, in place (never appending a + duplicate key). It **refuses to write** when neither name authenticates rather than guessing, and never prints a credential. +- `post-start.sh` runs the repair automatically on container start, before `mcp:sync`, so a divergence is resolved on the next start instead of producing an opaque 401. + +A credential rotated in `.env.local` also has to reach already-running agents. Two separate layers cache the old value, and both need handling: + +1. **Your shell.** `{env:VAR}` expands once at agent startup. `env-local.sh` is sourced with `DATAVIZ_ENV_LOCAL_PRECEDENCE=file`, which lets credential names take the `.env.local` + value over a stale exported copy. A new shell picks the change up with no rebuild. +2. **OpenCode's background service.** Every `opencode` session attaches to ONE shared server (`opencode serve --service`), discovered via `~/.local/state/opencode/service.json`. It + inherits the environment of whichever client happened to start it and holds it for its entire life, so **new sessions stay broken too** — the session is fine, the singleton + behind it is not. `.devcontainer/recycle-stale-opencode-service.sh` compares the running service's credentials against `.env.local` and, on a mismatch, recycles it so the next + session respawns a healthy one. It is a no-op when the service matches, when there is no `.env.local`, or when no service is running. `post-start.sh` runs it on every container + start. ```bash bash .devcontainer/recycle-stale-opencode-service.sh --dry-run # report only bash .devcontainer/recycle-stale-opencode-service.sh # recycle ``` -Contract tests: `.devcontainer/tests/test-mcp-credentials.sh` (credential -aliasing, divergence detection, repair), -`.devcontainer/tests/test-env-local-precedence.sh` (loader precedence), and -`.devcontainer/tests/test-opencode-service-recycle.sh` (service recycling). +Contract tests: `.devcontainer/tests/test-mcp-credentials.sh` (credential aliasing, divergence detection, repair), `.devcontainer/tests/test-env-local-precedence.sh` (loader +precedence), and `.devcontainer/tests/test-opencode-service-recycle.sh` (service recycling). ## One command to wire everything @@ -85,55 +62,38 @@ npm run mcp:sync -- --check # dry run Outputs (all machine-local, gitignored, regenerated on every container start): -| Frontend | File | -| --- | --- | -| Claude Code | `.mcp.json` (project scope) + `.claude/settings.local.json` (auto-approval) | -| Codex | `${CODEX_HOME:-~/.codex}/config.toml` (marker-managed block) | -| OpenCode | `opencode.json` (V2 `mcp.servers` block, `{env:VAR}` refs) | -| Nanocoder | `.nanocoder/mcp.json` (via `NANOCODER_MCPSERVERS_FILE`) + directory trust seed + `agents.config.json` providers (zai via `api.z.ai/api/paas/v4`, openrouter) | -| VS Code | `.vscode/mcp.json` (`${env:VAR}` refs) | -| Cursor | `.cursor/mcp.json` (project scope, `${env:VAR}` refs) | - -Codex ≥ 0.153 additionally deep-merges a *trusted* workspace -`.codex/config.toml` over the home config, so `npm run mcp:sync` also prunes -that file (removing it once nothing user-owned remains). Do not put MCP -servers there: a transport that conflicts with the home config hard-fails every -codex startup (`url is not supported for stdio`). Project-scoped MCP servers -for Codex live in the marker-managed block of `$CODEX_HOME/config.toml`. - -Nanocoder additionally needs a configured *provider* before it will run (the -zai + openrouter entries above, whose API keys are `${VAR}` env references -that nanocoder substitutes at load) and directory trust — both are seeded by -`npm run mcp:sync`. Note Z.ai's OpenAI-compatible base URL is -`https://api.z.ai/api/paas/v4`; `https://api.z.ai/api/v1` only serves the -Responses API (403 `model_access_denied` for chat completions). - -Claude project-scope servers also get their approval + folder trust recorded in -`~/.claude.json` (same state the interactive dialogs write), so servers are -usable without a first-run approval prompt. The isolated Claude state -directories used by the `claude-zai` / `claude-openrouter` launchers -(`~/.claude-zai`, `~/.claude-openrouter`) are seeded with the same trust + -approvals by `bash .devcontainer/ai-backends.sh install` (and on every -launcher start), because Claude only reads that state from the config dir its +| Frontend | File | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Claude Code | `.mcp.json` (project scope) + `.claude/settings.local.json` (auto-approval) | +| Codex | `${CODEX_HOME:-~/.codex}/config.toml` (marker-managed block) | +| OpenCode | `opencode.json` (V2 `mcp.servers` block, `{env:VAR}` refs) | +| Nanocoder | `.nanocoder/mcp.json` (via `NANOCODER_MCPSERVERS_FILE`) + directory trust seed + `agents.config.json` providers (zai via `api.z.ai/api/paas/v4`, openrouter) | +| VS Code | `.vscode/mcp.json` (`${env:VAR}` refs) | +| Cursor | `.cursor/mcp.json` (project scope, `${env:VAR}` refs) | + +Codex ≥ 0.153 additionally deep-merges a _trusted_ workspace `.codex/config.toml` over the home config, so `npm run mcp:sync` also prunes that file (removing it once nothing +user-owned remains). Do not put MCP servers there: a transport that conflicts with the home config hard-fails every codex startup (`url is not supported for stdio`). Project-scoped +MCP servers for Codex live in the marker-managed block of `$CODEX_HOME/config.toml`. + +Nanocoder additionally needs a configured _provider_ before it will run (the zai + openrouter entries above, whose API keys are `${VAR}` env references that nanocoder substitutes +at load) and directory trust — both are seeded by `npm run mcp:sync`. Note Z.ai's OpenAI-compatible base URL is `https://api.z.ai/api/paas/v4`; `https://api.z.ai/api/v1` only +serves the Responses API (403 `model_access_denied` for chat completions). + +Claude project-scope servers also get their approval + folder trust recorded in `~/.claude.json` (same state the interactive dialogs write), so servers are usable without a +first-run approval prompt. The isolated Claude state directories used by the `claude-zai` / `claude-openrouter` launchers (`~/.claude-zai`, `~/.claude-openrouter`) are seeded with +the same trust + approvals by `bash .devcontainer/ai-backends.sh install` (and on every launcher start), because Claude only reads that state from the config dir its `CLAUDE_CONFIG_DIR` points at. -Windsurf and other host GUI clients are not writable from the container; run -the official configurator on the host instead (see below). +Windsurf and other host GUI clients are not writable from the container; run the official configurator on the host instead (see below). -`fetch`/`git` are only registered when `uvx` is on PATH (baked into the -devcontainer image). Cursor's project config only carries host-executable -entries (it omits `zai-image`, whose script path is container-local). Merging -preserves servers configured by other tools. The Z.ai credential is read from -`ZAI_API_KEY` or `Z_AI_API_KEY`; `npm run mcp:sync` mirrors whichever exists -into the other inside `.env.local`. +`fetch`/`git` are only registered when `uvx` is on PATH (baked into the devcontainer image). Cursor's project config only carries host-executable entries (it omits `zai-image`, +whose script path is container-local). Merging preserves servers configured by other tools. The Z.ai credential is read from `ZAI_API_KEY` or `Z_AI_API_KEY`; `npm run mcp:sync` +mirrors whichever exists into the other inside `.env.local`. ## Unity MCP server (official Unity CLI) -Unity's official CLI ships an MCP server (`unity mcp`) built on the -`com.unity.pipeline` package — ~140 built-in tools on a typical project -(scenes, GameObjects, prefabs, materials, animation, packages, tests, builds, -plus an arbitrary C# `eval` tool). It requires the Unity CLI (Unity Hub) and -Unity 6.0 LTS+. +Unity's official CLI ships an MCP server (`unity mcp`) built on the `com.unity.pipeline` package — ~140 built-in tools on a typical project (scenes, GameObjects, prefabs, +materials, animation, packages, tests, builds, plus an arbitrary C# `eval` tool). It requires the Unity CLI (Unity Hub) and Unity 6.0 LTS+. ### One-time setup (host) @@ -147,88 +107,56 @@ unity mcp configure # `unity mcp configure --list` for all ### HTTP bridge for devcontainer agents -The `unity` CLI is a host binary and the pipeline server inside the editor -binds to editor-localhost only, so container agents cannot launch it directly. -`npm run unity:mcp:host` (on the host) bridges the official MCP server to -streamable-HTTP JSON-RPC: +The `unity` CLI is a host binary and the pipeline server inside the editor binds to editor-localhost only, so container agents cannot launch it directly. `npm run unity:mcp:host` +(on the host) bridges the official MCP server to streamable-HTTP JSON-RPC: ```bash npm run unity:mcp:host # host: spawns `unity mcp`, serves http://0.0.0.0:9020/mcp ``` -Container agents connect to `http://host.docker.internal:9020/mcp` (written -automatically by `npm run mcp:sync`). The bridge is transparent: JSON-RPC -requests are forwarded to the CLI with per-request id remapping, notifications -are passed through, and a crashed CLI is respawned lazily on the next request. +Container agents connect to `http://host.docker.internal:9020/mcp` (written automatically by `npm run mcp:sync`). The bridge is transparent: JSON-RPC requests are forwarded to the +CLI with per-request id remapping, notifications are passed through, and a crashed CLI is respawned lazily on the next request. Environment (prefer a gitignored `.env.local` at the workspace root): -| Variable | Purpose | -| --- | --- | -| `UNITY_CLI` | Unity CLI binary path (default `unity`) | -| `UNITY_MCP_CLI_ARGS` | Extra args appended after `mcp` (e.g. `--instance host:port`) | -| `UNITY_MCP_PROJECT` | `--project-path` value; must be the project **root** (`UNITY_PROJECT_PATH` alias honored; non-root values self-heal to the enclosing project) | -| `UNITY_MCP_HTTP_PORT` | Bridge port (default 9020). When busy, the bridge binds the next free port, persists the choice here, and re-syncs client configs (`UNITY_MCP_AUTO_SYNC=0` disables) | -| `UNITY_MCP_TOKEN` | Optional bearer token required by the bridge when set | -| `UNITY_MCP_TIMEOUT_MS` | Per-request timeout (default 180000) | - -Security: the bridge listens on all interfaces by default (Linux Docker cannot -reach a host-loopback bind via `host.docker.internal`); pass `--host 127.0.0.1` -to restrict it. `UNITY_MCP_TOKEN` (`.env.local`) gates HTTP access when set. -Only set `UNITY_MCP_TOKEN` if you accept managing bearer headers in every -frontend config. +| Variable | Purpose | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UNITY_CLI` | Unity CLI binary path (default `unity`) | +| `UNITY_MCP_CLI_ARGS` | Extra args appended after `mcp` (e.g. `--instance host:port`) | +| `UNITY_MCP_PROJECT` | `--project-path` value; must be the project **root** (`UNITY_PROJECT_PATH` alias honored; non-root values self-heal to the enclosing project) | +| `UNITY_MCP_HTTP_PORT` | Bridge port (default 9020). When busy, the bridge binds the next free port, persists the choice here, and re-syncs client configs (`UNITY_MCP_AUTO_SYNC=0` disables) | +| `UNITY_MCP_TOKEN` | Optional bearer token required by the bridge when set | +| `UNITY_MCP_TIMEOUT_MS` | Per-request timeout (default 180000) | + +Security: the bridge listens on all interfaces by default (Linux Docker cannot reach a host-loopback bind via `host.docker.internal`); pass `--host 127.0.0.1` to restrict it. +`UNITY_MCP_TOKEN` (`.env.local`) gates HTTP access when set. Only set `UNITY_MCP_TOKEN` if you accept managing bearer headers in every frontend config. ### Troubleshooting -- **`unity run unity:mcp:host` fails with "Not a Unity project".** That invoked - the Unity CLI, not the npm script: `unity run ` expects a project - path, so it treats `unity:mcp:host` as one. The bridge is an npm script — - start it with `npm run unity:mcp:host`. -- **Port already in use / dynamic ports.** Re-running the bridge is a no-op - while a healthy instance holds the port; if something else owns it, the - bridge binds the next free port, persists `UNITY_MCP_HTTP_PORT` to - `.env.local`, and re-syncs every frontend config automatically. No fixed - port is required anywhere: `configure.mjs`, the devcontainer health check, - and the doctors all read the persisted value. -- **`npm run mcp:sync` on the host.** It runs `configure.mjs` directly (pure - node, cross-platform); the bash wrapper `sync-mcp.sh` remains for container - hooks. `hasCommand` uses `where` on Windows so `uvx`-gated servers register - on the host too. -- **Shared configs are environment-agnostic.** Sync produces identical files - from the host and the container (verified by hash): the unity bridge URL - always uses `host.docker.internal` (resolves on the host under Docker - Desktop too), and zai-image uses PATH-resolved `node` with a - workspace-relative script. Never reintroduce `127.0.0.1` or absolute - binaries into shared entries — they silently break the other environment. -- **MCP auth fails in the container after editing `.env.local`.** `{env:VAR}` - references expand at AGENT STARTUP: restart the agent from a fresh shell - (interactive shells source `.env.local` via the bashrc loader, which now runs - with `DATAVIZ_ENV_LOCAL_PRECEDENCE=file` so a rotated credential wins over a - stale value already exported into the session — no container rebuild needed). - VS Code is special: its extension host inherits the server's frozen - environment, and `remoteEnv` forwards (`${localEnv:VAR}`) are empty unless the - host shell exported the variable — so VS Code entries for credential servers - are generated as wrappers that source `.env.local` at spawn time. -- **GitHub MCP 401 in the devcontainer.** See - [Credential pairs](#credential-pairs-github_personal_access_token--github_mcp_pat). -- **Health check:** `npm run mcp:doctor` verifies credentials, config flavor, - endpoint auth (github, zai-web-search), stdio spawn paths (zai-vision, - zai-image), and unity bridge reachability — from the host or the container. - `npm run unity:mcp:doctor` goes deeper on the unity bridge (project pin, - pipeline package, tools roundtrip, wrong-editor canary). Exit code 1 means - action required. -- **Tools drive the wrong project.** An unpinned `unity mcp` attaches to - whichever Editor happens to be running. `UNITY_MCP_PROJECT` must be the - project **root** (contains `ProjectSettings/ProjectVersion.txt`); pointing it - at `/Packages` or the package repo silently unpins. The bridge now - self-heals to the enclosing project with a warning, and the `/healthz` `cli` - line shows the exact spawned command. -- **Editor must be running.** Editor-dependent tools fail until an Editor is - open for the pinned project: `unity open `. +- **`unity run unity:mcp:host` fails with "Not a Unity project".** That invoked the Unity CLI, not the npm script: `unity run ` expects a project path, so it treats + `unity:mcp:host` as one. The bridge is an npm script — start it with `npm run unity:mcp:host`. +- **Port already in use / dynamic ports.** Re-running the bridge is a no-op while a healthy instance holds the port; if something else owns it, the bridge binds the next free port, + persists `UNITY_MCP_HTTP_PORT` to `.env.local`, and re-syncs every frontend config automatically. No fixed port is required anywhere: `configure.mjs`, the devcontainer health + check, and the doctors all read the persisted value. +- **`npm run mcp:sync` on the host.** It runs `configure.mjs` directly (pure node, cross-platform); the bash wrapper `sync-mcp.sh` remains for container hooks. `hasCommand` uses + `where` on Windows so `uvx`-gated servers register on the host too. +- **Shared configs are environment-agnostic.** Sync produces identical files from the host and the container (verified by hash): the unity bridge URL always uses + `host.docker.internal` (resolves on the host under Docker Desktop too), and zai-image uses PATH-resolved `node` with a workspace-relative script. Never reintroduce `127.0.0.1` or + absolute binaries into shared entries — they silently break the other environment. +- **MCP auth fails in the container after editing `.env.local`.** `{env:VAR}` references expand at AGENT STARTUP: restart the agent from a fresh shell (interactive shells source + `.env.local` via the bashrc loader, which now runs with `DATAVIZ_ENV_LOCAL_PRECEDENCE=file` so a rotated credential wins over a stale value already exported into the session — no + container rebuild needed). VS Code is special: its extension host inherits the server's frozen environment, and `remoteEnv` forwards (`${localEnv:VAR}`) are empty unless the host + shell exported the variable — so VS Code entries for credential servers are generated as wrappers that source `.env.local` at spawn time. +- **GitHub MCP 401 in the devcontainer.** See [Credential pairs](#credential-pairs-github_personal_access_token--github_mcp_pat). +- **Health check:** `npm run mcp:doctor` verifies credentials, config flavor, endpoint auth (github, zai-web-search), stdio spawn paths (zai-vision, zai-image), and unity bridge + reachability — from the host or the container. `npm run unity:mcp:doctor` goes deeper on the unity bridge (project pin, pipeline package, tools roundtrip, wrong-editor canary). + Exit code 1 means action required. +- **Tools drive the wrong project.** An unpinned `unity mcp` attaches to whichever Editor happens to be running. `UNITY_MCP_PROJECT` must be the project **root** (contains + `ProjectSettings/ProjectVersion.txt`); pointing it at `/Packages` or the package repo silently unpins. The bridge now self-heals to the enclosing project with a warning, + and the `/healthz` `cli` line shows the exact spawned command. +- **Editor must be running.** Editor-dependent tools fail until an Editor is open for the pinned project: `unity open `. ### Removing the legacy custom bridge -The previous hand-rolled `UnityMcpBridge.cs` (10 tools) is deprecated. If it -was installed into the Unity project, delete -`/Assets/Editor/UnityMcpBridge.cs` (and its `.meta`) — it is no -longer installed or served by anything in this repo. +The previous hand-rolled `UnityMcpBridge.cs` (10 tools) is deprecated. If it was installed into the Unity project, delete `/Assets/Editor/UnityMcpBridge.cs` (and its +`.meta`) — it is no longer installed or served by anything in this repo. diff --git a/.llm/references/RELEASING.md b/.llm/references/RELEASING.md index 91c182e..71a3231 100644 --- a/.llm/references/RELEASING.md +++ b/.llm/references/RELEASING.md @@ -1,35 +1,27 @@ # Releasing com.wallstop-studios.data-visualizer -One workflow owns release preparation, tagging, and publication: -[release.yml](../../.github/workflows/release.yml). The agent procedure is in +One workflow owns release preparation, tagging, and publication: [release.yml](../../.github/workflows/release.yml). The agent procedure is in [bump-version-release](../skills/bump-version-release/SKILL.md). -The workflow never runs Unity. EditMode and PlayMode suites remain local release -gates (see `.llm/context.md`). Unity archives are built and validated without an -editor or Unity license. +The workflow never runs Unity. EditMode and PlayMode suites remain local release gates (see `.llm/context.md`). Unity archives are built and validated without an editor or Unity +license. ## Release flow 1. Add user-facing entries under `CHANGELOG.md` `## [Unreleased]`. -2. Run `release.yml` on `main` with `operation=prepare`. Choose `bump` (`patch`, - `minor`, or `major`) or an `explicit_version`. The default `dry_run=true` - validates the candidate on the runner without remote writes. -3. Set `dry_run=false` to open `release/vX.Y.Z`. Preparation bumps `package.json`, - syncs `.llm/context.md`, dates the changelog section, validates the Unity - archive and notes, and explicitly starts `llm-lint.yml` for the branch. -4. Merge the validated release PR into `main`. The `release` job checks out that - exact merge commit, creates and pushes annotated `vX.Y.Z`, builds and validates - both packages, and extracts the notes from the dated changelog section. -5. After artifact validation passes, the same job publishes npm and creates or - updates the GitHub Release. It attaches the npm tarball and Unity package, - each with a SHA-256 checksum. Validated artifacts are retained for 14 days. - -An externally pushed `v*` tag also runs the publication job. It checks out that -exact tag and requires the package version and changelog notes to match. - -The `prepare` job and `release` job run on different events in the same workflow. -Tagging and publication run in the same job. No release workflow dispatch chain -or custom release credential is needed. +2. Run `release.yml` on `main` with `operation=prepare`. Choose `bump` (`patch`, `minor`, or `major`) or an `explicit_version`. The default `dry_run=true` validates the candidate + on the runner without remote writes. +3. Set `dry_run=false` to open `release/vX.Y.Z`. Preparation bumps `package.json`, syncs `.llm/context.md`, dates the changelog section, validates the Unity archive and notes, and + explicitly starts `llm-lint.yml` for the branch. +4. Merge the validated release PR into `main`. The `release` job checks out that exact merge commit, creates and pushes annotated `vX.Y.Z`, builds and validates both packages, and + extracts the notes from the dated changelog section. +5. After artifact validation passes, the same job publishes npm and creates or updates the GitHub Release. It attaches the npm tarball and Unity package, each with a SHA-256 + checksum. Validated artifacts are retained for 14 days. + +An externally pushed `v*` tag also runs the publication job. It checks out that exact tag and requires the package version and changelog notes to match. + +The `prepare` job and `release` job run on different events in the same workflow. Tagging and publication run in the same job. No release workflow dispatch chain or custom release +credential is needed. ## Authentication @@ -39,93 +31,75 @@ No custom secrets are required. -GitHub writes use the built-in per-run `GITHUB_TOKEN`. npm publication uses OIDC. -No npm token, PAT, or GitHub App is needed. +GitHub writes use the built-in per-run `GITHUB_TOKEN`. npm publication uses OIDC. No npm token, PAT, or GitHub App is needed. -Preparation declares Contents, Pull requests, and Actions write permissions. -It explicitly dispatches branch CI because a token-authored push does not start -another workflow. Publication declares Contents and ID token write permissions. -Its tag push does not need to trigger another workflow: publication continues in -the same job. +Preparation declares Contents, Pull requests, and Actions write permissions. It explicitly dispatches branch CI because a token-authored push does not start another workflow. +Publication declares Contents and ID token write permissions. Its tag push does not need to trigger another workflow: publication continues in the same job. -For automatic PR creation, enable GitHub repository Settings → Actions → -General → Workflow permissions → Allow GitHub Actions to create and approve -pull requests. This setting was enabled during the release audit. The workflow -only creates PRs. Manual dispatches must run from `main`. +For automatic PR creation, enable GitHub repository Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests. This setting was +enabled during the release audit. The workflow only creates PRs. Manual dispatches must run from `main`. ## npm Trusted Publisher setup -Configure `com.wallstop-studios.data-visualizer` on npmjs.com under package -settings → Trusted Publisher → GitHub Actions. +Configure `com.wallstop-studios.data-visualizer` on npmjs.com under package settings → Trusted Publisher → GitHub Actions. -| Field | Value | -| --- | --- | -| Organization or user | `wallstop` | -| Repository | `DataVisualizer` | -| Workflow filename | `release.yml` | -| Environment name | leave empty | -| Allowed actions | allow `npm publish` | +| Field | Value | +| -------------------- | ------------------- | +| Organization or user | `wallstop` | +| Repository | `DataVisualizer` | +| Workflow filename | `release.yml` | +| Environment name | leave empty | +| Allowed actions | allow `npm publish` | -If a publisher entry already names `npm-publish.yml`, update it to `release.yml` -before using the consolidated workflow. npm must trust the file that contains -the publish command; preparation, automatic publication, and manual recovery -all use this single file. +If a publisher entry already names `npm-publish.yml`, update it to `release.yml` before using the consolidated workflow. npm must trust the file that contains the publish command; +preparation, automatic publication, and manual recovery all use this single file. - Values are case-sensitive. Enter only the workflow filename and extension. -- The workflow calls `npm publish` directly, so allow that action. A publisher - that only allows staged publication will reject it. -- Trusted publishing requires npm CLI 11.5.1 or later and Node 22.14.0 or later. - The workflow uses Node 24 and checks the npm minimum before publication. +- The workflow calls `npm publish` directly, so allow that action. A publisher that only allows staged publication will reject it. +- Trusted publishing requires npm CLI 11.5.1 or later and Node 22.14.0 or later. The workflow uses Node 24 and checks the npm minimum before publication. - Publication uses a GitHub-hosted runner. No local npm login is needed. - `package.json` `repository.url` already matches this GitHub repository. -npm Trusted Publisher settings cannot be verified through public package -metadata. An owner must verify the registration above. +npm Trusted Publisher settings cannot be verified through public package metadata. An owner must verify the registration above. ## Rerun and recovery -| State | Action | -| --- | --- | -| Release PR was not opened | Re-dispatch `operation=prepare`. An identical existing branch is reused. If its content differs, inspect it before choosing a new version or opening the existing branch's PR. No branch is force-pushed. | -| Release failed before tagging | Fix the cause and rerun the failed job for the merged PR. | -| Tag exists, publication stopped | Dispatch `release.yml` with `operation=publish`, `tag=vX.Y.Z`, and `dry_run=false`. | -| npm published, GitHub Release failed | Use the same publish dispatch. npm skips the existing version; release notes and assets are updated. | -| Verify an existing tag | Dispatch `operation=publish`, `tag=vX.Y.Z`, and `dry_run=true`. | +| State | Action | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Release PR was not opened | Re-dispatch `operation=prepare`. An identical existing branch is reused. If its content differs, inspect it before choosing a new version or opening the existing branch's PR. No branch is force-pushed. | +| Release failed before tagging | Fix the cause and rerun the failed job for the merged PR. | +| Tag exists, publication stopped | Dispatch `release.yml` with `operation=publish`, `tag=vX.Y.Z`, and `dry_run=false`. | +| npm published, GitHub Release failed | Use the same publish dispatch. npm skips the existing version; release notes and assets are updated. | +| Verify an existing tag | Dispatch `operation=publish`, `tag=vX.Y.Z`, and `dry_run=true`. | -A rerun of the merged PR job reuses its tag only when the tag still points at -that exact merge commit. It never moves an existing tag. A conflicting tag -fails. Direct publication checks out the requested tag rather than current main. +A rerun of the merged PR job reuses its tag only when the tag still points at that exact merge commit. It never moves an existing tag. A conflicting tag fails. Direct publication +checks out the requested tag rather than current main. -npm publication is permanent. Recover from a bad release with a new patch -version. The pipeline is recoverable across registries, not a transaction: -npm may succeed before the GitHub Release API fails. +npm publication is permanent. Recover from a bad release with a new patch version. The pipeline is recoverable across registries, not a transaction: npm may succeed before the +GitHub Release API fails. ## Validation -| Script | Rejects | -| --- | --- | -| `prepare-release.mjs` | unsupported versions, missing or empty Unreleased notes, invalid changelog rotation | -| `tag-release.mjs` | branch/version disagreement, missing or duplicated dated notes, an existing tag, a dirty tree | -| `verify-release.mjs` | tag/version disagreement, packed files outside the allowlist, missing payload files | -| `build-unitypackage.mjs` | unsafe paths, missing metadata, duplicate or malformed GUIDs | +| Script | Rejects | +| --------------------------- | ------------------------------------------------------------------------------------------------------- | +| `prepare-release.mjs` | unsupported versions, missing or empty Unreleased notes, invalid changelog rotation | +| `tag-release.mjs` | branch/version disagreement, missing or duplicated dated notes, an existing tag, a dirty tree | +| `verify-release.mjs` | tag/version disagreement, packed files outside the allowlist, missing payload files | +| `build-unitypackage.mjs` | unsafe paths, missing metadata, duplicate or malformed GUIDs | | `validate-unitypackage.mjs` | corrupt archives, unsafe or duplicate members, checksum failures, disagreement with the tracked payload | -| `extract-release-notes.mjs` | missing, empty, or duplicated version notes | +| `extract-release-notes.mjs` | missing, empty, or duplicated version notes | -Both archives, checksums, and release notes must be ready before npm publication. -Pinned action revisions are tracked by Dependabot. Workflow contract tests run -with the cross-platform script suite. +Both archives, checksums, and release notes must be ready before npm publication. Pinned action revisions are tracked by Dependabot. Workflow contract tests run with the +cross-platform script suite. ## References -The audit compared release workflows in -[DxCommandTerminal](https://github.com/wallstop/DxCommandTerminal/tree/master/.github/workflows), -[unity-helpers](https://github.com/Ambiguous-Interactive/unity-helpers/tree/main/.github/workflows), -and [DxMessaging](https://github.com/Ambiguous-Interactive/DxMessaging/tree/master/.github/workflows). -They share curated changelog rotation, tags, required Unity archives, and npm +The audit compared release workflows in [DxCommandTerminal](https://github.com/wallstop/DxCommandTerminal/tree/master/.github/workflows), +[unity-helpers](https://github.com/Ambiguous-Interactive/unity-helpers/tree/main/.github/workflows), and +[DxMessaging](https://github.com/Ambiguous-Interactive/DxMessaging/tree/master/.github/workflows). They share curated changelog rotation, tags, required Unity archives, and npm publication after artifact validation. -Preparation [run 37553208060](https://github.com/wallstop/DataVisualizer/actions/runs/37553208060) -left `release/v0.1.0` after GitHub rejected automatic PR creation. Inspect that +Preparation [run 37553208060](https://github.com/wallstop/DataVisualizer/actions/runs/37553208060) left `release/v0.1.0` after GitHub rejected automatic PR creation. Inspect that branch before preparing the same version again. - [GitHub workflow triggers](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow) diff --git a/.llm/references/forbidden-patterns.md b/.llm/references/forbidden-patterns.md index 34862bf..ba36e22 100644 --- a/.llm/references/forbidden-patterns.md +++ b/.llm/references/forbidden-patterns.md @@ -2,39 +2,39 @@ Patterns that must not appear in this codebase, with the compliant alternative. -| Forbidden | Use Instead | Why | -| --- | --- | --- | -| `GetInstanceID()` on UnityEngine.Object where Unity 6000.5+ matters | `EntityId`-based flow (see commit "Migrate GetInstanceID to EntityId") | API retirement in newer Unity | -| Hardcoded package id / prefs prefix strings | `PackageId`, `PrefsPrefix` consts | single source of truth | -| Renaming serialized fields without `[FormerlySerializedAs]` | keep old name attribute + new camelCase field | saved assets and user state JSON break silently | -| `UnityEditor` usage in `Runtime/` without `#if UNITY_EDITOR` | wrap in the define | runtime assembly must compile without editor refs | -| Stringly-typed menu items, property paths, analytics IDs | `nameof` expressions | rename-safe lookups (repo rule) | -| Reflection to reach internals from tests | make the helper `public` (`ObjectIdExtensions`, `LabelFilterEvaluator` precedent) | `InternalsVisibleTo` is not honored for editor->tests | -| Reflection in hot paths (per-frame, per-asset loops) | cached delegates, direct calls, or `ReflectionHelper` with caching | window batches over thousands of assets | -| Culture-sensitive sorting in generated files/scripts | ordinal comparer (`StringComparer.Ordinal`) | byte-identical output across OS/CI machines | -| Bare `Sort()` / `string.Compare(a, b)` / `OrderBy(key)` / culture-sensitive `StartsWith` on package-owned identifiers (asset labels, namespace keys, type names, paths, GUIDs) | `StringComparer.Ordinal` or `StringComparison.Ordinal`; `OrdinalIgnoreCase` when casing is intentionally ignored | culture-dependent order and membership change across machines; `LabelFilterEvaluator`, `NamespaceTypeOrder`, and `BaseDataObject.CompareTo` are the Ordinal precedent | -| Instance `lhs.Equals(rhs, StringComparison)` on a receiver that can be null | static `string.Equals(lhs, rhs, StringComparison)`; for non-string values `EqualityComparer.Default.Equals` | the static form treats null operands as values instead of throwing; it is the codebase convention (see `LabelFilterEvaluator`, `Data/` models) | -| A lambda whose body uses only its own parameters passed without the `static` keyword | `static` lambda (`static (lhs, rhs) => ...`) | makes accidental capture a compile error; non-capturing delegates are cached once, so capture is the only per-call allocation risk; `DictionaryExtensions`, `ReflectionHelper`, and the window's sort comparers are the precedent | -| Editor-only API calls in `OnValidate` without `Application.isPlaying` guard | `#if UNITY_EDITOR` + play-mode check (see `BaseDataObject.TrySetAssetPath`) | build/player crashes | -| Growing `DataVisualizer.cs` with new subsystems | new files under `Data/`, `Search/`, `UI/`, `Utilities/` | the main file is already ~9.3k lines | -| Adding `Tests/`, `.llm/`, `docs/`, `scripts/`, or pointer files to `package.json` `files` | keep the whitelist Editor/Runtime only; docs imagery ships via GitHub, not npm | tarball hygiene for the UPM registry and consumer installs | -| Committing analyzer DLLs, `RoslynAnalyzer` metadata, notices, or `-analyzer` arguments in package response files | install and configure development analyzers in the host Unity project | development tooling must not exist in the package repository or reach consumers | -| `System.Diagnostics.Debug` for caught exceptions in Unity code | `UnityEngine.Debug.LogException` | diagnostics must reach the Unity Console consistently | -| Explicit `.ToString()` inside interpolation or composite formatting | interpolate/pass the value directly; retain overloads that specify an actual format or culture | formatting already calls `ToString`, while explicit calls add noise and can defeat format providers | -| Committing without `.meta` files for new Unity-visible paths | let Unity generate them, then `git add` | missing metas churn guids per machine | -| Editing `.llm/skills/index.md` by hand | `pwsh -NoProfile -File scripts/generate-skills-index.ps1` | file is generated; hand edits drift | -| Markdown files under `.llm/` exceeding 300 lines | split into skills/code-samples/references | context minimalism (hard limit, enforced) | -| Smart quotes / em-dashes in `.llm/skills/*/SKILL.md` frontmatter | ASCII-only frontmatter values | cross-OS index determinism | -| `foreach` over an `IReadOnlyList` / `IList` interface | counted `for`; use `foreach` for arrays/concrete value enumerators | avoid boxed/interface enumerator allocation in hot loops | -| Bitwise Boolean aggregation or mutation inside `|=` / `&=` | named operation results combined afterward with `||` / `&&` | preserve evaluation while making side effects and intent auditable | -| Direct lifecycle-state writes and scheduler changes across helpers | one transition owner and state-machine runner | prevent contradictory transitions and orphaned recurring work | -| Implicit enum values or a valid zero-valued enum member | explicit ordinals plus an obsolete zero sentinel | Unity serializes enum ordinals; reordering implicit members silently changes data | -| Static classes that own mutable service state, events, or lifecycle work | sealed instance service plus one explicit shared instance if needed | instances isolate tests and make ownership visible | -| C# members outside the repository tier/access order, or nested types interspersed with members | `npm run lint:csharp-member-order:fix`, followed by manual barrier fixes | consistent layout keeps APIs scannable and nested declarations from obscuring behavior | -| All-caps native typedef names used as managed type names | descriptive PascalCase names such as `NativeRect` | follows .NET naming conventions without shadowing framework or Unity types | -| `Assert.IsNull` / `Assert.IsNotNull`, `Is.Null` / `Is.Not.Null` constraints on any value, `?.`, `??`, or implicit bool tests on `UnityEngine.Object` | `Assert.That(value == null)` / `Assert.That(value != null)`; explicit `== null` / `!= null` comparisons | Unity overloads the equality operators so a destroyed-object wrapper compares null; NUnit's IsNull, Is.Null constraints, and C#'s `?.`/`??`/bool forms use reference equality and cannot see that lifetime (`scripts/lint-csharp-null-assertions.js` enforces the assertion and constraint forms) | -| Static mutable state with no documented reason or teardown reset | document why it is static, weak-key by lifecycle owner, unregister on every removal path, and reset it in the owning window's `Cleanup()` (`AssetGuidTypeIndex.Shared` suspension reset and the theme-selection ownership registry are the precedents) | static editor state outlives windows and strands cross-window state (PR #122 review finding) | -| `using` directives above the `namespace` declaration | place them inside the namespace block; file-level usings are sanctioned only for namespaceless assembly-attribute files and `[assembly: ...]` preambles like `InternalsVisibleTo` (`npm run lint:csharp-usings` enforces this, `:fix` moves the simple cases) | one namespace-scoped convention keeps the file header stable and the rule mechanically checkable (#124) | -| A guard that reads a machine-readable config resolves only part of a construct, such as the first `{a,b}` alternative, a nested group, or one level of a nested mapping | resolve every alternative and nesting level, or refuse the pattern with the file and line that uses it | a partially-read rule still matches its own representative path, so the guard passes while the real configuration disagrees; refusing beats guessing (PR #131 review finding in `scripts/lint-line-endings.js`) | -| A guard that names the argument forms it rejects and passes everything it does not name | require the exact arguments the policy permits, and report the file with every argument found | a switch the reader does not recognize turns off the policy the guard claims to enforce, so `-nowarn:1701` or `-warnaserror-` in a `csc.rsp` passed the guard that owns warnings-as-errors (`scripts/lint-assembly-warnings.ps1`, #132) | -| Pixel or region validation that records UI Toolkit subtrees without checking whether they render | filter candidates through a rendered check before measuring — `display:none` ancestor chain, resolved per-element visibility, and positive area (`DocsImageCapture.IsRenderedForCapture` precedent) | a `display:none` subtree reports laid-out children but paints nothing, so its clamped one-pixel sample always fails and rejects a faithful capture; persisted namespace collapse makes hidden rows a normal host state (PR #140 review finding) | +| Forbidden | Use Instead | Why | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GetInstanceID()` on UnityEngine.Object where Unity 6000.5+ matters | `EntityId`-based flow (see commit "Migrate GetInstanceID to EntityId") | API retirement in newer Unity | +| Hardcoded package id / prefs prefix strings | `PackageId`, `PrefsPrefix` consts | single source of truth | +| Renaming serialized fields without `[FormerlySerializedAs]` | keep old name attribute + new camelCase field | saved assets and user state JSON break silently | +| `UnityEditor` usage in `Runtime/` without `#if UNITY_EDITOR` | wrap in the define | runtime assembly must compile without editor refs | +| Stringly-typed menu items, property paths, analytics IDs | `nameof` expressions | rename-safe lookups (repo rule) | +| Reflection to reach internals from tests | make the helper `public` (`ObjectIdExtensions`, `LabelFilterEvaluator` precedent) | `InternalsVisibleTo` is not honored for editor->tests | +| Reflection in hot paths (per-frame, per-asset loops) | cached delegates, direct calls, or `ReflectionHelper` with caching | window batches over thousands of assets | +| Culture-sensitive sorting in generated files/scripts | ordinal comparer (`StringComparer.Ordinal`) | byte-identical output across OS/CI machines | +| Bare `Sort()` / `string.Compare(a, b)` / `OrderBy(key)` / culture-sensitive `StartsWith` on package-owned identifiers (asset labels, namespace keys, type names, paths, GUIDs) | `StringComparer.Ordinal` or `StringComparison.Ordinal`; `OrdinalIgnoreCase` when casing is intentionally ignored | culture-dependent order and membership change across machines; `LabelFilterEvaluator`, `NamespaceTypeOrder`, and `BaseDataObject.CompareTo` are the Ordinal precedent | +| Instance `lhs.Equals(rhs, StringComparison)` on a receiver that can be null | static `string.Equals(lhs, rhs, StringComparison)`; for non-string values `EqualityComparer.Default.Equals` | the static form treats null operands as values instead of throwing; it is the codebase convention (see `LabelFilterEvaluator`, `Data/` models) | +| A lambda whose body uses only its own parameters passed without the `static` keyword | `static` lambda (`static (lhs, rhs) => ...`) | makes accidental capture a compile error; non-capturing delegates are cached once, so capture is the only per-call allocation risk; `DictionaryExtensions`, `ReflectionHelper`, and the window's sort comparers are the precedent | +| Editor-only API calls in `OnValidate` without `Application.isPlaying` guard | `#if UNITY_EDITOR` + play-mode check (see `BaseDataObject.TrySetAssetPath`) | build/player crashes | +| Growing `DataVisualizer.cs` with new subsystems | new files under `Data/`, `Search/`, `UI/`, `Utilities/` | the main file is already ~9.3k lines | +| Adding `Tests/`, `.llm/`, `docs/`, `scripts/`, or pointer files to `package.json` `files` | keep the whitelist Editor/Runtime only; docs imagery ships via GitHub, not npm | tarball hygiene for the UPM registry and consumer installs | +| Committing analyzer DLLs, `RoslynAnalyzer` metadata, notices, or `-analyzer` arguments in package response files | install and configure development analyzers in the host Unity project | development tooling must not exist in the package repository or reach consumers | +| `System.Diagnostics.Debug` for caught exceptions in Unity code | `UnityEngine.Debug.LogException` | diagnostics must reach the Unity Console consistently | +| Explicit `.ToString()` inside interpolation or composite formatting | interpolate/pass the value directly; retain overloads that specify an actual format or culture | formatting already calls `ToString`, while explicit calls add noise and can defeat format providers | +| Committing without `.meta` files for new Unity-visible paths | let Unity generate them, then `git add` | missing metas churn guids per machine | +| Editing `.llm/skills/index.md` by hand | `pwsh -NoProfile -File scripts/generate-skills-index.ps1` | file is generated; hand edits drift | +| Markdown files under `.llm/` exceeding 300 lines | split into skills/code-samples/references | context minimalism (hard limit, enforced) | +| Smart quotes / em-dashes in `.llm/skills/*/SKILL.md` frontmatter | ASCII-only frontmatter values | cross-OS index determinism | +| `foreach` over an `IReadOnlyList` / `IList` interface | counted `for`; use `foreach` for arrays/concrete value enumerators | avoid boxed/interface enumerator allocation in hot loops | +| Bitwise Boolean aggregation or mutation inside `\|=` / `&=` | named operation results combined afterward with `\|\|` / `&&` | preserve evaluation while making side effects and intent auditable | +| Direct lifecycle-state writes and scheduler changes across helpers | one transition owner and state-machine runner | prevent contradictory transitions and orphaned recurring work | +| Implicit enum values or a valid zero-valued enum member | explicit ordinals plus an obsolete zero sentinel | Unity serializes enum ordinals; reordering implicit members silently changes data | +| Static classes that own mutable service state, events, or lifecycle work | sealed instance service plus one explicit shared instance if needed | instances isolate tests and make ownership visible | +| C# members outside the repository tier/access order, or nested types interspersed with members | `npm run lint:csharp-member-order:fix`, followed by manual barrier fixes | consistent layout keeps APIs scannable and nested declarations from obscuring behavior | +| All-caps native typedef names used as managed type names | descriptive PascalCase names such as `NativeRect` | follows .NET naming conventions without shadowing framework or Unity types | +| `Assert.IsNull` / `Assert.IsNotNull`, `Is.Null` / `Is.Not.Null` constraints on any value, `?.`, `??`, or implicit bool tests on `UnityEngine.Object` | `Assert.That(value == null)` / `Assert.That(value != null)`; explicit `== null` / `!= null` comparisons | Unity overloads the equality operators so a destroyed-object wrapper compares null; NUnit's IsNull, Is.Null constraints, and C#'s `?.`/`??`/bool forms use reference equality and cannot see that lifetime (`scripts/lint-csharp-null-assertions.js` enforces the assertion and constraint forms) | +| Static mutable state with no documented reason or teardown reset | document why it is static, weak-key by lifecycle owner, unregister on every removal path, and reset it in the owning window's `Cleanup()` (`AssetGuidTypeIndex.Shared` suspension reset and the theme-selection ownership registry are the precedents) | static editor state outlives windows and strands cross-window state (PR #122 review finding) | +| `using` directives above the `namespace` declaration | place them inside the namespace block; file-level usings are sanctioned only for namespaceless assembly-attribute files and `[assembly: ...]` preambles like `InternalsVisibleTo` (`npm run lint:csharp-usings` enforces this, `:fix` moves the simple cases) | one namespace-scoped convention keeps the file header stable and the rule mechanically checkable (#124) | +| A guard that reads a machine-readable config resolves only part of a construct, such as the first `{a,b}` alternative, a nested group, or one level of a nested mapping | resolve every alternative and nesting level, or refuse the pattern with the file and line that uses it | a partially-read rule still matches its own representative path, so the guard passes while the real configuration disagrees; refusing beats guessing (PR #131 review finding in `scripts/lint-line-endings.js`) | +| A guard that names the argument forms it rejects and passes everything it does not name | require the exact arguments the policy permits, and report the file with every argument found | a switch the reader does not recognize turns off the policy the guard claims to enforce, so `-nowarn:1701` or `-warnaserror-` in a `csc.rsp` passed the guard that owns warnings-as-errors (`scripts/lint-assembly-warnings.ps1`, #132) | +| Pixel or region validation that records UI Toolkit subtrees without checking whether they render | filter candidates through a rendered check before measuring — `display:none` ancestor chain, resolved per-element visibility, and positive area (`DocsImageCapture.IsRenderedForCapture` precedent) | a `display:none` subtree reports laid-out children but paints nothing, so its clamped one-pixel sample always fails and rejects a faithful capture; persisted namespace collapse makes hidden rows a normal host state (PR #140 review finding) | diff --git a/.llm/references/product-decisions.md b/.llm/references/product-decisions.md index e56f590..e483744 100644 --- a/.llm/references/product-decisions.md +++ b/.llm/references/product-decisions.md @@ -1,47 +1,32 @@ # Standing Product Decisions -Long-lived constraints for every DxVisualizer task. Recorded decisions stay -here instead of `PLAN.md`; revisit only by explicit owner decision. +Long-lived constraints for every DxVisualizer task. Recorded decisions stay here instead of `PLAN.md`; revisit only by explicit owner decision. ## Compatibility -- Preserve Unity 2021.3+ compatibility and existing assets, settings, saved - selections, ordering, and public extension contracts. -- Retain a minimal player-compatible runtime assembly. Exclude editor tooling - and development dependencies from players; do not break games whose assets - inherit from `BaseDataObject`. -- Preserve exact-type matching and current main-asset discovery. Subasset - browsing is out of scope; document the boundary rather than changing identity - semantics. +- Preserve Unity 2021.3+ compatibility and existing assets, settings, saved selections, ordering, and public extension contracts. +- Retain a minimal player-compatible runtime assembly. Exclude editor tooling and development dependencies from players; do not break games whose assets inherit from + `BaseDataObject`. +- Preserve exact-type matching and current main-asset discovery. Subasset browsing is out of scope; document the boundary rather than changing identity semantics. ## Product behavior -- Instant loading means immediate interaction and visible rows, with metadata - indexing and asset loading performed incrementally. Never require 10,000 live - ScriptableObjects before displaying results. -- Offer automatic density adjustment, a persisted manual scale override, and - responsive pane arrangements. -- Cross-platform determinism means package-owned layout and alignment within - one physical pixel at equivalent dimensions, DPI, and theme. OS font - rasterization and third-party inspector internals may differ. -- Pause background indexing, automatic refresh, and package-driven asset - editing during Play Mode. Keep the last view visible and resume afterwards. +- Instant loading means immediate interaction and visible rows, with metadata indexing and asset loading performed incrementally. Never require 10,000 live ScriptableObjects before + displaying results. +- Offer automatic density adjustment, a persisted manual scale override, and responsive pane arrangements. +- Cross-platform determinism means package-owned layout and alignment within one physical pixel at equivalent dimensions, DPI, and theme. OS font rasterization and third-party + inspector internals may differ. +- Pause background indexing, automatic refresh, and package-driven asset editing during Play Mode. Keep the last view visible and resume afterwards. ## Automation and extensibility -- Provide typed C# editor APIs and versioned JSON commands. Reuse the existing - Unity MCP bridge; do not build a dedicated MCP server, daemon, HTTP listener, - or background file watcher. +- Provide typed C# editor APIs and versioned JSON commands. Reuse the existing Unity MCP bridge; do not build a dedicated MCP server, daemon, HTTP listener, or background file + watcher. ## Build, test, and release -- Run all Unity tests, performance experiments, builds, and screenshots - locally. Do not add Unity CI runners, Unity-in-CI workflows, or license or - seat requirements. -- Cloud release automation may assemble a `.unitypackage` with ordinary archive - tooling, but must never install, invoke, or authenticate Unity. A validated - `.unitypackage` ships alongside the npm tarball. -- Build and validate documentation locally. A documentation-only GitHub Pages - publishing workflow may publish prepared artifacts without executing Unity. -- Preserve unrelated workspace changes. Do not perform broad cleanup of the - existing agent and devcontainer tooling. +- Run all Unity tests, performance experiments, builds, and screenshots locally. Do not add Unity CI runners, Unity-in-CI workflows, or license or seat requirements. +- Cloud release automation may assemble a `.unitypackage` with ordinary archive tooling, but must never install, invoke, or authenticate Unity. A validated `.unitypackage` ships + alongside the npm tarball. +- Build and validate documentation locally. A documentation-only GitHub Pages publishing workflow may publish prepared artifacts without executing Unity. +- Preserve unrelated workspace changes. Do not perform broad cleanup of the existing agent and devcontainer tooling. diff --git a/.llm/references/roadmap.md b/.llm/references/roadmap.md index efbba56..934da93 100644 --- a/.llm/references/roadmap.md +++ b/.llm/references/roadmap.md @@ -1,287 +1,182 @@ # Modernization Roadmap: Areas T01-T12 -Detail behind the roadmap table in `PLAN.md`. Areas are priorities and outcome -definitions, not a queue: select one bounded slice per run (`GOAL.md`). Mark an -item delivered only when the work is on the default branch; keep PR and issue -references as the evidence. +Detail behind the roadmap table in `PLAN.md`. Areas are priorities and outcome definitions, not a queue: select one bounded slice per run (`GOAL.md`). Mark an item delivered only +when the work is on the default branch; keep PR and issue references as the evidence. -Status legend: `done` (all known scope on main), `partial`, `open` (no known -scope delivered). Re-verify against GitHub issues before selecting work; issues -override this file when they disagree. +Status legend: `done` (all known scope on main), `partial`, `open` (no known scope delivered). Re-verify against GitHub issues before selecting work; issues override this file when +they disagree. ## T01 - Local validation harness and baseline (partial) -Ad hoc measurements exist per task (probes recorded on issues and PRs); no -standing harness. - -- [ ] Local driver under `scripts/` with host-project, editor-version, - fixture-size, suite, and output-directory arguments; callable through the - Unity MCP bridge and direct host execution with explicit path resolution. -- [ ] Disposable fixture project referencing this package; deterministic - datasets of 100 / 1,000 / 10,000 / 50,000 assets (one large type, - many-type distribution, `BaseDataObject` subclasses, labels, nested - fields, duplicate short names, saved selections); verified seeds. -- [ ] Record environment (package/fixture revision, Unity version, OS, CPU, - memory, storage, GPU, DPI, theme, window size) and run timing inside - Unity with monotonic timers and profiler markers; MCP transport excluded. +Ad hoc measurements exist per task (probes recorded on issues and PRs); no standing harness. + +- [ ] Local driver under `scripts/` with host-project, editor-version, fixture-size, suite, and output-directory arguments; callable through the Unity MCP bridge and direct host + execution with explicit path resolution. +- [ ] Disposable fixture project referencing this package; deterministic datasets of 100 / 1,000 / 10,000 / 50,000 assets (one large type, many-type distribution, `BaseDataObject` + subclasses, labels, nested fields, duplicate short names, saved selections); verified seeds. +- [ ] Record environment (package/fixture revision, Unity version, OS, CPU, memory, storage, GPU, DPI, theme, window size) and run timing inside Unity with monotonic timers and + profiler markers; MCP transport excluded. - [ ] Machine-readable JSON results plus a readable comparison report. -- [ ] Measure separately: clean vs incremental compiles; shell construction, - first visible rows, inspector readiness, full indexing; search, selection, - filtering, scroll, type switches; cold index, warm disk, warm session; - allocations and realized elements; Play entry with package absent, - closed, idle, indexing. Five warm-ups and 30+ repetitions for warm - scenarios; 5+ samples with spread for expensive scenarios. Package-absent - control uses an ordinary-ScriptableObject fixture. +- [ ] Measure separately: clean vs incremental compiles; shell construction, first visible rows, inspector readiness, full indexing; search, selection, filtering, scroll, type + switches; cold index, warm disk, warm session; allocations and realized elements; Play entry with package absent, closed, idle, indexing. Five warm-ups and 30+ repetitions + for warm scenarios; 5+ samples with spread for expensive scenarios. Package-absent control uses an ordinary-ScriptableObject fixture. -Done when: the baseline is reproducible locally, distinguishes cold from warm -work, and identifies costs without changing normal package behavior. +Done when: the baseline is reproducible locally, distinguishes cold from warm work, and identifies costs without changing normal package behavior. ## T02 - Compatibility and persistence characterization (partial) -Delivered: runtime assembly surface pins (#88), persisted-state contract pins -(#89), runtime signature pins with per-accessor access/virtualness (#90, #92), -enum contract tests, malformed user-JSON fail-closed parser (#24), identity -regressions, README accuracy corrections (#106). - -- [ ] `Tests/Runtime` PlayMode coverage: `BaseDataObject` lifecycle callbacks - and serialized asset-state persistence, in a correctly constrained - assembly; keep filesystem/editor-callback tests in EditMode. -- [ ] Behavior inventory still to capture as executable coverage: duplicate - type short names, missing scripts/assets, removed managed types, missing - saved selections, empty projects, clone/create/rename paths, both - persistence modes (dirty state, idempotent writes, mode switching). -- [ ] Runtime compatibility fixtures deriving from `BaseDataObject`, - implementing extension interfaces, and overriding virtuals; preserve - fields and `.meta` GUIDs when moving files. - -Done when: behavior that must survive refactoring has executable coverage and -saved-state examples suitable for migration tests. +Delivered: runtime assembly surface pins (#88), persisted-state contract pins (#89), runtime signature pins with per-accessor access/virtualness (#90, #92), enum contract tests, +malformed user-JSON fail-closed parser (#24), identity regressions, README accuracy corrections (#106). + +- [ ] `Tests/Runtime` PlayMode coverage: `BaseDataObject` lifecycle callbacks and serialized asset-state persistence, in a correctly constrained assembly; keep + filesystem/editor-callback tests in EditMode. +- [ ] Behavior inventory still to capture as executable coverage: duplicate type short names, missing scripts/assets, removed managed types, missing saved selections, empty + projects, clone/create/rename paths, both persistence modes (dirty state, idempotent writes, mode switching). +- [ ] Runtime compatibility fixtures deriving from `BaseDataObject`, implementing extension interfaces, and overriding virtuals; preserve fields and `.meta` GUIDs when moving + files. + +Done when: behavior that must survive refactoring has executable coverage and saved-state examples suitable for migration tests. ## T03 - Compilation and player boundary (partial) -Delivered: unused reflection-emit accessors removed (#84), editor-only helpers -moved to the editor assembly (#87), npm/UPM payload slimmed (#102, closes #32), -SonarWay enforcement with pinned installer (#94, #97, #98, #100, #102), -usings-inside-namespace lint (#124, #125). +Delivered: unused reflection-emit accessors removed (#84), editor-only helpers moved to the editor assembly (#87), npm/UPM payload slimmed (#102, closes #32), SonarWay enforcement +with pinned installer (#94, #97, #98, #100, #102), usings-inside-namespace lint (#124, #125). -- [ ] Player smoke projects with plain assets and with `BaseDataObject` assets; - verify editor/test assemblies and resources are absent and retained - runtime assets load. -- [ ] Before/after clean and incremental compile measurements with assembly - dependency evidence; report only improvements beyond baseline variation. -- [ ] Keep the runtime/editor split by default; add an assembly only when a T01 - experiment proves a compile benefit without API or serialization break. +- [ ] Player smoke projects with plain assets and with `BaseDataObject` assets; verify editor/test assemblies and resources are absent and retained runtime assets load. +- [ ] Before/after clean and incremental compile measurements with assembly dependency evidence; report only improvements beyond baseline variation. +- [ ] Keep the runtime/editor split by default; add an assembly only when a T01 experiment proves a compile benefit without API or serialization break. -Done when: player compatibility is preserved, unnecessary code is removed, and -compilation results are documented. +Done when: player compatibility is preserved, unnecessary code is removed, and compilation results are documented. ## T04 - Service extraction and work ownership (partial) -Delivered: index classification defers while the AssetDatabase is busy (#103), -GUID normalization defers while busy with idle re-normalization (#105), -Play Mode mutation gate and suspension (#101), event-driven debounced splitter -persistence (#93). - -- [ ] Extract focused editor services (type catalog, indexing/search, - persistence, asset operations) in small steps with the T02 suite green; - no framework, service locator, or DI dependency. -- [ ] One owner for queued work, subscriptions, cancellation; generation - identifiers so stale callbacks never update current state; idempotent - disposal across close, disable, destroy, compilation, domain reload. +Delivered: index classification defers while the AssetDatabase is busy (#103), GUID normalization defers while busy with idle re-normalization (#105), Play Mode mutation gate and +suspension (#101), event-driven debounced splitter persistence (#93). + +- [ ] Extract focused editor services (type catalog, indexing/search, persistence, asset operations) in small steps with the T02 suite green; no framework, service locator, or DI + dependency. +- [ ] One owner for queued work, subscriptions, cancellation; generation identifiers so stale callbacks never update current state; idempotent disposal across close, disable, + destroy, compilation, domain reload. - [ ] Separate logical state updates from view refresh and persistence writes. -- [ ] Dormant startup: closed-window import callbacks may mark invalidation but - must not load assets or build indexes. +- [ ] Dormant startup: closed-window import callbacks may mark invalidation but must not load assets or build indexes. -Done when: services are exercisable without opening a window, work ownership is -explicit, and no duplicate old/new implementation paths remain. +Done when: services are exercisable without opening a window, work ownership is explicit, and no duplicate old/new implementation paths remain. ## T05 - Incremental metadata index and lazy loading (open) -- [ ] Metadata records keyed by asset GUID (path, exact type identity, display - name, description, labels, ordering inputs, search payload, status); - assembly-qualified identity internally; never resolve duplicate short - names by first match. -- [ ] Cheap identity/path catalog before loading objects; prioritize saved - selection and visible rows. -- [ ] Versioned disposable cache under `Library/DataVisualizer/` with schema - marker and fingerprints; warm cache reads without synchronous full - reconciliation before first paint. -- [ ] Handle import, delete, move, label edits, serialized edits, Undo/Redo, - recompile; coalesce changes; reject irrelevant changes without loading - every `.asset`. -- [ ] Cooperative slices targeting 4 ms with cancellation; main-thread Unity - object access; release package-owned references; never use loaded-object - count as list length. +- [ ] Metadata records keyed by asset GUID (path, exact type identity, display name, description, labels, ordering inputs, search payload, status); assembly-qualified identity + internally; never resolve duplicate short names by first match. +- [ ] Cheap identity/path catalog before loading objects; prioritize saved selection and visible rows. +- [ ] Versioned disposable cache under `Library/DataVisualizer/` with schema marker and fingerprints; warm cache reads without synchronous full reconciliation before first paint. +- [ ] Handle import, delete, move, label edits, serialized edits, Undo/Redo, recompile; coalesce changes; reject irrelevant changes without loading every `.asset`. +- [ ] Cooperative slices targeting 4 ms with cancellation; main-thread Unity object access; release package-owned references; never use loaded-object count as list length. - [ ] Preserve custom order and selection across partial loads. -Tests: corrupt/incompatible caches, changes while closed, interrupted indexing, -type switching during loads, import bursts, duplicate GUIDs, selected-asset -deletion. +Tests: corrupt/incompatible caches, changes while closed, interrupted indexing, type switching during loads, import bursts, duplicate GUIDs, selected-asset deletion. -Done when: the 10k fixture is interactive before full indexing, warm metadata -is reused correctly, and retained live-object references stay bounded. +Done when: the 10k fixture is interactive before full indexing, warm metadata is reused correctly, and retained live-object references stay bounded. ## T06 - Search and rendering performance (partial) -Delivered: search match-source plumbing and highlight fixes (#77, #79, and -related); current search behavior documented on the docs site (#134). - -- [ ] Share the T05 index between browsing and global search; extract - searchable strings once per invalidation; cache reflection traversal - metadata by type with cycle and null handling. -- [ ] Show indexed matches during incomplete indexing with explicit status; - rerun the live query as updates arrive; cancel superseded queries. -- [ ] Metadata-backed `itemsSource` on the existing fixed-height `ListView` - with reusable rows and targeted refresh; `fixedItemHeight` from density - metrics. -- [ ] Virtualized flattened namespace/type navigation compatible with Unity - 2021.3; preserve collapse state and drag/drop ordering. -- [ ] No inspectors, `SerializedObject` bindings, or custom GUI for offscreen - rows; batch GUID-to-row index updates. - -Done when: indexed search meets the T12 timing target and realized visual -elements stay bounded by the viewport as dataset size grows. +Delivered: search match-source plumbing and highlight fixes (#77, #79, and related); current search behavior documented on the docs site (#134). + +- [ ] Share the T05 index between browsing and global search; extract searchable strings once per invalidation; cache reflection traversal metadata by type with cycle and null + handling. +- [ ] Show indexed matches during incomplete indexing with explicit status; rerun the live query as updates arrive; cancel superseded queries. +- [ ] Metadata-backed `itemsSource` on the existing fixed-height `ListView` with reusable rows and targeted refresh; `fixedItemHeight` from density metrics. +- [ ] Virtualized flattened namespace/type navigation compatible with Unity 2021.3; preserve collapse state and drag/drop ordering. +- [ ] No inspectors, `SerializedObject` bindings, or custom GUI for offscreen rows; batch GUID-to-row index updates. + +Done when: indexed search meets the T12 timing target and realized visual elements stay bounded by the viewport as dataset size grows. ## T07 - Persistent, responsive, deterministic layout (partial) -Delivered: debounced geometry persistence with close/reload flush and teardown -fixes (#93), density-token foundation and theme variants (#116, #119, #120). - -- [ ] Versioned per-user layout state (pane widths, floating geometry, scale - mode and manual scale) with one-time migration from EditorPrefs; - distinguish preferred from clamped dimensions. -- [ ] Restore after valid geometry; suppress writes during restoration; let - Unity own docked placement; remove platform-specific monitor P/Invoke - paths. -- [ ] Auto scale plus manual 80-150% in 5% steps and Reset; derive package - fonts, spacing, control heights, and row heights from one metrics model. -- [ ] Responsive panes: collapse navigation behind a toggle, list/detail - navigation when panes cannot fit, restore preferred arrangement later. -- [ ] Normalize control padding, borders, alignment, and icons through shared - USS classes and metrics; clamp popovers to window bounds. - -Done when: preferred geometry survives restoration without drift and -package-owned alignment is within one physical pixel under equivalent -conditions. +Delivered: debounced geometry persistence with close/reload flush and teardown fixes (#93), density-token foundation and theme variants (#116, #119, #120). + +- [ ] Versioned per-user layout state (pane widths, floating geometry, scale mode and manual scale) with one-time migration from EditorPrefs; distinguish preferred from clamped + dimensions. +- [ ] Restore after valid geometry; suppress writes during restoration; let Unity own docked placement; remove platform-specific monitor P/Invoke paths. +- [ ] Auto scale plus manual 80-150% in 5% steps and Reset; derive package fonts, spacing, control heights, and row heights from one metrics model. +- [ ] Responsive panes: collapse navigation behind a toggle, list/detail navigation when panes cannot fit, restore preferred arrangement later. +- [ ] Normalize control padding, borders, alignment, and icons through shared USS classes and metrics; clamp popovers to window bounds. + +Done when: preferred geometry survives restoration without drift and package-owned alignment is within one physical pixel under equivalent conditions. ## T08 - Play Mode overhead and suspension (partial) -Delivered: Play Mode suspension, paused indicator, mutation and serialized -inspector gating, resume path (#101). +Delivered: Play Mode suspension, paused indicator, mutation and serialized inspector gating, resume path (#101). -- [ ] Matched local benchmark showing added p95 Play-entry latency of at most - 20 ms (report window-open and window-closed separately). -- [ ] Exercise all four domain-reload/scene-reload combinations, including - transitions during indexing/search; queue and coalesce invalidation while - paused; resume incrementally. -- [ ] Verify closing the window leaves no scheduler, polling loop, or live - asset cache. +- [ ] Matched local benchmark showing added p95 Play-entry latency of at most 20 ms (report window-open and window-closed separately). +- [ ] Exercise all four domain-reload/scene-reload combinations, including transitions during indexing/search; queue and coalesce invalidation while paused; resume incrementally. +- [ ] Verify closing the window leaves no scheduler, polling loop, or live asset cache. -Done when: the benchmark and lifecycle regression tests demonstrate correct -cleanup with reload disabled. +Done when: the benchmark and lifecycle regression tests demonstrate correct cleanup with reload disabled. ## T09 - Public editor API and JSON automation (open) -- [ ] Documented facade in the editor namespace: configuration read/apply, - type discovery, paged metadata queries, index progress, selection, asset - operations, processor execution; GUID and assembly-qualified identifiers; - cancellation and progress; index completeness in results. -- [ ] UI mutations route through these services; mutation preview without side - effects; no modal dialogs; per-item outcomes; reject mutations while - playing. -- [ ] JSON schema version 1 with published schemas and examples; explicit - dispatch; file-based `-executeMethod` entrypoint plus an in-process - entrypoint via the existing MCP tooling; no daemons or HTTP listeners. -- [ ] Validate schema versions, targets, identities, paths, values; idempotent - configuration application. - -Tests: UI-service vs JSON equivalence, preview side effects, malformed input, -partial failures, Play Mode rejection. - -Done when: an agent can configure, query, and operate the package through -documented C# or JSON without opening the window or using private reflection. +- [ ] Documented facade in the editor namespace: configuration read/apply, type discovery, paged metadata queries, index progress, selection, asset operations, processor execution; + GUID and assembly-qualified identifiers; cancellation and progress; index completeness in results. +- [ ] UI mutations route through these services; mutation preview without side effects; no modal dialogs; per-item outcomes; reject mutations while playing. +- [ ] JSON schema version 1 with published schemas and examples; explicit dispatch; file-based `-executeMethod` entrypoint plus an in-process entrypoint via the existing MCP + tooling; no daemons or HTTP listeners. +- [ ] Validate schema versions, targets, identities, paths, values; idempotent configuration application. + +Tests: UI-service vs JSON equivalence, preview side effects, malformed input, partial failures, Play Mode rejection. + +Done when: an agent can configure, query, and operate the package through documented C# or JSON without opening the window or using private reflection. ## T10 - Automated local editor captures (partial) -Delivered: Tests-assembly offscreen capture with fail-closed guards (#123), -docs-image capture driver with shot manifest and `-executeMethod` CLI (#125). +Delivered: Tests-assembly offscreen capture with fail-closed guards (#123), docs-image capture driver with shot manifest and `-executeMethod` CLI (#125). -Blocked: the offscreen capture deterministically omits specific subtrees -(unselected rows, asset-name/labels rows, InspectorElement internals) while the -on-screen window paints them; twelve falsified workarounds recorded on #114. +Blocked: the offscreen capture deterministically omits specific subtrees (unselected rows, asset-name/labels rows, InspectorElement internals) while the on-screen window paints +them; twelve falsified workarounds recorded on #114. -- [ ] Fix the render gap; then regenerate the documented image inventory from - production UI. -- [ ] Scenario inventory and manifest validation (nonempty, distinct, expected - dimensions); staged publication preserving the previous complete set; - cleanup of textures, windows, and settings. -- [ ] Geometry assertions cross-platform; screenshot baselines pinned to - OS/Unity/theme/DPI; changed baselines require inspection. +- [ ] Fix the render gap; then regenerate the documented image inventory from production UI. +- [ ] Scenario inventory and manifest validation (nonempty, distinct, expected dimensions); staged publication preserving the previous complete set; cleanup of textures, windows, + and settings. +- [ ] Geometry assertions cross-platform; screenshot baselines pinned to OS/Unity/theme/DPI; changed baselines require inspection. -Done when: one local command regenerates the documented image inventory with -repeatable evidence and no consumer dependency or CI Unity execution. +Done when: one local command regenerates the documented image inventory with repeatable evidence and no consumer dependency or CI Unity execution. ## T11 - Documentation and GitHub Pages (partial) -Delivered: MkDocs Material foundation with strict build and Pages publishing -workflow (#130), guide pages for search/filtering, asset management, -organizing, and extending (#134). +Delivered: MkDocs Material foundation with strict build and Pages publishing workflow (#130), guide pages for search/filtering, asset management, organizing, and extending (#134). -- [x] README hero imagery from the capture pipeline (#140, #143); README - stays concise and links into the site. -- [ ] Troubleshooting page (incomplete indexing, stale metadata, missing - scripts, small windows, capture limits, host/container paths). -- [ ] Measured performance section once T01 produces numbers; captures embedded - with captions and alt text, validated against the manifest. +- [x] README hero imagery from the capture pipeline (#140, #143); README stays concise and links into the site. +- [ ] Troubleshooting page (incomplete indexing, stale metadata, missing scripts, small windows, capture limits, host/container paths). +- [ ] Measured performance section once T01 produces numbers; captures embedded with captions and alt text, validated against the manifest. - [ ] Odin behavior documented separately from the default inspector contract. -Done when: the published site is accurate with working examples and complete -screenshots; unavailable publication access is reported explicitly. +Done when: the published site is accurate with working examples and complete screenshots; unavailable publication access is reported explicitly. ## T12 - Acceptance matrix and release evidence (partial) -Delivered: Keep a Changelog file and package metadata (#107), release -preparation (#108), tag-driven publish (#111), deterministic `.unitypackage` -builder and fail-closed validator (#112, #113), publish-time package build, -validation, and GitHub Release with npm + `.unitypackage` artifacts (#126), -credential documentation pinned to workflows (#128), unified release -preparation and publication (#135), v0.1.0 shipped (#136). - -- [ ] Local compatibility matrix: pinned Unity 2021.3, 2022.3, representative - Unity 6 LTS, and the confirmed 6000.4.6f1 host; Windows/macOS/Linux; - light/dark themes, 100/150/200% DPI, manual scale endpoints, docking, - mixed-monitor movement; EditMode + PlayMode + player smoke; optional Odin - where licensed; EntityId branch when Unity 6000.5+ is available. Treat - unavailable combinations as unverified and publish the gaps. -- [ ] Performance acceptance: warm open p95 <= 100 ms at 10k; indexed search - p95 <= 100 ms including debounce; Play-entry p95 <= 20 ms added; 4 ms - cooperative slices; cold indexing measured; compile improvement beyond - noise; realized rows bounded by viewport; no package-owned memory - accumulation. Missed targets are recorded, not silently relaxed. -- [ ] Final milestone acceptance and handoff summary (what changed, what was - tested, measured improvements, unverified platforms). - -Done when: the support and performance matrix is published with unverified gaps -explicit; Unity validation remains a required local release gate. +Delivered: Keep a Changelog file and package metadata (#107), release preparation (#108), tag-driven publish (#111), deterministic `.unitypackage` builder and fail-closed validator +(#112, #113), publish-time package build, validation, and GitHub Release with npm + `.unitypackage` artifacts (#126), credential documentation pinned to workflows (#128), unified +release preparation and publication (#135), v0.1.0 shipped (#136). + +- [ ] Local compatibility matrix: pinned Unity 2021.3, 2022.3, representative Unity 6 LTS, and the confirmed 6000.4.6f1 host; Windows/macOS/Linux; light/dark themes, 100/150/200% + DPI, manual scale endpoints, docking, mixed-monitor movement; EditMode + PlayMode + player smoke; optional Odin where licensed; EntityId branch when Unity 6000.5+ is + available. Treat unavailable combinations as unverified and publish the gaps. +- [ ] Performance acceptance: warm open p95 <= 100 ms at 10k; indexed search p95 <= 100 ms including debounce; Play-entry p95 <= 20 ms added; 4 ms cooperative slices; cold indexing + measured; compile improvement beyond noise; realized rows bounded by viewport; no package-owned memory accumulation. Missed targets are recorded, not silently relaxed. +- [ ] Final milestone acceptance and handoff summary (what changed, what was tested, measured improvements, unverified platforms). + +Done when: the support and performance matrix is published with unverified gaps explicit; Unity validation remains a required local release gate. ## Sources and implementation references -Inspect upstream source and licenses at the pinned revision before adapting -code. Check API capabilities against the minimum supported Unity version. - -- [Unity 2021.3 ListView](https://docs.unity3d.com/2021.3/Documentation/Manual/UIE-uxml-element-ListView.html): - recycling, fixed-height virtualization, targeted refresh. -- [Unity assembly definitions](https://docs.unity3d.com/2021.3/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html): - assembly boundaries and compilation dependencies. -- [Unity domain reloading](https://docs.unity3d.com/2021.3/Documentation/Manual/DomainReloading.html): - lifecycle when reload is disabled. -- [Unity Helpers capture](https://github.com/Ambiguous-Interactive/unity-helpers/tree/main/Tests/Editor/Capture): - host windows, sentinels, documentation image tests. -- [DxMessaging](https://github.com/ambiguous-interactive/DxMessaging): - capture inventory and staged publication patterns; release-prepare and - release-tag workflows for the tag handoff shape. -- [Unity Helpers release workflow](https://github.com/Ambiguous-Interactive/unity-helpers/blob/main/.github/workflows/release.yml): - artifact identity, checksums, rerun behavior; replace its Unity export with - the no-Unity archive builder. -- [No-Unity `.unitypackage` builder example](https://github.com/premium-ads/max-adapter-unity/blob/main/scripts/pack-unitypackage.sh): - GUID-directory gzip/tar packages from committed `.meta` files. +Inspect upstream source and licenses at the pinned revision before adapting code. Check API capabilities against the minimum supported Unity version. + +- [Unity 2021.3 ListView](https://docs.unity3d.com/2021.3/Documentation/Manual/UIE-uxml-element-ListView.html): recycling, fixed-height virtualization, targeted refresh. +- [Unity assembly definitions](https://docs.unity3d.com/2021.3/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html): assembly boundaries and compilation + dependencies. +- [Unity domain reloading](https://docs.unity3d.com/2021.3/Documentation/Manual/DomainReloading.html): lifecycle when reload is disabled. +- [Unity Helpers capture](https://github.com/Ambiguous-Interactive/unity-helpers/tree/main/Tests/Editor/Capture): host windows, sentinels, documentation image tests. +- [DxMessaging](https://github.com/ambiguous-interactive/DxMessaging): capture inventory and staged publication patterns; release-prepare and release-tag workflows for the tag + handoff shape. +- [Unity Helpers release workflow](https://github.com/Ambiguous-Interactive/unity-helpers/blob/main/.github/workflows/release.yml): artifact identity, checksums, rerun behavior; + replace its Unity export with the no-Unity archive builder. +- [No-Unity `.unitypackage` builder example](https://github.com/premium-ads/max-adapter-unity/blob/main/scripts/pack-unitypackage.sh): GUID-directory gzip/tar packages from + committed `.meta` files. diff --git a/.llm/skills/ship-changes/SKILL.md b/.llm/skills/ship-changes/SKILL.md index 31b90c1..0ca46b5 100644 --- a/.llm/skills/ship-changes/SKILL.md +++ b/.llm/skills/ship-changes/SKILL.md @@ -15,6 +15,7 @@ Run the narrowest sufficient layer; escalate on failure: | --- | --- | --- | | Pre-commit hook | staged files | automatic (`pre-commit` + `.pre-commit-config.yaml`) | | C# member layout | all C# types | `npm run lint:csharp-member-order` | +| Markdown formatting | tracked Markdown | `npm run format:md:check` | | Local gate | whole harness | `npm run lint:llm` | | Harness self-tests | scripts | `pwsh -NoProfile -File scripts/tests/run-all.ps1` | | Packaging | tarball integrity | `npm pack` | @@ -27,6 +28,8 @@ Run the narrowest sufficient layer; escalate on failure: it automatically on staged C# files; verify with `-- check`). Then run `npm run lint:csharp-member-order`; the `:fix` command safely permutes complete member slices, but conditional/static-initialization barriers require manual review. +1b. Changed `.md` files (except `.prettierignore`d skills): `npm run format:md`, + verify with `npm run format:md:check`. 2. If any `.llm/**` file changed: `npm run lint:llm` (regenerates nothing; use `npm run lint:llm:fix` to auto-fix index drift and version sync, then review the diff and re-stage). diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..833f846 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,15 @@ +# Formatter scope is tracked Markdown; keep generated, vendored, and local +# trees out of the glob so runs stay deterministic. +.docs-captures/ +.docs-captures-run1/ +.docs-captures-run2/ +.docs-venv/ +.git/ +node_modules/ +progress/ +site/ + +# The skills harness owns these: index.md is generated by +# scripts/generate-skills-index.ps1, and SKILL.md files carry single-line +# frontmatter contracts enforced by scripts/lint-llm-instructions.ps1. +.llm/skills/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..1a5621b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 180, + "proseWrap": "always", + "endOfLine": "lf" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index cd28f13..3ec5c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,50 +2,37 @@ All notable changes to this package are documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added -- Add the Dx theme and make it the default: a fresh window opens in Dx and - Reset Theme returns to it. Explicit selections survive, and a missing Dx - asset falls back to the package style (#148). +- Add the Dx theme and make it the default: a fresh window opens in Dx and Reset Theme returns to it. Explicit selections survive, and a missing Dx asset falls back to the package + style (#148). ### Changed -- Renamed to DxVisualizer: display name, window title, and menu paths - (**Tools → Wallstop Studios → DxVisualizer**). Namespaces, types, package - name, saved state, and theme tokens are unchanged, so projects upgrade - without edits (#147). +- Renamed to DxVisualizer: display name, window title, and menu paths (**Tools → Wallstop Studios → DxVisualizer**). Namespaces, types, package name, saved state, and theme tokens + are unchanged, so projects upgrade without edits (#147). ### Fixed -- `BaseDataObject` asset ids no longer change on validation: an asset keeps - its authored id, and only an empty id fills from the asset GUID. Created - and cloned assets still get a fresh id (#137). +- `BaseDataObject` asset ids no longer change on validation: an asset keeps its authored id, and only an empty id fills from the asset GUID. Created and cloned assets still get a + fresh id (#137). ## [0.1.0] - 2026-10-07 ### Added -- Compact and Minimal built-in style variants: density-only theme presets that - keep the Classic palette and shrink window typography, action buttons, and - control chrome (Compact 13px type, 20px action buttons, 4px control radii; - Minimal 12px type, 16px action buttons, square control corners). -- Theme hot reload: an open Data Visualizer window re-applies the selected - theme immediately when its theme asset or referenced stylesheet is - reimported, reference-swapped, renamed, moved, or deleted; deletions fall - back to the package style with the existing saved-GUID semantics. -- A `.unitypackage` is now built and verified on each release and attached to - the GitHub Release alongside the npm package. +- Compact and Minimal built-in style variants: density-only theme presets that keep the Classic palette and shrink window typography, action buttons, and control chrome (Compact + 13px type, 20px action buttons, 4px control radii; Minimal 12px type, 16px action buttons, square control corners). +- Theme hot reload: an open Data Visualizer window re-applies the selected theme immediately when its theme asset or referenced stylesheet is reimported, reference-swapped, + renamed, moved, or deleted; deletions fall back to the package style with the existing saved-GUID semantics. +- A `.unitypackage` is now built and verified on each release and attached to the GitHub Release alongside the npm package. ### Changed -- Control typography and radii in the base stylesheet now resolve from the - `--dataviz-font-size`, `--dataviz-circle-size`, and `--dataviz-control-radius` - tokens instead of hardcoded values. The Classic preset keeps its type and - glyph sizes; the three large round action buttons are 1px smaller to match - the action-button token, and themes that override `--dataviz-font-size` now - also restyle the standard prose controls. +- Control typography and radii in the base stylesheet now resolve from the `--dataviz-font-size`, `--dataviz-circle-size`, and `--dataviz-control-radius` tokens instead of + hardcoded values. The Classic preset keeps its type and glyph sizes; the three large round action buttons are 1px smaller to match the action-button token, and themes that + override `--dataviz-font-size` now also restyle the standard prose controls. diff --git a/README.md b/README.md index c5f9d4d..b57377b 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,49 @@ > **🤖 AI Assistance Disclosure** > -> The early versions of Data Visualizer were heavily human-authored. More recent development has been a mix of human effort and AI assistance: human authors lead design, architecture, and review, while AI tools assist with feature development, bug detection, performance optimization, and documentation. +> The early versions of Data Visualizer were heavily human-authored. More recent development has been a mix of human effort and AI assistance: human authors lead design, +> architecture, and review, while AI tools assist with feature development, bug detection, performance optimization, and documentation. -Data Visualizer streamlines working with ScriptableObject-heavy systems by centralizing asset management, inspection, and batch operations in a single window. Instead of hunting through the Project panel and repeatedly switching contexts, you get a namespace-organized view of all your data types with inline editing, batch operations, and workflow automation. +Data Visualizer streamlines working with ScriptableObject-heavy systems by centralizing asset management, inspection, and batch operations in a single window. Instead of hunting +through the Project panel and repeatedly switching contexts, you get a namespace-organized view of all your data types with inline editing, batch operations, and workflow +automation. -Data Visualizer is free forever: no subscriptions, no paid upgrades, no feature-gated tiers. The full source is MIT-licensed, and every capability documented here ships in the free package. +Data Visualizer is free forever: no subscriptions, no paid upgrades, no feature-gated tiers. The full source is MIT-licensed, and every capability documented here ships in the free +package. -This guide captures the key points from the companion [video walkthrough](https://youtu.be/3oUxUSKNyhw) while keeping the instructions project-agnostic. A browsable version of this documentation, with per-topic pages, is published at . +This guide captures the key points from the companion [video walkthrough](https://youtu.be/3oUxUSKNyhw) while keeping the instructions project-agnostic. A browsable version of this +documentation, with per-topic pages, is published at . ## Getting Started -Open **Tools → Wallstop Studios → Data Visualizer** and dock it alongside the Inspector. The tool persists your layout, selection, and tracked types between sessions, so you can jump back into your workflow immediately. +Open **Tools → Wallstop Studios → Data Visualizer** and dock it alongside the Inspector. The tool persists your layout, selection, and tracked types between sessions, so you can +jump back into your workflow immediately. ## Window Layout -![Data Visualizer layout with namespace, object, and inspector columns](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-layout.png) -*Full window overview. Regenerated from the live window by the docs capture pipeline.* +![Data Visualizer layout with namespace, object, and inspector columns](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-layout.png) _Full +window overview. Regenerated from the live window by the docs capture pipeline._ The window uses a three-panel layout: -**Namespace & Type Panel (left)** organizes ScriptableObject types by C# namespace. Click a namespace to expose its types, then select a type to load all instances. Reorder namespaces and types by dragging them, or with the up and down arrow buttons, which move a row to the top or bottom of its list. Your ordering persists across sessions. +**Namespace & Type Panel (left)** organizes ScriptableObject types by C# namespace. Click a namespace to expose its types, then select a type to load all instances. Reorder +namespaces and types by dragging them, or with the up and down arrow buttons, which move a row to the top or bottom of its list. Your ordering persists across sessions. -**Objects Panel (center)** lists every instance of the selected type and keeps one selection at a time. Drag a row to place an instance precisely, or use its arrow buttons to move it to the top or bottom. Batch edits run through the per-type processors area, scoped to all instances or to the filtered set, and through the per-row actions. +**Objects Panel (center)** lists every instance of the selected type and keeps one selection at a time. Drag a row to place an instance precisely, or use its arrow buttons to move +it to the top or bottom. Batch edits run through the per-type processors area, scoped to all instances or to the filtered set, and through the per-row actions. -**Inspector Panel (right)** displays the full inspector for the selected asset, including Odin Inspector integrations and custom editors. Changes save immediately, just like Unity's default Inspector. +**Inspector Panel (right)** displays the full inspector for the selected asset, including Odin Inspector integrations and custom editors. Changes save immediately, just like +Unity's default Inspector. ## Instance Management ![Clone, rename, move, and delete controls on the object rows](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-instance-actions.png) -*Per-row asset management controls. Regenerated from the live window by the docs capture pipeline.* +_Per-row asset management controls. Regenerated from the live window by the docs capture pipeline._ Asset management controls sit on the right of each object row: -**Clone** (`++`) duplicates that row's asset in the same folder as the original, directly after it in the list. Any existing `(Clone)` or `(Clone n)` suffix is stripped from the source name first, then reapplied, so repeated clones read `(Clone)`, `(Clone 1)`, `(Clone 2)`, and so on. Useful for creating variants without leaving the window. +**Clone** (`++`) duplicates that row's asset in the same folder as the original, directly after it in the list. Any existing `(Clone)` or `(Clone n)` suffix is stripped from the +source name first, then reapplied, so repeated clones read `(Clone)`, `(Clone 1)`, `(Clone 2)`, and so on. Useful for creating variants without leaving the window. **Rename** opens a draggable prompt that renames the asset on disk. No need to coordinate between multiple panels or windows. @@ -46,21 +56,24 @@ Inspector edits save immediately. Your selection persists when switching between ## Creating Assets -![Create popover asking for a new asset name](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-create.png) -*Create popover over the object list. Regenerated from the live window by the docs capture pipeline.* +![Create popover asking for a new asset name](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-create.png) _Create popover over the object +list. Regenerated from the live window by the docs capture pipeline._ -The **Create** button asks for a name and spawns a new instance of the active type under your configured **Data Folder** (see Settings below), in a per-type folder named after the type's full namespace, such as `Assets/Data/MyGame/Items/WeaponData/`. Those folders are created for you. Clones stay beside their originals regardless of the Data Folder setting. Chain create with rename or move to place new assets exactly where you need them. +The **Create** button asks for a name and spawns a new instance of the active type under your configured **Data Folder** (see Settings below), in a per-type folder named after the +type's full namespace, such as `Assets/Data/MyGame/Items/WeaponData/`. Those folders are created for you. Clones stay beside their originals regardless of the Data Folder setting. +Chain create with rename or move to place new assets exactly where you need them. ## Building Your Type Catalog ![Manage-visible-types popover listing the project's ScriptableObject types](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-import.png) -*Type search popover over the namespace panel. Regenerated from the live window by the docs capture pipeline.* +_Type search popover over the namespace panel. Regenerated from the live window by the docs capture pipeline._ Three controls above the Namespace panel populate your catalog: **Search Types** queries Unity's known ScriptableObject types. Add individual types or entire namespaces in one operation. -**Scan Asset Folder** crawls a folder recursively, discovering all ScriptableObject types and wiring up existing instances. Ideal for bootstrapping Data Visualizer on established projects. +**Scan Asset Folder** crawls a folder recursively, discovering all ScriptableObject types and wiring up existing instances. Ideal for bootstrapping Data Visualizer on established +projects. **Scan Scripts Folder** targets source folders containing ScriptableObject classes. Use this when you've written new types but haven't created any assets yet. @@ -72,20 +85,29 @@ Organize the catalog to match your team's mental model. The structure persists a Three finders narrow different things. -The **Search** box in the window header row, next to the Settings button, searches every tracked type rather than only the selected one. A space-separated query matches an asset when any one of its terms matches an asset name, a type name, an exact asset GUID, or a string field on the asset or a nested plain object; field matching is case-insensitive and skips primitives, vectors, colors, and references to other Unity objects. Results are ordered by asset name then full type name, list up to two matched fields for context, highlight the matched terms, and cap at 25 results. Use Up and Down to move, Enter to open the highlighted asset, Escape to dismiss. +The **Search** box in the window header row, next to the Settings button, searches every tracked type rather than only the selected one. A space-separated query matches an asset +when any one of its terms matches an asset name, a type name, an exact asset GUID, or a string field on the asset or a nested plain object; field matching is case-insensitive and +skips primitives, vectors, colors, and references to other Unity objects. Results are ordered by asset name then full type name, list up to two matched fields for context, +highlight the matched terms, and cap at 25 results. Use Up and Down to move, Enter to open the highlighted asset, Escape to dismiss. The filter field above the Namespace list narrows type rows by display name with case-insensitive matching; namespace headers are not filtered. -Dragging labels from **Available** into the **AND:** or **OR:** rows filters the selected type's instances by Unity asset labels. The **AND &&** and **OR ||** switch chooses between requiring every dragged label and requiring any one of them; in OR mode an empty clause never counts as a match. The Advanced row refuses to collapse while OR mode or any OR label is active, so a filter that hides rows is never left concealed. A line under the filter reports how many objects are hidden. The selected asset's inspector panel adds and removes its labels, and the filter re-applies immediately. +Dragging labels from **Available** into the **AND:** or **OR:** rows filters the selected type's instances by Unity asset labels. The **AND &&** and **OR ||** switch chooses +between requiring every dragged label and requiring any one of them; in OR mode an empty clause never counts as a match. The Advanced row refuses to collapse while OR mode or any +OR label is active, so a filter that hides rows is never left concealed. A line under the filter reports how many objects are hidden. The selected asset's inspector panel adds and +removes its labels, and the filter re-applies immediately. ## Settings ![Settings popover with persistence toggles and data folder field](https://raw.githubusercontent.com/wallstop/DataVisualizer/main/docs/images/data-visualizer-settings.png) -*Settings popover. Regenerated from the live window by the docs capture pipeline.* +_Settings popover. Regenerated from the live window by the docs capture pipeline._ -**Persist State in UserState** (the default) stores the selected namespace and type, the selected object per type, namespace, type, and object ordering, collapse state, tracked types, per-type label filters, and the per-type processor scope in a per-user JSON file instead of a shared project asset. Each developer keeps a private arrangement and version control stays free of layout churn. +**Persist State in UserState** (the default) stores the selected namespace and type, the selected object per type, namespace, type, and object ordering, collapse state, tracked +types, per-type label filters, and the per-type processor scope in a per-user JSON file instead of a shared project asset. Each developer keeps a private arrangement and version +control stays free of layout churn. -Turning the setting off stores the same state inside a `DataVisualizerSettings` asset in the project, which suits a team that wants one shared arrangement. Data Visualizer creates one at `Assets/Editor/DataVisualizerSettings.asset` on first use if no such asset exists. Switching between the two copies the current state across. +Turning the setting off stores the same state inside a `DataVisualizerSettings` asset in the project, which suits a team that wants one shared arrangement. Data Visualizer creates +one at `Assets/Editor/DataVisualizerSettings.asset` on first use if no such asset exists. Switching between the two copies the current state across. **Select Active Object** syncs selection between Data Visualizer and Unity's Inspector window. Useful for cross-referencing assets in other editor windows. @@ -93,13 +115,22 @@ Turning the setting off stores the same state inside a `DataVisualizerSettings` ### Themes -**Classic**, **Nord**, **Dracula**, **Compact**, and **Minimal** ship under `Editor/DataVisualizer/Styles` in the package. In **Settings → Theme**, click the current theme to open a searchable dropdown. Search by name or asset path, use Up/Down and Enter to select, or press Escape to cancel. Themes in both Assets and Packages are listed; duplicate names show their paths. Classic uses the original palette, Nord uses blue-gray surfaces and cyan accents, and Dracula uses dark surfaces and purple accents. Compact and Minimal keep the Classic palette and shrink the density: window typography, action buttons, and control chrome. Compact uses 13px type with 20px action buttons and 4px control radii; Minimal uses 12px type with 16px action buttons and square control corners. These editor-only assets and their stylesheets are included in the package, not an optional sample. +**Classic**, **Nord**, **Dracula**, **Compact**, and **Minimal** ship under `Editor/DataVisualizer/Styles` in the package. In **Settings → Theme**, click the current theme to open +a searchable dropdown. Search by name or asset path, use Up/Down and Enter to select, or press Escape to cancel. Themes in both Assets and Packages are listed; duplicate names show +their paths. Classic uses the original palette, Nord uses blue-gray surfaces and cyan accents, and Dracula uses dark surfaces and purple accents. Compact and Minimal keep the +Classic palette and shrink the density: window typography, action buttons, and control chrome. Compact uses 13px type with 20px action buttons and 4px control radii; Minimal uses +12px type with 16px action buttons and square control corners. These editor-only assets and their stylesheets are included in the package, not an optional sample. -The palettes style the window background and text, lists, search results, popovers, label and processor panels, dividers, and standard UI Toolkit inputs, buttons, foldouts, toggles, scrollers, and inspector surfaces. Action colors remain distinct: danger for delete, positive for create/clone/confirm, secondary for rename/script-folder loading, warning for cancel/move, and emphasis for alternate toggle modes. **Reset Theme** keeps the compact Data Folder button sizing, clears the saved selection, and restores Classic. Selecting the Classic asset gives the same appearance but keeps an explicit selection. +The palettes style the window background and text, lists, search results, popovers, label and processor panels, dividers, and standard UI Toolkit inputs, buttons, foldouts, +toggles, scrollers, and inspector surfaces. Action colors remain distinct: danger for delete, positive for create/clone/confirm, secondary for rename/script-folder loading, warning +for cancel/move, and emphasis for alternate toggle modes. **Reset Theme** keeps the compact Data Folder button sizing, clears the saved selection, and restores Classic. Selecting +the Classic asset gives the same appearance but keeps an explicit selection. -Create a theme with **Assets → Create → Wallstop Studios → DataVisualizer → Data Visualizer Theme**. Assign a `.uss` asset to its **Style Sheet** field, then choose the theme in the window's **Settings → Theme** field. Keep your theme and stylesheet under an `Editor` folder; they are editor-only assets. +Create a theme with **Assets → Create → Wallstop Studios → DataVisualizer → Data Visualizer Theme**. Assign a `.uss` asset to its **Style Sheet** field, then choose the theme in +the window's **Settings → Theme** field. Keep your theme and stylesheet under an `Editor` folder; they are editor-only assets. -The selected theme follows the existing project/user persistence setting. Switching that setting copies the current selection. Choose **Classic (Default / Reset)** or use **Reset Theme** to restore the package style. A missing theme falls back to the package style without discarding its saved GUID; reset clears that reference too. +The selected theme follows the existing project/user persistence setting. Switching that setting copies the current selection. Choose **Classic (Default / Reset)** or use **Reset +Theme** to restore the package style. A missing theme falls back to the package style without discarding its saved GUID; reset clears that reference too. For example, this stylesheet changes the accent, standard button states, window font size, and action-button size: @@ -114,30 +145,45 @@ For example, this stylesheet changes the accent, standard button states, window } ``` -The override stylesheet is applied after the package stylesheet. Standard control rules are scoped to `.dataviz-root` inside the Data Visualizer window. Normal USS selector precedence still applies, and explicit inline styles take priority. Data color swatches and label colors remain data-driven; IMGUI and custom third-party inspector styling are not replaced. Use `Nord.uss` or `Dracula.uss` as palette references; copy them under your project's `Editor` folder before customizing rather than editing installed package files. The `--dataviz-background` and `--dataviz-input` tokens control the window and input surfaces. Semantic `--dataviz-danger`, `--dataviz-positive`, `--dataviz-secondary`, `--dataviz-warning`, and `--dataviz-emphasis` tokens each have a matching `--dataviz-on-*` foreground token for filled states. Theme changes apply to the open window without reselecting or reopening: editing the applied theme's stylesheet, changing its Style Sheet reference, or renaming/moving the assets updates immediately, and deleting the applied theme falls back to the package style while keeping the saved GUID semantics. +The override stylesheet is applied after the package stylesheet. Standard control rules are scoped to `.dataviz-root` inside the Data Visualizer window. Normal USS selector +precedence still applies, and explicit inline styles take priority. Data color swatches and label colors remain data-driven; IMGUI and custom third-party inspector styling are not +replaced. Use `Nord.uss` or `Dracula.uss` as palette references; copy them under your project's `Editor` folder before customizing rather than editing installed package files. The +`--dataviz-background` and `--dataviz-input` tokens control the window and input surfaces. Semantic `--dataviz-danger`, `--dataviz-positive`, `--dataviz-secondary`, +`--dataviz-warning`, and `--dataviz-emphasis` tokens each have a matching `--dataviz-on-*` foreground token for filled states. Theme changes apply to the open window without +reselecting or reopening: editing the applied theme's stylesheet, changing its Style Sheet reference, or renaming/moving the assets updates immediately, and deleting the applied +theme falls back to the package style while keeping the saved GUID semantics. ## Extensibility Data Visualizer exposes several extension points for custom workflows: -**Attributes** let you override display namespace or friendly names on ScriptableObject classes. `[CustomDataVisualization(Namespace = "...", TypeName = "...")]` replaces the namespace group and the display name the window shows. Without it the window files a type under the last segment of its C# namespace. Useful when code organization doesn't match your content taxonomy. +**Attributes** let you override display namespace or friendly names on ScriptableObject classes. `[CustomDataVisualization(Namespace = "...", TypeName = "...")]` replaces the +namespace group and the display name the window shows. Without it the window files a type under the last segment of its C# namespace. Useful when code organization doesn't match +your content taxonomy. **BaseDataObject** provides a ready-made base class for ScriptableObjects with built-in lifecycle support: + - Stores an asset GUID, title, and description for display and stable identity - Implements clone, create, and rename lifecycle interfaces with virtual `BeforeClone`, `AfterClone`, `BeforeCreate`, `AfterCreate`, `BeforeRename`, and `AfterRename` hooks - Supports custom UI Toolkit content through the virtual `BuildGUI` method Derived classes override only what they need—GUID generation, cache resets, companion asset syncing—without duplicating boilerplate. -**Lifecycle Interfaces** hook into asset events before and after clone, create, and rename operations. Enforce invariants like regenerating IDs or pushing audit logs to telemetry without writing per-asset editor scripts. +**Lifecycle Interfaces** hook into asset events before and after clone, create, and rename operations. Enforce invariants like regenerating IDs or pushing audit logs to telemetry +without writing per-asset editor scripts. -**UI Toolkit Extensions** render custom UI alongside the default inspector. Return a `VisualElement` tree—graphs, thumbnails, validation badges, or any UI Toolkit component—from `IGUIProvider.BuildGUI` (or `BaseDataObject.BuildGUI`) and Data Visualizer slots it in below the inspector. The `DataVisualizerGUIContext` argument carries the selected asset's `SerializedObject` for writing changes back. Because the entire window runs on UI Toolkit, this approach scales to complex dashboards without leaving the unified workflow. +**UI Toolkit Extensions** render custom UI alongside the default inspector. Return a `VisualElement` tree—graphs, thumbnails, validation badges, or any UI Toolkit component—from +`IGUIProvider.BuildGUI` (or `BaseDataObject.BuildGUI`) and Data Visualizer slots it in below the inspector. The `DataVisualizerGUIContext` argument carries the selected asset's +`SerializedObject` for writing changes back. Because the entire window runs on UI Toolkit, this approach scales to complex dashboards without leaving the unified workflow. -**Processors** are plain `IDataProcessor` classes that the window discovers with no registration. `Name` labels the button, `Description` is its tooltip, `Accepts` lists the types it applies to, and `Process(Type, IEnumerable)` receives the **ALL** or **FILTERED** object set you chose. The window instantiates processors through their public parameterless constructor, skips any that lack one, and surfaces a thrown exception in a dialog without stopping other processors. +**Processors** are plain `IDataProcessor` classes that the window discovers with no registration. `Name` labels the button, `Description` is its tooltip, `Accepts` lists the types +it applies to, and `Process(Type, IEnumerable)` receives the **ALL** or **FILTERED** object set you chose. The window instantiates processors through their public +parameterless constructor, skips any that lack one, and surfaces a thrown exception in a dialog without stopping other processors. ## Workflow Tips -**Treat namespace ordering as shared navigation** when persisting state in the project asset. A consistent structure reduces friction when different team members jump into the same data. +**Treat namespace ordering as shared navigation** when persisting state in the project asset. A consistent structure reduces friction when different team members jump into the same +data. **Set the Data Folder to your team's content root.** Predictable asset locations keep version control diffs clean and make batch operations safer. @@ -145,10 +191,13 @@ Derived classes override only what they need—GUID generation, cache resets, co The [video walkthrough](https://youtu.be/3oUxUSKNyhw) provides a step-by-step visual tour and explains the rationale behind UI choices. - # Overview -This is currently *ALPHA* software, intended for wallstop studios internal use only. Feel free to use. Currently there is no support and there may be breaking changes. Once stabilized, I will ready this repo for production use. + +This is currently _ALPHA_ software, intended for wallstop studios internal use only. Feel free to use. Currently there is no support and there may be breaking changes. Once +stabilized, I will ready this repo for production use. ## Contributing -This project uses [CSharpier](https://csharpier.com/) with the default configuration to enable an enforced, consistent style. If you would like to contribute, recommendation is to ensure that changed files are ran through CSharpier prior to merge. This can be done automatically through editor plugins, or, minimally, by installing a [pre-commit hook](https://pre-commit.com/#3-install-the-git-hook-scripts). \ No newline at end of file +This project uses [CSharpier](https://csharpier.com/) with the default configuration to enable an enforced, consistent style. If you would like to contribute, recommendation is to +ensure that changed files are ran through CSharpier prior to merge. This can be done automatically through editor plugins, or, minimally, by installing a +[pre-commit hook](https://pre-commit.com/#3-install-the-git-hook-scripts). diff --git a/docs/extending.md b/docs/extending.md index 4780f25..4351424 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -1,8 +1,7 @@ # Extending the window -The runtime assembly is part of the package and carries the extension surface, so -your types compile against it in both the editor and a player build. Everything -on this page is public API. +The runtime assembly is part of the package and carries the extension surface, so your types compile against it in both the editor and a player build. Everything on this page is +public API. ## Display attributes @@ -17,21 +16,16 @@ public sealed class WeaponData : ScriptableObject } ``` -`Namespace` replaces the namespace group the type is filed under. Without it the -window uses the last segment of your C# namespace, so -`MyGame.Items.Weapons` appears under `Weapons`, and a type in no namespace appears -under `No Namespace`. +`Namespace` replaces the namespace group the type is filed under. Without it the window uses the last segment of your C# namespace, so `MyGame.Items.Weapons` appears under +`Weapons`, and a type in no namespace appears under `No Namespace`. -`TypeName` replaces the type's display name in the list, in the type filter, and -in the dialogs that name the type. Without it the C# type name is used. +`TypeName` replaces the type's display name in the list, in the type filter, and in the dialogs that name the type. Without it the C# type name is used. -With the Odin Inspector available, the attribute also carries `UseOdinInspector`, -which defaults to `true`. See [Odin Inspector](#odin-inspector). +With the Odin Inspector available, the attribute also carries `UseOdinInspector`, which defaults to `true`. See [Odin Inspector](#odin-inspector). ## BaseDataObject -`BaseDataObject` is an abstract `ScriptableObject` that carries identity and the -lifecycle hooks. Derive from it instead of `ScriptableObject` to get them. +`BaseDataObject` is an abstract `ScriptableObject` that carries identity and the lifecycle hooks. Derive from it instead of `ScriptableObject` to get them. ```csharp using UnityEngine; @@ -47,30 +41,24 @@ public sealed class ItemData : BaseDataObject It provides: - `Id`, the asset's GUID, kept in sync with the AssetDatabase. -- `Title`, the display name. A blank title falls back to the asset name, so you - only set it when the asset name is not what you want to show. +- `Title`, the display name. A blank title falls back to the asset name, so you only set it when the asset name is not what you want to show. - `Description`. - Ordering by title, then asset name, then Id, then description. -The serialized fields `_assetGuid`, `_title`, and `_description` are shown in the -inspector under a **Base Data** header, read-only. +The serialized fields `_assetGuid`, `_title`, and `_description` are shown in the inspector under a **Base Data** header, read-only. ## Lifecycle hooks -Each hook is a `virtual` method on `BaseDataObject` and also a standalone -interface, so you can implement one without deriving. +Each hook is a `virtual` method on `BaseDataObject` and also a standalone interface, so you can implement one without deriving. -| Interface | Methods | When | -| --- | --- | --- | -| `IDuplicable` | `BeforeClone(ScriptableObject previous)`, `AfterClone(ScriptableObject previous)` | Around [Clone](managing-assets.md#clone) | -| `ICreatable` | `BeforeCreate()`, `AfterCreate()` | Around [Create](managing-assets.md#create) | -| `IRenamable` | `BeforeRename(string newName)`, `AfterRename(string newName)` | Around [Rename](managing-assets.md#rename) | +| Interface | Methods | When | +| ------------- | --------------------------------------------------------------------------------- | ------------------------------------------ | +| `IDuplicable` | `BeforeClone(ScriptableObject previous)`, `AfterClone(ScriptableObject previous)` | Around [Clone](managing-assets.md#clone) | +| `ICreatable` | `BeforeCreate()`, `AfterCreate()` | Around [Create](managing-assets.md#create) | +| `IRenamable` | `BeforeRename(string newName)`, `AfterRename(string newName)` | Around [Rename](managing-assets.md#rename) | -Override only what you need. The default implementations already do the work you -would otherwise repeat: `BeforeClone` clears the inherited asset GUID so the copy -does not share the original's identity, and `AfterClone` refreshes the GUID from -the new asset path and applies the `(Clone)`, `(Clone 1)`, `(Clone 2)`, … suffix -to the title. +Override only what you need. The default implementations already do the work you would otherwise repeat: `BeforeClone` clears the inherited asset GUID so the copy does not share +the original's identity, and `AfterClone` refreshes the GUID from the new asset path and applies the `(Clone)`, `(Clone 1)`, `(Clone 2)`, … suffix to the title. ```csharp public sealed class ItemData : BaseDataObject @@ -89,8 +77,7 @@ public sealed class ItemData : BaseDataObject ## Custom inspector content -Implement `IGUIProvider` and return a `VisualElement` from `BuildGUI`. The window -adds it below the standard inspector. +Implement `IGUIProvider` and return a `VisualElement` from `BuildGUI`. The window adds it below the standard inspector. ```csharp using UnityEngine.UIElements; @@ -105,20 +92,14 @@ public sealed class ItemData : BaseDataObject } ``` -Return `null` to add nothing. `BaseDataObject` implements `IGUIProvider` and -returns `null` by default, so a derived type only overrides the method when it -has something to show. +Return `null` to add nothing. `BaseDataObject` implements `IGUIProvider` and returns `null` by default, so a derived type only overrides the method when it has something to show. -The `DataVisualizerGUIContext` carries the `SerializedObject` for the selected -asset in the editor, which is what you need to write back through. The context -type has no editor-only members in a player build, so a `BuildGUI` override -compiles in both. +The `DataVisualizerGUIContext` carries the `SerializedObject` for the selected asset in the editor, which is what you need to write back through. The context type has no +editor-only members in a player build, so a `BuildGUI` override compiles in both. ## Display name -Implement `IDisplayable` to control the name shown for an object. `BaseDataObject` -implements it through `Title`. For a plain `ScriptableObject`, implement it -yourself: +Implement `IDisplayable` to control the name shown for an object. `BaseDataObject` implements it through `Title`. For a plain `ScriptableObject`, implement it yourself: ```csharp public sealed class ItemData : ScriptableObject, IDisplayable @@ -130,13 +111,11 @@ public sealed class ItemData : ScriptableObject, IDisplayable } ``` -The row label, the search result name, and the drag ghost all read `Title` when -the type provides it. +The row label, the search result name, and the drag ghost all read `Title` when the type provides it. ## Processors -A processor is a plain class implementing `IDataProcessor`. The window discovers -every implementation in your project, so no registration is needed. +A processor is a plain class implementing `IDataProcessor`. The window discovers every implementation in your project, so no registration is needed. ```csharp using System; @@ -168,46 +147,32 @@ public sealed class RecalculateRarity : IDataProcessor - `Name` is the button label and sorts the list. - `Description` is the button tooltip. -- `Accepts` limits the processor to the types you list, and the processor is - only offered for a selected type that appears in that list. Return `null` or an - empty list and the processor is never offered. -- `Process` receives the type and the objects, which is the **ALL** or - **FILTERED** set chosen in the window. See - [Processors](managing-assets.md#processors) for scope, the confirmation, and - the load-in-progress refusal. -- `WillEffect(Type, IEnumerable)` is a default interface method - that returns the number of objects. Override it when a processor will skip some. - -The window instantiates each processor with its public parameterless constructor. -A processor that is abstract, generic, or lacks that constructor is skipped and -logged. Exceptions from `Process` are caught, logged, and surfaced in a dialog; -one failing processor does not stop the others. - -The window saves assets and refreshes after a processor runs, so call -`EditorUtility.SetDirty` on anything you change. +- `Accepts` limits the processor to the types you list, and the processor is only offered for a selected type that appears in that list. Return `null` or an empty list and the + processor is never offered. +- `Process` receives the type and the objects, which is the **ALL** or **FILTERED** set chosen in the window. See [Processors](managing-assets.md#processors) for scope, the + confirmation, and the load-in-progress refusal. +- `WillEffect(Type, IEnumerable)` is a default interface method that returns the number of objects. Override it when a processor will skip some. + +The window instantiates each processor with its public parameterless constructor. A processor that is abstract, generic, or lacks that constructor is skipped and logged. Exceptions +from `Process` are caught, logged, and surfaced in a dialog; one failing processor does not stop the others. + +The window saves assets and refreshes after a processor runs, so call `EditorUtility.SetDirty` on anything you change. ## Odin Inspector -When the Odin Inspector is installed, the window renders assets through Odin -instead of the standard inspector in two cases: the type carries -`[CustomDataVisualization]` with `UseOdinInspector` set, and the type derives -from `SerializedScriptableObject` (which `BaseDataObject` does when Odin is -present). A type that opts out shows the standard inspector. +When the Odin Inspector is installed, the window renders assets through Odin instead of the standard inspector in two cases: the type carries `[CustomDataVisualization]` with +`UseOdinInspector` set, and the type derives from `SerializedScriptableObject` (which `BaseDataObject` does when Odin is present). A type that opts out shows the standard +inspector. -Odin integration is optional. The package has no dependency on Odin and compiles -and runs without it. +Odin integration is optional. The package has no dependency on Odin and compiles and runs without it. ## Removing a type from the catalog -Types deriving from `BaseDataObject` and types carrying -`[CustomDataVisualization]` are managed for you and have no remove button. Other -tracked types can be removed with the **X** on their row or their namespace -header. Removing a type stops Data Visualizer from tracking it; the assets stay on -disk. +Types deriving from `BaseDataObject` and types carrying `[CustomDataVisualization]` are managed for you and have no remove button. Other tracked types can be removed with the **X** +on their row or their namespace header. Removing a type stops Data Visualizer from tracking it; the assets stay on disk. ## Next steps - [Managing assets](managing-assets.md) — the operations your hooks run inside. -- [Search and filtering](search-and-filter.md) — the label filter your processors - can be scoped to. -- [Organizing your data](organizing.md) — themes, layout, and persistence. \ No newline at end of file +- [Search and filtering](search-and-filter.md) — the label filter your processors can be scoped to. +- [Organizing your data](organizing.md) — themes, layout, and persistence. diff --git a/docs/getting-started.md b/docs/getting-started.md index a7accbc..976984a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,15 +1,11 @@ # Getting started -This page takes you from an empty project to an edited asset. It covers the -first session only. The -[README](https://github.com/wallstop/DataVisualizer#readme) documents the full +This page takes you from an empty project to an edited asset. It covers the first session only. The [README](https://github.com/wallstop/DataVisualizer#readme) documents the full feature set, including themes, processors, and extension points. ## Requirements -- Unity 2021.3 or newer. `package.json` declares that floor, and the window is - developed and tested on Unity 6. The 2021.3 editor is not part of the local - validation yet, which +- Unity 2021.3 or newer. `package.json` declares that floor, and the window is developed and tested on Unity 6. The 2021.3 editor is not part of the local validation yet, which [#86](https://github.com/wallstop/DataVisualizer/issues/86) tracks. - No additional package dependencies. @@ -23,112 +19,80 @@ Unity imports the package, and the menu item appears. ## Open the window -Select **Tools → Wallstop Studios → Data Visualizer**, then dock it next to the -Inspector. The window has three panels: +Select **Tools → Wallstop Studios → Data Visualizer**, then dock it next to the Inspector. The window has three panels: -- **Namespaces and types** (left) lists your ScriptableObject types by C# - namespace. Expand a namespace, then select a type to load its instances. -- **Objects** (center) lists every instance of the selected type. One row is - selected at a time. -- **Inspector** (right) shows the full inspector for the selected asset, - including custom editors and Odin Inspector integration. +- **Namespaces and types** (left) lists your ScriptableObject types by C# namespace. Expand a namespace, then select a type to load its instances. +- **Objects** (center) lists every instance of the selected type. One row is selected at a time. +- **Inspector** (right) shows the full inspector for the selected asset, including custom editors and Odin Inspector integration. ## Add your types Three controls above the namespace panel fill the catalog: -- **Search Types** queries the ScriptableObject types Unity knows about. Add - single types or a whole namespace in one action. -- **Scan Asset Folder** crawls a folder and registers the types and existing - assets it finds. Use it to adopt a project that already has data. -- **Scan Scripts Folder** registers types from source folders, which is the - right choice before you create any assets. +- **Search Types** queries the ScriptableObject types Unity knows about. Add single types or a whole namespace in one action. +- **Scan Asset Folder** crawls a folder and registers the types and existing assets it finds. Use it to adopt a project that already has data. +- **Scan Scripts Folder** registers types from source folders, which is the right choice before you create any assets. -Removing a type or namespace only stops Data Visualizer from tracking it. Your -assets stay on disk. +Removing a type or namespace only stops Data Visualizer from tracking it. Your assets stay on disk. ![Manage-visible-types popover listing the project's ScriptableObject types](images/data-visualizer-import.png) -*Type search popover over the namespace panel. Regenerated from the live window -by the docs capture pipeline.* +_Type search popover over the namespace panel. Regenerated from the live window by the docs capture pipeline._ ## Edit your first asset 1. Select a type in the left panel. 2. Select an instance in the middle panel. -3. Edit a field in the right panel. The change saves immediately, as in Unity's - own Inspector. +3. Edit a field in the right panel. The change saves immediately, as in Unity's own Inspector. -Use the buttons above the object list to manage the selection: **Clone** writes -a copy with a `Clone` suffix beside the original, **Rename** renames the asset -on disk, **Move** retargets it to another folder, and **Delete** removes it -after you confirm. +Use the buttons above the object list to manage the selection: **Clone** writes a copy with a `Clone` suffix beside the original, **Rename** renames the asset on disk, **Move** +retargets it to another folder, and **Delete** removes it after you confirm. ## Create an asset -Select **Create** to add a new instance of the active type. The asset lands in -your **Data Folder**, which you set under **Settings**. Clones always stay -beside their original, whatever the Data Folder is. +Select **Create** to add a new instance of the active type. The asset lands in your **Data Folder**, which you set under **Settings**. Clones always stay beside their original, +whatever the Data Folder is. -![Create popover asking for a new asset name](images/data-visualizer-create.png) -*Create popover over the object list. Regenerated from the live window by the -docs capture pipeline.* +![Create popover asking for a new asset name](images/data-visualizer-create.png) _Create popover over the object list. Regenerated from the live window by the docs capture +pipeline._ ## Filter and search -Three finders narrow different things, and they do not affect each other. The -[Search and filtering](search-and-filter.md) page covers each one in full. +Three finders narrow different things, and they do not affect each other. The [Search and filtering](search-and-filter.md) page covers each one in full. -- The **Search** box in the window header row searches every tracked type, not - only the type you selected. It matches asset names, type names, asset GUIDs, - and string fields on the assets themselves, so you can find an asset by what is - inside it. -- The filter field above the namespace list narrows the type rows by display - name, without case sensitivity. Namespace headers stay visible. -- Drag labels from **Available** into the **AND:** or **OR:** rows to show only - assets that carry them. The **AND &&** and **OR ||** switch chooses between - matching every label you dragged and matching any of them. +- The **Search** box in the window header row searches every tracked type, not only the type you selected. It matches asset names, type names, asset GUIDs, and string fields on the + assets themselves, so you can find an asset by what is inside it. +- The filter field above the namespace list narrows the type rows by display name, without case sensitivity. Namespace headers stay visible. +- Drag labels from **Available** into the **AND:** or **OR:** rows to show only assets that carry them. The **AND &&** and **OR ||** switch chooses between matching every label you + dragged and matching any of them. ## Keep your arrangement -Ordering, collapse state, tracked types, and your selection persist between -sessions, and you can reorder namespaces, types, and instances directly in the -window. The [Organizing your data](organizing.md) page covers ordering, -persistence, themes, and the Play Mode pause. +Ordering, collapse state, tracked types, and your selection persist between sessions, and you can reorder namespaces, types, and instances directly in the window. The +[Organizing your data](organizing.md) page covers ordering, persistence, themes, and the Play Mode pause. Under **Settings**, choose where that state lives: -- **Persist State in UserState** (default) stores it in your local user cache, so - each developer keeps a private arrangement and version control stays free of - layout churn. -- The project settings asset stores the state in the repository, which suits a - team that wants one shared arrangement. +- **Persist State in UserState** (default) stores it in your local user cache, so each developer keeps a private arrangement and version control stays free of layout churn. +- The project settings asset stores the state in the repository, which suits a team that wants one shared arrangement. -**Select Active Object** mirrors your Data Visualizer selection in Unity's -Inspector, and **Data Folder** sets where new assets are created. New assets land -in a per-type folder under it; clones stay beside their original. +**Select Active Object** mirrors your Data Visualizer selection in Unity's Inspector, and **Data Folder** sets where new assets are created. New assets land in a per-type folder +under it; clones stay beside their original. ## While the editor is playing -Entering Play Mode pauses package-driven work. The window keeps the last loaded -view and shows *Package editing is paused during Play Mode*. While it is paused: +Entering Play Mode pauses package-driven work. The window keeps the last loaded view and shows _Package editing is paused during Play Mode_. While it is paused: -- **Create**, the add-type controls, and the inspector are disabled, and the - inspector shows read-only content. +- **Create**, the add-type controls, and the inspector are disabled, and the inspector shows read-only content. - Asset operations are refused: clone, rename, move, delete, and label changes. -- Selecting another type does not load its instances, and the catalog does not - change. +- Selecting another type does not load its instances, and the catalog does not change. Everything resumes when you leave Play Mode. ## Next steps -- [Search and filtering](search-and-filter.md) — what global search matches, its - 25-result limit, and the exact AND/OR label rules. -- [Managing assets](managing-assets.md) — where each asset operation lives, where - created files go, how clones are named, and how processors are scoped. -- [Organizing your data](organizing.md) — ordering, themes and their tokens, - persistence, and what pauses during Play Mode. -- [Extending the window](extending.md) — attributes, `BaseDataObject`, lifecycle - hooks, processors, and custom inspector content. +- [Search and filtering](search-and-filter.md) — what global search matches, its 25-result limit, and the exact AND/OR label rules. +- [Managing assets](managing-assets.md) — where each asset operation lives, where created files go, how clones are named, and how processors are scoped. +- [Organizing your data](organizing.md) — ordering, themes and their tokens, persistence, and what pauses during Play Mode. +- [Extending the window](extending.md) — attributes, `BaseDataObject`, lifecycle hooks, processors, and custom inspector content. - The [video walkthrough](https://youtu.be/3oUxUSKNyhw) gives a visual tour. diff --git a/docs/index.md b/docs/index.md index 72536b3..5d466ab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,33 +1,28 @@ # Data Visualizer -Data Visualizer is a Unity editor window for ScriptableObject-heavy projects. -It gathers your data in one place: a catalog of your ScriptableObject types -organized by namespace, a list of every instance of the selected type, and the -inspector for the asset you picked. You edit, reorder, and batch-process data -without moving between the Project panel and the Inspector. +Data Visualizer is a Unity editor window for ScriptableObject-heavy projects. It gathers your data in one place: a catalog of your ScriptableObject types organized by namespace, a +list of every instance of the selected type, and the inspector for the asset you picked. You edit, reorder, and batch-process data without moving between the Project panel and the +Inspector. -Data Visualizer is free forever. There are no subscriptions, no paid upgrades, -and no feature-gated tiers. The full source is MIT-licensed, and every -capability described here ships in the free package. +Data Visualizer is free forever. There are no subscriptions, no paid upgrades, and no feature-gated tiers. The full source is MIT-licensed, and every capability described here +ships in the free package. ![Data Visualizer layout with namespace, object, and inspector columns](images/data-visualizer-layout.png) -*Full window overview. Regenerated from the live window by the docs capture -pipeline.* +_Full window overview. Regenerated from the live window by the docs capture pipeline._ ## What it does -| Task | What the window gives you | -| --- | --- | -| Find | Search your ScriptableObject types by name, add a whole namespace at once, or scan a folder for assets you already have. | -| Inspect | Edit the selected asset in the inspector you already know, including custom editors and Odin Inspector integration. | -| Organize | Reorder namespaces, types, and instances, filter by label, and keep the arrangement across sessions. | -| Edit | Clone, rename, move, delete, and create assets in place, then run processors over one type or all of its instances. | +| Task | What the window gives you | +| -------- | ------------------------------------------------------------------------------------------------------------------------ | +| Find | Search your ScriptableObject types by name, add a whole namespace at once, or scan a folder for assets you already have. | +| Inspect | Edit the selected asset in the inspector you already know, including custom editors and Odin Inspector integration. | +| Organize | Reorder namespaces, types, and instances, filter by label, and keep the arrangement across sessions. | +| Edit | Clone, rename, move, delete, and create assets in place, then run processors over one type or all of its instances. | ## Requirements -- Unity 2021.3 or newer, the floor declared in `package.json`. The window is - developed and tested on Unity 6. +- Unity 2021.3 or newer, the floor declared in `package.json`. The window is developed and tested on Unity 6. - No additional package dependencies. ## Install @@ -36,8 +31,7 @@ pipeline.* 2. Select **+**, then **Add package from git URL**. 3. Enter `https://github.com/wallstop/DataVisualizer.git` and select **Add**. -Open the window with **Tools → Wallstop Studios → Data Visualizer**. -[Getting started](getting-started.md) walks through the first session. +Open the window with **Tools → Wallstop Studios → Data Visualizer**. [Getting started](getting-started.md) walks through the first session. !!! note diff --git a/docs/managing-assets.md b/docs/managing-assets.md index 00cbb15..d3363a8 100644 --- a/docs/managing-assets.md +++ b/docs/managing-assets.md @@ -1,115 +1,85 @@ # Managing assets -This page covers the operations that change assets on disk, and the processors -that change many at once. Everything here is refused while the window is paused -for Play Mode; see [While the editor is playing](organizing.md#while-the-editor-is-playing). +This page covers the operations that change assets on disk, and the processors that change many at once. Everything here is refused while the window is paused for Play Mode; see +[While the editor is playing](organizing.md#while-the-editor-is-playing). ## Where an action lives -**Create** is the **+** button in the object list header; it always acts on the -selected type. +**Create** is the **+** button in the object list header; it always acts on the selected type. -**Clone**, **Rename**, **Move**, and **Delete** are the four buttons on the right -of each object row, and they act on that row's object without selecting it first. +**Clone**, **Rename**, **Move**, and **Delete** are the four buttons on the right of each object row, and they act on that row's object without selecting it first. -![Clone, rename, move, and delete controls on the object rows](images/data-visualizer-instance-actions.png) -*Per-row asset management controls. Regenerated from the live window by the docs -capture pipeline.* +![Clone, rename, move, and delete controls on the object rows](images/data-visualizer-instance-actions.png) _Per-row asset management controls. Regenerated from the live window by +the docs capture pipeline._ ## Create -**Create** asks for a name, prefilled with the type's display name, and refuses a -blank name or one containing a character that is invalid in a file name. +**Create** asks for a name, prefilled with the type's display name, and refuses a blank name or one containing a character that is invalid in a file name. -The asset is written to a per-type folder under your **Data Folder**, using the -type's full name with each `.` replaced by a directory separator. For -`MyGame.Items.WeaponData` with a Data Folder of `Assets/Data`, the asset lands in -`Assets/Data/MyGame/Items/WeaponData/`. Those folders are created for you. +The asset is written to a per-type folder under your **Data Folder**, using the type's full name with each `.` replaced by a directory separator. For `MyGame.Items.WeaponData` with +a Data Folder of `Assets/Data`, the asset lands in `Assets/Data/MyGame/Items/WeaponData/`. Those folders are created for you. -If the name is already taken, nothing is written. When the existing asset is the -same type it is selected and shown, and the dialog says so. +If the name is already taken, nothing is written. When the existing asset is the same type it is selected and shown, and the dialog says so. ## Clone -**Clone** copies the selected asset into the same folder as the original, and -inserts the copy directly after the original in the list, then selects it. +**Clone** copies the selected asset into the same folder as the original, and inserts the copy directly after the original in the list, then selects it. -Naming strips any existing `(Clone)` or `(Clone n)` from the source file name -first, so repeated clones do not accumulate suffixes. The first clone is -`(Clone)`, then `(Clone 1)`, `(Clone 2)`, and so on. Unity's own unique-path -check breaks any remaining collision. +Naming strips any existing `(Clone)` or `(Clone n)` from the source file name first, so repeated clones do not accumulate suffixes. The first clone is `(Clone)`, then `(Clone 1)`, +`(Clone 2)`, and so on. Unity's own unique-path check breaks any remaining collision. -Clones always stay beside their original. Your **Data Folder** does not affect -them. +Clones always stay beside their original. Your **Data Folder** does not affect them. ## Rename -**Rename** asks for a new name without the extension and validates it before -touching the asset. The dialog reports `Invalid name.` for a blank name or an -invalid character, `Name is unchanged.` when nothing differs, and Unity's own -`Invalid: …` message when the move would be rejected, for example onto an -existing file. +**Rename** asks for a new name without the extension and validates it before touching the asset. The dialog reports `Invalid name.` for a blank name or an invalid character, +`Name is unchanged.` when nothing differs, and Unity's own `Invalid: …` message when the move would be rejected, for example onto an existing file. The asset keeps its folder; only the file name changes. ## Move -**Move** opens a folder picker starting in the asset's current folder. The target -must be inside the project's `Assets` directory; anything else is refused with an -**Invalid Folder** dialog. Moving an asset to the folder it is already in does -nothing. +**Move** opens a folder picker starting in the asset's current folder. The target must be inside the project's `Assets` directory; anything else is refused with an **Invalid +Folder** dialog. Moving an asset to the folder it is already in does nothing. The asset keeps its file name. ## Delete -**Delete** asks for confirmation, naming the asset. Confirming removes the `.asset` -file. Cancel does nothing. +**Delete** asks for confirmation, naming the asset. Confirming removes the `.asset` file. Cancel does nothing. ## Processors -The **Processors** area sits below the namespace panel. It lists every -`IDataProcessor` implementation in your project whose `Accepts` list covers the -selected type, sorted by name. A processor with no `Accepts` list applies to every -type. The collapsed header shows how many processors are listed; the area is -hidden entirely when none apply. +The **Processors** area sits below the namespace panel. It lists every `IDataProcessor` implementation in your project whose `Accepts` list covers the selected type, sorted by +name. A processor with no `Accepts` list applies to every type. The collapsed header shows how many processors are listed; the area is hidden entirely when none apply. -Clicking a processor opens a confirmation that names the processor and the number -of objects it will receive. Confirming runs `Process`, saves assets, refreshes the -AssetDatabase, and schedules a window refresh. A processor that throws is caught: -the exception is logged and an **Error running processor '…'** dialog reports the -message. Other processors are unaffected. +Clicking a processor opens a confirmation that names the processor and the number of objects it will receive. Confirming runs `Process`, saves assets, refreshes the AssetDatabase, +and schedules a window refresh. A processor that throws is caught: the exception is logged and an **Error running processor '…'** dialog reports the message. Other processors are +unaffected. ### ALL and FILTERED The **ALL** / **FILTERED** switch above the processor list sets the scope. - **ALL** passes every loaded instance of the selected type. -- **FILTERED** passes only the instances the [label filter](search-and-filter.md#label-filter) - currently shows. With no label filter, that is the same set as **ALL**. +- **FILTERED** passes only the instances the [label filter](search-and-filter.md#label-filter) currently shows. With no label filter, that is the same set as **ALL**. **FILTERED** is the default. ### Refusal while loading -Instances stream in asynchronously. Running a processor while the selected type -is still loading would touch only the subset that has arrived while the dialog -implied the whole type, so the run is refused with a **Loading In Progress** -dialog. Wait for the load to finish, then run it. +Instances stream in asynchronously. Running a processor while the selected type is still loading would touch only the subset that has arrived while the dialog implied the whole +type, so the run is refused with a **Loading In Progress** dialog. Wait for the load to finish, then run it. -Processors are also refused during the Play Mode pause, and the switch is -disabled while the window is paused. +Processors are also refused during the Play Mode pause, and the switch is disabled while the window is paused. ## Undo -These operations are not on Unity's undo stack. Clone, rename, move, and delete -change files directly. Inspector edits go through Unity's own serialized-object -path and do support undo. +These operations are not on Unity's undo stack. Clone, rename, move, and delete change files directly. Inspector edits go through Unity's own serialized-object path and do support +undo. ## Next steps -- [Search and filtering](search-and-filter.md) — find objects and narrow them by - label. -- [Organizing your data](organizing.md) — ordering, layout, persistence, and the - Play Mode pause. -- [Extending the window](extending.md) — write your own processors. \ No newline at end of file +- [Search and filtering](search-and-filter.md) — find objects and narrow them by label. +- [Organizing your data](organizing.md) — ordering, layout, persistence, and the Play Mode pause. +- [Extending the window](extending.md) — write your own processors. diff --git a/docs/organizing.md b/docs/organizing.md index 686a5d9..e6f5eb7 100644 --- a/docs/organizing.md +++ b/docs/organizing.md @@ -1,8 +1,6 @@ # Organizing your data -This page covers the arrangement of the window: what you can reorder, what -persists, where it is stored, and what the window does while you are in Play -Mode. +This page covers the arrangement of the window: what you can reorder, what persists, where it is stored, and what the window does while you are in Play Mode. ## Reordering @@ -10,120 +8,86 @@ Three lists can be reordered, and all three persist. ### Namespaces -Each namespace header has **↑** and **↓**. **↑** moves the namespace to the top -of the list, **↓** to the bottom. The buttons disable at the ends, so the control -tells you when there is nowhere to go. +Each namespace header has **↑** and **↓**. **↑** moves the namespace to the top of the list, **↓** to the bottom. The buttons disable at the ends, so the control tells you when +there is nowhere to go. -You can also drag a namespace header to any position. The dragged row follows the -pointer and shows a ghost of its label. +You can also drag a namespace header to any position. The dragged row follows the pointer and shows a ghost of its label. ### Types -Each type row has **↑** and **↓** with the same top-and-bottom behavior, scoped -to its own namespace. Types can also be dragged, within and across namespaces. +Each type row has **↑** and **↓** with the same top-and-bottom behavior, scoped to its own namespace. Types can also be dragged, within and across namespaces. ### Objects -Each object row has **↑** and **↓**, which move that object to the top or bottom -of the list for its type. **↓** is disabled on the last row. +Each object row has **↑** and **↓**, which move that object to the top or bottom of the list for its type. **↓** is disabled on the last row. -Objects can also be dragged directly. The list reorders as you drag, and the new -order is saved. +Objects can also be dragged directly. The list reorders as you drag, and the new order is saved. -The **X** on a namespace header or a type row removes it from Data Visualizer. -This is not destructive: your assets stay on disk. Removal is offered only for -types that are neither `BaseDataObject` subclasses nor types carrying -`[CustomDataVisualization]`, because the window manages those types for you. +The **X** on a namespace header or a type row removes it from Data Visualizer. This is not destructive: your assets stay on disk. Removal is offered only for types that are neither +`BaseDataObject` subclasses nor types carrying `[CustomDataVisualization]`, because the window manages those types for you. ## Collapsing namespaces -The arrow on a namespace header collapses and expands its types. Collapse state -persists per namespace. +The arrow on a namespace header collapses and expands its types. Collapse state persists per namespace. ## Pane widths -The two splitters between the three panels save their widths, so the -arrangement survives a reopen. Widths are saved per user through Unity's editor -preferences, independently of the persistence setting below. A width is -normalized so it cannot go below the pane's minimum, and the window remembers -your preferred size separately from a size the window had to clamp to, so -shrinking the editor and growing it back does not overwrite your choice. +The two splitters between the three panels save their widths, so the arrangement survives a reopen. Widths are saved per user through Unity's editor preferences, independently of +the persistence setting below. A width is normalized so it cannot go below the pane's minimum, and the window remembers your preferred size separately from a size the window had to +clamp to, so shrinking the editor and growing it back does not overwrite your choice. ## Persistence -One setting decides where the window's state lives: which namespace and type you -had selected, which object was selected in each type, the namespace, type, and -object ordering, collapse state, the types you track, the per-type label filters, -and the per-type processor scope and collapse state. +One setting decides where the window's state lives: which namespace and type you had selected, which object was selected in each type, the namespace, type, and object ordering, +collapse state, the types you track, the per-type label filters, and the per-type processor scope and collapse state. ### Persist in user state -**Persist State in UserState** is the default. State is written as JSON to -`DataVisualizerUserState.json` in Unity's per-user persistent data path. Each -developer keeps a private arrangement and version control stays free of layout -churn. +**Persist State in UserState** is the default. State is written as JSON to `DataVisualizerUserState.json` in Unity's per-user persistent data path. Each developer keeps a private +arrangement and version control stays free of layout churn. ### Persist in a project settings asset -Turning the setting off stores the same state inside a `DataVisualizerSettings` -asset in the project. Data Visualizer creates one at -`Assets/Editor/DataVisualizerSettings.asset` on first use if no -`DataVisualizerSettings` asset exists, and uses the first one it finds if you -have several. This suits a team that wants one shared arrangement, at the cost of +Turning the setting off stores the same state inside a `DataVisualizerSettings` asset in the project. Data Visualizer creates one at `Assets/Editor/DataVisualizerSettings.asset` on +first use if no `DataVisualizerSettings` asset exists, and uses the first one it finds if you have several. This suits a team that wants one shared arrangement, at the cost of layout churn in version control. -Switching between the two copies the current state across, so you do not lose -your arrangement when you toggle. +Switching between the two copies the current state across, so you do not lose your arrangement when you toggle. ### What is never shared -The Data Folder, the `selectActiveObject` preference, and the pane widths stay -with the `DataVisualizerSettings` asset regardless of where the window state -lives. +The Data Folder, the `selectActiveObject` preference, and the pane widths stay with the `DataVisualizerSettings` asset regardless of where the window state lives. ## Other settings The **Settings** popover opens from the **…** button in the header row. -![Settings popover with persistence toggles and data folder field](images/data-visualizer-settings.png) -*Settings popover. Regenerated from the live window by the docs capture -pipeline.* +![Settings popover with persistence toggles and data folder field](images/data-visualizer-settings.png) _Settings popover. Regenerated from the live window by the docs capture +pipeline._ -- **Select Active Object** mirrors your Data Visualizer selection in Unity's own - Inspector, for cross-referencing assets in other editor windows. +- **Select Active Object** mirrors your Data Visualizer selection in Unity's own Inspector, for cross-referencing assets in other editor windows. - **Theme** picks the appearance. See [Themes](#themes). -- **Data Folder** is where new assets are created. Click the path to ping the - folder, or **Select** to browse. The target must be inside `Assets`. Clones are - unaffected: they stay beside their original. +- **Data Folder** is where new assets are created. Click the path to ping the folder, or **Select** to browse. The target must be inside `Assets`. Clones are unaffected: they stay + beside their original. ## Themes -Five themes ship in the package: **Classic**, **Nord**, **Dracula**, **Compact**, -and **Minimal**. Compact and Minimal keep the Classic palette and shrink the -density: Compact uses 13px type, 20px action buttons, and 4px control corners; -Minimal uses 12px type, 16px action buttons, and square corners. +Five themes ship in the package: **Classic**, **Nord**, **Dracula**, **Compact**, and **Minimal**. Compact and Minimal keep the Classic palette and shrink the density: Compact uses +13px type, 20px action buttons, and 4px control corners; Minimal uses 12px type, 16px action buttons, and square corners. -Click the theme field to open a searchable dropdown. Search by name or asset -path, use Up and Down to move, Enter to select, Escape to cancel. Themes from -both `Assets` and `Packages` are listed, and a duplicate name shows its path so -you can tell them apart. +Click the theme field to open a searchable dropdown. Search by name or asset path, use Up and Down to move, Enter to select, Escape to cancel. Themes from both `Assets` and +`Packages` are listed, and a duplicate name shows its path so you can tell them apart. -**Reset Theme** restores the Classic appearance and clears the saved theme -selection, keeping the compact Data Folder button sizing. Selecting the Classic -asset gives the same appearance but keeps an explicit selection. +**Reset Theme** restores the Classic appearance and clears the saved theme selection, keeping the compact Data Folder button sizing. Selecting the Classic asset gives the same +appearance but keeps an explicit selection. ### Your own theme -Create one with **Assets → Create → Wallstop Studios → DataVisualizer → Data -Visualizer Theme**, assign a `.uss` asset to its **Style Sheet** field, then pick -it in **Settings → Theme**. Keep the theme and its stylesheet under an `Editor` -folder; they are editor-only assets. +Create one with **Assets → Create → Wallstop Studios → DataVisualizer → Data Visualizer Theme**, assign a `.uss` asset to its **Style Sheet** field, then pick it in **Settings → +Theme**. Keep the theme and its stylesheet under an `Editor` folder; they are editor-only assets. -The stylesheet you assign is applied after the package stylesheet, and standard -control rules are scoped to `.dataviz-root` inside the Data Visualizer window. -Ordinary USS precedence still applies, and inline styles win. Data color swatches -and label colors stay data-driven, and IMGUI or third-party inspector styling is -not replaced. +The stylesheet you assign is applied after the package stylesheet, and standard control rules are scoped to `.dataviz-root` inside the Data Visualizer window. Ordinary USS +precedence still applies, and inline styles win. Data color swatches and label colors stay data-driven, and IMGUI or third-party inspector styling is not replaced. Override the tokens you need rather than restyling controls: @@ -139,37 +103,26 @@ Override the tokens you need rather than restyling controls: } ``` -Use `Nord.uss` or `Dracula.uss` as palette references. Copy them under your -project's `Editor` folder before customizing rather than editing installed -package files. +Use `Nord.uss` or `Dracula.uss` as palette references. Copy them under your project's `Editor` folder before customizing rather than editing installed package files. -Theme changes apply to the open window without reselecting or reopening it. -Editing the applied theme's stylesheet, changing its Style Sheet reference, and -renaming or moving either asset all update immediately. Deleting the applied -theme falls back to the package style without discarding the saved GUID; reset -clears that reference too. +Theme changes apply to the open window without reselecting or reopening it. Editing the applied theme's stylesheet, changing its Style Sheet reference, and renaming or moving +either asset all update immediately. Deleting the applied theme falls back to the package style without discarding the saved GUID; reset clears that reference too. -The selected theme follows the persistence setting above, so a theme chosen in -user state is yours alone and one chosen in the settings asset is shared. +The selected theme follows the persistence setting above, so a theme chosen in user state is yours alone and one chosen in the settings asset is shared. ## While the editor is playing -Entering Play Mode suspends package-driven work. The window keeps the last -loaded view and shows **Package editing is paused during Play Mode**. While it is -paused: +Entering Play Mode suspends package-driven work. The window keeps the last loaded view and shows **Package editing is paused during Play Mode**. While it is paused: - **Create**, the add-type controls, and the processor switch are disabled. - The inspector is disabled and shows read-only content. - Asset operations are refused: clone, rename, move, delete, and label changes. -- Selecting another type does not load its instances, and the catalog does not - change. +- Selecting another type does not load its instances, and the catalog does not change. Everything resumes when you leave Play Mode. ## Next steps - [Search and filtering](search-and-filter.md) — find and narrow your data. -- [Managing assets](managing-assets.md) — create, rename, move, clone, delete, - and batch-process. -- [Extending the window](extending.md) — attributes, lifecycle hooks, custom - inspector content. \ No newline at end of file +- [Managing assets](managing-assets.md) — create, rename, move, clone, delete, and batch-process. +- [Extending the window](extending.md) — attributes, lifecycle hooks, custom inspector content. diff --git a/docs/search-and-filter.md b/docs/search-and-filter.md index 486a0fc..fc6583a 100644 --- a/docs/search-and-filter.md +++ b/docs/search-and-filter.md @@ -1,136 +1,99 @@ # Search and filtering -Data Visualizer has three separate finders. Each one narrows a different thing, -and they do not affect each other. +Data Visualizer has three separate finders. Each one narrows a different thing, and they do not affect each other. -| Finder | Where it is | What it narrows | -| --- | --- | --- | +| Finder | Where it is | What it narrows | +| ------------- | ------------------------------------------- | ----------------------------- | | Global search | Header row, next to the **Settings** button | Objects of every tracked type | -| Type filter | Above the namespace list | Type rows in the left panel | -| Label filter | Below the object list header | Objects of the selected type | +| Type filter | Above the namespace list | Type rows in the left panel | +| Label filter | Below the object list header | Objects of the selected type | ## Global search -The search box sits in the window header row, above all three panels. It is not -part of the Objects panel, and it does not search only the type you have -selected. +The search box sits in the window header row, above all three panels. It is not part of the Objects panel, and it does not search only the type you have selected. -Type a term and a popover opens with the matches. Each result shows the object's -name and its type name. When the term matched something other than the name, -type name, or GUID, the result also shows up to two `field: value` lines naming -the fields that matched, so you can tell why an asset is in the list. +Type a term and a popover opens with the matches. Each result shows the object's name and its type name. When the term matched something other than the name, type name, or GUID, +the result also shows up to two `field: value` lines naming the fields that matched, so you can tell why an asset is in the list. ### What it matches -Each space-separated term is matched on its own, and an object matches when any -one of its terms matches somewhere. A term is checked against, in order: +Each space-separated term is matched on its own, and an object matches when any one of its terms matches somewhere. A term is checked against, in order: 1. the asset name, 2. the type name, 3. the asset GUID, as an exact match, 4. string fields on the asset and on nested plain objects. -Field matching is checked only when a term has not already matched the name, type -name, or GUID, and it stops at the first field that matches, so a result names -one matching field per term rather than every match. +Field matching is checked only when a term has not already matched the name, type name, or GUID, and it stops at the first field that matches, so a result names one matching field +per term rather than every match. -Field matching is case-insensitive. It skips primitives, `Vector2`, `Vector3`, -`Vector4`, `Quaternion`, `Color`, `Rect`, and `Bounds`, and it does not follow -references to other Unity objects or assets. Cycles in nested plain objects are -tracked, so a self-referencing structure terminates. +Field matching is case-insensitive. It skips primitives, `Vector2`, `Vector3`, `Vector4`, `Quaternion`, `Color`, `Rect`, and `Bounds`, and it does not follow references to other +Unity objects or assets. Cycles in nested plain objects are tracked, so a self-referencing structure terminates. -Matching terms are highlighted in the results. The highlight deepens while the -pointer is over the row. +Matching terms are highlighted in the results. The highlight deepens while the pointer is over the row. ### Limits and behavior -- The popover lists at most 25 results. The list stops there; there is no - "show more". +- The popover lists at most 25 results. The list stops there; there is no "show more". - Results are ordered by asset name, then by full type name, both ordinal. -- Searching covers every tracked type, so a hit can be in a type you have not - selected yet. Selecting a result switches to that object's type when needed and - selects it there. -- The first search after the window opens can show **Building search index…**. - The index loads in the background; when it finishes, the current query is - re-run and the results appear. The popover stays open instead of dismissing - your query. +- Searching covers every tracked type, so a hit can be in a type you have not selected yet. Selecting a result switches to that object's type when needed and selects it there. +- The first search after the window opens can show **Building search index…**. The index loads in the background; when it finishes, the current query is re-run and the results + appear. The popover stays open instead of dismissing your query. - A query with no matches shows **No matching objects found.** -- Up and Down move the highlight and wrap around. Enter or Return opens the - highlighted result. Escape closes the popover and keeps your text. +- Up and Down move the highlight and wrap around. Enter or Return opens the highlighted result. Escape closes the popover and keeps your text. ## Type filter -The field above the namespace list narrows the type rows by display name, -without case sensitivity. Namespace headers are not filtered, so you keep the -structure while the types inside it narrow. +The field above the namespace list narrows the type rows by display name, without case sensitivity. Namespace headers are not filtered, so you keep the structure while the types +inside it narrow. -A display name comes from `[CustomDataVisualization]` when your type sets -`TypeName`; otherwise it is the C# type name. Every space-separated term must -match the display name. +A display name comes from `[CustomDataVisualization]` when your type sets `TypeName`; otherwise it is the C# type name. Every space-separated term must match the display name. ## Label filter -Labels are Unity asset labels, the same ones you see on an asset in the Project -panel. The filter has three rows: +Labels are Unity asset labels, the same ones you see on an asset in the Project panel. The filter has three rows: - **Available** lists every label used by an instance of the selected type. - **AND:** and **OR:** are drop targets. -Drag a label pill from **Available** into **AND:** or **OR:** to filter, and drag -it back out to remove it. Drag labels between the two rows to change which clause -they belong to. +Drag a label pill from **Available** into **AND:** or **OR:** to filter, and drag it back out to remove it. Drag labels between the two rows to change which clause they belong to. ### AND and OR -The **AND &&** / **OR ||** switch above the OR row picks how the two clauses -combine. +The **AND &&** / **OR ||** switch above the OR row picks how the two clauses combine. -In **AND** mode an object must satisfy every clause that has labels. An empty -clause adds no constraint. +In **AND** mode an object must satisfy every clause that has labels. An empty clause adds no constraint. -In **OR** mode an object must satisfy at least one clause **that has labels**. An -empty clause never counts as a match. This matters: if you only fill the AND row -and switch to OR, nothing matches, because the empty OR clause is not a pass. +In **OR** mode an object must satisfy at least one clause **that has labels**. An empty clause never counts as a match. This matters: if you only fill the AND row and switch to OR, +nothing matches, because the empty OR clause is not a pass. Labels compare exactly, so `Urgent` and `urgent` are two different labels. ### The Advanced row -**Advanced** reveals the AND/OR switch and the OR row. The row refuses to -collapse while you are in OR mode or while any OR label is present, so a filter -you cannot see is never left behind hiding objects. The **Labels** header above -the whole section refuses to collapse while any label is configured. +**Advanced** reveals the AND/OR switch and the OR row. The row refuses to collapse while you are in OR mode or while any OR label is present, so a filter you cannot see is never +left behind hiding objects. The **Labels** header above the whole section refuses to collapse while any label is configured. ### What filtering changes -The filter narrows the list you see. It does not change what is on disk, and it -does not change the order of the objects it keeps. +The filter narrows the list you see. It does not change what is on disk, and it does not change the order of the objects it keeps. -When a filter hides rows, a line below the filter reports how many: fewer than 20 -hidden is highlighted in yellow, 20 or more in red. When nothing is hidden, the -line disappears. +When a filter hides rows, a line below the filter reports how many: fewer than 20 hidden is highlighted in yellow, 20 or more in red. When nothing is hidden, the line disappears. -The filter is stored per type and follows the persistence setting described in -[Organizing your data](organizing.md). +The filter is stored per type and follows the persistence setting described in [Organizing your data](organizing.md). ## Label editing -The inspector panel for a selected asset has an **Asset Labels** section. Type a -label and press **Add**, or press Enter, to attach it. The field suggests labels -already used in the project, and you can move through the suggestions with the -arrow keys. Each attached label has a remove button. +The inspector panel for a selected asset has an **Asset Labels** section. Type a label and press **Add**, or press Enter, to attach it. The field suggests labels already used in +the project, and you can move through the suggestions with the arrow keys. Each attached label has a remove button. -Adding a label the asset already has is a no-op. Adding or removing a label -saves the asset, refreshes the label cache, and re-applies the current filter, so -you see the effect immediately. +Adding a label the asset already has is a no-op. Adding or removing a label saves the asset, refreshes the label cache, and re-applies the current filter, so you see the effect +immediately. -Label editing is package-driven asset editing, so it is refused during the Play -Mode pause described in [Managing assets](managing-assets.md). +Label editing is package-driven asset editing, so it is refused during the Play Mode pause described in [Managing assets](managing-assets.md). ## Next steps -- [Managing assets](managing-assets.md) — create, rename, move, clone, delete, - and run processors. +- [Managing assets](managing-assets.md) — create, rename, move, clone, delete, and run processors. - [Organizing your data](organizing.md) — ordering, layout, and persistence. -- [Extending the window](extending.md) — attributes, `BaseDataObject`, - processors, and custom inspector content. \ No newline at end of file +- [Extending the window](extending.md) — attributes, `BaseDataObject`, processors, and custom inspector content. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 5cab30a..a5a0c53 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,110 +1,74 @@ # Troubleshooting -This page lists behaviors that can look like a bug but are the window working -as designed, and what to do when something genuinely is wrong. Each section -links to the page that documents the feature in full. +This page lists behaviors that can look like a bug but are the window working as designed, and what to do when something genuinely is wrong. Each section links to the page that +documents the feature in full. ## The object list fills in gradually -Selecting a type loads its instances asynchronously, so the list can still be -filling while you work. The header row shows an italic `Loading... (n/total)` -counter until the load finishes. - -- Assets whose script is missing, or whose type does not exactly match the - selected type, are skipped and do not count toward the total, so the counter - reaches its end instead of stalling. -- Running a processor while the selected type is still loading is refused with - a **Loading In Progress** dialog, because it would touch only the subset that - has arrived. Wait for the counter to disappear. See - [Refusal while loading](managing-assets.md#refusal-while-loading). -- A list that never finishes loading usually means the console holds script - compile errors. Fix those first; the window reads only assets Unity can - resolve. +Selecting a type loads its instances asynchronously, so the list can still be filling while you work. The header row shows an italic `Loading... (n/total)` counter until the load +finishes. + +- Assets whose script is missing, or whose type does not exactly match the selected type, are skipped and do not count toward the total, so the counter reaches its end instead of + stalling. +- Running a processor while the selected type is still loading is refused with a **Loading In Progress** dialog, because it would touch only the subset that has arrived. Wait for + the counter to disappear. See [Refusal while loading](managing-assets.md#refusal-while-loading). +- A list that never finishes loading usually means the console holds script compile errors. Fix those first; the window reads only assets Unity can resolve. ## Search says "Building search index…" -The first search after the window opens may show **Building search index…** -while the background index loads. The popover stays open with your query, and -the search re-runs automatically when the index is ready — no action needed. -See [Global search](search-and-filter.md#global-search). +The first search after the window opens may show **Building search index…** while the background index loads. The popover stays open with your query, and the search re-runs +automatically when the index is ready — no action needed. See [Global search](search-and-filter.md#global-search). ## The window does not update after an asset change -While Unity reports the AssetDatabase busy — a script compile or a running -import — the window defers work that would force asset type resolution. The -deferred work happens once the database idles; nothing is lost. +While Unity reports the AssetDatabase busy — a script compile or a running import — the window defers work that would force asset type resolution. The deferred work happens once +the database idles; nothing is lost. -The window refreshes itself when a tracked `.asset` file is imported, moved, -or deleted, and a theme change to the applied theme applies immediately. If a -view looks stale after such a change, check the console for errors from other -packages first: an exception in another import handler can stop Unity's -broadcast chain. +The window refreshes itself when a tracked `.asset` file is imported, moved, or deleted, and a theme change to the applied theme applies immediately. If a view looks stale after +such a change, check the console for errors from other packages first: an exception in another import handler can stop Unity's broadcast chain. -While the editor is playing, package-driven work pauses entirely and refresh -requests are remembered until you leave Play Mode. See +While the editor is playing, package-driven work pauses entirely and refresh requests are remembered until you leave Play Mode. See [While the editor is playing](organizing.md#while-the-editor-is-playing). ## An asset is missing from the list -- The object list shows only assets whose exact type matches the selected row. - An asset of a subclass appears when that subclass is selected, not under the - base type. -- An asset whose script no longer compiles or whose script GUID is broken - resolves to no type at all and is skipped. Unity flags such assets as - *Missing (Mono Script)* in the Inspector; fixing the script restores the - asset in the list. -- A type whose assembly failed to compile disappears from the catalog until - the code compiles again. Tracked types and your ordering survive; nothing on - disk changes. -- Removing a type from the catalog never deletes assets. See - [Removing a type from the catalog](extending.md#removing-a-type-from-the-catalog). +- The object list shows only assets whose exact type matches the selected row. An asset of a subclass appears when that subclass is selected, not under the base type. +- An asset whose script no longer compiles or whose script GUID is broken resolves to no type at all and is skipped. Unity flags such assets as _Missing (Mono Script)_ in the + Inspector; fixing the script restores the asset in the list. +- A type whose assembly failed to compile disappears from the catalog until the code compiles again. Tracked types and your ordering survive; nothing on disk changes. +- Removing a type from the catalog never deletes assets. See [Removing a type from the catalog](extending.md#removing-a-type-from-the-catalog). ## The selection resets itself -The saved selection for a type is verified against the AssetDatabase. Right -after a recompile or import, verification would be unreliable and could log -Unity's missing-script warning for every unresolved asset, so the window waits -until the database idles and then keeps selections that are still valid and -clears only genuinely invalid ones — typically an asset whose script is gone. -If your selection drops after a recompile, the underlying asset usually failed -to resolve; check the console. +The saved selection for a type is verified against the AssetDatabase. Right after a recompile or import, verification would be unreliable and could log Unity's missing-script +warning for every unresolved asset, so the window waits until the database idles and then keeps selections that are still valid and clears only genuinely invalid ones — typically +an asset whose script is gone. If your selection drops after a recompile, the underlying asset usually failed to resolve; check the console. ## The window will not shrink below a certain size -The window enforces a minimum of 860x480 pixels, and each pane enforces its own -minimum width: 320 px for namespaces, 220 px for the object list, and 260 px -for the inspector. Layout below a pane minimum is refused rather than persisted, -so shrinking and growing the window back does not corrupt your saved widths. +The window enforces a minimum of 860x480 pixels, and each pane enforces its own minimum width: 320 px for namespaces, 220 px for the object list, and 260 px for the inspector. +Layout below a pane minimum is refused rather than persisted, so shrinking and growing the window back does not corrupt your saved widths. -When the operating system cannot honor the minimum — a small monitor or a tight -side-docked placement — the window clamps to the available space and treats -that as temporary: your preferred size is kept separately and restored when the -space returns. See [Pane widths](organizing.md#pane-widths). +When the operating system cannot honor the minimum — a small monitor or a tight side-docked placement — the window clamps to the available space and treats that as temporary: your +preferred size is kept separately and restored when the space returns. See [Pane widths](organizing.md#pane-widths). ## The arrangement did not load State lives in one of two places, chosen by **Persist State in UserState**: -- User state: `DataVisualizerUserState.json` in Unity's per-user persistent - data path. Each developer has a private copy. -- Project asset: a `DataVisualizerSettings` asset, created at - `Assets/Editor/DataVisualizerSettings.asset` on first use if none exists. +- User state: `DataVisualizerUserState.json` in Unity's per-user persistent data path. Each developer has a private copy. +- Project asset: a `DataVisualizerSettings` asset, created at `Assets/Editor/DataVisualizerSettings.asset` on first use if none exists. -If the stored state is empty or corrupt, the window logs a warning or error, -starts with default state, and keeps going — it never half-applies broken -state. To recover your arrangement, restore the file or asset from backup or -let the window rebuild it; switching between the two stores copies the current -state across. See [Persistence](organizing.md#persistence). +If the stored state is empty or corrupt, the window logs a warning or error, starts with default state, and keeps going — it never half-applies broken state. To recover your +arrangement, restore the file or asset from backup or let the window rebuild it; switching between the two stores copies the current state across. See +[Persistence](organizing.md#persistence). -The pane widths and the Data Folder are not part of that state; they stay with -the settings asset or editor preferences as described on the -[Organizing](organizing.md) page. +The pane widths and the Data Folder are not part of that state; they stay with the settings asset or editor preferences as described on the [Organizing](organizing.md) page. ## Recreating the documentation images -The five images in `docs/images/` are generated from the live window, not -hand-taken. The capture driver lives at `Tests/Editor/DocsImageCapture.cs` and -arranges a known window state before each shot. +The five images in `docs/images/` are generated from the live window, not hand-taken. The capture driver lives at `Tests/Editor/DocsImageCapture.cs` and arranges a known window +state before each shot. Regenerate all shots with a batch-mode Unity invocation: @@ -114,34 +78,22 @@ unity -projectPath -batchmode -quit -executeMethod \ -docsImageOutputDir ``` -- The output directory also comes from the `DATAVISUALIZER_DOCS_IMAGE_DIR` - environment variable; without either, shots land in the project's - `Temp/DocsImageCaptures`. The driver never writes into `docs/` by default; - review results before committing them. -- Relative output paths resolve against the Unity **project root**, not the - package folder. To stage shots under the package, pass an absolute path to a - dot-folder such as `.docs-captures/` — Unity generates no `.meta` files for - dot-folders, so the staging area stays out of the importer. -- Each capture is validated against the laid-out window: regions Unity's - offscreen renderer should have painted are measured in the PNG, and a capture - that omits them fails closed instead of saving a misleading image. - -Known capture limit, recorded on -[#114](https://github.com/wallstop/DataVisualizer/issues/114): Unity's -offscreen panel renderer on macOS 6000.4 omitted several inspector and list -subtrees (the same window paints them fully on screen and on Windows 6000.5). -The driver detects that host gap and fails closed rather than publishing -blanked imagery; every committed PNG was produced and pixel-verified on -Windows 6000.5. +- The output directory also comes from the `DATAVISUALIZER_DOCS_IMAGE_DIR` environment variable; without either, shots land in the project's `Temp/DocsImageCaptures`. The driver + never writes into `docs/` by default; review results before committing them. +- Relative output paths resolve against the Unity **project root**, not the package folder. To stage shots under the package, pass an absolute path to a dot-folder such as + `.docs-captures/` — Unity generates no `.meta` files for dot-folders, so the staging area stays out of the importer. +- Each capture is validated against the laid-out window: regions Unity's offscreen renderer should have painted are measured in the PNG, and a capture that omits them fails closed + instead of saving a misleading image. + +Known capture limit, recorded on [#114](https://github.com/wallstop/DataVisualizer/issues/114): Unity's offscreen panel renderer on macOS 6000.4 omitted several inspector and list +subtrees (the same window paints them fully on screen and on Windows 6000.5). The driver detects that host gap and fails closed rather than publishing blanked imagery; every +committed PNG was produced and pixel-verified on Windows 6000.5. ## Working from a container -When the package checkout is edited inside a container while Unity runs on the -host, the host picks up container-side edits through the mounted folder and -recompiles on focus. Do not edit files while a test suite or capture run is in -progress on the host: the host watcher racing a mid-run edit can fail the run -with unrelated errors. Let the host finish, recompile, and report up to date -before the next edit, then re-run. +When the package checkout is edited inside a container while Unity runs on the host, the host picks up container-side edits through the mounted folder and recompiles on focus. Do +not edit files while a test suite or capture run is in progress on the host: the host watcher racing a mid-run edit can fail the run with unrelated errors. Let the host finish, +recompile, and report up to date before the next edit, then re-run. ## Still stuck @@ -149,6 +101,5 @@ before the next edit, then re-run. - [Search and filtering](search-and-filter.md) — what each finder matches. - [Managing assets](managing-assets.md) — asset operations and processors. - [Organizing your data](organizing.md) — layout, themes, persistence. -- [Issues](https://github.com/wallstop/DataVisualizer/issues) — known problems - and where to report new ones. Data Visualizer is alpha software; a report - with the console output and Unity version helps a lot. +- [Issues](https://github.com/wallstop/DataVisualizer/issues) — known problems and where to report new ones. Data Visualizer is alpha software; a report with the console output and + Unity version helps a lot. diff --git a/package.json b/package.json index 59c3d48..eb2dc48 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "0.1.0", "displayName": "DxVisualizer", "description": "Your game data, one window. Find, inspect, organize, and edit ScriptableObject data in one Unity editor window.", - "dependencies": {}, "unity": "2021.3", "keywords": [ "DxVisualizer", @@ -59,42 +58,11 @@ "unity:mcp:host": "node .llm/mcp/unity-mcp-host.mjs", "unity:mcp:doctor": "node .llm/mcp/unity-mcp-doctor.mjs", "tools:install": "bash .devcontainer/install-npm-tools.sh", - "ai:backends:install": "bash .devcontainer/ai-backends.sh install" + "ai:backends:install": "bash .devcontainer/ai-backends.sh install", + "format:md": "prettier --write \"**/*.md\"", + "format:md:check": "prettier --check \"**/*.md\"" + }, + "devDependencies": { + "prettier": "3.9.9" } } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -