#!/usr/bin/env node /** * extract-manifest.js * * 验证版抽取器:扫 docs/prd/newfunction/sdd-*.html 把所有 * MVCP 定义(section[data-def-name][data-mvcp])抽成 build-manifest.json。 * * P1 正式版会改用 cheerio。本版只用 regex,零依赖,便于离线验证 data-* 锚点完整性。 */ const fs = require('fs'); const path = require('path'); const DOCS_DIR = path.resolve(__dirname, '../docs/prd/newfunction'); const MVCP_QUADRANTS = ['model', 'view', 'control', 'process']; const manifest = { meta: { source: 'docs/prd/newfunction', prdRev: 'v0.2', extractedAt: new Date().toISOString(), }, model: [], view: [], control: [], process: [], }; const issues = []; function stripTags(s) { return s.replace(/<[^>]+>/g, '').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim(); } function extractAttr(openTag, attr) { const m = openTag.match(new RegExp(`${attr}\\s*=\\s*"([^"]*)"`)); return m ? m[1] : null; } /** * Find all top-level
...
blocks. * Uses a depth counter to handle the (rare) nested section case correctly. */ function findDefSections(html) { const blocks = []; // Scan for opening tags with data-def-name const reOpen = /]*data-def-name="[^"]+"[^>]*)>/g; let m; while ((m = reOpen.exec(html)) !== null) { const start = m.index; const openTag = m[0]; const openAttrs = m[1]; // Walk forward, tracking
depth, until matching
let depth = 1; let pos = reOpen.lastIndex; const reTag = /<(\/?)section\b[^>]*>/g; reTag.lastIndex = pos; let t; let endIdx = -1; while ((t = reTag.exec(html)) !== null) { if (t[1] === '/') { depth--; if (depth === 0) { endIdx = t.index + t[0].length; break; } } else { depth++; } } if (endIdx < 0) { issues.push(`Unclosed
at offset ${start}`); continue; } blocks.push({ openAttrs, defName: extractAttr(openAttrs, 'data-def-name'), mvcp: extractAttr(openAttrs, 'data-mvcp'), body: html.slice(start, endIdx), }); reOpen.lastIndex = endIdx; } return blocks; } function extractPath(body) { // ... — class may be "path", "path mono", "mono path", etc. const re = /]*>([\s\S]*?)<\/code>/; const m = body.match(re); return m ? stripTags(m[2]) : null; } function extractShape(body) { //
...
— class may be "shape", "code shape", etc. const re = /]*>([\s\S]*?)<\/pre>/; const m = body.match(re); return m ? stripTags(m[2]) : null; } function extractListByClass(body, className) { //
    ...
→ list of
  • ...
  • texts const reUl = new RegExp(`]*>([\\s\\S]*?)<\\/ul>`); const um = body.match(reUl); if (!um) return []; const liRe = /]*>([\s\S]*?)<\/li>/g; const out = []; let lm; while ((lm = liRe.exec(um[1])) !== null) out.push(stripTags(lm[1])); return out; } // ── Scan SDD MVCP pages ── for (const quadrant of MVCP_QUADRANTS) { const file = path.join(DOCS_DIR, `sdd-${quadrant}.html`); if (!fs.existsSync(file)) { issues.push(`Missing file: sdd-${quadrant}.html`); continue; } const html = fs.readFileSync(file, 'utf8'); const blocks = findDefSections(html); if (blocks.length === 0) { issues.push(`sdd-${quadrant}.html: 0 definitions found`); continue; } for (const b of blocks) { if (!b.defName) { issues.push(`sdd-${quadrant}.html: section without data-def-name`); continue; } if (b.mvcp !== quadrant) { issues.push(`sdd-${quadrant}.html: section "${b.defName}" has data-mvcp="${b.mvcp}" (expected ${quadrant})`); } const def = { name: b.defName, path: extractPath(b.body), shape: extractShape(b.body), contract: extractListByClass(b.body, 'contract'), tests: extractListByClass(b.body, 'tests'), }; // Per-field completeness if (!def.path) issues.push(` · ${def.name} · missing path ()`); if (!def.shape) issues.push(` · ${def.name} · missing shape (
    )`);
        // contract / tests are optional
        manifest[quadrant].push(def);
      }
    }
    
    // ── Output ──
    const outPath = path.resolve(__dirname, '../build-manifest.json');
    fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2));
    
    const totals = MVCP_QUADRANTS.map(q => `${q}=${manifest[q].length}`).join(' · ');
    const sum = MVCP_QUADRANTS.reduce((s, q) => s + manifest[q].length, 0);
    
    console.log('\n=== build-manifest extraction report ===');
    console.log(`Source : ${DOCS_DIR}`);
    console.log(`Output : ${outPath}`);
    console.log(`Totals : ${totals}   →   sum=${sum}`);
    
    if (issues.length === 0) {
      console.log('\n✓ All MVCP definitions extracted cleanly. No issues.');
    } else {
      console.log(`\n⚠ ${issues.length} issue(s):`);
      for (const i of issues) console.log('  ' + i);
    }
    
    // Per-field completeness summary
    console.log('\n--- Per-field completeness ---');
    for (const q of MVCP_QUADRANTS) {
      const defs = manifest[q];
      const withPath     = defs.filter(d => d.path).length;
      const withShape    = defs.filter(d => d.shape).length;
      const withContract = defs.filter(d => d.contract.length > 0).length;
      const withTests    = defs.filter(d => d.tests.length > 0).length;
      console.log(`${q.padEnd(8)} | ${defs.length} defs | path ${withPath}/${defs.length} | shape ${withShape}/${defs.length} | contract ${withContract}/${defs.length} | tests ${withTests}/${defs.length}`);
    }