shape-system
The Shape System — the content_ir kind registry for structured content. Use when adding, activating, or rendering a kind/shape, writing `__kind` JSON, kind schemas or samples, XML-tag/fence detection
SKILL.md.claude/skills/shape-system/SKILL.md
GitHubShape System — how to add to it (and never around it)
<!-- rules:stamp -->Rules from
common-docs/systems/architecture/content-ir/FEATURE.md— edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).
- Every schema declares
__kindat every object position:const+requiredwhere the identity is known, and declared but optional everywhere else. The only exceptions are theboolean/numberscalar roots and thejsonkind. BEFORE triggers guarantee this onkind_definition,kind_example,kind_instanceandagent.definition.output_schema(wf_066). Never add a write-time check that rejects an unmarked schema, because the trigger fixes it. - Never strip
__kindand never loosen a schema to fit the data. Bring the data up to the schema withcontent_ir.stamp_markers_from_schema. Never reintroduceinjectKind: false, and never hand-patchemitted_json_schema, because it is emitted from the kind's source. Guards: aidreamscripts/check_kind_marker_law.py --liveand matrx-frontendpnpm check:kind-marker-law. __kindis the first key. An unfenced bound-agent stream is typed from its first key (block_detector.root_kind_declaration).jsonbreorders keys, so SQL that mints an instance builds it as text/jsonand reads the row back.- An agent is never asked for plain JSON. A structured result is a kind instance or it is markdown.
- A registered kind is not a payload with a
__kindkey.content_ir.evaluate_kind_activationchecks that its example validates, it has a real output component, and its schema is unique; the result is advice shown beside the switch (warnings), never a refusal (rule 24). The generic key/value view is not a component; activation changes go throughset_kind_activation. - Machine contracts are quarantined by residence. They live in
content_ir.io_contract. The family namesaction_io,tool_ioandagent_ioare reserved and never appear on akind_definitionrow.workflow_iois a pinned slug set, anddata_onlyis informational only: it never removes a form. Guard:scripts/check_kind_family_law.py. directive_vis a reserved prefix. No hand-authored or user kind may start with it (enforced in@kind, kind authoring and the shape creator, and checked byscripts/validate_kind_directive_registry.pyat zero). Capability is derived from the directive class and never stored.- The position law has one home:
matrx_graph.content_ir.directives. Onlycreate,update,deleteandactionexecute, and only at an agent's output root. Onlyreferenceandsecretresolve inside content. A side effect found in content renders as a button and runs only on a human click. - The directive ledger key is
sha256(namespace·type·item)withkindexcluded. Changing howtypeis derived from the slug re-applies every past action. Keep"matrx"inMATRX_OWNED_BLOCK_TYPES, never seed akind_definitionrow per directive, and never attach__irto amatrxblock, or directives render as raw JSON. - Shared logic lives once, in
aidream/apps/shared/content-ir-core(kernel) andcontent-ir-react(route, resolver, shared views). No consumer keeps a local copy of a parser, a route decision or the directive decoder. The kernel refuses React, and the render layer refuses Next.js, Redux and Supabase. Consumers ride the npmlatesttag, and versions live in each package's CHANGELOG. - Detection is identical in Python and TypeScript. A container never hides a kind: when a region closes, balanced JSON objects with a non-empty
__kindbecome their own instances, in document order. XML owns its body until its root closes, and a```matrxfence is a container. Shared conformance vectors pin markdown-fence nesting. A change to one runtime lands with the other and its vectors. - A streaming partial follows the partial wire contract. Partials ride
metadata.__ir_partial. Thestateispartial,supersededorretracted, and exactly one terminal state ends every partial, so a skeleton can never stick. Every partial is closed into valid JSON. The key is absent on most events: consumers carry state forward, never reset on absence. - Runtime wrappers delegate, never reimplement.
node_outcomerenders its inner kind through the ordinary route. Elided payloads ride once, throughoutput_ref. - A skipped check is never a pass. A degraded verdict carries a named
KindDegradedReason. Onlycatalog_unreachableis a loud skip,KindContractViolationis non-retryable, and an invalid typed output is never stored as a success. - Enforcement is the code constant
matrx_graph.kinds.KIND_ENFORCEMENT, never an environment variable. The DBadmission_configratchet coexists with it on purpose. - Undeclared keys never sit at a payload's root. They go in a declared map (for example
extras). - A primitive binds; it does not get a new component. A shape that maps onto an existing primitive binds through the primitive plus
props_transform. Block-type string values, routes, dispatch outcomes and stored content bytes are frozen, and a rename that changes one is a defect. - Every emitted kind declares a disposition. A kind with none gets no storage and files an
ops.system_errorrow (kind_disposition_undeclared). New kind data lands in records, never incontent_ir.kind_instance. - A missing or broken component is replaced at once and reviewed afterwards. The replacement shows a "just built" mark and a one-click way back to the plain data view. This never replaces a working, registered component without review.
- Reuse before you mint. Well-known kinds (web results, citations, address, rating) are reused through provider translation functions, never re-minted under a new name.
metadata.maturity = verifiedis earned only by the verification pass. - Every active shape has a skill, and a hand-written one is never overwritten. Generated skills come from the non-AI template (
kind_create_skill) and carrymetadata.created_via; a weekly refresh regenerates only those. A shape with no description gets a concise one from a weekly batch run on a fast model. - Shapes picked in chat: up to a knob (default 10) go to the model in full; past it, the model gets each shape's name and description and loads the ones it uses. Picks travel as
output_kinds(kind slugs) on start, chat and continue requests, never as skill ids; an omitted field means the chat's saved picks (chat.conversation.metadata.run_configuration.outputKinds). The server alone resolves shape → skill (aidreamservices/tooling/output_kinds.py, knobchat.output_kinds.full_body_max). - An agent locked to a structured output schema answers in its own shape; a different picked shape is never an error. The picker warns at the moment of selection, and a saved conflicting pick shows the same warning; the server drops picks the agent cannot honor and sends a visible warning (
output_kind_locked), never a failed run. The agent's schema is never modified per request. - Every kind, kind component, surface and converter is born ACTIVE, from every screen, tool, seed script or automation. Switching one off is a deliberate act; turning one on is never refused. Never write
is_active=Falseon create and never add a check that blocks activation or disables the Activate button. - A streamed shape's render state is a one-way ratchet: markdown text → JSON found →
__kindfound → kind resolved and its component rendering. Once forward it never goes back (no re-mount, no generic or text flash) because a chunk is partial, unparseable or fails validation; hold the last good render and fall back only on strong, repeated evidence (the stream ended and the final value is invalid). Mount count stays 1 per block; test with token-by-token replay including mid-key and mid-__kindsplits. - Rich content helps, never hinders. Every surface offers copy as markdown and copy as plain text one click away; formatting buttons work in every mode including raw markdown; editing happens in place. Replacing a UI never ships without full parity verified by screenshot in EVERY mode, and "done" means a clipboard capture plus a screenshot of the visible button, not a green guard. Judge each surface from the user's seat (copy, select, format, edit in place, every mode, window panels, phone) and audit every surface for consistency; copy never puts raw
<artifact>tags on the clipboard. Every rename or cleanup ships with its full inventory (tools, routes, DB, tokens, copy, docs, tests) and is done 100%, never half-way; the domain tree wins on names and placement. The basics come before any side work, and when Arman says "focus on my vision" every other lane drops. - A kind's name and schema are ONE shared contract per slug; how it looks layers user → org → system, most personal wins. Anyone fixes a bad render for themselves in one click, org admins for their organization, and super admins get a link to the admin pages for a global change (never admin power on a user route). The fix ladder starts at "kind not registered".<!-- rules:stamp-end -->
Glossary: "Shape" = product name; "kind" = the technical noun (content_ir.kind_definition.kind). One kind = one named, versioned structure that every platform (web, Python, workflow-studio, React Native later) validates and renders against.
Operating doc (read it): features/content-ir/docs/SHAPE_SYSTEM.md — rulings, posture, roadmap. FEATURE.md invariants: features/content-ir/FEATURE.md.
The model in three lines
- A Shape carries 7 assets: schema · skill(s) · content block · output component · input component · samples (
kind_example) · pydantic mirror. - Every input surface (
__kindJSON, XML tag, fence, tool result) converges to canonical__kindJSON at DETECTION (kind_surfaceis the one list). Below that boundary everything is format-agnostic by construction. - Consumers: agent outputs, workflow node I/O (live), tool results (Stage 7), custom apps.
Adding a kind (the paved road)
- Schema — insert the
kind_definitionrow (fields indata[]for ts-owned;emitted_json_schemaalways; org = system org39c38960-d30c-4840-b0c1-c9960de95582for platform kinds,visibility='public'). Python-owned kinds derive the schema from the pydantic model:Model.model_json_schema()— never hand-write what a model can emit. Idempotent SQL, applied via Supabase MCP, ledgered inpublic._schema_migrations. - Sample — one
kind_examplerow per kind@version (is_canonical=true). Never writevalidation_statusyourself — thekind_example_recompute_validationtrigger DERIVES it on every write, so a fabricatedpassedis both impossible and a defect. Helper:pnpm shape:sample <kind> --file|--stdin [--apply]. - Component — register the web component. Two tiers, both satisfy the render leg: compiled (
kinds/<slug>.ts→ append toSYSTEM_KIND_DEFINITIONS→ register the block type inSHAPE_BLOCK_DISPATCHinaidream/apps/shared/rich-content/src/display/chat-markdown/block-registry/block-dispatch.tsx; never add a switch case) or DB-authored (asource='db'kind_componentrow — no repo change, no deploy; what the creator agent writes). A bundled row'scomponent_keyMUST resolve inblock-dispatch.tsx—pnpm check:shapes:componentsis a blocking release gate, because a dangling key changes nothing at runtime (the compiled bridge routes anyway) while the registry claims coverage. A kind without either stays inactive and renders the generic viewer (correct, not a failure). - Skill + content block — one skill per kind per syntax (
kind_<slug>JSON /kind_<slug>_xml); skill bodies teach the REAL parser failure modes. Blocks pair with the skill under the Agent Skills category (two per skill: simple + complex), linked viaplatform.associations. Coexist-not-clobber: never overwrite a live legacy block; use a-kindsuffix during transition. - Surface — ONLY for a non-JSON arrival form (XML tag / custom fence language).
__kindJSON needs NO surface: the parser detects it natively in a ```json fence. A row is inert without itsparser_strategyimplemented insurfaces/xml-finalize.ts#SURFACE_PARSER_STRATEGIES, so registering one without the parser mints a phantom row (8 suchjson_root_keyrows already exist — do not add a ninth). After inserting, runpnpm check:shapes:surfaces:refresh— the compiled bootstraps in BOTH repos are generated fromkind_surface. Never hand-add a tag/fence literal tostream-block-accumulator.ts/content-splitter-v2.ts/block_detector.py. - Activate —
content_ir.set_kind_activation(id, true)is the ONE write path foris_active; it runs the dual gate (structural leg on the sample + render leg on the component) and raises with the specific missing asset. Surfaces: the owner control on/shapes/[kind], or thekind_activateagent tool. This step is not optional — an inactive kind renders through the generic viewer and, critically,isKindBindablerefuses to bind it to an agent's structured output. The admin Gate tab (/administration/kind-registry/<kind>?tab=gate) shows the verdict read-only and never writes. - Coverage gates — a new kind slug or surface token that no crosswalk rule claims FAILS the run.
pnpm check:shapes:crosswalk:refresh, thenpnpm check:content-ir:strict(crosswalk + aidream twin +check:shapes --strict).
Workflow node I/O (live)
NodeSpec.output_kindis MANDATORY for non-dynamic nodes (structural kind for stable shapes; genericjsonminimum).input_kindgates inputs pre-execute. Per-node authored overrides:data.input_kind/data.output_kind. Readpackages/matrx-graph/docs/node-authoring.md§kinds (aidream repo) before adding a node.- Runtime:
matrx_graph.kinds.get_kind / validate_against_kind(loud-fail-open). After ANY kind registry mutation callinvalidate_kind_catalog_cache()(and remember the FE warm registry re-reads on its own cadence). - LLM binding:
matrx_ai.kinds.response_format_for_kind(slug)→ strict portable response_format. Unknown/unportable kinds decline loudly withNone.
Buttons, AI runs and saved results inside a shape
A kind component receives runAction and itemState beside data: it can run a shortcut
on one item (in a window, or in the background saving the result onto the item), keep a
person's pick, and show saved results and generated images. How: the shape-authoring
skill. Never answer "a shape can't trigger an agent."
Hard rules
- One registry. Never a parallel kind list, detector, sample store, or component map. Extending
content_irIS the feature. - Verify live, never trust reports — after any DB write,
execute_sqlthe counts; after any render change, a runtime marker or screenshot. - XML is not legacy — and neither is anything else named "legacy" in this feature. Every
*-legacy-text.ts, every*_legacy_textstrategy key,legacyBlockType, andtoLegacyServerDatais LIVE code carrying migration-narrative vocabulary that froze into identifiers.legacyBlockTypein particular is THE render key —kind-route.tssetsblock.typefrom it andblock-dispatch.tsxroutes on it; nothing renders without it. Never read the name as permission to delete or "modernize". Rename pending (~100 files + 23 DB rows + an aidream generated file). - Render-leg satisfier order is load-bearing. The compiled-bridge check runs FIRST because it actually exercises the bridge; the resolved
kind_componentrow is the last resort. Reversing them short-circuits the "No<kind>available" guard for every compiled kind. Pinned by tests — do not reorder. - Matrx Directives (MatrxEnvelope) is off-limits except the collision guard and its
kind_surfacerow. Its invariants are listed in SHAPE_SYSTEM.md. sample_dataon kind_definition is interim — new samples go tokind_example; never add new readers ofsample_data.- DB changes follow the db-change skill (MCP apply → ledger →
pnpm db-types→ aidreampython db/generate.py→ live verify → both repos commit).
Definition of done
The doctor SHIPPED — pnpm check:shapes generates features/content-ir/docs/SHAPES_STATUS.md (8 asset columns per kind) and scripts/shape/shapes-status.json. Your kind's row is green and a preview screenshot exists.
Read the report's n/a doctrine before "closing gaps": a nested_only_child (renders only inside its parent) and a data_only contract kind are STRUCTURALLY exempt from component/skill/block/surface. Those cells are derived, never declared — building assets for them is wasted work, and the doctor will keep marking them n/a.
A fabricated validation_status='passed' is a defect; so is an is_active flipped by anything other than set_kind_activation.