Generated from cat-harness/skills/kg/kg-core/data-modelling.md — do not edit here.

✎ Edit this page’s source

Data modelling — the entities, before the fields

One question, and everything else follows from it:

What are the things, and what is true of each of them exactly once?

A model is wrong in a way no schema check can catch when a fact that belongs to one thing is written on another. That is not a style complaint. This repository has paid for it repeatedly and the shape is always the same:

  • fallbackCapabilityId said what substitutes for lean-toolchain on five SKILLS. It is a fact about lean-toolchain. Five copies, and a change of substitute could be made four times (bean sym3).
  • fallbackRole said which role takes over on a skill, when the BPMN already said it executably. One fact, two places, nothing asserting they agreed (bean 85e8).
  • roles: said who performs this skill in 114 files, in a vocabulary that resolved against nothing, for three months (bean qif9).

Each was a field on the wrong entity, or a second copy of a fact that already had a home. Modelling is the step that would have caught all three, and it costs an afternoon against the weeks those took to unwind.

The procedure

1. Name the entities, as nouns a domain expert already uses

Not the tables you expect to build. If the people who do this work say “attestation”, the entity is Attestation, even when it will be stored beside something else. A name you invented is a name nobody will look up.

Stop when two candidate entities have the same identifier. They are one entity with two names, and carrying both is how a codebase ends up with a Role registry and an actor-id vocabulary that do not resolve against each other.

2. For each entity, say what identifies it

An entity with no identifier is an attribute of something else — that is the test, and it is decisive. If you cannot say what makes two of these the same one, you have found a field rather than a thing.

3. Put each fact on the entity it is about

For every fact, ask whose property is this? The question that settles the hard cases:

If this entity disappeared, would the fact still be true of anything?

What substitutes for X survives the disappearance of every skill that uses X, so it belongs to X. What this skill does when X is missing does not — it is the skill’s.

4. State cardinality in both directions, and mean it

“One attestation per report” and “one report per attestation” are different claims, and a model that states only the first cannot tell you whether a re-signature is an update or a second row. Write both. Where the answer is “many”, say what ORDERS them or admits there is no order.

5. Say which facts are derived, and never store those

A derived fact stored is a cache, and an unvalidated cache is worse than no cache. If a value can be computed from the model, computing it is the declaration — see 85e8, where the role a process falls back to was recoverable from the diagram that executes it.

6. Declare it where a tool can read it

A model in prose is a proposal. A model in the declaration is a model. In this repository that means a graph typology in <name>.json, a Zod schema under the declared schemas directory, and a node kind that kg-export knows — placement is the skill that decides which.

And ship a reader in the same change. Everything here that was only documentation went inert within weeks; the three examples at the top of this page are all of that kind. A field with no consumer is not a model, it is a comment with punctuation.

7. Model VARIATION as inheritance, and earn a new kind with BEHAVIOUR

Two things are nearly the same. Do they get one type with a discriminator, two types, or one type and an override?

A new KIND is earned by a BEHAVIOUR difference — a consumer has to DO something different. Everything else is an OVERRIDE on an inherited default.

Owner’s ruling, 2026-09-20: “WHO should be able to override all theme properties, just defaults to inherited. if it needs to name a new kind it should if behaviour change.”

So the default posture is inherit everything, override anything. A variant declares what it CHANGES; every unstated field comes from its parent. A variant that states no new behaviour is not a new kind, however different it looks.

The test is on the consumer, not the values. Ask: does any reader of this model take a different code path? Different colours, sizes, labels or thresholds are the same behaviour with different inputs — one kind. Different FIELDS, different units, or a field whose meaning changes — the reader must branch, and that is a kind.

Worked, and it is worked because I got it wrong first

Theme dresses three surfaces: sticky notes, a website, a print publication.

