azcodr 1.5.2 → 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 (93) hide show
  1. package/.agents/hooks.json +42 -42
  2. package/.agents/hooks.json.example +42 -42
  3. package/.agents/mcp_config.json.example +29 -29
  4. package/.agents/scripts/safety_guard.sh +143 -34
  5. package/.agents/scripts/verify_completion.sh +90 -27
  6. package/.agents/skills/agentic-architect/SKILL.md +125 -125
  7. package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
  8. package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
  9. package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
  10. package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
  11. package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -402
  12. package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
  13. package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
  14. package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
  15. package/.agents/skills/compliance-audit/SKILL.md +120 -120
  16. package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
  17. package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
  18. package/.agents/skills/lets-build/SKILL.md +173 -173
  19. package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
  20. package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
  21. package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
  22. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
  23. package/.agents/skills/product-analyst/SKILL.md +154 -154
  24. package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
  25. package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
  26. package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
  27. package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
  28. package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
  29. package/.agents/skills/relentless-questioner/SKILL.md +128 -128
  30. package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
  31. package/.editorconfig +19 -19
  32. package/.github/workflows/ci.yml +167 -78
  33. package/.github/workflows/publish.yml +200 -0
  34. package/.gitignore +40 -25
  35. package/AGENTS.md +103 -102
  36. package/LICENSE +21 -21
  37. package/README.md +168 -165
  38. package/bin/azcodr.js +14 -228
  39. package/docs/knowledge/ubiquitous_language.md +31 -18
  40. package/docs/rules/agentic_configuration.md +259 -259
  41. package/docs/rules/api_architecture.md +179 -179
  42. package/docs/rules/authentication.md +76 -76
  43. package/docs/rules/authorization.md +75 -75
  44. package/docs/rules/caching.md +69 -69
  45. package/docs/rules/clean_code.md +62 -62
  46. package/docs/rules/cloud_native.md +41 -41
  47. package/docs/rules/cqrs.md +203 -203
  48. package/docs/rules/database_design.md +125 -125
  49. package/docs/rules/database_operations.md +69 -69
  50. package/docs/rules/design_patterns.md +98 -98
  51. package/docs/rules/devops_ci_cd.md +76 -76
  52. package/docs/rules/domain_driven_design.md +122 -122
  53. package/docs/rules/error_handling.md +54 -52
  54. package/docs/rules/feature_flags.md +59 -59
  55. package/docs/rules/frontend_architecture.md +157 -157
  56. package/docs/rules/multitenancy_architecture.md +98 -98
  57. package/docs/rules/product_ownership.md +127 -127
  58. package/docs/rules/project_management.md +49 -49
  59. package/docs/rules/relentless_questioning.md +52 -52
  60. package/docs/rules/requirements_engineering.md +98 -98
  61. package/docs/rules/security_compliance.md +53 -53
  62. package/docs/rules/server_driven_ui.md +88 -88
  63. package/docs/rules/test_driven_development.md +185 -185
  64. package/docs/rules/transactional_email.md +27 -27
  65. package/docs/rules/type_safety.md +65 -65
  66. package/docs/rules/ui_ux_architecture.md +150 -150
  67. package/docs/rules/workflow_state_machines.md +117 -117
  68. package/lib/cli-parse.js +51 -0
  69. package/lib/cli-target.js +109 -0
  70. package/lib/cli.js +180 -0
  71. package/lib/errors.js +28 -0
  72. package/lib/git.js +29 -0
  73. package/lib/guards.js +96 -0
  74. package/lib/index.d.ts +199 -134
  75. package/lib/index.js +5 -5
  76. package/lib/links.js +123 -0
  77. package/lib/permissions.js +44 -0
  78. package/lib/repo.js +90 -0
  79. package/lib/scaffold.js +238 -448
  80. package/memory.md +119 -36
  81. package/package.json +65 -62
  82. package/scripts/test_coverage.js +66 -38
  83. package/scripts/validate/adr.js +151 -0
  84. package/scripts/validate/io.js +84 -0
  85. package/scripts/validate/links.js +167 -0
  86. package/scripts/validate/parity.js +124 -0
  87. package/scripts/validate/root.js +184 -0
  88. package/scripts/validate/rules.js +44 -0
  89. package/scripts/validate/skills.js +96 -0
  90. package/scripts/validate/text.js +29 -0
  91. package/scripts/validate-cli.js +13 -0
  92. package/scripts/validate.js +140 -258
  93. 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();
