@deployanyway/ship-it-meter 0.2.0 → 0.3.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-08
4
+
5
+ - Useful structured API/CLI additions described in README.
6
+ - TypeScript declarations, CommonJS entry, coverage gates and installed archive checks.
7
+ - Linux Node 22/24 plus Windows/macOS Node 24 CI.
8
+
3
9
  ## 0.2.0 — 2026-10-08
4
10
 
5
11
  - Preflight, with a parachute: `preflight(input)` returns the same score, verdict and reasons as `shipIt`, plus actionable `actions`. CLI `--checklist` includes them in text or JSON output. It uses only supplied evidence and is not a deployment guarantee.
package/MIGRATION.md ADDED
@@ -0,0 +1,5 @@
1
+ # 0.3.0 migration
2
+
3
+ Node 22.13+ is required. Existing core APIs remain available; TypeScript declarations and CommonJS exports are new. The CLI now lives in src/cli.js behind the same executable path. Input/stdout behavior for new options is documented in README.
4
+
5
+ Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. Do not treat jokes or heuristic scores as production evidence.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ship-it-meter
2
2
 
3
+ > **Version 0.3.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0.
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/%40deployanyway%2Fship-it-meter)](https://www.npmjs.com/package/@deployanyway/ship-it-meter)
4
6
  [![CI](https://github.com/DeployAnyway/ship-it-meter/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DeployAnyway/ship-it-meter/actions/workflows/ci.yml)
5
7
 
@@ -167,3 +169,43 @@ API (import the named functions from this package):
167
169
  ```js
168
170
  preflight({ tests: 125, coverage: 82, build: true, day: "friday" });
169
171
  ```
172
+
173
+ ## An explicit gate for automation
174
+
175
+ ```js
176
+ import { releaseGate } from "@deployanyway/ship-it-meter";
177
+ const result = releaseGate(
178
+ { tests: 42, coverage: 92, build: true },
179
+ { minScore: 80 },
180
+ );
181
+ console.log(result.passed, result.blockers);
182
+ ```
183
+
184
+ A gate requires positive test count, coverage evidence and a passing build. Failing tests, critical issues, lint failures and dirty changes block it even when a low score threshold is chosen. Scores remain a heuristic, not authorization to deploy. The result adds passed, blockers and minScore to the preflight checklist.
185
+
186
+ ```sh
187
+ node bin/cli.js --tests 42 --coverage 92 --build pass --gate --json
188
+ node bin/cli.js --stdin --gate --min-score 90 --json < evidence.json
189
+ ```
190
+
191
+ --stdin accepts a bounded JSON evidence object (256 KiB); explicit flags override fields. --min-score requires --gate and a 0–100 threshold, default 80. Exit 0: gate passes or ordinary scoring succeeds; 1: gate blocked; 2: malformed input. Upstream pipeline status is your shell's responsibility.
192
+
193
+ ## Run from source
194
+
195
+ ```sh
196
+ git clone --branch main https://github.com/DeployAnyway/ship-it-meter.git
197
+ cd ship-it-meter
198
+ npm ci
199
+ npm run build
200
+ node bin/cli.js --help
201
+ ```
202
+
203
+ ## Candidate quality standard
204
+
205
+ Version 0.3 provides useful declaration types, ESM/CommonJS exports, installed-archive checks, and coverage gates (90% statements/lines/functions, 85% branches). CI covers Linux Node 22/24 and Windows/macOS Node 24. Node 22.13+ is required. No runtime dependencies, telemetry or network requests.
206
+
207
+ From a source checkout: npm ci, npm run build, npm run coverage, npm run test:types, npm run verify:package. Pack verification installs a temporary local archive and checks module entries, types, executable and offline npm exec.
208
+
209
+ [Contribution guide](CONTRIBUTING.md) · [Conduct](CODE_OF_CONDUCT.md) · [Security](SECURITY.md) · [Roadmap](ROADMAP.md) · [Migration](MIGRATION.md).
210
+
211
+ **Tools for developers who probably know better.** Software nobody requested, built with questionable priorities, and shipped with absolute confidence!
package/bin/cli.js CHANGED
@@ -1,68 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs } from "node:util";
3
- import { readFileSync } from "node:fs";
4
- import { URL } from "node:url";
5
- import { shipIt, preflight } from "../src/index.js";
6
-
7
- try {
8
- const { values } = parseArgs({
9
- options: {
10
- help: { type: "boolean", short: "h" },
11
- version: { type: "boolean", short: "v" },
12
- json: { type: "boolean" },
13
- checklist: { type: "boolean" },
14
- tests: { type: "string" },
15
- failing: { type: "string" },
16
- coverage: { type: "string" },
17
- day: { type: "string" },
18
- branch: { type: "string" },
19
- critical: { type: "string" },
20
- lint: { type: "string" },
21
- build: { type: "string" },
22
- dirty: { type: "boolean" },
23
- },
24
- });
25
- if (values.help) {
26
- console.log(
27
- "Usage: ship-it-meter [options]\n\nOptions:\n --tests number Total tests\n --failing number Failing tests\n --coverage number Coverage percent\n --build pass|fail Build result\n --critical number Open critical issues\n --lint number Lint failures\n --dirty Uncommitted changes\n --branch name Branch name\n --day weekday Full weekday name\n --checklist Actionable, mildly concerned preflight\n --json Structured output\n -h, --help Help\n -v, --version Version\n\nMissing test, coverage, and build evidence reduces readiness.\nExit codes: 0 scored successfully; 2 invalid arguments.",
28
- );
29
- } else if (values.version) {
30
- console.log(
31
- JSON.parse(
32
- readFileSync(new URL("../package.json", import.meta.url), "utf8"),
33
- ).version,
34
- );
35
- } else {
36
- const input = {};
37
- for (const [flag, key] of Object.entries({
38
- tests: "tests",
39
- failing: "failingTests",
40
- coverage: "coverage",
41
- critical: "criticalIssues",
42
- lint: "lintFailures",
43
- })) {
44
- if (values[flag] !== undefined) {
45
- if (!/^\d+(?:\.\d+)?$/.test(values[flag]))
46
- throw new TypeError(`${flag} must be a nonnegative decimal number.`);
47
- input[key] = Number(values[flag]);
48
- }
49
- }
50
- if (values.build !== undefined) {
51
- if (!["pass", "fail"].includes(values.build))
52
- throw new RangeError("--build must be pass or fail.");
53
- input.build = values.build === "pass";
54
- }
55
- for (const key of ["day", "branch"])
56
- if (values[key] !== undefined) input[key] = values[key];
57
- if (values.dirty) input.uncommittedChanges = true;
58
- const result = values.checklist ? preflight(input) : shipIt(input);
59
- console.log(
60
- values.json
61
- ? JSON.stringify(result, null, 2)
62
- : `${result.score}/100 — ${result.verdict}\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}${result.actions ? "\n\nBefore you ship:\n" + result.actions.map((action) => `[ ] ${action}`).join("\n") : ""}`,
63
- );
64
- }
65
- } catch (error) {
66
- console.error(`ship-it-meter: ${error.message}\nRun with --help for usage.`);
67
- process.exitCode = 2;
68
- }
2
+ import "../src/cli.js";
package/dist/index.cjs ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ preflight: () => preflight,
24
+ releaseGate: () => releaseGate,
25
+ shipIt: () => shipIt
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var days = [
29
+ "monday",
30
+ "tuesday",
31
+ "wednesday",
32
+ "thursday",
33
+ "friday",
34
+ "saturday",
35
+ "sunday"
36
+ ];
37
+ var counts = ["tests", "failingTests", "criticalIssues", "lintFailures"];
38
+ var allowed = [
39
+ ...counts,
40
+ "coverage",
41
+ "build",
42
+ "uncommittedChanges",
43
+ "branch",
44
+ "day"
45
+ ];
46
+ function releaseGate(input = {}, options = {}) {
47
+ if (!options || typeof options !== "object" || Array.isArray(options))
48
+ throw new TypeError("Gate options must be an object.");
49
+ const minScore = options.minScore ?? 80;
50
+ if (typeof minScore !== "number" || !Number.isFinite(minScore) || minScore < 0 || minScore > 100)
51
+ throw new RangeError("minScore must be between 0 and 100.");
52
+ const result = preflight(input);
53
+ const blockers = [];
54
+ if (!input.tests)
55
+ blockers.push("Provide a positive test count. Vibes cannot pass CI.");
56
+ if (input.build !== true) blockers.push("A passing build is required.");
57
+ if (input.coverage === void 0)
58
+ blockers.push("Coverage evidence is required.");
59
+ if (input.failingTests) blockers.push("Failing tests block the gate.");
60
+ if (input.criticalIssues) blockers.push("Critical issues block the gate.");
61
+ if (input.lintFailures) blockers.push("Lint failures block the gate.");
62
+ if (input.uncommittedChanges)
63
+ blockers.push("Uncommitted changes block the gate.");
64
+ if (result.score < minScore)
65
+ blockers.push(`Score ${result.score} is below ${minScore}.`);
66
+ return { ...result, minScore, passed: blockers.length === 0, blockers };
67
+ }
68
+ function shipIt(input = {}) {
69
+ if (!input || typeof input !== "object" || Array.isArray(input))
70
+ throw new TypeError("Input must be an object.");
71
+ for (const key of Object.keys(input))
72
+ if (!allowed.includes(key)) throw new TypeError(`Unknown input: ${key}.`);
73
+ for (const key of counts) {
74
+ if (input[key] !== void 0 && (!Number.isSafeInteger(input[key]) || input[key] < 0))
75
+ throw new RangeError(`${key} must be a nonnegative safe integer.`);
76
+ }
77
+ if (input.coverage !== void 0 && (typeof input.coverage !== "number" || !Number.isFinite(input.coverage) || input.coverage < 0 || input.coverage > 100))
78
+ throw new RangeError("coverage must be between 0 and 100.");
79
+ for (const key of ["build", "uncommittedChanges"])
80
+ if (input[key] !== void 0 && typeof input[key] !== "boolean")
81
+ throw new TypeError(`${key} must be a boolean.`);
82
+ for (const key of ["branch", "day"])
83
+ if (input[key] !== void 0 && (typeof input[key] !== "string" || !input[key].trim()))
84
+ throw new TypeError(`${key} must be a nonempty string.`);
85
+ const day = input.day?.trim().toLowerCase();
86
+ if (day !== void 0 && !days.includes(day))
87
+ throw new RangeError("day must be a full weekday name.");
88
+ const failing = input.failingTests ?? 0;
89
+ if (failing > 0 && input.tests === void 0)
90
+ throw new RangeError("tests is required when failingTests is positive.");
91
+ if (input.tests !== void 0 && failing > input.tests)
92
+ throw new RangeError("failingTests cannot exceed tests.");
93
+ let score = 100;
94
+ const reasons = [];
95
+ const deduct = (points, reason) => {
96
+ score -= points;
97
+ reasons.push(`${reason} (-${points})`);
98
+ };
99
+ if (!input.tests)
100
+ deduct(
101
+ 25,
102
+ input.tests === 0 ? "No tests reported" : "Test count not provided"
103
+ );
104
+ else if (failing)
105
+ deduct(
106
+ Math.min(50, 20 + Math.ceil(30 * (failing / input.tests))),
107
+ `${failing} of ${input.tests} tests are failing`
108
+ );
109
+ else reasons.push(`${input.tests} tests reported passing`);
110
+ if (input.coverage === void 0) deduct(15, "Coverage not provided");
111
+ else {
112
+ const penalty = Math.ceil((100 - input.coverage) * 0.2);
113
+ if (penalty) deduct(penalty, `Coverage is ${input.coverage}%`);
114
+ else reasons.push("Coverage is 100%");
115
+ }
116
+ if (input.build === void 0) deduct(15, "Build status not provided");
117
+ else if (!input.build) deduct(40, "Build failed");
118
+ else reasons.push("Build passed");
119
+ if (input.criticalIssues)
120
+ deduct(
121
+ Math.min(50, 25 * input.criticalIssues),
122
+ `${input.criticalIssues} open critical issues`
123
+ );
124
+ if (input.lintFailures)
125
+ deduct(
126
+ Math.min(20, 5 * input.lintFailures),
127
+ `${input.lintFailures} lint failures`
128
+ );
129
+ if (input.uncommittedChanges) deduct(10, "Uncommitted changes reported");
130
+ if (input.branch !== void 0 && !["main", "master"].includes(input.branch.trim()))
131
+ deduct(5, `Branch is ${input.branch.trim()}`);
132
+ if (day === "friday")
133
+ deduct(3, "Friday: the weekend would like a quiet entrance");
134
+ score = Math.max(0, Math.min(100, score));
135
+ if (input.build === false || input.criticalIssues > 0) {
136
+ score = Math.min(score, 29);
137
+ reasons.push("Failed build or critical issues cap the score at 29");
138
+ } else if (failing > 0) {
139
+ score = Math.min(score, 59);
140
+ reasons.push("Failing tests cap the score at 59");
141
+ }
142
+ const verdict = score < 30 ? "Absolutely Not" : score < 60 ? "Questionable" : score < 80 ? "Probably Fine" : score < 95 ? "Ship It" : "Suspiciously Ready";
143
+ return { score, verdict, reasons };
144
+ }
145
+ function preflight(input = {}) {
146
+ const result = shipIt(input);
147
+ const actions = [];
148
+ if (input.build !== true)
149
+ actions.push("Get a passing build. Confidence is not a build artifact.");
150
+ if (!input.tests)
151
+ actions.push("Run tests and report the count. Vibes are not assertions.");
152
+ if (input.failingTests)
153
+ actions.push("Fix failing tests. Red is not a festive deployment theme.");
154
+ if (input.coverage === void 0 || input.coverage < 80)
155
+ actions.push(
156
+ "Measure coverage and review untested paths. The bugs enjoy privacy."
157
+ );
158
+ if (input.criticalIssues)
159
+ actions.push(
160
+ "Resolve critical issues. The incident channel deserves a quiet afternoon."
161
+ );
162
+ if (input.lintFailures)
163
+ actions.push("Fix lint failures. Let the semicolons retire in peace.");
164
+ if (input.uncommittedChanges)
165
+ actions.push(
166
+ "Commit or stash local changes. Your laptop is not the release archive."
167
+ );
168
+ if (input.day?.trim().toLowerCase() === "friday")
169
+ actions.push(
170
+ "Confirm rollback and on-call cover. The weekend has other plans."
171
+ );
172
+ if (!actions.length)
173
+ actions.push(
174
+ "Confirm monitoring and rollback. Even excellent evidence needs a parachute."
175
+ );
176
+ return { ...result, actions };
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ preflight,
181
+ releaseGate,
182
+ shipIt
183
+ });
package/index.d.cts ADDED
@@ -0,0 +1,43 @@
1
+ export type Weekday =
2
+ | "monday"
3
+ | "tuesday"
4
+ | "wednesday"
5
+ | "thursday"
6
+ | "friday"
7
+ | "saturday"
8
+ | "sunday";
9
+ export interface Evidence {
10
+ tests?: number;
11
+ failingTests?: number;
12
+ coverage?: number;
13
+ build?: boolean;
14
+ uncommittedChanges?: boolean;
15
+ branch?: string;
16
+ day?: Weekday;
17
+ criticalIssues?: number;
18
+ lintFailures?: number;
19
+ }
20
+ export interface Readiness {
21
+ score: number;
22
+ verdict:
23
+ | "Absolutely Not"
24
+ | "Questionable"
25
+ | "Probably Fine"
26
+ | "Ship It"
27
+ | "Suspiciously Ready";
28
+ reasons: string[];
29
+ }
30
+ export interface Preflight extends Readiness {
31
+ actions: string[];
32
+ }
33
+ export interface Gate extends Preflight {
34
+ minScore: number;
35
+ passed: boolean;
36
+ blockers: string[];
37
+ }
38
+ export function shipIt(input?: Evidence): Readiness;
39
+ export function preflight(input?: Evidence): Preflight;
40
+ export function releaseGate(
41
+ input?: Evidence,
42
+ options?: { minScore?: number },
43
+ ): Gate;
package/index.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ export type Weekday =
2
+ | "monday"
3
+ | "tuesday"
4
+ | "wednesday"
5
+ | "thursday"
6
+ | "friday"
7
+ | "saturday"
8
+ | "sunday";
9
+ export interface Evidence {
10
+ tests?: number;
11
+ failingTests?: number;
12
+ coverage?: number;
13
+ build?: boolean;
14
+ uncommittedChanges?: boolean;
15
+ branch?: string;
16
+ day?: Weekday;
17
+ criticalIssues?: number;
18
+ lintFailures?: number;
19
+ }
20
+ export interface Readiness {
21
+ score: number;
22
+ verdict:
23
+ | "Absolutely Not"
24
+ | "Questionable"
25
+ | "Probably Fine"
26
+ | "Ship It"
27
+ | "Suspiciously Ready";
28
+ reasons: string[];
29
+ }
30
+ export interface Preflight extends Readiness {
31
+ actions: string[];
32
+ }
33
+ export interface Gate extends Preflight {
34
+ minScore: number;
35
+ passed: boolean;
36
+ blockers: string[];
37
+ }
38
+ export function shipIt(input?: Evidence): Readiness;
39
+ export function preflight(input?: Evidence): Preflight;
40
+ export function releaseGate(
41
+ input?: Evidence,
42
+ options?: { minScore?: number },
43
+ ): Gate;
package/package.json CHANGED
@@ -1,9 +1,20 @@
1
1
  {
2
2
  "name": "@deployanyway/ship-it-meter",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Explainable deployment readiness scoring for tests, coverage, and builds. Turns questionable confidence into a number.",
5
5
  "type": "module",
6
- "exports": "./src/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": {
9
+ "types": "./index.d.ts",
10
+ "default": "./src/index.js"
11
+ },
12
+ "require": {
13
+ "types": "./index.d.cts",
14
+ "default": "./dist/index.cjs"
15
+ }
16
+ }
17
+ },
7
18
  "bin": {
8
19
  "ship-it-meter": "./bin/cli.js"
9
20
  },
