You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Generate the seed mission config from plugin discovery and defaults
Summary
The seed mission is the config MMGIS boots from — it seeds new deployments (via the deployment skill) and is what PR previews come up running. Today it's a hand-maintained file that silently drifts whenever a tool or plugin changes. Replace it with a generated seed: discover the installed plugins, compose each plugin's declared defaults, add a small fixed scaffold for the parts plugins can't describe (layers, view, layout), and inject known environment values. The generated file is committed and consumed by the deployment skill exactly as the hand-written one is today; CI keeps it honest by regenerating and diffing.
Motivation
The seed is load-bearing — new deployments and every PR preview boot from it — and keeping it correct by hand is a standing tax and an easy thing to forget. Add a tool, forget the seed, and the preview shows nothing new. Generation makes the seed correct by construction: a new plugin appears the moment it declares defaults, and nothing has to be remembered or policed. It also matches where the project is going — plugins that own their own defaults, composed by the core.
How it works
Discover plugins. Enumerate the installed tools/plugins. Most already ship a manifest.
Compose declared defaults. Each plugin declares its default instance config — its settings, whether it starts on, where it prefers to sit. The generator assembles these into the seed's tool set.
Opt in by manifest. A plugin that declares defaults is included; one that doesn't is skipped. Legacy tools with no manifest simply don't appear — a deliberate nudge toward the plugin model. An explicit opt-out flag exists as the escape hatch for a manifested tool we don't want shown by default.
Per mode. Generation respects deployment mode, so the lean seed contains only the tools lean enables. (Today that exclusion is a single hardcoded rule in the tool assembler — there is no per-tool capability table yet; see the gotchas section.)
Fixed scaffold for the rest. The parts no plugin can describe — a basemap and any default layers, the initial view, projection, and overall look/layout — come from a small, curated scaffold input, not from discovery.
Env injection. Known environment values (basemap token, external service URLs, deployment mode) are injected at generation time.
Committed and checked. The generated seed is committed and is the file the deployment skill consumes. CI regenerates it and fails if the committed copy is stale — the diff is the review artifact, and there's nothing for a human to attest.
You never hand-edit the generated seed. You maintain its inputs: each plugin's defaults, the small scaffold, and the environment.
Decisions baked into this issue
The seed is always generated, never hand-edited. This replaces the earlier idea of a hand-maintained seed kept current via a config-update log and a CI grep — that relied on human attestation, was easy to game, and over-matched trivial edits.
It's committed and consumed by the deployment skill unchanged. The generator writes the same file the seed flow already bakes from; CI enforces freshness by regenerate-and-diff.
Plugins declare their own defaults. Presence of defaults is the opt-in; an explicit flag is the opt-out.
The non-plugin parts come from a small curated scaffold + env, not from discovery.
Behavior / acceptance
Running the generator produces the seed deterministically (stable ordering) from plugin defaults + scaffold + env.
A plugin that declares defaults appears in the generated seed without anyone editing the seed; one without defaults does not.
The generated seed is mode-aware — a lean seed contains only lean-enabled tools.
CI fails a PR whose committed seed differs from a fresh regeneration.
The deployment skill seeds from the committed generated file with no change to how it consumes it.
Adding a new tool requires only adding its defaults (and regenerating) — no manual seed editing, no log.
Open within this issue
Default layers — what, if anything, the scaffold ships beyond a basemap so a preview isn't a blank map. Lean references data by external URL, so a sensible default is basemap-from-env plus at most one curated demo layer; exact choice TBD.
Layout policy — how the generator orders and places tools from their declared placement hints.
Decision record to ship with this PR
This PR should add a short standalone ADR at docs/adr/20260617-generated-seed-config.md, to ensure the rationale is documented:
The seed mission config is generated from plugin discovery + defaults, not hand-maintained.
Context: the seed (new deployments + PR previews) drifts when tools change, and keeping it current by hand is a standing tax.
Decision: generate the seed by discovering plugins, composing each plugin's declared defaults, adding a small curated scaffold for the non-plugin parts (layers, view, projection, look), and injecting env; commit the result and enforce freshness with a regenerate-and-diff CI check.
Rationale: correct by construction — a new plugin self-registers via its defaults — and the freshness check is mechanical and ungameable. Rejected: a config-update log + grep CI (relies on human attestation, games easily, over-matches trivial edits); an AI-in-CI judge (overkill now, adds a token/secret dependency — revisit for richer code review).
Consequences: each plugin must declare a defaults block (a one-time migration across existing tools); legacy tools without a manifest drop out of the seed until they get one; the generator owns a layout policy and a small scaffold; mode-awareness mirrors the existing lean tool-exclusion (a per-tool capability table does not exist yet and would be introduced here if richer gating is needed).
Implementation sketch — rough guide, written as of 42b9e0ad on 2026-06-17; re-verify against latest
Discovery source (the rail mostly exists):
The tool registry is src/pre/tools.js; 20 tools already ship src/essence/Tools/*/config.json. Those manifests carry identity + placement metadata (name, description, defaultIcon, paths, metadata.preferredPosition/compatiblePositions) but not default instance values. Add a defaults block to each — the values a mission tool entry needs: { on, variables, position }. (A mission tool entry looks like {name, icon, js, on, variables}; e.g. Layers is {on:false, variables:{expanded:false}}.)
Opt-out escape hatch: a seed:false (or similar) flag on a manifest that has defaults but shouldn't appear by default.
Output (nothing downstream changes):
Write the generated seed to .claude/skills/mmgis-deployment/seed/baseline-mission.json — the exact file seed-golden.sh already bakes into the mmgis_golden template DB. Keep the existing {{MAPBOX_TOKEN}} env-injection placeholder convention so tokens stay out of the committed file.
Scaffold input (the only hand-maintained mission content):
A small committed base for the non-plugin sections (msv, projection, look, panelSettings, and the default layers — at least a basemap). The generator merges discovered tools[] over this base. This is the part discovery can't produce.
Mode-awareness (correction — no tool capability table exists):
Lean tool exclusion today is a single hardcoded line in API/updateTools.js (if (isLean() && name === 'Draw') continue). The Centralize deployment-mode gating behind a single capability definition #152 gating refactor gates backend routes, not tools, and Configure's capabilities.js gates form fields, not tools — so there is no table to reuse. Mode-aware generation mirrors that Draw skip; any richer per-tool gating would be introduced here. Generate lean and full variants if both are needed.
CI check:
A pull_request job runs the generator and git diff --exit-code on the seed (and the tool manifests). Gotcha — generation must be deterministic (sorted keys, fixed tool ordering) or the diff check flaps on noise.
Gotcha — no real plugins folder yet:
"Plugins" today = in-tree src/essence/Tools/**. The discovery + defaults convention is a down-payment on the plugin manifest the vision calls for; when a real plugins directory lands, point discovery at it.
A full read of the tool/seed code surfaced these — they reshape how much "generation" actually covers:
No per-tool default-instance block exists today.config.json carries only a Configure-UI schema + identity + placement — never on/variables/position. The one place defaults are materialized is the Configure UI (configure/src/components/Tabs/Tools/Modals/ToolModal/ToolModal.js), which fills variables defaults for only 4 component types (dropdown→options[0], checkbox/switch→defaultChecked, slider→default||min, colorpicker→default). So "compose each plugin's declared defaults" has no source yet — either (a) add a declarative defaults block to each of the 20 tools, or (b) port the ToolModal algorithm to Node (no per-tool work, but inherits the gaps below).
Schema-derived defaults are lossy.text/number/textarray/objectarray get no default from that algorithm. Anything list-shaped — Card.cards, Sites.sites, Isochrone/Shade/Viewshed data, Draw's text-default fields — cannot be generated from a schema and must live in the scaffold/hand-authored. Net: generation realistically covers only the scalar on/enum/slider knobs of tools[]; everything list-shaped, plus all of layers/msv/projection/look/panelSettings, is hand-maintained scaffold.
Discovery is already automatic — but tools.js is a generated artifact.API/updateTools.js does fs.readdirSync('src/essence/Tools'), reads each config.json, and writessrc/pre/tools.js + configure/public/toolConfigs.json. The generator should run the same dir scan, not parse tools.js (a stale snapshot).
BIGGEST TRAP — deepmerge array concatenation in configure/add. The seed is POSTed to /api/configure/add, which deepmerges config_template (ships tools:[Layers,Legend,Info]) with the posted config using array concatenation. So the seeded DB ends up with duplicate Layers/Legend/Info entries — the seeded mmgis_golden ≠ the committed baseline-mission.json. A regen-and-diff CI must diff against the committed file, not the DB; if we ever want file==DB, switch add() to a replacing arrayMerge.
Determinism hazards for the diff check: (1) tool order depends on readdirSync + a toolbarPriority sort that defaults to 1000 (ambiguous ties) — sort explicitly; (2) the committed file holds the literal {{MAPBOX_TOKEN}} placeholder (substituted at seed time) — compare the placeholder form; (3) MMGIS files are CRLF — a generator emitting LF explodes into a whole-file diff; match line endings.
MapControl is a phantom. The current baseline has a MapControl tool entry but there is no src/essence/Tools/MapControl dir and no MapControlTool anywhere. Under "no manifest = excluded" it silently drops — a behavior change. Open question: dead config, or a tool expected to arrive as a plugin?
Kinds is not a real tool (never a mission tools[] entry) — the generator must skip it.
Seed-flow specifics:add() overwrites msv.mission/missionFolderName to the posted mission name (those seed fields are don't-cares), stores Config{mission, config, version:0}, and does not run validate()/populateUUIDs() (those are in upsert() only) — a generated config doesn't strictly need to pass validation to seed, but should to survive later edits.
Open questions to resolve in this issue:
Declarative per-tool defaults block (migration across 20 tools) vs porting the lossy ToolModal algorithm — pick the source of truth for tool defaults.
Accept that the diff target is the committed file (not the seeded DB), or change add() to replace arrays so file==DB.
What to do with MapControl — drop it, or keep it in the scaffold until it's a real plugin.
Generate the seed mission config from plugin discovery and defaults
Summary
The seed mission is the config MMGIS boots from — it seeds new deployments (via the deployment skill) and is what PR previews come up running. Today it's a hand-maintained file that silently drifts whenever a tool or plugin changes. Replace it with a generated seed: discover the installed plugins, compose each plugin's declared defaults, add a small fixed scaffold for the parts plugins can't describe (layers, view, layout), and inject known environment values. The generated file is committed and consumed by the deployment skill exactly as the hand-written one is today; CI keeps it honest by regenerating and diffing.
Motivation
The seed is load-bearing — new deployments and every PR preview boot from it — and keeping it correct by hand is a standing tax and an easy thing to forget. Add a tool, forget the seed, and the preview shows nothing new. Generation makes the seed correct by construction: a new plugin appears the moment it declares defaults, and nothing has to be remembered or policed. It also matches where the project is going — plugins that own their own defaults, composed by the core.
How it works
You never hand-edit the generated seed. You maintain its inputs: each plugin's defaults, the small scaffold, and the environment.
Decisions baked into this issue
Behavior / acceptance
Open within this issue
Decision record to ship with this PR
This PR should add a short standalone ADR at
docs/adr/20260617-generated-seed-config.md, to ensure the rationale is documented:Implementation sketch — rough guide, written as of
42b9e0adon 2026-06-17; re-verify against latestDiscovery source (the rail mostly exists):
src/pre/tools.js; 20 tools already shipsrc/essence/Tools/*/config.json. Those manifests carry identity + placement metadata (name,description,defaultIcon,paths,metadata.preferredPosition/compatiblePositions) but not default instance values. Add adefaultsblock to each — the values a mission tool entry needs:{ on, variables, position }. (A mission tool entry looks like{name, icon, js, on, variables}; e.g. Layers is{on:false, variables:{expanded:false}}.)seed:false(or similar) flag on a manifest that has defaults but shouldn't appear by default.Output (nothing downstream changes):
.claude/skills/mmgis-deployment/seed/baseline-mission.json— the exact fileseed-golden.shalready bakes into themmgis_goldentemplate DB. Keep the existing{{MAPBOX_TOKEN}}env-injection placeholder convention so tokens stay out of the committed file.Scaffold input (the only hand-maintained mission content):
msv,projection,look,panelSettings, and the defaultlayers— at least a basemap). The generator merges discoveredtools[]over this base. This is the part discovery can't produce.Mode-awareness (correction — no tool capability table exists):
API/updateTools.js(if (isLean() && name === 'Draw') continue). The Centralize deployment-mode gating behind a single capability definition #152 gating refactor gates backend routes, not tools, and Configure'scapabilities.jsgates form fields, not tools — so there is no table to reuse. Mode-aware generation mirrors that Draw skip; any richer per-tool gating would be introduced here. Generate lean and full variants if both are needed.CI check:
pull_requestjob runs the generator andgit diff --exit-codeon the seed (and the tool manifests). Gotcha — generation must be deterministic (sorted keys, fixed tool ordering) or the diff check flaps on noise.Gotcha — no real plugins folder yet:
src/essence/Tools/**. The discovery +defaultsconvention is a down-payment on the plugin manifest the vision calls for; when a real plugins directory lands, point discovery at it.Implementation questions & gotchas (code investigation, 42b9e0a / 2026-06-18)
A full read of the tool/seed code surfaced these — they reshape how much "generation" actually covers:
config.jsoncarries only a Configure-UI schema + identity + placement — neveron/variables/position. The one place defaults are materialized is the Configure UI (configure/src/components/Tabs/Tools/Modals/ToolModal/ToolModal.js), which fillsvariablesdefaults for only 4 component types (dropdown→options[0], checkbox/switch→defaultChecked, slider→default||min, colorpicker→default). So "compose each plugin's declared defaults" has no source yet — either (a) add a declarativedefaultsblock to each of the 20 tools, or (b) port the ToolModal algorithm to Node (no per-tool work, but inherits the gaps below).text/number/textarray/objectarrayget no default from that algorithm. Anything list-shaped —Card.cards,Sites.sites,Isochrone/Shade/Viewsheddata,Draw's text-default fields — cannot be generated from a schema and must live in the scaffold/hand-authored. Net: generation realistically covers only the scalar on/enum/slider knobs oftools[]; everything list-shaped, plus all oflayers/msv/projection/look/panelSettings, is hand-maintained scaffold.tools.jsis a generated artifact.API/updateTools.jsdoesfs.readdirSync('src/essence/Tools'), reads eachconfig.json, and writessrc/pre/tools.js+configure/public/toolConfigs.json. The generator should run the same dir scan, not parsetools.js(a stale snapshot).configure/add. The seed is POSTed to/api/configure/add, whichdeepmergesconfig_template(shipstools:[Layers,Legend,Info]) with the posted config using array concatenation. So the seeded DB ends up with duplicate Layers/Legend/Info entries — the seededmmgis_golden≠ the committedbaseline-mission.json. A regen-and-diff CI must diff against the committed file, not the DB; if we ever want file==DB, switchadd()to a replacingarrayMerge.readdirSync+ atoolbarPrioritysort that defaults to1000(ambiguous ties) — sort explicitly; (2) the committed file holds the literal{{MAPBOX_TOKEN}}placeholder (substituted at seed time) — compare the placeholder form; (3) MMGIS files are CRLF — a generator emitting LF explodes into a whole-file diff; match line endings.MapControlis a phantom. The current baseline has aMapControltool entry but there is nosrc/essence/Tools/MapControldir and noMapControlToolanywhere. Under "no manifest = excluded" it silently drops — a behavior change. Open question: dead config, or a tool expected to arrive as a plugin?Kindsis not a real tool (never a missiontools[]entry) — the generator must skip it.add()overwritesmsv.mission/missionFolderNameto the posted mission name (those seed fields are don't-cares), storesConfig{mission, config, version:0}, and does not runvalidate()/populateUUIDs()(those are inupsert()only) — a generated config doesn't strictly need to pass validation to seed, but should to survive later edits.Open questions to resolve in this issue:
defaultsblock (migration across 20 tools) vs porting the lossy ToolModal algorithm — pick the source of truth for tool defaults.add()to replace arrays so file==DB.MapControl— drop it, or keep it in the scaffold until it's a real plugin.