azcodr 1.5.1 → 2.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/.agents/hooks.json.example +42 -42
- package/.agents/scripts/safety_guard.sh +143 -34
- package/.agents/scripts/verify_completion.sh +90 -27
- package/.agents/skills/agentic-architect/SKILL.md +125 -125
- package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
- package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
- package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
- package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
- package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -401
- package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
- package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
- package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
- package/.agents/skills/compliance-audit/SKILL.md +120 -120
- package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
- package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
- package/.agents/skills/lets-build/SKILL.md +173 -173
- package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
- package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
- package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
- package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
- package/.agents/skills/product-analyst/SKILL.md +154 -154
- package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
- package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
- package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
- package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
- package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
- package/.agents/skills/relentless-questioner/SKILL.md +128 -128
- package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
- package/.editorconfig +19 -19
- package/.github/workflows/ci.yml +167 -56
- package/.github/workflows/publish.yml +200 -0
- package/.gitignore +40 -25
- package/AGENTS.md +103 -102
- package/LICENSE +21 -21
- package/README.md +168 -154
- package/bin/azcodr.js +14 -228
- package/docs/knowledge/ubiquitous_language.md +31 -18
- package/docs/rules/agentic_configuration.md +259 -259
- package/docs/rules/api_architecture.md +179 -179
- package/docs/rules/authentication.md +76 -76
- package/docs/rules/authorization.md +75 -75
- package/docs/rules/caching.md +69 -69
- package/docs/rules/clean_code.md +62 -62
- package/docs/rules/cloud_native.md +41 -41
- package/docs/rules/cqrs.md +203 -203
- package/docs/rules/database_design.md +125 -125
- package/docs/rules/database_operations.md +69 -69
- package/docs/rules/design_patterns.md +98 -98
- package/docs/rules/devops_ci_cd.md +76 -76
- package/docs/rules/domain_driven_design.md +122 -122
- package/docs/rules/error_handling.md +54 -52
- package/docs/rules/feature_flags.md +59 -59
- package/docs/rules/frontend_architecture.md +157 -157
- package/docs/rules/multitenancy_architecture.md +98 -98
- package/docs/rules/product_ownership.md +127 -127
- package/docs/rules/project_management.md +49 -49
- package/docs/rules/relentless_questioning.md +52 -52
- package/docs/rules/requirements_engineering.md +98 -98
- package/docs/rules/security_compliance.md +53 -53
- package/docs/rules/server_driven_ui.md +88 -88
- package/docs/rules/test_driven_development.md +185 -185
- package/docs/rules/transactional_email.md +27 -27
- package/docs/rules/type_safety.md +65 -65
- package/docs/rules/ui_ux_architecture.md +150 -150
- package/docs/rules/workflow_state_machines.md +117 -117
- package/lib/cli-parse.js +51 -0
- package/lib/cli-target.js +109 -0
- package/lib/cli.js +180 -0
- package/lib/errors.js +28 -0
- package/lib/git.js +29 -0
- package/lib/guards.js +96 -0
- package/lib/index.d.ts +199 -134
- package/lib/index.js +5 -5
- package/lib/links.js +123 -0
- package/lib/permissions.js +44 -0
- package/lib/repo.js +90 -0
- package/lib/scaffold.js +238 -399
- package/memory.md +119 -36
- package/package.json +65 -62
- package/scripts/test_coverage.js +66 -38
- package/scripts/validate/adr.js +151 -0
- package/scripts/validate/io.js +84 -0
- package/scripts/validate/links.js +167 -0
- package/scripts/validate/parity.js +124 -0
- package/scripts/validate/root.js +184 -0
- package/scripts/validate/rules.js +44 -0
- package/scripts/validate/skills.js +96 -0
- package/scripts/validate/text.js +29 -0
- package/scripts/validate-cli.js +13 -0
- package/scripts/validate.js +112 -218
- package/.github/copilot-instructions.md +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readTextOrFail, readSkillFolders } = require('./io.js');
|
|
6
|
+
|
|
7
|
+
function frontMatterBlock(content, skill, fail) {
|
|
8
|
+
const lines = content.split('\n');
|
|
9
|
+
if (lines[0].trim() !== '---') {
|
|
10
|
+
fail(`Skill '${skill}' missing opening front matter delimiter (---)`);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const closingIdx = lines.slice(1).findIndex((l) => l.trim() === '---');
|
|
14
|
+
if (closingIdx === -1) {
|
|
15
|
+
fail(`Skill '${skill}' missing closing front matter delimiter (---)`);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
// Scope metadata lookups to the front-matter block only. Searching the
|
|
19
|
+
// whole file let body prose or a fenced example satisfy the checks.
|
|
20
|
+
const frontMatter = lines.slice(1, closingIdx + 1).join('\n');
|
|
21
|
+
const nameMatch = frontMatter.match(/^name:\s*(.+)$/m);
|
|
22
|
+
if (!nameMatch || nameMatch[1].trim() !== skill) {
|
|
23
|
+
fail(`Skill '${skill}' front matter 'name:' does not match directory name`);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return frontMatter;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractSkillDescription(frontMatter) {
|
|
30
|
+
const descMatch = frontMatter.match(/^description:\s*(.+)$/m);
|
|
31
|
+
const desc = descMatch ? descMatch[1].trim() : '';
|
|
32
|
+
if (!/^[>|][-+]?$/.test(desc)) return desc;
|
|
33
|
+
// YAML block scalars (>- / |) put the real text on following indented
|
|
34
|
+
// lines. Reading only the `>-` marker measured a 2-char "description" and
|
|
35
|
+
// defeated the 1024-char context-budget cap, so fold them in.
|
|
36
|
+
const afterDesc = frontMatter.slice(frontMatter.indexOf(descMatch[0]) + descMatch[0].length);
|
|
37
|
+
const folded = afterDesc.split('\n')
|
|
38
|
+
.filter((l) => /^\s+\S/.test(l))
|
|
39
|
+
.map((l) => l.trim())
|
|
40
|
+
.join(' ');
|
|
41
|
+
return folded.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function checkSkillDescription(desc, skill, ctx) {
|
|
45
|
+
if (!desc) {
|
|
46
|
+
ctx.fail(`Skill '${skill}' missing front matter 'description:'`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!/^Use when/i.test(desc)) ctx.warn(`Skill '${skill}' description should start with imperative 'Use when...'`);
|
|
50
|
+
if (!/do not use/i.test(desc)) ctx.warn(`Skill '${skill}' description should specify negative boundaries ('Do not use for...')`);
|
|
51
|
+
if (desc.length > 1024) ctx.fail(`Skill '${skill}' description exceeds 1024 chars (${desc.length} chars)`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkSkillBody(checked, ctx) {
|
|
55
|
+
const lines = checked.content.split('\n');
|
|
56
|
+
if (lines.length > 500) ctx.warn(`Skill '${checked.skill}' exceeds 500 lines (${lines.length} lines). Offload details to references/.`);
|
|
57
|
+
else ctx.pass(`Skill '${checked.skill}': ${lines.length} lines, description valid (${checked.desc.length} chars).`);
|
|
58
|
+
if (!/What NOT to do/i.test(checked.content) && !/Gotchas/i.test(checked.content)) {
|
|
59
|
+
ctx.warn(`Skill '${checked.skill}' missing mandatory 'Gotchas & What NOT to Do' section`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkOneSkill(skill, skillsDir, ctx) {
|
|
64
|
+
const skillFile = path.join(skillsDir, skill, 'SKILL.md');
|
|
65
|
+
if (!fs.existsSync(skillFile)) {
|
|
66
|
+
ctx.fail(`Skill '${skill}' missing SKILL.md`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const content = readTextOrFail(skillFile, `Skill '${skill}'`, ctx.fail);
|
|
70
|
+
if (content === null) return;
|
|
71
|
+
const frontMatter = frontMatterBlock(content, skill, ctx.fail);
|
|
72
|
+
if (frontMatter === null) return;
|
|
73
|
+
const desc = extractSkillDescription(frontMatter);
|
|
74
|
+
checkSkillDescription(desc, skill, ctx);
|
|
75
|
+
checkSkillBody({ content, desc, skill }, ctx);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function phaseSkills(ctx) {
|
|
79
|
+
ctx.log('');
|
|
80
|
+
ctx.heading('3. Checking Specialized Skills (.agents/skills)...');
|
|
81
|
+
const skillsDir = path.join(ctx.workspaceRoot, '.agents', 'skills');
|
|
82
|
+
if (!fs.existsSync(skillsDir)) {
|
|
83
|
+
ctx.fail(`Missing .agents/skills directory at ${skillsDir}`);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const folders = readSkillFolders(skillsDir, ctx.fail);
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const skill of folders) {
|
|
89
|
+
count += 1;
|
|
90
|
+
checkOneSkill(skill, skillsDir, ctx);
|
|
91
|
+
}
|
|
92
|
+
ctx.pass(`Validated ${count} skills in .agents/skills/.`);
|
|
93
|
+
return count;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { phaseSkills, checkOneSkill, extractSkillDescription, frontMatterBlock };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Text helpers shared by the validator phases.
|
|
5
|
+
*
|
|
6
|
+
* Both exist because generated documentation routinely contains ADR headings,
|
|
7
|
+
* rule headers, and Core Mandate lines inside examples. Matching the raw file
|
|
8
|
+
* would mistake documentation ABOUT the contract for the contract itself.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function stripHtmlComments(text) {
|
|
12
|
+
// Remove HTML comments, tolerating unclosed ones. A stray `<!--` from an
|
|
13
|
+
// editor's "comment selection" must not silently hide every ADR after it.
|
|
14
|
+
return text
|
|
15
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
16
|
+
.replace(/<!--[\s\S]*$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Removes fenced code blocks so that examples inside documentation are not
|
|
21
|
+
* mistaken for real content. Without this, a rule file whose only H1 and
|
|
22
|
+
* "Core Mandate" lines sit inside a ```md fence satisfies both checks, and a
|
|
23
|
+
* `#### ADR-001` shown as an example is counted as a real decision.
|
|
24
|
+
*/
|
|
25
|
+
function stripFencedCode(text) {
|
|
26
|
+
return text.replace(/^([ \t]*)(```|~~~)[\s\S]*?^\1\2[ \t]*$/gm, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { stripHtmlComments, stripFencedCode };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI entry for the agentic-architecture validator.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately separate from scripts/validate.js: keeping the library free of a
|
|
8
|
+
* `require.main` guard means requiring it never validates or exits, and it
|
|
9
|
+
* avoids a permanently-unreachable branch that blocks the 100% coverage gate.
|
|
10
|
+
*/
|
|
11
|
+
const { main } = require('./validate.js');
|
|
12
|
+
|
|
13
|
+
main();
|
package/scripts/validate.js
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { createReporter, readTextOrFail } = require('./validate/io.js');
|
|
7
|
+
const textHelpers = require('./validate/text.js');
|
|
8
|
+
const parityHelpers = require('./validate/parity.js');
|
|
9
|
+
const ioHelpers = require('./validate/io.js');
|
|
10
|
+
const { parseAdrLedger, checkAdrIndexConsistency, checkGlossaryPopulated } = require('./validate/adr.js');
|
|
11
|
+
const { phaseRootConfig } = require('./validate/root.js');
|
|
12
|
+
const { phaseRules } = require('./validate/rules.js');
|
|
13
|
+
const { phaseSkills } = require('./validate/skills.js');
|
|
14
|
+
const { phaseLinks } = require('./validate/links.js');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* NOTE: this module is a library. It never self-executes, so importing it from
|
|
18
|
+
* a test or another tool cannot trigger validation or call process.exit().
|
|
19
|
+
* The CLI entry point lives in `scripts/validate-cli.js`.
|
|
20
|
+
*/
|
|
6
21
|
|
|
7
22
|
function parseArgs(argv) {
|
|
8
23
|
let root = '';
|
|
@@ -13,234 +28,113 @@ function parseArgs(argv) {
|
|
|
13
28
|
return root || process.cwd();
|
|
14
29
|
}
|
|
15
30
|
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const agentsFile = path.join(workspaceRoot, 'AGENTS.md');
|
|
30
|
-
if (!fs.existsSync(agentsFile)) {
|
|
31
|
-
fail(`Missing root AGENTS.md at ${agentsFile}`);
|
|
32
|
-
} else {
|
|
33
|
-
pass('AGENTS.md exists.');
|
|
34
|
-
const lines = fs.readFileSync(agentsFile, 'utf-8').split('\n').length;
|
|
35
|
-
if (lines <= 120) pass(`AGENTS.md line count is lean: ${lines} lines (<= 120).`);
|
|
36
|
-
else if (lines <= 150) warn(`AGENTS.md line count is getting large: ${lines} lines (warn > 120).`);
|
|
37
|
-
else fail(`AGENTS.md exceeds maximum line limit: ${lines} lines (max 150).`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const agentsContent = fs.existsSync(agentsFile) ? fs.readFileSync(agentsFile, 'utf-8') : '';
|
|
41
|
-
|
|
42
|
-
function isValidAgentsTarget(target) {
|
|
43
|
-
return target === 'AGENTS.md' || target === './AGENTS.md' || target === agentsFile;
|
|
31
|
+
function reportMemoryLedger(mem, ctx) {
|
|
32
|
+
const { entryIds } = parseAdrLedger(mem);
|
|
33
|
+
if (!/^# /m.test(mem)) ctx.fail('memory.md missing H1 header');
|
|
34
|
+
// Uses the same parser as phase 6. The previous unanchored
|
|
35
|
+
// `/#### ADR-/` disagreed with phase 6's anchored form, so the validator
|
|
36
|
+
// contradicted itself within a single run.
|
|
37
|
+
else if (entryIds.length > 0) ctx.pass('memory.md contains ADR entries with valid envelope.');
|
|
38
|
+
else ctx.pass('memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build).');
|
|
39
|
+
// Template ADRs start well past ADR-024. Match any ID >= 25 regardless of
|
|
40
|
+
// zero padding.
|
|
41
|
+
const highestAdr = Math.max(0, ...entryIds);
|
|
42
|
+
if (highestAdr >= 25 && /azcodr/i.test(mem)) {
|
|
43
|
+
ctx.warn('memory.md may contain template-internal ADRs; fresh projects must start at ADR-001.');
|
|
44
44
|
}
|
|
45
|
+
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
? (target === '../AGENTS.md' || target === agentsFile || target === 'AGENTS.md')
|
|
54
|
-
: isValidAgentsTarget(target);
|
|
55
|
-
if (ok) pass(`${label} is a valid symlink to AGENTS.md.`);
|
|
56
|
-
else fail(`${label} points to '${target}' instead of 'AGENTS.md'.`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (stat.isFile()) {
|
|
60
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
61
|
-
const trimmed = content.trim();
|
|
62
|
-
if (trimmed === 'AGENTS.md' || trimmed === './AGENTS.md' || trimmed === agentsFile) {
|
|
63
|
-
pass(`${label} is a text pointer to AGENTS.md (symlink fallback).`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (label.endsWith('copilot-instructions.md') && content.includes('AGENTS.md')) {
|
|
67
|
-
pass(`${label} references AGENTS.md (symlink fallback).`);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (allowCopyFallback && agentsContent && content === agentsContent) {
|
|
71
|
-
warn(`${label} is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk).`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (label === 'agents.md') {
|
|
75
|
-
try {
|
|
76
|
-
const entries = fs.readdirSync(workspaceRoot);
|
|
77
|
-
if (!entries.includes('agents.md') && entries.includes('AGENTS.md')) {
|
|
78
|
-
pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
} catch { /* fall through */ }
|
|
82
|
-
}
|
|
83
|
-
fail(`${label} is not a symbolic link.`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
fail(`${label} is neither a symlink nor a regular file.`);
|
|
47
|
+
function phaseMemory(ctx) {
|
|
48
|
+
ctx.log('');
|
|
49
|
+
ctx.heading('5. Checking Memory & ADR Ledger...');
|
|
50
|
+
const memoryFile = path.join(ctx.workspaceRoot, 'memory.md');
|
|
51
|
+
if (!fs.existsSync(memoryFile)) {
|
|
52
|
+
ctx.fail('Missing memory.md ADR ledger.');
|
|
53
|
+
return;
|
|
87
54
|
}
|
|
55
|
+
// readTextOrFail already reported any read failure; skip the analysis.
|
|
56
|
+
const mem = readTextOrFail(memoryFile, 'memory.md', ctx.fail);
|
|
57
|
+
if (mem !== null) reportMemoryLedger(mem, ctx);
|
|
58
|
+
}
|
|
88
59
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (hasUpper && !hasLowerExact) {
|
|
96
|
-
let appearsSame = false;
|
|
97
|
-
try { appearsSame = fs.existsSync(lowerPath); } catch { appearsSame = false; }
|
|
98
|
-
if (appearsSame) {
|
|
99
|
-
pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
|
|
100
|
-
} else {
|
|
101
|
-
checkParity(lowerPath, 'agents.md', true);
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
checkParity(lowerPath, 'agents.md', true);
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
checkParity(lowerPath, 'agents.md', true);
|
|
60
|
+
function phaseLedger(ctx) {
|
|
61
|
+
ctx.log('');
|
|
62
|
+
ctx.heading('6. Checking ADR Index Consistency...');
|
|
63
|
+
const consistency = checkAdrIndexConsistency(ctx.workspaceRoot, ctx.fail, ctx.pass);
|
|
64
|
+
if (consistency === 'indexed' || consistency === 'inconsistent') {
|
|
65
|
+
checkGlossaryPopulated(ctx.workspaceRoot, { fail: ctx.fail, pass: ctx.pass, warn: ctx.warn });
|
|
108
66
|
}
|
|
109
|
-
|
|
110
|
-
checkParity(path.join(workspaceRoot, '.cursorrules'), '.cursorrules', true);
|
|
111
|
-
checkParity(path.join(workspaceRoot, '.windsurfrules'), '.windsurfrules', true);
|
|
112
|
-
const githubDir = path.join(workspaceRoot, '.github');
|
|
113
|
-
if (fs.existsSync(githubDir) && fs.statSync(githubDir).isDirectory()) {
|
|
114
|
-
const copilot = path.join(githubDir, 'copilot-instructions.md');
|
|
115
|
-
if (fs.existsSync(copilot)) {
|
|
116
|
-
checkParity(copilot, '.github/copilot-instructions.md', true);
|
|
117
|
-
} else {
|
|
118
|
-
warn('.github/copilot-instructions.md is missing (run scaffold to restore harness parity).');
|
|
119
|
-
}
|
|
120
|
-
if (!fs.existsSync(path.join(githubDir, 'workflows'))) {
|
|
121
|
-
warn('.github/workflows is missing (CI will not run in scaffolded projects).');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (fs.existsSync(path.join(workspaceRoot, '.gitignore'))) pass('.gitignore exists.');
|
|
126
|
-
else fail('Missing .gitignore');
|
|
67
|
+
}
|
|
127
68
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (stat.size > 24000) warn(`Rule ${name} exceeds 24KB token-economy cap (${stat.size} bytes)`);
|
|
144
|
-
}
|
|
145
|
-
pass(`Validated ${count} modular rule files in docs/rules/.`);
|
|
146
|
-
}
|
|
69
|
+
function buildValidationContext(workspaceRoot, reporter, counts) {
|
|
70
|
+
return {
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
agentsFile: '',
|
|
73
|
+
agentsContent: '',
|
|
74
|
+
pass: reporter.pass,
|
|
75
|
+
warn: (msg) => { counts.warnings += 1; reporter.warn(msg); },
|
|
76
|
+
fail: (msg) => { counts.errors += 1; reporter.fail(msg); },
|
|
77
|
+
log: reporter.log,
|
|
78
|
+
heading: reporter.heading,
|
|
79
|
+
broken: [],
|
|
80
|
+
linkCount: 0,
|
|
81
|
+
visitedRealPaths: new Set()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
147
84
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
85
|
+
function reportSummary(counts, reporter) {
|
|
86
|
+
reporter.log('');
|
|
87
|
+
reporter.log('--------------------------------------------------------------');
|
|
88
|
+
if (counts.errors === 0) {
|
|
89
|
+
reporter.log(`🎉 SUCCESS: All agentic configurations are valid and healthy! (${counts.warnings} warnings)`);
|
|
153
90
|
} else {
|
|
154
|
-
|
|
155
|
-
let count = 0;
|
|
156
|
-
for (const skill of folders) {
|
|
157
|
-
count += 1;
|
|
158
|
-
const skillFile = path.join(skillsDir, skill, 'SKILL.md');
|
|
159
|
-
if (!fs.existsSync(skillFile)) { fail(`Skill '${skill}' missing SKILL.md`); continue; }
|
|
160
|
-
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
161
|
-
const lines = content.split('\n');
|
|
162
|
-
if (lines[0].trim() !== '---') { fail(`Skill '${skill}' missing opening front matter delimiter (---)`); continue; }
|
|
163
|
-
const closing = lines.slice(1).findIndex((l) => l.trim() === '---');
|
|
164
|
-
if (closing === -1) fail(`Skill '${skill}' missing closing front matter delimiter (---)`);
|
|
165
|
-
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
166
|
-
if (!nameMatch || nameMatch[1].trim() !== skill) fail(`Skill '${skill}' front matter 'name:' does not match directory name`);
|
|
167
|
-
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
168
|
-
const desc = descMatch ? descMatch[1].trim() : '';
|
|
169
|
-
if (!desc) {
|
|
170
|
-
fail(`Skill '${skill}' missing front matter 'description:'`);
|
|
171
|
-
} else {
|
|
172
|
-
if (!/^Use when/.test(desc)) warn(`Skill '${skill}' description should start with imperative 'Use when...'`);
|
|
173
|
-
if (!/do not use/i.test(desc)) warn(`Skill '${skill}' description should specify negative boundaries ('Do not use for...')`);
|
|
174
|
-
if (desc.length > 1024) fail(`Skill '${skill}' description exceeds 1024 chars (${desc.length} chars)`);
|
|
175
|
-
}
|
|
176
|
-
if (lines.length > 500) warn(`Skill '${skill}' exceeds 500 lines (${lines.length} lines). Offload details to references/.`);
|
|
177
|
-
else pass(`Skill '${skill}': ${lines.length} lines, description valid (${desc.length} chars).`);
|
|
178
|
-
if (!/What NOT to do/i.test(content) && !/Gotchas/i.test(content)) {
|
|
179
|
-
warn(`Skill '${skill}' missing mandatory 'Gotchas & What NOT to Do' section`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
pass(`Validated ${count} skills in .agents/skills/.`);
|
|
91
|
+
reporter.log(`🚨 FAILURE: Found ${counts.errors} error(s) and ${counts.warnings} warning(s) in agentic configurations.`);
|
|
183
92
|
}
|
|
93
|
+
}
|
|
184
94
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
totalLinks += 1;
|
|
210
|
-
if (!fs.existsSync(path.normalize(path.join(dir, clean)))) {
|
|
211
|
-
broken.push(`${path.relative(workspaceRoot, filePath)} -> ${target}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
walk(workspaceRoot);
|
|
216
|
-
if (broken.length === 0) pass(`Validated ${totalLinks} internal links across workspace (0 broken links).`);
|
|
217
|
-
else for (const b of broken) fail(`Broken markdown link: ${b}`);
|
|
218
|
-
|
|
219
|
-
console.log('');
|
|
220
|
-
console.log('5. Checking Memory & ADR Ledger...');
|
|
221
|
-
const memoryFile = path.join(workspaceRoot, 'memory.md');
|
|
222
|
-
if (!fs.existsSync(memoryFile)) {
|
|
223
|
-
fail('Missing memory.md ADR ledger.');
|
|
224
|
-
} else {
|
|
225
|
-
const mem = fs.readFileSync(memoryFile, 'utf-8');
|
|
226
|
-
const withoutComments = mem.replace(/<!--[\s\S]*?-->/g, '');
|
|
227
|
-
if (!/^# /m.test(mem)) fail('memory.md missing H1 header');
|
|
228
|
-
else if (/#### ADR-/.test(withoutComments)) pass('memory.md contains ADR entries with valid envelope.');
|
|
229
|
-
else pass('memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build).');
|
|
230
|
-
if (/ADR-025|ADR-028/.test(withoutComments) && /azcodr/i.test(mem)) {
|
|
231
|
-
warn('memory.md may contain template-internal ADRs; fresh projects must start at ADR-001.');
|
|
232
|
-
}
|
|
233
|
-
}
|
|
95
|
+
function runValidation(workspaceRoot, reporter = createReporter()) {
|
|
96
|
+
// Counters live here, not on the reporter: an injected reporter only needs
|
|
97
|
+
// pass/warn/fail to observe outcomes, and the tally must not depend on the
|
|
98
|
+
// reporter's shape.
|
|
99
|
+
const counts = { errors: 0, warnings: 0 };
|
|
100
|
+
const ctx = buildValidationContext(workspaceRoot, reporter, counts);
|
|
101
|
+
reporter.heading(`🔍 Validating Agentic Architecture in: ${workspaceRoot}`);
|
|
102
|
+
reporter.log('--------------------------------------------------------------');
|
|
103
|
+
phaseRootConfig(ctx);
|
|
104
|
+
const validatedRules = phaseRules(ctx);
|
|
105
|
+
const validatedSkills = phaseSkills(ctx);
|
|
106
|
+
phaseLinks(ctx);
|
|
107
|
+
phaseMemory(ctx);
|
|
108
|
+
phaseLedger(ctx);
|
|
109
|
+
reportSummary(counts, reporter);
|
|
110
|
+
return {
|
|
111
|
+
errors: counts.errors,
|
|
112
|
+
warnings: counts.warnings,
|
|
113
|
+
validatedRules,
|
|
114
|
+
validatedSkills,
|
|
115
|
+
totalLinks: ctx.linkCount,
|
|
116
|
+
brokenLinks: ctx.broken
|
|
117
|
+
};
|
|
118
|
+
}
|
|
234
119
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
process.exit(0);
|
|
240
|
-
} else {
|
|
241
|
-
console.log(`🚨 FAILURE: Found ${errors} error(s) and ${warnings} warning(s) in agentic configurations.`);
|
|
242
|
-
process.exit(1);
|
|
243
|
-
}
|
|
120
|
+
function main(argv = process.argv.slice(2), exit = process.exit) {
|
|
121
|
+
const workspaceRoot = path.resolve(parseArgs(argv));
|
|
122
|
+
const result = runValidation(workspaceRoot);
|
|
123
|
+
return exit(result.errors === 0 ? 0 : 1);
|
|
244
124
|
}
|
|
245
125
|
|
|
246
|
-
|
|
126
|
+
module.exports = {
|
|
127
|
+
runValidation,
|
|
128
|
+
parseArgs,
|
|
129
|
+
createReporter,
|
|
130
|
+
main,
|
|
131
|
+
parseAdrLedger,
|
|
132
|
+
checkAdrIndexConsistency,
|
|
133
|
+
checkGlossaryPopulated,
|
|
134
|
+
evaluateParityTarget: parityHelpers.evaluateParityTarget,
|
|
135
|
+
isValidAgentsTarget: parityHelpers.isValidAgentsTarget,
|
|
136
|
+
createLowercaseParityLink: parityHelpers.createLowercaseParityLink,
|
|
137
|
+
stripHtmlComments: textHelpers.stripHtmlComments,
|
|
138
|
+
stripFencedCode: textHelpers.stripFencedCode,
|
|
139
|
+
walkMarkdown: ioHelpers.walkMarkdown
|
|
140
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
../AGENTS.md
|