Sign in to edit tickets from this page.

← all tickets · home

One scenario identifier: underscore grammar, enforced at every boundary

superseded 8d9e1587-abaf-4b03-92ce-f808c15a72a1

created_at
2026-04-27
updated_at
2026-04-27
priority
P3
ticket_type
bug
labels
substrate, mcp, ergonomics
resolved_at
2026-04-27
superseded_by
38d0ba4e-d2f6-4945-b211-037615db8957

Body

HOLD — DO NOT PICK UP UNTIL HUMAN AUTHORIZATION.

Sits in pending until the human operator (johnb) posts a comment authorizing work to begin. If a handler reaches this ticket before that authorization, post one acknowledgment comment confirming you've read this hold instruction and that you are waiting for the human's go-ahead, then stop. Do not branch, do not read code, do not draft a plan. The human will return to either authorize, defer, or rewrite.

Spec

A scenario has exactly one human-readable identifier. That identifier follows the underscore-only grammar ([a-z0-9_]+). The substrate enforces this grammar at every entry point. Divergence between manifest field and name binding is unrepresentable in the code — there is no path through which a single scenario can carry two distinct human-readable identifiers.

The scenario_names table goes away. The manifest's scenario_slug is the canonical and only human-readable identifier. Lookup by name is lookup by slug; they are the same thing.

What this means concretely

Substrate. The scenarios table keeps scenario_slug. The scenario_names table is dropped. Any column or struct field whose semantics were "name binding distinct from slug" is removed.

Grammar enforcement. Scenario assembly accepts only [a-z0-9_]+ for scenario_slug. Hyphens are rejected with a structured grammar error. No silent coercion, no normalization helper, no "try the other way" fallback path anywhere in the code.

MCP surface. set_scenario_name and unset_scenario_name are deleted (the table they manipulate no longer exists). get_scenario({ name }) becomes a direct lookup against scenarios.scenario_slug. get_scenario({ hash }) continues to work unchanged.

World output. The world's scenario_label field continues to surface the slug. The MCP output's "scenario": "<slug>" and "scenario_label": "<slug>" fields remain. Now they're both the actual canonical name a caller can pass back into get_scenario({ name: <slug> }).

Graph browser. The /scenarios/name/:name route works against the slug. Catalog reference rules for scenario_name / names.[*] either remain pointed at the (now-canonical) slug, or get repointed if any of them assumed the names-table existed. The Phase J catalog contract test (introduced by 04d1b392) continues to pass.

Existing data. All current scenarios, name bindings, worlds, attempts, turns, audit events, and execution provenance get blown away as part of the migration. No reconciliation logic, no preservation of existing forensic artifacts. The substrate is young; the data is ephemeral; the cleaner spec wins.

Acceptance

  1. The scenario_names table is dropped via migration. The migration also drops or truncates the dependent rows in any table that referenced it.
  2. The grammar [a-z0-9_]+ is enforced at every scenario-slug entry point. Hyphens, uppercase, and other grammar violations return a structured error. Round-trip tests confirm valid slugs accept and invalid ones reject cleanly.
  3. get_scenario({ name: <slug> }) returns the scenario with that slug. get_scenario({ name: <hyphen-form> }) returns UNKNOWN_SCENARIO (the hyphen form is grammar-invalid, but the lookup error path should be the same — the scenario simply doesn't exist).
  4. set_scenario_name and unset_scenario_name are removed from the MCP surface. Their handler code, their request/response types, their tests, their call sites are deleted.
  5. Discipline check. Searching the post-ticket codebase for normalization helpers, fallback logic, or "try the other way" patterns returns clean. The grep terms normalize, fallback, try_alt, coerce, applied within the scenario-name codepath, return zero hits unrelated to other concerns.
  6. Graph browser regression check. The catalog contract test from 04d1b392 Phase J passes. A fresh authenticated walk of the operator-shaped traversal completes cleanly.
  7. Live verification. After deploy, an operator creates a fresh scenario via the existing assembly path, creates a world from it, and reads get_world({ slug }). The "scenario" field in the output, passed directly back into get_scenario({ name }), returns that scenario.
  8. Substrate tests pass on the sacrificial sidecar at DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5433/postgres.

Out of scope

Sequencing

Independent of 4601f21a and 2dc48e22. Picks up whenever convenient.

Related

History (2 events)

Sign in as a human to drive this ticket from the page, or use the MCP tools.