@terpjs/eslint-boundaries 0.5.3 → 0.5.4

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.5.3",
3
+ "version": "0.5.4",
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",
package/src/budget.js CHANGED
@@ -190,6 +190,7 @@ function main() {
190
190
  JSON.stringify({
191
191
  terp_findings: 1,
192
192
  tool: "terp-boundaries-budget",
193
+ ok: problems.length === 0,
193
194
  rules: ["frontend/escape-hatch"],
194
195
  findings: problems.map((problem) => ({
195
196
  rule: "frontend/escape-hatch",
@@ -178,6 +178,7 @@ describe("terp-boundaries-budget --format json (the findings envelope)", () => {
178
178
  expect(envelope.findings[0].rule).toBe("frontend/escape-hatch");
179
179
  expect(envelope.findings[0].path).toBe("escape-hatch-budget.json");
180
180
  expect(envelope.findings[0].message).toMatch(/unbudgeted marker/);
181
+ expect(envelope.ok).toBe(false);
181
182
  expect(run.stderr).toMatch(/unbudgeted marker/);
182
183
  });
183
184
 
@@ -188,6 +189,9 @@ describe("terp-boundaries-budget --format json (the findings envelope)", () => {
188
189
  const envelope = JSON.parse(run.stdout);
189
190
  expect(envelope.rules).toEqual(["frontend/escape-hatch"]);
190
191
  expect(envelope.findings).toEqual([]);
192
+ // The version marker stays 1 on a clean run — `ok` is what says it passed.
193
+ expect(envelope.terp_findings).toBe(1);
194
+ expect(envelope.ok).toBe(true);
191
195
  });
192
196
 
193
197
  it("still reads a positional budget path alongside the flag", () => {
package/src/findings.js CHANGED
@@ -8,11 +8,19 @@
8
8
  * envelope** on stdout:
9
9
  *
10
10
  * { "terp_findings": 1, "tool": "@terpjs/eslint-boundaries",
11
+ * "ok": true, // did this run pass? (no findings)
11
12
  * "rules": ["frontend/<rule>", …], // every catalog rule this run evaluated
12
13
  * "not_applicable": ["frontend/<rule>", …], // opt-in rules this app has not enabled
13
14
  * "findings": [{ rule, path, line, message }, …], // spec findings.schema.json shape
14
15
  * "unattributed": [{ path, line, message, reported_as }, …] }
15
16
  *
17
+ * `terp_findings: 1` is the envelope's **format version**, not a count — it is the
18
+ * discriminator a consumer matches on (the same role `terp_check_report: 1` plays for
19
+ * the check report) and it stays 1 whether the run found zero problems or twenty. The
20
+ * count lives in `findings.length`, and the verdict in `ok`. Reading the marker as a
21
+ * finding count is a real reported misread, which is why `ok` is there: a consumer that
22
+ * wants "did this pass" should never have to interpret a version number.
23
+ *
16
24
  * `rules` is the evaluated-rule inventory ({@link catalogRuleIds}, minus the opt-in
17
25
  * rules listed under `not_applicable` — today `frontend/layout-contract` when the app
18
26
  * has no checked-in layout-contract.json, so a consumer never renders an unenforced
@@ -129,6 +137,9 @@ export function renderEnvelope(results, cwd = process.cwd(), options = {}) {
129
137
  envelope: {
130
138
  terp_findings: 1,
131
139
  tool: "@terpjs/eslint-boundaries",
140
+ // The verdict, stated: `terp_findings` is a format version and reads like a
141
+ // count, so leaving "did it pass" implicit invites the wrong answer.
142
+ ok: findings.length === 0,
132
143
  rules: catalogRuleIds().filter((id) => !notApplicable.includes(id)),
133
144
  not_applicable: notApplicable,
134
145
  findings,
@@ -81,6 +81,29 @@ describe("renderEnvelope", () => {
81
81
  expect(human.length).toBe(envelope.findings.length);
82
82
  });
83
83
 
84
+ it("states the verdict, so the format version is never read as a count", async () => {
85
+ // `terp_findings: 1` is a discriminator; it stays 1 whatever the run found. A
86
+ // reported misread ("0 findings but terp_findings says 1") cost an app a cycle,
87
+ // so the envelope answers "did this pass" outright.
88
+ const clean = await lintModule("export function View() {\n return null;\n}\n");
89
+ const cleanEnvelope = renderEnvelope(clean, path.resolve("."), {
90
+ layoutContract: true,
91
+ }).envelope;
92
+ expect(cleanEnvelope.findings).toEqual([]);
93
+ expect(cleanEnvelope.terp_findings).toBe(1);
94
+ expect(cleanEnvelope.ok).toBe(true);
95
+
96
+ const dirty = await lintModule(
97
+ 'export function View() {\n return <button style={{ color: "#fff" }}>x</button>;\n}\n',
98
+ );
99
+ const dirtyEnvelope = renderEnvelope(dirty, path.resolve("."), {
100
+ layoutContract: true,
101
+ }).envelope;
102
+ expect(dirtyEnvelope.findings.length).toBeGreaterThan(1);
103
+ expect(dirtyEnvelope.terp_findings).toBe(1);
104
+ expect(dirtyEnvelope.ok).toBe(false);
105
+ });
106
+
84
107
  it("publishes an un-opted-in layout contract as not_applicable, never as passing", async () => {
85
108
  // The opt-in rule is inert without a checked-in layout-contract.json; keeping it
86
109
  // in `rules` would let a consumer render "evaluated, zero findings" = green for a