Merge PR #3428: validate and sync every catalog project tree entry

This commit is contained in:
haelyra
2026-10-10 17:37:24 -04:00
4 changed files with 205 additions and 16 deletions
+2 -2
View File
@@ -827,8 +827,8 @@ Stable graduation of the 2.0 line: control-pane substrate, worktree lifecycle se
```text
ECC/
|-- agents/ # 71 specialized subagents for delegation
|-- skills/ # 293 reusable workflows loaded on demand
|-- commands/ # 94 maintained slash-command shims
|-- skills/ # 302 reusable workflows loaded on demand
|-- commands/ # 95 maintained slash-command shims
|-- rules/ # opt-in common and language standards
|-- hooks/ # runtime automation and enforcement
|-- scripts/ # install, repair, sync, orchestration, and checks
+51 -11
View File
@@ -87,6 +87,23 @@ function replaceOrThrow(content, regex, replacer, source) {
return content.replace(regex, replacer);
}
function countMarkers(content, regex) {
return [...content.matchAll(regex)].length;
}
function replaceAllOrThrow(content, regex, replacer, source, expectedCount) {
const found = countMarkers(content, regex);
if (found === 0) {
throw new Error(`${source} is missing the expected catalog marker`);
}
if (expectedCount !== undefined && found !== expectedCount) {
throw new Error(`${source} expected ${expectedCount} marker(s), found ${found}`);
}
return content.replace(regex, replacer);
}
function parseReadmeExpectations(readmeContent) {
const expectations = [];
@@ -103,17 +120,28 @@ function parseReadmeExpectations(readmeContent) {
{ category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'README.md quick-start summary' }
);
const projectTreeAgentsMatches = [...readmeContent.matchAll(/^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/gim)];
if (!projectTreeAgentsMatches.length) {
throw new Error('README.md project tree is missing the agents count');
}
const projectTreePatterns = [
{ category: 'agents', regex: /^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/gim, source: 'README.md project tree (agents)' },
{ category: 'commands', regex: /^\|\s*--\s*commands\/\s*#\s*(\d+)\s+maintained slash-command shims\s*$/gim, source: 'README.md project tree (commands)' },
{ category: 'skills', regex: /^\|\s*--\s*skills\/\s*#\s*(\d+)\s+reusable workflows loaded on demand\s*$/gim, source: 'README.md project tree (skills)' }
];
for (const [index, match] of projectTreeAgentsMatches.entries()) expectations.push({
category: 'agents',
mode: 'exact',
expected: Number(match[1]),
source: `README.md project tree ${index + 1} (agents)`
});
for (const pattern of projectTreePatterns) {
const matches = [...readmeContent.matchAll(pattern.regex)];
if (matches.length === 0) {
throw new Error(`${pattern.source} is missing the ${pattern.category} count`);
}
for (const match of matches) {
expectations.push({
category: pattern.category,
mode: 'exact',
expected: Number(match[1]),
source: `${pattern.source} block ${readmeContent.slice(0, match.index).split('\n').length}`
});
}
}
const tablePatterns = [
{ category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' },
@@ -409,12 +437,24 @@ function syncEnglishReadme(content, catalog) {
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count} legacy command shims`,
'README.md quick-start summary'
);
nextContent = replaceOrThrow(
nextContent = replaceAllOrThrow(
nextContent,
/^(\|\s*--\s*agents\/\s*#\s*)(\d+)(\s+specialized subagents for delegation\s*)$/gim,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'README.md project tree (agents)'
);
nextContent = replaceAllOrThrow(
nextContent,
/^(\|\s*--\s*commands\/\s*#\s*)(\d+)(\s+maintained slash-command shims\s*)$/gim,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'README.md project tree (commands)'
);
nextContent = replaceAllOrThrow(
nextContent,
/^(\|\s*--\s*skills\/\s*#\s*)(\d+)(\s+reusable workflows loaded on demand\s*)$/gim,
(_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`,
'README.md project tree (skills)'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+agents\s*\|)/i,
+74 -2
View File
@@ -46,7 +46,9 @@ function writeEnglishReadme(root, counts, options = {}) {
fs.writeFileSync(path.join(root, 'README.md'), `Access to ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} commands.
- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} legacy command shims.
|-- agents/ # ${counts.agents} specialized subagents for delegation
|-- agents/ # ${counts.agents + 1} specialized subagents for delegation
|-- skills/ # ${counts.skills} reusable workflows loaded on demand
|-- commands/ # ${counts.commands} maintained slash-command shims
${options.extraTreeBlock === undefined ? `|-- agents/ # ${counts.agents + 1} specialized subagents for delegation` : options.extraTreeBlock}
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
| --- | --- | --- | --- | --- |
| Agents | PASS: ${tableCounts.agents} agents |
@@ -171,7 +173,7 @@ function writeCatalogFixture(root, options = {}) {
fs.writeFileSync(path.join(root, 'commands', 'notes.txt'), 'not counted\n');
fs.mkdirSync(path.join(root, 'skills', 'missing-skill-file'), { recursive: true });
writeEnglishReadme(root, documentedCounts, { unrelatedSkillsCount });
writeEnglishReadme(root, documentedCounts, { unrelatedSkillsCount, extraTreeBlock: options.extraTreeBlock });
writeEnglishAgents(root, documentedCounts, { skillsMinimum });
writeCrossHarnessIdentityDocs(root, documentedCounts);
writeZhRootReadme(root, documentedCounts);
@@ -294,6 +296,76 @@ function runTests() {
}
})) passed++; else failed++;
if (test('flags a stale count hidden in a second project-tree block', () => {
const testDir = createTestDir();
try {
writeCatalogFixture(testDir, {
actualCounts: { agents: 2, skills: 3, commands: 4 },
documentedCounts: { agents: 2, skills: 3, commands: 4 },
extraTreeBlock: '|-- agents/ # 1 specialized subagents for delegation',
});
const result = runCatalogCheck({ root: testDir });
const failing = result.checks.filter(check => !check.ok && check.source.includes('project tree'));
assert.strictEqual(failing.length, 1);
assert.match(failing[0].source, /project tree \(agents\) block \d+/);
assert.strictEqual(failing[0].expected, 1);
assert.strictEqual(failing[0].actual, 2);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (test('write mode repairs every project-tree block, not just the first', () => {
const testDir = createTestDir();
try {
writeCatalogFixture(testDir, {
actualCounts: { agents: 2, skills: 3, commands: 4 },
documentedCounts: { agents: 2, skills: 3, commands: 4 },
extraTreeBlock: [
'|-- agents/ # 1 specialized subagents for delegation',
'|-- skills/ # 1 reusable workflows loaded on demand',
].join('\n'),
});
const result = runCatalogCheck({ root: testDir, writeMode: true });
assert.strictEqual(result.checks.filter(check => !check.ok).length, 0);
const readme = fs.readFileSync(path.join(testDir, 'README.md'), 'utf8');
assert.ok(!readme.includes('# 1 specialized subagents for delegation'));
assert.ok(!readme.includes('# 1 reusable workflows loaded on demand'));
assert.strictEqual(readme.split('# 2 specialized subagents for delegation').length - 1, 2);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (test('throws when a project-tree count entry is missing entirely', () => {
const testDir = createTestDir();
try {
writeCatalogFixture(testDir, {
actualCounts: { agents: 2, skills: 3, commands: 4 },
documentedCounts: { agents: 2, skills: 3, commands: 4 },
});
const readmePath = path.join(testDir, 'README.md');
const withoutSkills = fs.readFileSync(readmePath, 'utf8')
.split('\n')
.filter(line => !line.includes('reusable workflows loaded on demand'))
.join('\n');
fs.writeFileSync(readmePath, withoutSkills);
assert.throws(
() => runCatalogCheck({ root: testDir }),
/README\.md project tree \(skills\) is missing the skills count/
);
} finally {
cleanupTestDir(testDir);
}
})) passed++; else failed++;
if (test('throws a clear error for missing tracked documents', () => {
const testDir = createTestDir();
try {
+78 -1
View File
@@ -260,6 +260,9 @@ function writeCatalogFixture(testDir, options = {}) {
const {
readmeCounts = { agents: 1, skills: 1, commands: 1 },
readmeProjectTreeAgents = readmeCounts.agents,
readmeProjectTreeSkills = readmeCounts.skills,
readmeProjectTreeCommands = readmeCounts.commands,
readmeExtraTreeBlock = '',
readmeTableCounts = readmeCounts,
readmeParityCounts = readmeCounts,
readmeUnrelatedSkillsCount = 16,
@@ -306,7 +309,10 @@ function writeCatalogFixture(testDir, options = {}) {
fs.writeFileSync(path.join(testDir, 'commands', 'plan.md'), '---\ndescription: Plan\n---\n# Plan');
fs.writeFileSync(path.join(testDir, 'skills', 'demo-skill', 'SKILL.md'), '---\nname: demo-skill\ndescription: Demo skill\norigin: ECC\n---\n# Demo Skill');
fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\n- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} legacy command shims.\n|-- agents/ # ${readmeProjectTreeAgents} specialized subagents for delegation\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeTableCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeTableCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeTableCounts.skills} skills | Shared | Shared | 1 |\n\n| Feature | Count | Format |\n|-----------|-------|---------|\n| Skills | ${readmeUnrelatedSkillsCount} | .agents/skills/ |\n\n## Cross-Tool Feature Parity\n\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **Agents** | ${readmeParityCounts.agents} | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |\n| **Commands** | ${readmeParityCounts.commands} | Shared | Instruction-based | 31 |\n| **Skills** | ${readmeParityCounts.skills} | Shared | 10 (native format) | 37 |\n`);
fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\n- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} legacy command shims.\n|-- agents/ # ${readmeProjectTreeAgents} specialized subagents for delegation
|-- skills/ # ${readmeProjectTreeSkills} reusable workflows loaded on demand
|-- commands/ # ${readmeProjectTreeCommands} maintained slash-command shims
${readmeExtraTreeBlock}\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeTableCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeTableCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeTableCounts.skills} skills | Shared | Shared | 1 |\n\n| Feature | Count | Format |\n|-----------|-------|---------|\n| Skills | ${readmeUnrelatedSkillsCount} | .agents/skills/ |\n\n## Cross-Tool Feature Parity\n\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **Agents** | ${readmeParityCounts.agents} | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |\n| **Commands** | ${readmeParityCounts.commands} | Shared | Instruction-based | 31 |\n| **Skills** | ${readmeParityCounts.skills} | Shared | 10 (native format) | 37 |\n`);
fs.writeFileSync(agentsPath, `This is a **production-ready AI coding plugin** providing ${summaryCounts.agents} specialized agents, ${summaryCounts.skills} skills, ${summaryCounts.commands} commands, and automated hook workflows for software development.\n\n\`\`\`\n${structureLines.join('\n')}\n\`\`\`\n`);
fs.writeFileSync(zhRootReadmePath, `**完成!** 你现在可以使用 ${zhRootReadmeCounts.agents} 个代理、${zhRootReadmeCounts.skills} 个技能和 ${zhRootReadmeCounts.commands} 个命令。\n`);
fs.writeFileSync(zhDocsReadmePath, `**搞定!** 你现在可以使用 ${zhDocsReadmeCounts.agents} 个智能体、${zhDocsReadmeCounts.skills} 项技能和 ${zhDocsReadmeCounts.commands} 个命令了。\n| 功能特性 | Claude Code | OpenCode | 状态 |\n|---------|-------------|----------|--------|\n| 智能体 | \u2705 ${zhDocsTableCounts.agents} 个 | \u2705 12 个 | **Claude Code 领先** |\n| 命令 | \u2705 ${zhDocsTableCounts.commands} 个 | \u2705 31 个 | **Claude Code 领先** |\n| 技能 | \u2705 ${zhDocsTableCounts.skills} 项 | \u2705 37 项 | **Claude Code 领先** |\n\n| 功能特性 | 数量 | 格式 |\n|-----------|-------|---------|\n| 技能 | ${zhDocsUnrelatedSkillsCount} | .agents/skills/ |\n\n## 跨工具功能对等\n\n| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **智能体** | ${zhDocsParityCounts.agents} | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |\n| **命令** | ${zhDocsParityCounts.commands} | 共享 | 基于指令 | 31 |\n| **技能** | ${zhDocsParityCounts.skills} | 共享 | 10 (原生格式) | 37 |\n`);
@@ -842,6 +848,77 @@ function runTests() {
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('flags a stale count hidden in a second README project-tree block', () => {
const testDir = createTestDir();
const {
readmePath,
agentsPath,
zhRootReadmePath,
zhDocsReadmePath,
zhAgentsPath,
pluginJsonPath,
marketplaceJsonPath,
} = writeCatalogFixture(testDir, {
readmeExtraTreeBlock: '|-- agents/ # 9 specialized subagents for delegation',
});
const result = runCatalogValidator({
ROOT: testDir,
README_PATH: readmePath,
AGENTS_PATH: agentsPath,
README_ZH_CN_PATH: zhRootReadmePath,
DOCS_ZH_CN_README_PATH: zhDocsReadmePath,
DOCS_ZH_CN_AGENTS_PATH: zhAgentsPath,
PLUGIN_JSON_PATH: pluginJsonPath,
MARKETPLACE_JSON_PATH: marketplaceJsonPath,
});
const output = result.stdout + result.stderr;
assert.strictEqual(result.code, 1, `Should fail on a stale second block, got ${output}`);
assert.ok(
output.includes('README.md project tree (agents) block'),
`Should name the offending block, got ${output}`
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('write mode repairs a stale second README project-tree block', () => {
const testDir = createTestDir();
const {
readmePath,
agentsPath,
zhRootReadmePath,
zhDocsReadmePath,
zhAgentsPath,
pluginJsonPath,
marketplaceJsonPath,
} = writeCatalogFixture(testDir, {
readmeExtraTreeBlock: '|-- agents/ # 9 specialized subagents for delegation',
});
const result = runCatalogValidator({
argv: ['--write', '--text'],
ROOT: testDir,
README_PATH: readmePath,
AGENTS_PATH: agentsPath,
README_ZH_CN_PATH: zhRootReadmePath,
DOCS_ZH_CN_README_PATH: zhDocsReadmePath,
DOCS_ZH_CN_AGENTS_PATH: zhAgentsPath,
PLUGIN_JSON_PATH: pluginJsonPath,
MARKETPLACE_JSON_PATH: marketplaceJsonPath,
});
assert.strictEqual(result.code, 0, `Should sync every block, got ${result.stderr}`);
const readme = fs.readFileSync(readmePath, 'utf8');
assert.ok(!readme.includes('# 9 specialized subagents for delegation'), 'Should repair the second block too');
assert.strictEqual(
readme.split('# 1 specialized subagents for delegation').length - 1,
2,
'Should correct both blocks'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('exits 0 when hooks.json does not exist', () => {
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', '/nonexistent/hooks.json');
assert.strictEqual(result.code, 0, 'Should skip when no hooks.json');