Skip to content

Commit dd39d63

Browse files
author
tuanductran
committed
feat(schemas): validate maintenance reports with valibot
1 parent c7ec5b0 commit dd39d63

14 files changed

Lines changed: 218 additions & 62 deletions

File tree

AGENTS.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,33 +61,23 @@ nextdns-skills/
6161
│ │ ├── src/
6262
│ │ │ ├── cli.ts
6363
│ │ │ ├── index.ts
64-
│ │ │ ├── validate-rules.ts
65-
│ │ │ ├── update-counts.ts
66-
│ │ │ ├── check-duplicates.ts
67-
│ │ │ ├── check-tags.ts
68-
│ │ │ ├── generate-stats.ts
69-
│ │ │ └── utils.ts
64+
│ │ │ ├── commands/ # validate-rules, update-counts, checks, audit
65+
│ │ │ ├── core/ # shared utilities, schemas, paths, version
66+
│ │ │ └── __tests__/
7067
│ │ ├── tsconfig.json
71-
│ │ ├── tsdown.config.ts
68+
│ │ ├── vite.config.ts
7269
│ │ └── vitest.config.ts
7370
│ └── nextdns-skills-build/ # Build tooling and programmatic API
7471
│ ├── bin/
7572
│ │ └── nextdns-skills-build.js # Static entrypoint (checked into git)
7673
│ ├── src/
7774
│ │ ├── cli.ts
7875
│ │ ├── index.ts
79-
│ │ ├── build.ts
80-
│ │ ├── validate.ts
81-
│ │ ├── parser.ts
82-
│ │ ├── config.ts
83-
│ │ ├── types.ts
84-
│ │ ├── search.ts
85-
│ │ ├── export.ts
86-
│ │ ├── extract-tests.ts
87-
│ │ ├── migrate.ts
88-
│ │ └── utils.ts
76+
│ │ ├── commands/ # build, validate, search, export, migrate
77+
│ │ ├── core/ # config, parser, types, markdown, paths
78+
│ │ └── __tests__/
8979
│ ├── tsconfig.json
90-
│ ├── tsdown.config.ts
80+
│ ├── vite.config.ts
9181
│ └── vitest.config.ts
9282
├── templates/
9383
│ ├── rule-template.md
@@ -113,7 +103,8 @@ and any public package export together.
113103
### `nextdns-scripts` (package name: `nextdns-skills-scripts`)
114104

115105
Maintenance scripts: validate rule integrity, sync rule counts, check duplicates and tags, print
116-
statistics.
106+
statistics, run the combined audit, and expose Valibot schemas for report consumers. Shared modules
107+
are under `src/core/`; CLI commands are under `src/commands/`.
117108

118109
**Exports:**
119110

@@ -309,6 +300,7 @@ Run before finalising any changes:
309300
| `pnpm update-counts` | Sync rule counts in README.md |
310301
| `pnpm types:check` | Type-check all packages via Turbo |
311302
| `pnpm test` | Run Vitest across both packages |
303+
| `src/core/schemas.ts` | Valibot runtime schemas and parsers for audit/statistics report boundaries |
312304
| `pnpm test:coverage` | Run tests with v8 coverage report |
313305

314306
## Building AGENTS.md

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ runtime. This means pnpm creates the `.bin` symlink during `pnpm install` before
8282
### `nextdns-scripts` (package name: `nextdns-skills-scripts`)
8383

8484
Maintenance scripts: validate rule integrity, sync rule counts, check duplicates and tags, print
85-
statistics, and run the combined audit. Shared modules are under `src/core/`; CLI commands are
86-
under `src/commands/`.
85+
statistics, run the combined audit, and expose Valibot schemas for report consumers. Shared modules
86+
are under `src/core/`; CLI commands are under `src/commands/`.
8787

8888
**Exports:**
8989

@@ -119,6 +119,7 @@ under `src/commands/`.
119119
| `test` | `vitest run` |
120120
| `test:coverage` | `vitest run --coverage` |
121121
| `types:check` | `tsc --noEmit` |
122+
| `schema API` | `src/core/schemas.ts` exports Valibot report schemas and safe parsers |
122123

123124
### `nextdns-skills-build`
124125

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ The root README is the public inventory for rule counts. If a rule is added or r
6868

6969
## Build and validation flow
7070

