@vertekum/schema-builder 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @vertekum/schema-builder
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [`2e9a56f`](https://github.com/moros-oxus/vertekum/commit/2e9a56f5cc52de26ee8768ae0035178ebe4d847e) Thanks [@tschemmer](https://github.com/tschemmer)! - Names may start with digits when they contain a letter (`2xs`, `4k-display`) — everywhere a name works, including pick/omit lists. Pure numbers and scale expressions keep their numeric meaning; the syntax highlighting follows suit.
8
+
9
+ ## 0.1.5
10
+
11
+ ### Patch Changes
12
+
13
+ - [`ecb2e95`](https://github.com/moros-oxus/vertekum/commit/ecb2e95fa860fa1aed780b29222aa69d474d68fe) Thanks [@tschemmer](https://github.com/tschemmer)! - Adds `vertekum schema lint`: validate `.dfn` modules — fragments and unused productions included — with findings collected and positioned per file. Grammar errors now point at the offending source (file, line, column) instead of `1:1`, and a misplaced `*` explains where the open-set mark belongs.
14
+
3
15
  ## 0.1.4
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -18,13 +18,13 @@ import { defineConfig } from '@vertekum/core';
18
18
  import { schemaBuilderExtension } from '@vertekum/schema-builder';
19
19
 
20
20
  export default defineConfig({
21
- collection: './tokens',
22
21
  extensions: [schemaBuilderExtension],
23
- schemas: [{ from: './schemas', domain: 'vocabulary', use: { 'house.json': '*.json' } }],
22
+ // the BUILT schema (step 3) validating a token set from the collection
23
+ schemas: [{ from: './schemas', use: { 'color-schema.json': 'core-tokens.json' } }],
24
24
  });
25
25
  ```
26
26
 
27
- 2. Declare the vocabulary — `schemas/house.dfn`:
27
+ 2. Declare the vocabulary — `schemas/color-schema.dfn`:
28
28
 
29
29
  ```dfn
30
30
  emphasis = subtle | bold
@@ -35,96 +35,44 @@ root = color.text.[neutral | brand | success].<emphasis>
35
35
  3. Build it, then check as usual:
36
36
 
37
37
  ```bash
38
- npx vertekum schema build # writes schemas/house.json beside the module
38
+ npx vertekum schema build # writes schemas/color-schema.json beside the module
39
39
  npx vertekum check # the built schema now governs your token names
40
40
  ```
41
41
 
42
- `schema build` accepts a single module argument, defaults to every `.dfn` under
43
- `./schemas`, and supports `--dry-run` (list without writing), `--json`, and `--check`
44
- (verify built files are current; exit 1 when stale — the CI guard). Only modules with a
45
- `root` build: the sweep skips fragment modules (denotation files that exist to be `use`d),
46
- while naming one explicitly is an error.
47
-
48
- ## The definition language
49
-
50
- A module is line-oriented: `#` comments, pragmas, `use` imports, and productions. An
51
- indented line continues the statement above it.
52
-
53
- ### Statements
54
-
55
- | statement | meaning |
56
- | --- | --- |
57
- | `name = expression` | a **production**: a named fragment (a denotation, a branch, a whole subtree) |
58
- | `root = expression` | the reserved production `build` materializes; one per module |
59
- | `use "./other.dfn"` | import a module (relative path or package specifier) |
60
- | `use "./other.dfn" as x` | alias the import: `<@x>` is its root, `<@x/name>` qualifies into it — the resolver when two imports share a basename |
61
- | `id "…"` `title "…"` `description "…"` | pragmas: the built schema's `$id`, `title`, `description` |
62
- | `scope "branch"` | the schema governs only its named top-level branches — the document root stays unsealed so sibling vocabularies can bind over the same files (default: `"document"`, which seals it) |
63
-
64
- ### Expressions
65
-
66
- | syntax | meaning |
67
- | --- | --- |
68
- | `a.b.c` | nesting — one name-tree level per step |
69
- | `a \| b` | alternation — the set of permitted names |
70
- | `[ … ]` | grouping; groups may hold full sub-paths (branches) |
71
- | `<name>` | reference a local production |
72
- | `<@name>` | reference an imported production; an imported module's *root* goes by its basename |
73
- | `<@module/name>` | qualified: that import's production alone — the resolver when two imports share a name |
74
- | `<name [a, b]>` | **pick** — only the listed members of the set |
75
- | `<name ![a, b]>` | **omit** — the set minus the listed members |
76
- | `<name*>` / `[a \| b *]` | **open set** — additions beyond the listed names are permitted, and every member (listed or added) takes the same tail |
77
- | `step?` | **optional slot** — the step may be skipped: `<role>.<emphasis>?.<state>?` grants `role`, `role.<emphasis>`, `role.<state>`, and the full path |
78
- | `100-900/100` | an additive scale, inclusive both ends, enumerated at build; a leading zero on a written endpoint (`025-100/25`) zero-pads every emitted name |
79
- | `16-64*1.25~4` | a geometric scale: `min-max*factor` (fractional ok), `~quantum` rounding each step to the nearest multiple; steps that collide onto an earlier name are a build error |
80
-
81
- Pick and omit validate every listed name against the set's members — a typo is a build
82
- error, and a modified set is a new set. Naming one derives a narrowed denotation:
42
+ The grammar reads the way a design system talks: nesting is `.`, choice is `|`,
43
+ denotations are named productions referenced as `<emphasis>`, scales are enumerated
44
+ expressions (`100-900/100`, `16-64*1.25~4`), and `?` marks a slot as optional. One
45
+ expression grants a whole syntagm:
83
46
 
84
47
  ```dfn
85
- use "./colors.dfn"
86
-
87
- limited-accents = <@accent-color [red, blue]>
88
- ```
89
-
90
- ### Style: the root reads as the syntagm
91
-
92
- State the high-level shape in the root and let unused slots collapse; give forks their own
93
- named productions instead of nesting:
94
-
95
- ```dfn
96
- property = background | text | icon | border
97
- role = brand | danger | neutral | success | warning
98
- emphasis = subtle | bold
99
- state = hovered | pressed
100
-
101
- root = color.[
102
- <property>.<role>?.<emphasis>?.<state>?
103
- | <code>
104
- ]
105
-
106
- code = text.code.[comments | keywords | strings]
107
- ```
108
-
109
- ## Built artifacts and ownership
110
-
111
- A built file lands beside its module (`house.dfn` → `house.json`) and carries a `$comment`
112
- stamp naming its source. The stamp is the ownership contract: `schema build` regenerates
113
- stamped files and **never overwrites a file whose stamp was removed** — hand-edit the JSON
114
- and delete the stamp to take ownership, and the builder leaves it alone from then on.
115
-
116
- ## Ejecting grammar
117
-
118
- `vertekum schema eject` copies any resolvable file verbatim — including `.dfn` modules
119
- from packages that ship them:
120
-
121
- ```bash
122
- npx vertekum schema eject @vertekum/schema-atlassian/color.dfn ./schemas/color.dfn
123
- npx vertekum schema build
48
+ root = color.<property>.<role>?.<emphasis>?.<state>?
124
49
  ```
125
50
 
126
- Ejecting the grammar (rather than the built JSON) means your edits stay one-line grammar
127
- edits with a rebuild, instead of surgery on generated schema. Two notes: an ejected module
128
- that `use`s relative imports needs them ejected alongside it; and the config hint `eject`
129
- prints applies to schemas you bind directly — an ejected `.dfn` is not bound, its *built*
130
- `.json` is.
51
+ ## Documentation
52
+
53
+ - [The module](./docs/language.md) — statements, pragmas (`id`, `title`, `description`,
54
+ `scope`), comments and continuation, `root` and fragments.
55
+ - [Expressions](./docs/expressions.md) — nesting, alternation, groups, references,
56
+ pick/omit, open sets, optional slots; the style that keeps a root readable.
57
+ - [Scales](./docs/scales.md) — additive and geometric ranges, zero-padding,
58
+ quantization, the locked semantics.
59
+ - [Modules and composition](./docs/modules.md) — `use` and aliasing, fragments,
60
+ aggregate roots, nested directories, shipping and ejecting grammar.
61
+ - [`schema build`](./docs/build.md) — the command, the sweep, the ownership stamp, the
62
+ `--check` CI gate, programmatic use.
63
+ - [`schema lint`](./docs/lint.md) — validate the `.dfn` sources themselves: fragments
64
+ included, every production, findings collected with positions.
65
+ - [What a build emits](./docs/emission.md) — the names-and-order schema shape, open
66
+ positions, `$defs`, scope; a complete dfn→JSON example.
67
+
68
+ ## Ownership in one paragraph
69
+
70
+ A built file lands beside its module (`color.dfn` → `color.json`) and carries a
71
+ `$comment` stamp naming its source. `schema build` regenerates stamped files and
72
+ **never overwrites a file whose stamp was removed** — deleting the stamp line is how
73
+ you take ownership of the JSON and part ways with the grammar. Until then, the `.dfn`
74
+ is the source of truth and `--check` keeps CI honest about rebuilds.
75
+
76
+ ## License
77
+
78
+ Apache-2.0
package/docs/build.md ADDED
@@ -0,0 +1,81 @@
1
+ # `vertekum schema build`
2
+
3
+ Build `.dfn` modules into the JSON Schema files the `schemas` config binds. The command
4
+ is contributed by this extension — it exists once `schemaBuilderExtension` is in the
5
+ project's config, and appears in `vertekum --help` and `vertekum describe` from then on.
6
+
7
+ ```bash
8
+ # vertekum schema build [module] [--check] [--dry-run] [--json] [--cwd <dir>]
9
+ vertekum schema build # every module under ./schemas
10
+ vertekum schema build schemas/color.dfn # one module
11
+ vertekum schema build --check # CI: verify nothing is stale
12
+ ```
13
+
14
+ | Argument / flag | What it does |
15
+ | --------------- | ----------------------------------------------------------------------------- |
16
+ | `[module]` | A `.dfn` file, relative to the working directory. Default: the sweep (below). |
17
+ | `--check` | Verify the built files on disk are current; write nothing. |
18
+ | `--dry-run` | Report what would be written without writing — owned by the runner, like every contributed command (see `@vertekum/cli`'s documentation). |
19
+ | `--json` | Machine-readable result (`files`, plus `skipped` and `fragments` under `data`). |
20
+ | `--cwd <dir>` | Project discovery starts here. |
21
+
22
+ ## The sweep
23
+
24
+ With no argument, every `.dfn` under the project's `schemas/` directory builds —
25
+ recursively, so nested module directories are included (`node_modules` is not).
26
+ Modules without a `root` are **fragments**: the sweep skips them with a notice, while
27
+ naming one explicitly is an error — see [modules](./modules.md#fragments).
28
+
29
+ ## Artifacts
30
+
31
+ Each module builds to `<module-name>.json` **beside its own file** —
32
+ `schemas/color.dfn` → `schemas/color.json`, `schemas/palette/color.dfn` →
33
+ `schemas/palette/color.json`. What the file contains is [emission](./emission.md).
34
+
35
+ Writing goes through the CLI runner: `--dry-run` lists the same files and writes
36
+ nothing, and an artifact path can never escape the working directory.
37
+
38
+ ## Ownership: the stamp
39
+
40
+ Every built file carries a `$comment` stamp naming its source module:
41
+
42
+ ```
43
+ built by @vertekum/schema-builder from schemas/color.dfn — do not hand-edit; remove this comment to take ownership
44
+ ```
45
+
46
+ The stamp is the contract:
47
+
48
+ - A **stamped** file is the builder's — regenerated on every build.
49
+ - A **stampless** file at a build target has been taken over: hand-edit the JSON and
50
+ delete the stamp line, and the builder skips it from then on — reported
51
+ (`… has local edits (no stamp) — left as is`), never overwritten silently.
52
+
53
+ ## `--check`: the staleness gate
54
+
55
+ `--check` rebuilds in memory and compares against disk, writing nothing. Any
56
+ difference is an error (exit `1`) naming the stale files:
57
+
58
+ ```
59
+ stale built schemas: schemas/color.json — run `vertekum schema build`
60
+ ```
61
+
62
+ This is the CI guard: run it wherever the built schemas are committed, and a grammar
63
+ edit that forgot its rebuild cannot land.
64
+
65
+ `--check` asks "are the artifacts **current**?" — the complementary question, "is the
66
+ grammar **sound**?" (including fragments a build never reaches), belongs to
67
+ [`schema lint`](./lint.md); CI wants both.
68
+
69
+ ## Programmatic use
70
+
71
+ The pipeline is importable from `@vertekum/schema-builder/api` for tooling that wants
72
+ the pieces directly:
73
+
74
+ | Export | Does |
75
+ | -------------------------- | ----------------------------------------------------------------- |
76
+ | `parse(source)` | `.dfn` text → the module AST. |
77
+ | `resolveModule(path)` | Parse a file and, recursively, everything it `use`s. |
78
+ | `build(resolved)` | Expand a resolved module's root into its name tree. |
79
+ | `emit(tree, options)` | Name tree → the JSON Schema document text. |
80
+ | `buildModule(path, label?)`| The whole pipeline: path in, `{ target, content }` out. |
81
+ | `isStamped(content)` / `stamp(moduleFile)` | The ownership-stamp helpers. |
@@ -0,0 +1,129 @@
1
+ # What a build emits
2
+
3
+ A built schema is ordinary JSON Schema (2020-12), in the **names-and-order** shape
4
+ hand-written vocabulary schemas use — so a generated file and a hand-written one are
5
+ interchangeable, and taking ownership of a generated file (removing the stamp) leaves
6
+ you with idiomatic schema, not machine soup.
7
+
8
+ ## The position shape
9
+
10
+ Every position in the name tree becomes:
11
+
12
+ ```json
13
+ {
14
+ "type": "object",
15
+ "properties": { "…declared children…": {} },
16
+ "patternProperties": { "^\\$": true },
17
+ "unevaluatedProperties": false
18
+ }
19
+ ```
20
+
21
+ - `properties` holds the granted names, one position schema each.
22
+ - `patternProperties: { "^\\$": true }` lets DTCG's structural members (`$value`,
23
+ `$type`, `$description`, …) pass at every level — the vocabulary governs **names**,
24
+ not the token format (the format schemas do that separately).
25
+ - `unevaluatedProperties: false` is the closure: a name outside the grant is a
26
+ violation, which is the entire point of a vocabulary schema.
27
+
28
+ ## Open positions
29
+
30
+ A position whose set is [open](./expressions.md#open-sets) (`*`) replaces the closure
31
+ with a route for additions:
32
+
33
+ ```json
34
+ {
35
+ "type": "object",
36
+ "properties": { "…the listed members…": {} },
37
+ "patternProperties": { "^\\$": true },
38
+ "additionalProperties": { "…the members' shared tail…": {} }
39
+ }
40
+ ```
41
+
42
+ Additions join the set: they take the same tail every listed member has (which is
43
+ well-defined precisely because `*` is restricted to name-only sets).
44
+
45
+ ## Denotations become `$defs`
46
+
47
+ A terminal denotation referenced from **two or more** positions is emitted once under
48
+ `$defs` and `$ref`'d — matching how a hand author avoids repeating themselves. A
49
+ single-use denotation is inlined. A [picked or omitted](./expressions.md#pick-and-omit)
50
+ set never shares the source's `$def`: a modified set is a different set.
51
+
52
+ ## Document keys
53
+
54
+ | Key | From |
55
+ | ----------------------- | ----------------------------------------------------------------- |
56
+ | `$schema` | always `https://json-schema.org/draft/2020-12/schema` |
57
+ | `$id` | the [`id` pragma](./language.md#pragmas), when declared |
58
+ | `$comment` | the provenance stamp — the [ownership contract](./build.md#ownership-the-stamp) |
59
+ | `title`, `description` | their pragmas, when declared |
60
+ | `$defs` | shared denotations, when any |
61
+ | the root position | `type`, `properties`, `patternProperties`, and the seal |
62
+
63
+ ## Scope
64
+
65
+ The [`scope` pragma](./language.md#pragmas) decides the **root** seal only:
66
+
67
+ - `"document"` (default): the root carries `unevaluatedProperties: false` — nothing
68
+ beyond the granted top-level names may exist in a bound file.
69
+ - `"branch"`: the root omits it — the schema governs its own branches and ignores
70
+ siblings, so several per-aspect schemas can bind over one collection. Every level
71
+ *below* the root stays sealed either way.
72
+
73
+ ## A complete example
74
+
75
+ `schemas/color.dfn`:
76
+
77
+ ```dfn
78
+ emphasis = subtle | bold
79
+
80
+ root = color.[text | icon].<emphasis>
81
+ ```
82
+
83
+ builds to `schemas/color.json`:
84
+
85
+ ```json
86
+ {
87
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
88
+ "$comment": "built by @vertekum/schema-builder from schemas/color.dfn — do not hand-edit; remove this comment to take ownership",
89
+ "$defs": {
90
+ "emphasis": {
91
+ "type": "object",
92
+ "properties": {
93
+ "subtle": {
94
+ "type": "object",
95
+ "properties": {},
96
+ "patternProperties": { "^\\$": true },
97
+ "unevaluatedProperties": false
98
+ },
99
+ "bold": {
100
+ "type": "object",
101
+ "properties": {},
102
+ "patternProperties": { "^\\$": true },
103
+ "unevaluatedProperties": false
104
+ }
105
+ },
106
+ "patternProperties": { "^\\$": true },
107
+ "unevaluatedProperties": false
108
+ }
109
+ },
110
+ "type": "object",
111
+ "properties": {
112
+ "color": {
113
+ "type": "object",
114
+ "properties": {
115
+ "text": { "$ref": "#/$defs/emphasis" },
116
+ "icon": { "$ref": "#/$defs/emphasis" }
117
+ },
118
+ "patternProperties": { "^\\$": true },
119
+ "unevaluatedProperties": false
120
+ }
121
+ },
122
+ "patternProperties": { "^\\$": true },
123
+ "unevaluatedProperties": false
124
+ }
125
+ ```
126
+
127
+ `<emphasis>` sits at two terminal positions (`color.text`, `color.icon`), so it became
128
+ a shared `$def`. A token file bound to this schema may hold `color.text.subtle` with
129
+ any `$`-members along the way — and nothing named outside the grant.
@@ -0,0 +1,150 @@
1
+ # Expressions
2
+
3
+ An expression describes a **name tree**: which token names may exist beneath a
4
+ position, and in what order the nesting proceeds. Everything to the right of a
5
+ production's `=` is one of these forms, freely composed.
6
+
7
+ | Syntax | Form |
8
+ | ----------------------- | ---------------------------------------------------------------- |
9
+ | [`a.b.c`](#nesting) | nesting — one tree level per step |
10
+ | [`a \| b`](#alternation) | alternation — the set of permitted names |
11
+ | [`[ … ]`](#groups) | grouping; groups may hold full sub-paths |
12
+ | [`<name>`](#references) | reference a local production |
13
+ | [`<@name>`, `<@module/name>`](#references) | reference an imported production |
14
+ | [`<name [a, b]>`](#pick-and-omit) | **pick** — only the listed members |
15
+ | [`<name ![a, b]>`](#pick-and-omit) | **omit** — the set minus the listed members |
16
+ | [`<name*>`, `[a \| b *]`](#open-sets) | **open set** — additions are permitted |
17
+ | [`step?`](#optional-slots) | **optional slot** — the step may be skipped |
18
+ | [`100-900/100`, `16-64*1.25~4`](./scales.md) | a numeric scale, enumerated at build — see [scales](./scales.md) |
19
+
20
+ ## Nesting
21
+
22
+ `a.b.c` — each `.` step is one level of the name tree: the token path `a.b.c` and
23
+ nothing beside it.
24
+
25
+ ```dfn
26
+ root = color.text.base
27
+ ```
28
+
29
+ grants exactly one path: `color.text.base`.
30
+
31
+ ## Alternation
32
+
33
+ `a | b | c` — the union: this position permits these names.
34
+
35
+ ```dfn
36
+ role = brand | danger | neutral | success | warning
37
+ ```
38
+
39
+ ## Groups
40
+
41
+ `[ … ]` groups an expression, usually to alternate inside a path:
42
+
43
+ ```dfn
44
+ root = color.[text | icon].base
45
+ ```
46
+
47
+ grants `color.text.base` and `color.icon.base`.
48
+
49
+ A group's options may be full **sub-paths**, which is how one root states branches with
50
+ different shapes:
51
+
52
+ ```dfn
53
+ root = color.[
54
+ <property>.<role>
55
+ | text.code.[comments | keywords | strings]
56
+ ]
57
+ ```
58
+
59
+ ## References
60
+
61
+ | Form | Resolves to |
62
+ | ------------------- | ------------------------------------------------------------------------------ |
63
+ | `<name>` | the local production `name`. |
64
+ | `<@name>` | an imported production: searched across every import; an imported module's **root** goes by the module's key (its basename, or its `use … as` alias). |
65
+ | `<@module/name>` | qualified — that import's production alone. |
66
+
67
+ An unqualified `<@name>` that two imports could satisfy is an error naming the fix:
68
+ qualify it as `<@module/name>`. The import model — keys, aliasing, fragments — is
69
+ [modules](./modules.md).
70
+
71
+ ## Pick and omit
72
+
73
+ A reference may narrow the set it names: `[a, b]` **picks** only the listed members,
74
+ `![a, b]` **omits** them. Every listed member is validated against the set — a typo is
75
+ a build error (`'<scale>' has no member '600' to omit`), and numbers are valid members:
76
+
77
+ ```dfn
78
+ use "./space.dfn"
79
+
80
+ root = space.[<scale> | negative.<scale ![0, 1000]>]
81
+ ```
82
+
83
+ A modified set is a **new** set: it never shares the source denotation's `$def` in the
84
+ built schema — narrowing a set and changing the set are the same act. Naming the
85
+ narrowed form derives a new denotation:
86
+
87
+ ```dfn
88
+ limited-accents = <@accent-color [red, blue]>
89
+ ```
90
+
91
+ ## Open sets
92
+
93
+ `*` marks a position's set as **open**: names beyond the listed ones are permitted, and
94
+ every member — listed or added — takes the same tail.
95
+
96
+ ```dfn
97
+ root = color.<color-role*>.value
98
+ ```
99
+
100
+ grants `color.<any color-role, or any addition>.value`. The trailing `*` also works
101
+ inside a group: `[small | medium | large *]`.
102
+
103
+ `*` opens a set of **names**: the production (or group) it marks must be name-only —
104
+ names, scales, and alternations of them. Opening a set of sub-paths is a build error,
105
+ because an addition would have no well-defined shape to take. The shared tail is
106
+ exactly what makes additions meaningful; the emitted schema routes them accordingly
107
+ ([emission](./emission.md#open-positions)).
108
+
109
+ ## Optional slots
110
+
111
+ `?` marks a path step as skippable. The grant is the **slot-collapse lattice** — every
112
+ combination of present and skipped optional steps:
113
+
114
+ ```dfn
115
+ root = <role>.<emphasis>?.<state>?
116
+ ```
117
+
118
+ grants four shapes:
119
+
120
+ - `<role>`
121
+ - `<role>.<emphasis>`
122
+ - `<role>.<state>`
123
+ - `<role>.<emphasis>.<state>`
124
+
125
+ Order is preserved — `<state>` always follows `<emphasis>` when both appear; skipping
126
+ is the only freedom. This is how a vocabulary states "emphasis and state are optional
127
+ refinements" in one line instead of enumerating the lattice.
128
+
129
+ ## Style: the root reads as the syntagm
130
+
131
+ State the high-level shape in the root and let unused slots collapse; give forks their
132
+ own named productions instead of nesting them inline:
133
+
134
+ ```dfn
135
+ property = background | text | icon | border
136
+ role = brand | danger | neutral | success | warning
137
+ emphasis = subtle | bold
138
+ state = hovered | pressed
139
+
140
+ root = color.[
141
+ <property>.<role>?.<emphasis>?.<state>?
142
+ | <code>
143
+ ]
144
+
145
+ code = text.code.[comments | keywords | strings]
146
+ ```
147
+
148
+ The root is the sentence — a reader sees the whole grammar of the vocabulary at the top
149
+ of the file; the productions below are its glossary. Deep inline nesting states the
150
+ same tree while hiding its shape.
@@ -0,0 +1,96 @@
1
+ # The module
2
+
3
+ A `.dfn` file is a **module**: a line-oriented text file declaring a vocabulary. This
4
+ page is the module's structure — statements, pragmas, and imports; the expression forms
5
+ that appear to the right of `=` are [expressions](./expressions.md).
6
+
7
+ ```dfn
8
+ # color.dfn — the colour vocabulary this project enforces
9
+ title "Colour vocabulary"
10
+
11
+ use "./denotations/emphasis.dfn"
12
+
13
+ role = neutral | brand | success
14
+
15
+ root = color.[text | icon].<role>.<@emphasis>
16
+ ```
17
+
18
+ ## Lexical rules
19
+
20
+ - **Statements are lines.** One statement per line; a line that starts with whitespace
21
+ **continues** the statement above it, so a long expression wraps without any
22
+ continuation character:
23
+
24
+ ```dfn
25
+ root = color.[
26
+ <property>.<role>?.<emphasis>?
27
+ | <code>
28
+ ]
29
+ ```
30
+
31
+ - **`#` starts a comment**, running to the end of the line.
32
+ - **Identifiers** are letters, digits, and hyphens (`color-role`), and may start with
33
+ digits **when they contain a letter** (`2xs`, `4k-display`). A term that is purely
34
+ numeric keeps its numeric meaning: a bare number (`950`) or a
35
+ [scale expression](./scales.md) (`100-300/50`) — which is why hyphens inside names
36
+ never collide with range syntax.
37
+ - **Strings** (pragma and `use` arguments) are double-quoted.
38
+
39
+ ## Statements
40
+
41
+ | Statement | What it is |
42
+ | ------------------------ | ----------------------------------------------------------------- |
43
+ | [`name = expression`](#productions) | A **production**: a named fragment of the vocabulary. |
44
+ | [`root = expression`](#root) | The reserved production a build materializes. |
45
+ | [`use "…" [as name]`](#use) | Import another module. |
46
+ | [`id` / `title` / `description` / `scope` `"…"`](#pragmas) | Document metadata pragmas. |
47
+
48
+ ### Productions
49
+
50
+ `name = expression` binds a name to an expression — a denotation (a set of names), a
51
+ branch, or a whole subtree. Productions exist to be referenced (`<name>`) from other
52
+ productions or the root; an unreferenced production builds nothing on its own. A
53
+ production may not expand through itself — a reference cycle is a build error.
54
+
55
+ ### `root`
56
+
57
+ `root = expression` is the reserved production the build materializes: the module's
58
+ whole name tree grows from it. One per module.
59
+
60
+ A module **without** a root is a **fragment** — a library of productions that exists to
61
+ be `use`d by other modules. The build sweep skips fragments (with a notice); naming one
62
+ explicitly as the build argument is an error, so a typo'd `root` cannot pass silently.
63
+ See [modules](./modules.md#fragments).
64
+
65
+ ### `use`
66
+
67
+ `use "<specifier>"` imports another module; `as <name>` renames the import. Specifier
68
+ forms, import accessors, and collision handling are [modules](./modules.md).
69
+
70
+ ### Pragmas
71
+
72
+ A pragma is an identifier followed by a string, each usable at most once per module:
73
+
74
+ | Pragma | Value | Lands in the built schema as |
75
+ | ------------- | ---------------------------- | ----------------------------------------- |
76
+ | `id` | any string (usually a URI) | `$id` |
77
+ | `title` | any string | `title` |
78
+ | `description` | any string | `description` |
79
+ | `scope` | `"document"` \| `"branch"` | whether the document root is sealed |
80
+
81
+ `scope` is the one with semantics, and exactly two values:
82
+
83
+ | Value | Meaning |
84
+ | ------------ | ---------------------------------------------------------------------------------------------- |
85
+ | `"document"` | Default. The schema **seals** the document root: nothing beyond the granted names may exist. |
86
+ | `"branch"` | The schema governs only the top-level branches it names — the root stays **unsealed**, so sibling vocabularies (a colour module, a spacing module) can each bind over the same token files without rejecting each other's branches. |
87
+
88
+ A single whole-vocabulary schema wants the default; per-aspect schemas that co-validate
89
+ one collection want `"branch"`. The exact effect on the emitted JSON is
90
+ [emission](./emission.md#scope).
91
+
92
+ ## Editor support
93
+
94
+ The package ships a TextMate grammar and language configuration for `.dfn` under
95
+ `grammar/` — editors and tooling that consume TextMate grammars can pick them up from
96
+ there.