@@ -1,258 +1,140 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- function parseArgs(argv) {
8
- let root = '';
9
- for (const arg of argv) {
10
- if (arg === '--fix') continue;
11
- if (!root) root = arg;
12
- }
13
- return root || process.cwd();
14
- }
15
-
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
- const normalized = target.replace(/\\/g, '/');
44
- const rootNormalized = agentsFile.replace(/\\/g, '/');
45
- return normalized === 'AGENTS.md' || normalized === './AGENTS.md' || normalized === rootNormalized;
46
- }
47
-
48
- function checkParity(filePath, label, allowCopyFallback) {
49
- let stat = null;
50
- try { stat = fs.lstatSync(filePath); } catch { fail(`${label} is missing.`); return; }
51
- if (stat.isSymbolicLink()) {
52
- let target = '';
53
- try { target = fs.readlinkSync(filePath); } catch { fail(`${label} readlink failed.`); return; }
54
- const normalized = target.replace(/\\/g, '/');
55
- const ok = label.endsWith('copilot-instructions.md')
56
- ? (normalized === '../AGENTS.md' || normalized === agentsFile.replace(/\\/g, '/') || normalized === 'AGENTS.md')
57
- : isValidAgentsTarget(target);
58
- if (ok) pass(`${label} is a valid symlink to AGENTS.md.`);
59
- else fail(`${label} points to '${target}' instead of 'AGENTS.md'.`);
60
- return;
61
- }
62
- if (stat.isFile()) {
63
- const content = fs.readFileSync(filePath, 'utf-8');
64
- const trimmed = content.trim();
65
- if (trimmed === 'AGENTS.md' || trimmed === './AGENTS.md' || trimmed === agentsFile) {
66
- pass(`${label} is a text pointer to AGENTS.md (symlink fallback).`);
67
- return;
68
- }
69
- if (label.endsWith('copilot-instructions.md') && content.includes('AGENTS.md')) {
70
- pass(`${label} references AGENTS.md (symlink fallback).`);
71
- return;
72
- }
73
- if (allowCopyFallback && agentsContent && content === agentsContent) {
74
- warn(`${label} is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk).`);
75
- return;
76
- }
77
- if (label === 'agents.md') {
78
- try {
79
- const entries = fs.readdirSync(workspaceRoot);
80
- if (!entries.includes('agents.md') && entries.includes('AGENTS.md')) {
81
- pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
82
- return;
83
- }
84
- } catch { /* fall through */ }
85
- }
86
- fail(`${label} is not a symbolic link.`);
87
- return;
88
- }
89
- fail(`${label} is neither a symlink nor a regular file.`);
90
- }
91
-
92
- checkParity(path.join(workspaceRoot, 'CLAUDE.md'), 'CLAUDE.md', true);
93
- const lowerPath = path.join(workspaceRoot, 'agents.md');
94
- try {
95
- const entries = fs.readdirSync(workspaceRoot);
96
- const hasUpper = entries.includes('AGENTS.md');
97
- const hasLowerExact = entries.includes('agents.md');
98
- if (hasUpper && !hasLowerExact) {
99
- let appearsSame = false;
100
- try { appearsSame = fs.existsSync(lowerPath); } catch { appearsSame = false; }
101
- if (appearsSame) {
102
- pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
103
- } else {
104
- // Case-sensitive filesystem gap (agents.md cannot be committed alongside
105
- // AGENTS.md from case-insensitive systems): restore parity in place.
106
- let created = false;
107
- try { fs.symlinkSync('AGENTS.md', lowerPath); created = true; }
108
- catch {
109
- try { fs.writeFileSync(lowerPath, 'AGENTS.md\n', 'utf-8'); created = true; }
110
- catch { created = false; }
111
- }
112
- if (created) pass('Created agents.md parity link to AGENTS.md (case-sensitive filesystem).');
113
- checkParity(lowerPath, 'agents.md', true);
114
- }
115
- } else {
116
- checkParity(lowerPath, 'agents.md', true);
117
- }
118
- } catch {
119
- checkParity(lowerPath, 'agents.md', true);
120
- }
121
- checkParity(path.join(workspaceRoot, 'GEMINI.md'), 'GEMINI.md', true);
122
- checkParity(path.join(workspaceRoot, '.cursorrules'), '.cursorrules', true);
123
- checkParity(path.join(workspaceRoot, '.windsurfrules'), '.windsurfrules', true);
124
- const githubDir = path.join(workspaceRoot, '.github');
125
- if (fs.existsSync(githubDir) && fs.statSync(githubDir).isDirectory()) {
126
- const copilot = path.join(githubDir, 'copilot-instructions.md');
127
- if (fs.existsSync(copilot)) {
128
- checkParity(copilot, '.github/copilot-instructions.md', true);
129
- } else {
130
- warn('.github/copilot-instructions.md is missing (run scaffold to restore harness parity).');
131
- }
132
- if (!fs.existsSync(path.join(githubDir, 'workflows'))) {
133
- warn('.github/workflows is missing (CI will not run in scaffolded projects).');
134
- }
135
- }
136
-
137
- if (fs.existsSync(path.join(workspaceRoot, '.gitignore'))) pass('.gitignore exists.');
138
- else fail('Missing .gitignore');
139
-
140
- console.log('');
141
- console.log('2. Checking Progressive Disclosure Rules...');
142
- const rulesDir = path.join(workspaceRoot, 'docs', 'rules');
143
- if (!fs.existsSync(rulesDir)) {
144
- fail(`Missing docs/rules directory at ${rulesDir}`);
145
- } else {
146
- const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md'));
147
- let count = 0;
148
- for (const name of files) {
149
- count += 1;
150
- const fp = path.join(rulesDir, name);
151
- const content = fs.readFileSync(fp, 'utf-8');
152
- const stat = fs.statSync(fp);
153
- if (!/^# /m.test(content)) fail(`Rule ${name} missing H1 header (# Title)`);
154
- if (!/^> \*\*Core Mandate:\*\*/m.test(content)) warn(`Rule ${name} missing standardized '> **Core Mandate:**' summary`);
155
- if (stat.size > 24000) warn(`Rule ${name} exceeds 24KB token-economy cap (${stat.size} bytes)`);
156
- }
157
- pass(`Validated ${count} modular rule files in docs/rules/.`);
158
- }
159
-
160
- console.log('');
161
- console.log('3. Checking Specialized Skills (.agents/skills)...');
162
- const skillsDir = path.join(workspaceRoot, '.agents', 'skills');
163
- if (!fs.existsSync(skillsDir)) {
164
- fail(`Missing .agents/skills directory at ${skillsDir}`);
165
- } else {
166
- const folders = fs.readdirSync(skillsDir).filter((f) => fs.statSync(path.join(skillsDir, f)).isDirectory());
167
- let count = 0;
168
- for (const skill of folders) {
169
- count += 1;
170
- const skillFile = path.join(skillsDir, skill, 'SKILL.md');
171
- if (!fs.existsSync(skillFile)) { fail(`Skill '${skill}' missing SKILL.md`); continue; }
172
- const content = fs.readFileSync(skillFile, 'utf-8');
173
- const lines = content.split('\n');
174
- if (lines[0].trim() !== '---') { fail(`Skill '${skill}' missing opening front matter delimiter (---)`); continue; }
175
- const closing = lines.slice(1).findIndex((l) => l.trim() === '---');
176
- if (closing === -1) fail(`Skill '${skill}' missing closing front matter delimiter (---)`);
177
- const nameMatch = content.match(/^name:\s*(.+)$/m);
178
- if (!nameMatch || nameMatch[1].trim() !== skill) fail(`Skill '${skill}' front matter 'name:' does not match directory name`);
179
- const descMatch = content.match(/^description:\s*(.+)$/m);
180
- const desc = descMatch ? descMatch[1].trim() : '';
181
- if (!desc) {
182
- fail(`Skill '${skill}' missing front matter 'description:'`);
183
- } else {
184
- if (!/^Use when/.test(desc)) warn(`Skill '${skill}' description should start with imperative 'Use when...'`);
185
- if (!/do not use/i.test(desc)) warn(`Skill '${skill}' description should specify negative boundaries ('Do not use for...')`);
186
- if (desc.length > 1024) fail(`Skill '${skill}' description exceeds 1024 chars (${desc.length} chars)`);
187
- }
188
- if (lines.length > 500) warn(`Skill '${skill}' exceeds 500 lines (${lines.length} lines). Offload details to references/.`);
189
- else pass(`Skill '${skill}': ${lines.length} lines, description valid (${desc.length} chars).`);
190
- if (!/What NOT to do/i.test(content) && !/Gotchas/i.test(content)) {
191
- warn(`Skill '${skill}' missing mandatory 'Gotchas & What NOT to Do' section`);
192
- }
193
- }
194
- pass(`Validated ${count} skills in .agents/skills/.`);
195
- }
196
-
197
- console.log('');
198
- console.log('4. Checking Markdown Internal Links & Cross-References...');
199
- const broken = [];
200
- let totalLinks = 0;
201
- function walk(dir) {
202
- let entries = [];
203
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
204
- for (const entry of entries) {
205
- if (entry.name === '.git' || entry.name === 'node_modules') continue;
206
- const full = path.join(dir, entry.name);
207
- if (entry.isDirectory()) walk(full);
208
- else if (entry.isFile() && entry.name.endsWith('.md')) checkFile(full);
209
- }
210
- }
211
- function checkFile(filePath) {
212
- const content = fs.readFileSync(filePath, 'utf-8');
213
- const dir = path.dirname(filePath);
214
- const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
215
- let m;
216
- while ((m = regex.exec(content)) !== null) {
217
- const target = m[2].trim();
218
- if (/^(https?:|mailto:|#|conversation:\/\/|file:\/\/)/.test(target)) continue;
219
- const clean = target.split('#')[0];
220
- if (!clean) continue;
221
- totalLinks += 1;
222
- if (!fs.existsSync(path.normalize(path.join(dir, clean)))) {
223
- broken.push(`${path.relative(workspaceRoot, filePath)} -> ${target}`);
224
- }
225
- }
226
- }
227
- walk(workspaceRoot);
228
- if (broken.length === 0) pass(`Validated ${totalLinks} internal links across workspace (0 broken links).`);
229
- else for (const b of broken) fail(`Broken markdown link: ${b}`);
230
-
231
- console.log('');
232
- console.log('5. Checking Memory & ADR Ledger...');
233
- const memoryFile = path.join(workspaceRoot, 'memory.md');
234
- if (!fs.existsSync(memoryFile)) {
235
- fail('Missing memory.md ADR ledger.');
236
- } else {
237
- const mem = fs.readFileSync(memoryFile, 'utf-8');
238
- const withoutComments = mem.replace(/<!--[\s\S]*?-->/g, '');
239
- if (!/^# /m.test(mem)) fail('memory.md missing H1 header');
240
- else if (/#### ADR-/.test(withoutComments)) pass('memory.md contains ADR entries with valid envelope.');
241
- else pass('memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build).');
242
- if (/ADR-025|ADR-028/.test(withoutComments) && /azcodr/i.test(mem)) {
243
- warn('memory.md may contain template-internal ADRs; fresh projects must start at ADR-001.');
244
- }
245
- }
246
-
247
- console.log('');
248
- console.log('--------------------------------------------------------------');
249
- if (errors === 0) {
250
- console.log(`🎉 SUCCESS: All agentic configurations are valid and healthy! (${warnings} warnings)`);
251
- process.exit(0);
252
- } else {
253
- console.log(`🚨 FAILURE: Found ${errors} error(s) and ${warnings} warning(s) in agentic configurations.`);
254
- process.exit(1);
255
- }
256
- }
257
-
258
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
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
+ */
21
+
22
+ function parseArgs(argv) {
23
+ let root = '';
24
+ for (const arg of argv) {
25
+ if (arg === '--fix') continue;
26
+ if (!root) root = arg;
27
+ }
28
+ return root || process.cwd();
29
+ }
30
+
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
+ }
45
+ }
46
+
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;
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
+ }
59
+
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 });
66
+ }
67
+ }
68
+
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
+ }
84
+
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)`);
90
+ } else {
91
+ reporter.log(`🚨 FAILURE: Found ${counts.errors} error(s) and ${counts.warnings} warning(s) in agentic configurations.`);
92
+ }
93
+ }
94
+
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
+ }
119
+
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);
124
+ }
125
+
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