Spaces:
Runtime error
Runtime error
Aryan Mishra
feat: initial commit - Multilingual ABSA project setup with 6 phases, 36 requirements
a6b96c2 | ; | |
| /** | |
| * Core Utilities β Shared low-level utility primitives | |
| * | |
| * ADR-857 rollout phase 2c: extracted from core.cts (issue #877). | |
| * Owns POSIX path normalization, sub-repo/subdirectory scanning, | |
| * phase file stats, slug/one-liner/plan-id helpers, and time-ago. | |
| * Behaviour is preserved byte-for-behaviour from the prior location; | |
| * only the module boundary moved. core.cjs re-exports every public symbol | |
| * here under its own `export =` object so existing consumers are unaffected. | |
| * | |
| * New imports should pull core-utils helpers from core-utils.cjs directly. | |
| * | |
| * Dependencies (leaf modules only β no core.cjs, no loadConfig): | |
| * - node:fs / node:path (stdlib) | |
| * - ./phase-id.cjs (comparePhaseNum, used by readSubdirectories) | |
| * - ./planning-workspace.cjs (findContextMdIn, used by getPhaseFileStats) | |
| */ | |
| 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")); | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const phaseIdModule = require("./phase-id.cjs"); | |
| const { comparePhaseNum } = phaseIdModule; | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const planningWorkspace = require("./planning-workspace.cjs"); | |
| const { findContextMdIn } = planningWorkspace; | |
| // βββ Path helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Normalize a relative path to always use forward slashes (cross-platform). */ | |
| function toPosixPath(p) { | |
| return p.split(node_path_1.default.sep).join('/'); | |
| } | |
| /** | |
| * Scan immediate child directories for separate git repos. | |
| * Returns a sorted array of directory names that have their own `.git`. | |
| * Excludes hidden directories and node_modules. | |
| */ | |
| function detectSubRepos(cwd) { | |
| const results = []; | |
| try { | |
| const entries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| if (!entry.isDirectory()) | |
| continue; | |
| if (entry.name.startsWith('.') || entry.name === 'node_modules') | |
| continue; | |
| const gitPath = node_path_1.default.join(cwd, entry.name, '.git'); | |
| try { | |
| if (node_fs_1.default.existsSync(gitPath)) { | |
| results.push(entry.name); | |
| } | |
| } | |
| catch { /* ignore */ } | |
| } | |
| } | |
| catch { /* ignore */ } | |
| return results.sort(); | |
| } | |
| // βββ Summary body helpers βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Extract a one-liner from the summary body when it's not in frontmatter. | |
| */ | |
| function extractOneLinerFromBody(content) { | |
| if (!content) | |
| return null; | |
| const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | |
| const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, ''); | |
| const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m); | |
| if (!match) | |
| return null; | |
| const boldInner = match[1].trim(); | |
| const afterBold = match[2]; | |
| if (/:\s*$/.test(boldInner)) { | |
| const prose = afterBold.trim(); | |
| return prose.length > 0 ? prose : null; | |
| } | |
| return boldInner.length > 0 ? boldInner : null; | |
| } | |
| // βββ Misc utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function pathExistsInternal(cwd, targetPath) { | |
| const fullPath = node_path_1.default.isAbsolute(targetPath) ? targetPath : node_path_1.default.join(cwd, targetPath); | |
| try { | |
| node_fs_1.default.statSync(fullPath); | |
| return true; | |
| } | |
| catch { | |
| return false; | |
| } | |
| } | |
| function generateSlugInternal(text) { | |
| if (!text) | |
| return null; | |
| return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 60); | |
| } | |
| // βββ Phase file helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Filter a file list to just PLAN.md / *-PLAN.md entries. */ | |
| function filterPlanFiles(files) { | |
| return files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md'); | |
| } | |
| /** Filter a file list to just SUMMARY.md / *-SUMMARY.md entries. */ | |
| function filterSummaryFiles(files) { | |
| return files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); | |
| } | |
| /** | |
| * Read a phase directory and return counts/flags for common file types. | |
| */ | |
| function getPhaseFileStats(phaseDir) { | |
| const files = node_fs_1.default.readdirSync(phaseDir); | |
| return { | |
| plans: filterPlanFiles(files), | |
| summaries: filterSummaryFiles(files), | |
| hasResearch: files.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'), | |
| hasContext: findContextMdIn(files) !== null, | |
| hasVerification: files.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'), | |
| hasReviews: files.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'), | |
| }; | |
| } | |
| /** | |
| * Read immediate child directories from a path. | |
| * Returns [] if the path doesn't exist or can't be read. | |
| * Pass sort=true to apply comparePhaseNum ordering. | |
| */ | |
| function readSubdirectories(dirPath, sort = false) { | |
| try { | |
| const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true }); | |
| const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); | |
| return sort ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs; | |
| } | |
| catch { | |
| return []; | |
| } | |
| } | |
| /** | |
| * Format a Date as a fuzzy relative time string (e.g. "5 minutes ago"). | |
| */ | |
| function timeAgo(date) { | |
| const seconds = Math.floor((Date.now() - date.getTime()) / 1000); | |
| if (seconds < 5) | |
| return 'just now'; | |
| if (seconds < 60) | |
| return `${seconds} seconds ago`; | |
| const minutes = Math.floor(seconds / 60); | |
| if (minutes === 1) | |
| return '1 minute ago'; | |
| if (minutes < 60) | |
| return `${minutes} minutes ago`; | |
| const hours = Math.floor(minutes / 60); | |
| if (hours === 1) | |
| return '1 hour ago'; | |
| if (hours < 24) | |
| return `${hours} hours ago`; | |
| const days = Math.floor(hours / 24); | |
| if (days === 1) | |
| return '1 day ago'; | |
| if (days < 30) | |
| return `${days} days ago`; | |
| const months = Math.floor(days / 30); | |
| if (months === 1) | |
| return '1 month ago'; | |
| if (months < 12) | |
| return `${months} months ago`; | |
| const years = Math.floor(days / 365); | |
| if (years === 1) | |
| return '1 year ago'; | |
| return `${years} years ago`; | |
| } | |
| // βββ Plan ID helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Extract the canonical plan ID from a filename. | |
| * Private to the core cluster β exported so core.cjs:searchPhaseInDir can | |
| * import it from this leaf without circular dependency, but NOT re-exported | |
| * from core.cjs's public `export =` block. | |
| */ | |
| function extractCanonicalPlanId(filename) { | |
| const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, ''); | |
| const parts = base.split('-').filter(Boolean); | |
| const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i; | |
| const phaseIdx = parts.findIndex(p => tokenRe.test(p)); | |
| if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) { | |
| return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`; | |
| } | |
| return base; | |
| } | |
| module.exports = { | |
| toPosixPath, | |
| detectSubRepos, | |
| extractOneLinerFromBody, | |
| pathExistsInternal, | |
| generateSlugInternal, | |
| filterPlanFiles, | |
| filterSummaryFiles, | |
| getPhaseFileStats, | |
| readSubdirectories, | |
| timeAgo, | |
| extractCanonicalPlanId, | |
| }; | |