variantkind?why
iris-web vs the default web themenodifferent hex values, same fields. Nothing branches. An override.
publication vs stickyyesa sticky theme’s geometry is {minWidth, padding, fontScale} across laptop \| mobile \| card; the WHO style guide states A4 \| A5 \| A5 landscape with trim size and margin, and no column width anywhere in 66 sections. minWidth means “before the grid reflows” — a fixed page does not reflow. A renderer MUST branch.

The first draft reused {minWidth, padding, fontScale} for print, reading minWidth as trim width. It would have typechecked and meant nothing: a reader following the field name would have been told the wrong thing about a real publication. A shape that typechecks is not a shape that models.

And the palette stays SHARED across all three kinds, which is the other half of the rule. theme.ts exists because 106 hardcoded hex colours became 22 named properties; three kinds each with their own palette vocabulary would reintroduce that one level up — three spellings of “accent colour”, free to disagree about what an accent is.

What inheritance costs, and what it must not hide

A required field stays required after resolution, never before. “All three layouts, or invalid” is a property of the RESOLVED variant; a declaration that states one and inherits two is complete. Checking requiredness on the declaration instead is how inheritance turns into optionality by accident.

And an inherited value is still a fact somebody must be able to trace. A variant that overrides a value cites why, the same as any other asserted fact — otherwise the model records what the value is and loses where it came from, which is the failure the uses[] rule exists to prevent elsewhere.

8. Point from the dependent at the general node

A relation between two entities is written on ONE of them. Write it on the entity that DEPENDS, pointing at the one it depends on. A general, reused node never holds a field naming the things that use it.

