repo-contract 0.1.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +967 -0
  4. package/dist/.dts/config/define-repo-contract.d.ts +36 -0
  5. package/dist/.dts/config/define-repo-contract.d.ts.map +1 -0
  6. package/dist/.dts/config/tokenize-command.d.ts +33 -0
  7. package/dist/.dts/config/tokenize-command.d.ts.map +1 -0
  8. package/dist/.dts/config/validate-config.d.ts +32 -0
  9. package/dist/.dts/config/validate-config.d.ts.map +1 -0
  10. package/dist/.dts/errors.d.ts +155 -0
  11. package/dist/.dts/errors.d.ts.map +1 -0
  12. package/dist/.dts/evidence/build-evidence.d.ts +26 -0
  13. package/dist/.dts/evidence/build-evidence.d.ts.map +1 -0
  14. package/dist/.dts/execution/abort-signals.d.ts +29 -0
  15. package/dist/.dts/execution/abort-signals.d.ts.map +1 -0
  16. package/dist/.dts/execution/concurrency-pool.d.ts +14 -0
  17. package/dist/.dts/execution/concurrency-pool.d.ts.map +1 -0
  18. package/dist/.dts/execution/dependency-scheduler.d.ts +30 -0
  19. package/dist/.dts/execution/dependency-scheduler.d.ts.map +1 -0
  20. package/dist/.dts/execution/process-tree.d.ts +48 -0
  21. package/dist/.dts/execution/process-tree.d.ts.map +1 -0
  22. package/dist/.dts/execution/run-checks.d.ts +29 -0
  23. package/dist/.dts/execution/run-checks.d.ts.map +1 -0
  24. package/dist/.dts/execution/spawn-check.d.ts +30 -0
  25. package/dist/.dts/execution/spawn-check.d.ts.map +1 -0
  26. package/dist/.dts/index.d.ts +13 -0
  27. package/dist/.dts/index.d.ts.map +1 -0
  28. package/dist/.dts/parsing/parse-json.d.ts +8 -0
  29. package/dist/.dts/parsing/parse-json.d.ts.map +1 -0
  30. package/dist/.dts/parsing/parse-output.d.ts +10 -0
  31. package/dist/.dts/parsing/parse-output.d.ts.map +1 -0
  32. package/dist/.dts/parsing/parse-text.d.ts +8 -0
  33. package/dist/.dts/parsing/parse-text.d.ts.map +1 -0
  34. package/dist/.dts/parsing/parse-yaml.d.ts +21 -0
  35. package/dist/.dts/parsing/parse-yaml.d.ts.map +1 -0
  36. package/dist/.dts/policy/run-policies.d.ts +38 -0
  37. package/dist/.dts/policy/run-policies.d.ts.map +1 -0
  38. package/dist/.dts/presets/arethetypeswrong.d.ts +39 -0
  39. package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -0
  40. package/dist/.dts/presets/broken-links.d.ts +16 -0
  41. package/dist/.dts/presets/broken-links.d.ts.map +1 -0
  42. package/dist/.dts/presets/commitlint.d.ts +22 -0
  43. package/dist/.dts/presets/commitlint.d.ts.map +1 -0
  44. package/dist/.dts/presets/dead-code.d.ts +23 -0
  45. package/dist/.dts/presets/dead-code.d.ts.map +1 -0
  46. package/dist/.dts/presets/duplication.d.ts +14 -0
  47. package/dist/.dts/presets/duplication.d.ts.map +1 -0
  48. package/dist/.dts/presets/e2e.d.ts +4 -0
  49. package/dist/.dts/presets/e2e.d.ts.map +1 -0
  50. package/dist/.dts/presets/format.d.ts +4 -0
  51. package/dist/.dts/presets/format.d.ts.map +1 -0
  52. package/dist/.dts/presets/index.d.ts +31 -0
  53. package/dist/.dts/presets/index.d.ts.map +1 -0
  54. package/dist/.dts/presets/license.d.ts +4 -0
  55. package/dist/.dts/presets/license.d.ts.map +1 -0
  56. package/dist/.dts/presets/lint.d.ts +20 -0
  57. package/dist/.dts/presets/lint.d.ts.map +1 -0
  58. package/dist/.dts/presets/markdownlint.d.ts +23 -0
  59. package/dist/.dts/presets/markdownlint.d.ts.map +1 -0
  60. package/dist/.dts/presets/publint.d.ts +13 -0
  61. package/dist/.dts/presets/publint.d.ts.map +1 -0
  62. package/dist/.dts/presets/security-deps.d.ts +4 -0
  63. package/dist/.dts/presets/security-deps.d.ts.map +1 -0
  64. package/dist/.dts/presets/security-secrets.d.ts +4 -0
  65. package/dist/.dts/presets/security-secrets.d.ts.map +1 -0
  66. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +13 -0
  67. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +1 -0
  68. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +23 -0
  69. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +1 -0
  70. package/dist/.dts/presets/shared/missing-dependency.d.ts +19 -0
  71. package/dist/.dts/presets/shared/missing-dependency.d.ts.map +1 -0
  72. package/dist/.dts/presets/shared/read-json-report.d.ts +33 -0
  73. package/dist/.dts/presets/shared/read-json-report.d.ts.map +1 -0
  74. package/dist/.dts/presets/shared/terminal-status.d.ts +25 -0
  75. package/dist/.dts/presets/shared/terminal-status.d.ts.map +1 -0
  76. package/dist/.dts/presets/shared/vitest-json-policy.d.ts +20 -0
  77. package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +1 -0
  78. package/dist/.dts/presets/stylelint.d.ts +17 -0
  79. package/dist/.dts/presets/stylelint.d.ts.map +1 -0
  80. package/dist/.dts/presets/test.d.ts +4 -0
  81. package/dist/.dts/presets/test.d.ts.map +1 -0
  82. package/dist/.dts/presets/typecheck.d.ts +4 -0
  83. package/dist/.dts/presets/typecheck.d.ts.map +1 -0
  84. package/dist/.dts/run-repo-contract.d.ts +38 -0
  85. package/dist/.dts/run-repo-contract.d.ts.map +1 -0
  86. package/dist/.dts/types.d.ts +324 -0
  87. package/dist/.dts/types.d.ts.map +1 -0
  88. package/dist/index.cjs +46 -0
  89. package/dist/index.cjs.map +1 -0
  90. package/dist/index.d.cts +1 -0
  91. package/dist/index.d.ts +1 -0
  92. package/dist/index.js +11 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/presets.cjs +30 -0
  95. package/dist/presets.cjs.map +1 -0
  96. package/dist/presets.d.cts +1 -0
  97. package/dist/presets.d.ts +1 -0
  98. package/dist/presets.js +13 -0
  99. package/dist/presets.js.map +1 -0
  100. package/package.json +192 -0
  101. package/presets/package.json +5 -0
  102. package/schemas/evidence.schema.json +253 -0
  103. package/schemas/verdict.schema.json +66 -0
  104. package/src/config/define-repo-contract.ts +38 -0
  105. package/src/config/tokenize-command.ts +214 -0
  106. package/src/config/validate-config.ts +368 -0
  107. package/src/errors.ts +229 -0
  108. package/src/evidence/build-evidence.ts +91 -0
  109. package/src/execution/abort-signals.ts +56 -0
  110. package/src/execution/concurrency-pool.ts +64 -0
  111. package/src/execution/dependency-scheduler.ts +216 -0
  112. package/src/execution/process-tree.ts +107 -0
  113. package/src/execution/run-checks.ts +348 -0
  114. package/src/execution/spawn-check.ts +494 -0
  115. package/src/index.ts +44 -0
  116. package/src/parsing/parse-json.ts +18 -0
  117. package/src/parsing/parse-output.ts +26 -0
  118. package/src/parsing/parse-text.ts +10 -0
  119. package/src/parsing/parse-yaml.ts +40 -0
  120. package/src/policy/run-policies.ts +261 -0
  121. package/src/presets/arethetypeswrong.ts +116 -0
  122. package/src/presets/broken-links.ts +95 -0
  123. package/src/presets/commitlint.ts +77 -0
  124. package/src/presets/dead-code.ts +223 -0
  125. package/src/presets/duplication.ts +137 -0
  126. package/src/presets/e2e.ts +144 -0
  127. package/src/presets/format.ts +25 -0
  128. package/src/presets/index.ts +30 -0
  129. package/src/presets/license.ts +90 -0
  130. package/src/presets/lint.ts +116 -0
  131. package/src/presets/markdownlint.ts +105 -0
  132. package/src/presets/publint.ts +38 -0
  133. package/src/presets/security-deps.ts +142 -0
  134. package/src/presets/security-secrets.ts +93 -0
  135. package/src/presets/shared/error-warning-pass-policy.ts +39 -0
  136. package/src/presets/shared/exit-code-fail-rationale.ts +34 -0
  137. package/src/presets/shared/missing-dependency.ts +31 -0
  138. package/src/presets/shared/read-json-report.ts +46 -0
  139. package/src/presets/shared/terminal-status.ts +70 -0
  140. package/src/presets/shared/vitest-json-policy.ts +95 -0
  141. package/src/presets/stylelint.ts +101 -0
  142. package/src/presets/test.ts +19 -0
  143. package/src/presets/typecheck.ts +25 -0
  144. package/src/run-repo-contract.ts +80 -0
  145. package/src/types.ts +340 -0