@@ -12,16 +23,26 @@
12
23
  "bin",
13
24
  "README.md",
14
25
  "LICENSE",
15
- "CHANGELOG.md"
26
+ "CHANGELOG.md",
27
+ "dist",
28
+ "index.d.ts",
29
+ "index.d.cts",
30
+ "MIGRATION.md"
16
31
  ],
17
32
  "engines": {
18
- "node": ">=22"
33
+ "node": ">=22.13"
19
34
  },
20
35
  "scripts": {
21
- "test": "node --test",
36
+ "test": "node --test test/*.test.js",
22
37
  "lint": "eslint .",
23
38
  "format": "prettier --write .",
24
- "format:check": "prettier --check ."
39
+ "format:check": "prettier --check .",
40
+ "build": "node scripts/build.mjs",
41
+ "pretest": "npm run build",
42
+ "prepack": "npm run build",
43
+ "coverage": "c8 --all --include=src/**/*.js --reporter=text --reporter=json-summary --reporter=lcov --check-coverage --lines=90 --statements=90 --functions=90 --branches=85 node --test test/*.test.js",
44
+ "test:types": "tsc -p tsconfig.json",
45
+ "verify:package": "node scripts/verify-package.mjs"
25
46
  },
26
47
  "repository": {
27
48
  "type": "git",
@@ -35,8 +56,12 @@
35
56
  "access": "public"
36
57
  },
