@drkmttr/adhere 0.3.0 → 0.4.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.
@@ -1,68 +0,0 @@
1
- /**
2
- * The npm packages that carry the compiled executable, one per platform. Each
3
- * is an optional dependency of `@drkmttr/adhere` limited by `os` and `cpu`, so
4
- * an install fetches only its own machine's, and `bin/adhere.js` runs it.
5
- *
6
- * bun scripts/npm-packages.ts build compile every platform into dist/npm/
7
- * bun scripts/npm-packages.ts pin list them in package.json at its version
8
- *
9
- * Only the publish job runs `pin`. The committed package.json leaves them out
10
- * because a version is not on npm until that job has published it.
11
- */
12
- import packageJson from "../package.json";
13
- import { $ } from "bun";
14
- import { rm } from "node:fs/promises";
15
-
16
- process.chdir(new URL("../", import.meta.url).pathname);
17
-
18
- /** `os` and `cpu` are Node's `process.platform` and `process.arch`, which the launcher names the package from. */
19
- const platforms = [
20
- { os: "darwin", cpu: "arm64", target: "bun-darwin-arm64" },
21
- { os: "darwin", cpu: "x64", target: "bun-darwin-x64" },
22
- { os: "linux", cpu: "arm64", target: "bun-linux-arm64" },
23
- { os: "linux", cpu: "x64", target: "bun-linux-x64" },
24
- { os: "win32", cpu: "x64", target: "bun-windows-x64" },
25
- ] as const;
26
-
27
- type Platform = (typeof platforms)[number];
28
-
29
- const packageName = ({ os, cpu }: Platform) => `${packageJson.name}-${os}-${cpu}`;
30
-
31
- const writeJson = (path: string, value: unknown) =>
32
- Bun.write(path, `${JSON.stringify(value, null, 2)}\n`);
33
-
34
- const build = async () => {
35
- for (const platform of platforms) {
36
- const directory = `dist/npm/${platform.os}-${platform.cpu}`;
37
- const executable = platform.os === "win32" ? "adhere.exe" : "adhere";
38
- await rm(directory, { recursive: true, force: true });
39
- await $`bun build --compile --minify --sourcemap --target=${platform.target} src/main.ts --asset ./presets --outfile ${directory}/bin/${executable}`;
40
- await writeJson(`${directory}/package.json`, {
41
- name: packageName(platform),
42
- version: packageJson.version,
43
- description: `The adhere executable for ${platform.os}-${platform.cpu}. Install ${packageJson.name}, which depends on it.`,
44
- repository: packageJson.repository,
45
- os: [platform.os],
46
- cpu: [platform.cpu],
47
- files: [`bin/${executable}`],
48
- preferUnplugged: true,
49
- publishConfig: packageJson.publishConfig,
50
- });
51
- }
52
- };
53
-
54
- const pin = () =>
55
- writeJson("package.json", {
56
- ...packageJson,
57
- optionalDependencies: Object.fromEntries(
58
- platforms.map((platform) => [packageName(platform), packageJson.version]),
59
- ),
60
- });
61
-
62
- const command = Bun.argv[2];
63
- if (command === "build") await build();
64
- else if (command === "pin") await pin();
65
- else {
66
- console.error("usage: bun scripts/npm-packages.ts build|pin");
67
- process.exit(2);
68
- }
@@ -1,540 +0,0 @@
1
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { Crypto, Effect, FileSystem, Layer, Path, Record } from "effect";
5
- import { describe, expect, it } from "vite-plus/test";
6
- import { findContradictions, formatContradictions } from "../src/contradictions.ts";
7
- import {
8
- AdhereConfig as AdhereConfigSchema,
9
- decodeConfig,
10
- type Loaded,
11
- type Preset,
12
- resolveConfig,
13
- } from "../src/config.ts";
14
- import { initProject } from "../src/init.ts";
15
- import { parseRuleMarkdown } from "../src/markdown.ts";
16
- import type { ScannedFile } from "../src/models/Audit.ts";
17
- import { applicableRules, loadAdhereRuleSet, loadRules } from "../src/rules.ts";
18
- import { AdhereConfig } from "../src/services/AdhereConfig.ts";
19
- import { AuditCache, type CacheEntry } from "../src/services/AuditCache.ts";
20
- import { blockBody, Jev, judgeBody, locateBody, type Rules } from "../src/services/Jev.ts";
21
- import { SourceWalker } from "../src/services/SourceWalker.ts";
22
- import { render, runAudit } from "../src/workflows/audit.ts";
23
-
24
- const a = {
25
- description: "Ports are branded.",
26
- reference: '\nconst Port = Schema.Int.pipe(Schema.brand("Port"))\ntype Port = typeof Port.Type\n',
27
- };
28
- const b = {
29
- description: "Secrets are redacted.",
30
- reference: 'const key = yield* Config.redacted("API_KEY")',
31
- };
32
- const rules: Rules = { a, b };
33
-
34
- /** Echoes the input as its digest, so equal text still shares a cache key. */
35
- const testCrypto = Layer.succeed(
36
- Crypto.Crypto,
37
- Crypto.make({
38
- randomBytes: (size) => new Uint8Array(size),
39
- digest: (_algorithm, data) => Effect.succeed(data),
40
- }),
41
- );
42
-
43
- const memoryCache = () => {
44
- const entries = new Map<string, CacheEntry>();
45
- return Layer.succeed(AuditCache, {
46
- get: (path) => Effect.sync(() => entries.get(path)),
47
- put: (path, entry) =>
48
- Effect.sync(() => {
49
- entries.set(path, entry);
50
- }),
51
- });
52
- };
53
-
54
- const recordingJev = (answers: {
55
- readonly judge: Record<string, number>;
56
- readonly locate: Record<string, number>;
57
- }) => {
58
- const calls = { judge: [] as Array<Rules>, locate: [] as Array<Rules> };
59
- const asked = (table: Record<string, number>, rules: Rules) =>
60
- Record.filter(table, (_, id) => rules[id] !== undefined);
61
- const layer = Layer.succeed(Jev, {
62
- judge: (_lines, rules) =>
63
- Effect.sync(() => {
64
- calls.judge.push(rules);
65
- return asked(answers.judge, rules);
66
- }),
67
- locate: (_lines, rules) =>
68
- Effect.sync(() => {
69
- calls.locate.push(rules);
70
- return asked(answers.locate, rules);
71
- }),
72
- });
73
- return { calls, layer };
74
- };
75
-
76
- const source: ScannedFile = {
77
- path: "/repo/src/server.ts",
78
- lines: [
79
- "const before = 1;",
80
- " const port: number = Number(process.env.PORT);",
81
- "const after = 2;",
82
- ],
83
- };
84
-
85
- const audit = (options: {
86
- readonly rules: Rules;
87
- readonly threshold?: number;
88
- readonly jev: Layer.Layer<Jev>;
89
- readonly cache: Layer.Layer<AuditCache>;
90
- }) =>
91
- Effect.runPromise(
92
- runAudit.pipe(
93
- Effect.provide(
94
- Layer.mergeAll(
95
- Layer.succeed(AdhereConfig, {
96
- model: "jev-latest",
97
- threshold: options.threshold ?? 0.7,
98
- rules: options.rules,
99
- }),
100
- Layer.succeed(SourceWalker, { files: Effect.succeed([source]) }),
101
- options.jev,
102
- options.cache,
103
- testCrypto,
104
- ),
105
- ),
106
- ),
107
- );
108
-
109
- const findingA = {
110
- rule: "a",
111
- description: a.description,
112
- reference: a.reference,
113
- file: "/repo/src/server.ts",
114
- line: 2,
115
- snippet: "const port: number = Number(process.env.PORT);",
116
- probability: 0.9,
117
- };
118
-
119
- describe("request bodies", () => {
120
- const code = ["const x = 1;", "", " const y = 2;"];
121
-
122
- it("judge: one noul per rule over the numbered code and the rules", () => {
123
- expect(judgeBody("jev-latest", code, rules)).toEqual({
124
- model: "jev-latest",
125
- state: {
126
- code: "1 | const x = 1;\n2 | \n3 | const y = 2;",
127
- rules: {
128
- a: { description: a.description, reference: a.reference },
129
- b: { description: b.description, reference: b.reference },
130
- },
131
- },
132
- questions: {
133
- a: {
134
- type: "noul",
135
- instructions:
136
- 'Does state.code diverge from the pattern shown in state.rules["a"].reference, as described by state.rules["a"].description? Answer no if the pattern does not apply to this file.',
137
- },
138
- b: {
139
- type: "noul",
140
- instructions:
141
- 'Does state.code diverge from the pattern shown in state.rules["b"].reference, as described by state.rules["b"].description? Answer no if the pattern does not apply to this file.',
142
- },
143
- },
144
- });
145
- });
146
-
147
- it("locate: one choice per rule with a criterion per non-blank line", () => {
148
- expect(locateBody("jev-latest", code, { a })).toEqual({
149
- model: "jev-latest",
150
- state: {
151
- code: "1 | const x = 1;\n2 | \n3 | const y = 2;",
152
- rules: { a: { description: a.description, reference: a.reference } },
153
- },
154
- questions: {
155
- a: {
156
- type: "choice",
157
- instructions:
158
- 'Which line of state.code most clearly diverges from state.rules["a"].reference?',
159
- criteria: { "1": "const x = 1;", "3": "const y = 2;" },
160
- },
161
- },
162
- });
163
- });
164
-
165
- it("over 255 lines: a choice over 20-line blocks, then a choice inside the chosen block", () => {
166
- const long = Array.from({ length: 300 }, (_, index) => `const v${index + 1} = ${index + 1};`);
167
- const blocks = blockBody("jev-latest", long, { a });
168
- expect(blocks.questions.a?.type).toBe("choice");
169
- expect(blocks.questions.a?.instructions).toBe(
170
- 'Which block of state.code contains the line that most clearly diverges from state.rules["a"].reference?',
171
- );
172
- expect(Object.keys(blocks.questions.a?.criteria ?? {})).toHaveLength(15);
173
- expect(blocks.questions.a?.criteria["0"]).toBe("const v1 = 1;...");
174
- expect(blocks.questions.a?.criteria["14"]).toBe("const v281 = 281;...");
175
-
176
- const inside = locateBody("jev-latest", long, { a }, { a: 3 });
177
- expect(Object.keys(inside.questions.a?.criteria ?? {})).toEqual(
178
- Array.from({ length: 20 }, (_, index) => String(61 + index)),
179
- );
180
- expect(inside.questions.a?.criteria["61"]).toBe("const v61 = 61;");
181
- expect(inside.questions.a?.criteria["80"]).toBe("const v80 = 80;");
182
- });
183
- });
184
-
185
- /** A decoded config whose rules are already in memory, as the loader hands them on. */
186
- const loaded = (config: typeof AdhereConfigSchema.Encoded, rules: Rules = {}) =>
187
- Effect.map(decodeConfig(config), (decoded) => ({ ...decoded, rules }));
188
-
189
- describe("config", () => {
190
- it("applies the default model and threshold", async () => {
191
- const config = await Effect.runPromise(
192
- loaded({}, { "data/brand": { description: "d", reference: "r" } }),
193
- );
194
- expect(resolveConfig(config)).toEqual({
195
- model: "jev-latest",
196
- threshold: 0.7,
197
- rules: { "data/brand": { description: "d", reference: "r" } },
198
- });
199
- });
200
-
201
- it("folds presets into the rules, with a config rule winning over a preset rule", async () => {
202
- const override = { description: "mine", reference: "mine" };
203
- const preset: Loaded<Preset> = { rules: { a, b } };
204
- const config = await Effect.runPromise(loaded({}, { a: override }));
205
- const resolved = resolveConfig(config, { presets: ["effect"] }, { effect: preset });
206
- expect(resolved.rules).toEqual({ a: override, b });
207
- });
208
-
209
- it("leaves rules unset when the config names none, so the loader can pick .adhere/", async () => {
210
- const decoded = await Effect.runPromise(decodeConfig({}));
211
- expect(decoded.rules).toBeUndefined();
212
- });
213
-
214
- it("accepts a directory path as the rules of a config", async () => {
215
- const decoded = await Effect.runPromise(decodeConfig({ rules: "./team-rules" }));
216
- expect(decoded.rules).toBe("./team-rules");
217
- });
218
-
219
- it("threshold precedence: command line, then config, then preset, then 0.7", async () => {
220
- const registry = { effect: { threshold: 0.9, rules: {} } };
221
- const bare = await Effect.runPromise(loaded({}));
222
- expect(resolveConfig(bare).threshold).toBe(0.7);
223
- expect(resolveConfig(bare, { presets: ["effect"] }, registry).threshold).toBe(0.9);
224
- const configured = await Effect.runPromise(loaded({ presets: ["effect"], threshold: 0.6 }));
225
- expect(resolveConfig(configured, {}, registry).threshold).toBe(0.6);
226
- expect(resolveConfig(configured, { threshold: 0.85 }, registry).threshold).toBe(0.85);
227
- });
228
-
229
- it("refuses an unknown preset", async () => {
230
- const refused = await Effect.runPromise(Effect.flip(decodeConfig({ presets: ["react"] })));
231
- expect(refused._tag).toBe("ConfigUnavailable");
232
- expect(refused.message).toContain("effect");
233
- });
234
-
235
- it("refuses a rule without a reference", async () => {
236
- const refused = await Effect.runPromise(
237
- Effect.flip(decodeConfig({ rules: { "data/brand": { description: "d" } } })),
238
- );
239
- expect(refused._tag).toBe("ConfigUnavailable");
240
- expect(refused.message).toContain("reference");
241
- });
242
- });
243
-
244
- describe("markdown rules", () => {
245
- const parse = (text: string) => Effect.runPromise(parseRuleMarkdown(text, "rules/a.md"));
246
-
247
- it("front matter is the description and threshold; the fenced block is the reference", async () => {
248
- const rule = await parse(
249
- [
250
- "---",
251
- "description: Ports are branded.",
252
- "threshold: 0.8",
253
- "---",
254
- "",
255
- "Prose for GitHub, ignored by adhere.",
256
- "",
257
- "```ts",
258
- 'const Port = Schema.Int.pipe(Schema.brand("Port"))',
259
- "type Port = typeof Port.Type",
260
- "```",
261
- "",
262
- ].join("\n"),
263
- );
264
- expect(rule).toEqual({
265
- description: "Ports are branded.",
266
- threshold: 0.8,
267
- reference: 'const Port = Schema.Int.pipe(Schema.brand("Port"))\ntype Port = typeof Port.Type',
268
- });
269
- });
270
-
271
- it("without a fence, the whole body is the reference", async () => {
272
- const rule = await parse("---\ndescription: d\n---\nconst x = 1\n");
273
- expect(rule).toEqual({ description: "d", reference: "const x = 1" });
274
- });
275
-
276
- it("refuses a file without a description, naming the file", async () => {
277
- const refused = await Effect.runPromise(
278
- Effect.flip(parseRuleMarkdown("---\nthreshold: 0.8\n---\ncode", "rules/a.md")),
279
- );
280
- expect(refused._tag).toBe("ConfigUnavailable");
281
- expect(refused.message).toContain("rules/a.md");
282
- expect(refused.message).toContain("description");
283
- });
284
-
285
- it("loads a directory: the relative path without .md is the rule id", async () => {
286
- const tree: Record<string, string> = {
287
- "/repo/rules/basics/gen.md": "---\ndescription: gen\n---\n```ts\ngen()\n```\n",
288
- "/repo/rules/data/brand.md": "---\ndescription: brand\n---\nbrand()\n",
289
- "/repo/rules/README.md": "---\ndescription: readme\n---\nnot a rule\n",
290
- "/repo/rules/notes.txt": "ignored",
291
- };
292
- const fs = FileSystem.layerNoop({
293
- readDirectory: () =>
294
- Effect.succeed(["basics/gen.md", "data/brand.md", "README.md", "notes.txt"]),
295
- readFileString: (file) => Effect.succeed(tree[file] ?? ""),
296
- });
297
- const rules = await Effect.runPromise(
298
- loadRules("/repo/rules").pipe(Effect.provide(Layer.merge(fs, Path.layer))),
299
- );
300
- expect(rules).toEqual({
301
- README: { description: "readme", reference: "not a rule" },
302
- "basics/gen": { description: "gen", reference: "gen()" },
303
- "data/brand": { description: "brand", reference: "brand()" },
304
- });
305
- });
306
- });
307
-
308
- describe("nested .adhere rules", () => {
309
- it("loads every .adhere directory with the containing directory as scope", async () => {
310
- const tree: Record<string, string> = {
311
- "/repo/.adhere/style/service.md": "---\ndescription: root\n---\nroot()\n",
312
- "/repo/.adhere/cache/ignored.md": "---\ndescription: cache\n---\ncache()\n",
313
- "/repo/packages/api/.adhere/style/service.md": "---\ndescription: api\n---\napi()\n",
314
- "/repo/packages/api/.adhere/api/schema.md": "---\ndescription: schema\n---\nschema()\n",
315
- };
316
- const fs = FileSystem.layerNoop({
317
- readDirectory: () =>
318
- Effect.succeed([
319
- ".adhere/style/service.md",
320
- ".adhere/cache/ignored.md",
321
- "packages/api/.adhere/style/service.md",
322
- "packages/api/.adhere/api/schema.md",
323
- ]),
324
- readFileString: (file) => Effect.succeed(tree[file] ?? ""),
325
- });
326
-
327
- const entries = await Effect.runPromise(
328
- loadAdhereRuleSet("/repo").pipe(Effect.provide(Layer.merge(fs, Path.layer))),
329
- );
330
-
331
- expect(entries).toEqual([
332
- {
333
- id: "style/service",
334
- file: "/repo/.adhere/style/service.md",
335
- scope: "/repo",
336
- rule: { description: "root", reference: "root()" },
337
- },
338
- {
339
- id: "api/schema",
340
- file: "/repo/packages/api/.adhere/api/schema.md",
341
- scope: "/repo/packages/api",
342
- rule: { description: "schema", reference: "schema()" },
343
- },
344
- {
345
- id: "style/service",
346
- file: "/repo/packages/api/.adhere/style/service.md",
347
- scope: "/repo/packages/api",
348
- rule: { description: "api", reference: "api()" },
349
- },
350
- ]);
351
- });
352
-
353
- it("applies root rules globally and lets the nearest nested rule shadow the same id", () => {
354
- const rootOnly = { description: "Root only.", reference: "rootOnly()" };
355
- const rootShadowed = { description: "Root service.", reference: "root()" };
356
- const apiShadow = { description: "API service.", reference: "api()" };
357
- const apiOnly = { description: "API only.", reference: "apiOnly()" };
358
-
359
- const entries = [
360
- {
361
- id: "style/root-only",
362
- file: "/repo/.adhere/style/root-only.md",
363
- scope: "/repo",
364
- rule: rootOnly,
365
- },
366
- {
367
- id: "style/service",
368
- file: "/repo/.adhere/style/service.md",
369
- scope: "/repo",
370
- rule: rootShadowed,
371
- },
372
- {
373
- id: "style/service",
374
- file: "/repo/packages/api/.adhere/style/service.md",
375
- scope: "/repo/packages/api",
376
- rule: apiShadow,
377
- },
378
- {
379
- id: "api/schema",
380
- file: "/repo/packages/api/.adhere/api/schema.md",
381
- scope: "/repo/packages/api",
382
- rule: apiOnly,
383
- },
384
- ];
385
-
386
- expect(applicableRules("/repo/packages/api/src/index.ts", entries)).toEqual({
387
- "api/schema": apiOnly,
388
- "style/root-only": rootOnly,
389
- "style/service": apiShadow,
390
- });
391
- expect(applicableRules("/repo/packages/web/src/index.ts", entries)).toEqual({
392
- "style/root-only": rootOnly,
393
- "style/service": rootShadowed,
394
- });
395
- });
396
- });
397
-
398
- describe("contradictions", () => {
399
- it("finds opposite textual rules that overlap in scope", () => {
400
- const contradictions = findContradictions([
401
- {
402
- id: "style/use-services",
403
- file: "/repo/.adhere/style/use-services.md",
404
- scope: "/repo",
405
- rule: { description: "Use service classes for IO.", reference: "class Api {}" },
406
- },
407
- {
408
- id: "style/avoid-services",
409
- file: "/repo/packages/api/.adhere/style/avoid-services.md",
410
- scope: "/repo/packages/api",
411
- rule: {
412
- description: "Do not use service classes for IO.",
413
- reference: "const Api = {}",
414
- },
415
- },
416
- {
417
- id: "style/unrelated",
418
- file: "/repo/packages/web/.adhere/style/unrelated.md",
419
- scope: "/repo/packages/web",
420
- rule: { description: "Avoid mutable globals.", reference: "x()" },
421
- },
422
- ]);
423
-
424
- expect(contradictions).toHaveLength(1);
425
- expect(formatContradictions(contradictions)).toContain("/repo/.adhere/style/use-services.md");
426
- expect(formatContradictions(contradictions)).toContain(
427
- "/repo/packages/api/.adhere/style/avoid-services.md",
428
- );
429
- });
430
- });
431
-
432
- describe("init", () => {
433
- it("scaffolds config and example rules without clobbering existing files", async () => {
434
- const root = join(tmpdir(), `adhere-init-${Date.now()}`);
435
- await Effect.runPromise(initProject(root));
436
- const config = await readFile(join(root, ".adhere", "config.ts"), "utf8");
437
- const rule = await readFile(join(root, ".adhere", "style", "prefer-small-files.md"), "utf8");
438
-
439
- expect(config).toContain("satisfies Config");
440
- expect(rule).toContain("description:");
441
-
442
- const second = await Effect.runPromise(initProject(root));
443
- expect(second.created).toEqual([]);
444
- expect(second.skipped).toContain(".adhere/config.ts");
445
- expect(second.skipped).toContain(".adhere/style/prefer-small-files.md");
446
- });
447
-
448
- it("keeps a config at another accepted path instead of adding a second one", async () => {
449
- const root = join(tmpdir(), `adhere-init-existing-${Date.now()}`);
450
- await mkdir(root, { recursive: true });
451
- await writeFile(join(root, "adhere.config.ts"), "export default {};\n", "utf8");
452
-
453
- const result = await Effect.runPromise(initProject(root, { force: true }));
454
-
455
- expect(result.skipped).toContain("adhere.config.ts");
456
- expect(result.created).not.toContain(".adhere/config.ts");
457
- await expect(access(join(root, ".adhere", "config.ts"))).rejects.toThrow();
458
- });
459
- });
460
-
461
- describe("pipeline", () => {
462
- it("judges every rule in one call, locates only the flagged rule, reports the located line", async () => {
463
- const jev = recordingJev({ judge: { a: 0.9, b: 0.2 }, locate: { a: 2 } });
464
- const result = await audit({ rules, jev: jev.layer, cache: memoryCache() });
465
- expect(jev.calls.judge).toEqual([{ a, b }]);
466
- expect(jev.calls.locate).toEqual([{ a }]);
467
- expect(result).toEqual({
468
- files: 1,
469
- judged: 1,
470
- cached: 0,
471
- skipped: 0,
472
- findings: [findingA],
473
- });
474
- });
475
-
476
- it("answers a second run from the cache and re-judges only an edited rule", async () => {
477
- const cache = memoryCache();
478
- const first = recordingJev({ judge: { a: 0.9, b: 0.2 }, locate: { a: 2 } });
479
- await audit({ rules, jev: first.layer, cache });
480
-
481
- const second = recordingJev({ judge: { a: 0.9, b: 0.2 }, locate: { a: 2 } });
482
- const cached = await audit({ rules, jev: second.layer, cache });
483
- expect(second.calls).toEqual({ judge: [], locate: [] });
484
- expect(cached).toEqual({
485
- files: 1,
486
- judged: 0,
487
- cached: 1,
488
- skipped: 0,
489
- findings: [findingA],
490
- });
491
-
492
- const editedB = { ...b, reference: 'const token = yield* Config.redacted("TOKEN")' };
493
- const third = recordingJev({ judge: { a: 0.9, b: 0.3 }, locate: { a: 2 } });
494
- const edited = await audit({ rules: { a, b: editedB }, jev: third.layer, cache });
495
- expect(third.calls).toEqual({ judge: [{ b: editedB }], locate: [] });
496
- expect(edited.findings).toEqual([findingA]);
497
- expect(edited.judged).toBe(1);
498
- });
499
-
500
- it("locates a cached judgment when a lower threshold flags it", async () => {
501
- const cache = memoryCache();
502
- const strict = recordingJev({ judge: { a: 0.9, b: 0.2 }, locate: { a: 2 } });
503
- const quiet = await audit({ rules, threshold: 0.95, jev: strict.layer, cache });
504
- expect(strict.calls.locate).toEqual([]);
505
- expect(quiet.findings).toEqual([]);
506
-
507
- const lenient = recordingJev({ judge: { a: 0.9, b: 0.2 }, locate: { a: 2 } });
508
- const result = await audit({ rules, threshold: 0.7, jev: lenient.layer, cache });
509
- expect(lenient.calls).toEqual({ judge: [], locate: [{ a }] });
510
- expect(result.findings).toEqual([findingA]);
511
- });
512
- });
513
-
514
- describe("render", () => {
515
- const result = { files: 1, judged: 1, cached: 0, skipped: 0, findings: [findingA] };
516
-
517
- it("prints the vp-lint frame with the reference as the hint", () => {
518
- expect(render(result, { root: "/repo" }).join("\n")).toBe(
519
- [
520
- " × a (0.90): Ports are branded.",
521
- " ╭─[src/server.ts:2:1]",
522
- " 2 │ const port: number = Number(process.env.PORT);",
523
- " · ──────────────────────────────────────────────",
524
- " ╰────",
525
- ' hint: const Port = Schema.Int.pipe(Schema.brand("Port"))',
526
- " type Port = typeof Port.Type",
527
- "",
528
- "Found 1 error.",
529
- "1 file, 1 judged, 0 cached.",
530
- ].join("\n"),
531
- );
532
- });
533
-
534
- it("colors the header red on a terminal and counts skipped files", () => {
535
- const colored = render({ ...result, skipped: 2 }, { color: true }).join("\n");
536
- expect(colored).toContain("\u001b[38;2;219;91;81;1m×");
537
- expect(colored).toContain("\u001b[38;2;5;125;160;1m/repo/src/server.ts");
538
- expect(colored.endsWith("1 file, 1 judged, 0 cached, 2 skipped.")).toBe(true);
539
- });
540
- });
package/test/cli.test.ts DELETED
@@ -1,45 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { promisify } from "node:util";
6
- import packageJson from "../package.json";
7
- import { describe, expect, it } from "vite-plus/test";
8
-
9
- const execFileAsync = promisify(execFile);
10
- const main = join(process.cwd(), "src", "main.ts");
11
-
12
- describe("cli", () => {
13
- it("reports the package version", async () => {
14
- const { stderr, stdout } = await execFileAsync("bun", ["src/main.ts", "--version"], {
15
- cwd: process.cwd(),
16
- });
17
-
18
- expect(stderr).toBe("");
19
- expect(stdout.trim()).toBe(`adhere v${packageJson.version}`);
20
- });
21
-
22
- it("prints help listing every command when run bare", async () => {
23
- const { stdout } = await execFileAsync("bun", [main], { cwd: process.cwd() });
24
- const subcommands = stdout.slice(stdout.indexOf("SUBCOMMANDS"), stdout.indexOf("EXAMPLES"));
25
-
26
- for (const name of ["lint", "validate", "init", "skill"]) {
27
- expect(subcommands).toMatch(new RegExp(`^ ${name} `, "m"));
28
- }
29
- });
30
-
31
- it("validates a repo's rules without calling Jev", async () => {
32
- const root = join(tmpdir(), `adhere-validate-${Date.now()}`);
33
- await mkdir(join(root, ".adhere"), { recursive: true });
34
- await writeFile(
35
- join(root, ".adhere", "ports.md"),
36
- '---\ndescription: A port is a branded integer.\n---\n\nconst Port = Schema.Int.pipe(Schema.brand("Port"))\n',
37
- "utf8",
38
- );
39
-
40
- const { stdout } = await execFileAsync("bun", [main, "validate"], { cwd: root });
41
-
42
- expect(stdout).toContain("1 rule loaded.");
43
- expect(stdout).toContain("No contradictions found among configured rules.");
44
- });
45
- });
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "ESNext",
4
- "moduleResolution": "Bundler",
5
- "allowArbitraryExtensions": true,
6
- "allowImportingTsExtensions": true,
7
- "esModuleInterop": true,
8
- "resolveJsonModule": true,
9
- "target": "ES2022",
10
- "lib": ["ESNext"],
11
- "types": ["bun", "node", "vitest/globals"],
12
- "strict": true,
13
- "skipLibCheck": true,
14
- "noUncheckedIndexedAccess": true,
15
- "isolatedModules": true,
16
- "verbatimModuleSyntax": true,
17
- "forceConsistentCasingInFileNames": true,
18
- "noEmit": true,
19
- "rootDir": ".",
20
- "paths": {
21
- "#*": ["./src/*"]
22
- }
23
- },
24
- "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", ".adhere/config.ts"],
25
- "exclude": ["dist", "node_modules"]
26
- }