Imported from IntranetFactory/domain-map (
.claude/skills/semantius-skill-maker/SKILL.md). Install upstream withnpx skills add IntranetFactory/domain-map --skill semantius-skill-maker. Copyright stays with the author.
semantius-skill-maker
Turn a semantic-model markdown file (the artifact produced by
semantic-model-analyst) into one task-aware Agent Skill that
captures the domain knowledge needed to act on that model efficiently.
The generated skill does not duplicate use-semantius, it sits on top
of it, focusing exclusively on the domain (entities, lifecycles, label
rules, cross-FK invariants) that a generic platform skill cannot know.
The generated skill is plain Agent Skills format, a SKILL.md with
YAML frontmatter under a folder named after the model, and works in
any agent harness that loads Agent Skills, including Claude Code.
Writing conventions (apply to every file and message this skill produces)
These rules apply to chat output, the generated SKILL.md, the generated README.mdx, every reference file, and every script this skill writes.
1. US English spellings, always. Never British English. Examples (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 everywhere this skill writes: SKILL.md, README.mdx, semantic-model citations, references, scripts, and chat output. This propagates. LLMs mirror the style of their context, so an em-dash inside generator prose causes generated skills to emit em-dashes too. 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 on — used as punctuation. Before writing any file, literal-search 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.
4. README.mdx catalog-description rules. The README.mdx this skill generates is rendered side-by-side with other skills in a catalog gallery. These are hard constraints for catalog readability:
- Front-matter
description: one sentence, ≤140 characters, unique value-prop for this skill. Never starts with "This skill" and never contains the substring "this skill". Do not just restate the domain. Shape: verb-led, names actual capabilities (e.g. "Manages the recruiting funnel from requisition through application, interview, offer, and hire."). Do not use the "[verb] [object] so [outcome]" shape; the "so..." clause reads as trigger-fodder, not a capability list. - Front-matter
titleand the#heading: must always contain the word "Skill", and must read as natural English. If the sourcesystem_nameends in "System" (e.g. "Applicant Tracking System"), drop "System" and append "Skill" → "Applicant Tracking Skill". Otherwise append "Skill" directly: "Workforce Planning Skill", "Product Roadmap Skill", "Customer Relations Skill". - Body, immediately after heading: repeat the model's domain description verbatim (or near-verbatim), pulled from the model file's
domainfront-matter or §1 narrative. The body describes the SYSTEM, not the skill. The skill's unique angle lives only in the front-matterdescription. - No
generated_fromkey in README front-matter. - Validate before declaring done: literal-search for "this skill" (must be zero hits) and
—(must be zero hits), and check the heading contains "Skill".
5. Semantius entity-label symmetry (when the generated SKILL.md cites entity labels). singular_label is the bare singular noun matching plural_label. ✅ Product / Products. ❌ Product Name / Products. The skill-maker reads these from the source model file; if the source has an asymmetric pair, treat the source as buggy and ask the user to fix the model via the analyst before re-running the skill-maker (do not paper over it in generated output).
Schema compatibility: EXPECTED_MAJOR = 3
This skill expects model files written by semantic-model-analyst major 3. The model file's front-matter version: "MAJOR.MINOR" is checked at the start of Step 2. Major must equal EXPECTED_MAJOR, minor is informational and not compared. Files with a different major are rejected; the resulting per-domain skill would bake in stale recipes. Three cases:
- Older major, the file was written using a structure this skill no longer understands (different section numbering, different table shapes, missing fields). Tell the user to run
semantic-model-analyst; its archived-knowledge mode reads the older file and re-authors a current-major file from the same semantic content. Re-run skill-maker against the new file. - Newer major, the file was written by a newer analyst than this skill knows. Tell the user to update
semantius-skill-makerbefore retrying. - Missing
versionkey (legacy, pre-versioning), treat as major0; same response as older-major.
When the analyst's major bumps, this skill's EXPECTED_MAJOR must be bumped in lock-step (same commit when feasible). The trio of analyst, deployer, and skill-maker share the major.
The history of the skill-maker's contract changes lives in CHANGELOG.md — what each analyst-lockstep bump changed in the parser, indices, generated-skill preambles, and self-review principles. That file is not loaded at runtime; the body of this SKILL.md is the current contract, the CHANGELOG is the history.
Inputs
MODEL_PATH: absolute path to a*-semantic-model.mdfile with valid frontmatter (system_slug,system_name,entities, etc.) and §3 entity definitions.
Output
A single folder under the user's Claude skills root. Two top-level
files are always written; references/ and scripts/ subfolders are
written only when the JTBD classifier in Step 3 (Pass 3) puts work
there.
<skills-root>/<modelslug>/
├── SKILL.md # for the calling agent: glossary, JTBD outlines, guardrails
├── README.mdx # for humans browsing a catalog: narrative + diagram
├── references/ # optional, per-JTBD detail, loaded on demand
│ └── <jtbd-slug>.md
└── scripts/ # optional, deterministic ops, invoked not loaded
└── <op-slug>.sh
SKILL.md is what an agent harness loads at runtime. README.mdx is a
human-facing catalog entry: a downstream system renders these into a
gallery so a person can browse available skills, understand each one's
purpose at a glance, and decide whether to install it. The two files
share a folder, but their audiences and formats are different, do not
collapse them, and do not skip the README.
The references/ and scripts/ subfolders exist to keep SKILL.md
small. SKILL.md is loaded into context every time the skill triggers;
a reference file is loaded only when the agent enters that specific
JTBD; a script is never loaded, the agent invokes it. A 400-line
SKILL.md plus a handful of focused 80-line reference files costs less
per trigger than a 900-line monolith, because the agent typically
engages one JTBD at a time. The classifier in Step 3 (Pass 3) decides
which JTBD belongs in which file.
<modelslug> is the model's system_slug converted to kebab-case ,
underscores become dashes (e.g. customer_relations →
customer-relations, product_roadmap → product-roadmap). Every
multi-word skill in Claude Code uses kebab-case (use-semantius,
semantic-model-analyst, skill-creator); mashing words together
(customerrelations) breaks the convention and makes the description
text harder to read. The folder name and the SKILL.md name
frontmatter match exactly.
<skills-root> resolution order:
- Project skills root, the nearest
.claude/skills/directory walking up from the model file's location, or from the current working directory. Prefer this if found. - User skills root,
~/.claude/skills/(on Windows:%USERPROFILE%\.claude\skills\).
If both exist, ask the user which to use; default to the project root. If neither exists, ask before creating one.
Source-of-truth and the model reference
The generated SKILL.md is self-contained at runtime for the cross-cutting facts (glossary, enums, recipes resolve every value at generation time so the calling agent never needs to open the model file to act). FK shape and audit-logging detail live with the JTBD that uses them, not in SKILL.md, see "Where FK detail lives" below.
The model file is still referenced, but only as provenance metadata in
frontmatter (semantic_model: key, see the template below), not as a
clickable link in the body. Two reasons:
- A body link invites the agent to fetch a 400+ line file the SKILL.md has already condensed. That defeats the point of generating the skill.
- Provenance still needs to live somewhere, for re-generation, audit, and drift detection, and frontmatter is the right place because it's machine-readable and the agent doesn't render it as a follow-up action.
Set semantic_model to the model's system_slug value (e.g.
product_roadmap), not a path. The slug is stable across machines
and re-locations of the model file; an absolute path bakes in the
generator's working directory and breaks the moment the file moves
or the skill is shared with another user. The downstream tools that
consume this provenance (re-generation, drift detection) resolve the
slug against the current *-semantic-model.md files in the working
directory, so the slug is sufficient.
Workflow
Step 1, Load the Semantius reference
Before writing recipes, read the use-semantius skill so the JTBD recipes
use the right CLI patterns:
Read: <skills-root>/use-semantius/SKILL.md
Read: <skills-root>/use-semantius/references/data-modeling.md
Read: <skills-root>/use-semantius/references/crud-tools.md
Read: <skills-root>/use-semantius/references/cube-queries.md
You will not run semantius yourself in this skill, but the recipes you
bake in must be valid CLI invocations. If use-semantius cannot be located,
stop and ask the user.
Step 2, Parse the model
Read MODEL_PATH and gate on the version first before extracting anything else. Compare the file's front-matter version major against this skill's EXPECTED_MAJOR (see "Schema compatibility" near the top). Major equal → continue. Major older or missing → stop with a message naming the file's version, this skill's expected major, and the recommended fix (run the analyst's audit to migrate). Major newer → stop and ask the user to update this skill. Do not parse anything else when the gate fails; the file's structure may not match what the rest of Step 2 assumes.
Once the version gate passes, extract:
-
system_slug,system_name,domain, andmodule_type("domain"or"master"; absent → treat as"domain") from frontmatter.module_typecontrols two things downstream: the generated SKILL.md's "Module type" preamble line (Step 5), and the JSON-array merge note (master-module entities'computed_fields/validation_rulesentries carry an extrasource_modulereconciliation tag — see the per-entity extraction note below). -
Entity list with
singular_label,label_column, fields (name, format, required), enum values (§5), FK relationships (§4), parent/cascade-child flags,audit_log,computed_fieldsarray,validation_rulesarray, and (analyst v3.0+) any**Shared master cluster:** <name>annotation under the §3 entity heading. The two latter JSON blocks are platform-enforced JsonLogic — they change what the calling agent must do and what the calling agent must NOT do. See "Pass 2" merit-test adjustments below.When the entity belongs to a master module (
module_type: masterin frontmatter), itscomputed_fieldsandvalidation_rulesentries may each carry asource_modulekey naming the consuming module that contributed the entry. Treatsource_moduleas opaque reconciliation metadata: surface its existence in the preamble note but never use it to filter entries (every entry applies regardless of which module contributed it). Domain-module entities never carrysource_module.Record the cluster annotation per-entity into a
master_cluster_by_entityindex. The annotation is informational for skill-maker: it signals that the entity is a classic master concept (vendors →parties, cost_centers →finance, etc.) and that writes to it may have cross-domain visibility. Use it to surface a "shared across domains" callout in the generated skill's preamble (Step 5) and as a guardrail on every write recipe touching the entity (Step 6).Treat each entry's JsonLogic body as opaque and read only the human-readable metadata (
nameanddescriptionforcomputed_fields;code,message, anddescriptionforvalidation_rules). All matching downstream, Pass 2 suppression, gap-tracking, SKILL.md preamble copy, JTBD failure-mode wiring, uses that metadata. The analyst's job is to writedescription/messagetext precise enough that a candidate transition or derivation can be matched against it by reading. If the analyst's text is too vague to match, that is a model defect to send back; do not try to evaluate the JsonLogic to recover.One exception: scan each rule's JsonLogic body literally (string-match only) for the two platform-extension operators. Both are first-class JTBD signals:
require_permission, i.e.{"require_permission": "<code>"}somewhere inside the rule body. Extract(entity, rule_code, rule_description, permission_code)for every match into aconditional_permissionsindex. This index drives the new "Conditional-permission gate" merit signal in Pass 2 and thePlatform-enforced permissionsSKILL.md preamble (Step 5). The match is purely literal; which condition gates the permission still comes from the rule'sdescriptiontext, not from interpreting the JsonLogic.value_changed, i.e.{"value_changed": "<field>"}somewhere inside the rule body. Note(entity, field)pairs into atransition_gated_fieldsindex. This is informational, not a merit signal on its own; it helps Pass 2 confirm that arequire_permissionrule actually fires on transition rather than on every write (a quality check that the analyst's audit also runs).
In analyst v2.0+ files, the §2 Permissions summary table is the canonical source for the module's full permission catalog (not §8 step 1). Parse the table verbatim — five columns:
Permission | Type | Description | Used by | Hierarchy parent. Build apermissions_catalogindex from the rows. Use this index for everything: the SKILL.md "Platform-enforced permissions" preamble (one row per workflow permission), the role-hint lookup for eachconditional_permissionsentry (theDescriptionandUsed bycells give the role-hint richer than v1.11's §8 prose), and the cross-check that everyconditional_permissions[].permission_codeappears in the table. A mismatch is a model defect; refuse to generate and route back to the analyst. §8 step 1 in a v2 file is a procedural pointer to the table and no longer enumerates permissions itself; do not parse §8 for the permission list.v3.0 cross-module hierarchy rows. The
Hierarchy parentcell may reference a permission in a different module (e.g. an ITSM module'sitsm:readrow carryingHierarchy parent: parties:read); this is a cross-module bridge to a master module that the deployer materializes viapermission_hierarchyrows taggedorigin = "model_master". Recognize these by the presence of a<other_slug>:<suffix>value in the cell where<other_slug>is not this module'ssystem_slug. Record cross-module parents into amaster_inclusionsindex keyed by(this_permission, master_permission). The index drives a one-line note in the SKILL.md "Platform-enforced permissions" preamble: every permission with at least one cross-module parent gets a "includes : from the<master>master module" sub-note so the calling agent knows that holding it grants visibility into shared data. Same-module hierarchy rows still surface as the regular rollup chain; the index split only matters for the preamble note. -
Inbound-FK delete-mode index). Walk the §4 relationship table once more and build a
restrict_inboundindex per entity: for every row whoseKindisreferenceandDeleteisrestrict, record(child_entity, fk_field)against the target (the entity on the right side of the relationship). This index drives the new "Restrict-chained cleanup" merit signal in Pass 2 and is consumed by Pattern J (delete / archive of a parent entity that has restrict-children). The pattern matters because the platform refuses to delete a row that has livereference + restrictchildren; the calling agent must clean them up first, in dependency order, and a genericuse-semantiusDELETEwill surface only a single per-row constraint failure rather than the full chain. Also note inboundreference + cascadeandreference + clearrows separately; they don't trigger Pattern J but the SKILL.md preamble should still call out the cascade-delete / orphan-clear behavior for downstream awareness. -
Read-side rule indices). Two new optional §3 sub-blocks per entity. Build one index for each; both stay opaque on the JsonLogic body and read only the human-readable metadata.
row_visibility_rules— for every entity that declares a non-emptySelect rulesub-block, record(entity, jsonlogic_object, description?).descriptionis the entity-level prose that explains "what the rule restricts" — if the analyst didn't write one, walk §3 prose for a sentence that names the per-row predicate (e.g. "a note is visible to its author, or to anyone when itsvisibilityfield ispublic"). Surface a defect to the user if neither the sub-block carries adescriptionnor §3 prose names the scope; the SKILL.md preamble and per-JTBD guardrails need readable scope text. Critical (v2.2): the rule applies uniformly to every caller withview_permission. The platform evaluates the JsonLogic body per row with$today/$now/$user_idas reserved variables; there is no documented mechanism by which holding a specific permission causes the rule to be skipped for that caller. If the analyst's model contains a §7 architectural decision naming a documented broadening mechanism (Stage 12 option C separate-entity surface, option D PostgresBYPASSRLSrole attribute), record it underrow_visibility_broadeningkeyed by entity so the SKILL.md preamble can name the actual mechanism the user resolved. Never invent a "view_all bypass" the model file does not explicitly resolve in §7. This index drives the Row-level read scope SKILL.md preamble (Step 5), the Row-level read scope cross-cutting guardrail attached to every read recipe against a scoped entity (Step 6 / Step 7), and a new severity row in Step 9 self-review.dynamic_input_types— for everyInput type rulesentry across every entity, record(entity, field, jsonlogic_object, description?). Thedescriptionfield describes when the rule changes the field's UI mode —"hidden until status=approved, then readonly"is the canonical shape. Walk the JsonLogic body literally (string-match only, same posture asrequire_permission/value_changed) for one pattern that bears on JTBD shaping: a return value of"required"triggered by a sibling-field comparison (e.g.status=approved → "required") flags the field as a conditional-required side-effect on the same transition, which co-fires with Pattern A's side-effect-fields-on-transition merit signal. Record those into a sub-indexconditional_required_fieldskeyed by(entity, trigger_field, trigger_value, dependent_field). This index drives the Conditional field UI states SKILL.md preamble (Step 5), additional side-effect-fields-on-transition coverage in Pattern A recipes (Step 6 / Step 7), and a new severity row in Step 9 self-review.
Skip both indices entirely when the model declares no entity-level
Select ruleand no field-levelInput type rules. v2.1 and earlier files have neither.
Compute modelslug = system_slug.replace(/_/g, "-").
Refuse if §7.1 lists open blockers, the model is not finished and the skill would bake in wrong recipes.
Step 3, Plan jobs to be done
JTBD discovery is a two-pass process: nominate broadly with the pattern catalog below, then filter with the merit test. The merit test matters because every section sits in one file, slack adds noise to every load.
Pass 1, Nominate with the JTBD pattern catalog
The catalog below is generic across domains, not just transactional business models. Walk all nine patterns every time, regardless of what the model "looks like" at first glance.
Pattern A, Lifecycle transitions on status enums.
Shape test: entity has a *_status enum + side-effect fields like
approved_at, committed_at, rejected_at, closed_at. Each
non-trivial transition (more than a status flip) is a candidate. Pure
status flips with no side effects collapse into the entity's primary
lifecycle JTBD.
Sub-shape: approval / sign-off / workflow gate), broadened in v1.12). Any
transition whose validation_rules entry invokes {"require_permission": "..."} (look up the entity's conditional_permissions index from Step
2) is automatically a strong JTBD candidate. The agent calling this
skill is the one that will trip the permission check, so it needs an
explicit recipe: name the permission, name the role(s) that typically
hold it (read from §8 step 1 description), and tell the agent how to
recover from the platform's throw (surface the rule's message,
propose escalation paths like "ask an offer-approver to confirm" or
"defer to the user who is signed in as the approver"). This sub-shape
does not get suppressed by the merit-test "platform-enforced" rule,
because the platform enforces the check but the calling agent still
owns the workflow around it (the read of who-approves-what, the
confirmation prompt, the failure-message phrasing). Treat every entry
in conditional_permissions as one JTBD candidate (or a branch inside
the entity's primary lifecycle JTBD), even if Pattern A would otherwise
collapse the transition.
The sub-shape covers three distinct trigger fields, all flagged the
same way once value_changed + require_permission co-occur in the
rule body:
- Status-transition gates:
value_changed: "status"→ one ofapproved/signed/released/ etc. Recipe: confirm with the user, then PATCHstatus. - Submit-then-lock gates:
value_changed: "is_submitted"(oris_locked/is_final/is_complete) →true. Recipe: confirm the user is the row's owner (or holds the override permission); compose the PATCH that flips the lock; after the flip, treat the row as immutable except via the override. - Closure / cancellation gates:
value_changed: "status"→ one ofclosed/cancelled/void/hired(when §3 prose names weight). Same recipe shape as (1).
Sub-shape: housekeeping field appears on transition). For each entry in the conditional_required_fields sub-index (built in Step 2 from Input type rules), the transition JTBD that fires the trigger condition must include the dependent field as a same-PATCH side-effect. Concretely: when dynamic_input_types says "offers.approved_at hidden until status=approved, then readonly", the "approve offer" JTBD's PATCH body MUST set approved_at = $now in the same call as status = "approved" — the UI rule controls when the form renders the field, but a recipe that drives the transition through the API has no form to render and would silently leave approved_at empty. This is the same merit signal as the existing "Side-effect fields on transition" row (the *_at field that must be set in the same PATCH as the status flip), but the input_type_rule makes the pairing mechanical and detectable instead of relying on the analyst remembering to mention it in prose. Cross-reference with the entity's validation_rules: a paired family-5 conditional-required rule (server-side must be non-null when status='approved') confirms the intent and gives the JTBD's Failure modes block the platform code to surface on bad writes. When the UI rule fires but no server-side family-5 rule exists, surface as a platform_enforceable_gaps entry of kind validation_rule (the UI says the field must be set; the server doesn't enforce it; the recipe enforces it client-side as a stopgap).
Pattern B, Polymorphic action / event staging.
Shape test: entity named *_actions, *_events, *_transactions,
*_movements, with a polymorphic *_type enum that fans out
behavior. Nominate one JTBD with branches per type value, do not
split per enum value (that produces overlapping recipes).
Pattern C, Materialization / handoff.
Shape test: entity exists so another entity can be created from it;
look for originated_from_*_id back-pointers, or "approved → real"
flows where a staging row spawns rows in a different table. These are
the highest-value candidates because they touch multiple tables and
are the most error-prone for a calling agent to derive from the schema
alone.
Pattern D, Hierarchy operations.
Shape test: self-referencing FK like parent_*_id, manager_*_id,
backfill_for_*_id; tree-shaped data. Candidates: reparent without
orphaning, roll-up, cycle prevention. Skip if the self-FK is purely
informational ("previous version of this row") with no operation that
restructures the tree.
Pattern E, Ownership / sharing.
Shape test: owner_*_id field, sharing tables. Candidates: transfer
ownership of X, share X with, revoke access to X. (Multi-tenant
scoping is not a Semantius concern: each customer organization runs
its own Semantius deployment against its own database, so a single
instance never holds rows for two different tenants.)
Sub-shape: owner-or-manager edit gate). Entity
carries a created_by / author_id / owner_id / assignee_id
field AND its conditional_permissions index includes a rule that
references the owner field against $user_id plus an elevated
<slug>:manage_all_<plural> permission. The agent calling this skill
is often not the original owner (it acts on behalf of users who may
be different from the record's creator), so the recipe must surface:
"editing this record requires that the caller is the original
<owner_field> OR holds <slug>:manage_all_<plural>; if neither
holds, the platform will reject with <rule.code>: <rule.message>,
propose handing off to the original owner or to a user who holds the
override permission." Treat as a JTBD candidate even when no other
Pattern-E shape fires.
Pattern F, Publication / versioning.
Shape test: draft/published states distinct from approval; version
chains (*_version, previous_version_id); published_at separate
from created_at. Candidates: publish X, unpublish X, create new
version of X. Distinct from Pattern A approval, publication is about
distribution, not sign-off, and the side effects differ (cache
invalidation, notification, visibility scope).
Pattern G, External-system handoff.
Shape test: fields like external_*_url, *_external_id,
webhook_* entities, or a status value such as synced / failed.
Candidates: sync X to external, replay failed X, reconcile X.
Pattern H, Bulk ingest. Shape test: model declares webhook receivers explicitly, or has an entity whose typical population unit is a batch (a CSV-shaped table with no parent beyond the batch). See "pattern-level adjustments" below, in this skill, bulk ingest usually becomes a one-line pointer rather than a JTBD.
Pattern I, Cross-entity reporting.
Shape test: 3+ entities joined by FKs and at least one numeric measure
(cost, count, duration, FTE, amount). See "pattern-level adjustments"
below, in this skill, reporting becomes a ## Common queries
appendix, not a JTBD section.
Pattern J, Restrict-chained cleanup).
Shape test: an entity that appears as the target of at least one
inbound reference + restrict FK from another entity in the model
(read the restrict_inbound index from Step 2). The platform refuses
to delete a row that has live restrict-children; the calling agent
needs an explicit cleanup recipe naming each restrict-child entity
in dependency order, with a per-child query shape ("find all
<child> where <fk_field> = <parent_id>") and a delete loop
before the parent delete itself. This pattern is the v1.13
parent-vs-reference rule's direct downstream consequence: when the
analyst flips a parent (cascade) FK to reference (restrict)
because the child has divergent permission scope, the analyst is
deliberately moving the cleanup decision into the application
layer rather than letting the platform silently cascade-delete
permission-scoped rows. The skill-maker materialises that decision
as a recipe. Candidates: "delete <parent>", "archive
<parent>", "merge two <parent> records" (where merge ends with
a delete of one). Skip when the entity has zero inbound
restrict-FKs (the platform's own DELETE is enough) or when the
restrict-children themselves carry no permission-scope divergence
(then the analyst's choice of restrict was overly cautious; flag
to the user but still emit the recipe).
Skip rules
Do not nominate any of the following, they fall outside the "job to be done" frame:
- Single-row CRUD on master-data tables with no lifecycle (insert
one department, edit a job code). The calling agent uses
use-semantiusdirectly; an extra section is just noise. - Seed / sample / test-data population. One-off developer work, not a recurring job. If the user wants a seed script, ask separately.
- Entities listed in §7.2 "Future considerations", they don't exist yet.
- Pure read-by-id lookups, the calling agent uses
postgrestRequestdirectly.
When none of the patterns fire
If a model has shapes none of the patterns recognize (rare, but possible for unusual domains), name the unmatched shape explicitly in the Pass-2 confirmation step and ask the user whether it warrants a custom JTBD. Do not invent a job to fill space.
Pattern-level adjustments specific to this skill
- Pattern I (cross-entity reporting) does not become a JTBD section
here. Promote it instead to a
## Common queriesappendix at the end of the SKILL.md, 3–5 pre-shaped cube queries the calling agent can adapt. Reporting is largelyuse-semantiusterritory once the schema is known; baking in example queries is useful, but framing it as a "job" misleads the calling agent into routing every analytic question through this skill. - Pattern H (bulk ingest) becomes a one-line pointer, not a recipe,
unless the model declares webhook receivers explicitly. If it does,
write a JTBD; otherwise the SKILL.md just notes "for CSV import, see
use-semantiusreferences/webhook-import.md".
Pass 2, Apply the merit test (earn-its-place filter)
For each candidate from Pass 1, ask: would the calling agent get this
right with use-semantius alone? If yes, drop the candidate, an extra
section is just noise, the calling agent should call use-semantius
directly. A candidate earns a section only if it answers YES to ≥1
of the following:
| Merit signal | What to check in the model |
|---|---|
| Caller-populated label | Junction or sub-entity has a required *_label column distinct from any label_column, with no DB-level default. The recipe must compose the label client-side, not obvious from the schema alone. |
| Computed field | A stored derived field (e.g. rice_score, total_amount, days_open) whose value depends on sibling fields. Match by exact name against the entity's computed_fields[].name array, then resolve to one of three cases. (a) Field is in computed_fields → signal is suppressed; does NOT earn a section on its own. The platform overwrites caller payloads on every write, so the calling agent must not include the derived field in any POST/PATCH body and must not recompute it client-side. That rule is documented globally in the SKILL.md Platform-derived fields preamble (resolved at generation time); no per-JTBD recipe is needed. A candidate whose only merit signal is this row, in case (a), is dropped and listed under skipped. (b) §3 prose names a derivation but no matching computed_fields[].name entry exists → gap-tracked (record under platform_enforceable_gaps; see below). The signal earns a section ONLY when the candidate also passes one of the other merit signals (cascade, side-effect, junction without uniqueness, etc.); the recipe enforces the derivation client-side as a stopgap until the analyst adds the rule to the model. (c) Neither field-name match nor §3 prose names a derivation → signal does not fire. |
| DB-unguarded lifecycle gate | Status enum where some transitions are valid and others aren't, but the DB accepts any value. Suppression here is per-transition, matched by description / message text, not per-block: walk the entity's validation_rules[] and ask "does any entry's description or message text name this specific transition, or the invariant it gates?" Then resolve. (a) A validation_rules[] entry covers the transition → signal is suppressed for that transition; does NOT earn a section on its own. The platform rejects the bad write with { "errors": [{ "code", "message" }, ...] }; the recipe just surfaces that. The code is documented globally in the SKILL.md Platform-enforced invariants preamble; no per-JTBD recipe is needed for the gate. A candidate whose only merit is this row, in case (a), is dropped and listed under skipped. (Scripts that exist for other merit reasons may name the platform code in their stderr diagnostics so the agent surfaces a clean recovery hint; they must not duplicate the JsonLogic check.) (b) §3 prose names the invariant but no matching validation_rules[] description does → gap-tracked. The signal earns a section ONLY when the candidate also passes another merit signal (cascade, side-effect, etc.); the recipe enforces the gate client-side as a stopgap. (c) Neither prose nor validation_rules names the invariant → signal does not fire. |
| DB-unguarded invariant across FKs | E.g. features.release_id and features.product_id must agree on product. The recipe must read both rows and check before patching. Cross-row constraints are out of scope for validation_rules (entity-level only), so this signal is not suppressed by the platform. |
| Cascade flow | Flipping one parent row should flip a filtered set of children in the same logical operation (e.g. release-shipped → its planned/in-progress features → shipped). |
| Junction without uniqueness | M:N junction without a DB-level unique constraint on the natural key. The recipe must dedupe-before-insert. |
| Materialization / handoff | One entity row spawns rows in a different table (Pattern C). The order, FK back-pointers, and source-status flip are easy to get wrong. |
| Side-effect fields on transition | approved_at, committed_at, actual_release_date, etc. that must be set in the same PATCH as the status flip, easy to forget. |
| Audit-trail read | Audit-logged entity (audit_log: true) where "who/when changed X" is a likely user question. Worth a short recipe even though writes need no special handling. |
| Conditional-permission gate | The entity's conditional_permissions index from Step 2 carries one or more entries (i.e. at least one validation_rules rule invokes {"require_permission": "<code>"}). Unlike the basic "DB-unguarded lifecycle gate" row, this signal is never suppressed by the platform-enforces-it rule, the platform enforces the check but the calling agent still owns the workflow: detecting that the caller lacks the permission before the write attempt (cheap UX win), surfacing the rule's message cleanly on the throw, and proposing the right escalation path (which role typically holds the permission, who to hand off to). One JTBD per (entity, permission_code) pair, or one combined JTBD per entity when several conditional gates share the same approver role. Cross-reference the §8 step 1 description for the permission to know what role typically holds it. |
| Restrict-chained cleanup | The entity appears in at least one other entity's restrict_inbound index (i.e. is the target of at least one reference + restrict inbound FK). Pattern J fires. The recipe walks the dependency tree of restrict-children, naming each one and providing the find-and-delete query shape, then performs the parent delete. The merit signal is the chain itself: a generic use-semantius DELETE against the parent will surface only one row's constraint failure per attempt, never the full chain; the recipe makes the order explicit. |
| Row-scoped read | The entity appears in the row_visibility_rules index from Step 2 (i.e. carries a non-empty Select rule). Like the conditional-permission gate above, this signal is never suppressed — the platform filters rows from the result set, but the calling agent still owns the workflow of surfacing the limit to the user. Critical (v2.2): the rule applies uniformly to every caller with view_permission. A read recipe against a scoped entity must surface that as a uniform limit ("every caller sees only <predicate>"), not as a tiered limit. If the analyst's model has resolved an architectural-decision §7 entry that names a documented broadening mechanism (option C separate-entity surface, option D Postgres BYPASSRLS role), the recipe can name it as the path to broader access; otherwise the broader-access path is "talk to a user with that role" and the recipe says so. Never invent a view_all_<plural> permission bypass. The signal earns a section when the entity is a target of meaningful read recipes (list, search, look up by non-id) — those recipes need a preamble noting the scope. Pure write recipes on the entity (insert, update with the id already known) are not affected and don't need a separate JTBD; the cross-cutting Row-level read scope preamble in the SKILL.md body covers them. Entities whose only operations against them are writes-by-known-id get the preamble entry but no per-JTBD section. |
| Conditional field UI state | The entity has at least one field in the dynamic_input_types index. By itself, this is not a merit signal that earns a section: an input_type_rule is UI control only, the platform doesn't gate writes on it, and recipes that POST/PATCH bypass the UI entirely. The exception is the conditional_required_fields sub-index (UI rules that go default → required on a transition): those compose with Pattern A's existing "Side-effect fields on transition" signal and the housekeeping-field-appears-on-transition sub-shape above, and earn coverage inside the transition JTBD that fires the trigger. Cross-cutting documentation of every input_type_rule lives in the Conditional field UI states SKILL.md preamble (Step 5), not in per-JTBD sections — same posture as Platform-derived fields. |
Trap, the most common defect: when a candidate's only merit signal is Computed field and the field is in
computed_fields, OR only DB-unguarded lifecycle gate and avalidation_rules[]entry covers the transition (matched by description), the candidate is dropped, not earned. The platform owns the rule end-to-end; a JTBD section would just restate what the SKILL.md preamble already documents globally. List under skipped with a reason likepure CRUD; <field> is platform-derivedorpure CRUD; transition is platform-enforced via <code>. The "Score with RICE" anti-pattern is the canonical example: every input has avalidation_rulesrange check andrice_scoreis incomputed_fields, so the recipe is one PATCH and the platform handles the rest, no JTBD section earns a place.
If the only thing a candidate does is single-table CRUD with the platform
defaults (no merit signals), drop it. List dropped candidates in the Step
4 summary as skipped: pure CRUD against <table>, calling agent uses use-semantius directly. This is not a failure; it is the design.
Track platform-enforceable gaps
While walking the merit table, maintain a side list platform_enforceable_gaps
keyed by entity. Append an entry whenever the Computed field or
DB-unguarded lifecycle gate signal fires and the entity's model
block does not already capture the rule:
- Computed-field gap. A field's §3 prose names a derivation
(
(reach × impact × confidence) / effort, "subtotal of line amounts", "days between open_date and close_date") and the entity'scomputed_fieldsarray carries no entry whosenamematches that field. Record{entity, field, kind: "computed_field", evidence: "<the §3 prose snippet>"}. - Lifecycle-gate gap. The §3 prose for a status enum (or any
field-level invariant) names a constraint ("only set X once Y is
committed", "X cannot decrease", "X required when Y is
paid") and novalidation_rulesentry on the entity encodes it. Record{entity, field, kind: "validation_rule", evidence: "<the §3 prose snippet>"}.
The detection is conservative: only fire when the §3 prose names the derivation or constraint explicitly. Do not infer rules the analyst did not write down — that is the analyst's job, not the skill-maker's.
A gap by itself does not earn a JTBD section. A candidate still has
to pass the merit test on at least one other signal (cascade,
side-effect on transition, junction without uniqueness, materialization
handoff, caller-populated label, etc.). When it does, the gap-tracked
rule earns a stopgap recipe inside that JTBD until the analyst adds the
matching computed_fields / validation_rules entry to the model. The
gaps list itself is purely the Step 3 confirmation checkpoint, so the
human can decide whether to pause and add the rule via the analyst
skill, or accept the stopgap recipe.
Pass 3, Classify each surviving JTBD into a file
For each JTBD that passed the merit test, decide where its body lives. The default is reference. Inline and script are exceptions earned by specific shape.
Inline (kept in SKILL.md body). Reserve for JTBDs whose entire recipe is one POST or PATCH with no read-first, no client-side composition, no branching. Rare; most JTBDs that earned a merit signal have at least one of these. If the inline body would exceed ~5 lines of recipe, promote to reference.
Script (scripts/<op-slug>.sh). The decision rule for "script vs
reference" is one test, not a checklist:
Does any branch in this recipe require the agent to ask the user something before continuing?
If no, it's a script. The script reads its parents, composes any labels internally, recomputes any stored values internally, validates preconditions, and either succeeds (exit 0) or refuses with a diagnostic message and non-zero exit. The agent invokes it with a small set of arguments (titles, emails, codes, dates) and checks the exit code; the script body never loads into context.
What is not a judgment branch (these all stay in scripts):
- Label composition.
feature_vote_label = "{user_full_name} -> {feature_title}"is mechanical: read the parents, format the string, no agent involvement. The script reads the rows it composes from. - Computed values.
rice_score = (reach * impact * confidence) / effortis mechanical: read current values, overlay caller's deltas, arithmetic, PATCH the result. Round to the column scale; if the inputs make the result undefined (effort null/zero), set the computed field to null and exit 0 with a diagnostic, do not exit non-zero. - Mechanical preconditions ("refuse if release_status is
releasedorcancelled", "refuse if effort_score is null"). The script checks, exits 1 with a clear message, and the agent surfaces the message to the user. That is not the agent asking the user something; that is the script telling the agent it cannot proceed. - Dedupe-on-junction. "PATCH if the row exists, POST if not" is a fixed branch on what the read returned; the agent does not intervene.
What is a judgment branch (these belong in references):
- "Ask the user before rewriting a committed row's history."
- "Ask the user before charging this work to an inactive cost center."
- "Ask the user whether to abort or recreate when the parent was deleted."
Anything that needs a yes/no from the user mid-operation is a reference. Everything else is a script.
The script must also satisfy two structural requirements that follow from being callable by the agent:
- Idempotent: re-running with the same inputs is safe. Filter writes to "rows that still need the change", not "all rows in the parent set"; running twice is then a deterministic no-op on the second run.
- Failure messages are diagnostic: name the step that failed and what the agent should tell the user (e.g. "step 1: feature '' not found, ask the user for the correct title"), so the agent can recover or escalate without re-reading the script body.
Reference (references/<jtbd-slug>.md). Use this when the recipe
has a judgment branch that needs the agent to mediate a user
confirmation (the test above). The reference file owns the
read-first calls, the branching prose, the user-prompt phrasing, and
the long failure-mode discussion. The agent loads the reference only
when it enters the JTBD; reading it is part of the operation.
A JTBD's classification controls the SKILL.md template body for that section: inline carries the full recipe; reference and script carry only Triggers, Inputs, a one-line Recipe pointer, Validation, and a terse Failure-modes summary, with the long body living in the linked file. The classification belongs in the Step 10 summary so the user can sanity-check it.
Pass 3 is mandatory. "Inline" is a rare exception, not a default.
Every merit signal that earned a JTBD a section in Pass 2
(caller-populated label, computed field, DB-unguarded lifecycle gate,
cascade flow, junction without uniqueness, materialization handoff,
side-effect fields on transition) is also a reason the JTBD belongs
in references/ rather than inline. If you find yourself classifying
every surviving JTBD as inline, you have not classified, you have
skipped Pass 3. Re-read the merit table: each signal's recipe needs
read-first, branch, compose, recompute, or cascade logic that runs to
~30+ lines including comments. None of that fits the inline criterion
(≤5 lines, no branching, no composition).
Concrete shape of a healthy classification, derived from prior generations applied with the sharpened "user-prompt branch" rule:
- A skill with 5–10 JTBDs typically produces 3–5 reference files, 3–5 script files, and 0–2 inline JTBDs. A run that produces zero reference files is suspicious (most domains have at least one user-confirmation branch); a run that produces zero script files is almost always a misclassification (most domains have at least one cascade or pure-mechanical operation).
- The ratio of reference + script to inline should be at least 3:1. If your classification gives you 7 inline + 1 reference, walk back through the merit table and force yourself to name, for each inline JTBD, why it has zero of: read-first, label composition, computed recompute, cascade, branching. If you cannot, the JTBD is at least a reference, possibly a script.
- Cascade-shaped JTBDs (Pattern A side effects + Pattern C materialization, e.g. "ship the release" with a feature-status sweep) are scripts, unless they carry a user-confirmation branch.
- Dedupe-on-junction JTBDs (Pattern junction-without-uniqueness, e.g. "vote on a feature", "tag a feature") are scripts. The label composition is mechanical and the script reads the parents to compose; do not promote to reference just because the label is caller-populated.
- Computed-field recompute JTBDs (e.g. "score with RICE", "recalculate total amount") are scripts. The arithmetic is mechanical; the script reads current values and writes the new ones in one PATCH.
- Lifecycle-gate JTBDs (Pattern A, e.g. "triage", "schedule", "approve") are references when at least one transition needs a user confirmation, scripts otherwise.
Sizing
After filtering, aim for 5–10 sections plus the optional Common queries appendix.
- Fewer than 5 sections after filtering: the model may be too thin to
justify a domain skill. Tell the user; ask whether to ship it anyway
or extend
use-semantiuswith a glossary file instead. - More than ~10 sections after filtering: a single skill that long under-triggers, the description gets diluted and the matcher loses signal. Run Step 3.5 (Cluster check) below; it inspects the entity graph for natural cut points and proposes a split when one exists. If no clean split surfaces, push back to the user: drop the lower-merit candidates, or split the model file itself. Proceed with one oversized skill only if the user confirms.
Step 3.5, Cluster check
After Pass 3 classifies every JTBD, before presenting the confirmation checkpoint, inspect the model for natural sub-domain cuts. Most models generate cleanly as one skill; a minority (typically ITIL-shaped or multi-process domains) read as two or three loosely coupled sub-domains stitched into one model file. A skill that spans loose sub-domains under-triggers for the same reason an oversized skill does, the description has to cover too many vocabularies and the matcher loses signal.
Run this step on every generation, regardless of JTBD count. The size-based threshold in Sizing is one trigger for splitting, not the only one; a 7-JTBD model split into two clear clusters of 4 and 3 still benefits from being two skills.
Compute the cluster signal
- Build an undirected graph. Nodes are the model's entities
(§3). Edges are FK relationships from §4, weighted by count
(multi-FK pairs get higher weight). Skip edges to ubiquitous
hub entities,
users,departments, and any §6 cross-model targets, they connect to almost everything and obscure the real clustering. - Find candidate clusters. A simple greedy modularity walk is enough; you do not need a full community-detection library here. Start with each entity in its own cluster and merge the pair whose merger most improves the within/cross edge ratio until further merges stop improving it. Aim for 2–4 clusters; stop merging when only one cluster remains or when the next merge would drop the ratio.
- Assign each JTBD to a cluster. A JTBD lives in the cluster that owns the majority of its touched entities (parents, children, joined targets in lookups). JTBDs that span clusters (e.g. one that joins entities from both) count as cross-cluster and are flagged separately.
- Compute the cut score.
cross_edges / (within_edges + cross_edges). Lower is better, a model with two genuine sub-domains scores < 0.20; a tightly interlocked single-domain model scores > 0.40.
Surface a split proposal only when all of these hold
- ≥ 2 clusters with ≥ 3 JTBDs each. (Smaller clusters are not skill-shaped; fold them into the larger neighbor.)
- Cut score < 0.25.
- ≤ 2 cross-cluster JTBDs. More than that means the recipes routinely span both halves and splitting forces the calling agent to load both skills anyway, defeating the point.
- Each cluster has its own distinct trigger vocabulary (the
cluster's JTBD verb-phrases would not naturally appear in the
other cluster's description). If the same verbs (
approve,assign,close) dominate both clusters, the matcher will thrash even with a split.
If any condition fails, the model generates as one skill and Step 3.5 produces no extra prompt.
When all conditions hold, propose the split
Add this to the Step 3 confirmation checkpoint as a fourth list, before writing files. Example shape:
The entity graph splits cleanly into two clusters:
Cluster A:
<slug>-incidents(entities: incidents, incident_comments, incident_categories; JTBDs: log incident, resolve incident, link to problem, post comment).Cluster B:
<slug>-changes(entities: changes, change_tasks, change_approvals; JTBDs: submit change, approve change, schedule change, execute change).Cross-cluster: 1 JTBD (problem-resolved-by-change) would need both skills loaded.
Cut score: 0.12. Each half has its own trigger vocabulary ("incident / ticket / outage" vs "change / deployment / maintenance window").
Generate as one skill
<slug>(default), or split into<slug>-incidentsand<slug>-changes?
The default is one skill even when the proposal fires. Splitting fragments shared cross-cutting rules, doubles the README/glossary maintenance burden, and surprises users who installed "the X skill" and find half their work routes elsewhere. Only split when the user explicitly confirms.
What changes when the user accepts a split
- Each cluster becomes its own folder under
<skills-root>/, with its own SKILL.md, README.mdx, references/, and scripts/. - Each SKILL.md's description mentions the sibling skill ("loads
alongside
<sibling-slug>for cross-process work like X") so the matcher knows the pair composes. - Cross-cluster JTBDs go in whichever cluster owns the action's
primary verb, not duplicated. If "resolve a problem with a
change" lives in
<slug>-problems(because the verb is "resolve"),<slug>-changesonly needs a one-line note that problem-resolution writes to changes; the recipe lives in the problems skill. - The
semantic_model:provenance key on every generated SKILL.md still points to the same source slug (e.g.itsm); the model file is shared, the skills are split.
Confirmation checkpoint
Present three lists to the user (four when Step 3.5 fired a split
proposal; five when platform_enforceable_gaps is non-empty):
-
Sections, the JTBDs that earned a place (one bullet each, with the merit signals that justified them).
-
Common queries, the cube queries that go in the appendix.
-
Skipped, Pass-1 candidates that failed the merit test, with the reason. The user may disagree and ask to add some back.
-
Split proposal (only when Step 3.5's conditions all held), the cluster breakdown and the one-skill-vs-split question. Default to one skill; only split when the user explicitly confirms.
-
⚠️ Platform-enforceable gaps detected (only when
platform_enforceable_gapsis non-empty), one bullet per gap listing the entity, the field, the kind (computed_fieldorvalidation_rule), and a short quote of the §3 prose evidence. Frame as a question, not a command:The model documents these rules in §3 prose, but the entity's
computed_fields/validation_rulesblocks do not encode them. A recipe-level stopgap will be baked into the generated skill, but the platform will not enforce the rule on writes that bypass the skill (direct CLI calls, other skills, future migrations).features.rice_score(computed_field): §3 says "(reach × impact × confidence) / effort". Nocomputed_fieldsentry derivesrice_score.features.release_id(validation_rule): §3 says "null until scheduled, …commitment is derived (status ∈ {planned, in_progress, shipped} ⇒ committed)". Novalidation_rulesentry gatesrelease_idagainst status.
Recommended: pause this skill-maker run, route to the
semantic-model-analystskill (Audit / Extend mode) to add the matchingcomputed_fields/validation_rulesblocks, redeploy viasemantic-model-deployer, then re-run skill-maker. The regenerated skill will surface the platform-derived fields and error codes instead of baking client-side recipes for them.Or: proceed and accept the stopgap recipes. Pick a route.
Default is to surface the gap and let the user decide; do not silently proceed past the checkpoint when this list is non-empty. If the user picks "proceed", the run continues to Step 4 with the JTBDs intact; if they pick "pause", stop the run and tell them which skill to invoke next.
Wait for confirmation before writing files. This is the only human checkpoint.
Step 4, Audit existing artifacts
If <skills-root>/<modelslug>/ already exists from a prior generation,
read every file under it and check for drift against (1) the
current source model, (2) the current generation's plan, and (3) the
current platform conventions. The audit is read-only; nothing is
rewritten in this step. Findings go into the Step 10 summary, and the
user decides whether to regenerate flagged files in Step 5–7.
Skip this step entirely on a fresh generation (target folder does not
exist or contains only a stale SKILL.md with no references/ or
scripts/). On a fresh run, there is nothing to audit; everything
gets written in Steps 5–8.
Model drift is the first thing to check
The generated skill is a frozen snapshot of the source model at
generation time. The model can move between generations, an analyst
can rename a singular_label, swap an enum value, add a
computed_field, simplify the §1 narrative, redraw the mermaid
diagram, or close out a §7.2 future-consideration. Any of those
changes leaves baked content stale. Detect drift before running
the artifact-shape checks below; a stale glossary or recipe is
worse than a non-canonical script.
The audit re-parses the source model file at MODEL_PATH (same
parse Step 2 does) and diffs the resulting facts against what is
baked into the generated files:
- README
## Semantic modelmermaid block: byte-diff against the model's §2 mermaid block. Any difference is a defect. - README body §1 paragraph(s): byte-diff against the model's §1 narrative. The README copies §1 verbatim; a mismatch means the analyst rewrote §1 (or the generator paraphrased instead of copying). Defect.
- SKILL.md domain glossary: every entity in §2 / §3 should
appear in the glossary table with its current
singular_label, and no extra entities should appear. Add / remove / rename = defect. - SKILL.md "Key enums": every enum the model declares
(§5 plus per-entity
enumfields) and every value within each enum should appear; no extras. Def
Truncated - read the full file at https://github.com/IntranetFactory/domain-map/blob/2b37cf10a536b57355ea6e966cb5cd1f6c084978/.claude/skills/semantius-skill-maker/SKILL.md.