37
58
  "devDependencies": {
59
+ "@types/node": "^26.6.4",
60
+ "c8": "^12.0.0",
61
+ "esbuild": "^0.28.2",
38
62
  "eslint": "^10.12.0",
39
- "prettier": "^3.6.2"
63
+ "prettier": "^3.6.2",
64
+ "typescript": "^7.0.2"
40
65
  },
41
66
  "keywords": [
42
67
  "nodejs",
@@ -47,6 +72,12 @@
47
72
  "readiness",
48
73
  "test-coverage",
49
74
  "build",
50
- "scoring"
51
- ]
75
+ "scoring",
76
+ "typescript",
77
+ "humor",
78
+ "deployanyway"
79
+ ],
80
+ "main": "./dist/index.cjs",
81
+ "types": "./index.d.ts",
82
+ "homepage": "https://deployanyway.github.io/"
52
83
  }
package/src/cli.js ADDED
@@ -0,0 +1,89 @@
1
+ import { parseArgs } from "node:util";
2
+ import { readFileSync } from "node:fs";
3
+ import { URL } from "node:url";
4
+ import { shipIt, preflight, releaseGate } from "./index.js";
5
+
6
+ import { readStdin } from "./input.js";
7
+
8
+ try {
9
+ const { values } = parseArgs({
10
+ options: {
11
+ help: { type: "boolean", short: "h" },
12
+ version: { type: "boolean", short: "v" },
13
+ json: { type: "boolean" },
14
+ stdin: { type: "boolean" },
15
+ gate: { type: "boolean" },
16
+ "min-score": { type: "string" },
17
+ checklist: { type: "boolean" },
18
+ tests: { type: "string" },
19
+ failing: { type: "string" },
20
+ coverage: { type: "string" },
21
+ day: { type: "string" },
22
+ branch: { type: "string" },
23
+ critical: { type: "string" },
24
+ lint: { type: "string" },
25
+ build: { type: "string" },
26
+ dirty: { type: "boolean" },
27
+ },
28
+ });
29
+ if (values.help) {
30
+ console.log(
31
+ "Usage: ship-it-meter [options]\n\nOptions:\n --tests number Total tests\n --failing number Failing tests\n --coverage number Coverage percent\n --build pass|fail Build result\n --critical number Open critical issues\n --lint number Lint failures\n --dirty Uncommitted changes\n --branch name Branch name\n --day weekday Full weekday name\n --checklist Actionable, mildly concerned preflight\n --json Structured output\n -h, --help Help\n -v, --version Version\n\n--stdin reads a JSON evidence object (256 KiB); explicit flags override it. --gate requires passing evidence; --min-score 80 selects its threshold. A blocked gate exits 1. Missing test, coverage, and build evidence reduces readiness.\nExit codes: 0 scored successfully; 2 invalid arguments.",
32
+ );
33
+ } else if (values.version) {
34
+ console.log(
35
+ JSON.parse(
36
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
37
+ ).version,
38
+ );
39
+ } else {
40
+ const input = values.stdin ? JSON.parse(await readStdin()) : {};
41
+ if (!input || typeof input !== "object" || Array.isArray(input))
42
+ throw new TypeError("stdin must contain an evidence object.");
43
+ if (
44
+ values["min-score"] !== undefined &&
45
+ (!values.gate || !/^\d+(?:\.\d+)?$/.test(values["min-score"]))
46
+ )
47
+ throw new TypeError("--min-score requires --gate and a decimal number.");
48
+ for (const [flag, key] of Object.entries({
49
+ tests: "tests",
50
+ failing: "failingTests",
51
+ coverage: "coverage",
52
+ critical: "criticalIssues",
53
+ lint: "lintFailures",
54
+ })) {
55
+ if (values[flag] !== undefined) {
56
+ if (!/^\d+(?:\.\d+)?$/.test(values[flag]))
57
+ throw new TypeError(`${flag} must be a nonnegative decimal number.`);
58
+ input[key] = Number(values[flag]);
59
+ }
60
+ }
61
+ if (values.build !== undefined) {
62
+ if (!["pass", "fail"].includes(values.build))
63
+ throw new RangeError("--build must be pass or fail.");
64
+ input.build = values.build === "pass";
65
+ }
66
+ for (const key of ["day", "branch"])
67
+ if (values[key] !== undefined) input[key] = values[key];
68
+ if (values.dirty) input.uncommittedChanges = true;
69
+ const result = values.gate
70
+ ? releaseGate(input, {
71
+ minScore:
72
+ values["min-score"] === undefined
73
+ ? undefined
74
+ : Number(values["min-score"]),
75
+ })
76
+ : values.checklist
77
+ ? preflight(input)
78
+ : shipIt(input);
79
+ if (values.gate && !result.passed) process.exitCode = 1;
80
+ console.log(
81
+ values.json
82
+ ? JSON.stringify(result, null, 2)
83
+ : `${result.score}/100 — ${result.verdict}${values.gate ? (result.passed ? " — gate passed" : " — gate blocked") : ""}\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}${result.actions ? "\n\nBefore you ship:\n" + result.actions.map((action) => `[ ] ${action}`).join("\n") : ""}`,
84
+ );
85
+ }
86
+ } catch (error) {
87
+ console.error(`ship-it-meter: ${error.message}\nRun with --help for usage.`);
88
+ process.exitCode = 2;
89
+ }
package/src/index.js CHANGED
@@ -17,6 +17,35 @@ const allowed = [
17
17
  "day",
18
18
  ];
