Scope: how to develop, test, and prototype changes in this mixed PHP + TypeScript repo without losing your mind. Operational companion to ARCHITECTURE.md.
.
├── README.md, THESIS.md # narrative — what this is, why
├── CHANGELOG.md # what shipped when
├── docs/ # ARCHITECTURE.md, PROTOCOL.md
├── package.json, tsconfig.json # JS toolchain (Vite + Vitest)
├── vitest.config.ts
├── composer.json # PHP toolchain (PHPUnit)
├── .ddev/ # local Drupal sandbox
├── src/ # JS — theme runtime
│ └── world/
├── test/ # JS tests
├── modules/
│ ├── world_embeddings/ # PHP — embedding lifecycle owner
│ │ ├── world_embeddings.{info.yml,module,services.yml,install}
│ │ ├── src/{Embedder,Storage,Search,Projection,Plugin}/
│ │ └── tests/src/{Unit,Kernel}/
│ └── world_signature/ # PHP — companion module (the cypher)
│ ├── world_signature.{info.yml,module,services.yml,install}
│ ├── src/
│ └── tests/src/{Unit,Kernel}/
└── theme/ # Drupal theme files
├── drupal_threejs.info.yml
├── drupal_threejs.libraries.yml
└── templates/
Two toolchains, side by side. JS work touches src/ and test/. PHP
work touches modules/world_embeddings/ or modules/world_signature/.
Theme work touches theme/. They merge in CI and at deploy time.
Nothing in theme/ ever imports from modules/; coupling is via the
descriptor contract only.
Working principle. All code operations run inside DDEV. There is no bare-host fallback path. The host machine needs only Docker + DDEV installed; PHP, Composer, Node, MariaDB, and MongoDB are all provided by the containerized environment.
A sandbox property lives inside this repo's .ddev/. It is a
disposable Drupal 11 site that requires this theme via a Composer
path-repository, so changes in modules/world_embeddings/,
modules/world_signature/, theme/, and src/ show up immediately.
ddev start # bring up the container stack
ddev composer install # install PHP deps
ddev npm install # install JS deps
ddev drush site:install minimal
ddev drush en world_embeddings world_signature advancedqueue
ddev drush theme:install drupal_threejs
ddev drush theme:default drupal_threejs| Layer | Command |
|---|---|
| JS unit (Vitest) | ddev npm test |
| PHP unit (PHPUnit, no Drupal bootstrap) | ddev composer run test:unit |
| PHP kernel (PHPUnit + Drupal kernel) | ddev exec phpunit modules/*/tests/src/Kernel |
ddev npm run dev # Vite dev server, exposed via DDEV's reverse proxy
ddev exec drush cr # cache rebuild after Drupal-side changesThe sandbox is disposable. fixtures/sandbox.sql is the canonical
seed. Rebuild:
ddev drush sql:cli < fixtures/sandbox.sql
ddev drush cache:rebuildSave the current sandbox state as the new fixture:
ddev drush sql:dump > fixtures/sandbox.sqlThe cypher writes through a gateway, not directly to a database. Local development and production use the same shape:
| Service | Sandbox (DDEV) | Production (tecnocratica VPS) |
|---|---|---|
| MongoDB Atlas cluster | M0 free tier (one shared sandbox cluster) | One cluster per client/theme deployment |
| RESTHeart gateway | Container in DDEV (ddev get add-on or custom service in .ddev/docker-compose.restheart.yaml) |
Sidecar container in the property's compose stack |
| Embedding generation | Atlas-managed (Voyage) — fires on insert into a Vector Search-indexed collection. No separate embedder service. | Same — Atlas handles it server-side. |
| LLM operations (chatvatars, query translation in v0.0.3+) | drupal/ai + drupal/ai_provider_anthropic from Drupal |
Same |
| Layer | Lives in | Speed | What it covers |
|---|---|---|---|
| JS unit | test/*.test.ts |
<100 ms / suite | URI→coordinate, layout math, descriptor parsing, vantage |
| PHP unit | modules/*/tests/src/Unit/ |
<1 s / suite | Signature extractor math, serialization round-trip, embedding interface contracts |
| PHP kernel | modules/*/tests/src/Kernel/ |
seconds / suite | Hooks fire, queue worker runs, snapshot publishing produces the expected JSON, search service returns ranked results |
All three run inside DDEV. CI runs all three on every push. Local development typically runs only the unit layers between commits; kernel tests run on push, on a sandbox rebuild, or manually before a PR.
| # | Decision | Choice | Rationale |
|---|---|---|---|
| 1 | Module location | In this repo, alongside the theme. | One repo, one history; atomic changes across cypher + renderer + theme. Will split out if either module reaches independent reuse. |
| 2 | DDEV | Set up here as a sandbox property. | Doubles as integration test surface and prototyping playground. |
| 3 | Queue mechanism | drupal/advancedqueue (contrib). |
Retries + admin UI; sturdier than core Queue API. |
| 4 | Embedding posture | First-class capability from day one. Not a deferred slot. | E1–E7 below; the embedding epic. |
| 5 | All code operations under DDEV | No bare-host fallback. | Single source of truth for tool versions; matches the webrunners stack working norm. |
| Embedding epic | |||
| E1 | Embedding source | Voyage AI for production (voyage-multilingual-2); local sentence-transformers for sandbox. Both behind the same EmbedderInterface. Pending user confirmation that "Claude" was shorthand for "Anthropic-aligned" — Anthropic does not offer first-party embeddings as of 2026-05-05. |
Closest-to-Claude posture that exists today; ships free in dev, paid in prod with a swap. |
| E2 | Vector storage | MongoDB Atlas ($vectorSearch aggregation, Atlas-managed Voyage embeddings on insert). One cluster per client / per theme deployment for tenant isolation. |
Purpose-built filter/projection language; scales independently of MariaDB; per-tenant isolation is a hard requirement once the cypher becomes a service offering. |
| E2a | Atlas access path | RESTHeart sidecar as the gateway. Drupal speaks Guzzle/HTTPS to RESTHeart; RESTHeart routes per-tenant to the right Atlas cluster. (Pivot recorded 2026-05-07; Atlas App Services was sunset 2025-09-30, killing the original Function-as-bridge plan.) | Multi-tenant routing, central auth, observability gateway, no ext-mongodb in DDEV. Direct mongodb/mongodb PHP driver kept on file as the second-fallback. |
| E3 | Indexing strategy | Async via advancedqueue. |
Editor save is fast; vector arrives seconds later; resilient to embedder outages. |
| E4 | Search API surface | Both — Drupal route (/api/world/search?q=...) + JSON:API resource. |
One backend, two surfaces. |
| E5 | Search type | Hybrid (BM25 + vector rerank). | Modern default; better recall on uncommon proper nouns. |
| E6 | Reindex protocol | Cron-driven bulk reindex and lazy on-retrieval determination. Reads check the stored vector's modelVersion tag; if stale, mark for reembed and serve stale this time. Cron sweeps the marked set. |
Belt-and-suspenders. Cron prevents lazy path overload; lazy catches stragglers and post-cron model upgrades. |
| E7 | Language model | English is the language core for all content. Translations added for completeness. Default site language set per property only after the English core is in place. monpetitcafe → English-core / Spanish-default. | One vector space across the ecosystem; cross-property search free; query-translation via LLM (Claude is good at it) → embed in English → display in property's default language. |
| Card model | |||
| C1 | Card source | Drupal view modes. Deck = enabled view modes for the bundle. | No new abstraction; reuse Drupal's existing render-array machinery. |
| C2 | Activation (trigger pad) | Click + hover preview. Touch maps to tap. | Modality without surprise; hover signals interactability without committing. |
| C3 | Paragraphs: cards or sub-objects | Per-paragraph-type config. Hero/quote/callout = cards on parent. Body-text = inline content of parent's main card. Section/divider = sub-objects in the world. | Editorial flexibility with sane defaults per type. |
| C4 | Rendering surface | DOM overlay over canvas (real <div>s). |
Accessibility, copy-paste, browser-native features all work without bridges. |
| C5 | URL coupling | Bloom = URL fragment (#card=<id>); FullView = path segment (/v/<viewMode>). |
Fragment for ephemeral state (no server hit on back/forward); path for deep-linkable, server-renderable state. |
| C6 | Deck UI | Single card visible with tabs to switch. Multi-pane responsive is v2. | Familiar; tabs map to aria-tabpanel for accessibility. |
| C7 | World during full view | Engine paused (renderer.setAnimationLoop(null)). Canvas freezes / fades; DOM overlay carries the read. |
Battery and focus win bigger than DoF blur, especially on mobile. |
| C8 | Mode transition | Mode = exploration | reading flag on SceneManager. Single state owner; clean enter/exit hooks. |
Single source of truth for the renderer's macro-state. |
| C9 | Trigger pad design | Skinned per content type, with a sane default. Editorial config exposes the skin. | Visual identity per content type without forcing custom geometry per object. |
| C10 | Bloom event triggers | Schedule, proximity, search match, world-event-bus. Editorial config picks the subset per card. | Extensible; other modules can publish triggers without modifying the renderer. |
| C11 | Bloom auto-close | TTL per trigger source. Search blooms fade after 30 s; schedule blooms persist; manual blooms persist until dismissed. | Different sources have different urgency profiles. |
| C12 | Multi-card simultaneous bloom | Allowed, capped at 3 visible. Excess queue. | "This object has several reasons to look at it right now." |
| C13 | Engine-pause granularity | Full pause. | Throttle is a half-measure that loses most of the battery win. |
Claude Code agents working on this project benefit from a direct window into Atlas. The dev environment expects (but does not require) the MongoDB MCP server registered at the user level.
- Inspect the
descriptorscollection without going through Drupal. - Verify a queue worker's Atlas write actually landed.
- Run
$vectorSearchaggregations during search-tuning (Sprint 3b-2+). - Inspect vector index definitions Atlas auto-creates.
The MCP server is user-level, not project-level. Each engineer
configures it in their own ~/.claude.json against a personal
read-only credential. No MCP config is committed to this repo.
Setup (per-engineer, one-time):
- In Atlas: create a database user
mcp_readonly(or similar) withreadrole scoped todrupal_three_js_theme_worldonly. Do not reuse the cluster's admin user (csantanad_db_user). - In WSL: run
npx mongodb-mcp-server@latest setup. Pick Claude Code as the client, paste the connection string for themcp_readonlyuser, enable read-only mode (the wizard's recommended default). - Restart Claude Code to register the MCP server.
The resulting config block in ~/.claude.json:
{
"mcpServers": {
"MongoDB": {
"command": "npx",
"args": ["-y", "mongodb-mcp-server@latest", "--readOnly"],
"env": {
"MDB_MCP_CONNECTION_STRING": "mongodb+srv://mcp_readonly:<password>@<cluster-host>/drupal_three_js_theme_world"
}
}
}
}- Read-only by policy. The
--readOnlyarg + the user's scopedreadrole are belt-and-suspenders. Don't grant the MCP user write access; if you need to clear test data, use Drupal-side or the Atlas web UI. - Not a production tool. Production Drupal writes via the cypher's queue worker; MCP is for development verification only.
- Credential hygiene. Never paste the
mcp_readonlypassword into chat or commit messages. The password lives in~/.claude.json(gitignored at the user level by Claude Code itself).
After restarting Claude Code, the agent should see tools prefixed
with mcp__MongoDB__* (e.g. mcp__MongoDB__find,
mcp__MongoDB__aggregate, mcp__MongoDB__list_collections).
A simple sanity query: list collections in
drupal_three_js_theme_world — should return [] until Sprint
3b-2 writes the first descriptor.
Once v0.1.3 lands its ProfileBuilder (per
docs/v0.1/SMART_OBJECTS.md), the renderer needs real 3D assets
— rigged figures, idle animations, props. Two MCP servers give
agents working on this project the ability to source and
manipulate those assets without leaving the chat.
Both are optional: every Builder degrades to a primitive
when its required asset is missing (per
FallbackBuilder.matches() and the try/catch guards inside
each specific Builder). An engineer not running these MCPs sees
the cube fallback for any glb-dependent entity; nothing breaks.
Searches and downloads free 3D models from Sketchfab. The
matching pattern for our use: rigged low-poly characters under
CC0 or CC-BY licenses, exported as .glb.
- Search Sketchfab's catalog by keyword + license filter + format.
- Read model metadata (polycount, animation count, license, attribution required y/n).
- Download in
gltf/glb/usdz/ source format.
For the project this collapses asset sourcing from "open browser, search, download, unzip, find the glb, copy to theme assets" to a single agent call.
-
Get a Sketchfab API token. Free account at sketchfab.com; profile → settings → Password & API → copy the token. The token grants the same read/download permissions the logged-in user has — keep it private.
-
Add to
~/.claude.json(user-level, not committed):{ "mcpServers": { "Sketchfab": { "command": "npx", "args": ["-y", "@gregkop/sketchfab-mcp-server"], "env": { "SKETCHFAB_API_TOKEN": "<your-token>" } } } } -
Restart Claude Code so the server registers.
The Windows-host ~/.claude.json is the one Claude Code reads;
if you're working through WSL, follow the host-side merge pattern
documented in the founding-session battle scars (jq merge of the
mcpServers block) rather than maintaining two configs.
| Want | Agent does |
|---|---|
| A stylized humanoid figure | mcp__Sketchfab__search with q="low-poly character", license=cc0, format=glb, animated=true |
| Inspect a candidate before downloading | mcp__Sketchfab__get_model with the model UID |
| Download to project | mcp__Sketchfab__download_model to web/themes/custom/drupal_threejs/assets/models/<slug>.glb |
License hygiene — when a CC-BY asset lands in the project,
the attribution string (author + URL + license) must be
recorded in docs/ASSET_ATTRIBUTIONS.md (created the first time
a CC-BY asset arrives). CC0 assets need no attribution but are
worth recording anyway for provenance. The Sketchfab MCP returns
the attribution block per asset; agents are expected to append
it on download.
- Free / open-license only. Don't download anything under
Sketchfab's "Editorial" or "Standard" commercial licenses for
use in the project; the matching
license=cc0|cc-by|cc-by-safilter is non-negotiable for shipped assets. - Read-only operations only. The server supports search and download; do not enable the upload-side of Sketchfab's API from this MCP.
- API rate limits. Sketchfab's free tier rate-limits search to ~60/minute. Don't sweep the catalog in a tight loop.
When no library has what we need (a property-specific Chatvatar, a one-off ceremonial object, a stylized barista to match the atlas_coffee subject brief), Tripo MCP is the dedicated text-to-3D and image-to-3D generation path. From the Tripo team at VAST AI Research, who released the foundational TripoSR open-source model in 2024 and have since productised it.
Tripo MCP is preferred over Blender MCP's built-in Hyper3D / Hunyuan3D generators for generative work. The Blender generators remain on file as fallbacks if Tripo is down or the prompt fails to converge, but Tripo is the specialist — purpose-built, single API, single license model. Don't multi-shop generative AI services for the same artifact unless you have a specific quality reason to compare.
- Text-to-3D generation (
mcp__Tripo__generate_text_to_model). - Image-to-3D generation (
mcp__Tripo__generate_image_to_model) — useful when an artist hands you a reference still. - Refinement / texture / animation pipelines as separate tool calls — generation is multi-pass by default.
- Direct hand-off into Blender via the Tripo Blender Addon (companion repo VAST-AI-Research/tripo-3d-for-blender). Result lands in the open Blender scene; Blender MCP can then refine and export.
-
Get a Tripo API token. Free account at tripo3d.ai; profile → API → create key. Tripo's free tier covers exploratory work; paid tiers kick in for high-volume / commercial use. Tag the key in the key vault as project-specific so per-project cost attribution stays clean.
-
Add to
~/.claude.json:{ "mcpServers": { "Tripo": { "command": "uvx", "args": ["tripo-mcp"], "env": { "TRIPO_API_KEY": "<your-key>" } } } } -
(Optional, for Blender hand-off) install the Tripo Blender Addon into your local Blender install. Without it, Tripo MCP still generates glb files — they just don't auto-open in Blender; you receive a download URL or path and route them manually.
-
Restart Claude Code to register the MCP server.
| Want | Agent does |
|---|---|
| A barista chatvatar for the atlas_coffee subject | mcp__Tripo__generate_text_to_model with prompt="stylized Guatemalan barista, apron, holding a coffee pour-over kettle, low-poly", then refine + texture passes |
| Convert a reference photo of a producer into a 3D bust | mcp__Tripo__generate_image_to_model with the image URL or upload |
| Land the result in Blender for re-rigging | Tripo Blender Addon picks it up if Blender is running with the addon active; otherwise fetch the glb from the result URL and load it manually |
- Generated content gets license-tagged. Tripo outputs are
generally permissive for commercial use under the free + paid
tiers (verify against Tripo's current terms before shipping
commercial properties), but the prompt + seed + output URL go
in
docs/ASSET_ATTRIBUTIONS.mdso the provenance is reproducible. - Don't iterate generation in a tight loop. Each generation costs credits + walltime. Reach for Sketchfab first; only reach for Tripo when no library candidate fits.
- The MCP is currently alpha (per the upstream repo's own
README as of mid-2026). Tool names and parameters may shift
between releases; pin the MCP version in
~/.claude.jsonrather than tracking@latestfor cross-engineer reproducibility.
When Sketchfab doesn't have what we need (or what's there needs re-rigging, re-scaling, animation re-targeting, or stylization to match the world's aesthetic), and Tripo's generation doesn't land in a usable place either, Blender MCP lets an agent drive Blender directly: create scenes, modify objects, run Python, export glb. It's also the workshop where Tripo's output gets final touches before shipping.
- Scene inspection (current objects, materials, modifiers).
- Object create / modify / delete + parameter control.
- Arbitrary Python execution inside Blender (the escape hatch).
- Pull from Poly Haven (CC0) and Sketchfab (forwarded through Blender MCP's own Sketchfab integration).
- AI-generated geometry via Hyper3D / Hunyuan3D text-to-3D (kept on file as a fallback to Tripo MCP — see §4c.2).
- Export the active scene or selection as
.glb/.gltf/.fbx/.obj.
Blender MCP is heavier than Sketchfab or Tripo MCPs — it talks to a running Blender instance via a Blender addon — but its scope covers any post-import refinement (UV cleanup, decimation, animation re-target, material baking) that the asset library or the generator didn't quite nail.
-
Install Blender (4.0+ recommended).
- Windows: blender.org/download.
- WSL: Blender is GUI-heavy; run it on the Windows host even if your project files live in WSL. Save exported glbs into the WSL-mounted project path.
-
Install the Blender addon. Clone ahujasid/blender-mcp, open Blender → Edit → Preferences → Add-ons → Install and point it at the repo's
addon.py. Enable the addon. The addon shows a panel in the 3D viewport's N-side; click Start MCP Server to open the listener (default port 9876). -
Add the MCP server to
~/.claude.json:{ "mcpServers": { "Blender": { "command": "uvx", "args": ["blender-mcp"] } } }The server is a Python tool installed via
uv/uvx; ifuvisn't on PATH, install it withcurl -LsSf https://astral.sh/uv/install.sh | sh(per astral.sh) then re-run. -
Restart Claude Code. Blender must be running with the addon's MCP server started before an agent reaches for
mcp__Blender__*tools; otherwise the calls fail with a connection-refused error.
| Want | Agent does |
|---|---|
| A Poly Haven HDRI for environment lighting | mcp__Blender__poly_haven_search + download_asset |
| Stylize a Sketchfab figure to match the world | search via Sketchfab MCP, download via Sketchfab MCP, open in Blender via Blender MCP, run a Python script to recolor/decimate/re-rig, export glb |
| Generate a chair "looking like Antigua coffee shop" | mcp__Blender__hyper3d_generate with the prompt |
| Recompute UVs or bake a procedural material | mcp__Blender__execute_python with the recipe |
- Local Blender instance only. No agent should
apt installBlender into the DDEV web container — Blender is a developer-tool, not a runtime dependency. Per the DDEV-only working principle (§1), Blender is an exception: it lives on the host because it's authoring software, not application code. - Generated content gets license-tagged. Hyper3D / Hunyuan3D
generations are CC0 by default; record the prompt and seed in
docs/ASSET_ATTRIBUTIONS.mdso the provenance is reproducible. - Python execution is wide-open. The
mcp__Blender__execute_pythontool runs arbitrary Python inside Blender. Don't paste Python that touches the filesystem outside the project'sweb/themes/custom/drupal_threejs/assets/tree without a clear reason.
Decision tree at the point of needing an asset:
- Is it already in
web/themes/custom/drupal_threejs/assets/? Use it. - Is there a known CC0 source (Quaternius, Kenney.nl, Poly Haven, three.js examples)? Fetch by curl, drop into assets/. Fastest path; no MCP setup needed.
- Does Sketchfab have a near-fit under CC0 or CC-BY? Sketchfab MCP, search + download, record attribution.
- Does the asset need shape/material modification to fit the world's aesthetic? Sketchfab (or three.js examples) → Blender MCP for the modification step → export glb.
- Is the asset something no library plausibly has (the property's brand persona, a custom Chatvatar)? Tripo MCP for the generation step → optionally Blender MCP for refinement → export. Hyper3D / Hunyuan3D inside Blender MCP stay on file as fallbacks if Tripo doesn't converge.
Most v0.1.3 work (ProfileBuilder, first rigged figure) lives in step 2 or 3. Steps 4–5 wait for v0.2+ when the world's aesthetic is more crystallized and the property-specific Chatvatars arrive.
The three MCPs form a clean pipeline rather than competing: Sketchfab finds, Tripo generates, Blender refines. Each is optional; each accelerates one stage; none can stand in for another's job.
After restarting Claude Code with any combination configured:
- Sketchfab: the agent should see
mcp__Sketchfab__search,mcp__Sketchfab__get_model,mcp__Sketchfab__download_model. Sanity query: searchq="cube",license=cc0,count=1. - Tripo: the agent should see
mcp__Tripo__generate_text_to_model,mcp__Tripo__generate_image_to_model, plus refinement + texture tool variants per the upstream README. Sanity query:generate_text_to_modelwithprompt="small wooden cube",seed=1— returns a credit-cost preview before committing the credits. - Blender: with Blender running + addon active + Start MCP
Server clicked, the agent should see
mcp__Blender__get_scene_info,mcp__Blender__execute_python,mcp__Blender__poly_haven_search, etc. Sanity query:mcp__Blender__get_scene_info→ returns the current scene's object list (likely just the default cube + camera + light).
If any MCP is registered but tools don't show up, check Claude Code's MCP logs (settings → MCP Servers → server name → View logs) for connection errors. Common scars:
- Sketchfab token expired — generate a new one;
npxwon't warn, the search just returns 401s. - Tripo credits exhausted — generation calls return a
quota-exceeded error; check
tripo3d.aiaccount balance before assuming the MCP is broken. - Blender addon not listening — the Start MCP Server button must be clicked each time Blender restarts; the addon doesn't auto-start.
- Tripo MCP version drift — the upstream is alpha as of
mid-2026; pin a specific version in
argsrather than lettinguvxresolve@latestso cross-engineer behavior stays reproducible.
Architecture pivot recorded 2026-05-07. Atlas App Services (and its Custom HTTPS Endpoints + Functions) was sunset on 2025-09-30. The original Sprint 3b-2 design that bridged Drupal → Guzzle HTTPS → Atlas Function → cluster is dead. We pivoted to RESTHeart as the data gateway. See docs/ARCHITECTURE.md §9 for the full pattern.
| Layer | Component | Notes |
|---|---|---|
| Drupal-side data access | Guzzle (already in Drupal core) | No PHP MongoDB driver, no ext-mongodb. Drupal sees only HTTPS to RESTHeart. |
| Gateway | RESTHeart (Java/Kotlin, OSS, Apache 2.0) | Self-hosted on the tecnocratica VPS as a Docker sidecar. |
| Vector store | MongoDB Atlas — one cluster per client / per theme deployment | Atlas-managed embeddings (Voyage-powered, post-MongoDB-acquisition) trigger on insert; no separate embedder service needed. |
| Auth Drupal → RESTHeart | JWT or scoped API key issued by RESTHeart | Per-property credential. Token leak compromises one property, not all clusters. |
| Auth RESTHeart → Atlas | MongoDB connection string per cluster, configured in RESTHeart's restheart.yml |
One credential per managed cluster. RESTHeart never exposes the connection strings outward. |
Captured 2026-05-07 from upstream sources:
| Component | Version line |
|---|---|
| RESTHeart | track latest 8.x; pin to a specific tag in production compose for reproducibility |
| MongoDB Atlas Vector Search | M0+ supports $vectorSearch; managed embeddings GA per Atlas docs |
Drupal guzzlehttp/guzzle |
tracks Drupal core's pin (^7.x as of D11.3) |
If RESTHeart turns out to be operationally unsuitable in the future (unlikely but possible; the pivot's escape hatch is the same one we nearly took before App Services died):
| Driver | Version |
|---|---|
mongodb/mongodb (PHPLIB) |
^1.11 |
ext-mongodb (PECL) |
^1.10 |
The fallback recipe — install ext-mongodb via PECL in
.ddev/web-build/Dockerfile.example, add mongodb/mongodb to
composer.json, replace WorldSearchClient's HTTPS calls with
direct MongoDB\Client calls — is the same shape we documented
pre-pivot. Recorded here as the second-fallback option only.
The protocol for trying a new mapping (e.g. what if a news_alert
content type became a thunderclap moving across the sky?).
- Add the content type to the sandbox's config (
config/sync/). - Define the metaphor mapping in the theme's per-content-type config
(
theme/config/world_metaphors.yml). - Configure the deck — which view modes are cards, which paragraphs are cards-on-parent vs sub-objects in the world, what events bloom each card, what the trigger pad looks like.
ddev drush world:publish— refresh the snapshot.- Reload the renderer. The new content type renders by mapping defaults until each override is implemented.
- Iterate. Commit only when the mapping has stabilised.
The sandbox is disposable. fixtures/sandbox.sql is the canonical
seed.
- Pre-commit:
ddev npm test && ddev composer run test:unit(unit only, fast). - Pre-push (manual or CI): kernel tests +
ddev exec tsc --noEmit- PHP static analysis (phpstan or psalm — TBD).
- CI: unit + kernel + linters +
ddev npm run build. Image build only onmain. - Deploy on a property:
drush deployfollowed bydrush world:publishso the snapshot reflects live config.
Until the project ships to a real install, configuration
changes do not get hook_update_N hooks. hook_update_N is
migration infrastructure for deployed installs — it bridges
schema changes across versions that exist in production. We have
one disposable dev sandbox and zero production installs. A
hook_update_N written now migrates nothing; it's theater.
The rule:
- Site-building config (vocabularies, fields, content types,
view modes, queues, the palette, the manifesto) lives as
static
config/install/*.ymlinweb/modules/custom/world_signature/. The module is the project's Drupal scaffold — a freshdrush si standard && drush en world_signaturebrings the full structure with no hooks, no scaffold scripts, nodrush config:setadaptations. - Changing a shipped default: edit the
config/install/yml. Re-apply to the sandbox by reinstalling the module, or with a one-offscaffold/*.phpscript, ordrush config:setfor a single key. The sandbox is disposable — a fulldrush sirebuild is a known, cheap quantity. - Content (taxonomy terms, nodes) is not config — it stays
in
scaffold/seed-atlas-coffee.php. The seeder creates content inside the structure the module's config/install/ already established; it never builds structure itself. - The first
hook_update_Ngets written the day before the first production deploy — not before. At that point it earns its keep: it migrates the real install that now exists.
This rule was applied retroactively in v0.2.x: world_signature.install
(five update hooks, no hook_install) was deleted outright, and
the floating site-building it half-managed was condensed into
config/install/.
- Don't import PHP-side enums into TS or vice versa. Keep type definitions parallel and synced via the descriptor contract.
- The descriptor schema lives in two places: as TS types in
src/world/types.ts, and as PHP value objects inmodules/world_signature/src/Signature/. A schema test in each language asserts they round-trip the same JSON fixture (fixtures/descriptor-canonical.json). - When the descriptor changes, both sides update in the same commit. CI fails loudly if the canonical fixture doesn't round-trip on both sides.
- The
EmbedderInterfacecontract lives in PHP. The renderer never calls an embedder directly; it consumes pre-computed vectors from the snapshot. Crossing this line is a smell.