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.
Files changed (91) hide show
  1. package/.agents/hooks.json.example +42 -42
  2. package/.agents/scripts/safety_guard.sh +143 -34
  3. package/.agents/scripts/verify_completion.sh +90 -27
  4. package/.agents/skills/agentic-architect/SKILL.md +125 -125
  5. package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
  6. package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
  7. package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
  8. package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
  9. package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -401
  10. package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
  11. package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
  12. package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
  13. package/.agents/skills/compliance-audit/SKILL.md +120 -120
  14. package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
  15. package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
  16. package/.agents/skills/lets-build/SKILL.md +173 -173
  17. package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
  18. package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
  19. package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
  20. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
  21. package/.agents/skills/product-analyst/SKILL.md +154 -154
  22. package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
  23. package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
  24. package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
  25. package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
  26. package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
  27. package/.agents/skills/relentless-questioner/SKILL.md +128 -128
  28. package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
  29. package/.editorconfig +19 -19
  30. package/.github/workflows/ci.yml +167 -56
  31. package/.github/workflows/publish.yml +200 -0
  32. package/.gitignore +40 -25
  33. package/AGENTS.md +103 -102
  34. package/LICENSE +21 -21
  35. package/README.md +168 -154
  36. package/bin/azcodr.js +14 -228
  37. package/docs/knowledge/ubiquitous_language.md +31 -18
  38. package/docs/rules/agentic_configuration.md +259 -259
  39. package/docs/rules/api_architecture.md +179 -179
  40. package/docs/rules/authentication.md +76 -76
  41. package/docs/rules/authorization.md +75 -75
  42. package/docs/rules/caching.md +69 -69
  43. package/docs/rules/clean_code.md +62 -62
  44. package/docs/rules/cloud_native.md +41 -41
  45. package/docs/rules/cqrs.md +203 -203
  46. package/docs/rules/database_design.md +125 -125
  47. package/docs/rules/database_operations.md +69 -69
  48. package/docs/rules/design_patterns.md +98 -98
  49. package/docs/rules/devops_ci_cd.md +76 -76
  50. package/docs/rules/domain_driven_design.md +122 -122
  51. package/docs/rules/error_handling.md +54 -52
  52. package/docs/rules/feature_flags.md +59 -59
  53. package/docs/rules/frontend_architecture.md +157 -157
  54. package/docs/rules/multitenancy_architecture.md +98 -98
  55. package/docs/rules/product_ownership.md +127 -127
  56. package/docs/rules/project_management.md +49 -49
  57. package/docs/rules/relentless_questioning.md +52 -52
  58. package/docs/rules/requirements_engineering.md +98 -98
  59. package/docs/rules/security_compliance.md +53 -53
  60. package/docs/rules/server_driven_ui.md +88 -88
  61. package/docs/rules/test_driven_development.md +185 -185
  62. package/docs/rules/transactional_email.md +27 -27
  63. package/docs/rules/type_safety.md +65 -65
  64. package/docs/rules/ui_ux_architecture.md +150 -150
  65. package/docs/rules/workflow_state_machines.md +117 -117
  66. package/lib/cli-parse.js +51 -0
  67. package/lib/cli-target.js +109 -0
  68. package/lib/cli.js +180 -0
  69. package/lib/errors.js +28 -0
  70. package/lib/git.js +29 -0
  71. package/lib/guards.js +96 -0
  72. package/lib/index.d.ts +199 -134
  73. package/lib/index.js +5 -5
  74. package/lib/links.js +123 -0
  75. package/lib/permissions.js +44 -0
  76. package/lib/repo.js +90 -0
  77. package/lib/scaffold.js +238 -399
  78. package/memory.md +119 -36
  79. package/package.json +65 -62
  80. package/scripts/test_coverage.js +66 -38
  81. package/scripts/validate/adr.js +151 -0
  82. package/scripts/validate/io.js +84 -0
  83. package/scripts/validate/links.js +167 -0
  84. package/scripts/validate/parity.js +124 -0
  85. package/scripts/validate/root.js +184 -0
  86. package/scripts/validate/rules.js +44 -0
  87. package/scripts/validate/skills.js +96 -0
  88. package/scripts/validate/text.js +29 -0
  89. package/scripts/validate-cli.js +13 -0
  90. package/scripts/validate.js +112 -218
  91. 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();
