Spaces:
Runtime error
Runtime error
Aryan Mishra
feat: initial commit - Multilingual ABSA project setup with 6 phases, 36 requirements
a6b96c2 | ; | |
| /** | |
| * Open Artifact Audit β Cross-type unresolved state scanner | |
| * | |
| * Scans all .planning/ artifact categories for items with open/unresolved state. | |
| * Returns structured JSON for workflow consumption. | |
| * Called by: gsd-tools.cjs audit-open | |
| * Used by: /gsd:complete-milestone pre-close gate | |
| * | |
| * ADR-457 build-at-publish: the hand-written bin/lib/audit.cjs collapsed | |
| * to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour | |
| * from the prior hand-written .cjs; only strict types are added. | |
| */ | |
| var __importDefault = (this && this.__importDefault) || function (mod) { | |
| return (mod && mod.__esModule) ? mod : { "default": mod }; | |
| }; | |
| const node_fs_1 = __importDefault(require("node:fs")); | |
| const node_path_1 = __importDefault(require("node:path")); | |
| const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs"); | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const planningWorkspace = require("./planning-workspace.cjs"); | |
| const { planningDir } = planningWorkspace; | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const frontmatter = require("./frontmatter.cjs"); | |
| const { extractFrontmatter } = frontmatter; | |
| const security_cjs_1 = require("./security.cjs"); | |
| // Terminal UAT states: `complete` (legacy) and `resolved` (post-gap-closure | |
| // per workflows/execute-phase.md). Hoisted outside scanUatGaps so the Set is | |
| // not recreated on each loop iteration. | |
| const TERMINAL_UAT_STATUSES = new Set(['complete', 'resolved']); | |
| // βββ scanDebugSessions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/debug/ for open sessions. | |
| * Open = status NOT in ['resolved', 'complete']. | |
| * Ignores the resolved/ subdirectory. | |
| */ | |
| function scanDebugSessions(planDir) { | |
| const debugDir = node_path_1.default.join(planDir, 'debug'); | |
| if (!node_fs_1.default.existsSync(debugDir)) | |
| return []; | |
| const results = []; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(debugDir, { withFileTypes: true }); | |
| } | |
| catch { | |
| return [{ scan_error: true, slug: '', status: '', updated: '', hypothesis: '' }]; | |
| } | |
| for (const entry of files) { | |
| if (!entry.isFile()) | |
| continue; | |
| if (!entry.name.endsWith('.md')) | |
| continue; | |
| const filePath = node_path_1.default.join(debugDir, entry.name); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'debug session file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| const status = (fm.status || 'unknown').toLowerCase(); | |
| if (status === 'resolved' || status === 'complete') | |
| continue; | |
| // Extract hypothesis from "Current Focus" block if parseable | |
| let hypothesis = ''; | |
| const focusMatch = content.match(/##\s*Current Focus[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i); | |
| if (focusMatch) { | |
| const focusText = focusMatch[1].trim().split('\n')[0].trim(); | |
| hypothesis = (0, security_cjs_1.sanitizeForDisplay)(focusText.slice(0, 100)); | |
| } | |
| const slug = node_path_1.default.basename(entry.name, '.md'); | |
| results.push({ | |
| slug: (0, security_cjs_1.sanitizeForDisplay)(slug), | |
| status: (0, security_cjs_1.sanitizeForDisplay)(status), | |
| updated: (0, security_cjs_1.sanitizeForDisplay)(fm.updated || fm.date || ''), | |
| hypothesis, | |
| }); | |
| } | |
| return results; | |
| } | |
| // βββ scanQuickTasks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/quick/ for incomplete tasks. | |
| * Incomplete if SUMMARY.md missing or status !== 'complete'. | |
| */ | |
| function scanQuickTasks(planDir) { | |
| const quickDir = node_path_1.default.join(planDir, 'quick'); | |
| if (!node_fs_1.default.existsSync(quickDir)) | |
| return []; | |
| let entries; | |
| try { | |
| entries = node_fs_1.default.readdirSync(quickDir, { withFileTypes: true }); | |
| } | |
| catch { | |
| return [{ scan_error: true, slug: '', date: '', status: '', description: '' }]; | |
| } | |
| const results = []; | |
| for (const entry of entries) { | |
| if (!entry.isDirectory()) | |
| continue; | |
| const dirName = entry.name; | |
| const taskDir = node_path_1.default.join(quickDir, dirName); | |
| let safeTaskDir; | |
| try { | |
| safeTaskDir = (0, security_cjs_1.requireSafePath)(taskDir, planDir, 'quick task dir', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| // workflows/quick.md mandates `${quick_id}-SUMMARY.md`; older flows used | |
| // bare `SUMMARY.md`. Accept either to avoid false-positive "missing". | |
| let summaryPath = null; | |
| try { | |
| const summaryFiles = node_fs_1.default.readdirSync(safeTaskDir, { withFileTypes: true }) | |
| .filter(e => e.isFile() && (e.name === 'SUMMARY.md' || e.name.endsWith('-SUMMARY.md'))); | |
| if (summaryFiles.length > 0) { | |
| // Prefer the per-task `${quick_id}-SUMMARY.md` form when present. | |
| const preferred = summaryFiles.find(e => e.name === `${dirName}-SUMMARY.md`) | |
| || summaryFiles.find(e => e.name.endsWith('-SUMMARY.md')) | |
| || summaryFiles[0]; | |
| summaryPath = node_path_1.default.join(safeTaskDir, preferred.name); | |
| } | |
| } | |
| catch { | |
| // fall through with summaryPath = null β status: missing | |
| } | |
| let status = 'missing'; | |
| const description = ''; | |
| if (summaryPath && node_fs_1.default.existsSync(summaryPath)) { | |
| let safeSum; | |
| try { | |
| safeSum = (0, security_cjs_1.requireSafePath)(summaryPath, planDir, 'quick task summary', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeSum); | |
| if (content === null) { | |
| status = 'unreadable'; | |
| } | |
| else { | |
| const fm = extractFrontmatter(content); | |
| status = (fm.status || 'unknown').toLowerCase(); | |
| } | |
| } | |
| if (status === 'complete') | |
| continue; | |
| // Parse date and slug from directory name: YYYYMMDD-slug or YYYY-MM-DD-slug | |
| let date = ''; | |
| let slug = (0, security_cjs_1.sanitizeForDisplay)(dirName); | |
| const dateMatch = dirName.match(/^(\d{4}-?\d{2}-?\d{2})-(.+)$/); | |
| if (dateMatch) { | |
| date = dateMatch[1]; | |
| slug = (0, security_cjs_1.sanitizeForDisplay)(dateMatch[2]); | |
| } | |
| results.push({ | |
| slug, | |
| date, | |
| status: (0, security_cjs_1.sanitizeForDisplay)(status), | |
| description, | |
| }); | |
| } | |
| return results; | |
| } | |
| // βββ scanThreads ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/threads/ for open threads. | |
| * Open if status in ['open', 'in_progress', 'in progress'] (case-insensitive). | |
| */ | |
| function scanThreads(planDir) { | |
| const threadsDir = node_path_1.default.join(planDir, 'threads'); | |
| if (!node_fs_1.default.existsSync(threadsDir)) | |
| return []; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(threadsDir, { withFileTypes: true }); | |
| } | |
| catch { | |
| return [{ scan_error: true, slug: '', status: '', updated: '', title: '' }]; | |
| } | |
| const openStatuses = new Set(['open', 'in_progress', 'in progress']); | |
| const results = []; | |
| for (const entry of files) { | |
| if (!entry.isFile()) | |
| continue; | |
| if (!entry.name.endsWith('.md')) | |
| continue; | |
| const filePath = node_path_1.default.join(threadsDir, entry.name); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'thread file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| let status = (fm.status || '').toLowerCase().trim(); | |
| // Fall back to scanning body for ## Status: OPEN / IN PROGRESS | |
| if (!status) { | |
| const bodyStatusMatch = content.match(/##\s*Status:\s*(OPEN|IN PROGRESS|IN_PROGRESS)/i); | |
| if (bodyStatusMatch) { | |
| status = bodyStatusMatch[1].toLowerCase().replace(/ /g, '_'); | |
| } | |
| } | |
| if (!openStatuses.has(status)) | |
| continue; | |
| // Extract title from # Thread: heading or frontmatter title | |
| let title = (0, security_cjs_1.sanitizeForDisplay)(fm.title || ''); | |
| if (!title) { | |
| const headingMatch = content.match(/^#\s*Thread:\s*(.+)$/m); | |
| if (headingMatch) { | |
| title = (0, security_cjs_1.sanitizeForDisplay)(headingMatch[1].trim().slice(0, 100)); | |
| } | |
| } | |
| const slug = node_path_1.default.basename(entry.name, '.md'); | |
| results.push({ | |
| slug: (0, security_cjs_1.sanitizeForDisplay)(slug), | |
| status: (0, security_cjs_1.sanitizeForDisplay)(status), | |
| updated: (0, security_cjs_1.sanitizeForDisplay)(fm.updated || fm.date || ''), | |
| title, | |
| }); | |
| } | |
| return results; | |
| } | |
| // βββ scanTodos ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/todos/pending/ for pending todos. | |
| * Returns array of { filename, priority, area, summary }. | |
| * Display limited to first 5 + count of remainder. | |
| */ | |
| function scanTodos(planDir) { | |
| const pendingDir = node_path_1.default.join(planDir, 'todos', 'pending'); | |
| if (!node_fs_1.default.existsSync(pendingDir)) | |
| return []; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(pendingDir, { withFileTypes: true }); | |
| } | |
| catch { | |
| return [{ scan_error: true, filename: '', priority: '', area: '', summary: '' }]; | |
| } | |
| const mdFiles = files.filter(e => e.isFile() && e.name.endsWith('.md')); | |
| const results = []; | |
| const displayFiles = mdFiles.slice(0, 5); | |
| for (const entry of displayFiles) { | |
| const filePath = node_path_1.default.join(pendingDir, entry.name); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'todo file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| // Extract first line of body after frontmatter | |
| const bodyMatch = content.replace(/^---[\s\S]*?---\n?/, ''); | |
| const firstLine = bodyMatch.trim().split('\n')[0] || ''; | |
| const summary = (0, security_cjs_1.sanitizeForDisplay)(firstLine.slice(0, 100)); | |
| results.push({ | |
| filename: (0, security_cjs_1.sanitizeForDisplay)(entry.name), | |
| priority: (0, security_cjs_1.sanitizeForDisplay)(fm.priority || ''), | |
| area: (0, security_cjs_1.sanitizeForDisplay)(fm.area || ''), | |
| summary, | |
| }); | |
| } | |
| if (mdFiles.length > 5) { | |
| results.push({ _remainder_count: mdFiles.length - 5, filename: '', priority: '', area: '', summary: '' }); | |
| } | |
| return results; | |
| } | |
| // βββ scanSeeds ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/seeds/SEED-*.md for unimplemented seeds. | |
| * Unimplemented if status in ['dormant', 'active', 'triggered']. | |
| */ | |
| function scanSeeds(planDir) { | |
| const seedsDir = node_path_1.default.join(planDir, 'seeds'); | |
| if (!node_fs_1.default.existsSync(seedsDir)) | |
| return []; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(seedsDir, { withFileTypes: true }); | |
| } | |
| catch { | |
| return [{ scan_error: true, seed_id: '', slug: '', status: '', title: '' }]; | |
| } | |
| const unimplementedStatuses = new Set(['dormant', 'active', 'triggered']); | |
| const results = []; | |
| for (const entry of files) { | |
| if (!entry.isFile()) | |
| continue; | |
| if (!entry.name.startsWith('SEED-') || !entry.name.endsWith('.md')) | |
| continue; | |
| const filePath = node_path_1.default.join(seedsDir, entry.name); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'seed file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| const status = (fm.status || 'dormant').toLowerCase(); | |
| if (!unimplementedStatuses.has(status)) | |
| continue; | |
| // Extract seed_id from filename or frontmatter | |
| const seedIdMatch = entry.name.match(/^(SEED-[\w-]+)\.md$/); | |
| const seed_id = seedIdMatch ? seedIdMatch[1] : node_path_1.default.basename(entry.name, '.md'); | |
| const slug = (0, security_cjs_1.sanitizeForDisplay)(seed_id.replace(/^SEED-/, '')); | |
| let title = (0, security_cjs_1.sanitizeForDisplay)(fm.title || ''); | |
| if (!title) { | |
| const headingMatch = content.match(/^#\s*(.+)$/m); | |
| if (headingMatch) | |
| title = (0, security_cjs_1.sanitizeForDisplay)(headingMatch[1].trim().slice(0, 100)); | |
| } | |
| results.push({ | |
| seed_id: (0, security_cjs_1.sanitizeForDisplay)(seed_id), | |
| slug, | |
| status: (0, security_cjs_1.sanitizeForDisplay)(status), | |
| title, | |
| }); | |
| } | |
| return results; | |
| } | |
| // βββ scanUatGaps ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/phases for UAT gaps (UAT files with status != 'complete'). | |
| */ | |
| function scanUatGaps(planDir) { | |
| const phasesDir = node_path_1.default.join(planDir, 'phases'); | |
| if (!node_fs_1.default.existsSync(phasesDir)) | |
| return []; | |
| let dirs; | |
| try { | |
| dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true }) | |
| .filter(e => e.isDirectory()) | |
| .map(e => e.name) | |
| .sort(); | |
| } | |
| catch { | |
| return [{ scan_error: true, phase: '', file: '', status: '', open_scenario_count: 0 }]; | |
| } | |
| const results = []; | |
| for (const dir of dirs) { | |
| const phaseDir = node_path_1.default.join(phasesDir, dir); | |
| const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); | |
| const phaseNum = phaseMatch ? phaseMatch[1] : dir; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(phaseDir); | |
| } | |
| catch { | |
| continue; | |
| } | |
| for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) { | |
| const filePath = node_path_1.default.join(phaseDir, file); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'UAT file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| const status = (fm.status || 'unknown').toLowerCase(); | |
| const result = (fm.result || '').toLowerCase(); | |
| // Also accept `result: all_pass` as a fallback when status is absent | |
| // β covers UATs that omit `status:`. | |
| if (TERMINAL_UAT_STATUSES.has(status)) | |
| continue; | |
| if (status === 'unknown' && result === 'all_pass') | |
| continue; | |
| // Count open scenarios | |
| const pendingMatches = (content.match(/result:\s*(?:pending|\[pending\])/gi) || []).length; | |
| results.push({ | |
| phase: (0, security_cjs_1.sanitizeForDisplay)(phaseNum), | |
| file: (0, security_cjs_1.sanitizeForDisplay)(file), | |
| status: (0, security_cjs_1.sanitizeForDisplay)(status), | |
| open_scenario_count: pendingMatches, | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| // βββ scanVerificationGaps βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/phases for VERIFICATION gaps. | |
| */ | |
| function scanVerificationGaps(planDir) { | |
| const phasesDir = node_path_1.default.join(planDir, 'phases'); | |
| if (!node_fs_1.default.existsSync(phasesDir)) | |
| return []; | |
| let dirs; | |
| try { | |
| dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true }) | |
| .filter(e => e.isDirectory()) | |
| .map(e => e.name) | |
| .sort(); | |
| } | |
| catch { | |
| return [{ scan_error: true, phase: '', file: '', status: '' }]; | |
| } | |
| const results = []; | |
| for (const dir of dirs) { | |
| const phaseDir = node_path_1.default.join(phasesDir, dir); | |
| const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); | |
| const phaseNum = phaseMatch ? phaseMatch[1] : dir; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(phaseDir); | |
| } | |
| catch { | |
| continue; | |
| } | |
| for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) { | |
| const filePath = node_path_1.default.join(phaseDir, file); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'VERIFICATION file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| const status = (fm.status || 'unknown').toLowerCase(); | |
| if (status !== 'gaps_found' && status !== 'human_needed') | |
| continue; | |
| results.push({ | |
| phase: (0, security_cjs_1.sanitizeForDisplay)(phaseNum), | |
| file: (0, security_cjs_1.sanitizeForDisplay)(file), | |
| status: (0, security_cjs_1.sanitizeForDisplay)(status), | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| // βββ scanContextQuestions βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Scan .planning/phases for CONTEXT files with open_questions. | |
| */ | |
| function scanContextQuestions(planDir) { | |
| const phasesDir = node_path_1.default.join(planDir, 'phases'); | |
| if (!node_fs_1.default.existsSync(phasesDir)) | |
| return []; | |
| let dirs; | |
| try { | |
| dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true }) | |
| .filter(e => e.isDirectory()) | |
| .map(e => e.name) | |
| .sort(); | |
| } | |
| catch { | |
| return [{ scan_error: true, phase: '', file: '', question_count: 0, questions: [] }]; | |
| } | |
| const results = []; | |
| for (const dir of dirs) { | |
| const phaseDir = node_path_1.default.join(phasesDir, dir); | |
| const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); | |
| const phaseNum = phaseMatch ? phaseMatch[1] : dir; | |
| let files; | |
| try { | |
| files = node_fs_1.default.readdirSync(phaseDir); | |
| } | |
| catch { | |
| continue; | |
| } | |
| for (const file of files.filter(f => f.includes('-CONTEXT') && f.endsWith('.md'))) { | |
| const filePath = node_path_1.default.join(phaseDir, file); | |
| let safeFilePath; | |
| try { | |
| safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'CONTEXT file', { allowAbsolute: true }); | |
| } | |
| catch { | |
| continue; | |
| } | |
| const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath); | |
| if (content === null) | |
| continue; | |
| const fm = extractFrontmatter(content); | |
| // Check frontmatter open_questions field | |
| let questions = []; | |
| if (fm.open_questions) { | |
| if (Array.isArray(fm.open_questions) && fm.open_questions.length > 0) { | |
| questions = fm.open_questions.map(q => (0, security_cjs_1.sanitizeForDisplay)(String(q).slice(0, 200))); | |
| } | |
| } | |
| // Also check for ## Open Questions section in body | |
| if (questions.length === 0) { | |
| const oqMatch = content.match(/##\s*Open Questions[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i); | |
| if (oqMatch) { | |
| const oqBody = oqMatch[1].trim(); | |
| if (oqBody && oqBody.length > 0 && !/^\s*none\s*$/i.test(oqBody)) { | |
| const items = oqBody.split('\n') | |
| .map((l) => l.trim()) | |
| .filter((l) => l && l !== '-' && l !== '*') | |
| .filter((l) => /^[-*\d]/.test(l) || l.includes('?')); | |
| questions = items.slice(0, 3).map((q) => (0, security_cjs_1.sanitizeForDisplay)(q.slice(0, 200))); | |
| } | |
| } | |
| } | |
| if (questions.length === 0) | |
| continue; | |
| results.push({ | |
| phase: (0, security_cjs_1.sanitizeForDisplay)(phaseNum), | |
| file: (0, security_cjs_1.sanitizeForDisplay)(file), | |
| question_count: questions.length, | |
| questions: questions.slice(0, 3), | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| // βββ auditOpenArtifacts βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Main audit function. Scans all .planning/ artifact categories. | |
| * | |
| * @param cwd - Project root directory | |
| * @returns Structured audit result | |
| */ | |
| function auditOpenArtifacts(cwd) { | |
| const planDir = planningDir(cwd); | |
| const debugSessions = (() => { | |
| try { | |
| return scanDebugSessions(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, slug: '', status: '', updated: '', hypothesis: '' }]; | |
| } | |
| })(); | |
| const quickTasks = (() => { | |
| try { | |
| return scanQuickTasks(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, slug: '', date: '', status: '', description: '' }]; | |
| } | |
| })(); | |
| const threads = (() => { | |
| try { | |
| return scanThreads(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, slug: '', status: '', updated: '', title: '' }]; | |
| } | |
| })(); | |
| const todos = (() => { | |
| try { | |
| return scanTodos(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, filename: '', priority: '', area: '', summary: '' }]; | |
| } | |
| })(); | |
| const seeds = (() => { | |
| try { | |
| return scanSeeds(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, seed_id: '', slug: '', status: '', title: '' }]; | |
| } | |
| })(); | |
| const uatGaps = (() => { | |
| try { | |
| return scanUatGaps(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, phase: '', file: '', status: '', open_scenario_count: 0 }]; | |
| } | |
| })(); | |
| const verificationGaps = (() => { | |
| try { | |
| return scanVerificationGaps(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, phase: '', file: '', status: '' }]; | |
| } | |
| })(); | |
| const contextQuestions = (() => { | |
| try { | |
| return scanContextQuestions(planDir); | |
| } | |
| catch { | |
| return [{ scan_error: true, phase: '', file: '', question_count: 0, questions: [] }]; | |
| } | |
| })(); | |
| // Count real items (not scan_error sentinels) | |
| const countReal = (arr) => arr.filter(i => !i.scan_error && !i._remainder_count).length; | |
| const counts = { | |
| debug_sessions: countReal(debugSessions), | |
| quick_tasks: countReal(quickTasks), | |
| threads: countReal(threads), | |
| todos: countReal(todos), | |
| seeds: countReal(seeds), | |
| uat_gaps: countReal(uatGaps), | |
| verification_gaps: countReal(verificationGaps), | |
| context_questions: countReal(contextQuestions), | |
| total: 0, | |
| }; | |
| counts.total = counts.debug_sessions + counts.quick_tasks + counts.threads + counts.todos + counts.seeds + counts.uat_gaps + counts.verification_gaps + counts.context_questions; | |
| return { | |
| scanned_at: new Date().toISOString(), | |
| has_open_items: counts.total > 0, | |
| counts, | |
| items: { | |
| debug_sessions: debugSessions, | |
| quick_tasks: quickTasks, | |
| threads, | |
| todos, | |
| seeds, | |
| uat_gaps: uatGaps, | |
| verification_gaps: verificationGaps, | |
| context_questions: contextQuestions, | |
| }, | |
| }; | |
| } | |
| // βββ formatAuditReport ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Format the audit result as a human-readable report. | |
| * | |
| * @param auditResult - Result from auditOpenArtifacts() | |
| * @returns Formatted report | |
| */ | |
| function formatAuditReport(auditResult) { | |
| const { counts, items, has_open_items } = auditResult; | |
| const lines = []; | |
| const hr = 'βββββββββββββββββββββββββββββββββββββββββββββββββββββ'; | |
| lines.push(hr); | |
| lines.push(' Milestone Close: Open Artifact Audit'); | |
| lines.push(hr); | |
| if (!has_open_items) { | |
| lines.push(''); | |
| lines.push(' All artifact types clear. Safe to proceed.'); | |
| lines.push(''); | |
| lines.push(hr); | |
| return lines.join('\n'); | |
| } | |
| // Debug sessions (blocking quality β red) | |
| if (counts.debug_sessions > 0) { | |
| lines.push(''); | |
| lines.push(`π΄ Debug Sessions (${counts.debug_sessions} open)`); | |
| for (const item of items.debug_sessions.filter(i => !i.scan_error)) { | |
| const hyp = item.hypothesis ? ` β ${item.hypothesis}` : ''; | |
| lines.push(` β’ ${item.slug} [${item.status}]${hyp}`); | |
| } | |
| } | |
| // UAT gaps (blocking quality β red) | |
| if (counts.uat_gaps > 0) { | |
| lines.push(''); | |
| lines.push(`π΄ UAT Gaps (${counts.uat_gaps} phases with incomplete UAT)`); | |
| for (const item of items.uat_gaps.filter(i => !i.scan_error)) { | |
| lines.push(` β’ Phase ${item.phase}: ${item.file} [${item.status}] β ${item.open_scenario_count} pending scenarios`); | |
| } | |
| } | |
| // Verification gaps (blocking quality β red) | |
| if (counts.verification_gaps > 0) { | |
| lines.push(''); | |
| lines.push(`π΄ Verification Gaps (${counts.verification_gaps} unresolved)`); | |
| for (const item of items.verification_gaps.filter(i => !i.scan_error)) { | |
| lines.push(` β’ Phase ${item.phase}: ${item.file} [${item.status}]`); | |
| } | |
| } | |
| // Quick tasks (incomplete work β yellow) | |
| if (counts.quick_tasks > 0) { | |
| lines.push(''); | |
| lines.push(`π‘ Quick Tasks (${counts.quick_tasks} incomplete)`); | |
| for (const item of items.quick_tasks.filter(i => !i.scan_error)) { | |
| const d = item.date ? ` (${item.date})` : ''; | |
| lines.push(` β’ ${item.slug}${d} [${item.status}]`); | |
| } | |
| } | |
| // Todos (incomplete work β yellow) | |
| if (counts.todos > 0) { | |
| const realTodos = items.todos.filter(i => !i.scan_error && !i._remainder_count); | |
| const remainder = items.todos.find(i => i._remainder_count); | |
| lines.push(''); | |
| lines.push(`π‘ Pending Todos (${counts.todos} pending)`); | |
| for (const item of realTodos) { | |
| const area = item.area ? ` [${item.area}]` : ''; | |
| const pri = item.priority ? ` (${item.priority})` : ''; | |
| lines.push(` β’ ${item.filename}${area}${pri}`); | |
| if (item.summary) | |
| lines.push(` ${item.summary}`); | |
| } | |
| if (remainder) { | |
| lines.push(` ... and ${remainder._remainder_count} more`); | |
| } | |
| } | |
| // Threads (deferred decisions β blue) | |
| if (counts.threads > 0) { | |
| lines.push(''); | |
| lines.push(`π΅ Open Threads (${counts.threads} active)`); | |
| for (const item of items.threads.filter(i => !i.scan_error)) { | |
| const title = item.title ? ` β ${item.title}` : ''; | |
| lines.push(` β’ ${item.slug} [${item.status}]${title}`); | |
| } | |
| } | |
| // Seeds (deferred decisions β blue) | |
| if (counts.seeds > 0) { | |
| lines.push(''); | |
| lines.push(`π΅ Unimplemented Seeds (${counts.seeds} pending)`); | |
| for (const item of items.seeds.filter(i => !i.scan_error)) { | |
| const title = item.title ? ` β ${item.title}` : ''; | |
| lines.push(` β’ ${item.seed_id} [${item.status}]${title}`); | |
| } | |
| } | |
| // Context questions (deferred decisions β blue) | |
| if (counts.context_questions > 0) { | |
| lines.push(''); | |
| lines.push(`π΅ CONTEXT Open Questions (${counts.context_questions} phases with open questions)`); | |
| for (const item of items.context_questions.filter(i => !i.scan_error)) { | |
| lines.push(` β’ Phase ${item.phase}: ${item.file} (${item.question_count} question${item.question_count !== 1 ? 's' : ''})`); | |
| for (const q of item.questions) { | |
| lines.push(` - ${q}`); | |
| } | |
| } | |
| } | |
| lines.push(''); | |
| lines.push(hr); | |
| lines.push(` ${counts.total} item${counts.total !== 1 ? 's' : ''} require decisions before close.`); | |
| lines.push(hr); | |
| return lines.join('\n'); | |
| } | |
| module.exports = { auditOpenArtifacts, formatAuditReport }; | |