19
19
 
20
+ /** Explicit automation gate; a score alone is never a deployment approval. */
21
+ export function releaseGate(input = {}, options = {}) {
22
+ if (!options || typeof options !== "object" || Array.isArray(options))
23
+ throw new TypeError("Gate options must be an object.");
24
+ const minScore = options.minScore ?? 80;
25
+ if (
26
+ typeof minScore !== "number" ||
27
+ !Number.isFinite(minScore) ||
28
+ minScore < 0 ||
29
+ minScore > 100
30
+ )
31
+ throw new RangeError("minScore must be between 0 and 100.");
32
+ const result = preflight(input);
33
+ const blockers = [];
34
+ if (!input.tests)
35
+ blockers.push("Provide a positive test count. Vibes cannot pass CI.");
36
+ if (input.build !== true) blockers.push("A passing build is required.");
37
+ if (input.coverage === undefined)
38
+ blockers.push("Coverage evidence is required.");
39
+ if (input.failingTests) blockers.push("Failing tests block the gate.");
40
+ if (input.criticalIssues) blockers.push("Critical issues block the gate.");
41
+ if (input.lintFailures) blockers.push("Lint failures block the gate.");
42
+ if (input.uncommittedChanges)
43
+ blockers.push("Uncommitted changes block the gate.");
44
+ if (result.score < minScore)
45
+ blockers.push(`Score ${result.score} is below ${minScore}.`);
46
+ return { ...result, minScore, passed: blockers.length === 0, blockers };
47
+ }
48
+
20
49
  /**
21
50
  * Calculate a stateless readiness score. Missing evidence reduces the score.
22
51
  * @param {{tests?: number, failingTests?: number, coverage?: number, build?: boolean, uncommittedChanges?: boolean, branch?: string, day?: string, criticalIssues?: number, lintFailures?: number}} [input]
package/src/input.js ADDED
@@ -0,0 +1,15 @@
1
+ /** Read bounded UTF-8 stdin without swallowing upstream failures. */
2
+ export async function readStdin(stream = process.stdin) {
3
+ if (stream.isTTY) throw new TypeError("Pipe or redirect input.");
4
+ let size = 0;
5
+ const chunks = [];
6
+ for await (const chunk of stream) {
7
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
8
+ size += buffer.length;
9
+ if (size > 262144) throw new RangeError("stdin exceeds 256 KiB.");
10
+ chunks.push(buffer);
11
+ }
12
+ const text = Buffer.concat(chunks).toString("utf8").trim();
13
+ if (!text) throw new TypeError("Provide nonempty input.");
14
+ return text;
15
+ }