Entities and Properties
How to add typed business-data definitions to a SpecScore repository.
Frozen. The
entityandpropertyDoc-Kinds are feature-frozen per decision 0003 β One Structural Language: existing files, lint rules, and the shipped CLI support (specscore entity β¦,specscore property β¦inspecscore/specscore-cli) remain supported unchanged, but the Doc-Kinds accept no new capabilities. New structural modelling work uses ModelSpec; a legacy-to-ModelSpec migration path will be specified before formal deprecation. This page remains the author walkthrough for repositories that already use the frozen Doc-Kinds. Authoritative specs:entity,property.
Two Doc-Kinds, One Pair
SpecScore historically only described behavior. Entities and properties add a typed data layer:
| Doc-Kind | One-line job | File pattern |
|---|---|---|
| property | Reusable single-field definition (email, iso-currency-code) |
<slug>.property.md |
| entity | Typed business object that bundles properties (User, Order) |
<slug>.entity.md |
Both Doc-Kinds live anywhere under spec/features/** β typically next to the feature or module that owns them. There is no central spec/entities/ directory. See the property Feature's location rule and the matching entity Feature rule.
The Big Idea: Frontmatter is the Source of Truth
Unlike every other SpecScore Doc-Kind (feature, idea, plan, scenario, task), an entity or property file carries its structured data in YAML frontmatter, not in Markdown prose. The body is human commentary on the frontmatter, plus two machine-maintained sections.
This is a deliberate departure. The reason: every YAML parser in every language can consume the definition directly, without parsing Markdown tables. That makes downstream tooling β validators, code generators, third-party UI surfaces over the spec graph β viable on top of SpecScore.
A worked example: the email property lives in this repo as a smoke-test fixture. Its full source of truth is:
---
kind: property
id: email
data_type: string
description: An RFC 5322 email address used to identify the human or service owner of a SpecScore artifact.
checks:
required: true
max_length: 320
pattern: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+{{content}}quot;
trim: true
lowercase: true
---
That YAML is consumed by lint, by specscore property refs email (once shipped), and by anything else that wants to know what email means. The body's ## Description is for humans; the body's ## Referenced by is regenerated by specscore lint --fix from a fresh scan of every entity in the repo.
Walkthrough: Add an Entity that Reuses a Property
Suppose you want to add a User entity that has an email field, and you already have a shared email.property.md file. The entity references the property β no copy-paste of the email checks.
The full source of the smoke-test fixture in this repo, spec/features/idea/user.entity.md:
---
kind: entity
id: user
singular: User
plural: Users
description: The human or service identity that owns a SpecScore artifact.
properties:
- name: id
data_type: string
description: Stable identifier.
checks:
required: true
min_length: 1
max_length: 64
pattern: "^[a-zA-Z0-9_\\-.@]+{{content}}quot;
- name: email
ref: ./email.property.md
- name: display_name
data_type: string
checks:
required: false
max_length: 80
trim: true
- name: created_at
data_type: datetime
checks:
required: false
---
Two things to notice:
idanddisplay_nameare inline. Theirdata_typeandcheckslive directly inside the entity. Good for properties that aren't reused.emailis aref:. The entity carries onlyname: emailplus the path to the standalone Property file. The shared file owns the field's shape; this entity inherits it by reference.
The body of the entity follows the same managed-section pattern as the property file β a hand-written ## Description, then a managed ## Properties table that specscore lint --fix renders from the frontmatter, then a managed ## Referenced by section that lists every other entity that inherits from this one. See the entity Feature's body-shape rules for the canonical contract.
Inheritance: Additive Only
If one entity logically extends another, declare it in frontmatter:
---
kind: entity
id: admin-user
singular: AdminUser
plural: AdminUsers
inherits: ./user.entity.md
properties:
- name: permissions
data_type: array
checks:
required: true
items:
data_type: string
---
AdminUser carries every property of User plus the new permissions field. Lint enforces that the child does NOT redefine, tighten, or remove any inherited property β additive only in MVP. Override semantics are a tar pit; they're deferred until real reuse patterns demand them. See the inheritance rule and the no-cycles rule.
Managed Sections
Two body sections in an entity, and one in a property, are managed views over the frontmatter and the repo scan respectively. They are wrapped in canonical markers:
## Properties
<!-- managed-by: specscore lint --fix -->
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | yes | Stable identifier. |
| `email` | string *(via [email](./email.property.md))* | yes | An RFC 5322 email address. |
β¦
<!-- end-managed -->
Rules:
- Never hand-edit between the markers. Lint rejects hand-edits β the section reflects the frontmatter (or the repo scan), not human writing. If you want to change a property's description, edit the frontmatter.
specscore lint --fixrewrites the body in place. Run it before committing. Until the CLI ships entity/property awareness, hand-author the managed body to match what the spec would emit; the Feature READMEs define the exact rendering rules.- Empty back-references read
- _No references yet._. Never empty.
See the managed-section requirements for the canonical contract.
When to Use What
| You want to⦠| Use |
|---|---|
Define a single reusable field (email, iso-currency-code, slug) |
A standalone Property file |
Define a bag of fields that describes a business object (User, Order, LineItem) |
An Entity file |
| Reuse the same field shape on three different entities | Define it once as a Property; ref: it from each entity |
| One-off field used by exactly one entity | Inline the field inside the entity's properties list |
| Extend an entity that already exists | Add inherits: to the child; only append new properties |
What's Not in MVP
Named explicitly so you don't trip over a missing feature:
- Override semantics for inherited properties β child entities cannot redefine parent properties. A separate follow-on Idea will tackle this once real reuse patterns surface.
- Cross-repo
@import/@fromβref:resolves only inside the current repo. - Feature-level
consumes:/produces:declarations β letting a feature declare which entities it consumes or produces is the natural next step, but it's a separate Idea. Today, mention entities in feature prose; the structured link will land later. - i18n for
singular/plural/descriptionβ English only for now. - Recordset and field Doc-Kinds β physical-shape Doc-Kinds (CSV columns, SQL tables) are a separate concept from business-object entities; they're a follow-on Idea.
See the source Idea's Not Doing list for the full set with rationale.
See Also
- entity Feature spec β authoritative spec for the
*.entity.mdDoc-Kind. - property Feature spec β authoritative spec for the
*.property.mdDoc-Kind. - document-types-registry β describes the comma-separated
Consumer Pathform that lets one registry row cover both*.entity.mdand*.property.mdfiles. - entity-and-property-definitions Idea β the upstream Idea that motivated both Doc-Kinds.
- Plan: Entity and Property Doc-Kinds β the implementation plan tracking the rollout.
Outstanding Questions
None at this time.