@terpjs/eslint-boundaries 0.9.0 → 0.10.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terpjs/eslint-boundaries",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Terp frontend boundary rules (as data) + the ESLint adapter: no cross-module imports, no package internals, design-token-only styling (no style/className/module stylesheets), token-styled components for raw HTML tags, router-only in-app links, generated-client-only, and browser XSS/navigation sink bans. Strict-only (no modes); governed opt-outs via terp-allow markers + the escape-hatch budget ratchet (terp-boundaries-budget).",
6
6
  "main": "./src/index.js",
@@ -25,7 +25,7 @@
25
25
  "typescript-eslint": "^8.20.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@terpjs/spec": "0.25.0",
28
+ "@terpjs/spec": "0.26.1",
29
29
  "eslint": "^10.8.0",
30
30
  "vitest": "^4.1.9"
31
31
  },
package/src/layouts.js CHANGED
@@ -35,9 +35,15 @@ export const LAYOUT_CONTRACTS = {
35
35
  description:
36
36
  "The standard three-level shape: hub bodies are card grids (HubCard only), " +
37
37
  "overview bodies are data collections (DataView / ResourceList + framework " +
38
- "states), detail bodies are record sections (DetailList / Stack / Tabs + " +
39
- "framework states); Card is allowed in overview and detail bodies as the " +
40
- "sanctioned visual separation between sections. A bespoke screen composes " +
38
+ "states), detail bodies are record sections (DetailList / Stack / Grid / Tabs " +
39
+ "+ framework states); Card is allowed in overview and detail bodies as the " +
40
+ "sanctioned visual separation between sections, and Divider / Text as a rule " +
41
+ "between sections and a lead paragraph above them. Three specialised shapes sit " +
42
+ "beside those: a form body is a container (Stack) with optional Grid / Card " +
43
+ "sections and no Field at the top level, so the body is always a container rather " +
44
+ "than a loose run of controls; a settings body is Card sections and holds no " +
45
+ "collection; and a split " +
46
+ "body is two SplitPanes and nothing else. A bespoke screen composes " +
41
47
  "the plain Page, which the contract deliberately leaves unconstrained.",
42
48
  slots: {
43
49
  HubPage: {
@@ -50,6 +56,8 @@ export const LAYOUT_CONTRACTS = {
50
56
  ModuleNav: "module-nav",
51
57
  Stack: "stack",
52
58
  Card: "card",
59
+ Divider: "divider",
60
+ Text: "text",
53
61
  EmptyState: "empty-state",
54
62
  ErrorState: "error-state",
55
63
  LoadingState: "loading-state",
@@ -57,6 +65,36 @@ export const LAYOUT_CONTRACTS = {
57
65
  ConfirmDialog: "dialog",
58
66
  },
59
67
  },
68
+ FormPage: {
69
+ components: {
70
+ Stack: "stack",
71
+ Grid: "grid",
72
+ Card: "card",
73
+ Divider: "divider",
74
+ Text: "text",
75
+ EmptyState: "empty-state",
76
+ ErrorState: "error-state",
77
+ LoadingState: "loading-state",
78
+ Alert: "alert",
79
+ ConfirmDialog: "dialog",
80
+ },
81
+ },
82
+ SettingsPage: {
83
+ components: {
84
+ Card: "card",
85
+ Stack: "stack",
86
+ Divider: "divider",
87
+ Text: "text",
88
+ EmptyState: "empty-state",
89
+ ErrorState: "error-state",
90
+ LoadingState: "loading-state",
91
+ Alert: "alert",
92
+ ConfirmDialog: "dialog",
93
+ },
94
+ },
95
+ SplitPage: {
96
+ components: { SplitPane: "splitpane" },
97
+ },
60
98
  DetailPage: {
61
99
  components: {
62
100
  DetailList: "detail-list",
@@ -65,6 +103,9 @@ export const LAYOUT_CONTRACTS = {
65
103
  ModuleNav: "module-nav",
66
104
  DataView: "dataview",
67
105
  Card: "card",
106
+ Grid: "grid",
107
+ Divider: "divider",
108
+ Text: "text",
68
109
  EmptyState: "empty-state",
69
110
  ErrorState: "error-state",
70
111
  LoadingState: "loading-state",
package/src/spec.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * NOT by this package's own suite, which certification runs against candidate spec releases
16
16
  * whose version is allowed to be newer).
17
17
  */
18
- export const SPEC_VERSION = "0.25.0";
18
+ export const SPEC_VERSION = "0.26.1";
19
19
 
20
20
  export const BOUNDARY_SPEC = {
21
21
  /** App module files the boundary + frontend security defaults apply to. */
@@ -42,12 +42,19 @@ export const BOUNDARY_SPEC = {
42
42
  * Extra guidance for an element whose named replacement does not fit every case, so the
43
43
  * refusal states what to do instead of implying something is missing.
44
44
  *
45
- * `dialog` is the one that needed it. The replacement is `ConfirmDialog`, which is right
46
- * for a confirmation and wrong advice for anything else and an author who reads "use
47
- * ConfirmDialog" for an edit form concludes the framework ships no dialog and that the
48
- * rule cannot be obeyed. The guidance below is what a reporting app arrived at on its
49
- * own, and recorded as the better outcome: the editor moved into an expanded row, so the
50
- * finding that sent the author there stayed on screen while they fixed it.
45
+ * The criterion: a restricted element earns a sentence here when its named replacement is
46
+ * right for some cases and actively misleading for others, so that an author who reads the
47
+ * bare rule concludes the framework ships nothing for their case. Two elements meet it.
48
+ *
49
+ * `dialog` was the first. The replacement is `ConfirmDialog`, which is right for a
50
+ * confirmation and wrong advice for anything else an author who reads "use ConfirmDialog"
51
+ * for an edit form concludes the framework ships no dialog and the rule cannot be obeyed.
52
+ * The guidance below is what a reporting app arrived at on its own, and recorded as the
53
+ * better outcome: the editor moved into an expanded row, so the finding that sent the author
54
+ * there stayed on screen while they fixed it.
55
+ *
56
+ * `table` is the second (ADR 0099 §4). "Use DataView" reads as disproportionate for five
57
+ * static rows unless the sentence names the two-line recipe, so it does.
51
58
  */
52
59
  restrictedElementGuidance: {
53
60
  dialog:
@@ -57,6 +64,15 @@ export const BOUNDARY_SPEC = {
57
64
  "screen. If you have a case neither covers, say so rather than reaching for a raw " +
58
65
  "<dialog>: the modal contract (focus trap, focus restore, Escape, top layer) is " +
59
66
  "what ConfirmDialog exists to provide and a hand-rolled one silently drops.",
67
+ table:
68
+ "Use DataView, and if it looks like too much for five static rows, the recipe is " +
69
+ "two lines: variant=\"embedded\" drops the view toggle, the page-size selector and " +
70
+ "the footer, and InMemoryDataViewRepository needs two functions: getRowId and " +
71
+ "getValue. That is worth " +
72
+ "saying because \"use DataView\" on its own reads as wrong advice here, and an " +
73
+ "author who believes it reaches for a raw <table> — which silently gives up " +
74
+ "sorting, column resizing and settings, the mobile card reflow and the row-level " +
75
+ "activation the rest of the app has.",
60
76
  },
61
77
  restrictedAttributes: ["style", "className"],
62
78
  /**
@@ -44,6 +44,22 @@ describe("structural parity: BOUNDARY_SPEC realises exactly the declared surface
44
44
  );
45
45
  });
46
46
 
47
+ it("every guidance sentence belongs to an element that is actually restricted", () => {
48
+ // ADR 0099 §4 makes the WORDING a control: a restricted element whose named replacement is
49
+ // right for some cases and wrong advice for others gets a sentence naming the recipe. A
50
+ // sentence keyed to an element nobody restricts is never emitted, so it reads as shipped
51
+ // guidance while being unreachable — and nothing else here would notice, because the two
52
+ // objects are only ever read one key at a time at lint time.
53
+ const guided = Object.keys(BOUNDARY_SPEC.restrictedElementGuidance).sort();
54
+ const restricted = Object.keys(BOUNDARY_SPEC.restrictedElements);
55
+ expect(guided.filter((element) => !restricted.includes(element))).toEqual([]);
56
+ // And the sentences are non-empty: an empty string is falsy, so it would silently fall back
57
+ // to the bare refusal at the call site.
58
+ for (const element of guided) {
59
+ expect(BOUNDARY_SPEC.restrictedElementGuidance[element].length).toBeGreaterThan(0);
60
+ }
61
+ });
62
+
47
63
  it("restricted attributes match", () => {
48
64
  expect([...BOUNDARY_SPEC.restrictedAttributes].sort()).toEqual(
49
65
  [...SURFACE.restrictedAttributes].sort(),