71-
The root package uses pnpm scripts and Turbo to coordinate the two workspace packages. Each TypeScript package separates reusable modules under `src/core/`, command implementations under `src/commands/`, and the dispatcher under `src/cli.ts`; Vite may flatten those source entries into named files under `dist/`, so package exports and CLI maps remain the compiled boundary. `nextdns-skills-build` compiles rules into generated context and validates structure. `nextdns-skills-scripts` provides focused maintenance commands and the combined `audit` aggregator. The audit runs referential-integrity, frontmatter, tags, duplicate-title, and duplicate-tag checks, then includes generated statistics in its `AuditReport`. Its public package surface exports `runAudit`, `formatAuditText`, `AuditCheck`, and `AuditReport` from [`src/index.ts`](../packages/nextdns-scripts/src/index.ts). The root scripts compose these checks with formatting, type-checking, Markdown lint, link validation, duplicate-code detection, and tests.
71+
The root package uses pnpm scripts and Turbo to coordinate the two workspace packages. Each TypeScript package separates reusable modules under `src/core/`, command implementations under `src/commands/`, and the dispatcher under `src/cli.ts`; Vite may flatten those source entries into named files under `dist/`, so package exports and CLI maps remain the compiled boundary. `nextdns-skills-build` compiles rules into generated context and validates structure. `nextdns-skills-scripts` provides focused maintenance commands and the combined `audit` aggregator. The audit runs referential-integrity, frontmatter, tags, duplicate-title, and duplicate-tag checks, then includes generated statistics in its `AuditReport`. Its public package surface exports `runAudit`, `formatAuditText`, `AuditCheck`, `AuditReport`, the Valibot schemas, and `parseAuditReport`/`parseStatsReport` from [`src/index.ts`](../packages/nextdns-scripts/src/index.ts). `runAudit` validates its assembled report at the runtime boundary before returning it. The root scripts compose these checks with formatting, type-checking, Markdown lint, link validation, duplicate-code detection, and tests.
7272

7373
| Change | Minimum build or validation |
7474
| :--- | :--- |

