semantius-modeler Skill
This skill is the executor of the three-skill workflow:
semantius-architectproduces the blueprint (entity-level, platform-agnostic).semantius-analystreconciles the blueprint against the live Semantius catalog → produces the spec (field-level, with explicit reconciliation annotations on every owned entity).semantius-modeler(this skill) takes the spec → executes the deploy.
Division of responsibility:
- The analyst owns the catalog gatekeeping: collision detection, classification, merge / rename / promote decisions, optional-entity selection. All of that is baked into the spec by the time the modeler runs.
- This skill owns the execution workflow: parsing the spec, verifying the spec's reconciliation annotations still hold, rendering the plan, orchestrating writes, verifying, and optional sample data.
- The use-semantius skill owns the low-level operations: all Semantius operations are done via the
semantiusCLI tool, following that skill's patterns and reference docs.
Writing conventions (apply to every output this skill produces)
These rules apply to chat output, plan summaries, verification reports, and anything else this skill writes for the user to read. They are not optional style preferences. They do NOT apply to data the deployer sends to Semantius — model text (entity descriptions, field descriptions, JsonLogic, enum values, rule messages, etc.) is the user's data and is governed by the "Data fidelity" section below. Never apply em-dash rewrites, US-spelling fixes, or any other house-style edit to a payload bound for create_entity / update_entity / create_field / update_field / create_permission. The model's content travels untouched into the catalog; the deployer's prose styling stays in chat.
1. US English spellings, always. Never British English. Examples that come up often (left = correct US form, right in backticks = banned British form): optimize (not optimise), behavior (not behaviour), modeling (not modelling), customize (not customise), recognize (not recognise), labeled (not labelled), materialize (not materialise), organization (not organisation), summarize (not summarise), categorize (not categorise), uncategorized (not uncategorised), normalize (not normalise), harmonize (not harmonise), analyze (not analyse). When in doubt between two spellings, pick the -ize / -or / -er form.
2. No em-dashes (—, U+2014). Banned as a parenthetical break or "and" substitute. Replace with: X — Y parenthetical → X (Y) or X, Y; X — but Y contrast → X. But Y. or X; Y; A — B — C triplet → split into two sentences. The en-dash (–) and hyphen (-) are fine in number ranges and compound words; the ban is specifically on — used as punctuation. Before writing any file or assistant message, scan for — and convert each instance.
3. Singular-subject grammar in confirmation prompts. "Looks good?" not "Look good?"; "Sounds right?" not "Sound right?". Use the form that agrees with the singular implicit subject; avoid colloquial elided-auxiliary forms in written text.
4. Semantius entity-label symmetry. When this skill writes about or proposes entity labels: singular_label is the bare singular noun matching plural_label. ✅ Product / Products. ❌ Product Name / Products. Field-level titles like "Product Name" go on the auto-created label field's title via update_field (the deployer corrects this only when the platform-derived title differs from the §3 Label; see the label-column title note under "Fields That Are Auto-Generated"), never on the entity's singular_label.
5. Plain language in every user-facing surface. Anything the user reads — AskUserQuestion widgets, plan summaries (rendered before each Execute pass), the cross-model-links prompt (when there are 4+ proposals), the final yes/no pre-execute confirmation, verification reports, the closing message, every chat status update along the way — is written for someone who has never opened a spec file. The user is a domain expert (HR director, ATS administrator, operations lead), not a data modeler.
This convention covers two surfaces equally:
- Surface A:
AskUserQuestionfields and plan/verify summaries — anything assembled into a structured block the user reads. - Surface B: every other thing the user sees in chat — status updates ("Verifying the spec against your semantic model..."), progress reports, the closing message ("Applied
ats-candidate-crmto your semantic model. 14 writes.").
Both surfaces follow the same ban list and the same "required" list below.
Banned in any user-facing surface:
- Section references:
§1,§3,§6,§7.1, "the model's §...", "spec's §...". Describe what the section is instead — "the entities list" (not §3), "the cross-module connections" (not §6). - Reconciliation annotation values as words:
reuse-from,rename-incoming-from,promote-to-master,dropped,create-new. - File-format / pipeline terms:
spec,blueprint,frontmatter,manifest,annotation,reconciliation,reconcile, "the spec carries". Where naming the artifact is unavoidable in a status message, use plain English ("the file" / "this design"). "Reconciliation" is the analyst's internal name for its own work — never narrate it back to the user; say "the analyst's pass" / "the planning step" / "what was decided about reuse" instead. - Platform-internal names for the deployed state:
the catalog,the live catalog,catalog drift,the model in the catalog. The user-facing name isyour semantic model(or "your live semantic model" when emphasizing what's deployed). "Catalog" is implementer vocabulary; "semantic model" pairs with the modeler skill name and reads as a coherent system in the user's hands. - Architectural jargon:
gatekeeper,data silo,embedded master,consumer role,contributor role,mastered_in,module_type,classDef,platform_builtin,cross-model link suggestion(use "connection to another module"),additive optional column(use "an optional link"). - Raw identifiers when a display name exists: prefer entity Plural Labels (
Candidates) overtable_name(candidates), and module display names over slugs. Backticked snake_case tokens are a leak even in status messages. - Calling an entity a "field," or calling entities "records." Keep the three nouns distinct in everything the user reads. An entity (table) is a type of thing the module keeps — name it by its Plural Label (
Asset Contracts), or collectively call them tables / record types, never "fields" and never bare "records." A field is a single column / detail on a record (annual cost) — only columns are "fields." A record (row) is one saved item; reserve "records" / "rows" for actual data rows (e.g. sample data). So the deploy contract reads "Create 5 tables and their fields" (or "…and their details"), ✅ — not "Create 5 records and their fields" ❌ (that calls entities "records"), and never "create the fields" when the tables/entities are meant ❌.
Required in any user-facing surface:
- Entity Singular / Plural Labels from the spec (
Candidates,Skill Profiles). - Module display names (read from
module_nameviaread_modulewhen slugs aren't friendly). - Plain verbs: connect, link, add, skip, deploy, update.
- Plain consequences: one optional link added between two modules, can be removed later by editing the design, will resurface next deploy unless removed from the source.
The internal annotation values (reuse-from, promote-to-master, etc.) still get read from the spec by the parser — only what the user sees changes. Plan-summary icons (🟢 / 📥 / 🆎 / ❌) are fine as visual cues; the accompanying text is what needs to read plainly.
Pre-emit check (mandatory): before sending any chat message, before firing any AskUserQuestion, before printing any plan or verification summary, scan the assembled text for any banned token. Rewrite before sending.
Narration restraint. Plain language is necessary but not sufficient. Volume matters too. The user did not ask for a narrated walkthrough of the deploy; they asked for a deployed module. Hard rules:
- Do not announce what you're about to do before doing it. No "Let me verify the reconciliation annotations...", no "Let me check the live catalog...". Just do it.
- Do not narrate self-corrections mid-flight; fix them silently.
- Plan summary is one block, not a running commentary. Render the plan once before execution (per Stage 3), then execute silently. Do not narrate every entity / field / permission creation in chat — the tool-call lines in the transcript are the receipt.
- Verification (Stage 5) runs and prints its structured report at the end; nothing else along the way. No intermediate "verifying X..." updates.
- Close-out is the Closing Contract, nothing more. The final message is the three-part call-to-action defined in "Closing Contract: clean and sticky" below (a status line, a clickable
[Open <System Name> in Semantius →](<ui_baseurl>/<module_slug>)link, and the sample-data question). It is not a paragraph, not a recap, not a list of every operation, and never a/semantius:*slash command in place of the link. Detailed counts, reused built-ins, and caveats live in the Stage 5 verification summary ABOVE the closing block, separated by a---.
A useful test: "if I deleted this chat message before sending, would the user notice anything was missing?" If the answer is "no, the work still got done", delete the message.
Data fidelity: model text is user data
Every string the deployer extracts from the model and sends to Semantius (description, singular_label, plural_label, title, JsonLogic message / description cells, enum value labels, permission descriptions, select_rule and input_type_rule JsonLogic, computed_fields / validation_rules arrays) is user data, not deployer prose. It travels into the catalog byte-for-byte unchanged. The rules below are not stylistic preferences; they are correctness invariants. A deploy that violates any of them produces silent catalog drift the user cannot see until they read the record in the UI.
1. No truncation. Ever. Entity and field descriptions in the model are often multi-sentence (3–6 sentences is normal for entities like service_requests, incidents, change_requests). Every sentence is part of the meaning — typically sentence 2+ encodes invariants, lifecycle rules, terminal states, and gating constraints. Sending only the first sentence loses that information. Read the full description through to the next blank line / next **...** heading / next markdown structural element, and pass the entire span. If the description spans markdown paragraphs, include the blank line and the second paragraph. Do not summarize for "brevity," do not paraphrase, do not synthesize a shorter version.
2. No normalization. The model's text passes through verbatim. Specifically:
- Backticks (
`) around enum tokens, table names, status values stay backticks. *Do not* strip them. They render as inline code in the UI and carry semantic emphasis ("the valueretiredis terminal"). Stripping them turns the prose into "the value retired is terminal" which reads as a different sentence. - Apostrophes (
') in possessives (team's,user's,incident's) stay apostrophes. Do not delete them, do not convert to "smart" quotes, do not rewrite the possessive. - Em-dashes (
—), if the model contains them, stay em-dashes. The Writing Conventions ban on em-dashes applies to deployer chat output only. - Quotes stay as the model wrote them (straight
", curly"/", doesn't matter — whatever is in the source byte-for-byte). - Unicode characters stay. The platform stores UTF-8; the model is UTF-8; no transliteration is needed.
3. Shell-safe transport for any text containing special characters. Backticks, apostrophes, double quotes, dollar signs, multi-line content, and Unicode all break inline shell-arg quoting in subtle ways:
- Double-quoting the JSON (
"{...}") makes bash evaluate backticks (`cmd`) as command substitution. Disastrous. - Single-quoting the JSON (
'{...}') breaks the moment any value contains a single quote / apostrophe. - Escaping is fragile and easy to get wrong field-by-field.
- Heredocs (
<<'EOF') inside an inline Bash invocation are NOT enough. The agent harness transports the entire Bash command as a string through its own quoting layer; an apostrophe inside a heredoc body can still trip the outer parser before bash ever sees the heredoc as a heredoc. Heredocs are safe inside a file that bash then reads, not inside a command argument bash is being told to evaluate.
Canonical pattern: write a script file with the Write tool, then run it. This is the only form that fully decouples the model's text from any shell quoting layer. The script file is opaque bytes to the harness; the runtime reads it from disk and parses string literals locally.
Use Bun (TypeScript), not Python. Bun is a native cross-platform runtime — the same .ts file runs identically under PowerShell, Git Bash, macOS, and Linux without path-mapping or interpreter-shim issues. Python is forbidden in this skill: Windows python3 may not be on PATH, /tmp/ resolves differently between Git Bash and Windows-side Python, and subprocess piping behaves differently across shells. Bun avoids all of that.
// Write tool target: <cwd>/.tmp_deploy/deploy_xxx.ts (see path note below)
async function call(tool: string, payload: unknown) {
const proc = Bun.spawn(["semantius", "call", "crud", tool], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write(JSON.stringify(payload));
proc.stdin.end();
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const code = await proc.exited;
if (code !== 0) throw new Error(`${tool} failed (${code}): ${stderr}`);
return JSON.parse(stdout);
}
await call("create_entity", {
data: {
description: "Multi-sentence text with `backticks`, apostrophes (team's), and \"quotes\" — all safe.",
},
});
# Shell: just runs the file, no inline content. Bun reads the .ts source directly.
bun run <cwd>/.tmp_deploy/deploy_xxx.ts
The model's text lives inside a TypeScript string literal in a file on disk; it is serialized to JSON by JSON.stringify (which never strips backticks, apostrophes, em-dashes, or Unicode); the JSON is fed to semantius over stdin as raw bytes by Bun.spawn. No shell quoting layer ever sees the text.
Inline heredoc is a fallback for short ASCII-only payloads only. When the payload is small and contains no apostrophes, backticks, or Unicode, an inline heredoc is fine:
semantius call crud create_module <<'JSON'
{"data":{"module_name":"ATS","module_slug":"ats","description":"Applicant Tracking System","module_type":"domain"}}
JSON
Other supported transport forms (when the file already exists on disk, e.g. produced by an earlier Write call):
cat /tmp/payload.json | semantius call crud create_entity
semantius call crud create_entity < /tmp/payload.json
Build the payload with JSON.stringify inside the Bun script (as the in-script wrapper above does). For one-off JSON extraction from a pipeline, use bun -e (see the postgrestRequest envelope section below). Never string-concatenate the model's text into a shell-quoted JSON literal — that's the path that forces character stripping to keep the command parseable. If you find yourself trying to "clean" the model text so it fits an inline command, stop, write a .ts script via the Write tool, and run it with bun run.
Cross-platform path note. Bun on Windows is a native executable, not a POSIX layer, so it resolves paths the same way every shell on the box does — no Git Bash /tmp/ vs Windows /tmp/ mismatch. Even so, write deploy scratch files under a folder inside the current working directory (e.g. <cwd>/.tmp_deploy/script.ts), not under $TMPDIR / /tmp/. Two reasons: the user can inspect the file by path if a run fails, and <cwd> is the one path every shell, the Write tool, and the harness already agree on without translation. Add .tmp_deploy/ to .gitignore once and never think about path mapping again. Clean up the file after the run.
This applies to every write call where the payload contains any model-authored text: create_entity, update_entity, create_field, update_field, create_permission, update_permission, anything else that carries user prose or JsonLogic.
4. Each record carries its own complete payload. When iterating over multiple entities or fields whose model declarations look similar (e.g. the four *_comments entities each declare a visibility field with the same description and the same input_type_rule), do not "optimize" by writing one full payload then short payloads for the rest. Every create_field record — every item of the array a bulk call sends — carries every column the model declares for that field, description included, every time. The four comment entities each get their own complete visibility item, each with the full description string. Identical text repeated across entities is the expected case, not a redundancy to eliminate. Bulk calls make this trap easier to fall into, not harder: the server fills an omitted key of one array item from the column default, so an item that "borrows" its description from the previous item silently lands with ''. Generating a batch script that re-uses the first entity's payload as a template and elides "duplicate" keys for subsequent entities is exactly how the service_request_comments.visibility.description empty-string regression happens.
5. update_* calls are minimal. PostgREST PATCH semantics: keys you send are written, keys you omit are left alone. When Stage 4f issues update_field to set data.input_type_rule = <jsonlogic>, the payload contains only input_type_rule — never include description, title, format, or any other column unless the model genuinely declares a drift on that column too. Specifically: the rule-entry's own description field (the analyst's commentary about the rule itself, like "Visibility is editable for the author...") is not the same thing as the field-column's description (the analyst's description of what the column stores, like "Public replies are visible to the requester; internal notes are agent-only"). The rule-entry's description lives inside input_type_rule's JsonLogic-array entry and travels into Semantius as part of that array. It must never leak out to become the field's description column. Two different surfaces, two different meanings, never crossed.
Verification posture. Stage 5's per-entity check (see "Per-area checks") should round-trip every description (entity-level and field-level) the model declared and assert byte-equality with the live catalog value. A mismatch is a Stage 5 defect — quote the diff and offer a retry of the offending write. This is the only way truncation / normalization regressions surface before the user notices them in the UI.
Generated artifacts (scripts, intermediate files)
This skill emits shell and Bun (TypeScript) helper scripts during a deploy (e.g. the bulk seeders described in Stage 5, ad-hoc update_entity rule appliers, batch field creators when a model has many fields). These are ephemeral one-shots, tied to a single model and a single deploy run. They are not skill source.
The deploy script's committed resources are two files: references/deploy-lib.ts (schema-agnostic primitives — the loud write transport, the exit-code-aware read1 / readMany / readIn existence checks, the create-or-read ensure and its bulk twins ensureMany / ensurePairs / createMany (one array create_* call per set of records, ids from a re-read), the Layer-2 postMany / seedEnsureMany for seed rows, and the halting runDeploy harness; knows no column names, so it never changes) and references/scaffold-lib.ts (schema-coupled and version-stamped — the baseline-scaffold builder scaffoldModule(), one array call per row kind, plus the live-schema preflightSchemas guard). Copy both into .tmp_deploy/ and import them; never re-implement the primitives or hand-roll the baseline scaffold in each script. references/deploy-script-template.md shows how to assemble the bespoke orchestration around them. A script that wraps writes in a bare catch and continues will report success over a partial deploy — the exact failure the "Failure is loud and halting" invariant exists to prevent.
Use Bun, not Python. Any helper that needs more than trivial shell logic — JSON construction, response-envelope unwrapping, capturing IDs across many POSTs, conditional logic over the live catalog — is a .ts file run with bun run. Python is forbidden: Windows installs don't reliably expose python3 on PATH, virtualenv state pollutes the project, and the Git Bash vs Windows-side /tmp/ split makes script paths unreliable. Bun is a single native binary, installs once, runs the same on every platform.
Where they go:
- Always under the current working directory in a scratch folder, e.g.
<cwd>/.tmp_deploy/deploy_<short>.ts(or.shfor the rare pure-shell seeder).<cwd>is the one path every shell, the Write tool, and the harness already agree on — no translation, no surprises. Add.tmp_deploy/to.gitignoreonce. Delete the file after a successful run. - Do not write to
$TMPDIR//tmp//$env:TEMP. Those paths resolve differently between Git Bash and Windows-native runtimes, and the user cannot inspect them by path if a run fails.
Where they must not go:
- ❌ The skill folder (
.claude/skills/semantius-modeler/). The skill folder is read-only at runtime; only the maintainer edits it. Never leak deploy scratch files here. - ❌ The user's working directory. Pollutes the project, surfaces in
git status, and survives across sessions. - ❌ Any path under the model file's directory. Same reasons.
Cleanup: Delete the scratch file after a successful run with rm (Unix / Git Bash) or Remove-Item (PowerShell). If the run fails, leave the file in place and report its path so the user can inspect — under <cwd>/.tmp_deploy/, never in the skill folder.
This applies to every script this skill writes, not just the seed script at Stage 5.
Schema compatibility: EXPECTED_MAJOR = 5
This skill expects spec files written by semantius-analyst major 5. The spec file's front-matter version: "MAJOR.MINOR" is checked at the start of Stage 1. Major must equal EXPECTED_MAJOR, minor is informational and not compared. Files with a different major are rejected with the message:
"This spec is for analyst v<N>; you have modeler at EXPECTED_MAJOR=5. Re-run
semantius-analyston the source blueprint to regenerate the spec."
A file lacking reconciliation annotations on any entity (the v3.x format) is rejected with the same routing message. The modeler trusts the spec; it does NOT classify entities itself.
The spec also carries blueprint_version (the blueprint artifact version the analyst worked against; default "3.0"). The modeler does not re-validate it against the architect; the analyst did.
Cross-entity JsonLogic primitives (set_record, let, throw_error) are passed through byte-for-byte inside validation_rules / computed_fields and (with care) select_rule. The "column must exist on this entity" parse check skips column references qualified by a set_record / let binding (the bound variable's columns resolve against the bound entity). See references/conflict-resolution.md.
Permission-prefix resolution rule (the "entity-owning-module rule"). Workflow gates and row-scope overrides for entity E are prefixed by E's CURRENT owning module slug, not by the installing unit. The rule fires on every install regardless of module_kind. Stage 4a-scaffold honors it when minting gates / overrides for entities with re-prefixed-from annotations; Stage 4n handles the master-install reconciliation when a Branch-B promotion moves an entity to a new owning module (sweep every non-catalog-prefixed permission for the entity's verbs, mint sibling catalog-prefixed permissions and role_permissions rows, re-emit hierarchy edges; no deletes, per the no-auto-deletion rule).
The history of the deployer's contract changes lives in CHANGELOG.md — what each analyst-lockstep bump changed in the deployer's parser, stage numbering, and audit checks. That file is not loaded at runtime; the body of this SKILL.md is the current contract, the CHANGELOG is the history.
- Older major (e.g. file is
"0.x", this skill expects"1.x"), the file was written by an older analyst version using a structure this deployer no longer understands. Tell the user to run the analyst skill; its archived-knowledge mode reads the older file and re-authors a current-major file from the same semantic content. - Newer major (e.g. file is
"2.x", this skill expects"1.x"), the file was written by a newer analyst than this deployer knows about. Tell the user to update this deployer skill before retrying. - Missing
versionkey (legacy, pre-versioning), treat as major0; same response as older-major above.
Your role: thin executor of a reconciled spec
The analyst is the gatekeeper. The modeler executes.
Hard prerequisite before any write: Step 0. Everything below is the workflow (what to deploy, in what order). How each write actually talks to Semantius — response shapes, the exact field column names, nullability rules, the
update_fieldid format, the Golden Rules — lives in the use-semantius skill you load at Step 0 below. Step 0 is a gate you pass through before issuing a singlecreate_*/update_*, not a "read it if you get stuck" reference. The single most common way this deploy fails is authoring a Bun script straight from the spec without loading use-semantius first, then tripping over column names and response shapes that Step 0 documents. A condensed safety-net cheat table lives in Step 0 too — but it is a backstop, not a substitute for reading the files.
Semantius is a unified platform, a universal system of records. The analyst has already done the catalog-gatekeeping work: collision detection, similarity heuristic, merge / rename / promote widgets, optional-entity selection. By the time the modeler runs, every entity in the spec carries an explicit **Reconciliation:** annotation:
| Annotation | Modeler does |
|---|---|
create-new (default, also omitted line) |
execute create_entity + all fields in the spec |
reuse-from <module>.<entity> |
skip create_entity; read existing entity for FK targets; the spec has no Fields block for this entity (or has only an **Additive fields** block, applied via create_field) |
rename-incoming-from <module>.<entity> as <new_name> |
execute create_entity under <new_name> (analyst already chose the disambiguating name) |
promote-to-master <master_module>.<entity> |
execute create_entity in the master module (not this domain module); add cross-module permission inclusions per spec frontmatter promotion_decisions |
dropped (optional, user declined) |
skip entirely; no writes |
The modeler does not: classify entities itself, detect cross-module collisions, run a similarity heuristic, drive AskUserQuestion widgets for catalog decisions, prompt the user about optional entities. All of that lives in the analyst.
The modeler's only catalog-inspection job is a thin pre-flight verify (Stage 2): every reuse-from <module>.<entity> still resolves to a live entity; every rename-incoming-from source still exists; every promote-to-master <module>.<entity> target master module is present. If pre-flight fails, halt and route the user back to the analyst — your semantic model has changed since the planning step ran; re-run semantius-analyst to refresh.
This skill is designed to be re-run whenever the spec changes. Because the analyst re-runs against a fresh catalog snapshot whenever the user invokes it, and because the spec carries reconciled_against_catalog_snapshot in frontmatter, the modeler can detect "spec is older than current catalog state" and refuse cleanly.
The spec's entity decisions are fully resolved. No fuzzy matching at the modeler level for owned entities: every §3 entity carries an explicit **Reconciliation:** annotation and every in-model FK target is fully-qualified (<module>.<table>). The §6 cross-model link rows are the one exception — they are deliberately unresolved hints (From | To | Verb | Cardinality | Delete, no module prefix on To); the modeler resolves each To against the live catalog at deploy time (Stage 2g), proposes the FK when a single match exists, and asks when several plausibly fit.
Built-ins stay built-ins. Entities annotated reuse-from semantius_builtin.<table> (the analyst flagged them in its Stage 2b) are platform infrastructure (users, roles, permissions, …) and never replaced. Additive fields on a built-in are applied via create_field per the spec's per-built-in **Additive fields** block.
Cross-cutting safety invariants
These rules apply across every stage and stay resident in the spine. The canonical copies are here; where a stage file repeats one (provenance, fail-loud, no-deletion) it carries a marked-duplicate note pointing back here.
Failure is loud and halting (the recovery model depends on it). The deploy's entire recovery story is re-run convergence: the spec is the target, every Stage 4 op is read-before-write and idempotent, and a failed or partial deploy is recovered by re-running — there is no transaction, rollback, or resume (PostgREST is stateless). That model is only safe if a partial failure is visible. So when any Stage 4 sub-stage's write fails (a create_* / update_* / postgrestRequest returns non-zero, a platform constraint trips, or a ⚠ row forces a FAIL LOUD), stop immediately and tell the user the deploy is incomplete and must be re-run — do not swallow the error, do not continue to the next sub-stage, and never let the closing message or the Stage 5 summary print a success-shaped result over a partial write. The single way this model breaks in practice is a partial failure that reads as success, so the operator never re-runs. State the halt plainly (and within the Writing Conventions, no em-dashes in this user-facing line): "Deploy halted at <sub-stage> after N writes. The deploy is incomplete: fix the cause and re-run, and the modeler reconciles forward from wherever it stopped (every op is idempotent, so re-running never double-creates)." This is especially load-bearing inside 4k living-mode, which materializes the RACI engine across five separate postgrestRequest batches (processes → raci_assignments → process_gates → enforcement rules → raci_mode flag): a mid-sequence abort there must surface, never be summarized away.
Provenance stamping (core columns; applies to every create in this stage)
The platform ships core provenance columns the modeler is the only writer of. The deployer stamps these values at provision time — they are how rename detection, catalog-owner-arrival, behavior discovery, and cross-domain merges become deterministic platform reads downstream (the analyst on re-reconcile, and every use-* discovery skill). The rules, once, for the whole stage:
- Stamp VALUES only — never
create_fieldthese columns, never writectype. Core registers them withctype = 'core'(sois_coreis derived asctype <> '');ctypeis privilege-locked. The modeler does not create these columns and does not stampis_core— it passes the column values on thecreate_*/update_*payload it already sends. (If a deploy ever errors that one of these columns is missing, the platform is too old — surface that; do not try tocreate_fieldit.) entities.catalog_entity_code= the catalog code, from the spec's**Catalog entity code:**line (NOTtable_name, which holds the deployed / dialect / silo name). Default totable_nameonly when the line is absent.entities.catalog_owner_module= the owner-module slug from the spec's**Catalog owner:**line (anembedded_masterprovisioned locally as a placeholder while its catalog owner module is absent);''when the line is absent (this module owns the entity (role = master), or it is local). Soft string, not an FK.entities.entity_type= the class from the spec's**Entity type:**line;'unclassified'(never'') when absent. Must be one of the six CHECK values.entities.catalog_entity_aliases= APPENDED to on a reuse/merge that renames an incoming entity onto an existing host (read the host's current array, push each new{alias_code, source_domain, source_module, decided}element, write back). Never rewrite or drop prior elements; a plaincreate_entityleaves it at[].modules.catalog_module_code= the catalog blueprint /system_slugthe module was provisioned from; the top-level columnsdomain_code,access_scope, andicon_name; plus themodules.settingskeys (naming_mode,module_kind,catalog_snapshot,promotion_decisions), oncreate_module/update_module.roles.catalog_role_code= the catalog persona/role slug a role was provisioned from, on everycreate_role.- Codes are write-once at create. The two scalar codes (
catalog_entity_code/catalog_module_code) are set on the create call and never re-sent on a later rename — a rename touchestable_name/module_slugonly. Core enforces immutability-once-non-empty, so a re-send of a changed value is rejected; a re-run that re-sends the same value is a harmless idempotent no-op.
No auto-deletion of catalog records (load-bearing safety rule). The deployer never deletes roles, permissions, role_permissions, permission_hierarchy rows, or modules, regardless of origin. This is symmetric across every catalog-record kind the deployer can write. Even model_master rows the deployer wrote in a previous run are off-limits for deletion in subsequent runs. The only legal mutation on them is FK adjustment (including_permission_id / included_permission_id) during master operations.
Specifically:
- Master-merge (4c-merge-master): leaves source masters and their unused permissions, default roles,
role_permissions, and intra-master hierarchy rows in place as quiet orphans. The deployer does not actively detect or report these as orphans either. - Master-rename (4b-rename): updates slugs and names; no deletions, no orphans (rename is in-place updates).
- Any reduction in the model file (entity removed, permission removed, role removed): treated as a no-op against the live catalog. The model file shrinking is not a signal to delete; it might be a typo, a refactor in progress, or the author thinking the entity is now obsolete but other consumers still depend on it.
The deployer does not maintain an orphan registry, does not detect orphans in re-runs, and does not surface orphan candidates in the verification report. The rule is a safety boundary against accidentally destroying admin work, not a feature for catalog hygiene.
Sample-data consent gate. Sample records are NEVER written without an explicit, scoped "yes" to the sample-data question. Continuation words (continue / ok / proceed / go on) are NOT consent. Full rules: references/stage-6-sample-data.md.
Natural keys over numeric ids. Every read filter, diff, verification line, and user-facing narration uses the natural key. Full convention: "Lookup conventions" below in this file.
Preflight (runs before Step 0, every invocation)
The environment checks are shared across all four Semantius skills and live in one place: ../semantius-admin/references/preflight.md. Do not duplicate them here.
- Orchestrated by
semantius-admin(aRun context:block is present in your input): the admin already ran the preflight (toolchain installed, CLI authenticated,adeninguard passed). Skip the checks and proceed. - Standalone (no
Run context:block): run the shared preflight yourself. In brief: stay in the repo root; install the toolchain if missing; probegetCurrentUserto install/authenticate the CLI and halt if the org isadenin. The modeler critically needs Bun (its deploy and sample-data scripts run withbun run);jqparses CLI JSON. It does not needyqand does not consult the customizations file, so check 4 (theCUSTOMIZATIONS_FILEcomputation) does not apply and a missingyqmust not block a standalone modeler run — specs already carry every decision. The full per-check procedure and install matrix are in the reference file.
Step 0 (hard gate): Load the use-semantius Skill
This is a blocking prerequisite, not a suggestion. Do not author a deploy script and do not issue a single create_* / update_* call until you have read all four files below. Every write this skill makes goes through use-semantius's patterns. The failures that look like platform bugs — wrong column names, null rejected on a column you thought was optional, "I got an array, I expected an object" — are almost always Step 0 not being read. Read all four, now:
Read: ../use-semantius/SKILL.md
Read: ../use-semantius/references/data-modeling.md
Read: ../use-semantius/references/jsonlogic.md
Read: ../use-semantius/references/select-rule.md
The data-modeling reference gives you the mandatory creation order, all field formats, the Golden Rules, and exact CLI syntax. The jsonlogic and select-rule references define the rule properties the spec carries and this skill deploys verbatim (computed_fields, validation_rules, input_type_rule, select_rule). Everything in the execution stages below follows those patterns. Also read ../use-semantius/references/cli-usage.md if you need help with CLI invocation, piping, or error handling.
Safety-net cheat table (does NOT replace reading the files above)
These are the traps that have actually broken deploys. This table is a backstop for when you read Step 0 but a detail slips — it is a pointer to the authoritative text, never a substitute for it. When anything here is incomplete or seems to conflict with use-semantius, use-semantius wins; go read the cited section.
| Trap | Wrong | Right | Authoritative section |
|---|---|---|---|
| Read response shape | Treating a crud read as a bare object; trusting exit 0 to mean "found" |
crud reads return a JSON array by default (even for one row); exit 0 + [] means "found nothing." Pass --single for any read that must resolve to exactly one row: it returns a bare object and exits 1 (none) / 2 (ambiguous). |
use-semantius SKILL.md → Response handling: exit code is not enough |
| Make a field mandatory / unique | A required field column ("required": true) |
There is no required column. Mandatory = input_type: "required". Unique = |
…(truncated)