@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 +29 -0
- package/README.md +6 -2
- package/docs/build.md +31 -6
- package/docs/emission.md +110 -48
- package/docs/expressions.md +12 -2
- package/docs/format.md +75 -0
- package/docs/language.md +41 -26
- package/docs/lint.md +23 -2
- package/docs/modules.md +8 -5
- package/docs/scales.md +29 -2
- package/grammar/dfn.tmLanguage.json +33 -4
- package/package.json +4 -4
- package/src/api.ts +16 -3
- package/src/ast.ts +23 -9
- package/src/build.ts +177 -25
- package/src/cli.ts +360 -32
- package/src/emit.ts +189 -65
- package/src/format.ts +273 -0
- package/src/index.ts +27 -1
- package/src/lint.ts +21 -4
- package/src/parser.ts +98 -13
- package/src/resolve.ts +12 -5
- package/src/tokenizer.ts +56 -4
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`)
|
|
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,
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
naming one explicitly is an error
|
|
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)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
##
|
|
8
|
+
## Patterns and positions
|
|
9
9
|
|
|
10
|
-
|
|
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": { "…
|
|
22
|
+
"properties": { "…locals and expansions…": {} },
|
|
16
23
|
"patternProperties": { "^\\$": true },
|
|
17
24
|
"unevaluatedProperties": false
|
|
18
25
|
}
|
|
19
26
|
```
|
|
20
27
|
|
|
21
|
-
- `
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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),
|
|
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` |
|
|
61
|
-
| the
|
|
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
|
-
##
|
|
119
|
+
## Sealing
|
|
64
120
|
|
|
65
|
-
The [`
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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": {
|
|
116
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
package/docs/expressions.md
CHANGED
|
@@ -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
|
|
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
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
| [
|
|
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) |
|
|
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.
|
|
52
|
-
|
|
53
|
-
|
|
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 |
|
|
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"` \| `"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
| `"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
51
|
+
imported:
|
|
52
52
|
|
|
53
53
|
```dfn
|
|
54
54
|
# denotations/emphasis.dfn
|
|
55
55
|
emphasis = subtle | bold
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`
|
|
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
|
|