@@ -0,0 +1,223 @@
1
+ import type { CheckDefinitionConfig } from "../types.js"
2
+ import { checkDependencyInstalled } from "./shared/missing-dependency.js"
3
+ import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
4
+
5
+ /**
6
+ * Knip does not expose the serialized JSON reporter result as a public
7
+ * TypeScript type, so the local evidence type below intentionally models
8
+ * that JSON contract. This targets knip's `json` reporter as of knip 6:
9
+ * `{ issues: KnipIssue[] }`, one entry per file, each carrying an array per
10
+ * issue category (`knip 5` additionally had a top-level `files: string[]` for
11
+ * unused files; knip 6 folds those into `issues[].files`). The policy reports
12
+ * the actual issue category and name instead of only returning a count.
13
+ */
14
+ interface KnipIssueEntry {
15
+ readonly name: string
16
+ readonly line?: number
17
+ readonly col?: number
18
+ readonly pos?: number
19
+ }
20
+
21
+ /** `duplicates`/`cycles` group several symbols together; every other category is a flat entry. */
22
+ type KnipCategory = readonly (KnipIssueEntry | readonly KnipIssueEntry[])[]
23
+
24
+ interface KnipIssue {
25
+ readonly file: string
26
+ readonly dependencies?: KnipCategory
27
+ readonly devDependencies?: KnipCategory
28
+ readonly optionalPeerDependencies?: KnipCategory
29
+ readonly unlisted?: KnipCategory
30
+ readonly unresolved?: KnipCategory
31
+ readonly exports?: KnipCategory
32
+ readonly types?: KnipCategory
33
+ readonly nsExports?: KnipCategory
34
+ readonly nsTypes?: KnipCategory
35
+ readonly namespaceMembers?: KnipCategory
36
+ readonly enumMembers?: KnipCategory
37
+ readonly binaries?: KnipCategory
38
+ readonly duplicates?: KnipCategory
39
+ readonly cycles?: KnipCategory
40
+ readonly files?: KnipCategory
41
+ }
42
+
43
+ interface KnipReport {
44
+ readonly issues: readonly KnipIssue[]
45
+ }
46
+
47
+ /** Options accepted by {@link deadCode}. */
48
+ interface DeadCodeOptions {
49
+ /**
50
+ * devDependency names to exclude from the "unused devDependency" issue
51
+ * category -- e.g. CLI tools knip has no way to see are used, because
52
+ * nothing `import`s them. Round-tripped through knip's own
53
+ * `--reporter-options` flag (see `readReporterOptions`) rather than kept in
54
+ * a closure, so the exempt list this check actually ran with is visible on
55
+ * the recorded command line and in persisted evidence, not just in this
56
+ * file's source. Defaults to an empty list -- repo-contract ships no
57
+ * built-in exemptions; add your own repository's here.
58
+ */
59
+ readonly exemptUnusedDevDependencies?: readonly string[]
60
+ }
61
+
62
+ /**
63
+ * Recovers the JSON payload knip's own `--reporter-options` flag was invoked
64
+ * with, if any. This is how `deadCode`'s exempt list gets from `run` back
65
+ * into the policy purely via `result.args` -- see {@link DeadCodeOptions}.
66
+ * @param args - the exact argv the check's command was spawned with (`CheckEvidence.args`).
67
+ * @returns the parsed `--reporter-options` payload, or `undefined` if the flag is absent or its value isn't valid JSON.
68
+ */
69
+ function readReporterOptions(args: readonly string[]): DeadCodeOptions | undefined {
70
+ const flagIndex = args.indexOf("--reporter-options")
71
+ const raw = flagIndex === -1 ? undefined : args[flagIndex + 1]
72
+
73
+ // Provably equivalent, not a coverage gap: `JSON.parse(undefined)` (what
74
+ // happens if this guard is removed or forced false) always throws
75
+ // ("undefined" is not valid JSON, confirmed empirically), which the catch
76
+ // block below already converts back to `undefined` -- the exact same
77
+ // observable result this early return produces. Kept for clarity (avoids
78
+ // relying on that throw-and-catch as the mechanism), not for behavior no
79
+ // test can otherwise reach.
80
+ // Stryker disable next-line ConditionalExpression,EqualityOperator,BlockStatement -- JSON.parse(undefined) always throws, and the catch block below already converts that back to undefined, the same result this early return produces
81
+ if (raw === undefined) {
82
+ return undefined
83
+ }
84
+
85
+ // Provably equivalent, not a coverage gap: the catch block below is the
86
+ // last statement in the function, so an empty block there falls off the
87
+ // end and implicitly returns `undefined` anyway -- identical to its
88
+ // explicit `return undefined`. Kept for clarity, not for behavior no
89
+ // test can otherwise reach.
90
+ // Stryker disable BlockStatement -- an empty catch block falls off the end of the function and implicitly returns undefined, identical to its explicit return
91
+ try {
92
+ return JSON.parse(raw) as DeadCodeOptions
93
+ } catch {
94
+ return undefined
95
+ }
96
+ // Stryker restore all
97
+ }
98
+
99
+ /**
100
+ * Renders one category entry as `{ name, location }`. `duplicates`/`cycles` entries are a
101
+ * group of symbols (knip pushes `symbols.map(...)`); every other category is a flat entry.
102
+ * @param entry - a single `KnipIssueEntry`, or a `duplicates`/`cycles` group of them.
103
+ * @returns the joined symbol name(s) and a `:line:col` suffix (empty for a group or a
104
+ * location-less entry).
105
+ */
106
+ function formatEntry(entry: KnipIssueEntry | readonly KnipIssueEntry[]): {
107
+ readonly name: string
108
+ readonly location: string
109
+ } {
110
+ if (Array.isArray(entry)) {
111
+ const group = entry as readonly KnipIssueEntry[]
112
+ return { name: group.map((member) => member.name).join(", "), location: "" }
113
+ }
114
+
115
+ const single = entry as KnipIssueEntry
116
+ const location =
117
+ typeof single.line === "number" ? `:${String(single.line)}:${String(single.col ?? 1)}` : ""
118
+ return { name: single.name, location }
119
+ }
120
+
121
+ /**
122
+ * Dead/unused-code detection via knip.
123
+ * @param options - configuration for this check; see {@link DeadCodeOptions}.
124
+ * @returns the configured check.
125
+ */
126
+ export function deadCode(options: DeadCodeOptions = {}): CheckDefinitionConfig {
127
+ const { exemptUnusedDevDependencies = [] } = options
128
+
129
+ return {
130
+ run: [
131
+ "knip",
132
+ "--reporter",
133
+ "json",
134
+ "--reporter-options",
135
+ JSON.stringify({ exemptUnusedDevDependencies } satisfies DeadCodeOptions),
136
+ ],
137
+ output: { format: "json" },
138
+ policy: ({ result }) => {
139
+ const missing = checkDependencyInstalled(result, "knip")
140
+ if (missing) return missing
141
+
142
+ const terminated = checkTerminatedAbnormally(result, "Knip")
143
+ if (terminated) return terminated
144
+
145
+ if (!result.output?.success) {
146
+ return { outcome: "fail", rationale: "Knip output could not be parsed as JSON." }
147
+ }
148
+
149
+ // `output.success` only means `JSON.parse` didn't throw -- `value` is `unknown` at
150
+ // runtime. Guard that it is a real object (not `null` / a primitive / an array) before
151
+ // trusting the `KnipReport` cast, so a policy TypeError never escapes into the run.
152
+ const value: unknown = result.output.value
153
+
154
+ if (typeof value !== "object" || value === null) {
155
+ return { outcome: "fail", rationale: "Knip produced invalid JSON report data." }
156
+ }
157
+
158
+ const report = value as KnipReport
159
+
160
+ if (!Array.isArray(report.issues)) {
161
+ return { outcome: "fail", rationale: "Knip produced invalid JSON report data." }
162
+ }
163
+
164
+ // Re-annotated rather than used directly: `Array.isArray` narrows its
165
+ // argument to `any[]` regardless of the checked value's declared type (a
166
+ // long-standing TypeScript limitation), so `report.issues` would
167
+ // otherwise silently lose its element type below.
168
+ const issues: readonly KnipIssue[] = report.issues
169
+
170
+ const exemptDevDependencies = new Set(
171
+ readReporterOptions(result.args)?.exemptUnusedDevDependencies ?? [],
172
+ )
173
+
174
+ const details: string[] = []
175
+
176
+ for (const issue of issues) {
177
+ const addEntries = (
178
+ label: string,
179
+ entries: KnipCategory | undefined,
180
+ ignored: ReadonlySet<string> = new Set(),
181
+ ): void => {
182
+ for (const entry of entries ?? []) {
183
+ const { name, location } = formatEntry(entry)
184
+
185
+ if (ignored.has(name)) {
186
+ continue
187
+ }
188
+
189
+ details.push(`${issue.file}${location} — ${label}: ${name}`)
190
+ }
191
+ }
192
+
193
+ addEntries("unused dependency", issue.dependencies)
194
+ addEntries("unused devDependency", issue.devDependencies, exemptDevDependencies)
195
+ addEntries("unused optional peer dependency", issue.optionalPeerDependencies)
196
+ addEntries("unlisted dependency", issue.unlisted)
197
+ addEntries("unresolved import", issue.unresolved)
198
+ addEntries("unused export", issue.exports)
199
+ addEntries("unused export (namespace)", issue.nsExports)
200
+ addEntries("unused type", issue.types)
201
+ addEntries("unused type (namespace)", issue.nsTypes)
202
+ addEntries("unused namespace member", issue.namespaceMembers)
203
+ addEntries("unused enum member", issue.enumMembers)
204
+ addEntries("unlisted binary", issue.binaries)
205
+ addEntries("duplicate export", issue.duplicates)
206
+ addEntries("circular dependency", issue.cycles)
207
+ addEntries("unused file", issue.files)
208
+ }
209
+
210
+ if (details.length === 0) {
211
+ return { outcome: "pass", rationale: "Knip reported 0 issues." }
212
+ }
213
+
214
+ return {
215
+ outcome: "fail",
216
+ rationale: [
217
+ `Knip reported ${String(details.length)} issue(s):`,
218
+ ...details.map((detail: string) => `- ${detail}`),
219
+ ].join("\n"),
220
+ }
221
+ },
222
+ }
223
+ }
@@ -0,0 +1,137 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import type { CheckDefinitionConfig } from "../types.js"
3
+ import { checkDependencyInstalled } from "./shared/missing-dependency.js"
4
+ import { readJsonReport } from "./shared/read-json-report.js"
5
+ import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
6
+
7
+ interface JscpdFileSpan {
8
+ readonly name: string
9
+ readonly start: number
10
+ readonly end: number
11
+ }
12
+
13
+ interface JscpdDuplicate {
14
+ readonly format: string
15
+ readonly lines: number
16
+ readonly tokens: number
17
+ readonly firstFile: JscpdFileSpan
18
+ readonly secondFile: JscpdFileSpan
19
+ }
20
+
21
+ interface JscpdStatisticsTotal {
22
+ readonly clones: number
23
+ readonly duplicatedLines: number
24
+ readonly lines: number
25
+ readonly percentage: number
26
+ readonly sources: number
27
+ }
28
+
29
+ interface JscpdReport {
30
+ readonly duplicates?: readonly JscpdDuplicate[]
31
+ readonly statistics?: {
32
+ readonly total?: JscpdStatisticsTotal
33
+ }
34
+ }
35
+
36
+ /** Options accepted by {@link duplication}. */
37
+ interface DuplicationOptions {
38
+ /** Directory to scan for duplicated code, passed straight through to jscpd as its positional target. Defaults to `"."` -- narrow it (e.g. `"src"`) to scope the scan to your own source tree. */
39
+ readonly path?: string
40
+ }
41
+
42
+ // Kept in lockstep with the `--output reports/jscpd` argument below. Both are
43
+ // relative and resolve against the *host* process's cwd, not the check's own
44
+ // `cwd` option: jscpd writes here and this policy's `readFile` reads from here,
45
+ // so a check configured with a different `cwd` would look in the wrong place.
46
+ // This preset does not expose an output-path option -- a consumer needing one
47
+ // runs jscpd via a custom `run`/`policy` instead.
48
+ const REPORT_PATH = "reports/jscpd/jscpd-report.json"
49
+
50
+ // jscpd's JSON reporter writes its report to disk -- it never prints JSON to
51
+ // stdout, which instead carries decorative progress/summary text -- so the
52
+ // report is read from disk directly. jscpd's own `--threshold`/`--exit-code`
53
+ // flags are deliberately not used: this preset reads the raw clone count
54
+ // from evidence and owns the pass/fail decision itself, exactly as the
55
+ // deadCode/securityDeps presets already do for their own tools.
56
+ /**
57
+ * Duplicated-code detection via jscpd.
58
+ * @param options - configuration for this check; see {@link DuplicationOptions}.
59
+ * @returns the configured check.
60
+ */
61
+ export function duplication(options: DuplicationOptions = {}): CheckDefinitionConfig {
62
+ const { path = "." } = options
63
+
64
+ return {
65
+ run: ["jscpd", path, "--reporters", "json", "--output", "reports/jscpd", "--silent"],
66
+ policy: async ({ result }) => {
67
+ const missing = checkDependencyInstalled(result, "jscpd")
68
+ if (missing) return missing
69
+
70
+ const terminated = checkTerminatedAbnormally(result, "jscpd")
71
+ if (terminated) return terminated
72
+
73
+ const parsed = await readJsonReport<JscpdReport>(
74
+ // Provably equivalent, not a coverage gap: `JSON.parse` coerces
75
+ // whatever `readFile` returns via that value's own `toString()`
76
+ // (confirmed empirically for a Buffer, which is what a mutated/
77
+ // unrecognized encoding argument produces here) -- and `Buffer`'s
78
+ // default `toString()` encoding is itself utf8, so parsing succeeds
79
+ // identically either way. No test can observe a difference through
80
+ // this policy's output.
81
+ // Stryker disable next-line StringLiteral -- JSON.parse coerces via toString(), which defaults to utf8 for a Buffer, so parsing succeeds identically either way
82
+ () => readFile(REPORT_PATH, "utf8"),
83
+ "jscpd did not produce its expected JSON report.",
84
+ "jscpd produced invalid JSON evidence.",
85
+ )
86
+ if (!parsed.ok) return parsed.result
87
+ // `readJsonReport<T>` narrows only at the type level -- the value is `unknown` at
88
+ // runtime. Guard it is a real object before trusting the cast, so `.statistics` /
89
+ // `.duplicates` never throw a TypeError out of the policy (matching markdownlint.ts).
90
+ const value: unknown = parsed.value
91
+
92
+ if (typeof value !== "object" || value === null) {
93
+ return { outcome: "fail", rationale: "jscpd produced invalid JSON report data." }
94
+ }
95
+
96
+ const report = value as JscpdReport
97
+ const total = report.statistics?.total
98
+
99
+ if (
100
+ !Array.isArray(report.duplicates) ||
101
+ !total ||
102
+ typeof total.percentage !== "number" ||
103
+ !Number.isFinite(total.percentage)
104
+ ) {
105
+ // `total.percentage` feeds `.toFixed(2)` below -- a missing or non-finite
106
+ // value would throw or render "NaN" instead of failing cleanly.
107
+ return { outcome: "fail", rationale: "jscpd produced invalid JSON report data." }
108
+ }
109
+
110
+ // Re-annotated rather than used directly: `Array.isArray` narrows its
111
+ // argument to `any[]` regardless of the checked value's declared type (a
112
+ // long-standing TypeScript limitation), so `report.duplicates` would
113
+ // otherwise silently lose its `JscpdDuplicate` element type below.
114
+ const duplicates: readonly JscpdDuplicate[] = report.duplicates
115
+
116
+ if (duplicates.length === 0) {
117
+ return {
118
+ outcome: "pass",
119
+ rationale: `jscpd found 0 duplicated block(s) across ${String(total.sources)} file(s) (${String(total.lines)} lines).`,
120
+ }
121
+ }
122
+
123
+ const details = duplicates.map(
124
+ (duplicate) =>
125
+ `${duplicate.firstFile.name}:${String(duplicate.firstFile.start)} duplicates ${duplicate.secondFile.name}:${String(duplicate.secondFile.start)} -- ${String(duplicate.lines)} lines / ${String(duplicate.tokens)} tokens`,
126
+ )
127
+
128
+ return {
129
+ outcome: "fail",
130
+ rationale: [
131
+ `jscpd found ${String(duplicates.length)} duplicated block(s) (${total.percentage.toFixed(2)}% of ${String(total.lines)} lines across ${String(total.sources)} file(s)):`,
132
+ ...details.map((detail) => `- ${detail}`),
133
+ ].join("\n"),
134
+ }
135
+ },
136
+ }
137
+ }
@@ -0,0 +1,144 @@
1
+ import type { CheckDefinitionConfig } from "../types.js"
2
+ import { checkDependencyInstalled } from "./shared/missing-dependency.js"
3
+ import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
4
+
5
+ /**
6
+ * Playwright's own `--reporter=json` contract -- not published as a
7
+ * TypeScript type by the tool. Suites nest (a project, then a file, then
8
+ * `describe` blocks), so failing specs are collected recursively.
9
+ */
10
+ interface PlaywrightTestResult {
11
+ readonly status?: string
12
+ readonly error?: { readonly message?: string }
13
+ }
14
+
15
+ interface PlaywrightTest {
16
+ readonly results?: readonly PlaywrightTestResult[]
17
+ }
18
+
19
+ interface PlaywrightSpec {
20
+ readonly title: string
21
+ readonly file?: string
22
+ readonly line?: number
23
+ readonly ok: boolean
24
+ readonly tests?: readonly PlaywrightTest[]
25
+ }
26
+
27
+ interface PlaywrightSuite {
28
+ readonly title: string
29
+ readonly specs?: readonly PlaywrightSpec[]
30
+ readonly suites?: readonly PlaywrightSuite[]
31
+ }
32
+
33
+ interface PlaywrightStats {
34
+ readonly expected: number
35
+ readonly unexpected: number
36
+ readonly flaky: number
37
+ readonly skipped: number
38
+ }
39
+
40
+ interface PlaywrightReport {
41
+ readonly suites?: readonly PlaywrightSuite[]
42
+ readonly stats?: PlaywrightStats
43
+ }
44
+
45
+ /**
46
+ * Recursively collects one human-readable line per failing spec across a (possibly nested) suite tree.
47
+ * @param suites - the suite tree to walk, as `PlaywrightReport.suites` or a suite's own nested `suites`.
48
+ * @returns one rendered `file:line title -- message` line per failing spec, depth-first.
49
+ */
50
+ function collectFailingSpecs(suites: readonly PlaywrightSuite[]): string[] {
51
+ const details: string[] = []
52
+
53
+ for (const suite of suites) {
54
+ for (const spec of suite.specs ?? []) {
55
+ if (spec.ok) continue
56
+
57
+ const location = spec.line !== undefined ? `:${String(spec.line)}` : ""
58
+ // Provably equivalent, not a coverage gap: whatever non-array/non-test
59
+ // garbage a mutated `?? []` fallback substitutes here, every field
60
+ // access on it below (`.results`, `.error`, `.message`) resolves
61
+ // safely to `undefined` via its own optional chaining -- confirmed by
62
+ // hand-tracing both fallback sites -- converging on the exact same
63
+ // `message: undefined` this line already produces for a genuinely
64
+ // empty `spec.tests`/`t.results`. No test can observe a difference.
65
+ // Stryker disable next-line ArrayDeclaration -- any fallback garbage here resolves safely to undefined via optional chaining below, same as a genuinely empty array
66
+ const lastResult = (spec.tests ?? []).flatMap((t) => t.results ?? []).at(-1)
67
+ const message = lastResult?.error?.message?.trim()
68
+
69
+ details.push(
70
+ `${spec.file ?? suite.title}${location} ${spec.title}${message ? ` — ${message}` : ""}`,
71
+ )
72
+ }
73
+
74
+ if (suite.suites) details.push(...collectFailingSpecs(suite.suites))
75
+ }
76
+
77
+ return details
78
+ }
79
+
80
+ /** End-to-end test execution via Playwright, reading its JSON reporter output. */
81
+ export const e2e: CheckDefinitionConfig = {
82
+ run: ["playwright", "test", "--reporter=json"],
83
+ output: { format: "json" },
84
+ policy: ({ result }) => {
85
+ const missing = checkDependencyInstalled(result, "@playwright/test")
86
+ if (missing) return missing
87
+
88
+ const terminated = checkTerminatedAbnormally(result, "Playwright")
89
+ if (terminated) return terminated
90
+
91
+ if (!result.output?.success) {
92
+ return { outcome: "fail", rationale: "Playwright output could not be parsed as JSON." }
93
+ }
94
+
95
+ // `output.success` only means `JSON.parse` didn't throw -- `value` is `unknown` at
96
+ // runtime. Guard it is a real object before trusting the cast, matching how
97
+ // dead-code.ts / duplication.ts guard theirs, so `.stats` never throws out of the policy.
98
+ const value: unknown = result.output.value
99
+
100
+ if (typeof value !== "object" || value === null) {
101
+ return { outcome: "fail", rationale: "Playwright produced invalid JSON report data." }
102
+ }
103
+
104
+ const report = value as PlaywrightReport
105
+ const stats = report.stats
106
+
107
+ if (!stats || typeof stats.unexpected !== "number" || typeof stats.flaky !== "number") {
108
+ // The outcome checks below compare `unexpected`/`flaky` numerically -- a
109
+ // missing or non-numeric counter must fail cleanly, not slip through every
110
+ // comparison as `false`.
111
+ return { outcome: "fail", rationale: "Playwright produced invalid JSON report data." }
112
+ }
113
+
114
+ if (stats.unexpected === 0 && stats.flaky === 0) {
115
+ return {
116
+ outcome: "pass",
117
+ rationale: `Playwright completed ${String(stats.expected)} test(s) with 0 unexpected failures.`,
118
+ }
119
+ }
120
+
121
+ if (stats.unexpected > 0) {
122
+ // Provably equivalent, not a coverage gap: collectFailingSpecs itself
123
+ // only ever reads `.specs`/`.suites` off each element via safe
124
+ // optional chaining (see its own comment), so substituting any
125
+ // non-suite garbage for a genuinely absent `report.suites` still
126
+ // yields zero collected detail lines either way.
127
+ // Stryker disable next-line ArrayDeclaration -- collectFailingSpecs only reads .specs/.suites via safe optional chaining, so fallback garbage yields zero details either way
128
+ const details = collectFailingSpecs(report.suites ?? [])
129
+
130
+ return {
131
+ outcome: "fail",
132
+ rationale: [
133
+ `Playwright reported ${String(stats.unexpected)} unexpected failure(s):`,
134
+ ...details.map((detail) => `- ${detail}`),
135
+ ].join("\n"),
136
+ }
137
+ }
138
+
139
+ return {
140
+ outcome: "warn",
141
+ rationale: `Playwright completed with ${String(stats.flaky)} flaky test(s) that eventually passed on retry.`,
142
+ }
143
+ },
144
+ }
@@ -0,0 +1,25 @@
1
+ import type { CheckDefinitionConfig } from "../types.js"
2
+ import { checkDependencyInstalled } from "./shared/missing-dependency.js"
3
+ import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
4
+ import { exitCodeFailRationale } from "./shared/exit-code-fail-rationale.js"
5
+
6
+ /** Code formatting via Prettier, applied in place. */
7
+ export const format: CheckDefinitionConfig = {
8
+ run: ["prettier", "--write", "."],
9
+ policy: ({ result }) => {
10
+ const missing = checkDependencyInstalled(result, "prettier")
11
+ if (missing) return missing
12
+
13
+ const terminated = checkTerminatedAbnormally(result, "Prettier")
14
+ if (terminated) return terminated
15
+
16
+ if (result.exitCode === 0) {
17
+ return { outcome: "pass", rationale: "Prettier reported no formatting failures." }
18
+ }
19
+
20
+ return {
21
+ outcome: "fail",
22
+ rationale: exitCodeFailRationale(result, "Prettier reported formatting failures"),
23
+ }
24
+ },
25
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Curated, growing catalog of preset checks for tools common across
3
+ * TypeScript/JavaScript repositories -- each preset encodes how to execute
4
+ * and interpret a common tool, never a repository's definition of quality
5
+ * (see specs/decisions/0004-public-surface-stays-narrow-no-cli-experimental-presets.md). Import a preset,
6
+ * spread it into your own `checks` record, and override whatever you need
7
+ * -- most often `policy`; a factory preset's options are the preferred way
8
+ * to change what it executes, a direct `run` override is an escape hatch.
9
+ * Never re-exported from the package root (`src/index.ts`) -- presets are
10
+ * an opt-in extra, not part of the core execution/evidence/policy surface
11
+ * those exports describe, and this barrel never re-exports anything from
12
+ * there either; the two stay independent.
13
+ * @packageDocumentation
14
+ */
15
+ export { arethetypeswrong } from "./arethetypeswrong.js"
16
+ export { brokenLinks } from "./broken-links.js"
17
+ export { commitlint } from "./commitlint.js"
18
+ export { deadCode } from "./dead-code.js"
19
+ export { duplication } from "./duplication.js"
20
+ export { e2e } from "./e2e.js"
21
+ export { format } from "./format.js"
22
+ export { license } from "./license.js"
23
+ export { lint } from "./lint.js"
24
+ export { markdownlint } from "./markdownlint.js"
25
+ export { publint } from "./publint.js"
26
+ export { securityDeps } from "./security-deps.js"
27
+ export { securitySecrets } from "./security-secrets.js"
28
+ export { stylelint } from "./stylelint.js"
29
+ export { test } from "./test.js"
30
+ export { typecheck } from "./typecheck.js"
@@ -0,0 +1,90 @@
1
+ import type { CheckDefinitionConfig } from "../types.js"
2
+ import { exitCodeFailRationale } from "./shared/exit-code-fail-rationale.js"
3
+ import { checkDependencyInstalled } from "./shared/missing-dependency.js"
4
+ import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
5
+
6
+ interface LicenseeEntry {
7
+ readonly name: string
8
+ readonly version: string
9
+ readonly license?: string
10
+ }
11
+
12
+ // `--production` scopes evaluation to the dependency graph actually shipped
13
+ // to consumers -- devDependencies never end up in a consumer's install, so
14
+ // their licenses carry no obligation onto this package's own license. Same
15
+ // runtime-only rationale the securityDeps preset already applies to `npm
16
+ // audit --omit=dev`. `--osi` treats Open Source Initiative approval as the
17
+ // approval criterion rather than a repository-maintained SPDX allowlist --
18
+ // the same "policy is repository policy, not a repo-contract opinion"
19
+ // stance securityDeps documents, just resolved to the broadest
20
+ // widely-recognized standard instead of a bespoke list. `--errors-only`
21
+ // keeps the ndjson evidence limited to the packages actually driving the
22
+ // verdict, matching the deadCode/duplication presets' report-issues-only
23
+ // shape.
24
+ /** Dependency license compliance via licensee. */
25
+ export const license: CheckDefinitionConfig = {
26
+ run: ["licensee", "--production", "--osi", "--errors-only", "--ndjson"],
27
+ policy: ({ result }) => {
28
+ const missing = checkDependencyInstalled(result, "licensee")
29
+ if (missing) return missing
30
+
31
+ const terminated = checkTerminatedAbnormally(result, "licensee")
32
+ if (terminated) return terminated
33
+
34
+ const trimmed = result.stdout.trim()
35
+
36
+ // `--errors-only` makes licensee exit non-zero *because* it found an
37
+ // offending dependency -- so a non-zero exit with ndjson on stdout is the
38
+ // expected failure path, handled below. But a non-zero exit with *empty*
39
+ // stdout means licensee never evaluated anything (node_modules not
40
+ // installed, an unreadable dependency tree, an internal `die()`): the
41
+ // error is on stderr and there is nothing to parse. Treating that as
42
+ // "found 0 offending dependencies" would let the compliance gate pass on
43
+ // a run that never actually ran. Only an empty stdout from a process that
44
+ // exited cleanly on its own is a genuine "nothing offending" pass.
45
+ if (trimmed.length === 0) {
46
+ if (result.status === "completed" && result.exitCode === 0) {
47
+ return {
48
+ outcome: "pass",
49
+ rationale: "licensee found 0 production dependencies with a non-OSI-approved license.",
50
+ }
51
+ }
52
+
53
+ return {
54
+ outcome: "fail",
55
+ rationale: exitCodeFailRationale(
56
+ result,
57
+ "licensee did not evaluate any dependency licenses (it produced no output)",
58
+ ),
59
+ }
60
+ }
61
+
62
+ let entries: readonly LicenseeEntry[]
63
+
64
+ try {
65
+ entries = trimmed
66
+ .split("\n")
67
+ // licensee emits one JSON object per line; a blank line between
68
+ // records (some versions/locales do this, as can a leading
69
+ // informational line) is not invalid evidence -- skip it rather than
70
+ // feeding "" to JSON.parse and discarding the real violation list.
71
+ .map((line) => line.trim())
72
+ .filter((line) => line.length > 0)
73
+ .map((line) => JSON.parse(line) as LicenseeEntry)
74
+ } catch {
75
+ return { outcome: "fail", rationale: "licensee produced invalid JSON evidence." }
76
+ }
77
+
78
+ const details = entries
79
+ .map((entry) => `${entry.name}@${entry.version}: ${entry.license ?? "unknown license"}`)
80
+ .sort()
81
+
82
+ return {
83
+ outcome: "fail",
84
+ rationale: [
85
+ `licensee found ${String(details.length)} production dependency(ies) without an OSI-approved license:`,
86
+ ...details.map((detail) => `- ${detail}`),
87
+ ].join("\n"),
88
+ }
89
+ },
90
+ }