@vertekum/schema-builder 0.1.7 → 0.2.1

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,34 @@
1
1
  # @vertekum/schema-builder
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @vertekum/core@0.2.1
9
+
10
+ ## 0.2.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [`1faab42`](https://github.com/moros-oxus/vertekum/commit/1faab429acdd5c53565b3975654ce8da05e89bfc) Thanks [@tschemmer](https://github.com/tschemmer)! - The emission model: public productions emit as open `$defs` patterns (sealing belongs to the positions that apply them, composed via `allOf`); `:name` declares a private production — inlined, invisible to importers; the `scope` pragma names the file's nature (`document` | `def` | `inline` — a def file's root also lands as `$defs.<filename>` and rootless files emit defs-only artifacts by default) while `sealed "true" | "false"` takes over the document-top seal (`scope "branch"` parses as a deprecated alias); the empty leaf dedupes into `$defs.terminal`; `schemaId` derives artifact `$id`s from a configured base; linked emission gains `#/$defs/…` pattern refs; lint reports non-failing warnings (open-merge, deprecations). Atlassian artifacts regenerate under the new shape — the granted vocabulary is unchanged.
15
+
16
+ ### Patch Changes
17
+
18
+ - [`84172d2`](https://github.com/moros-oxus/vertekum/commit/84172d2ca90320b60ec4c42730a69c056631a81c) Thanks [@tschemmer](https://github.com/tschemmer)! - Affixed scales: a parenthesized formula may carry word fragments on either or both ends — `(2-4)xs` → `2xs 3xs 4xs`, `xs(2-4)`, `x(2-8/2)s` — with the full formula grammar (geometric ratios, quantization, zero-pad) available inside the parentheses. `evaluateScale` gains optional `prefix`/`suffix`; affixes wrap the names while values stay numeric. The additive step is now optional: `2-4` means `2-4/1`.
19
+
20
+ - [`cd36a42`](https://github.com/moros-oxus/vertekum/commit/cd36a421dbec1a71a06f173a32dbcd4482f85199) Thanks [@tschemmer](https://github.com/tschemmer)! - `schema lint`, `schema fmt`, and `schema build` accept a directory argument and sweep it — `.dfn` sources need not live under `./schemas`. A nonexistent path errors up front instead of `1:1 cannot read`.
21
+
22
+ - [`be89083`](https://github.com/moros-oxus/vertekum/commit/be89083f58d8d01062240a84679ea9967071b3c4) Thanks [@tschemmer](https://github.com/tschemmer)! - Adds `vertekum schema fmt` — canonical `.dfn` formatting with JS-literal block indentation (indent resolves from `format.indent`, then `.editorconfig`), `--check` as the CI gate — and `schema lint --fix`, which relocates a misplaced trailing `*` into the reference or group it opens. The grammar itself gains block statements: a statement ends at the first newline at bracket depth 0, so a multi-line `[ … ]` may close at any indentation, and an unclosed `[` reports its opening position.
23
+
24
+ - [`c81e024`](https://github.com/moros-oxus/vertekum/commit/c81e024133cf4f67b3cf690bfb77b849921ca9a2) Thanks [@tschemmer](https://github.com/tschemmer)! - Linked emission (`schemaBuilderExtension({ link: true })`): an unmodified `<@module>` root embedding emits a `$ref` into the child module's own artifact — `"color": { "$ref": "./primitives/color.json#/properties/color" }` — instead of inlining a duplicate. Property keys stay local so sealing is unchanged; modified, open, tailed, and production references still inline, as do modules the project does not build. Default remains self-contained artifacts.
25
+
26
+ - [`ef1ca4a`](https://github.com/moros-oxus/vertekum/commit/ef1ca4a0b696b799275c0f0acf7fde9414793f8d) Thanks [@tschemmer](https://github.com/tschemmer)! - Pick/omit lists take references as members — set algebra with sets as operands: `<@fullcolors ![<@fullcolors/saturated>, white]>` omits every saturated name plus white. Operands resolve with normal scope and privacy rules, keep member-by-member validation, and an open member reference is refused.
27
+
28
+ - [`1de5e76`](https://github.com/moros-oxus/vertekum/commit/1de5e76929f9a1e17e6625cb3bfd36a3fd86ceec) Thanks [@tschemmer](https://github.com/tschemmer)! - The input/output pair is configurable on the extension — `schemaBuilderExtension({ source: './src/dfn', out: './src/schemas' })`. `source` becomes the default sweep for `schema build`, `lint`, and `fmt`; `out` redirects built schemas, mirroring `source`'s directory structure. `schema build` also takes a positional `[out]` for one invocation: a directory argument mirrors into it, a file argument lands directly in it.
29
+ - Updated dependencies [[`84172d2`](https://github.com/moros-oxus/vertekum/commit/84172d2ca90320b60ec4c42730a69c056631a81c), [`82b695e`](https://github.com/moros-oxus/vertekum/commit/82b695e32cf1b7ee37f9d2f2f753aeff8ae3d15f)]:
30
+ - @vertekum/core@0.2.0
31
+
3
32
  ## 0.1.7
4
33
 
5
34
  ### Patch Changes
package/README.md CHANGED
@@ -41,7 +41,8 @@ npx vertekum check # the built schema now governs your token nam
41
41
 
42
42
  The grammar reads the way a design system talks: nesting is `.`, choice is `|`,
43
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
44
+ expressions — plain (`100-900/100`, `16-64*1.25~4`) or affixed (`(2-4)xs` → `2xs 3xs
45
+ 4xs`) — and `?` marks a slot as optional. One
45
46
  expression grants a whole syntagm:
46
47
 
47
48
  ```dfn
@@ -61,7 +62,10 @@ root = color.<property>.<role>?.<emphasis>?.<state>?
61
62
  - [`schema build`](./docs/build.md) — the command, the sweep, the ownership stamp, the
62
63
  `--check` CI gate, programmatic use.
63
64
  - [`schema lint`](./docs/lint.md) — validate the `.dfn` sources themselves: fragments
64
- included, every production, findings collected with positions.
65
+ included, every production, findings collected with positions; `--fix` repairs the
66
+ mechanical ones.
67
+ - [`schema fmt`](./docs/format.md) — canonical formatting: JS-literal blocks, indent
68
+ from `format.indent`/.editorconfig, `--check` for CI.
65
69
  - [What a build emits](./docs/emission.md) — the names-and-order schema shape, open
66
70
  positions, `$defs`, scope; a complete dfn→JSON example.
67
71
 
package/docs/build.md CHANGED
@@ -5,26 +5,51 @@ is contributed by this extension — it exists once `schemaBuilderExtension` is
5
5
  project's config, and appears in `vertekum --help` and `vertekum describe` from then on.
6
6
 
7
7
  ```bash
8
- # vertekum schema build [module] [--check] [--dry-run] [--json] [--cwd <dir>]
8
+ # vertekum schema build [module|directory] [out] [--check] [--dry-run] [--json] [--cwd <dir>]
9
9
  vertekum schema build # every module under ./schemas
10
10
  vertekum schema build schemas/color.dfn # one module
11
+ vertekum schema build src/dfn src/schemas # sweep a directory, mirror into an out dir
11
12
  vertekum schema build --check # CI: verify nothing is stale
12
13
  ```
13
14
 
14
15
  | Argument / flag | What it does |
15
16
  | --------------- | ----------------------------------------------------------------------------- |
16
- | `[module]` | A `.dfn` file, relative to the working directory. Default: the sweep (below). |
17
+ | `[module]` | A `.dfn` file, or a directory swept with the same semantics as the default (below). Default: the configured [`source`](#configuration). |
18
+ | `[out]` | Output directory for this run — a directory `[module]` mirrors its structure into it; a file lands directly in it. Default: the configured [`out`](#configuration), else beside each module. |
17
19
  | `--check` | Verify the built files on disk are current; write nothing. |
18
20
  | `--dry-run` | Report what would be written without writing — owned by the runner, like every contributed command (see `@vertekum/cli`'s documentation). |
19
21
  | `--json` | Machine-readable result (`files`, plus `skipped` and `fragments` under `data`). |
20
22
  | `--cwd <dir>` | Project discovery starts here. |
21
23
 
24
+ ## Configuration
25
+
26
+ The input/output pair is declared once, on the extension:
27
+
28
+ ```ts
29
+ extensions: [
30
+ schemaBuilderExtension({ source: './src/dfn', out: './src/schemas' }),
31
+ ],
32
+ schemas: [{ from: './src/schemas', use: { 'color.json': '*-tokens.json' } }],
33
+ ```
34
+
35
+ | Setting | Default | Meaning |
36
+ | -------- | ------------- | --------------------------------------------------------------- |
37
+ | `source` | `'./schemas'` | Where the `.dfn` modules live — the default sweep for `build`, [`lint`](./lint.md), and [`fmt`](./format.md). |
38
+ | `out` | — (beside) | Where built `.json` files land, mirroring `source`'s structure. |
39
+ | `link` | `false` | [Linked emission](./emission.md#linked-emission): cross-file embeddings `$ref` the child's artifact instead of inlining a copy. |
40
+ | `schemaId` | — | Base URI for derived `$id`s: each artifact gets base + its artifact-relative path; a file's `id` pragma wins. |
41
+
42
+ Input and output are a **pair**, not a redirect: `out` maps modules under `source`
43
+ (and a positional `[out]` maps modules under this invocation's `[module]` root); a
44
+ module outside the pair's root builds beside itself. The stamp-ownership contract and
45
+ `--check` follow the mapped target.
46
+
22
47
  ## The sweep
23
48
 
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).
49
+ With no argument, every `.dfn` under the configured `source` builds — recursively,
50
+ with `node_modules` excluded. Every module emits by default (a rootless one emits a
51
+ defs-only artifact); only [`scope "inline"`](./language.md#pragmas) modules are
52
+ skipped, with a notice — and naming one explicitly is an error.
28
53
 
29
54
  ## Artifacts
30
55
 
package/docs/emission.md CHANGED
@@ -1,29 +1,49 @@
1
1
  # What a build emits
2
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.
3
+ A built schema is ordinary JSON Schema (2020-12). One content rule, every file:
4
+ **public productions emit as `$defs` patterns; the root emits as the document's
5
+ `properties` — the definitive syntagma; private productions inline.** A pattern never
6
+ seals — sealing belongs to the positions that apply patterns.
7
7
 
8
- ## The position shape
8
+ ## Patterns and positions
9
9
 
10
- Every position in the name tree becomes:
10
+ A **pattern** (a `$def` body) is open at its top:
11
+
12
+ ```json
13
+ { "type": "object", "properties": { "…members…": {} }, "patternProperties": { "^\\$": true } }
14
+ ```
15
+
16
+ A **position** that applies patterns composes them under its own seal:
11
17
 
12
18
  ```json
13
19
  {
20
+ "allOf": [{ "$ref": "#/$defs/named-even" }, { "$ref": "#/$defs/named-odd" }],
14
21
  "type": "object",
15
- "properties": { "…declared children…": {} },
22
+ "properties": { "…locals and expansions…": {} },
16
23
  "patternProperties": { "^\\$": true },
17
24
  "unevaluatedProperties": false
18
25
  }
19
26
  ```
20
27
 
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.
28
+ - A reference becomes a `$ref` exactly when it **ends its path**; a tail forces
29
+ expansion at that spot (a `$ref` cannot be parameterized). Modified (pick/omit) and
30
+ open references always expand — a modified set is a new set.
31
+ - A merge that grew a pattern's member drops the `$ref` back to expansion — emission
32
+ deep-checks every pattern before referencing it.
33
+ - `patternProperties: { "^\\$": true }` lets DTCG's structural members pass at every
34
+ level; `unevaluatedProperties: false` is the closure, and 2020-12's `unevaluated`
35
+ sees through `allOf`/`$ref`, which is what makes sealed composition sound.
36
+ - The empty sealed leaf emits once as **`$defs.terminal`** and is referenced from
37
+ every leaf.
38
+
39
+ ## The file's nature
40
+
41
+ Per the [`scope` pragma](./language.md#pragmas): a `document` file emits its root tree
42
+ as sealed properties (per `sealed`, default true). A `def` file emits its `$defs` —
43
+ and when it has a root, the root ALSO lands as `$defs.root` while the document body
44
+ applies it (`allOf`, unsealed by default — pattern-natured), so the file stands alone
45
+ AND serves as a pattern source. Externally, a def module's root is referenced as the
46
+ FILE itself. An `inline` file never emits.
27
47
 
28
48
  ## Open positions
29
49
 
@@ -42,33 +62,68 @@ with a route for additions:
42
62
  Additions join the set: they take the same tail every listed member has (which is
43
63
  well-defined precisely because `*` is restricted to name-only sets).
44
64
 
45
- ## Denotations become `$defs`
65
+ ## Linked emission
66
+
67
+ By default every artifact is **self-contained** — an embedded module's expansion is
68
+ inlined, so a file validates and diffs alone. With `link: true` on the extension
69
+ (see [build § configuration](./build.md#configuration)), an unmodified `<@module>`
70
+ root embedding with nothing threaded beneath it is emitted as a reference into the
71
+ child module's own artifact instead — a document child by top-level name, a def
72
+ child's root as the whole file (or, when the child is `sealed "true"`, as its unsealed
73
+ `…#/$defs/root`), any public production by pattern:
74
+
75
+ ```json
76
+ "color": { "$ref": "./primitives/color.json#/properties/color" }
77
+ { "allOf": [{ "$ref": "./primitives/color.json" }], "…seal…": "" }
78
+ "scale": { "allOf": [{ "$ref": "./primitives/color.json#/$defs/scale" }], "…seal…": "" }
79
+ ```
80
+
81
+ Property keys stay local — the parent's seal is untouched. What still inlines under
82
+ `link: true`, for correctness: picked/omitted refs (a modified set is a new set),
83
+ open refs, refs with a non-terminal tail (the tail threads through every leaf),
84
+ `scope "inline"` children, and modules this project does not build (package imports).
85
+ Binding validation resolves the linked graph offline via the relative paths;
86
+ standalone validators need the referenced files registered.
46
87
 
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.
88
+ ## Linked emission
89
+
90
+ By default every artifact is **self-contained** — an embedded module's expansion is
91
+ inlined, so a file validates and diffs alone. With `link: true` on the extension
92
+ (see [build § configuration](./build.md#configuration)), an unmodified `<@module>`
93
+ root embedding with nothing threaded beneath it is emitted as a reference into the
94
+ child module's own artifact instead:
95
+
96
+ ```json
97
+ "color": { "$ref": "./primitives/color.json#/properties/color" }
98
+ ```
99
+
100
+ The property key stays local — the parent's seal is untouched — and the referenced
101
+ subtree is semantically identical to the inline copy. What still inlines under
102
+ `link: true`, for correctness: picked/omitted refs (a modified set is a new set),
103
+ open refs, refs with a non-terminal tail (the tail threads through every leaf),
104
+ production refs (fragments and productions have no artifact), and modules this
105
+ project does not build (package imports). Binding validation resolves the linked
106
+ pair offline; standalone validators need the referenced files registered.
51
107
 
52
108
  ## Document keys
53
109
 
54
110
  | Key | From |
55
111
  | ----------------------- | ----------------------------------------------------------------- |
56
112
  | `$schema` | always `https://json-schema.org/draft/2020-12/schema` |
57
- | `$id` | the [`id` pragma](./language.md#pragmas), when declared |
113
+ | `$id` | the [`id` pragma](./language.md#pragmas), else derived from the configured [`schemaId`](./build.md#configuration) |
58
114
  | `$comment` | the provenance stamp — the [ownership contract](./build.md#ownership-the-stamp) |
59
115
  | `title`, `description` | their pragmas, when declared |
60
- | `$defs` | shared denotations, when any |
61
- | the root position | `type`, `properties`, `patternProperties`, and the seal |
116
+ | `$defs` | `terminal`, the public patterns (declaration order), then a def-scope root |
117
+ | the document body | `allOf?`, `type`, `properties`, `patternProperties`, and the seal |
62
118
 
63
- ## Scope
119
+ ## Sealing
64
120
 
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.
121
+ The [`sealed` pragma](./language.md#pragmas) decides the **document top** only:
122
+ `"true"` (default) carries `unevaluatedProperties: false` — nothing beyond the granted
123
+ top-level names may exist in a bound file; `"false"` omits it, so several per-aspect
124
+ schemas can bind over one collection. Every level *below* the top stays sealed either
125
+ way — and patterns are open at their tops by nature, sealed by the positions that
126
+ apply them.
72
127
 
73
128
  ## A complete example
74
129
 
@@ -87,24 +142,19 @@ builds to `schemas/color.json`:
87
142
  "$schema": "https://json-schema.org/draft/2020-12/schema",
88
143
  "$comment": "built by @vertekum/schema-builder from schemas/color.dfn — do not hand-edit; remove this comment to take ownership",
89
144
  "$defs": {
145
+ "terminal": {
146
+ "type": "object",
147
+ "properties": {},
148
+ "patternProperties": { "^\\$": true },
149
+ "unevaluatedProperties": false
150
+ },
90
151
  "emphasis": {
91
152
  "type": "object",
92
153
  "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
- }
154
+ "subtle": { "$ref": "#/$defs/terminal" },
155
+ "bold": { "$ref": "#/$defs/terminal" }
105
156
  },
106
- "patternProperties": { "^\\$": true },
107
- "unevaluatedProperties": false
157
+ "patternProperties": { "^\\$": true }
108
158
  }
109
159
  },
110
160
  "type": "object",
@@ -112,8 +162,18 @@ builds to `schemas/color.json`:
112
162
  "color": {
113
163
  "type": "object",
114
164
  "properties": {
115
- "text": { "$ref": "#/$defs/emphasis" },
116
- "icon": { "$ref": "#/$defs/emphasis" }
165
+ "text": {
166
+ "allOf": [{ "$ref": "#/$defs/emphasis" }],
167
+ "type": "object",
168
+ "patternProperties": { "^\\$": true },
169
+ "unevaluatedProperties": false
170
+ },
171
+ "icon": {
172
+ "allOf": [{ "$ref": "#/$defs/emphasis" }],
173
+ "type": "object",
174
+ "patternProperties": { "^\\$": true },
175
+ "unevaluatedProperties": false
176
+ }
117
177
  },
118
178
  "patternProperties": { "^\\$": true },
119
179
  "unevaluatedProperties": false
@@ -124,6 +184,8 @@ builds to `schemas/color.json`:
124
184
  }
125
185
  ```
126
186
 
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.
187
+ `emphasis` is a public production, so it emits as an open pattern in `$defs`; the
188
+ positions `color.text` and `color.icon` apply it under their own seals; every empty
189
+ leaf is one `$ref` to `$defs.terminal`. A token file bound to this schema may hold
190
+ `color.text.subtle` with any `$`-members along the way — and nothing named outside
191
+ the grant.
@@ -15,7 +15,7 @@ production's `=` is one of these forms, freely composed.
15
15
  | [`<name ![a, b]>`](#pick-and-omit) | **omit** — the set minus the listed members |
16
16
  | [`<name*>`, `[a \| b *]`](#open-sets) | **open set** — additions are permitted |
17
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) |
18
+ | [`100-900/100`, `16-64*1.25~4`, `(2-4)xs`](./scales.md) | a numeric scale, enumerated at build, optionally affixed — see [scales](./scales.md) |
19
19
 
20
20
  ## Nesting
21
21
 
@@ -80,7 +80,17 @@ use "./space.dfn"
80
80
  root = space.[<scale> | negative.<scale ![0, 1000]>]
81
81
  ```
82
82
 
83
- A modified set is a **new** set: it never shares the source denotation's `$def` in the
83
+ A member may itself be a **reference**, denoting every name of the set it names — set
84
+ algebra with sets as operands, validated member-by-member exactly like plain names
85
+ (and an open member `<x*>` is refused: an operand names a closed set):
86
+
87
+ ```dfn
88
+ use "./fullcolors.dfn"
89
+
90
+ accent-only = <@fullcolors ![<@fullcolors/saturated>, white]>
91
+ ```
92
+
93
+ A modified set is a **new** set: it never shares the source pattern's `$def` in the
84
94
  built schema — narrowing a set and changing the set are the same act. Naming the
85
95
  narrowed form derives a new denotation:
86
96
 
package/docs/format.md ADDED
@@ -0,0 +1,75 @@
1
+ # `vertekum schema fmt`
2
+
3
+ Canonical formatting for the `.dfn` sources. One style, mechanically applied — the
4
+ choices that stay yours are *where* statements break; everything inside that choice is
5
+ the formatter's.
6
+
7
+ ```bash
8
+ # vertekum schema fmt [module|directory] [--check] [--dry-run] [--json] [--cwd <dir>]
9
+ vertekum schema fmt # every module under ./schemas
10
+ vertekum schema fmt schemas/color.dfn # one module
11
+ vertekum schema fmt --check # CI: verify formatting, write nothing
12
+ ```
13
+
14
+ | Argument / flag | What it does |
15
+ | --------------- | ----------------------------------------------------------------------------- |
16
+ | `[module]` | A `.dfn` file **or directory** (swept recursively), relative to the working directory. Default: every module under the configured [`source`](./build.md#configuration) — fragments included. |
17
+ | `--check` | Verify formatting; write nothing. Unformatted files are an error (exit `1`). |
18
+ | `--dry-run` | Report what would be rewritten without writing. |
19
+ | `--json` | Machine-readable result. |
20
+ | `--cwd <dir>` | Project discovery starts here. |
21
+
22
+ A module whose grammar cannot even be lexed is **skipped with a notice** — broken
23
+ grammar is [`schema lint`](./lint.md)'s report, and fmt never rewrites what it cannot
24
+ read.
25
+
26
+ ## Blocks: JS-literal formatting, your break choices
27
+
28
+ A `[` with content on its line is **inline**; a `[` followed by a newline is a
29
+ **block** — exactly a JavaScript array literal, and exactly what the grammar means: a
30
+ statement ends at the first newline at bracket depth 0, so a block carries its
31
+ statement to the matching `]`. The formatter never changes which you chose. What it
32
+ normalizes, recursively at any depth:
33
+
34
+ - each block level indents **one unit**;
35
+ - the closing `]` dedents to its opener's line;
36
+ - a leading `|` sits at its level's indent;
37
+ - inline groups ride inside block levels without adding one.
38
+
39
+ ```dfn
40
+ root = color.[
41
+ <property>.<role>?.<emphasis>?
42
+ | border.[bold | code | focused]
43
+ | interaction.[
44
+ hovered
45
+ | pressed
46
+ ]
47
+ ]
48
+ ```
49
+
50
+ ## The indent unit resolves from what the repo uses
51
+
52
+ 1. **`format.indent`** in `vertekum.config.ts` — the same field JSON output honors.
53
+ 2. **`.editorconfig`** — standard walk-up and section cascade for the file
54
+ (`indent_style` / `indent_size`; `[*.dfn]` over `[*]`; `root = true` stops the walk).
55
+ 3. Default: two spaces.
56
+
57
+ Everything below the indent unit — bracket hugging, operator spacing, comma style — is
58
+ **canonical, deliberately without knobs**: `.dfn` files are published, ejected, and
59
+ diffed across repos, and one style is what keeps those diffs meaningful.
60
+
61
+ ## Canonical spacing
62
+
63
+ | Around | Rule | Example |
64
+ | --------------------- | ------------------------------------------- | ------------------------ |
65
+ | `=`, `\|` | one space each side | `a = b \| c` |
66
+ | `.` | tight | `color.text` |
67
+ | `?` | tight to its step | `<role>?` |
68
+ | `*` | tight in a reference; spaced before `]` | `<x*>`, `[a \| b *]` |
69
+ | pick/omit | space before `[`/`![`; `, ` between members | `<scale ![0, 1000]>` |
70
+ | group after a dot | tight | `color.[text \| icon]` |
71
+ | affixed scales | tight throughout | `(2-4)xs` |
72
+ | comments | full-line at its level; trailing two spaces off the code | `100 # loud` |
73
+
74
+ Plus hygiene: trailing whitespace stripped, blank-line runs collapse to one, exactly
75
+ one trailing newline.
package/docs/language.md CHANGED
@@ -17,15 +17,17 @@ root = color.[text | icon].<role>.<@emphasis>
17
17
 
18
18
  ## Lexical rules
19
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:
20
+ - **A statement ends at the first newline at bracket depth 0.** A `[` whose content
21
+ continues past its line opens a **block**, closed by its `]` — newlines inside are
22
+ insignificant, nesting is unbounded, and an unclosed `[` reports its opening
23
+ position. A line that starts with whitespace also continues the statement above it
24
+ (the unbracketed-wrap fallback).
23
25
 
24
26
  ```dfn
25
27
  root = color.[
26
- <property>.<role>?.<emphasis>?
28
+ <property>.<role>?.<emphasis>?
27
29
  | <code>
28
- ]
30
+ ]
29
31
  ```
30
32
 
31
33
  - **`#` starts a comment**, running to the end of the line.
@@ -33,24 +35,29 @@ root = color.[text | icon].<role>.<@emphasis>
33
35
  digits **when they contain a letter** (`2xs`, `4k-display`). A term that is purely
34
36
  numeric keeps its numeric meaning: a bare number (`950`) or a
35
37
  [scale expression](./scales.md) (`100-300/50`) — which is why hyphens inside names
36
- never collide with range syntax.
38
+ never collide with range syntax. A parenthesized formula with word affixes
39
+ (`(2-4)xs`) is one term; parentheses appear nowhere else in the grammar.
37
40
  - **Strings** (pragma and `use` arguments) are double-quoted.
38
41
 
39
42
  ## Statements
40
43
 
41
44
  | Statement | What it is |
42
45
  | ------------------------ | ----------------------------------------------------------------- |
43
- | [`name = expression`](#productions) | A **production**: a named fragment of the vocabulary. |
44
- | [`root = expression`](#root) | The reserved production a build materializes. |
46
+ | [`name = expression`](#productions) | A **public** production — a named export: importable (`<@mod/name>`) and emitted as a `$defs` pattern. |
47
+ | [`:name = expression`](#productions) | A **private** production — inlined at use, invisible to importers. |
48
+ | [`root = expression`](#root) | The default export: the definitive syntagma. |
45
49
  | [`use "…" [as name]`](#use) | Import another module. |
46
- | [`id` / `title` / `description` / `scope` `"…"`](#pragmas) | Document metadata pragmas. |
50
+ | [`id` / `title` / `description` / `scope` / `sealed` `"…"`](#pragmas) | Metadata and nature pragmas. |
47
51
 
48
52
  ### Productions
49
53
 
50
54
  `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.
55
+ branch, or a whole subtree. A production is referenced as `<name>` locally and, when
56
+ public, as `<@module/name>` by importers. Like JavaScript's exports: a plain
57
+ declaration is a named export — it emits into the artifact's `$defs` as a PATTERN —
58
+ while `:name = …` declares it **private**: usable locally, inlined wherever it
59
+ appears, refused to importers, absent from the artifact. A production may not expand
60
+ through itself — a reference cycle is a build error.
54
61
 
55
62
  ### `root`
56
63
 
@@ -71,23 +78,31 @@ forms, import accessors, and collision handling are [modules](./modules.md).
71
78
 
72
79
  A pragma is an identifier followed by a string, each usable at most once per module:
73
80
 
74
- | Pragma | Value | Lands in the built schema as |
81
+ | Pragma | Value | Meaning |
75
82
  | ------------- | ---------------------------- | ----------------------------------------- |
76
- | `id` | any string (usually a URI) | `$id` |
83
+ | `id` | any string (usually a URI) | `$id` (wins over the configured [`schemaId`](./build.md#configuration) derivation) |
77
84
  | `title` | any string | `title` |
78
85
  | `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).
86
+ | `scope` | `"document"` \| `"def"` \| `"inline"` | The file's NATURE — how it emits and how consumers reference it. |
87
+ | `sealed` | `"true"` (default) \| `"false"` | Whether the document top is sealed. |
88
+
89
+ `scope` — the file's nature:
90
+
91
+ | Value | Emitted? | root | Consumers reference |
92
+ | ------------ | -------- | ---- | ------------------------------------------------------- |
93
+ | `"document"` | own file | **required** | its top-level names (`…#/properties/<name>`) |
94
+ | `"def"` | own file | optional — also lands as `$defs.root` | the root as the **file itself** (`{ "$ref": "./color.json" }`); productions as `…#/$defs/<name>` |
95
+ | `"inline"` | never | optional | expansion only — `:private` at file level |
96
+
97
+ Absent, the nature defaults from the root: root → `document`, rootless → `def`.
98
+
99
+ `sealed` defaults by nature: a `document` seals (`"true"` — it validates alone), a
100
+ `def` file does not (`"false"` — pattern-natured, so consumers can whole-file-compose
101
+ it beside siblings); either can override. `sealed "false"` on a document lets
102
+ per-aspect vocabularies bind over the same token files without rejecting each other's
103
+ branches. (The historical `scope "branch"` still parses as an alias, with a lint
104
+ deprecation warning.)
105
+ The exact effect on the emitted JSON is [emission](./emission.md).
91
106
 
92
107
  ## Editor support
93
108
 
package/docs/lint.md CHANGED
@@ -4,14 +4,17 @@ Validate the `.dfn` sources themselves — fragments included, every production
4
4
  without building anything.
5
5
 
6
6
  ```bash
7
- # vertekum schema lint [module] [--json] [--cwd <dir>]
7
+ # vertekum schema lint [module|directory] [--fix] [--json] [--cwd <dir>]
8
8
  vertekum schema lint # every module under ./schemas
9
9
  vertekum schema lint schemas/color.dfn # one module — fragment or rooted
10
+ vertekum schema lint src/dfn # sweep any directory
11
+ vertekum schema lint --fix # repair what is mechanical first
10
12
  ```
11
13
 
12
14
  | Argument / flag | What it does |
13
15
  | --------------- | ----------------------------------------------------------------------------- |
14
- | `[module]` | A `.dfn` file, relative to the working directory. Default: every module under `./schemas`, recursively — **fragments included**. |
16
+ | `[module]` | A `.dfn` file **or directory** (swept recursively), relative to the working directory. Default: every module under the configured [`source`](./build.md#configuration) — **fragments included**. |
17
+ | `--fix` | Apply mechanical repairs before linting (below). |
15
18
  | `--json` | Machine-readable result. |
16
19
  | `--cwd <dir>` | Project discovery starts here. |
17
20
 
@@ -40,6 +43,24 @@ Lint's contract is the source:
40
43
  Findings **collect**: one broken production does not hide the next, so a module with
41
44
  three mistakes is one lint run, not three.
42
45
 
46
+ Lint also reports **warnings** — non-failing (exit `0` when nothing else is wrong):
47
+ an open set merging with closed siblings at one position (the whole position becomes
48
+ open — restructure or accept it), and deprecated forms such as `scope "branch"`
49
+ (write `sealed "false"`).
50
+
51
+ ## `--fix`
52
+
53
+ `--fix` applies the repairs that are **mechanical and unambiguous** — currently one:
54
+ a trailing open-set mark relocates into the reference or group it opens (`<roles>*`
55
+ → `<roles*>`, `[a | b]*` → `[a | b *]`), printed through the
56
+ [formatter](./format.md)'s emitter so the repaired line comes out canonical. Repairs
57
+ that would guess intent are never applied — the bare `*` (`color.*`) stays a hinted
58
+ diagnostic, because *which* set to open is yours to say.
59
+
60
+ The fixed content is validated in memory before anything is written, and the runner
61
+ persists it (`--dry-run` previews). Exit `0` when fixes clear everything; unfixable
62
+ findings still exit `1`, listed after the applied fixes.
63
+
43
64
  ## Sound vs. current
44
65
 
45
66
  `lint` and [`build --check`](./build.md#--check-the-staleness-gate) answer different
package/docs/modules.md CHANGED
@@ -45,19 +45,22 @@ use "./system/color.dfn" as system-color
45
45
  When two imports both declare `<accent>`, the unqualified `<@accent>` refuses with the
46
46
  qualified form to use. Local productions (`<name>`, no `@`) never collide with imports.
47
47
 
48
- ## Fragments
48
+ ## Fragments and natures
49
49
 
50
50
  A module without a `root` is a **fragment** — a library of denotations meant to be
51
- imported, never built:
51
+ imported:
52
52
 
53
53
  ```dfn
54
54
  # denotations/emphasis.dfn
55
55
  emphasis = subtle | bold
56
56
  ```
57
57
 
58
- The build sweep skips fragments with a notice (`… declares no root (a fragment) —
59
- skipped`); naming one explicitly as the build argument is an error, so a module whose
60
- `root` was mistyped cannot be silently skipped.
58
+ By default a fragment still EMITS — a defs-only artifact (`$defs.emphasis`), its
59
+ public productions addressable by consumers. A file that should never emit says so:
60
+ [`scope "inline"`](./language.md#pragmas) — the sweep skips it with a notice, and
61
+ naming it explicitly as the build argument is an error. Private productions
62
+ (`:name = …`) are invisible to importers in every nature — a qualified reference to
63
+ one is refused, naming the module's public productions.
61
64
 
62
65
  ## Aggregate roots
63
66