ss-bs-discovering-domain-model — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ss-bs-discovering-domain-model (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are loaded inline by ss-bs-bootstrapping-project (NOT dispatched as a subagent). A domain model captures the conceptual shape of the data — what each entity represents, how they relate, what lifecycles they go through. The schema reveals tables, columns, and FKs; the code reveals state-machine transitions and business operations; but only the user can confirm which entities are load-bearing for newcomers vs. infrastructure noise, resolve ambiguous cardinality, and surface workflow exceptions the schema doesn't show (e.g., "an Order can be returned even after fulfilled"). A subagent could surface candidates from one read pass but couldn't have the back-and-forth needed to settle the cut list and the rules. So this skill stays in the coordinator's context.
Core principle: Attributes are conceptual ("amount", "expiry", "owner"), not implementation ("INT NOT NULL", "FK references customers(id)"). The audience is someone reasoning about how the business logic fits together, not someone designing a DB migration.
Announce at start: "I'm using the ss-bs-discovering-domain-model skill to build docs/DOMAIN.md with you."
You're invoked when the user picked Create / Extend / Replace for domain model. The coordinator passes you:
REPO_ROOT — absolute path to the repo rootMODE — create, extend, replace, or audit. Audit invokes the drift-check path (Step 1.6) and the drift-resolution Q0 in Step 3.EXISTING_CONTENT — verbatim current docs/DOMAIN.md content (only for extend / replace; empty otherwise)FILE_PATH — target write path (typically docs/DOMAIN.md; honors context.domain_path config override if non-default)on or off. When on, run Step 1.5 (silent diagnose) and surface Q1.5 in Step 3. When off, skip both — identical to pre-suggestion-pass behaviour. Defaulted by the coordinator from suggest.default in config and the opt-in question at bootstrap start. Always on in audit mode.extend mode. Extend merges; only replace overwrites.on (regardless of input). This is documented in the Inputs section.┌─────────────────────────────────────────────────────┐
│ MODE = create or replace │
│ → Step 1: silent code scan (schemas, models, │
│ fixtures, state machines, relationships)│
│ → Step 1.5: silent diagnose (if SUGGEST=on) │
│ → Step 2: announce findings (+ diagnoses) │
│ → Step 3: targeted questions (Q1, Q1.5 if SUGGEST,│
│ then Q2-Q5) │
│ → Step 4: synthesize draft → show to user │
│ → Step 5: refine via tweak loop (cap 3) │
│ → Step 6: atomic write │
├─────────────────────────────────────────────────────┤
│ MODE = extend │
│ → Step 1: silent code scan + read EXISTING_CONTENT│
│ → Step 1.5: silent diagnose (if SUGGEST=on) │
│ → Step 2: announce findings + gaps + diagnoses │
│ → Step 3: targeted questions on gaps + Q1.5 │
│ → Step 4: synthesize additions → show diff │
│ → Step 5: refine via tweak loop (cap 3) │
│ → Step 6: atomic write of merged content │
├─────────────────────────────────────────────────────┤
│ MODE = audit │
│ → Step 1: silent code scan │
│ → Step 1.5: silent diagnose (always on for audit) │
│ → Step 1.6: drift check vs EXISTING_CONTENT │
│ → Step 2: announce findings + drift + diagnoses │
│ → Step 3: Q0 (drift resolution) → Q1 → Q1.5 → ...│
│ → Step 4-6: as usual │
└─────────────────────────────────────────────────────┘Read all of the following that exist. Don't narrate progress to the user — this happens silently, then you announce findings once in Step 2.
Look for:
migrations/, db/migrations/, prisma/schema.prisma, schema.rb, schema.sqlmodels/*.{ts,py,rb,go}, entities/*.{ts,java,cs}, prisma/schema.prismaInventory: every table or model class. For each, capture:
status field, state enum, etc.)For typed languages: read the type definitions that mirror the DB models. These often reveal:
? in TS, Optional[] in Python, Option<> in Rust)Money, Email, UUID) — note these; they're worth mentioning in attribute descriptionsLook at tests/fixtures/, factories/, seeds/, tests/factories.ts, etc. Factories show what a typical instance looks like:
Example signal: a User factory only ever sets email, name, tenant_id. That suggests the User's load-bearing conceptual attributes are those three, even if the table has 20 columns.
Search for state-machine code:
status or state columns/fieldsenum OrderStatus { Pending, Settled, Refunded }xstate, statesman, aasm, transitionsif (status === 'pending'), order.transition_to('settled'), etc.For each entity with lifecycle states:
For each entity, read its FK declarations / associations:
has_many, belongs_to, has_one (Ruby/Rails)@OneToMany, @ManyToOne (TypeORM, JPA)relations: { customer: { ... } } (Prisma)models.ForeignKey(...) (Django)Translate to conceptual cardinality:
Drop join tables and pure-association tables. Don't list OrderItem as a top-level entity unless it carries its own meaningful attributes beyond linking Order ↔ Item.
Flag ambiguous cardinality (e.g., two FKs that could be 1:1 or 1:N depending on uniqueness constraints) for Q3.
Your inventory from Step 1a may have 30+ tables. Pre-filter to a candidate list (the user makes the final cut in Q1):
Drop automatically:
Keep as candidates:
settle_quote, reconcile_payment)Aim for a candidate list of ≤25 (you'll trim to ≤15 with the user). If you have <3 candidates, the domain may be too small for a separate domain model — flag in the Step 2 announce.
EXISTING_CONTENT (it's empty). Build candidate list from scratch.EXISTING_CONTENT and identify which entities are already documented, which are missing, and which have stale information (e.g., the existing doc says Order lifecycle is Open → Settled but the code has Open → Settled → Refunded). Flag conflicts for Q4.EXISTING_CONTENT. Build candidates fresh.For each candidate entity, hold:
null if no state machine)SUGGEST=on)If SUGGEST=off, skip this step entirely and proceed to Step 2.
Diagnose looks for domain-modeling anti-patterns and missing-but-typically-valuable structural decisions. Every diagnose finding must be evidence-cited with specific file paths or concrete counts. Abstract "you should do X" findings are not allowed.
For each category below, scan additional files if needed (within the budget — see Hard Gates). Scan each category for candidates. One strong candidate per category is the target; the aggregate cap of 5 is enforced in 1.5b after dropping unsupported candidates.
Order, OrderItem, OrderEvent) all defined as top-level entities with no aggregate-root documentation. Evidence: list the entities + the ownership signal (foreign keys, embedded relationships).Each candidate must include:
severity: one of MUST, SHOULD, INFO — see Hard Gates for the matching evidence bartitle: one-line headline (e.g., "Document Order aggregate root boundary")evidence: specific file paths or counts (e.g., prisma/schema.prisma:45, src/models/order.ts:12, src/services/order-service.ts:88)proposed_addition: exact markdown text to add to the artifact (a new entity section, a lifecycle note, or a relationships clarification)Drop any candidate that cannot be cited with specific evidence. Drop any candidate where the severity guess cannot be justified from the evidence (no MUST without observable harm).
If more than 5 candidates remain after dropping unsupported ones, rank by:
Surface the top 5. If 0 candidates remain, the candidate list is empty and Q1.5 in Step 3 is skipped silently.
MODE=audit)Compare each entry in EXISTING_CONTENT against current code state. The goal is to detect entries that have gone stale.
For each domain-model entry, check:
{Draft, Submitted} vs current code's enum {Draft, Submitted, Cancelled, Shipped}. Evidence: enum file path.User 1:N Order; current FK is unique (1:1) or has been moved to a join table (N:N). Evidence: schema diff.Each finding: kind (entity-removed / entity-added / lifecycle-drift / cardinality-drift / aspiration-met / aspiration-still-pending), entry (the doc text being challenged), evidence (file paths showing the current code state).
No cap on drift findings — every observable drift is surfaced. The user resolves each in Q0.
One short message (3-6 sentences; 3-7 when SUGGEST=on extends with the diagnose-mention sentence). Example:
"Here's what I picked up: 27 tables in prisma/schema.prisma. After dropping audit logs and join tables, I have 14 candidate entities. Strongest signals: Customer, Quote, RFQ, Order, Settlement (all with lifecycle state machines and consistent test fixtures). A few I want your call on — DiscountCode (only used in 2 places, might be too edge-case), and the cardinality between Order and PaymentMethod (could be 1:1 or 1:N from the schema alone). I'll ask, then show you a draft."If SUGGEST=on AND Step 1.5 produced ≥1 candidate, extend the announcement with: "…and I noticed a few domain-modeling clarifications worth documenting — I'll surface those after we confirm the observed entities."
If create mode and the candidate list is <3:
"I didn't find much domain shape — fewer than 3 candidate entities after filtering. The project may be too early or too thin for a separate domain model. Want to continue with what I found, or skip?"
If extend mode:
"Your existing domain model covers [N] entities. I scanned and found [M] missing candidates (and [K] entities where the existing lifecycle / relationships look stale). I'll ask about those, then propose additions / refinements."
If MODE=audit: "Audit mode. Scan + diagnose + drift check complete. Found N drift items (X entity changes, Y lifecycle/cardinality changes, Z provenance re-evaluations) — I'll surface those first in the questions, then walk through observed candidates and suggestions as usual."
Ask in this order, one question per turn. Skip a question if the scan and (for extend mode) the existing file already answered it.
MODE=audit AND Step 1.6 produced ≥1 drift finding)Ask one question per drift finding (do NOT bundle — drift items often have nuanced individual resolutions):
Question: "Drift detected: <entry summary>. Current code state: <evidence>. What's the right resolution?"
Options:
- "Update the doc to match code"
- "Keep the doc — code is wrong / will be fixed"
- "Remove the entry — no longer applies"
- "Both — clarify scope (split into multiple entries)"For provenance re-evaluation findings, the question is different:
Question: "Aspirational entry '<title>' was added on <date>. Evidence at that time: <original evidence>. Current code state: <current evidence>. Has this aspiration been realized?"
Options:
- "Yes — code has caught up. Remove the provenance marker (promote to normal)."
- "No — code still has the original problem. Keep as aspirational; refresh the marker date."
- "Drop the entry — we've decided not to pursue this aspiration."Record each resolution. Apply during Step 4 (Draft Synthesis).
The most important question. Present the candidate list with a one-line role description each, grouped by signal strength.
Question: "I found these candidate entities in your schemas, models, and fixtures. Which are the load-bearing domain entities? (Cap is 15.)"
Multi-select. Group options as:
- Core (has lifecycle + appears in business logic + fixtures): [list]
- Supporting (has fixtures + relationships but no lifecycle): [list]
- Edge cases (sparse use; you may want to include or drop): [list]
Recommended option:
- "All Core + Supporting (Recommended)" — if combined is ≤15
- Or: "Core only" — if total is otherwise too many
- Or: "All of the above" — if total ≤15 after filteringIf the user selects >15, immediately follow up:
Question: "That's more than 15. Which should I drop?"
Multi-select from the over-cap list.SUGGEST=on AND Step 1.5 produced ≥1 candidate)Question: "Here are some things I'd suggest adding even though they're
not currently codified. These are opinionated — pick any you want to
include:"
Options (multi-select, one option per Step 1.5 candidate, plus a "None" escape):
- [suggestion · <severity> · <evidence-summary>] <title>
Evidence: <evidence>
Proposed addition: <one-line summary of proposed_addition>
- ... (one option per remaining candidate, up to 5)
- "None of these — keep the domain model descriptive only"
Use the harness's multi-select question tool. Do not present as plain text.If the user picks none, treat as "no suggestions accepted" and proceed to Q2. Accepted suggestions are carried into Step 4 (Draft & Show) and rendered with provenance markers.
For each entity with a detected lifecycle, confirm completeness:
Question: "[Entity X] has these lifecycle states from the code: [list]. Is that the full set?"
Options:
- "Yes — complete"
- "Missing a state (I'll specify which)" → free-form follow-up
- "Some are redundant (I'll specify which)" → free-form follow-up
- "No lifecycle for this entity — the state field is vestigial"Cap this loop at the number of selected entities with detected lifecycles. If a user picks "Missing"/"Redundant", capture the free-form text and apply to the draft.
For each ambiguous relationship flagged in Step 1e:
Question: "[Entity A] ↔ [Entity B] — what's the cardinality?"
Options:
- "[A] has many [B]"
- "[A] has one [B]"
- "Many-to-many (via join table)"
- "Not a direct relationship — they touch the same parent but don't reference each other"Cap this loop at 5 ambiguous relationships; if more, ask the user to scan and confirm the list themselves.
If Step 1g flagged contradictions between EXISTING_CONTENT and current code, ask the user how to resolve each:
Question: "Your existing domain model says '[Entity X]: <existing lifecycle/relationship>', but the code now shows '<observed>'. What's the right resolution?"
Options:
- "Update to match current code"
- "Keep existing — the code is in violation"
- "Drop entirely — it no longer applies"
- "Document both (I'll clarify why)" → free-form follow-upQuestion: "Any non-obvious workflow rules the schema doesn't show? Examples:
- 'An Order can be returned even after Fulfilled — creates a Reversal'
- 'A User can be reactivated within 30 days of deletion'
- 'An Invoice is immutable once sent — corrections create a new Invoice'
- 'A Quote auto-expires after the Expiry timestamp via a cron job'
Free-form text. List each rule on its own line, or skip if there are none."These get woven into the relevant entity's "what it represents" paragraph or lifecycle notes.
Synthesize the draft using:
Use the canonical template (see Output Template section). Order entities thoughtfully — usually start with the most central / most-referenced (e.g., Customer first), then dependents. Show the full draft to the user, then ask:
Question: "How does this look?"
Options:
- "Approve — write it as-is" (Recommended)
- "Tweak — I'll tell you what to change"
- "Start over — wrong direction"
- "Abort — skip this file"On Tweak: capture user's free-form notes (often "rewrite the Settlement attributes — drop currency, add settlement_provider" or "move RFQ above Quote in ordering"); apply; re-show; re-ask Step 4. Cap at 3 iterations:
"We've done three rounds and the draft still isn't matching what you want. Want to: (a) keep the current draft anyway, (b) skip the domain model for now, or (c) supply the file yourself — you write the markdown, I'll save it?"
On Start over: restart Step 3 from Q1 (scan findings carry over; user answers reset).
On Abort: report skipped (declined mid-skill) to the coordinator and exit.
cat > "$FILE_PATH.tmp" <<EOF
<draft content>
EOF
mv "$FILE_PATH.tmp" "$FILE_PATH"For extend mode: merge EXISTING_CONTENT + the new entity sections / refinements into a single document, then write atomically. Preserve existing accurate entries; replace only the sections flagged in Q4.
Report to the coordinator one of:
created (mode = create, full draft written)extended (mode = extend, merged content written)replaced (mode = replace, full draft written)skipped (declined mid-skill) (user aborted partway)Each accepted Q1.5 suggestion becomes a new entity section in the artifact. Append a provenance block at the end of the Entity section using this exact format:
## <Entity Name>
**Attributes:** <list>
**Lifecycle:** <states>
**Relationships:** <list>
> _Added via bootstrap suggestion pass (YYYY-MM-DD)._
> _Evidence: <evidence summary from the Q1.5 candidate>._
> _This entity / lifecycle is observable in code but was previously undocumented._Replace YYYY-MM-DD with today's date. The three blockquote lines each carry a self-contained italic span (italic spans never cross blockquote line boundaries). The audit skill reads this marker on re-runs to ask whether the finding has been addressed.
Canonical structure (omit Lifecycle for entities with no state machine):
# Domain Model
<Optional one-paragraph intro describing the domain at a high level —
what kind of system this is, what the entities collectively represent.
Helps orient readers before they dive into the per-entity sections.>
## <Entity Name>
<One paragraph: what this entity represents in the domain. Plain language,
not technical. Include any workflow exceptions captured in Q5 here, if
relevant to the entity.>
**Key attributes:**
- <attribute-name> — <one-line meaning>
- <attribute-name> — <one-line meaning>
(3-10 attributes; conceptual, not DB columns)
**Relationships:**
- <One per line, with cardinality. E.g., "Has many `<OtherEntity>`s — each
`<OtherEntity>` belongs to exactly one `<EntityName>`.">
**Lifecycle:** (only if this entity has meaningful states)
- States: `Pending` → `Settled` → `Refunded`; or `Pending` → `Expired`
- Notes: <one or two short lines about non-obvious transitions or terminal states, including any Q5 workflow exceptions relevant to lifecycle>
## <Next Entity>
...Drafting guidelines:
int, string, DateTime). NO nullability annotations. NO column-level constraints.has many, belongs to, has exactly one). State the direction both ways if non-obvious.Example entry:
## Quote
A price commitment given to a customer in response to an RFQ, valid until
its expiry timestamp. Once accepted, it can be settled via the configured
payment method.
**Key attributes:**
- ID — opaque identifier
- Amount — total committed price (in customer's currency)
- Currency — ISO 4217 currency code
- Expiry — timestamp after which the quote is no longer honorable
- Owner — the customer the quote was issued to
**Relationships:**
- Belongs to one Customer (the recipient)
- Belongs to one RFQ (the request that produced it)
- Has at most one Settlement (created on acceptance; never replaced)
**Lifecycle:** Pending → Accepted → Settled (terminal), OR
Pending → Expired (terminal, automatic after Expiry timestamp).| Mistake | Fix |
|---|---|
Including DB column types (VARCHAR(255), INT NOT NULL) | Conceptual attributes only — no implementation details |
| Adding a Mermaid/ER diagram | Text only — no diagram syntax of any kind |
| Listing every table including join/audit/session tables | Filter to core domain — drop infrastructure-level tables |
| Listing every enum value as a lifecycle state | Only include lifecycle states with meaningful transitions |
| Lengthy entity descriptions (full-paragraph attributes) | One-line meaning per attribute; ≤2 sentences for "what it represents" |
| 20+ entities in the final draft | Trim with the user; never silently |
| Restating schema layout | Schema ≠ domain model; describe meaning, not structure |
| Forgetting to state cardinality on relationships | "Has many X" / "belongs to one X" — always state cardinality |
| Inventing lifecycles the code doesn't have | Observe, don't speculate — ask the user (Q5) for non-code-visible rules |
| Bundling multiple questions in one ask | One question per turn |
| Looping past 3 tweak iterations | Surface to user with bail options |
| Overwriting in extend mode | Extend merges; only replace overwrites |
| Surfacing a diagnose candidate without file-path evidence | Drop it; only evidence-cited candidates pass the gate |
| Surfacing >5 diagnose candidates | Hard cap is 5; rank by severity → evidence → impact, drop the rest |
| Forgetting the provenance marker on an accepted Q1.5 suggestion | Audit relies on the marker to recognize aspirational entries; without it, drift detection breaks |
| Running Step 1.5 when SUGGEST=off | Skip Step 1.5 entirely when off; do not run-but-suppress |
| Skipping Step 1.6 in audit mode | Drift detection is the audit's reason for existing |
| Bundling Q0 drift resolutions into one multi-select | Each item gets its own question — resolutions are not collectively decidable |
| Forgetting to refresh the provenance marker date when "Keep aspirational" is chosen | Audit needs the refresh to track time-since-last-evaluation |
| Forgetting to strip the provenance marker when "Promote to normal" is chosen | The marker is the audit's hook; promoting means removing it |
VARCHAR/INT/UUID as types → STOP; conceptual onlyaudit_logs or sessions table → STOP; system-level, not domainA domain model draws from two sources — what the code shows (tables, type defs, state machines, FKs) and what the user knows (which entities are load-bearing vs. infrastructure noise, workflow exceptions the schema can't capture, ambiguous cardinality only the team can resolve). A subagent could surface candidates from one read pass but couldn't have the back-and-forth needed to cut 25 candidates down to 15, confirm half a dozen lifecycle completeness questions, resolve cardinality ambiguities, and capture workflow exceptions. Routing all that through a coordinator (subagent returns 25 candidates → coordinator paraphrases to user → user replies → coordinator re-dispatches) wastes turns and drifts intent.
So this skill stays inline. It scans the code itself, then talks to the user about which entities matter and how they really behave. The four sibling skills (ss-bs-discovering-constitution, ss-bs-discovering-architecture, ss-bs-discovering-glossary, ss-bs-discovering-design) follow the same pattern for the same reason.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.