cliguard 0.5.0 → 0.6.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/README.md +42 -3
- package/dist/bin.js +94 -8
- package/dist/core/storage.d.ts +15 -1
- package/dist/core/storage.js +51 -0
- package/dist/core/types.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# cliguard
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/cliguard)
|
|
4
|
+
[](https://www.npmjs.com/package/cliguard)
|
|
5
|
+
[](https://github.com/Bryandero98/cliguard/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/Bryandero98/cliguard/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
Snapshot testing for CLI contracts.
|
|
4
9
|
|
|
5
10
|
## The problem
|
|
@@ -23,7 +28,7 @@ The first line fails your CI. The second one doesn't - `--dry-run` is new and op
|
|
|
23
28
|
npm install --save-dev cliguard
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
Point cliguard straight at your existing CLI's entry file - most real CLIs work unmodified, since cliguard automatically captures the framework instance they build at load time even if they never export it (see "Entry files that build the CLI lazily" below). Exporting the instance is still the cleanest way to adopt cliguard where you can, since it never risks running any of your CLI's real logic:
|
|
27
32
|
|
|
28
33
|
```js
|
|
29
34
|
// bin/cli.js
|
|
@@ -113,13 +118,33 @@ npx cliguard check ./bin/cli.js --json
|
|
|
113
118
|
"changes": [
|
|
114
119
|
{ "type": "BREAKING", "path": "root -> build -> option[--target]", "message": "Option \"--target\" was removed." }
|
|
115
120
|
],
|
|
116
|
-
"summary": { "breaking": 1, "additive": 0, "patch": 0 },
|
|
121
|
+
"summary": { "breaking": 1, "acknowledgedBreaking": 0, "additive": 0, "patch": 0 },
|
|
117
122
|
"suggestedBump": "major"
|
|
118
123
|
}
|
|
119
124
|
```
|
|
120
125
|
|
|
121
126
|
`suggestedBump` is the semver bump this diff implies (`"major"`, `"minor"`, `"patch"`, or `null` if nothing changed) - a direct read of the same BREAKING/ADDITIVE/PATCH classification the emoji output already uses, so a release script never has to re-derive it.
|
|
122
127
|
|
|
128
|
+
### Accepting an intentional breaking change
|
|
129
|
+
|
|
130
|
+
Sometimes a `BREAKING` change is exactly what you meant to ship - a flag genuinely needed to go away in a major version. Running `cliguard update` after a real, intentional break re-baselines the *entire* contract silently; it doesn't leave a record of what changed or why. `cliguard accept` does:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
npx cliguard accept ./bin/cli.js "root -> build -> option[--target]" --reason "removed in v2.0, replaced by --targets"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This only works against a change `check` would currently report as `BREAKING` - it reads the exact `path` from your own `check` output (text or `--json`), so there's nothing to guess. It writes `.cliguard/accepted-breaks.json` (commit this file); from then on, `check` still shows that change - now as a 🟣 acknowledged line with the reason attached - but stops counting it toward the `BREAKING` total that fails your build. Any *other*, un-accepted breaking change still fails CI as normal. Once you're done, `cliguard update` still re-baselines the contract to match reality, same as always.
|
|
137
|
+
|
|
138
|
+
### Comparing two contracts directly
|
|
139
|
+
|
|
140
|
+
`cliguard diff <old.json> <new.json>` runs the same comparison as `check`, but reads both sides straight off disk instead of running any CLI - useful for comparing two tags' committed contracts (`git show v1.0.0:.cliguard/contract.json > old.json`), or reviewing a contract change in a PR without a working copy of the target CLI at all:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
npx cliguard diff old-contract.json new-contract.json --json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It respects `.cliguard/accepted-breaks.json` the same way `check` does, and exits `1` on an un-acknowledged `BREAKING` change.
|
|
147
|
+
|
|
123
148
|
## How changes get classified
|
|
124
149
|
|
|
125
150
|
| | Removed | Added | Required flipped | Value type / default changed |
|
|
@@ -133,10 +158,14 @@ Full rules live in [`src/core/diff.engine.ts`](src/core/diff.engine.ts) - it's t
|
|
|
133
158
|
|
|
134
159
|
## CI integration
|
|
135
160
|
|
|
161
|
+
The bundled GitHub Action (`Bryandero98/cliguard@v1`) is the recommended way to run this in CI: on top of the same exit-code gate as `npx cliguard check`, it posts the diff as a PR comment - updated in place on every push, not a new one each time - so a reviewer sees exactly what changed without opening the CI log:
|
|
162
|
+
|
|
136
163
|
```yaml
|
|
137
164
|
# .github/workflows/cliguard.yml
|
|
138
165
|
name: CLI contract
|
|
139
166
|
on: [pull_request]
|
|
167
|
+
permissions:
|
|
168
|
+
pull-requests: write # needed for the PR comment
|
|
140
169
|
jobs:
|
|
141
170
|
check:
|
|
142
171
|
runs-on: ubuntu-latest
|
|
@@ -145,7 +174,17 @@ jobs:
|
|
|
145
174
|
- uses: actions/setup-node@v4
|
|
146
175
|
with: { node-version: 22.x }
|
|
147
176
|
- run: npm ci
|
|
148
|
-
-
|
|
177
|
+
- uses: Bryandero98/cliguard@v1
|
|
178
|
+
with:
|
|
179
|
+
entry: ./bin/cli.js
|
|
180
|
+
# adapter: yargs # default: commander
|
|
181
|
+
# comment-on-pr: false # default: true
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Set `comment-on-pr: false` to keep the exit-code gate without the comment, or use the raw CLI directly for a non-GitHub CI provider:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
- run: npx cliguard check ./bin/cli.js
|
|
149
188
|
```
|
|
150
189
|
|
|
151
190
|
## Supported frameworks
|
package/dist/bin.js
CHANGED
|
@@ -66,20 +66,63 @@ program
|
|
|
66
66
|
const oldContract = (0, storage_1.readContract)();
|
|
67
67
|
const newContract = await resolveAdapter(options.adapter).extract(entry);
|
|
68
68
|
const diff = diffEngine.compare(oldContract, newContract);
|
|
69
|
-
const
|
|
69
|
+
const acceptedPaths = indexAcceptedBreaks((0, storage_1.readAcceptedBreaks)());
|
|
70
|
+
const hasBreaking = diff.some((change) => change.type === types_1.ChangeType.BREAKING && !acceptedPaths.has(change.path));
|
|
70
71
|
if (options.json) {
|
|
71
|
-
console.log(JSON.stringify(toJsonResult(diff), null, 2));
|
|
72
|
+
console.log(JSON.stringify(toJsonResult(diff, acceptedPaths), null, 2));
|
|
72
73
|
return hasBreaking ? 1 : 0;
|
|
73
74
|
}
|
|
74
75
|
if (diff.length === 0) {
|
|
75
76
|
console.log("✅ CLI contract is intact.");
|
|
76
77
|
return 0;
|
|
77
78
|
}
|
|
78
|
-
printDiff(diff);
|
|
79
|
+
printDiff(diff, acceptedPaths);
|
|
79
80
|
return hasBreaking ? 1 : 0;
|
|
80
81
|
});
|
|
81
82
|
process.exit(exitCode);
|
|
82
83
|
});
|
|
84
|
+
program
|
|
85
|
+
.command("accept")
|
|
86
|
+
.description("Record that a specific BREAKING change is intentional, so `check` stops failing CI for it")
|
|
87
|
+
.argument("<entry>", "path to the target CLI's entry file")
|
|
88
|
+
.argument("<changePath>", 'the exact DiffResult path to accept, e.g. "root -> build -> option[--target]"')
|
|
89
|
+
.requiredOption("-r, --reason <text>", "why this break is intentional - shown in check output")
|
|
90
|
+
.option(...adapterOption)
|
|
91
|
+
.action(async (entry, changePath, options) => {
|
|
92
|
+
const exitCode = await withSuppressedExit(async () => {
|
|
93
|
+
const reason = options.reason.trim();
|
|
94
|
+
if (!reason) {
|
|
95
|
+
console.error("cliguard: --reason can't be blank - it's the audit trail for why this break is OK.");
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
const oldContract = (0, storage_1.readContract)();
|
|
99
|
+
const newContract = await resolveAdapter(options.adapter).extract(entry);
|
|
100
|
+
const diff = diffEngine.compare(oldContract, newContract);
|
|
101
|
+
const match = diff.find((change) => change.type === types_1.ChangeType.BREAKING && change.path === changePath);
|
|
102
|
+
if (!match) {
|
|
103
|
+
const breaking = diff.filter((change) => change.type === types_1.ChangeType.BREAKING);
|
|
104
|
+
console.error(`cliguard: no current BREAKING change at path "${changePath}".` +
|
|
105
|
+
(breaking.length === 0
|
|
106
|
+
? " There are no BREAKING changes right now - nothing to accept."
|
|
107
|
+
: ` Currently breaking:\n${breaking.map((change) => ` - ${change.path}`).join("\n")}`));
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
// Replaces any earlier acceptance at the same path rather than
|
|
111
|
+
// accumulating duplicates - re-running `accept` updates the reason.
|
|
112
|
+
const remaining = (0, storage_1.readAcceptedBreaks)().filter((accepted) => accepted.path !== changePath);
|
|
113
|
+
const accepted = {
|
|
114
|
+
path: changePath,
|
|
115
|
+
reason,
|
|
116
|
+
acceptedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
(0, storage_1.writeAcceptedBreaks)([...remaining, accepted]);
|
|
119
|
+
console.log(`✅ Accepted: [${changePath}] ${match.message}`);
|
|
120
|
+
console.log(` Reason: ${reason}`);
|
|
121
|
+
console.log(` Recorded in ${(0, storage_1.getAcceptedBreaksDisplayPath)()} - commit this file.`);
|
|
122
|
+
return 0;
|
|
123
|
+
});
|
|
124
|
+
process.exit(exitCode);
|
|
125
|
+
});
|
|
83
126
|
program
|
|
84
127
|
.command("update")
|
|
85
128
|
.description("Overwrite the committed contract with the CLI's current surface")
|
|
@@ -94,6 +137,33 @@ program
|
|
|
94
137
|
});
|
|
95
138
|
process.exit(exitCode);
|
|
96
139
|
});
|
|
140
|
+
program
|
|
141
|
+
.command("diff")
|
|
142
|
+
.description("Compare two contract files directly, without running any CLI")
|
|
143
|
+
.argument("<oldContract>", "path to the older contract JSON file")
|
|
144
|
+
.argument("<newContract>", "path to the newer contract JSON file")
|
|
145
|
+
.option("--json", "print a machine-readable JSON result instead of text", false)
|
|
146
|
+
.action((oldPath, newPath, options) => {
|
|
147
|
+
// No adapter, no target CLI ever loaded here - just two files off
|
|
148
|
+
// disk - so none of withSuppressedExit's process.exit-race concerns
|
|
149
|
+
// apply. A thrown Error (bad path, corrupt JSON) still surfaces via
|
|
150
|
+
// this program's own top-level parseAsync().catch() below.
|
|
151
|
+
const oldContract = (0, storage_1.readContractFile)(oldPath);
|
|
152
|
+
const newContract = (0, storage_1.readContractFile)(newPath);
|
|
153
|
+
const diff = diffEngine.compare(oldContract, newContract);
|
|
154
|
+
const acceptedPaths = indexAcceptedBreaks((0, storage_1.readAcceptedBreaks)());
|
|
155
|
+
const hasBreaking = diff.some((change) => change.type === types_1.ChangeType.BREAKING && !acceptedPaths.has(change.path));
|
|
156
|
+
if (options.json) {
|
|
157
|
+
console.log(JSON.stringify(toJsonResult(diff, acceptedPaths), null, 2));
|
|
158
|
+
process.exit(hasBreaking ? 1 : 0);
|
|
159
|
+
}
|
|
160
|
+
if (diff.length === 0) {
|
|
161
|
+
console.log("✅ Contracts are identical.");
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
printDiff(diff, acceptedPaths);
|
|
165
|
+
process.exit(hasBreaking ? 1 : 0);
|
|
166
|
+
});
|
|
97
167
|
/**
|
|
98
168
|
* Runs `action` with `process.exit` neutralized, restoring the real one
|
|
99
169
|
* the instant `action` settles - then the caller calls the *real*
|
|
@@ -125,9 +195,19 @@ async function withSuppressedExit(action) {
|
|
|
125
195
|
process.exit = realExit;
|
|
126
196
|
}
|
|
127
197
|
}
|
|
128
|
-
function
|
|
198
|
+
function indexAcceptedBreaks(accepted) {
|
|
199
|
+
return new Map(accepted.map((entry) => [entry.path, entry]));
|
|
200
|
+
}
|
|
201
|
+
function toJsonResult(diff, acceptedPaths) {
|
|
202
|
+
const changes = diff.map((entry) => {
|
|
203
|
+
if (entry.type !== types_1.ChangeType.BREAKING)
|
|
204
|
+
return entry;
|
|
205
|
+
const accepted = acceptedPaths.get(entry.path);
|
|
206
|
+
return accepted ? { ...entry, acknowledged: true, reason: accepted.reason } : entry;
|
|
207
|
+
});
|
|
129
208
|
const summary = {
|
|
130
|
-
breaking:
|
|
209
|
+
breaking: changes.filter((change) => change.type === types_1.ChangeType.BREAKING && !change.acknowledged).length,
|
|
210
|
+
acknowledgedBreaking: changes.filter((change) => change.type === types_1.ChangeType.BREAKING && change.acknowledged).length,
|
|
131
211
|
additive: diff.filter((entry) => entry.type === types_1.ChangeType.ADDITIVE).length,
|
|
132
212
|
patch: diff.filter((entry) => entry.type === types_1.ChangeType.PATCH).length,
|
|
133
213
|
};
|
|
@@ -138,11 +218,17 @@ function toJsonResult(diff) {
|
|
|
138
218
|
: summary.patch > 0
|
|
139
219
|
? "patch"
|
|
140
220
|
: null;
|
|
141
|
-
return { ok: summary.breaking === 0, changes
|
|
221
|
+
return { ok: summary.breaking === 0, changes, summary, suggestedBump };
|
|
142
222
|
}
|
|
143
|
-
function printDiff(diff) {
|
|
223
|
+
function printDiff(diff, acceptedPaths) {
|
|
144
224
|
for (const entry of diff) {
|
|
145
|
-
|
|
225
|
+
const accepted = entry.type === types_1.ChangeType.BREAKING ? acceptedPaths.get(entry.path) : undefined;
|
|
226
|
+
if (accepted) {
|
|
227
|
+
console.log(`🟣 [${entry.path}] ${entry.message} (acknowledged: ${accepted.reason})`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`${emojiFor(entry.type)} [${entry.path}] ${entry.message}`);
|
|
231
|
+
}
|
|
146
232
|
}
|
|
147
233
|
}
|
|
148
234
|
function emojiFor(type) {
|
package/dist/core/storage.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import type { Contract } from "./types";
|
|
1
|
+
import type { AcceptedBreak, Contract } from "./types";
|
|
2
2
|
/** Contract path relative to cwd, normalized to forward slashes - display only, never used for I/O. */
|
|
3
3
|
export declare function getContractDisplayPath(): string;
|
|
4
|
+
/** Accepted-breaks path relative to cwd, normalized to forward slashes - display only, never used for I/O. */
|
|
5
|
+
export declare function getAcceptedBreaksDisplayPath(): string;
|
|
4
6
|
export declare function contractExists(): boolean;
|
|
5
7
|
export declare function readContract(): Contract;
|
|
6
8
|
export declare function writeContract(contract: Contract): void;
|
|
9
|
+
/**
|
|
10
|
+
* Reads a Contract from an arbitrary path, not the committed
|
|
11
|
+
* `.cliguard/contract.json` - for `cliguard diff <a> <b>`, comparing two
|
|
12
|
+
* contract files directly (e.g. two tags' committed contracts pulled via
|
|
13
|
+
* `git show`) without running any real CLI. `displayPath` is what error
|
|
14
|
+
* messages name; defaults to `path` itself since a caller-supplied path is
|
|
15
|
+
* already about as displayable as it gets.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readContractFile(path: string, displayPath?: string): Contract;
|
|
18
|
+
/** Unlike readContract, a missing file is normal (most projects never accept a break) - returns [] rather than throwing. */
|
|
19
|
+
export declare function readAcceptedBreaks(): AcceptedBreak[];
|
|
20
|
+
export declare function writeAcceptedBreaks(breaks: readonly AcceptedBreak[]): void;
|
package/dist/core/storage.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getContractDisplayPath = getContractDisplayPath;
|
|
4
|
+
exports.getAcceptedBreaksDisplayPath = getAcceptedBreaksDisplayPath;
|
|
4
5
|
exports.contractExists = contractExists;
|
|
5
6
|
exports.readContract = readContract;
|
|
6
7
|
exports.writeContract = writeContract;
|
|
8
|
+
exports.readContractFile = readContractFile;
|
|
9
|
+
exports.readAcceptedBreaks = readAcceptedBreaks;
|
|
10
|
+
exports.writeAcceptedBreaks = writeAcceptedBreaks;
|
|
7
11
|
const fs_1 = require("fs");
|
|
8
12
|
const path_1 = require("path");
|
|
9
13
|
const CONTRACT_PATH = (0, path_1.join)(process.cwd(), ".cliguard", "contract.json");
|
|
14
|
+
const ACCEPTED_BREAKS_PATH = (0, path_1.join)(process.cwd(), ".cliguard", "accepted-breaks.json");
|
|
10
15
|
/** Contract path relative to cwd, normalized to forward slashes - display only, never used for I/O. */
|
|
11
16
|
function getContractDisplayPath() {
|
|
12
17
|
return (0, path_1.relative)(process.cwd(), CONTRACT_PATH).split("\\").join("/");
|
|
13
18
|
}
|
|
19
|
+
/** Accepted-breaks path relative to cwd, normalized to forward slashes - display only, never used for I/O. */
|
|
20
|
+
function getAcceptedBreaksDisplayPath() {
|
|
21
|
+
return (0, path_1.relative)(process.cwd(), ACCEPTED_BREAKS_PATH).split("\\").join("/");
|
|
22
|
+
}
|
|
14
23
|
function contractExists() {
|
|
15
24
|
return (0, fs_1.existsSync)(CONTRACT_PATH);
|
|
16
25
|
}
|
|
@@ -39,3 +48,45 @@ function writeContract(contract) {
|
|
|
39
48
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(CONTRACT_PATH), { recursive: true });
|
|
40
49
|
(0, fs_1.writeFileSync)(CONTRACT_PATH, JSON.stringify(contract, null, 2) + "\n", "utf-8");
|
|
41
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Reads a Contract from an arbitrary path, not the committed
|
|
53
|
+
* `.cliguard/contract.json` - for `cliguard diff <a> <b>`, comparing two
|
|
54
|
+
* contract files directly (e.g. two tags' committed contracts pulled via
|
|
55
|
+
* `git show`) without running any real CLI. `displayPath` is what error
|
|
56
|
+
* messages name; defaults to `path` itself since a caller-supplied path is
|
|
57
|
+
* already about as displayable as it gets.
|
|
58
|
+
*/
|
|
59
|
+
function readContractFile(path, displayPath = path) {
|
|
60
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
61
|
+
throw new Error(`cliguard: no such file: "${displayPath}".`);
|
|
62
|
+
}
|
|
63
|
+
const raw = (0, fs_1.readFileSync)(path, "utf-8");
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(raw);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
69
|
+
throw new Error(`cliguard: "${displayPath}" is not valid JSON (${reason}).`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Unlike readContract, a missing file is normal (most projects never accept a break) - returns [] rather than throwing. */
|
|
73
|
+
function readAcceptedBreaks() {
|
|
74
|
+
if (!(0, fs_1.existsSync)(ACCEPTED_BREAKS_PATH))
|
|
75
|
+
return [];
|
|
76
|
+
const raw = (0, fs_1.readFileSync)(ACCEPTED_BREAKS_PATH, "utf-8");
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// See readContract's identical-purpose catch for why naming the file
|
|
82
|
+
// and the fix matters here too.
|
|
83
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
84
|
+
throw new Error(`cliguard: "${getAcceptedBreaksDisplayPath()}" is not valid JSON (${reason}). ` +
|
|
85
|
+
"If this file was hand-edited or came out of a bad merge, fix it or delete it " +
|
|
86
|
+
"and re-run `cliguard accept` for whatever was in it.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function writeAcceptedBreaks(breaks) {
|
|
90
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(ACCEPTED_BREAKS_PATH), { recursive: true });
|
|
91
|
+
(0, fs_1.writeFileSync)(ACCEPTED_BREAKS_PATH, JSON.stringify(breaks, null, 2) + "\n", "utf-8");
|
|
92
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -51,6 +51,21 @@ export interface Contract {
|
|
|
51
51
|
readonly capturedAt: string;
|
|
52
52
|
readonly root: CommandContract;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A specific BREAKING change the maintainer has deliberately accepted -
|
|
56
|
+
* written by `cliguard accept` and committed to `.cliguard/accepted-breaks.json`
|
|
57
|
+
* so the decision is auditable in the repo, not a silent CLI flag. `check`
|
|
58
|
+
* matches these against a diff's `DiffResult.path` and stops counting a
|
|
59
|
+
* match toward its exit code, while still showing it in the output.
|
|
60
|
+
*/
|
|
61
|
+
export interface AcceptedBreak {
|
|
62
|
+
/** Must equal the DiffResult.path of the breaking change being accepted, e.g. "root -> build -> option[--target]". */
|
|
63
|
+
readonly path: string;
|
|
64
|
+
/** Why this break is intentional - required, never blank, shown alongside the change. */
|
|
65
|
+
readonly reason: string;
|
|
66
|
+
/** ISO-8601 timestamp of when `cliguard accept` recorded this. */
|
|
67
|
+
readonly acceptedAt: string;
|
|
68
|
+
}
|
|
54
69
|
/** Severity of a single detected difference between two contracts. */
|
|
55
70
|
export declare enum ChangeType {
|
|
56
71
|
/** Removes or narrows something a caller may already depend on. */
|