@konvert7/klint 0.29.0 → 0.31.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 CHANGED
@@ -209,6 +209,39 @@ arch:
209
209
 
210
210
  `klint.config.json` is still supported for backwards compatibility.
211
211
 
212
+ ### Editor support
213
+
214
+ The package ships `klint.schema.json` and `klint.schema.yaml`, so the recommended
215
+ setup points at the installed copy:
216
+
217
+ ```yaml
218
+ $schema: ./node_modules/@konvert7/klint/klint.schema.json
219
+ ```
220
+
221
+ That file is installed alongside klint itself, so it cannot drift — upgrading klint
222
+ upgrades the schema with it. Both the inline `$schema` key above and the
223
+ `# yaml-language-server: $schema=…` modeline work; when both are present the modeline
224
+ wins. `klint.config.json` files are handled by the editor's built-in JSON support.
225
+
226
+ ### Pinning the schema version
227
+
228
+ To pin a specific published schema instead, set `$schema` to a version-tagged URL:
229
+
230
+ ```yaml
231
+ $schema: https://raw.githubusercontent.com/konvert7/klint/refs/tags/v0.29.0/klint.schema.json
232
+ ```
233
+
234
+ When the tagged version differs from the installed klint, the run reports a
235
+ `klint/schema-version` warning naming both versions and the URL to move to. It is a
236
+ warning, not an error — the exit code is unaffected. Local paths carry no version and
237
+ are never checked, which is why the installed copy above never warns.
238
+
239
+ This check runs when klint runs. Editors validate the config's *shape* against the
240
+ schema, but have no way to compare it against the installed klint, so the warning
241
+ appears in klint's output rather than as an editor diagnostic.
242
+
243
+ `klint --version` prints the installed version.
244
+
212
245
  ## Architecture as Code
213
246
 
214
247
  The `arch:` section turns project boundaries into executable policy.
package/cli/output.ts CHANGED
@@ -85,6 +85,25 @@ export function mergeJsonOutputs(...outputs: JsonPayload[]): JsonPayload {
85
85
  };
86
86
  }
87
87
 