docs/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ For documentation-only changes, `pnpm lint:md`, `pnpm lint:links`, and `git diff
6969

7070
`pnpm run audit` invokes the `audit` command from [`nextdns-skills-scripts`](../packages/nextdns-scripts/src/commands/audit.ts). It aggregates five repository checks: referential integrity, frontmatter validity, tag hygiene, duplicate titles, and duplicate tag sets. Duplicate-title findings are split into errors and warnings; only errors make that check fail. Duplicate tag sets are counted as warnings in the report, but any duplicate makes the `duplicate-tags` check fail. The command exits with status `0` only when every check passes and exits with status `1` when any check fails.
7171

72-
Use `pnpm run audit -- --json` when CI, scripts, or a reviewer needs a machine-readable [`AuditReport`](../packages/nextdns-scripts/src/commands/audit.ts). The report includes the generation timestamp, current rule count, per-check pass/error/warning counts, and aggregate statistics. Treat `generatedAt` and `ruleCount` as run-time values rather than stable documentation facts. The combined audit complements, but does not replace, `pnpm build:check`, Markdown and link linting, duplicate-code scanning, rule validation, or the test suite.
72+
Use `pnpm run audit -- --json` when CI, scripts, or a reviewer needs a machine-readable [`AuditReport`](../packages/nextdns-scripts/src/commands/audit.ts). The report includes the generation timestamp, current rule count, per-check pass/error/warning counts, and aggregate statistics. Treat `generatedAt` and `ruleCount` as run-time values rather than stable documentation facts. The public [`schemas.ts`](../packages/nextdns-scripts/src/core/schemas.ts) exports Valibot schemas and `parseAuditReport`/`parseStatsReport` helpers for consumers that receive report JSON from an external process. The combined audit complements, but does not replace, `pnpm build:check`, Markdown and link linting, duplicate-code scanning, rule validation, or the test suite.
7373

7474
### Run the duplicate-code check
7575

docs/DOCUMENTATION.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ When documenting `pnpm run audit`, describe its stable contract rather than copy
4747
| Audit detail | Documentation treatment |
4848
| :--- | :--- |
4949
| Check names and pass/fail semantics | Treat as repository facts and link to `src/commands/audit.ts` |
50-
| JSON fields and public TypeScript types | Link to `AuditReport` and `AuditCheck`; verify tests when changing the shape |
50+
| JSON fields and public TypeScript types | Link to `AuditReport`, `AuditCheck`, and the public Valibot schemas; verify schema tests when changing the shape |
5151
| Current rule count or statistics | State the observation date or obtain it from a fresh command; do not use it as a timeless invariant |
5252
| A failed check | Explain the owning validator and repair path instead of hiding the failure |
5353
| Relationship to other gates | State explicitly that audit does not replace build drift, Markdown, link, rule, type, or test checks |
5454

5555
Use a fresh `pnpm run audit -- --json` result in release or review evidence when needed, but do not paste account data, generated logs, or an unbounded report into a public document.
5656

57+
Runtime schemas are repository facts, not a replacement for semantic validators. The Valibot schemas in [`packages/nextdns-scripts/src/core/schemas.ts`](../packages/nextdns-scripts/src/core/schemas.ts) validate the shape and primitive invariants of audit/statistics reports; the existing rule validators still own frontmatter semantics, cross-file references, tags, and content structure. Document `parseAuditReport` and `parseStatsReport` as the safe boundary for unknown JSON, and do not claim that schema parsing proves a rule is semantically correct.
58+
5759
## Documenting duplicate-code checks
5860

5961
When documenting `pnpm lint:duplicates`, describe the committed policy rather than copying a single jscpd run. Link to [`.jscpd.json`](../.jscpd.json) for scan scope, candidate size, ignored paths, reporters, and threshold. Treat clone counts, duplicated-line percentages, and file names as run-time evidence; they can change after a refactor or when source files are added. Explain the remediation path: first determine whether the clone represents shared behavior, then extract reusable logic into `src/core/` or keep the duplication only when the behavior is intentionally different and the exception is documented.

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This directory contains the public documentation for **NextDNS Skills**. It is i
1010
| Understand source, generated output, and CI | [ARCHITECTURE.md](ARCHITECTURE.md) | Contributors and reviewers |
1111
| Add a rule or prepare a pull request | [CONTRIBUTING.md](CONTRIBUTING.md) | Contributors |
1212
| Write or review reliable documentation | [DOCUMENTATION.md](DOCUMENTATION.md) | Authors and reviewers |
13-
| Run or consume the repository audit | [CONTRIBUTING.md](CONTRIBUTING.md) and [`audit.ts`](../packages/nextdns-scripts/src/audit.ts) | Maintainers and package contributors |
13+
| Run or consume the repository audit | [CONTRIBUTING.md](CONTRIBUTING.md), [`audit.ts`](../packages/nextdns-scripts/src/commands/audit.ts), and [`schemas.ts`](../packages/nextdns-scripts/src/core/schemas.ts) | Maintainers and package contributors |
1414

1515
## Documentation boundaries
1616

packages/nextdns-scripts/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
"update-counts": "nextdns-skills-scripts update-counts",
3939
"validate-rules": "nextdns-skills-scripts validate-rules"
4040
},
41+
"dependencies": {
42+
"valibot": "catalog:runtime"
43+
},
4144
"devDependencies": {
4245
"@types/node": "catalog:dev",
4346
"@vitest/coverage-v8": "catalog:test",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { parseAuditReport, parseStatsReport } from '../core/schemas.js';
4+
5+
const validStatistics = {
6+
generatedAt: '2026-08-18T00:00:00.000Z',
7+
totalRules: 1,
8+
skills: [
9+
{
10+
name: 'nextdns-api',
11+
total: 1,
12+
capability: 1,
13+
efficiency: 0,
14+
high: 1,
15+
medium: 0,
16+
low: 0,
17+
},
18+
],
19+
impactDistribution: { HIGH: 1, MEDIUM: 0, LOW: 0 },
20+
topTags: [{ tag: 'api', count: 1 }],
21+
rulesWithNoTags: [],
22+
};
23+
24+
const validChecks = [
25+
{ name: 'referential-integrity', passed: true, errors: 0, warnings: 0 },
26+
{ name: 'frontmatter', passed: true, errors: 0, warnings: 0 },
27+
{ name: 'tags', passed: true, errors: 0, warnings: 0 },
28+
{ name: 'duplicate-titles', passed: true, errors: 0, warnings: 0 },
29+
{ name: 'duplicate-tags', passed: true, errors: 0, warnings: 0 },
30+
];
31+
32+
describe('schema parsing', () => {
33+
it('accepts a complete audit report', () => {
34+
const report = parseAuditReport({
35+
generatedAt: '2026-08-18T00:00:00.000Z',
36+
passed: true,
37+
ruleCount: 1,
38+
checks: validChecks,
39+
statistics: validStatistics,
40+
});
41+
42+
expect(report.statistics.totalRules).toBe(1);
43+
expect(report.checks).toHaveLength(5);
44+
});
45+
46+
it('rejects an unknown audit check name', () => {
47+
expect(() =>
48+
parseAuditReport({
49+
generatedAt: '2026-08-18T00:00:00.000Z',
50+
passed: false,
51+
ruleCount: 1,
52+
checks: [
53+
...validChecks.slice(0, 4),
54+
{ name: 'unknown-check', passed: false, errors: 1, warnings: 0 },
55+
],
56+
statistics: validStatistics,
57+
})
58+
).toThrow();
59+
});
60+
61+
it('rejects missing nested statistics', () => {
62+
expect(() =>
63+
parseAuditReport({
64+
generatedAt: '2026-08-18T00:00:00.000Z',
65+
passed: true,
66+
ruleCount: 1,
67+
checks: validChecks,
68+
})
69+
).toThrow();
70+
});
71+
72+
it('rejects negative counters', () => {
73+
expect(() =>
74+
parseStatsReport({
75+
...validStatistics,
76+
totalRules: -1,
77+
})
78+
).toThrow();
79+
});
80+
81+
it('returns type-safe stats data for a valid report', () => {
82+
const report = parseStatsReport(validStatistics);
83+
84+
expect(report.skills[0]?.name).toBe('nextdns-api');
85+
expect(report.impactDistribution.HIGH).toBe(1);
86+
});
87+
});

packages/nextdns-scripts/src/commands/audit.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,16 @@ import path from 'node:path';
1111
import { fileURLToPath } from 'node:url';
1212

1313
import { getRepositoryRoot } from '../core/paths.js';
14+
import { parseAuditReport, type AuditCheck, type AuditReport } from '../core/schemas.js';
1415
import { checkDuplicateTags, checkDuplicateTitles, loadAllRules } from './check-duplicates.js';
1516
import { validateTags } from './check-tags.js';
16-
import { buildReport, type StatsReport } from './generate-stats.js';
17+
import { buildReport } from './generate-stats.js';
1718
import { validateFrontmatter, validateReferentialIntegrity } from './validate-rules.js';
1819

1920
const REPO_ROOT = getRepositoryRoot(import.meta.url);
2021
const SKILLS_DIR = path.join(REPO_ROOT, 'skills');
2122

22-
export interface AuditCheck {
23-
name: 'referential-integrity' | 'frontmatter' | 'tags' | 'duplicate-titles' | 'duplicate-tags';
24-
passed: boolean;
25-
errors: number;
26-
warnings: number;
27-
}
28-
29-
export interface AuditReport {
30-
generatedAt: string;
31-
passed: boolean;
32-
ruleCount: number;
33-
checks: AuditCheck[];
34-
statistics: StatsReport;
35-
}
23+
export type { AuditCheck, AuditReport } from '../core/schemas.js';
3624

3725
function suppressOutput<T>(fn: () => T): T {
3826
const log = console.log;
@@ -91,13 +79,13 @@ export function runAudit(
9179
},
9280
];
9381

94-
return {
82+
return parseAuditReport({
9583
generatedAt: new Date().toISOString(),
9684
passed: checks.every((check) => check.passed),
9785
ruleCount: rules.length,
9886
checks,
9987
statistics: buildReport(skillsDir, repoRoot),
100-
};
88+
});
10189
}
10290

10391
export function formatAuditText(report: AuditReport): string {

packages/nextdns-scripts/src/commands/generate-stats.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,13 @@ import path from 'node:path';
2020
import { fileURLToPath } from 'node:url';
2121

2222
import { getRepositoryRoot } from '../core/paths.js';
23+
import { parseStatsReport, type SkillStats, type StatsReport } from '../core/schemas.js';
2324
import { collectRuleFiles, parseFrontmatter } from '../core/utils.js';
2425

2526
const REPO_ROOT = getRepositoryRoot(import.meta.url);
2627
const SKILLS_DIR = path.join(REPO_ROOT, 'skills');
2728

28-
export interface SkillStats {
29-
name: string;
30-
total: number;
31-
capability: number;
32-
efficiency: number;
33-
high: number;
34-
medium: number;
35-
low: number;
36-
}
37-
38-
export interface StatsReport {
39-
generatedAt: string;
40-
totalRules: number;
41-
skills: SkillStats[];
42-
impactDistribution: { HIGH: number; MEDIUM: number; LOW: number };
43-
topTags: Array<{ tag: string; count: number }>;
44-
rulesWithNoTags: string[];
45-
}
29+
export type { SkillStats, StatsReport } from '../core/schemas.js';
4630

4731
export function buildReport(
4832
skillsDir: string = SKILLS_DIR,
@@ -113,14 +97,14 @@ export function buildReport(
11397
.slice(0, 20)
11498
.map(([tag, count]) => ({ tag, count }));
11599

116-
return {
100+
return parseStatsReport({
117101
generatedAt: new Date().toISOString(),
118102
totalRules,
119103
skills,
120104
impactDistribution,
121105
topTags,
122106
rulesWithNoTags,
123-
};
107+
});
124108
}
125109

126110
export function printText(report: StatsReport): void {

0 commit comments

Comments
 (0)