@polarpoint/agentsmd-validator 1.0.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 ADDED
@@ -0,0 +1,77 @@
1
+ # @polarpoint/agentsmd-validator
2
+
3
+ Validates `AGENTS.md` files against a zone-structured schema. Designed for use in CI pipelines across platform engineering repos.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @polarpoint/agentsmd-validator \
9
+ --schema https://raw.githubusercontent.com/your-org/platform-standards/main/schema.json \
10
+ --file AGENTS.md
11
+ ```
12
+
13
+ ### Options
14
+
15
+ | Flag | Description |
16
+ |---|---|
17
+ | `--file <path>` | Path to the AGENTS.md file to validate (required) |
18
+ | `--schema <url\|path>` | URL or local path to a schema.json (optional) |
19
+ | `--strict` | Treat warnings as errors (non-zero exit) |
20
+
21
+ ## What it checks
22
+
23
+ 1. **Zone markers present** — `<!-- zone:1:start -->` / `<!-- zone:1:end -->` exist
24
+ 2. **Zone order** — zones appear in ascending numeric order
25
+ 3. **Required Zone 1 sections** — `## Sync model`, `## Forbidden paths`, `## Naming conventions`, `## Running tests` all present
26
+ 4. **Running tests is actionable** — the section contains at least one executable command (code block or inline command)
27
+ 5. **Zone 1 hash** — if a schema is supplied, warns when Zone 1 content has drifted from the template version
28
+
29
+ ## Zone structure
30
+
31
+ ```markdown
32
+ <!-- zone:1:start -->
33
+ ## Sync model
34
+ ...
35
+ ## Forbidden paths
36
+ ...
37
+ ## Naming conventions
38
+ ...
39
+ ## Running tests
40
+ ...
41
+ <!-- zone:1:end -->
42
+
43
+ <!-- zone:2:start -->
44
+ ## Team conventions
45
+ ...
46
+ <!-- zone:2:end -->
47
+
48
+ <!-- zone:3:start -->
49
+ ## Repo-specific notes
50
+ ...
51
+ <!-- zone:3:end -->
52
+ ```
53
+
54
+ ## GitHub Actions
55
+
56
+ Copy `.github/workflows/validate-agents-md.yml` into your repo, update the `--schema` URL to point at your `platform-standards` repo, and the validator runs on every PR that touches `AGENTS.md`.
57
+
58
+ ## Schema format
59
+
60
+ ```json
61
+ {
62
+ "templateVersion": "1.0.0",
63
+ "zone1Hash": "98dfe660cbdaf240",
64
+ "requiredSections": ["Sync model", "Forbidden paths", "Naming conventions", "Running tests"]
65
+ }
66
+ ```
67
+
68
+ After editing your template, regenerate `zone1Hash`:
69
+
70
+ ```bash
71
+ node -e "
72
+ const c=require('crypto'), f=require('fs');
73
+ const z=f.readFileSync('templates/default-agents-md.md','utf8')
74
+ .match(/zone:1:start[\s\S]*?zone:1:end/)[0];
75
+ console.log(c.createHash('sha256').update(z.replace(/\s+/g,' ').trim()).digest('hex').slice(0,16))
76
+ "
77
+ ```
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const https = require('https');
7
+ const http = require('http');
8
+ const { validate } = require('../lib/validator');
9
+
10
+ // ── CLI argument parsing ──────────────────────────────────────────────────────
11
+ const args = process.argv.slice(2);
12
+ let schemaArg = null;
13
+ let fileArg = null;
14
+ let strict = false;
15
+
16
+ for (let i = 0; i < args.length; i++) {
17
+ if (args[i] === '--schema' && args[i + 1]) { schemaArg = args[++i]; continue; }
18
+ if (args[i] === '--file' && args[i + 1]) { fileArg = args[++i]; continue; }
19
+ if (args[i] === '--strict') { strict = true; continue; }
20
+ if (args[i] === '--version' || args[i] === '-v') {
21
+ const pkg = require('../package.json');
22
+ console.log(pkg.version);
23
+ process.exit(0);
24
+ }
25
+ if (args[i] === '--help' || args[i] === '-h') {
26
+ console.log(`
27
+ Usage: agentsmd-validator --file <path> [--schema <url|path>] [--strict]
28
+
29
+ Options:
30
+ --file <path> Path to AGENTS.md file to validate (required)
31
+ --schema <url> URL or path to schema.json for drift detection (optional)
32
+ --strict Treat warnings as errors (non-zero exit code)
33
+ --version, -v Print version
34
+ --help, -h Print this help
35
+
36
+ Examples:
37
+ npx @polarpoint/agentsmd-validator --file AGENTS.md
38
+ npx @polarpoint/agentsmd-validator \\
39
+ --file AGENTS.md \\
40
+ --schema https://raw.githubusercontent.com/your-org/platform-standards/main/schema.json
41
+ `);
42
+ process.exit(0);
43
+ }
44
+ }
45
+
46
+ if (!fileArg) {
47
+ console.error('Error: --file is required\n');
48
+ console.error('Usage: agentsmd-validator --file <path> [--schema <url|path>] [--strict]');
49
+ console.error(' agentsmd-validator --help');
50
+ process.exit(1);
51
+ }
52
+
53
+ // ── Helpers ───────────────────────────────────────────────────────────────────
54
+ function fetchUrl(url) {
55
+ return new Promise((resolve, reject) => {
56
+ const client = url.startsWith('https') ? https : http;
57
+ client.get(url, res => {
58
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
59
+ return fetchUrl(res.headers.location).then(resolve).catch(reject);
60
+ }
61
+ if (res.statusCode !== 200) {
62
+ return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
63
+ }
64
+ let data = '';
65
+ res.on('data', chunk => { data += chunk; });
66
+ res.on('end', () => resolve(data));
67
+ }).on('error', reject);
68
+ });
69
+ }
70
+
71
+ function loadSchema(schemaArg) {
72
+ if (!schemaArg) return Promise.resolve(null);
73
+ if (schemaArg.startsWith('http://') || schemaArg.startsWith('https://')) {
74
+ return fetchUrl(schemaArg).then(JSON.parse).catch(err => {
75
+ console.warn(` ⚠ Could not fetch schema from ${schemaArg}: ${err.message}`);
76
+ return null;
77
+ });
78
+ }
79
+ try {
80
+ return Promise.resolve(JSON.parse(fs.readFileSync(path.resolve(schemaArg), 'utf8')));
81
+ } catch (err) {
82
+ console.warn(` ⚠ Could not read schema file ${schemaArg}: ${err.message}`);
83
+ return Promise.resolve(null);
84
+ }
85
+ }
86
+
87
+ // ── Main ──────────────────────────────────────────────────────────────────────
88
+ async function main() {
89
+ const filePath = path.resolve(fileArg);
90
+ if (!fs.existsSync(filePath)) {
91
+ console.error(` ✗ File not found: ${filePath}`);
92
+ process.exit(1);
93
+ }
94
+
95
+ const content = fs.readFileSync(filePath, 'utf8');
96
+ const schemaData = await loadSchema(schemaArg);
97
+
98
+ console.log(`\nValidating ${path.basename(filePath)}\n${'─'.repeat(50)}`);
99
+
100
+ const { errors, warnings, zone1Hash } = validate(content, schemaData);
101
+
102
+ if (zone1Hash) {
103
+ console.log(` Zone 1 hash : ${zone1Hash}`);
104
+ }
105
+
106
+ if (warnings.length) {
107
+ console.log('');
108
+ warnings.forEach(w => console.log(` ⚠ ${w}`));
109
+ }
110
+
111
+ if (errors.length) {
112
+ console.log('');
113
+ errors.forEach(e => console.error(` ✗ ${e}`));
114
+ console.log(`\n${'─'.repeat(50)}`);
115
+ console.log(` FAILED ${errors.length} error(s), ${warnings.length} warning(s)\n`);
116
+ process.exit(1);
117
+ }
118
+
119
+ console.log(`\n${'─'.repeat(50)}`);
120
+ if (warnings.length) {
121
+ console.log(` PASSED with ${warnings.length} warning(s)\n`);
122
+ if (strict) {
123
+ console.log(' (--strict mode: warnings treated as errors)');
124
+ process.exit(1);
125
+ }
126
+ } else {
127
+ console.log(' PASSED\n');
128
+ }
129
+ }
130
+
131
+ main().catch(err => {
132
+ console.error(` ✗ Unexpected error: ${err.message}`);
133
+ process.exit(1);
134
+ });
package/lib/parser.js ADDED
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const ZONE_START = /<!--\s*zone:(\d+):start\s*-->/;
6
+ const ZONE_END = /<!--\s*zone:(\d+):end\s*-->/;
7
+
8
+ /**
9
+ * Parse an AGENTS.md file into its zone segments.
10
+ * Returns { zones: { 1: string, 2: string, 3: string }, raw: string }
11
+ */
12
+ function parseZones(content) {
13
+ const lines = content.split('\n');
14
+ const zones = {};
15
+ let currentZone = null;
16
+ let buffer = [];
17
+ const order = []; // track order zones were opened
18
+
19
+ for (const line of lines) {
20
+ const startMatch = line.match(ZONE_START);
21
+ const endMatch = line.match(ZONE_END);
22
+
23
+ if (startMatch) {
24
+ currentZone = parseInt(startMatch[1], 10);
25
+ order.push(currentZone);
26
+ buffer = [];
27
+ continue;
28
+ }
29
+
30
+ if (endMatch) {
31
+ const z = parseInt(endMatch[1], 10);
32
+ if (currentZone === z) {
33
+ zones[z] = buffer.join('\n').trim();
34
+ currentZone = null;
35
+ buffer = [];
36
+ }
37
+ continue;
38
+ }
39
+
40
+ if (currentZone !== null) {
41
+ buffer.push(line);
42
+ }
43
+ }
44
+
45
+ return { zones, order, raw: content };
46
+ }
47
+
48
+ /**
49
+ * Compute a stable SHA-256 hash of zone content (normalised whitespace).
50
+ */
51
+ function hashZone(content) {
52
+ const normalised = content.replace(/\s+/g, ' ').trim();
53
+ return crypto.createHash('sha256').update(normalised).digest('hex').slice(0, 16);
54
+ }
55
+
56
+ /**
57
+ * Extract all headings (## level) from a zone string.
58
+ */
59
+ function headingsInZone(zoneContent) {
60
+ return (zoneContent.match(/^##\s+.+/gm) || []).map(h => h.replace(/^##\s+/, '').trim());
61
+ }
62
+
63
+ /**
64
+ * Detect whether a block of text contains at least one shell-executable command.
65
+ * Heuristic: a line inside a fenced code block, or a line starting with common
66
+ * shell tokens (make, npm, yarn, pnpm, go, cargo, pytest, mvn, gradle, ./...).
67
+ */
68
+ function hasExecutableCommand(zoneContent) {
69
+ const codeBlocks = [...zoneContent.matchAll(/```[\s\S]*?```/g)];
70
+ if (codeBlocks.length > 0) {
71
+ for (const block of codeBlocks) {
72
+ const inner = block[0].replace(/^```[^\n]*\n/, '').replace(/```$/, '');
73
+ if (/\S/.test(inner)) return true;
74
+ }
75
+ }
76
+ // inline code that looks like a command
77
+ const inlineCode = [...zoneContent.matchAll(/`([^`]+)`/g)].map(m => m[1]);
78
+ const cmdPattern = /^(make|npm|yarn|pnpm|go\s|cargo|pytest|mvn|gradle|\.\/|python|node|bash|sh\s|docker|kubectl)/;
79
+ return inlineCode.some(c => cmdPattern.test(c.trim()));
80
+ }
81
+
82
+ module.exports = { parseZones, hashZone, headingsInZone, hasExecutableCommand };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const { parseZones, hashZone, headingsInZone, hasExecutableCommand } = require('./parser');
4
+
5
+ const REQUIRED_ZONE1_SECTIONS = [
6
+ 'Sync model',
7
+ 'Forbidden paths',
8
+ 'Naming conventions',
9
+ 'Running tests',
10
+ ];
11
+
12
+ /**
13
+ * Run all checks against the parsed content.
14
+ * Returns { errors: string[], warnings: string[], zone1Hash: string }
15
+ */
16
+ function validate(content, schemaData) {
17
+ const errors = [];
18
+ const warnings = [];
19
+
20
+ const { zones, order } = parseZones(content);
21
+
22
+ // ── 1. Zone markers present ───────────────────────────────────────────────
23
+ if (!zones[1]) {
24
+ errors.push('Zone 1 is missing. Add <!-- zone:1:start --> and <!-- zone:1:end --> markers.');
25
+ }
26
+ if (!zones[2]) {
27
+ warnings.push('Zone 2 markers not found. Guarded section is optional but recommended.');
28
+ }
29
+ if (!zones[3]) {
30
+ warnings.push('Zone 3 markers not found. Free section is optional but recommended.');
31
+ }
32
+
33
+ // ── 2. Zone order ─────────────────────────────────────────────────────────
34
+ if (order.length > 1) {
35
+ for (let i = 1; i < order.length; i++) {
36
+ if (order[i] < order[i - 1]) {
37
+ errors.push(`Zone markers are out of order: zone ${order[i]} appears after zone ${order[i - 1]}.`);
38
+ }
39
+ }
40
+ }
41
+
42
+ // ── 3. Required sections inside Zone 1 ───────────────────────────────────
43
+ if (zones[1]) {
44
+ const headings = headingsInZone(zones[1]);
45
+ for (const section of REQUIRED_ZONE1_SECTIONS) {
46
+ if (!headings.some(h => h.toLowerCase().includes(section.toLowerCase()))) {
47
+ errors.push(`Required section "## ${section}" is missing from Zone 1.`);
48
+ }
49
+ }
50
+
51
+ // ── 4. Running tests must contain an executable command ────────────────
52
+ const testsIdx = headings.findIndex(h => h.toLowerCase().includes('running tests'));
53
+ if (testsIdx !== -1) {
54
+ // extract just the Running tests sub-section
55
+ const z1Lines = zones[1].split('\n');
56
+ let inTests = false;
57
+ let testsBuffer = [];
58
+ for (const line of z1Lines) {
59
+ if (/^##\s+Running tests/i.test(line)) { inTests = true; continue; }
60
+ if (inTests && /^##\s+/.test(line)) break;
61
+ if (inTests) testsBuffer.push(line);
62
+ }
63
+ const testsContent = testsBuffer.join('\n');
64
+ if (!hasExecutableCommand(testsContent)) {
65
+ errors.push('"## Running tests" in Zone 1 must contain at least one executable command (code block or inline command).');
66
+ }
67
+ }
68
+
69
+ // ── 5. Zone 1 hash drift check ────────────────────────────────────────
70
+ const zone1Hash = hashZone(zones[1]);
71
+ if (schemaData && schemaData.zone1Hash) {
72
+ if (zone1Hash !== schemaData.zone1Hash) {
73
+ warnings.push(
74
+ `Zone 1 content hash (${zone1Hash}) does not match template version ` +
75
+ `${schemaData.templateVersion || 'unknown'} (${schemaData.zone1Hash}). ` +
76
+ `Run the sync engine or update Zone 1 from platform-standards.`
77
+ );
78
+ }
79
+ }
80
+
81
+ return { errors, warnings, zone1Hash };
82
+ }
83
+
84
+ return { errors, warnings, zone1Hash: null };
85
+ }
86
+
87
+ module.exports = { validate };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@polarpoint/agentsmd-validator",
3
+ "version": "1.0.0",
4
+ "description": "Validate AGENTS.md files against a zone-structured schema",
5
+ "main": "lib/validator.js",
6
+ "bin": {
7
+ "agentsmd-validator": "./bin/agentsmd-validator.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "prepublishOnly": "node bin/agentsmd-validator.js --file bin/agentsmd-validator.js 2>/dev/null || true"
16
+ },
17
+ "keywords": [
18
+ "agents",
19
+ "ai",
20
+ "validation",
21
+ "platform-engineering",
22
+ "gitops",
23
+ "agentsmd"
24
+ ],
25
+ "author": "Polarpoint <hello@polarpoint.io>",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/polarpoint-io/ai-capabilities.git",
37
+ "directory": "agentsmd-validator"
38
+ },
39
+ "homepage": "https://github.com/polarpoint-io/ai-capabilities/tree/main/agentsmd-validator#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/polarpoint-io/ai-capabilities/issues"
42
+ }
43
+ }