zodrift 0.1.1 → 0.1.3
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 +44 -2
- package/dist/cli.js +2 -1
- package/dist/commands/check.js +13 -0
- package/dist/commands/fix.js +1 -0
- package/dist/core/checker.js +14 -2
- package/dist/core/semantic-checker.d.ts +10 -0
- package/dist/core/semantic-checker.js +190 -0
- package/dist/core/types.d.ts +5 -1
- package/dist/reporters/json.js +2 -0
- package/dist/reporters/pretty.js +3 -0
- package/dist/reporters/sarif.js +7 -0
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
[](https://github.com/greyllmmoder/zodrift/actions/workflows/ci.yml)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
ESLint for TypeScript and Zod drift. Catch contract drift before it reaches runtime.
|
|
8
|
+
|
|
9
|
+
Built for teams that already have TS types and Zod schemas and want CI to fail when they drift.
|
|
8
10
|
|
|
9
11
|
```ts
|
|
10
12
|
import { z } from "zod";
|
|
@@ -27,13 +29,53 @@ export const UserSchema = z.object({
|
|
|
27
29
|
npx zodrift check
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-

|
|
33
|
+
|
|
34
|
+
Example output:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
✗ User ↔ UserSchema
|
|
38
|
+
- optional mismatch for email: type=optional, schema=required
|
|
39
|
+
- type mismatch for age: type=number, schema=string
|
|
40
|
+
- extra in schema: role
|
|
41
|
+
```
|
|
31
42
|
|
|
32
43
|
What it catches:
|
|
33
44
|
- missing fields
|
|
34
45
|
- extra fields
|
|
35
46
|
- required vs optional mismatch
|
|
36
47
|
- basic type mismatch
|
|
48
|
+
- semantic mismatch when your TS type is not assignable to `z.input<typeof Schema>` / `z.output<typeof Schema>` (optional mode)
|
|
49
|
+
|
|
50
|
+
CI quick start:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
- run: npx zodrift check --pattern "src/**/*.{ts,tsx}" --semantics both
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Exit codes:
|
|
57
|
+
- `0`: no drift
|
|
58
|
+
- `1`: drift found
|
|
59
|
+
- `2`: parser/runtime error
|
|
60
|
+
|
|
61
|
+
Useful commands:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Check current project
|
|
65
|
+
npx zodrift check --pattern "src/**/*.{ts,tsx}"
|
|
66
|
+
|
|
67
|
+
# Add semantic compatibility checks for z.input/z.output
|
|
68
|
+
npx zodrift check --pattern "src/**/*.{ts,tsx}" --semantics both
|
|
69
|
+
|
|
70
|
+
# Machine-readable report for CI artifacts
|
|
71
|
+
npx zodrift check --format json --out reports/zodrift.json
|
|
72
|
+
|
|
73
|
+
# SARIF for GitHub code scanning
|
|
74
|
+
npx zodrift check --format sarif --out reports/zodrift.sarif
|
|
75
|
+
|
|
76
|
+
# Safe autofix pass (dry-run first)
|
|
77
|
+
npx zodrift fix --pattern "src/**/*.ts" --dry-run
|
|
78
|
+
```
|
|
37
79
|
|
|
38
80
|
Roadmap:
|
|
39
81
|
- nested support
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ function printHelp() {
|
|
|
9
9
|
process.stdout.write(`zodrift
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
zodrift check [--pattern <glob>] [--format pretty|json|sarif] [--out <file>] [--changed]
|
|
12
|
+
zodrift check [--pattern <glob>] [--format pretty|json|sarif] [--semantics off|input|output|both] [--out <file>] [--changed]
|
|
13
13
|
zodrift fix [--pattern <glob>] [--target schema] [--dry-run] [--write]
|
|
14
14
|
zodrift codegen [--from ts|zod] [--pattern <glob>] [--out-dir <dir>] [--write]
|
|
15
15
|
zodrift openapi [--pattern <glob>] [--out <file>]
|
|
@@ -18,6 +18,7 @@ Usage:
|
|
|
18
18
|
Examples:
|
|
19
19
|
npx zodrift check
|
|
20
20
|
npx zodrift check --pattern "examples/**/*.ts"
|
|
21
|
+
npx zodrift check --semantics both
|
|
21
22
|
npx zodrift check --format json --out reports/zodrift.json
|
|
22
23
|
npx zodrift fix --pattern "src/**/*.ts" --dry-run
|
|
23
24
|
`);
|
package/dist/commands/check.js
CHANGED
|
@@ -3,16 +3,29 @@ import path from "node:path";
|
|
|
3
3
|
import { runCheck } from "../core/checker.js";
|
|
4
4
|
import { renderOutput } from "../reporters/index.js";
|
|
5
5
|
import { flagBoolean, flagNumber, flagString } from "../utils/args.js";
|
|
6
|
+
function parseSemanticsMode(value) {
|
|
7
|
+
if (value === "off" || value === "input" || value === "output" || value === "both") {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
6
12
|
export function runCheckCommand(flags) {
|
|
7
13
|
const cwd = process.cwd();
|
|
8
14
|
const pattern = flagString(flags, "pattern", "**/*.{ts,tsx}");
|
|
9
15
|
const format = flagString(flags, "format", "pretty");
|
|
16
|
+
const semanticsRaw = flagString(flags, "semantics", "off");
|
|
17
|
+
const semantics = parseSemanticsMode(semanticsRaw);
|
|
18
|
+
if (!semantics) {
|
|
19
|
+
process.stderr.write(`Invalid --semantics value: ${semanticsRaw}. Use one of: off, input, output, both.\n`);
|
|
20
|
+
return 2;
|
|
21
|
+
}
|
|
10
22
|
const maxIssues = flagNumber(flags, "max-issues");
|
|
11
23
|
const changedOnly = flagBoolean(flags, "changed", false);
|
|
12
24
|
const result = runCheck({
|
|
13
25
|
cwd,
|
|
14
26
|
pattern,
|
|
15
27
|
format,
|
|
28
|
+
semantics,
|
|
16
29
|
maxIssues,
|
|
17
30
|
changedOnly,
|
|
18
31
|
});
|
package/dist/commands/fix.js
CHANGED
package/dist/core/checker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { comparePair } from "./compare.js";
|
|
2
2
|
import { discoverSourceFiles } from "./file-discovery.js";
|
|
3
3
|
import { pairTypesWithSchemas } from "./pairing.js";
|
|
4
|
+
import { collectSemanticIssues, pairKey } from "./semantic-checker.js";
|
|
4
5
|
import { parseTypeDeclarations } from "./ts-parser.js";
|
|
5
6
|
import { parseSchemaDeclarations } from "./zod-parser.js";
|
|
6
7
|
export function runCheck(options) {
|
|
@@ -12,14 +13,23 @@ export function runCheck(options) {
|
|
|
12
13
|
const typeParsed = parseTypeDeclarations(files);
|
|
13
14
|
const schemaParsed = parseSchemaDeclarations(files);
|
|
14
15
|
const paired = pairTypesWithSchemas(typeParsed.declarations, schemaParsed.declarations);
|
|
16
|
+
const semantic = collectSemanticIssues({
|
|
17
|
+
cwd: options.cwd,
|
|
18
|
+
pairs: paired.pairs,
|
|
19
|
+
semantics: options.semantics,
|
|
20
|
+
});
|
|
15
21
|
const pairs = [];
|
|
16
22
|
let totalIssues = 0;
|
|
23
|
+
let semanticIssueCount = 0;
|
|
17
24
|
for (const pair of paired.pairs) {
|
|
18
|
-
const
|
|
25
|
+
const structuralIssues = comparePair(pair);
|
|
26
|
+
const semanticIssues = semantic.issuesByPair.get(pairKey(pair)) ?? [];
|
|
27
|
+
const issues = [...structuralIssues, ...semanticIssues];
|
|
19
28
|
const cappedIssues = options.maxIssues && options.maxIssues > 0
|
|
20
29
|
? issues.slice(0, options.maxIssues)
|
|
21
30
|
: issues;
|
|
22
31
|
totalIssues += cappedIssues.length;
|
|
32
|
+
semanticIssueCount += cappedIssues.filter((issue) => issue.kind === "semantic_mismatch").length;
|
|
23
33
|
pairs.push({
|
|
24
34
|
typeName: pair.typeDecl.name,
|
|
25
35
|
schemaName: pair.schemaDecl.name,
|
|
@@ -34,6 +44,8 @@ export function runCheck(options) {
|
|
|
34
44
|
checkedPairs: pairs.length,
|
|
35
45
|
unmatchedTypes: paired.unmatchedTypes,
|
|
36
46
|
unmatchedSchemas: paired.unmatchedSchemas,
|
|
37
|
-
|
|
47
|
+
semanticsMode: options.semantics,
|
|
48
|
+
semanticIssueCount,
|
|
49
|
+
errors: [...typeParsed.errors, ...schemaParsed.errors, ...semantic.errors],
|
|
38
50
|
};
|
|
39
51
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DriftIssue, Pairing, SemanticsMode } from "./types.js";
|
|
2
|
+
export declare function pairKey(pair: Pairing): string;
|
|
3
|
+
export declare function collectSemanticIssues(options: {
|
|
4
|
+
cwd: string;
|
|
5
|
+
pairs: Pairing[];
|
|
6
|
+
semantics: SemanticsMode;
|
|
7
|
+
}): {
|
|
8
|
+
issuesByPair: Map<string, DriftIssue[]>;
|
|
9
|
+
errors: string[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
const TYPE_ALIAS_NAME = "__ZodriftTypeAlias";
|
|
5
|
+
const INPUT_ALIAS_NAME = "__ZodriftInputAlias";
|
|
6
|
+
const OUTPUT_ALIAS_NAME = "__ZodriftOutputAlias";
|
|
7
|
+
const TYPE_IMPORT_NAME = "__ZodriftType";
|
|
8
|
+
const SCHEMA_IMPORT_NAME = "__ZodriftSchema";
|
|
9
|
+
function pairName(pair) {
|
|
10
|
+
return `${pair.typeDecl.name} ↔ ${pair.schemaDecl.name}`;
|
|
11
|
+
}
|
|
12
|
+
export function pairKey(pair) {
|
|
13
|
+
return `${path.resolve(pair.typeDecl.sourceFilePath)}::${pair.typeDecl.name}::${path.resolve(pair.schemaDecl.sourceFilePath)}::${pair.schemaDecl.name}`;
|
|
14
|
+
}
|
|
15
|
+
function loadCompilerOptions(cwd) {
|
|
16
|
+
const fallback = {
|
|
17
|
+
target: ts.ScriptTarget.ES2022,
|
|
18
|
+
module: ts.ModuleKind.NodeNext,
|
|
19
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
20
|
+
jsx: ts.JsxEmit.Preserve,
|
|
21
|
+
allowJs: false,
|
|
22
|
+
skipLibCheck: true,
|
|
23
|
+
strict: true,
|
|
24
|
+
exactOptionalPropertyTypes: true,
|
|
25
|
+
noEmit: true,
|
|
26
|
+
};
|
|
27
|
+
const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
|
|
28
|
+
if (!configPath) {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
32
|
+
if (read.error) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath));
|
|
36
|
+
return {
|
|
37
|
+
...parsed.options,
|
|
38
|
+
module: ts.ModuleKind.NodeNext,
|
|
39
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
40
|
+
jsx: parsed.options.jsx ?? ts.JsxEmit.Preserve,
|
|
41
|
+
rootDir: cwd,
|
|
42
|
+
strict: true,
|
|
43
|
+
exactOptionalPropertyTypes: true,
|
|
44
|
+
skipLibCheck: true,
|
|
45
|
+
noEmit: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function toImportSpecifier(fromFilePath, targetFilePath) {
|
|
49
|
+
const relative = path
|
|
50
|
+
.relative(path.dirname(fromFilePath), targetFilePath)
|
|
51
|
+
.split(path.sep)
|
|
52
|
+
.join("/");
|
|
53
|
+
const withDot = relative.startsWith(".") ? relative : `./${relative}`;
|
|
54
|
+
const withoutExt = withDot.replace(/\.[cm]?tsx?$/i, "");
|
|
55
|
+
return `${withoutExt}.js`;
|
|
56
|
+
}
|
|
57
|
+
function formatDiagnostic(diagnostic) {
|
|
58
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
59
|
+
if (!diagnostic.file || typeof diagnostic.start !== "number") {
|
|
60
|
+
return message;
|
|
61
|
+
}
|
|
62
|
+
const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
63
|
+
const filePath = path.resolve(diagnostic.file.fileName);
|
|
64
|
+
return `${filePath}:${pos.line + 1}:${pos.character + 1} ${message}`;
|
|
65
|
+
}
|
|
66
|
+
function typeText(checker, type) {
|
|
67
|
+
return checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
68
|
+
}
|
|
69
|
+
function compareEquivalence(checker, left, right) {
|
|
70
|
+
return {
|
|
71
|
+
leftToRight: checker.isTypeAssignableTo(left, right),
|
|
72
|
+
rightToLeft: checker.isTypeAssignableTo(right, left),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function makeSemanticIssue(params) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "semantic_mismatch",
|
|
78
|
+
pairName: pairName(params.pair),
|
|
79
|
+
path: `(semantic:${params.mode})`,
|
|
80
|
+
message: params.message,
|
|
81
|
+
typeValue: params.typeValue,
|
|
82
|
+
schemaValue: params.schemaValue,
|
|
83
|
+
typeLocation: params.pair.typeDecl.location,
|
|
84
|
+
schemaLocation: params.pair.schemaDecl.location,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildSemanticProbeContent(pair, probeFilePath) {
|
|
88
|
+
const typeSpecifier = toImportSpecifier(probeFilePath, pair.typeDecl.sourceFilePath);
|
|
89
|
+
const schemaSpecifier = toImportSpecifier(probeFilePath, pair.schemaDecl.sourceFilePath);
|
|
90
|
+
return `import { z } from "zod";
|
|
91
|
+
import type { ${pair.typeDecl.name} as ${TYPE_IMPORT_NAME} } from "${typeSpecifier}";
|
|
92
|
+
import { ${pair.schemaDecl.name} as ${SCHEMA_IMPORT_NAME} } from "${schemaSpecifier}";
|
|
93
|
+
|
|
94
|
+
type ${TYPE_ALIAS_NAME} = ${TYPE_IMPORT_NAME};
|
|
95
|
+
type ${INPUT_ALIAS_NAME} = z.input<typeof ${SCHEMA_IMPORT_NAME}>;
|
|
96
|
+
type ${OUTPUT_ALIAS_NAME} = z.output<typeof ${SCHEMA_IMPORT_NAME}>;
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
function findAliasType(checker, sourceFile, aliasName) {
|
|
100
|
+
const alias = sourceFile.statements.find((statement) => ts.isTypeAliasDeclaration(statement) && statement.name.text === aliasName);
|
|
101
|
+
if (!alias) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return checker.getTypeAtLocation(alias);
|
|
105
|
+
}
|
|
106
|
+
export function collectSemanticIssues(options) {
|
|
107
|
+
const issuesByPair = new Map();
|
|
108
|
+
const errors = [];
|
|
109
|
+
if (options.semantics === "off" || options.pairs.length === 0) {
|
|
110
|
+
return { issuesByPair, errors };
|
|
111
|
+
}
|
|
112
|
+
const compilerOptions = loadCompilerOptions(options.cwd);
|
|
113
|
+
const tempDir = fs.mkdtempSync(path.join(options.cwd, ".zodrift-semantic-"));
|
|
114
|
+
const runInput = options.semantics === "input" || options.semantics === "both";
|
|
115
|
+
const runOutput = options.semantics === "output" || options.semantics === "both";
|
|
116
|
+
try {
|
|
117
|
+
for (let index = 0; index < options.pairs.length; index += 1) {
|
|
118
|
+
const pair = options.pairs[index];
|
|
119
|
+
const pairIssues = [];
|
|
120
|
+
const probeFilePath = path.join(tempDir, `probe-${index}.ts`);
|
|
121
|
+
fs.writeFileSync(probeFilePath, buildSemanticProbeContent(pair, probeFilePath), "utf8");
|
|
122
|
+
const program = ts.createProgram([probeFilePath], compilerOptions);
|
|
123
|
+
const diagnostics = ts
|
|
124
|
+
.getPreEmitDiagnostics(program)
|
|
125
|
+
.filter((diag) => diag.category === ts.DiagnosticCategory.Error);
|
|
126
|
+
if (diagnostics.length > 0) {
|
|
127
|
+
const message = formatDiagnostic(diagnostics[0]);
|
|
128
|
+
pairIssues.push(makeSemanticIssue({
|
|
129
|
+
pair,
|
|
130
|
+
mode: runInput ? "input" : "output",
|
|
131
|
+
message: `semantic check could not evaluate pair: ${message}`,
|
|
132
|
+
}));
|
|
133
|
+
issuesByPair.set(pairKey(pair), pairIssues);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const checker = program.getTypeChecker();
|
|
137
|
+
const probeSource = program.getSourceFile(probeFilePath);
|
|
138
|
+
if (!probeSource) {
|
|
139
|
+
pairIssues.push(makeSemanticIssue({
|
|
140
|
+
pair,
|
|
141
|
+
mode: runInput ? "input" : "output",
|
|
142
|
+
message: "semantic check could not load probe source file",
|
|
143
|
+
}));
|
|
144
|
+
issuesByPair.set(pairKey(pair), pairIssues);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const typeType = findAliasType(checker, probeSource, TYPE_ALIAS_NAME);
|
|
148
|
+
const inputType = findAliasType(checker, probeSource, INPUT_ALIAS_NAME);
|
|
149
|
+
const outputType = findAliasType(checker, probeSource, OUTPUT_ALIAS_NAME);
|
|
150
|
+
if (!typeType || !inputType || !outputType) {
|
|
151
|
+
pairIssues.push(makeSemanticIssue({
|
|
152
|
+
pair,
|
|
153
|
+
mode: runInput ? "input" : "output",
|
|
154
|
+
message: "semantic check could not infer probe alias types",
|
|
155
|
+
}));
|
|
156
|
+
issuesByPair.set(pairKey(pair), pairIssues);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const evaluate = (mode, schemaType) => {
|
|
160
|
+
const comparison = compareEquivalence(checker, typeType, schemaType);
|
|
161
|
+
if (comparison.leftToRight) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
pairIssues.push(makeSemanticIssue({
|
|
165
|
+
pair,
|
|
166
|
+
mode,
|
|
167
|
+
message: `semantic mismatch (${mode}): ${pair.typeDecl.name} is not assignable to z.${mode}<typeof ${pair.schemaDecl.name}> (type->schema=${comparison.leftToRight}, schema->type=${comparison.rightToLeft})`,
|
|
168
|
+
typeValue: typeText(checker, typeType),
|
|
169
|
+
schemaValue: typeText(checker, schemaType),
|
|
170
|
+
}));
|
|
171
|
+
};
|
|
172
|
+
if (runInput) {
|
|
173
|
+
evaluate("input", inputType);
|
|
174
|
+
}
|
|
175
|
+
if (runOutput) {
|
|
176
|
+
evaluate("output", outputType);
|
|
177
|
+
}
|
|
178
|
+
if (pairIssues.length > 0) {
|
|
179
|
+
issuesByPair.set(pairKey(pair), pairIssues);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
errors.push(`Failed semantic check pass: ${String(error)}`);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
return { issuesByPair, errors };
|
|
190
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface SchemaDeclaration {
|
|
|
65
65
|
sourceFilePath: string;
|
|
66
66
|
objectNodeText?: string;
|
|
67
67
|
}
|
|
68
|
-
export type DriftIssueKind = "missing_in_schema" | "extra_in_schema" | "optional_mismatch" | "type_mismatch";
|
|
68
|
+
export type DriftIssueKind = "missing_in_schema" | "extra_in_schema" | "optional_mismatch" | "type_mismatch" | "semantic_mismatch";
|
|
69
69
|
export interface DriftIssue {
|
|
70
70
|
kind: DriftIssueKind;
|
|
71
71
|
pairName: string;
|
|
@@ -89,6 +89,8 @@ export interface CheckResult {
|
|
|
89
89
|
checkedPairs: number;
|
|
90
90
|
unmatchedTypes: string[];
|
|
91
91
|
unmatchedSchemas: string[];
|
|
92
|
+
semanticsMode: SemanticsMode;
|
|
93
|
+
semanticIssueCount: number;
|
|
92
94
|
errors: string[];
|
|
93
95
|
}
|
|
94
96
|
export interface Pairing {
|
|
@@ -100,10 +102,12 @@ export interface ReporterPayload {
|
|
|
100
102
|
cwd: string;
|
|
101
103
|
}
|
|
102
104
|
export type OutputFormat = "pretty" | "json" | "sarif";
|
|
105
|
+
export type SemanticsMode = "off" | "input" | "output" | "both";
|
|
103
106
|
export interface CheckOptions {
|
|
104
107
|
cwd: string;
|
|
105
108
|
pattern: string;
|
|
106
109
|
format: OutputFormat;
|
|
110
|
+
semantics: SemanticsMode;
|
|
107
111
|
maxIssues?: number;
|
|
108
112
|
changedOnly: boolean;
|
|
109
113
|
}
|
package/dist/reporters/json.js
CHANGED
|
@@ -3,6 +3,8 @@ export function renderJson(payload) {
|
|
|
3
3
|
summary: {
|
|
4
4
|
checkedPairs: payload.result.checkedPairs,
|
|
5
5
|
totalIssues: payload.result.totalIssues,
|
|
6
|
+
semanticsMode: payload.result.semanticsMode,
|
|
7
|
+
semanticIssueCount: payload.result.semanticIssueCount,
|
|
6
8
|
unmatchedTypes: payload.result.unmatchedTypes,
|
|
7
9
|
unmatchedSchemas: payload.result.unmatchedSchemas,
|
|
8
10
|
errors: payload.result.errors,
|
package/dist/reporters/pretty.js
CHANGED
|
@@ -31,5 +31,8 @@ export function renderPretty(payload) {
|
|
|
31
31
|
.join(", ")}`);
|
|
32
32
|
}
|
|
33
33
|
lines.push(`Checked pairs: ${payload.result.checkedPairs} | Issues: ${payload.result.totalIssues}`);
|
|
34
|
+
if (payload.result.semanticsMode !== "off") {
|
|
35
|
+
lines.push(`Semantic mode: ${payload.result.semanticsMode} | Semantic issues: ${payload.result.semanticIssueCount}`);
|
|
36
|
+
}
|
|
34
37
|
return lines.join("\n").trim();
|
|
35
38
|
}
|
package/dist/reporters/sarif.js
CHANGED
|
@@ -3,6 +3,7 @@ function levelForIssue(kind) {
|
|
|
3
3
|
switch (kind) {
|
|
4
4
|
case "type_mismatch":
|
|
5
5
|
case "optional_mismatch":
|
|
6
|
+
case "semantic_mismatch":
|
|
6
7
|
return "error";
|
|
7
8
|
default:
|
|
8
9
|
return "warning";
|
|
@@ -18,6 +19,8 @@ function ruleName(kind) {
|
|
|
18
19
|
return "optional-mismatch";
|
|
19
20
|
case "type_mismatch":
|
|
20
21
|
return "type-mismatch";
|
|
22
|
+
case "semantic_mismatch":
|
|
23
|
+
return "semantic-mismatch";
|
|
21
24
|
default:
|
|
22
25
|
return "drift";
|
|
23
26
|
}
|
|
@@ -40,6 +43,10 @@ export function renderSarif(payload) {
|
|
|
40
43
|
id: "type-mismatch",
|
|
41
44
|
shortDescription: { text: "Type mismatch between TS and Zod" },
|
|
42
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: "semantic-mismatch",
|
|
48
|
+
shortDescription: { text: "TypeScript semantic mismatch against z.input/z.output" },
|
|
49
|
+
},
|
|
43
50
|
];
|
|
44
51
|
const results = payload.result.pairs.flatMap((pair) => pair.issues.map((issue) => {
|
|
45
52
|
const location = issue.schemaLocation ?? issue.typeLocation;
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodrift",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "ESLint-style CI drift checker for TypeScript types and Zod schemas.",
|
|
5
|
+
"author": "greyllmmoder",
|
|
6
|
+
"homepage": "https://github.com/greyllmmoder/zodrift#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/greyllmmoder/zodrift/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/greyllmmoder/zodrift.git"
|
|
13
|
+
},
|
|
5
14
|
"type": "module",
|
|
6
15
|
"license": "MIT",
|
|
7
16
|
"bin": {
|
|
@@ -24,10 +33,17 @@
|
|
|
24
33
|
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
25
34
|
},
|
|
26
35
|
"keywords": [
|
|
36
|
+
"zodrift",
|
|
37
|
+
"eslint-for-zod",
|
|
27
38
|
"zod",
|
|
28
39
|
"typescript",
|
|
29
40
|
"schema",
|
|
30
41
|
"drift",
|
|
42
|
+
"drift-detection",
|
|
43
|
+
"schema-validation",
|
|
44
|
+
"api-contracts",
|
|
45
|
+
"type-safety",
|
|
46
|
+
"ci",
|
|
31
47
|
"cli"
|
|
32
48
|
],
|
|
33
49
|
"devDependencies": {
|