// Truthful coverage ledger: source-backed, approximate and excluded artifacts stay separate. import { readFileSync, writeFileSync, existsSync } from 'node:fs' const read = (p) => JSON.parse(readFileSync(p, 'utf8')) const inventory = read('tools/city-massing/city/city-inventory.json') const regular = read('tools/city-massing/city/regular-estate/lod-preview/manifest.json') const oldtown = read('tools/city-massing/city/oldtown/lod-preview/manifest.json') const sample = read('tools/city-massing/district/lod-preview/manifest.json') const estatePlan = read('tools/city-massing/city/regular-estate/plan.json') const oldPlan = read('tools/city-massing/city/oldtown/plan.json') const seated = read('tools/city-massing/city/oldtown/seated-plan.json') const verification = existsSync('tools/city-massing/city/city-model-verification.json') ? read('tools/city-massing/city/city-model-verification.json') : null const imageryReport = existsSync('tools/city-massing/city/imagery-footprints-report.json') ? read('tools/city-massing/city/imagery-footprints-report.json') : null const placementSources = {} for (const building of oldPlan.buildings) placementSources[building.placementSource] = (placementSources[building.placementSource] || 0) + 1 const construction = {} for (const building of seated.buildings) construction[building.construction || 'flat'] = (construction[building.construction || 'flat'] || 0) + 1 const exclusionReasons = {} for (const entry of oldPlan.excluded) exclusionReasons[entry.reason] = (exclusionReasons[entry.reason] || 0) + 1 const output = { bounds: inventory.bounds, sourceInventory: inventory.originalSources, districts: inventory.districts, sourceBacked: { scope: regular.scope, roofTraces: estatePlan.roofs.length, acceptedModules: regular.count, excludedModules: estatePlan.modules.length - regular.count, excludedReasons: 'original DEM relief >2.5m', }, oldTownBacked: { scope: oldtown.scope, placementSources, footprintPlanned: oldPlan.buildings.length, footprintSources: oldPlan.sources.length, seated: seated.buildings.length, seatedSources: new Set(seated.buildings.map((b) => b.sourceId)).size, construction, excludedFootprints: oldPlan.excluded.length, excludedModules: seated.excluded.length, exclusionReasons, imagerySegmentation: imageryReport ? { accepted: imageryReport.totals.accepted, trustedMasks: [65, 66, 126], untrustedMasks: { 63: 'river/farmland sliver; greenhouse roofs', 130: 'industry park; bright yards read as roof' }, limitations: imageryReport.limitations, } : null, }, previousSample: { scope: sample.scope, accepted: sample.count }, runtimeVerification: verification ? { passed: verification.passed, checkedAt: verification.checkedAt, trees: verification.summary, sourceConflictCount: verification.sourceConflictCount, } : null, runtime: { defaultModelEnabled: false, reviewUrl: 'http://127.0.0.1:8092/preview.html', lightingFlag: 'map.tilesets[].lighting=true', lod: 'coarse spatial parents with REPLACE LOD1/LOD0 children', }, disclaimer: 'This ledger counts generated artifacts, not surveyed coverage. Heights and facades are estimated; imagery-segmented footprints are per-roof rectangles, not survey geometry; the estate source conflicts listed in city-model-verification.json still need a human decision.', } writeFileSync('tools/city-massing/city/coverage-summary.json', JSON.stringify(output, null, 2)) console.log( JSON.stringify({ regular: regular.count, oldtownPlaced: oldtown.count, oldtownSeatedSources: output.oldTownBacked.seatedSources, construction, imageryTrustedMasks: [65, 66, 126], omittedEstate: output.sourceBacked.excludedModules, omittedOldtownModules: seated.excluded.length, verificationPassed: verification?.passed ?? null, conflicts: verification?.sourceConflictCount ?? null, }) )