specshield 3.3.5 → 3.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # SpecShield CLI changelog
2
2
 
3
+ ## 3.4.0 — 2026-07-29 — `govern` command (API governance in the CLI)
4
+
5
+ ### Added
6
+
7
+ - **`specshield govern <spec>` — API governance & linting from the CLI.** Scores an
8
+ OpenAPI spec against SpecShield's governance rules (OWASP + design) via the hosted
9
+ gate (`POST /api/governance/gate`) and prints findings (grouped by severity, with
10
+ rule id + location), a compliance score + A–F grade, and a PASS/FAIL verdict.
11
+ Exit codes match the other gates: `0` pass · `1` fail · `2` error.
12
+ - Flags: `--ruleset <path>` (lint against a Spectral-format ruleset file),
13
+ `--min-score <n>`, `--fail-on-warning`, `--no-fail-on-error`, `--org <key>`
14
+ (apply the org's active waivers), `--advisory` (report but never fail CI),
15
+ `--json`, `--output <file>`, `--api-token`, `--server`.
16
+ - Team plan and above (needs an API key); free/unauthed callers get a friendly
17
+ "requires a paid plan" message and exit `2`.
18
+ - README: new **Governance** section + command reference; the "API governance"
19
+ use case now points at `specshield govern` instead of "not a CLI command yet".
20
+
21
+ ## 3.3.6 — 2026-07-28 — Docs & platform positioning
22
+
23
+ Documentation-only release — **no CLI behavior change**; existing CI keeps working.
24
+
25
+ ### Changed
26
+
27
+ - **README reframed around the SpecShield™ platform.** New "One platform, five ways
28
+ to run it" section covering the GitHub App, GitHub Action, this CLI, the
29
+ IntelliJ/JetBrains plugin, and the MCP server — plus the hosted dashboard.
30
+ - **API governance surfaced as a first-class capability** (OWASP + design-rule
31
+ linting, standard rule packs, waivers, SARIF/JUnit/HTML reports) — available via
32
+ the IntelliJ plugin, GitHub Action, MCP server, and dashboard.
33
+ - Corrected the "API governance" use case that previously described drift-tracking;
34
+ split into "API drift tracking" and proper "API governance & linting".
35
+ - Noted that Optic was archived (Jan 2026) and linked the full comparison at
36
+ `specshield.io/alternatives`.
37
+ - Applied the ™ mark to the SpecShield brand where appropriate.
38
+
3
39
  ## 3.3.0 — 2026-06-20 — Complex-spec hardening + "contract compatibility testing"
4
40
 
5
41
  A minor release focused on the local diff engine's accuracy on advanced OpenAPI
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # SpecShield CLI
1
+ # SpecShield CLI
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/specshield)](https://www.npmjs.com/package/specshield)
4
4
  [![downloads](https://img.shields.io/npm/dt/specshield)](https://www.npmjs.com/package/specshield)
@@ -17,6 +17,9 @@
17
17
  **Commands — local & hosted compare**
18
18
  - [Local Compare](#local-compare) · [Remote Compare](#remote-compare) · [Comparison History](#comparison-history) · [Share a Comparison](#share-a-comparison) · [GitHub App PR Checks](#github-app--pr-checks)
19
19
 
20
+ **Governance — lint & gate**
21
+ - [`specshield govern`](#governance--lint-a-spec-and-gate-the-pr)
22
+
20
23
  **Contract Compatibility Testing — the `bdct` commands**
21
24
  - [Overview & full command reference](#bi-directional-contract-testing-bdct)
22
25
  - [`bdct capture from-har` — HAR → consumer contract](#bdct-capture-from-har--turn-real-traffic-into-a-consumer-contract)
@@ -34,28 +37,47 @@
34
37
 
35
38
  ---
36
39
 
37
- > **Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection · `can-i-deploy` Deploy Gate · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
40
+ > **Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection · `can-i-deploy` Deploy Gate · API Governance & Linting · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
38
41
 
39
42
  ---
40
43
 
41
44
  ## Never ship a breaking change to your API consumers
42
45
 
43
- **SpecShield is contract compatibility testing for APIs** — catch breaking changes before they reach your consumers, and gate every deploy with `can-i-deploy`. *(Contract compatibility testing is also known as bidirectional contract testing.)*
46
+ **SpecShield is contract compatibility testing for APIs** — catch breaking changes before they reach your consumers, and gate every deploy with `can-i-deploy`. *(Contract compatibility testing is also known as bidirectional contract testing.)*
44
47
 
45
- It's the one CLI that does four things to keep your API safe:
48
+ It's the one CLI that does five things to keep your API safe:
46
49
 
47
50
  1. **Diff** two OpenAPI specs and fail CI on breaking changes.
48
51
  2. **Contract compatibility testing** with `can-i-deploy` — block a deploy that would break a consumer.
49
- 3. **`bdct capture from-har`** — turn recorded traffic into an accurate consumer contract (no Pact DSL).
50
- 4. **`bdct verify-provider`** — prove the running provider actually matches its OpenAPI spec.
52
+ 3. **`govern`** — lint your spec against OWASP + design rules and gate the PR on violations.
53
+ 4. **`bdct capture from-har`** — turn recorded traffic into an accurate consumer contract (no Pact DSL).
54
+ 5. **`bdct verify-provider`** — prove the running provider actually matches its OpenAPI spec.
51
55
 
52
56
  ```
53
- OpenAPI diff + contract compatibility checks + HAR → consumer contract + spec-vs-production conformance — in one CLI.
57
+ OpenAPI diff + contract compatibility checks + API governance + HAR → consumer contract + spec-vs-production conformance — in one CLI.
54
58
  ```
55
59
 
56
60
  No broker. No Pact DSL. Language-agnostic. Works in 30 seconds. Local mode never uploads your specs.
57
61
 
58
- > 🤖 **Using an AI coding agent?** The [SpecShield MCP server](https://github.com/specshield26/specshield-mcp-server) brings the same `can-i-deploy` deploy gate into Claude, Cursor, and other MCP clients — ask *"is it safe to ship this API change to my consumers?"* right inside your agent. Same job, two entry points: this CLI for CI/CD, the MCP server for agents.
62
+ ---
63
+
64
+ ## One platform, five ways to run it
65
+
66
+ This CLI is one surface of the **SpecShield™ platform**. The same contract-compatibility and governance engine powers every entry point — they share one project file (`.specshield.yml`), one API key, and one set of audit events. Adopt the one that fits your stack today and grow into the others:
67
+
68
+ | Surface | Best for |
69
+ |---|---|
70
+ | **GitHub App** — [github.com/apps/specshield](https://github.com/apps/specshield) | Zero-config PR checks. Install once; every PR gets a check run and a diff comment. |
71
+ | **GitHub Action** — [`specshield26/bdct-action@v1`](https://github.com/marketplace/actions/specshield-bdct) | Pinned, reproducible BDCT + governance gate in any GitHub workflow. |
72
+ | **CLI** *(this package)* | Any CI — GitLab, Jenkins, CircleCI, Bitbucket, Drone — or your laptop. |
73
+ | **IntelliJ plugin** — [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/33137-specshield) | Inline spec findings, one-click auto-fixes, compare & explain — inside IntelliJ IDEA, WebStorm, PyCharm and more. |
74
+ | **MCP server** — [`specshield-mcp-server`](https://github.com/specshield26/specshield-mcp-server) | Give Claude, Cursor, and other AI agents the same deploy gate. |
75
+
76
+ Plus a hosted **dashboard** at [specshield.io](https://specshield.io) — compatibility matrix, consumer registry, append-only audit log, and **API governance** scorecards (OWASP + design-rule linting, standard rule packs, waivers, SARIF/JUnit/HTML reports).
77
+
78
+ > 🤖 **AI agents:** the MCP server answers *"is it safe to ship this API change to my consumers?"* right inside your agent — analyze-only, it never touches your code. Same job, two entry points: this CLI for CI/CD, the MCP server for agents.
79
+
80
+ > See how the platform stacks up against Pactflow, Pact, Optic, oasdiff, Bump.sh, Stoplight, and API gateways (Apigee, Azure API Management) at **[specshield.io/alternatives](https://specshield.io/alternatives)**.
59
81
 
60
82
  ---
61
83
 
@@ -191,9 +213,10 @@ specshield bdct can-i-deploy --version $GITHUB_SHA
191
213
  - **Pull-request validation** — catch breaking changes before merge with `specshield compare`.
192
214
  - **CI/CD gating** — exit code `1` stops the pipeline automatically.
193
215
  - **Microservices contract safety** — consumers publish what they expect, providers verify they deliver it, no cross-team surprises.
194
- - **API governance** — track API drift over time across your platform; know what changed, when, by whom.
195
- - **Provider conformance** *(new)* make sure your *running* service actually matches its published OpenAPI spec, not just on paper.
196
- - **Pact-free consumer contracts** *(new)* record real traffic with `capture from-har` and get an OpenAPI consumer contract without writing a single line of Pact DSL.
216
+ - **API drift tracking** — track how your specs change over time across the platform; know what changed, when, and by whom.
217
+ - **API governance & linting** — score every spec against OWASP + design rules, bring your own Spectral ruleset, and gate PRs on violations with [`specshield govern`](#governance--lint-a-spec-and-gate-the-pr). The same engine also powers the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io).
218
+ - **Provider conformance** — make sure your *running* service actually matches its published OpenAPI spec, not just on paper.
219
+ - **Pact-free consumer contracts** — record real traffic with `capture from-har` and get an OpenAPI consumer contract without writing a single line of Pact DSL.
197
220
 
198
221
  ---
199
222
 
@@ -338,6 +361,68 @@ github:
338
361
 
339
362
  ---
340
363
 
364
+ ## Governance — lint a spec and gate the PR
365
+
366
+ **Static API governance: score your OpenAPI spec against OWASP + design rules and fail CI on violations.** *(Team plan and above — runs on SpecShield's hosted engine, so it needs an API key.)*
367
+
368
+ ```bash
369
+ # Lint against SpecShield's built-in governance rules
370
+ specshield govern api/openapi.yaml
371
+
372
+ # Lint against a Spectral-format ruleset (OWASP pack, your house style, …)
373
+ specshield govern api/openapi.yaml --ruleset .specshield/ruleset.yaml
374
+
375
+ # Gate CI: require a minimum score, and fail on warnings too
376
+ specshield govern api/openapi.yaml --min-score 80 --fail-on-warning
377
+
378
+ # Report only — print findings but never fail the build
379
+ specshield govern api/openapi.yaml --advisory
380
+ ```
381
+
382
+ Sample output:
383
+
384
+ ```
385
+ SpecShield™ Governance Report
386
+ ─────────────────────────────────────────
387
+ error security-scheme-missing $.paths./payments.post
388
+ operation has no security scheme
389
+ fix: add a security requirement
390
+ info operation-operation-id $.paths./health.get
391
+ operation is missing an operationId
392
+ ─────────────────────────────────────────
393
+ Score 74/100 · grade C · 1 error · 0 warning · 1 info
394
+
395
+ ✖ FAIL — governance policy not met:
396
+ • 1 error-severity finding
397
+ ```
398
+
399
+ Exit codes: `0` = passed the gate · `1` = failed the gate · `2` = error (invalid spec, missing token, or paid-plan required).
400
+
401
+ | Flag | Purpose | Default |
402
+ |---|---|---|
403
+ | `--ruleset <path>` | Lint against a Spectral-format ruleset file (uses the ruleset engine). | built-in rules |
404
+ | `--min-score <n>` | Minimum compliance score (0–100) required to pass. | `70` |
405
+ | `--fail-on-warning` | Fail the gate when any warning-severity finding is present. | off |
406
+ | `--no-fail-on-error` | Do **not** fail the gate on error-severity findings. | fails on error |
407
+ | `--org <key>` | Apply your org's active governance waivers to the verdict. | none |
408
+ | `--advisory` | Report findings but always exit `0` (never fail CI). | off |
409
+ | `--json` | Machine-readable JSON output (the full gate response). | off |
410
+ | `--output <file>` | Also save the JSON result to a file. | — |
411
+ | `--api-token <token>` | API token (else `SPECSHIELD_API_KEY` / stored config). | — |
412
+ | `--server <url>` | Override the SpecShield server URL (self-hosted / staging). | `https://specshield.io` |
413
+
414
+ Run it in CI right after your build:
415
+
416
+ ```yaml
417
+ - name: Govern the API spec
418
+ env: { SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }} }
419
+ run: specshield govern api/openapi.yaml --min-score 80
420
+ ```
421
+
422
+ > The same governance engine powers the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io) — with catalog scorecards, waivers, and SARIF/JUnit/HTML reports.
423
+
424
+ ---
425
+
341
426
  # Bi-Directional Contract Testing (BDCT)
342
427
 
343
428
  **Compatibility without Pact's broker overhead or DSL.**
@@ -1093,6 +1178,8 @@ jobs:
1093
1178
  | CLI-first | ✅ | ❌ | ✅ | partial |
1094
1179
  | Free tier | ✅ | ❌ | ✅ (OSS) | ✅ (OSS) |
1095
1180
 
1181
+ > **Note:** Optic — a popular OpenAPI-diff tool — was **archived in January 2026** and is no longer maintained; SpecShield is a drop-in replacement for its diff and CI checks. For the full, up-to-date comparison (including Bump.sh, Stoplight, and API gateways like Apigee and Azure API Management), see **[specshield.io/alternatives](https://specshield.io/alternatives)**.
1182
+
1096
1183
  ---
1097
1184
 
1098
1185
  ## Pricing
@@ -1326,6 +1413,23 @@ specshield share <reportId | base.yaml target.yaml> [options]
1326
1413
  | `--expires <days>` | Make the link expire after N days (default: never) |
1327
1414
  | `--api-key <key>` | Override stored API key |
1328
1415
 
1416
+ ```bash
1417
+ specshield govern <spec> [options]
1418
+ ```
1419
+
1420
+ | Option | Description |
1421
+ |---|---|
1422
+ | `--ruleset <path>` | Lint against a Spectral-format ruleset file |
1423
+ | `--min-score <n>` | Minimum compliance score (0–100) to pass (default 70) |
1424
+ | `--fail-on-warning` | Fail the gate on any warning-severity finding |
1425
+ | `--no-fail-on-error` | Do not fail the gate on error-severity findings |
1426
+ | `--org <key>` | Apply the org's active governance waivers |
1427
+ | `--advisory` | Report findings but always exit 0 |
1428
+ | `--json` | Machine-readable JSON output |
1429
+ | `--output <file>` | Save the JSON result to a file |
1430
+ | `--api-token <token>` | API token (overrides env / stored config) |
1431
+ | `--server <url>` | Override SpecShield server URL |
1432
+
1329
1433
  ```bash
1330
1434
  specshield bdct <subcommand> [options]
1331
1435
  ```
@@ -1395,7 +1499,9 @@ MIT © SpecShield Software Private Limited
1395
1499
 
1396
1500
  <div align="center">
1397
1501
 
1398
- **[ Star on GitHub](https://github.com/specshield26/specshield-cli) · [📦 View on npm](https://www.npmjs.com/package/specshield) · [🚀 Create free account](https://specshield.io)**
1502
+ **The SpecShield™ platform:** [Dashboard](https://specshield.io) · [GitHub App](https://github.com/apps/specshield) · [GitHub Action](https://github.com/marketplace/actions/specshield-bdct) · [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/33137-specshield) · [MCP Server](https://github.com/specshield26/specshield-mcp-server)
1503
+
1504
+ **[⭐ Star on GitHub](https://github.com/specshield26/specshield-cli) · [📦 View on npm](https://www.npmjs.com/package/specshield) · [🚀 Create free account](https://specshield.io) · [⚖️ Compare](https://specshield.io/alternatives)**
1399
1505
 
1400
1506
  *Stop finding out about API breakage from your users.*
1401
1507
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "3.3.5",
3
+ "version": "3.4.0",
4
4
  "description": "Contract compatibility testing for APIs — catch breaking OpenAPI changes before they reach your consumers, with can-i-deploy deploy gating and GitHub PR checks. (a.k.a. bidirectional contract testing.)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -108,6 +108,19 @@ async function listConsumerContracts(server, apiToken, { org, consumer, provider
108
108
  } catch (err) { throw apiError(err); }
109
109
  }
110
110
 
111
+ /**
112
+ * Run the governance deploy gate on a spec. Always returns HTTP 200 with a
113
+ * pass/fail verdict in the body (a failed gate is a valid result, not an error),
114
+ * so only real failures (400 invalid spec, 402 paid-plan, 403 org, 503 disabled,
115
+ * network) throw. `payload` = { spec, ruleset?, policy?, orgKey? }.
116
+ */
117
+ async function governanceGate(server, apiToken, payload) {
118
+ try {
119
+ const res = await buildClient(server, apiToken).post('/api/governance/gate', payload);
120
+ return res.data;
121
+ } catch (err) { throw apiError(err); }
122
+ }
123
+
111
124
  module.exports = {
112
125
  publishProviderSpec,
113
126
  publishConsumerContract,
@@ -117,4 +130,5 @@ module.exports = {
117
130
  getMatrix,
118
131
  listProviderSpecs,
119
132
  listConsumerContracts,
133
+ governanceGate,
120
134
  };
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ const initCommand = require('./commands/init');
7
7
  const loginCommand = require('./commands/login');
8
8
  const logoutCommand = require('./commands/logout');
9
9
  const bdctCommand = require('./commands/bdct');
10
+ const governCommand = require('./commands/govern');
10
11
  const historyCommand = require('./commands/history');
11
12
  const shareCommand = require('./commands/share');
12
13
  const whoamiCommand = require('./commands/whoami');
@@ -27,6 +28,7 @@ program.addCommand(initCommand);
27
28
  program.addCommand(loginCommand);
28
29
  program.addCommand(logoutCommand);
29
30
  program.addCommand(bdctCommand);
31
+ program.addCommand(governCommand);
30
32
  program.addCommand(historyCommand);
31
33
  program.addCommand(shareCommand);
32
34
  program.addCommand(whoamiCommand);
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const fsExtra = require('fs-extra');
7
+ const logger = require('../utils/logger');
8
+ const { loadSpec } = require('../core/loadSpec');
9
+ const { getStoredApiKey } = require('../config/localConfig');
10
+ const { governanceGate } = require('../api/bdctClient');
11
+
12
+ // Token precedence mirrors the bdct subcommands.
13
+ async function resolveApiToken(opts) {
14
+ return opts.apiToken || process.env.SPECSHIELD_API_KEY || (await getStoredApiKey()) || null;
15
+ }
16
+
17
+ const SEV_ORDER = { error: 0, warning: 1, info: 2 };
18
+
19
+ function sevLabel(sev) {
20
+ const s = String(sev || '').toLowerCase();
21
+ if (s === 'error') return chalk.red('error ');
22
+ if (s === 'warning') return chalk.yellow('warning');
23
+ return chalk.blue('info ');
24
+ }
25
+
26
+ function gradeColor(grade) {
27
+ const g = String(grade || '—').toUpperCase();
28
+ if (g === 'A' || g === 'B') return chalk.green(g);
29
+ if (g === 'C') return chalk.yellow(g);
30
+ return chalk.red(g);
31
+ }
32
+
33
+ const govern = new Command('govern')
34
+ .description('Lint an OpenAPI spec against governance rules (OWASP + design) and gate the PR')
35
+ .argument('<spec>', 'Path to the OpenAPI spec to govern (YAML or JSON)')
36
+ .option('--ruleset <path>', 'Lint against a Spectral-format ruleset file (uses the ruleset engine)')
37
+ .option('--min-score <n>', 'Minimum compliance score (0–100) required to pass (default 70)', (v) => parseInt(v, 10))
38
+ .option('--fail-on-warning', 'Fail the gate when any warning-severity finding is present')
39
+ .option('--no-fail-on-error', 'Do NOT fail the gate on error-severity findings')
40
+ .option('--org <key>', 'Org key — applies your org’s active governance waivers to the verdict')
41
+ .option('--advisory', 'Report findings but always exit 0 (never fail CI)')
42
+ .option('--json', 'Output machine-readable JSON')
43
+ .option('--output <file>', 'Also save the JSON result to this file')
44
+ .option('--server <url>', 'SpecShield server URL')
45
+ .option('--api-token <token>', 'API token (overrides env / stored config)')
46
+ .action(async (specPath, opts) => {
47
+ try {
48
+ const token = await resolveApiToken(opts);
49
+ if (!token) {
50
+ logger.error('No API token found. Pass --api-token, set SPECSHIELD_API_KEY, or run: specshield login --api-key <KEY>');
51
+ process.exit(2);
52
+ }
53
+
54
+ const spinner = opts.json ? null : ora('Loading spec...').start();
55
+ const specText = await loadSpec(specPath);
56
+ const ruleset = opts.ruleset ? await loadSpec(opts.ruleset) : null;
57
+
58
+ // Only send policy fields the user overrode; the server applies documented
59
+ // defaults (minScore 70, failOnError true, failOnWarning false) otherwise.
60
+ const policy = {};
61
+ if (opts.minScore !== undefined && !Number.isNaN(opts.minScore)) policy.minScore = opts.minScore;
62
+ if (opts.failOnWarning) policy.failOnWarning = true;
63
+ if (opts.failOnError === false) policy.failOnError = false; // set by --no-fail-on-error
64
+
65
+ if (spinner) spinner.text = 'Running governance review...';
66
+ const resp = await governanceGate(opts.server, token, {
67
+ spec: specText,
68
+ ruleset,
69
+ policy: Object.keys(policy).length ? policy : null,
70
+ orgKey: opts.org || null,
71
+ });
72
+ if (spinner) spinner.stop();
73
+
74
+ if (opts.json) {
75
+ const out = JSON.stringify(resp, null, 2);
76
+ process.stdout.write(out + '\n');
77
+ if (opts.output) await fsExtra.outputFile(opts.output, out);
78
+ } else {
79
+ renderHuman(resp);
80
+ if (opts.output) await fsExtra.outputFile(opts.output, JSON.stringify(resp, null, 2));
81
+ }
82
+
83
+ if (opts.advisory) process.exit(0);
84
+ process.exit(resp.passed ? 0 : 1);
85
+ } catch (err) {
86
+ if (err.status === 402) {
87
+ logger.error('API governance requires a paid plan (Team or above). See https://specshield.io/pricing');
88
+ process.exit(2);
89
+ }
90
+ if (err.status === 503) {
91
+ logger.error('Governance is not enabled on this SpecShield server.');
92
+ process.exit(2);
93
+ }
94
+ logger.error(`Error: ${err.message}`);
95
+ process.exit(2);
96
+ }
97
+ });
98
+
99
+ function renderHuman(resp) {
100
+ const review = resp.review || {};
101
+ const findings = Array.isArray(review.findings) ? review.findings.slice() : [];
102
+ const score = review.score || {};
103
+
104
+ process.stdout.write('\n ' + chalk.bold('SpecShield™ Governance Report') + '\n');
105
+ process.stdout.write(' ' + chalk.gray('─────────────────────────────────────────') + '\n');
106
+
107
+ if (findings.length === 0) {
108
+ process.stdout.write(' ' + chalk.green('✔ No governance findings.') + '\n');
109
+ } else {
110
+ findings.sort((a, b) =>
111
+ (SEV_ORDER[String(a.severity).toLowerCase()] ?? 3) - (SEV_ORDER[String(b.severity).toLowerCase()] ?? 3));
112
+ for (const f of findings) {
113
+ const loc = f.location ? chalk.gray(' ' + f.location) : '';
114
+ process.stdout.write(` ${sevLabel(f.severity)} ${chalk.cyan(f.ruleId || '')}${loc}\n`);
115
+ if (f.message) process.stdout.write(` ${f.message}\n`);
116
+ if (f.suggestedFix) process.stdout.write(` ${chalk.gray('fix: ' + f.suggestedFix)}\n`);
117
+ }
118
+ }
119
+
120
+ process.stdout.write(' ' + chalk.gray('─────────────────────────────────────────') + '\n');
121
+ const scoreVal = (score.value === undefined || score.value === null) ? '—' : score.value;
122
+ process.stdout.write(
123
+ ` Score ${chalk.bold(scoreVal)}/100 · grade ${gradeColor(score.grade)} · `
124
+ + `${chalk.red((review.errorCount || 0) + ' error')} · `
125
+ + `${chalk.yellow((review.warningCount || 0) + ' warning')} · `
126
+ + `${chalk.blue((review.infoCount || 0) + ' info')}`
127
+ + (resp.waivedCount ? ` · ${chalk.gray(resp.waivedCount + ' waived')}` : '')
128
+ + '\n'
129
+ );
130
+
131
+ if (resp.passed) {
132
+ process.stdout.write('\n ' + chalk.green.bold('✔ PASS')
133
+ + chalk.gray(` — meets the governance policy (min score ${resp.scoreThreshold}).`) + '\n\n');
134
+ } else {
135
+ process.stdout.write('\n ' + chalk.red.bold('✖ FAIL') + chalk.gray(' — governance policy not met:') + '\n');
136
+ for (const r of (resp.reasons || [])) process.stdout.write(' ' + chalk.red('•') + ' ' + r + '\n');
137
+ if (resp.recommendedAction) process.stdout.write('\n ' + chalk.gray(resp.recommendedAction) + '\n');
138
+ process.stdout.write('\n');
139
+ }
140
+ }
141
+
142
+ module.exports = govern;