Owner, 2026-09-23 (#1168): “tasks are used to implement skills, skills should not know about tasks” and “voice external to a role. should not be in data model… need to flip arrows.”

The test is the step-3 question applied to the relation: if every user of this node disappeared, would the node still be complete? A skill with no task implementing it is still a skill; a task with no skill is not a task. So the pointer lives on the task.

relationwritten onnever onwhy
a task implements a skillthe BPMN task (<bootstrap.processes:skill ref>)the skilla skill is reused by many tasks in many processes; listing them makes every new process an edit to the skill
a voice speaks for a rolethe voice (activeIn.roles)the role (voice)a role is what an actor does in a lane; how it sounds is a separate, swappable thing
a lane names a rolethe lane (partitionElementRef)the role (lanes)the role names diagrams it cannot know about
a user story is for a rolethe story (role, in scenarios/stories.json)the role (useCases)stories are added by whoever writes them, not by editing the role
a Tool satisfies a skillthe Tool (satisfies)the skill (scripts, validators, mcpServices)several Tools may satisfy one skill, and a script is added without editing the skill
a skill or capability discharges a requirement statementthe skill’s front matter or the capability (satisfies: req:<id>#<key>)the statement (satisfiedBy)a requirement is written once; what discharges it arrives later
a skill says how to read a graph typologythe skill’s front matter (graph-typologies:)the kind (skill)a kind is registered once; skills that read it come and go
a skill takes and produces a contractthe skill’s front matter (input:, output: — a path into the instance or an https IRI)a directory named after the skillthe skill can then say its contract is elsewhere, and a contract no skill names is visible as unclaimed
a test checks a skill’s contractthe test run (skill, with its cases checked against the skill’s input:/output:)the skilltests come and go; the contract does not

Prose counts. A skill body that says “the only step of initialize-harness” knows its caller just as surely as a field does, and goes stale the same way. Say what the skill does, not who calls it; the caller’s own declaration says the rest.

The general node owns its CONTRACT. What a skill takes and returns is a fact about the skill, so it is declared there: an input and an output schema reference, each either a KG schema or a pinned external schema. Tasks, Tools and tests point at the skill and are checked against that contract. That is how a general node stays complete without naming its users.

Declare which nodes are general, and let the audit hold the line. Which nodes are general is a modelling decision, so it is written down: the schema declaration’s doc comment carries @general. Today that is Role, Skill, Requirement statement, Graph typology, Capability, Actor and Process (#1168, B5). kg:audit’s arrow-direction criterion then reports two things: a @general schema whose @ref names a declaration that is not general, and a BPMN <folio:…> element pointing from a process at anything but a skill, role, decision table, convention or precondition. It cannot see a bare string field with no @ref (step 9 is how those become visible) or prose, so a clean run is a claim about declared pointers only.

Prose may name a dependent; data may not. The rule governs references, the things a tool follows. A general node’s prose can still name a dependent to explain itself. For example, a process’s documentation may say “feature-staging.yml publishes the branch” so the reader knows why a step exists. The owner kept those explanations on 2026-09-24, since deleting them loses the reason a step exists (bean epbt). If the relation matters to a tool, it must also exist as data held by the dependent, like a workflow’s # bpmn: line. The prose then explains that data and does not replace it. What prose cannot do is notice a rename, so kg:audit’s advisory prose-names-resolve criterion lists a named path whose directory exists and whose file does not. A bare name that nothing in the repository carries is left undetermined, because it could be a run’s output, a folio’s file or an example.

9. Make every cross-node reference a typed KG reference

A reference is a claim that another node exists. A bare string makes that claim in a vocabulary nothing resolves: a typo, a rename or a deleted node reads exactly like a valid name. Owner, 2026-09-23: “Things like skills should be KG reference.”

So a field that names another node holds a KG reference: the target’s kind and id, resolvable in the knowledge graph, and projected by kg-export as an @id link, never a literal. The IRI that link carries is <doc dir>/<kind>/<id>, never ….jsonld#<kind>/<id> — minted by the exporter, not by the model; kg-export §”Subject IRIs — a path, not a #fragment” owns that rule. Use the helpers that already exist rather than inventing one:

helpershapefor
KgRefSchema (schemas/carried-note.ts){kind, id, note?}any KG node
TaskRefSchema (schemas/carried-note.ts){process, task}a BPMN task: the pair, never a bare task id
VoiceRefSchema (schemas/voices.ts){instance?, voiceId}a voice, possibly in another instance
SkillNameSchema (schemas/tool-types.ts)kebab ida skill id, where a whole ref is not needed
module#Export, instance:module#Export (parseValidatorRef)string with a checked grammara schema

A reference that crosses into another instance says which instance. And a reference is resolved by a check, not trusted: the audit that reads the field fails on one that points at nothing.

Free text stays free text only when it is not a claim about another node: a description, a note, a persona. useCases was free text that named things the graph could not point at; that is the case this rule exists for, and the stories are now nodes that point at their role.

What falsifies a model

Write these down as you go and check them at the end. A model nobody tried to break is a diagram.

testwhat it catches
name two instances that differ only in a field you called an identifierthe identifier is not one
find a fact you would have to update in two placesit is on the wrong entity, or duplicated
find a relation whose cardinality changes under a plausible futurethe model states today’s data, not the domain
count the facts nothing readsthe part you modelled for its own sake
find two kinds whose consumers take the SAME code pathone of them is an override wearing a discriminator
find a required field a variant can never staterequiredness is being checked before resolution, not after
delete every user of a general node; is it still complete?a field on the general node names its users (step 8)
rename a node; does anything still read as valid?a reference is a bare string, not a KG reference (step 9)

What this skill is NOT

Not a storage design. Tables, files, indices and formats come after, and a model that assumed one of them has usually smuggled a storage constraint in as a domain fact.

Not domain content. folio-assistant is the platform. A model of clinical recommendations, FHIR profiles or any one folio’s vocabulary belongs in that folio — this skill is the procedure, and the procedure carries no subject matter. See the platform-boundary rule in placement.

Processes that run this skill

processstep(s) that name it
CRDM Phases 2–4 — BPA and requirementsPhases 3–4: Define requirements + impact

folio-assistant — code under the Apache License 2.0, documentation under CC BY 3.0. Generated schema and API references are rebuilt from source on every push to main.

deployed b08c961 · 2026-10-23T23:11:09Z