Framewerk 1 specification
The rules of the Framewerk 1 document format, stated as what a document, a reader and a validator must do, each matched to what validate() checks.
Framewerk 1 specification
This page states the rules of Framewerk 1: what a document must contain, what a reader must do with it, and what a validator must report.
The other pages teach the format by example. This one is the reference a second implementation is written against. Where the two disagree, this page is wrong and should be reported.
Status of this page
This is a working draft. It is not linked from the rest of the docs yet.
Two kinds of rule are on it, and each rule says which kind it is:
- Rules that
validate()in@framewerk/specenforces today. - Rules that are part of the format and that
validate()does not check yet. They are listed so that nobody mistakes a missing check for permission.
The rule index at the end lists every rule with what validate() does when a document breaks it. Each row of that index is run against the real validator in the package's tests.
Conformance
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as described in RFC 2119 and RFC 8174: they carry those meanings only when written in capitals.
Three things can conform:
| Conforms when | |
|---|---|
| A document | it breaks no MUST rule addressed to documents |
| A reader | it is a program that takes a document in (a renderer, a code generator, an agent tool) and follows every MUST rule addressed to readers |
| A validator | it is a reader that reports problems, and it reports an error for every broken MUST rule this page marks as checked |
A rule is addressed to documents unless it names a reader or a validator.
A validator MAY report more warnings than this page lists. It MUST NOT report an error for a document that breaks no MUST rule.
The document
A Framewerk document is one JSON value, as defined by RFC 8259, encoded as UTF-8.
DOC-1. The document MUST be a JSON object.
DOC-2. The document MUST NOT be a JSON array. validate() does not check this yet: it reports a string, a number or null at the root, and lets an array through.
The members of the object are the parts of the document. Every part is optional, and an empty object is a conforming document.
| Member | JSON type | Holds | Stability |
|---|---|---|---|
project | object | name, theme, personas, defaults | stable |
screens | array | the screens | stable |
components | object | reusable subtrees, by name | stable |
styles | array | token sets and rules | stable |
brands | array | brand identity a style is derived from | stable |
flows | array | screen-to-screen edges | stable |
flowPackages | array | named journeys through several screens | stable |
content | object | localized labels and fixture data | stable |
pages | array | grouping for large projects | stable |
aliases | object | redirects from a renamed id to the current one | stable |
requirements | array | what the product must do, linked to screens | draft |
DOC-3. A member in the table, when present, MUST have the JSON type the table gives.
DOC-4. project, when present, MUST be an object and not an array. validate() does not check this yet.
DOC-5. A reader MUST pass over a top-level member it does not know. It MUST NOT treat one as an error.
A tool that reads a document and writes it back SHOULD keep the members it does not know.
Versioning
A document says which version of the format it was written against in one top-level member, formatVersion. The value is a string of two numbers, major.minor. This page describes version 1.0.
{
"formatVersion": "1.0",
"screens": [
{
"id": "menu",
"layout": {
"_id": "menu-root",
"type": "column",
"gap": 12,
"padding": 16,
"children": [
{ "_id": "title", "type": "text", "variant": "heading", "content": "Coffee Cart" }
]
}
}
]
}
VER-1. formatVersion, when present, MUST be a string of the form major.minor, where each part is a non-negative whole number with no leading zero.
VER-2. A reader MUST refuse a document whose major version it does not know. A reader MUST accept any minor version of a major version it knows, and pass over what that minor added.
VER-3. A document SHOULD carry formatVersion. A reader MUST read a document that has none as version 1.0, and a validator reports a warning for it, never an error. Every document written before the member existed has none.
VER-4. A document that states a formatVersion and uses a draft part MUST name that part in a top-level drafts array of strings. The only draft part of version 1 is requirements, and a document uses it when its requirements array has at least one entry. A reader MUST pass over a name in drafts that it does not know.
{
"formatVersion": "1.0",
"drafts": ["requirements"],
"screens": [
{
"id": "menu",
"layout": {
"_id": "menu-root",
"type": "column",
"gap": 12,
"padding": 16,
"children": [
{ "_id": "title", "type": "text", "variant": "heading", "content": "Coffee Cart" },
{ "_id": "order", "type": "button", "variant": "primary", "content": "Order", "action": "navigate(order)" }
]
}
},
{
"id": "order",
"layout": {
"_id": "order-root",
"type": "column",
"padding": 16,
"children": [
{ "_id": "confirmed", "type": "text", "content": "One espresso, on its way." }
]
}
}
],
"requirements": [
{
"id": "order-a-coffee",
"title": "A customer orders a coffee from the menu",
"type": "user-story",
"scope": "capability",
"source": "stakeholder",
"priority": "P0",
"status": "implemented",
"linkedScreens": ["menu", "order"],
"criteria": [
{ "id": "order-button", "text": "The menu has an Order button", "verified": true }
]
}
]
}
A document has one version. Draft parts are named beside it and are not versioned on their own.
A document with no formatVersion predates both members, so it is not held to VER-4: it gets the one warning of VER-3.
A writer can leave both members to the package: withFormatVersion(document) from @framewerk/spec returns the document stating the current version and naming every draft part it uses.
project.version is the version of the project the document describes. It has nothing to do with the format version.
Identifiers and references
Parts of a document point at each other by id. An id is a JSON string. Comparison is exact: case matters and no whitespace is trimmed.
ID-1. Every screen MUST have an id that is a non-empty string, and no two screens may share one.
ID-2. Within one screen's layout tree, no two nodes may share an _id. Two screens may use the same _id.
ID-3. Every node SHOULD carry an _id. A node without one cannot be addressed by a patch, an anchor or a test, and a tool that fills ids in will not give the same node the same id twice. validate() does not report a missing _id.
aliases maps an old id to the current one, so a rename does not break every reference to the old name.
ID-4. Every alias value MUST be a non-empty string. An alias MUST NOT name the id of a live screen or component as its key. A chain of aliases MUST resolve within three steps and MUST NOT loop.
A reader resolves a screen or component id through aliases before it decides the id names nothing.
Screens and the layout tree
A screen is an object with an id and a layout. The layout is a tree of nodes. Each node has a type, its own props, and for a container type a children array of nodes.
SCR-1. Every entry of screens MUST be an object.
SCR-2. Every node MUST have a type, and the type MUST be one in the node catalog. The catalog is the node reference.
SCR-3. children, when present, MUST be an array of nodes.
SCR-4. A prop that takes a closed list of values MUST take one of them. The node reference gives the list for each such prop.
SCR-5. A reader MUST pass over a node prop it does not know. It MUST NOT treat one as an error, and it MUST still read the rest of the node.
A reader that meets a node type it does not know is reading a document that breaks SCR-2. A renderer SHOULD draw a visible placeholder in its place and continue with the rest of the tree.
The props each node takes, how the tree is laid out, and devices are described in The layout tree, Screens and devices and the node reference. Those pages are the definition of each node until its rules are written here.
Components
A component is a named subtree in components. A node of type component places one, and names it in ref.
CMP-1. A component node MUST have a ref that is a non-empty string.
CMP-2. ref MUST name a key of components, directly or through aliases. validate() does not check this yet.
Props, slots and states of components are described in Components.
Actions
An action is a string on a node that says what happens when it is pressed. Version 1 has six: navigate(screenId), back(), setState(key, value), toggleState(key), scrollTo(anchor) and openUrl(address).
ACT-1. An action SHOULD be one of the six. A validator reports any other string as a warning. A renderer MUST do nothing when a node with such an action is pressed, and MUST NOT evaluate the string in any other way.
ACT-2. The target of navigate() SHOULD be the id of a screen in the document. A validator reports a target that names nothing as a warning.
Both are warnings because the document still draws correctly. They are the most expensive mistakes to miss, so a tool that gates on validation SHOULD treat warnings as failures.
The grammar of actions and of the expressions in showWhen is described in Actions and State and expressions.
Flows
A flow is one edge from a screen to a screen. A flow package is a named journey through several screens.
FLOW-1. Every flow MUST be an object with a from and a to, and each MUST be the id of a screen in the document.
FLOW-2. Every flow package MUST have an id and a name that are non-empty strings, and no two flow packages may share an id.
FLOW-3. Every entry of a flow package's screens SHOULD be the id of a screen in the document. A validator reports one that names nothing as a warning.
Project and styles
PROJ-1. Every entry of project.personas MUST have an id and a name that are non-empty strings.
PROJ-2. No two personas may share an id. validate() does not check this yet.
PROJ-3. project.theme, when present, MUST be one of wireframe, blueprint or high-fidelity.
STY-1. Every style MUST have an id and a name that are non-empty strings, and no two styles may share an id.
Tokens, rules and modes are described in Styles and tokens.
Requirements
This part is draft. See Stability for what that means.
A requirement says what the product must do, in the same document as the screens that answer it. Requirements teaches the part by example.
REQ-1. Every entry of requirements MUST be an object.
REQ-2. A requirement MUST have an id and a title that are non-empty strings, and a type, a scope, a source and a status. No two requirements may share an id.
REQ-3. A requirement MUST have a priority. validate() does not report a requirement without one yet; it reports a priority outside the list.
REQ-4. These members take one value from a closed list:
| Member | Values |
|---|---|
type | user-story, requirement, acceptance-criteria, bug, feedback, qa-note |
scope | capability, task |
source | stakeholder, research, agent, audit |
priority | P0, P1, P2 |
status | proposed, accepted, in-progress, implemented, blocked, verified |
severity | critical, major, minor, cosmetic |
resolution | fixed, wont-fix, duplicate, cannot-reproduce, superseded |
scope is the level, and it is always written. A capability says what the product does and never closes. A task is a piece of the work that makes a capability true. A document MUST NOT leave scope out and expect a reader to assume one.
REQ-5. A link out of a requirement MUST name something in the same document:
| Member | Each entry names |
|---|---|
linkedScreens | the id of a screen |
linkedFlows | the id of a flow package |
personas | the id of an entry of project.personas |
parentCapabilityId | the id of another requirement |
REQ-6. parentCapabilityId SHOULD name a requirement whose scope is capability, and SHOULD be carried only by a requirement whose scope is task. A validator reports either the other way round as a warning.
REQ-7. supersededBy MUST be carried only with resolution set to superseded, and MUST NOT name the requirement that carries it.
REQ-8. supersededBy SHOULD name a requirement in the document. A validator reports one that names nothing as a warning, because a replacement that was later removed should not make the whole document invalid.
REQ-9. criteria, when present, MUST be an array of objects. Each criterion MUST have an id and a text that are non-empty strings, and no two criteria of one requirement may share an id. verified, when present, MUST be true or false.
REQ-10. A requirement whose status is blocked SHOULD carry a blockerReason.
REQ-11. createdAt, updatedAt, startedAt and closedAt, when present, MUST be strings.
REQ-12. Each of those four SHOULD be an ISO 8601 date with a time, such as 2026-10-13T09:30:00Z. A validator reports a date with no time as a warning.
REQ-13. A reader MUST pass over a member of a requirement that it does not know. A tool MAY add its own members to a requirement, and SHOULD choose names unlikely to be taken by a later version of the format.
status says how far the work got. resolution says why a requirement stopped being live. They are separate members: a requirement that shipped and was later replaced keeps its status and gains resolution: "superseded".
The remaining optional members are feature, description, tags, blockerReason, refCode, reporter and assignee. reporter and assignee are names to show. A document MUST NOT put an account id from a tool's own user table in either.
Validation results
A validator takes a document and returns three things: whether it is valid, a list of errors and a list of warnings.
- A document is valid exactly when the list of errors is empty.
- An error is a broken MUST rule: something a reader cannot make sense of.
- A warning is a broken SHOULD rule, or anything else worth a person's attention. A document with warnings is valid.
Each problem has a path and a message.
The path says where the problem is. It starts at the root with /, and each step is a member name or an array index counted from zero: /requirements/0/linkedScreens/1 is the second linked screen of the first requirement. A problem with the document as a whole has the path /.
The message is an English sentence for a person. Its wording is not part of the format and changes without notice. A tool MUST NOT decide what a problem is by matching the message.
A stable code for each kind of problem is planned, with a published list. Until it ships, a tool that needs to tell problems apart can only use the path.
Stability
Each part of the format is either stable or draft. The table in The document says which.
Stable
A stable part changes in a breaking way only with a new major version. These are breaking:
- removing a member, a node type, a prop or a value from a closed list
- narrowing what a member accepts
- making an optional member required
- turning a warning into an error
- changing what a reader must do with a document that was already valid
These are not breaking, and may arrive in a minor version:
- a new optional member, node type or prop
- a new warning
Whether a new value in a closed list is breaking has not been decided. Until it is, no value is added to a closed list of a stable part in a minor version.
Draft
A draft part may change in any minor version, in breaking ways as well. Every such change is listed in the changelog of @framewerk/spec, marked as a change to the format.
A document that uses a draft part says so in drafts (see VER-4), so a reader can tell before it reads the part that the shape is not yet promised.
Leaving draft
A part leaves draft on a named condition, not on a judgement that it feels finished. Either condition is enough:
- an implementation other than
@framewerk/specreads the part, or - a set period passes with no breaking change to the part.
The length of that period has not been decided. It will be written here before any part is proposed for stable.
Proposing a change
A change to the format is proposed as an issue on the public repository. The maintainers decide. The decision is recorded on the issue, and a change that is accepted is listed in the changelog.
What the format does not carry
A document is a record. Programs that do work on one are outside the format: the tool that stores it, the agents that write it, anything that compares it with code or walks a running app against it. A document that conforms does not depend on any of them.
Planning records other than requirements are not part of version 1: feature briefs, test plans, roadmaps, releases and boards. A tool MAY carry them in its own top-level members, which a reader passes over (DOC-5).
Rule index
What validate() in @framewerk/spec does today with a document that breaks each rule.
- error: the document is invalid, and an error names the place.
- warning: the document is valid, and a warning names the place.
- passed over: the rule is on the reader. Nothing is reported, which is the rule being followed.
- not checked: the rule is part of the format and
validate()does not enforce it yet.
| Rule | Level | validate() | What it says |
|---|---|---|---|
| DOC-1 | MUST | error | The document is a JSON object |
| DOC-2 | MUST | not checked | The document is not an array |
| DOC-3 | MUST | error | Each known member has its JSON type |
| DOC-4 | MUST | not checked | project is an object, not an array |
| DOC-5 | MUST | passed over | A reader passes over an unknown top-level member |
| VER-1 | MUST | error | formatVersion is a major.minor string |
| VER-2 | MUST | error | A reader refuses an unknown major version |
| VER-3 | SHOULD | warning | A document carries formatVersion; none is read as 1.0 |
| VER-4 | MUST | error | A versioned document names the draft parts it uses in drafts |
| ID-1 | MUST | error | Screen ids are non-empty and unique |
| ID-2 | MUST | error | A node _id is unique within its screen |
| ID-3 | SHOULD | not checked | Every node carries an _id |
| ID-4 | MUST | error | Aliases resolve, do not loop, and do not shadow a live id |
| SCR-1 | MUST | error | A screen is an object |
| SCR-2 | MUST | error | A node has a type from the catalog |
| SCR-3 | MUST | error | children is an array |
| SCR-4 | MUST | error | A closed-list prop takes a listed value |
| SCR-5 | MUST | passed over | A reader passes over an unknown node prop |
| CMP-1 | MUST | error | A component node has a non-empty ref |
| CMP-2 | MUST | not checked | ref names a component in the document |
| ACT-1 | SHOULD | warning | An action is one of the six |
| ACT-2 | SHOULD | warning | navigate() targets a screen in the document |
| FLOW-1 | MUST | error | A flow has a from and a to that name screens |
| FLOW-2 | MUST | error | A flow package has an id and a name; ids are unique |
| FLOW-3 | SHOULD | warning | A flow package's screens name screens |
| PROJ-1 | MUST | error | A persona has an id and a name |
| PROJ-2 | MUST | not checked | Persona ids are unique |
| PROJ-3 | MUST | error | project.theme is a listed value |
| STY-1 | MUST | error | A style has an id and a name; ids are unique |
| REQ-1 | MUST | error | A requirement is an object |
| REQ-2 | MUST | error | A requirement has id, title, type, scope, source, status; ids are unique |
| REQ-3 | MUST | not checked | A requirement has a priority |
| REQ-4 | MUST | error | Closed-list members take a listed value |
| REQ-5 | MUST | error | Links name something in the document |
| REQ-6 | SHOULD | warning | A parent is a capability, carried by a task |
| REQ-7 | MUST | error | supersededBy goes with resolution: superseded, never to itself |
| REQ-8 | SHOULD | warning | supersededBy names a requirement in the document |
| REQ-9 | MUST | error | Criteria have unique ids, a text, and a boolean verified |
| REQ-10 | SHOULD | warning | A blocked requirement says why |
| REQ-11 | MUST | error | The four dates are strings |
| REQ-12 | SHOULD | warning | The four dates are ISO 8601 with a time |
| REQ-13 | MUST | passed over | A reader passes over an unknown requirement member |