@terpjs/spec 0.25.0 → 0.26.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/README.md +15 -4
- package/VERSION +1 -1
- package/catalog/frontend/layout-contract.json +2 -2
- package/layout-declaration.schema.json +78 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -67,17 +67,28 @@ The artifacts:
|
|
|
67
67
|
extensions, deep-import segments). The portable prohibition rules cite it
|
|
68
68
|
structurally (the `restricted_surface` catalog field); which sanctioned
|
|
69
69
|
component answers each primitive is per-stack configuration.
|
|
70
|
-
7. **The
|
|
70
|
+
7. **The layout declaration** (`layout-declaration.schema.json`) — the
|
|
71
|
+
stack-neutral, normative schema for the document an app checks in to declare
|
|
72
|
+
which slot-typed page contract it opts into, which palette it starts on, and
|
|
73
|
+
how its application shell is shaped (content measure, density, navigation
|
|
74
|
+
placement, the navigation groups a module's items name by id, and the mark
|
|
75
|
+
the app is recognised by). One document, so a build-time checker and the running app read the
|
|
76
|
+
same bytes and neither can hold a different answer — and so a tool that edits
|
|
77
|
+
files rather than code can read and rewrite those choices. Naming the contract
|
|
78
|
+
and naming a palette are the app's to do and their values are per-stack, hence
|
|
79
|
+
plain strings; the shell's vocabulary is fixed normatively, and the reference
|
|
80
|
+
stack's resolver is parity-tested against it.
|
|
81
|
+
8. **The residual ratchet** (`corpus/RESIDUALS.json`) — the statically-erased
|
|
71
82
|
or renamed forms deliberately outside the corpus contract, per rule, as
|
|
72
83
|
shrink-only data (see "Detector boundaries" below).
|
|
73
|
-
|
|
84
|
+
9. **The scorecard format** (`scorecard.schema.json`) — the machine-readable
|
|
74
85
|
certification summary a conformant checker emits (spec version, per-rule
|
|
75
86
|
verdicts over the corpus, residuals claimed), so "certified against spec
|
|
76
87
|
X.Y.Z" is a verifiable artifact instead of a claim.
|
|
77
|
-
|
|
88
|
+
10. **The changelog** (`CHANGELOG.md`) — the change history keyed to `VERSION`
|
|
78
89
|
(the top entry must match, held by the spec suite), so a checker certified
|
|
79
90
|
against an earlier version can see exactly what changed since.
|
|
80
|
-
|
|
91
|
+
11. **The rule pages** (`docs/rules/`) — plain-language documentation generated
|
|
81
92
|
from the catalog (`tools/generate_rule_docs.py`; regenerate-and-compare
|
|
82
93
|
parity in the spec suite, so the pages cannot drift from the data).
|
|
83
94
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.26.1
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "frontend/layout-contract",
|
|
3
3
|
"surface": "frontend",
|
|
4
4
|
"title": "An opted-in app's archetype body slots accept only the contract's components",
|
|
5
|
-
"intent": "With a checked-in layout
|
|
5
|
+
"intent": "With a checked-in layout declaration, each page archetype's body slot is constrained to the contract's sanctioned components, so screens stay structurally consistent and the failure message tells an agent exactly how to build the screen. Paired with the fail-closed runtime check on the rendered slot. The declaration is the single source for the opt-in, for the palette the app opens on, and for the shell's own shape down to the navigation groups a module's items name by id and the mark the app is recognised by (layout-declaration.schema.json): a build-time checker and the running app read the same document, so neither can enforce a contract the other does not, and a key declared both in the document and in the app's own code is refused rather than resolved by an invisible precedence.",
|
|
6
6
|
"layer": "static-bespoke",
|
|
7
7
|
"enforcement": [
|
|
8
8
|
{
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"opt_out": "// terp-allow-layout-contract: <reason>",
|
|
21
|
-
"reference": "frontend/layout-contract.json
|
|
21
|
+
"reference": "frontend/layout-contract.json, the document layout-declaration.schema.json describes (ADR 0079); the app imports it and the reference stack's bootstrap resolves it, refusing an unknown key, a value outside its enum, and any key declared twice. The per-stack values: contracts are the keys of LAYOUT_CONTRACTS in @terpjs/react-core, and defaultTheme names one of the palettes @terpjs/contract compiles or the reserved system. Neither list is written out here: the contract ids and the palette names both live in @terpjs/react-core's published layout.manifest.json, with the values filled in for the release an app is pinned to, and every copy of them in that stack is gated against it. A fourth copy in this field would be the one nothing checks, and it would sit in the very field this schema points a reader at for the truth. shell.navGroups is groupNav's group table in @terpjs/react-core, whose NavItem.group references it; shell.brand names paths Vite serves from the app's own frontend/public, rendered into AppShell's logo and logoDark slots. verifySlotChildren in @terpjs/react-core is the runtime DOM check.",
|
|
22
22
|
"runtime": {
|
|
23
23
|
"applicability": "required"
|
|
24
24
|
},
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Terp Standard - the app's layout declaration",
|
|
3
|
+
"description": "The normative, stack-neutral schema for the document an app checks in to declare its layout: which slot-typed page contract it opts into, which palette it starts on, and how its application shell is shaped. One document, so a build-time checker and a running app read the same bytes and neither can hold a different answer - and so a tool that edits files, rather than code, can read and rewrite these choices. Naming the contract and naming a palette are the app's to do and the values are per-stack, hence plain strings; the shell's vocabulary is fixed here, because a density or a navigation placement means the same thing on any stack. Every key is optional: an absent key is not a default, it is the app declining to declare, and a consumer must leave whatever was already in force alone. A key a consumer does not recognise must be refused rather than ignored, or a declaration that does nothing looks like one that works.",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"contract": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"minLength": 1,
|
|
10
|
+
"description": "The named slot-typed page contract this app opts into. Which contracts exist, and which components each of their page slots admits, is per-stack configuration recorded non-normatively in the catalog entry's reference field - the portable part is that the opt-in is declared here, once, for every consumer that enforces it."
|
|
11
|
+
},
|
|
12
|
+
"defaultTheme": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"description": "The palette the app starts on, until a person chooses another and for as long as they have not. Declared here rather than in the app's own code for the reason the shell's shape is: a choice reachable only by editing code is out of reach of anything that edits files, and which palette an app opens on is among the most visible choices its operator makes. Which palettes exist is per-stack configuration recorded non-normatively in the catalog entry's reference field, hence a plain string rather than an enum - the portable part is that the choice is declared here, once. The one reserved name is \"system\", which declares that the app opens on whatever light or dark preference the viewer's own platform reports; every other value names a palette the stack ships, and a consumer must refuse a name it does not ship rather than fall back to one it does."
|
|
16
|
+
},
|
|
17
|
+
"shell": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"description": "How the application shell is shaped. Separate from the contract above because that governs the inside of a page and this governs the frame around it. Everything in here has a vocabulary this schema fixes, which is what separates it from the two keys above: a density, a navigation placement, the parts of a navigation group and the parts of a brand mark all mean the same thing on any stack, while which page contracts and which palettes exist do not.",
|
|
21
|
+
"properties": {
|
|
22
|
+
"brand": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"description": "The app's own mark, as paths a consumer resolves the way it resolves any other static asset the app ships. Declared here for the reason everything else in this document is: a mark is among the most visible things about an app and it was reachable only by editing that app's source, which put it out of reach of anything that edits files. Both keys are optional, and an app that declares neither shows whatever placeholder its stack ships.",
|
|
26
|
+
"properties": {
|
|
27
|
+
"logo": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1,
|
|
30
|
+
"description": "The mark shown inside the app's own frame. A consumer sizes it to the frame rather than to itself, so an oversized asset is scaled down rather than clipped."
|
|
31
|
+
},
|
|
32
|
+
"logoDark": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"minLength": 1,
|
|
35
|
+
"description": "The mark for a dark palette, declared separately rather than derived. A mark with fixed colours cannot survive a dark background and no consumer can tell whether this one can, so an app that has a second asset says so and an app that does not says nothing and keeps one mark everywhere. A consumer that has both must choose by the appearance of the palette actually in force, not by the viewer's platform preference: an app may open on a dark palette on a light platform, and a mark chosen by the platform would then be the wrong one."
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"contentWidth": {
|
|
40
|
+
"enum": ["full", "measured"],
|
|
41
|
+
"description": "Whether routed content is capped at the shell's declared reading measure or spans the full track."
|
|
42
|
+
},
|
|
43
|
+
"density": {
|
|
44
|
+
"enum": ["comfortable", "compact"],
|
|
45
|
+
"description": "The app-wide control height and cell padding, declared once for the whole shell rather than per screen."
|
|
46
|
+
},
|
|
47
|
+
"navGroups": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"description": "The navigation groups this app declares, which is the only place they can be declared: a group spans modules, so no module can own one. A module's navigation item names a group by id, an item naming a group this list does not declare is not an error but the normal state of an app mid-adoption, and every such item falls into a trailing group with no label. Declaring two groups with the same id is an authoring error a consumer must refuse.",
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["id", "label"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"id": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"description": "The group's own name, referenced by a navigation item to say which group it belongs to. Non-empty: a consumer may well treat the empty string as a usable key at render time, where being total matters more than being strict, but declaring one is an authoring error with no legitimate transient form - it is a group nothing can name on purpose."
|
|
59
|
+
},
|
|
60
|
+
"label": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "What a consumer renders above the group's list. The empty string declares a positioning-only group: no label element at all, which is how an app places otherwise ungrouped items somewhere other than the end without inventing a heading for them. Required rather than optional, so that having no label is a decision this document states rather than a key someone forgot."
|
|
63
|
+
},
|
|
64
|
+
"order": {
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"description": "Sort key against sibling groups, ascending. Absent is the same as zero and the sort is stable, so groups that tie keep the order they are declared in - which means an app that wants only a sequence can write the groups in that sequence and set this on none of them. An integer, because a sort key is an ordinal."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"navPlacement": {
|
|
72
|
+
"enum": ["header", "sidebar"],
|
|
73
|
+
"description": "Where the primary navigation sits on a wide viewport: a full-height rail beside the content, or a row within the header."
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "The Terp Standard — stack-neutral rule catalog, violation corpus, finding format, and refused-surface declaration (ADRs 0080/0081; packaged per ADR 0082, published per ADR 0086). Data only: consumers resolve the spec root via require.resolve('@terpjs/spec/package.json').",
|
|
5
5
|
"files": [
|
|
6
6
|
"VERSION",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"app-check-report.schema.json",
|
|
9
9
|
"scorecard.schema.json",
|
|
10
10
|
"assurance-profile.schema.json",
|
|
11
|
+
"layout-declaration.schema.json",
|
|
11
12
|
"restricted-surface.json",
|
|
12
13
|
"catalog",
|
|
13
14
|
"corpus"
|