@@ -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 main() {
17
- const workspaceRoot = path.resolve(parseArgs(process.argv.slice(2)));
18
- let errors = 0;
19
- let warnings = 0;
20
-
21
- const pass = (msg) => console.log(` ✅ ${msg}`);
22
- const warn = (msg) => { warnings += 1; console.log(` ⚠️ ${msg}`); };
23
- const fail = (msg) => { errors += 1; console.log(` ❌ ${msg}`); };
24
-
25
- console.log(`🔍 Validating Agentic Architecture in: ${workspaceRoot}`);
26
- console.log('--------------------------------------------------------------');
27
-
28
- console.log('1. Checking Root Configuration & Symlinks...');
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
- function checkParity(filePath, label, allowCopyFallback) {
47
- let stat = null;
48
- try { stat = fs.lstatSync(filePath); } catch { fail(`${label} is missing.`); return; }
49
- if (stat.isSymbolicLink()) {
50
- let target = '';
51
- try { target = fs.readlinkSync(filePath); } catch { fail(`${label} readlink failed.`); return; }
52
- const ok = label.endsWith('copilot-instructions.md')
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
- checkParity(path.join(workspaceRoot, 'CLAUDE.md'), 'CLAUDE.md', true);
90
- const lowerPath = path.join(workspaceRoot, 'agents.md');
91
- try {
92
- const entries = fs.readdirSync(workspaceRoot);
93
- const hasUpper = entries.includes('AGENTS.md');
94
- const hasLowerExact = entries.includes('agents.md');
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
- checkParity(path.join(workspaceRoot, 'GEMINI.md'), 'GEMINI.md', true);
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
- console.log('');
129
- console.log('2. Checking Progressive Disclosure Rules...');
130
- const rulesDir = path.join(workspaceRoot, 'docs', 'rules');
131
- if (!fs.existsSync(rulesDir)) {
132
- fail(`Missing docs/rules directory at ${rulesDir}`);
133
- } else {
134
- const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md'));
135
- let count = 0;
136
- for (const name of files) {
137
- count += 1;
138
- const fp = path.join(rulesDir, name);
139
- const content = fs.readFileSync(fp, 'utf-8');
140
- const stat = fs.statSync(fp);
141
- if (!/^# /m.test(content)) fail(`Rule ${name} missing H1 header (# Title)`);
142
- if (!/^> \*\*Core Mandate:\*\*/m.test(content)) warn(`Rule ${name} missing standardized '> **Core Mandate:**' summary`);
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
- console.log('');
149
- console.log('3. Checking Specialized Skills (.agents/skills)...');
150
- const skillsDir = path.join(workspaceRoot, '.agents', 'skills');
151
- if (!fs.existsSync(skillsDir)) {
152
- fail(`Missing .agents/skills directory at ${skillsDir}`);
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
- const folders = fs.readdirSync(skillsDir).filter((f) => fs.statSync(path.join(skillsDir, f)).isDirectory());
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
- console.log('');
186
- console.log('4. Checking Markdown Internal Links & Cross-References...');
187
- const broken = [];
188
- let totalLinks = 0;
189
- function walk(dir) {
190
- let entries = [];
191
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
192
- for (const entry of entries) {
193
- if (entry.name === '.git' || entry.name === 'node_modules') continue;
194
- const full = path.join(dir, entry.name);
195
- if (entry.isDirectory()) walk(full);
196
- else if (entry.isFile() && entry.name.endsWith('.md')) checkFile(full);
197
- }
198
- }
199
- function checkFile(filePath) {
200
- const content = fs.readFileSync(filePath, 'utf-8');
201
- const dir = path.dirname(filePath);
202
- const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
203
- let m;
204
- while ((m = regex.exec(content)) !== null) {
205
- const target = m[2].trim();
206
- if (/^(https?:|mailto:|#|conversation:\/\/|file:\/\/)/.test(target)) continue;
207
- const clean = target.split('#')[0];
208
- if (!clean) continue;
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
- console.log('');
236
- console.log('--------------------------------------------------------------');
237
- if (errors === 0) {
238
- console.log(`🎉 SUCCESS: All agentic configurations are valid and healthy! (${warnings} warnings)`);
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
- main();
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