mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
Merge PR #3428: validate and sync every catalog project tree entry
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user