88
+ export function withAdvisories(
89
+ stdout: string,
90
+ advisories: Array<Omit<Violation, "fix">>
91
+ ): string {
92
+ if (advisories.length === 0) return stdout;
93
+ const payload = parseJsonPayload(stdout);
94
+ if (!isJsonPayload(payload)) return stdout;
95
+
96
+ const violations = [
97
+ ...payload.violations,
98
+ ...advisories.map((advisory) => ({ ...advisory, fix: null })),
99
+ ];
100
+ const errors = violations.filter((v) => v.severity === "error").length;
101
+ return JSON.stringify({
102
+ violations,
103
+ summary: { errors, warnings: violations.length - errors },
104
+ });
105
+ }
106
+
88
107
  export function isJsonPayload(value: unknown): value is JsonPayload {
89
108
  if (!value || typeof value !== "object") return false;
90
109
  const candidate = value as Partial<JsonPayload>;
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { runKlint } from "../core/runner";
6
- import type { KlintRule, RuleConfigValue } from "../core/types";
6
+ import type { KlintRule, RuleConfigValue, Violation } from "../core/types";
7
7
  import {
8
8
  emptyJsonPayload,
9
9
  isJsonPayload,
@@ -13,6 +13,7 @@ import {
13
13
  parseJsonPayload,
14
14
  toJsonCliResult,
15
15
  toJsonPayload,
16
+ withAdvisories,
16
17
  writeTextOutput,
17
18
  } from "./output";
18
19
  import { PACKAGE_ROOT } from "./paths";
@@ -25,6 +26,7 @@ import {
25
26
  } from "./rust-config";
26
27
 
27
28
  export function runRustEngine({
29
+ advisories,
28
30
  fix,
29
31
  json,
30
32
  raw,
@@ -32,6 +34,7 @@ export function runRustEngine({
32
34
  rulesFile,
33
35
  startedAt,
34
36
  }: {
37
+ advisories: Array<Omit<Violation, "fix">>;
35
38
  fix: boolean;
36
39
  json: boolean;
37
40
  raw: {
@@ -73,7 +76,7 @@ export function runRustEngine({
73
76
 
74
77
  if (result.stderr) process.stderr.write(result.stderr);
75
78
  if (json) {
76
- if (result.stdout) process.stdout.write(result.stdout);
79
+ if (result.stdout) process.stdout.write(withAdvisories(result.stdout, advisories));
77
80
  process.exit(result.status ?? 1);
78
81
  }
79
82
 
@@ -82,10 +85,11 @@ export function runRustEngine({
82
85
  process.stderr.write("klint: --engine rust failed to parse Rust JSON output\n");
83
86
  process.exit(1);
84
87
  }
85
- writeTextOutput(payload.violations, startedAt);
88
+ writeTextOutput([...payload.violations, ...advisories], startedAt);
86
89
  }
87
90
 
88
91
  export function runCompareEngine({
92
+ advisories,
89
93
  fix,
90
94
  json,
91
95
  raw,
@@ -93,6 +97,7 @@ export function runCompareEngine({
93
97
  rulesFile,
94
98
  tsViolations,
95
99
  }: {
100
+ advisories: Array<Omit<Violation, "fix">>;
96
101
  fix: boolean;
97
102
  json: boolean;
98
103
  raw: {
@@ -154,7 +159,7 @@ export function runCompareEngine({
154
159
  process.exit(1);
155
160
  }
156
161
 
157
- process.stdout.write(tsResult.stdout);
162
+ process.stdout.write(withAdvisories(tsResult.stdout, advisories));
158
163
  process.exit(tsResult.status);
159
164
  }
160
165
 
@@ -188,6 +193,7 @@ function writeRustEngineConfig({
188
193
  }
189
194
 
190
195
  export function runAutoEngine({
196
+ advisories,
191
197
  fix,
192
198
  json,
193
199
  raw,
@@ -197,6 +203,7 @@ export function runAutoEngine({
197
203
  customRules,
198
204
  customRulesMap,
199
205
  }: {
206
+ advisories: Array<Omit<Violation, "fix">>;
200
207
  fix: boolean;
201
208
  json: boolean;
202
209
  raw: {
@@ -241,10 +248,10 @@ export function runAutoEngine({
241
248
  const tsOutput = toJsonPayload(tsViolations);
242
249
  const merged = mergeJsonOutputs(rustOutput, tsOutput);
243
250
  if (json) {
244
- process.stdout.write(JSON.stringify(merged));
251
+ process.stdout.write(withAdvisories(JSON.stringify(merged), advisories));
245
252
  process.exit(merged.summary.errors > 0 ? 2 : 0);
246
253
  }
247
- writeTextOutput(merged.violations, startedAt);
254
+ writeTextOutput([...merged.violations, ...advisories], startedAt);
248
255
  }
249
256
 
250
257
  function runAutoRustSubset({
package/cli.ts CHANGED
@@ -8,14 +8,26 @@ import { installSkill } from "./cli/install-skill";
8
8
  import {
9
9
  formatDuration,
10
10
  toJsonCliResult,
11
+ withAdvisories,
11
12
  writeDebugEvent,
12
13
  writeTextOutput,
13
14
  } from "./cli/output";
15
+ import { PACKAGE_ROOT } from "./cli/paths";
14
16
  import { runAutoEngine, runCompareEngine, runRustEngine } from "./cli/rust-engine";
15
17
  import { applyFixes } from "./core/fixer";
16
18
  import { runKlint } from "./core/runner";
19
+ import { schemaVersionAdvisory } from "./core/schema-version";
17
20
  import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core/types";
18
21
 
22
+ async function installedVersion(): Promise<string> {
23
+ const manifest = await readFile(resolve(PACKAGE_ROOT, "package.json"), "utf-8");
24
+ try {
25
+ return (JSON.parse(manifest) as { version?: string }).version ?? "0.0.0";
26
+ } catch {
27
+ return "0.0.0";
28
+ }
29
+ }
30
+
19
31
  interface CliOptions {
20
32
  configDir?: string;
21
33
  rulesFile?: string;
@@ -52,6 +64,9 @@ export async function main(opts: CliOptions = {}): Promise<void> {
52
64
  else if (args[i] === "--help" || args[i] === "-h") {
53
65
  printHelp();
54
66
  process.exit(0);
67
+ } else if (args[i] === "--version" || args[i] === "-V") {
68
+ process.stdout.write(`klint ${await installedVersion()}\n`);
69
+ process.exit(0);
55
70
  }
56
71
  }
57
72
 
@@ -70,6 +85,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
70
85
  }
71
86
 
72
87
  interface RawConfig {
88
+ $schema?: string;
73
89
  root?: string;
74
90
  include?: string[];
75
91
  plugins?: string[];
@@ -77,17 +93,25 @@ export async function main(opts: CliOptions = {}): Promise<void> {
77
93
  arch?: unknown;
78
94
  }
79
95
  let raw: RawConfig;
96
+ let configText = "";
80
97
  try {
81
- const text = await readFile(configPath, "utf-8");
82
- raw = (usingYaml ? parseYaml(text) : JSON.parse(text)) as RawConfig;
98
+ configText = await readFile(configPath, "utf-8");
99
+ raw = (usingYaml ? parseYaml(configText) : JSON.parse(configText)) as RawConfig;
83
100
  } catch {
84
101
  process.stderr.write(`klint: failed to parse ${configPath}\n`);
85
102
  process.exit(1);
86
103
  }
87
104
  const root = resolve(configDir, raw.root ?? ".");
88
105
 
106
+ const advisories = schemaVersionAdvisory({
107
+ schema: raw.$schema,
108
+ installed: await installedVersion(),
109
+ configFile: usingYaml ? "klint.yaml" : "klint.config.json",
110
+ configText,
111
+ });
112
+
89
113
  if (engine === "rust") {
90
- runRustEngine({ fix, json, raw, root, rulesFile, startedAt });
114
+ runRustEngine({ advisories, fix, json, raw, root, rulesFile, startedAt });
91
115
  return;
92
116
  }
93
117
  if (
@@ -119,6 +143,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
119
143
 
120
144
  if (engine === "auto") {
121
145
  runAutoEngine({
146
+ advisories,
122
147
  fix,
123
148
  json,
124
149
  raw,
@@ -145,6 +170,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
145
170
 
146
171
  if (engine === "compare") {
147
172
  runCompareEngine({
173
+ advisories,
148
174
  fix,
149
175
  json,
150
176
  raw,
@@ -157,7 +183,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
157
183
 
158
184
  if (json) {
159
185
  const tsResult = toJsonCliResult(violations);
160
- process.stdout.write(tsResult.stdout);
186
+ process.stdout.write(withAdvisories(tsResult.stdout, advisories));
161
187
  process.exit(tsResult.status);
162
188
  }
163
189
 
@@ -189,7 +215,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
189
215
  process.stdout.write(`${msg} Finished in ${formatDuration(startedAt)}.\n`);
190
216
  process.exit(0);
191
217
  }
192
- writeTextOutput(violations, startedAt);
218
+ writeTextOutput([...violations, ...advisories], startedAt);
193
219
  }
194
220
 
195
221
  if (import.meta.main) await main();
@@ -15,6 +15,8 @@ function collectComments(file: string, content: string): CommentSpan[] {
15
15
  const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, variant, content);
16
16
  const starts = lineStarts(content);
17
17
  const spans: CommentSpan[] = [];
18
+ const substitutionDepths: number[] = [];
19
+ let braceDepth = 0;
18
20
  let token = scanner.scan();
19
21
  while (token !== ts.SyntaxKind.EndOfFileToken) {
20
22
  if (
@@ -28,6 +30,18 @@ function collectComments(file: string, content: string): CommentSpan[] {
28
30
  endLine: lineAt(starts, Math.max(start, end - 1)),
29
31
  isDoc: isDocComment(content.slice(start, end)),
30
32
  });
33
+ } else if (token === ts.SyntaxKind.TemplateHead) {
34
+ substitutionDepths.push(braceDepth);
35
+ } else if (token === ts.SyntaxKind.OpenBraceToken) {
36
+ braceDepth += 1;
37
+ } else if (token === ts.SyntaxKind.CloseBraceToken) {
38
+ if (substitutionDepths.at(-1) === braceDepth) {
39
+ if (scanner.reScanTemplateToken(false) === ts.SyntaxKind.TemplateTail) {
40
+ substitutionDepths.pop();
41
+ }
42
+ } else {
43
+ braceDepth -= 1;
44
+ }
31
45
  }
32
46
  token = scanner.scan();
33
47
  }
@@ -244,7 +244,7 @@ const CountDocCommentsSchema = z
244
244
  .boolean()
245
245
  .optional()
246
246
  .describe(
247
- "Count doc-comments toward the limit. Doc-comments are `/** … */` JSDoc blocks (TS/JS) and docstrings (Python); ordinary `//`, `/* */`, and `#` comments are always counted. Defaults to false, so documentation is exempt and only explanatory/inline comments are capped."
247
+ "Count doc-comments toward the limit. Doc-comments are `/** … */` JSDoc blocks (TS/JS), `///` (Swift), and `///` or `//!` (Rust); ordinary `//`, `/* */`, and `#` comments are always counted. Defaults to false, so documentation is exempt and only explanatory/inline comments are capped. Has no effect on Python: docstrings are string expressions rather than comments, so they never count toward either limit."
248
248
  );
249
249
 
250
250
  const CommentIgnoreSchema = StringOrStringArray.optional().describe(
@@ -377,7 +377,7 @@ export const KlintConfigSchema = z
377
377
  .string()
378
378
  .optional()
379
379
  .describe(
380
- "JSON Schema reference. Use ./klint.schema.json for local validation or https://klint.dev/schema.json for the published schema."
380
+ "JSON Schema reference. Use ./klint.schema.json for local validation, or a version-tagged URL such as https://raw.githubusercontent.com/konvert7/klint/refs/tags/v0.29.0/klint.schema.json to pin the schema. A tagged URL naming a version other than the installed one raises a klint/schema-version warning."
381
381
  ),
382
382
  root: z
383
383
  .string()
@@ -0,0 +1,46 @@
1
+ import type { Violation } from "./types";
2
+
3
+ const SCHEMA_VERSION_RULE = "klint/schema-version";
4
+
5
+ const TAGGED_SCHEMA_URL = /\/refs\/tags\/v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)\//;
6
+ const UNRELEASED = "0.0.0";
7
+
8
+ export function schemaUrlForVersion(version: string): string {
9
+ return `https://raw.githubusercontent.com/konvert7/klint/refs/tags/v${version}/klint.schema.json`;
10
+ }
11
+
12
+ export function declaredSchemaVersion(schema: string | undefined): string | undefined {
13
+ return schema?.match(TAGGED_SCHEMA_URL)?.[1];
14
+ }
15
+
16
+ export function schemaVersionAdvisory({
17
+ schema,
18
+ installed,
19
+ configFile,
20
+ configText,
21
+ }: {
22
+ schema: string | undefined;
23
+ installed: string;
24
+ configFile: string;
25
+ configText: string;
26
+ }): Array<Omit<Violation, "fix">> {
27
+ const declared = declaredSchemaVersion(schema);
28
+ if (declared === undefined || declared === installed || installed === UNRELEASED) {
29
+ return [];
30
+ }
31
+
32
+ return [
33
+ {
34
+ file: configFile,
35
+ line: schemaDeclarationLine(configText),
36
+ rule: SCHEMA_VERSION_RULE,
37
+ severity: "warn",
38
+ message: `config declares schema v${declared} but klint ${installed} is installed — update $schema to ${schemaUrlForVersion(installed)}`,
39
+ },
40
+ ];
41
+ }
42
+
43
+ function schemaDeclarationLine(configText: string): number {
44
+ const index = configText.split("\n").findIndex((line) => line.includes("$schema"));
45
+ return index === -1 ? 1 : index + 1;
46
+ }
package/core/types.ts CHANGED
@@ -108,7 +108,7 @@ interface ArchMaxLinesRule {
108
108
  interface ArchCommentRuleBase {
109
109
  limit: number;
110
110
  in: string | string[];
111
- /** Count doc-comments (`/** *​/` JSDoc, Python docstrings) toward the limit. Defaults to false. */
111
+ /** Count doc-comments (`/** *​/` JSDoc, Swift `///`, Rust `///` and `//!`) toward the limit. Defaults to false. No effect on Python, whose docstrings are never counted. */
112
112
  countDocComments?: boolean;
113
113
  /** Comment lines matching these patterns count toward neither limit. Literal substring, or `re:` for a regex. */
114
114
  ignore?: string | string[];