Spaces:
Runtime error
Runtime error
Aryan Mishra
feat: initial commit - Multilingual ABSA project setup with 6 phases, 36 requirements
a6b96c2 | ; | |
| /** | |
| * ADR Markdown parser — parses Architecture Decision Record documents into | |
| * structured objects for downstream processing (adr command, gap checker, etc.). | |
| * | |
| * ADR-457 build-at-publish: the hand-written bin/lib/adr-parser.cjs collapsed | |
| * to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour | |
| * from the prior hand-written .cjs; only 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 security_cjs_1 = require("./security.cjs"); | |
| const STATUS_REJECT_SET = new Set(['superseded', 'rejected', 'deprecated']); | |
| const CANONICAL_HEADERS = { | |
| status: ['status', 'state', 'lifecycle', 'stage'], | |
| goal: [ | |
| 'context', | |
| 'background', | |
| 'problem statement', | |
| 'problem', | |
| 'situation', | |
| 'forces', | |
| 'motivation', | |
| 'issue', | |
| 'drivers', | |
| 'pain points', | |
| 'story', | |
| 'setting', | |
| 'premise', | |
| 'status quo', | |
| 'context and problem statement', | |
| ], | |
| decisions: [ | |
| 'decision', | |
| 'decisions', | |
| 'resolution', | |
| 'conclusion', | |
| 'choice', | |
| 'we decided', | |
| 'direction', | |
| 'approach', | |
| 'solution', | |
| 'outcome', | |
| 'selected option', | |
| 'recommendation', | |
| 'strategy', | |
| 'decision outcome', | |
| ], | |
| considered_options: [ | |
| 'considered options', | |
| 'alternatives', | |
| 'options', | |
| 'choices', | |
| 'candidates', | |
| 'approaches considered', | |
| 'variants', | |
| 'trade-offs', | |
| 'pros and cons of the options', | |
| 'discussion', | |
| ], | |
| risks: [ | |
| 'risks', | |
| 'trade-offs', | |
| 'drawbacks', | |
| 'cost', | |
| 'tensions', | |
| 'liabilities', | |
| 'negative consequences', | |
| 'side effects', | |
| ], | |
| success_criteria: [ | |
| 'success criteria', | |
| 'acceptance criteria', | |
| 'validation', | |
| "how we'll know", | |
| 'metrics', | |
| 'kpis', | |
| 'verification', | |
| 'test strategy', | |
| 'compliance', | |
| 'definition of done', | |
| 'exit criteria', | |
| 'positive consequences', | |
| ], | |
| plan_sequence: [ | |
| 'implementation plan', | |
| 'implementation notes', | |
| 'steps', | |
| 'tasks', | |
| 'roadmap', | |
| 'sequence', | |
| 'migration plan', | |
| 'plan', | |
| 'action items', | |
| 'work breakdown', | |
| 'phases', | |
| 'milestones', | |
| 'stages', | |
| ], | |
| key_files: [ | |
| 'affected files', | |
| 'files touched', | |
| 'surface area', | |
| 'modules affected', | |
| 'code locations', | |
| 'file changes', | |
| 'diff summary', | |
| 'touched code', | |
| ], | |
| out_of_scope: [ | |
| 'out of scope', | |
| 'non-goals', | |
| 'excluded', | |
| 'not in this adr', | |
| 'out of bounds', | |
| "won't do", | |
| "won't have", | |
| 'beyond scope', | |
| 'anti-goals', | |
| ], | |
| deferred: [ | |
| 'future work', | |
| 'deferred', | |
| 'future', | |
| 'later', | |
| 'follow-up', | |
| 'next steps', | |
| ], | |
| dependencies: [ | |
| 'dependencies', | |
| 'depends on', | |
| 'prerequisites', | |
| 'sequencing', | |
| 'order', | |
| 'blocked by', | |
| 'cross-cuts', | |
| 'related adrs', | |
| 'links', | |
| 'references', | |
| 'see also', | |
| 'upstream', | |
| 'inbound', | |
| ], | |
| update: [ | |
| 'update', | |
| 'revision', | |
| 'amendment', | |
| 'locked design', | |
| 'final decision', | |
| 'post-grilling', | |
| 'addendum', | |
| ], | |
| consequences: [ | |
| 'consequences', | |
| 'implications', | |
| 'impact', | |
| 'what this means', | |
| 'result', | |
| ], | |
| }; | |
| const CONSEQUENCE_NEGATIVE_HINTS = [ | |
| 'negative', | |
| 'drawback', | |
| 'risk', | |
| 'cost', | |
| 'liability', | |
| 'trade-off', | |
| 'tension', | |
| 'side effect', | |
| ]; | |
| const CONSEQUENCE_POSITIVE_HINTS = [ | |
| 'positive', | |
| 'success', | |
| 'metric', | |
| 'kpi', | |
| 'verification', | |
| 'acceptance', | |
| 'benefit', | |
| ]; | |
| function normalizeAdrHeader(raw) { | |
| const s = typeof raw === 'string' ? raw : ''; | |
| return s | |
| .trim() | |
| .toLowerCase() | |
| .replace(/[\s:._-]+/g, ' ') | |
| .replace(/[^\w\s]/g, '') | |
| .trim(); | |
| } | |
| function classifyHeader(normalizedHeader) { | |
| for (const [canonical, synonyms] of Object.entries(CANONICAL_HEADERS)) { | |
| for (const synonym of synonyms) { | |
| if (normalizedHeader === synonym) | |
| return canonical; | |
| if (normalizedHeader.startsWith(`${synonym} `)) | |
| return canonical; | |
| } | |
| } | |
| return null; | |
| } | |
| function splitEntries(blockText) { | |
| return (typeof blockText === 'string' ? blockText : '') | |
| .split(/\r?\n/) | |
| .map((line) => line.trim()) | |
| .filter(Boolean) | |
| .map((line) => line.replace(/^[-*+]\s+/, '').trim()) | |
| .filter(Boolean); | |
| } | |
| function parseSections(markdown) { | |
| const lines = (typeof markdown === 'string' ? markdown : '').split(/\r?\n/); | |
| const sections = []; | |
| let current = { heading: null, body: [] }; | |
| for (const line of lines) { | |
| const m = line.match(/^#{1,6}\s+(.*)$/); | |
| if (m) { | |
| if (current.heading || current.body.length) | |
| sections.push(current); | |
| current = { heading: m[1].trim(), body: [] }; | |
| } | |
| else { | |
| current.body.push(line); | |
| } | |
| } | |
| if (current.heading || current.body.length) | |
| sections.push(current); | |
| return sections; | |
| } | |
| function parseStatusFromSections(sections) { | |
| for (const section of sections) { | |
| const canonical = classifyHeader(normalizeAdrHeader(section.heading)); | |
| if (canonical !== 'status') | |
| continue; | |
| const line = splitEntries(section.body.join('\n'))[0] || ''; | |
| const norm = normalizeAdrHeader(line); | |
| if (!norm) | |
| return ''; | |
| if (norm.includes('accepted')) | |
| return 'accepted'; | |
| if (norm.includes('proposed')) | |
| return 'proposed'; | |
| if (norm.includes('superseded')) | |
| return 'superseded'; | |
| if (norm.includes('rejected')) | |
| return 'rejected'; | |
| if (norm.includes('deprecated')) | |
| return 'deprecated'; | |
| return norm; | |
| } | |
| return ''; | |
| } | |
| function pushUnique(target, values) { | |
| const seen = new Set(target); | |
| for (const value of values) { | |
| if (!seen.has(value)) { | |
| target.push(value); | |
| seen.add(value); | |
| } | |
| } | |
| } | |
| function parseConsequences(lines, out) { | |
| for (const entry of lines) { | |
| const lower = entry.toLowerCase(); | |
| if (CONSEQUENCE_NEGATIVE_HINTS.some((hint) => lower.includes(hint))) { | |
| out.consequences_negative.push(entry); | |
| continue; | |
| } | |
| if (CONSEQUENCE_POSITIVE_HINTS.some((hint) => lower.includes(hint))) { | |
| out.consequences_positive.push(entry); | |
| continue; | |
| } | |
| out.consequences_positive.push(entry); | |
| } | |
| } | |
| function parseAdrMarkdown(markdown, { sourcePath = '', format = 'auto' } = {}) { | |
| const sections = parseSections(markdown); | |
| const titleLine = (typeof markdown === 'string' ? markdown : '').split(/\r?\n/).find((line) => /^#\s+/.test(line)) || ''; | |
| const title = titleLine.replace(/^#\s+/, '').trim(); | |
| const out = { | |
| title, | |
| status: parseStatusFromSections(sections) || 'accepted', | |
| context: '', | |
| decisions: [], | |
| options_considered: [], | |
| consequences_positive: [], | |
| consequences_negative: [], | |
| out_of_scope: [], | |
| deferred: [], | |
| dependencies: [], | |
| updates: [], | |
| source_path: sourcePath, | |
| key_files: [], | |
| plan_sequence: [], | |
| format, | |
| unmapped_headers: [], | |
| }; | |
| for (const section of sections) { | |
| const heading = section.heading || ''; | |
| if (!heading) | |
| continue; | |
| const canonical = classifyHeader(normalizeAdrHeader(heading)); | |
| const entries = splitEntries(section.body.join('\n')); | |
| const prose = section.body.join('\n').trim(); | |
| if (!canonical) { | |
| out.unmapped_headers.push(heading); | |
| continue; | |
| } | |
| switch (canonical) { | |
| case 'goal': | |
| if (!out.context && prose) | |
| out.context = prose; | |
| break; | |
| case 'decisions': | |
| pushUnique(out.decisions, entries); | |
| break; | |
| case 'considered_options': | |
| pushUnique(out.options_considered, entries); | |
| break; | |
| case 'risks': | |
| pushUnique(out.consequences_negative, entries); | |
| break; | |
| case 'success_criteria': | |
| pushUnique(out.consequences_positive, entries); | |
| break; | |
| case 'plan_sequence': | |
| pushUnique(out.plan_sequence, entries); | |
| break; | |
| case 'key_files': | |
| pushUnique(out.key_files, entries); | |
| break; | |
| case 'out_of_scope': | |
| pushUnique(out.out_of_scope, entries); | |
| break; | |
| case 'deferred': | |
| pushUnique(out.deferred, entries); | |
| break; | |
| case 'dependencies': | |
| pushUnique(out.dependencies, entries); | |
| break; | |
| case 'update': | |
| out.updates.push({ heading, entries }); | |
| break; | |
| case 'consequences': | |
| parseConsequences(entries, out); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return out; | |
| } | |
| function shouldRejectAdrStatus(status) { | |
| return STATUS_REJECT_SET.has(normalizeAdrHeader(status)); | |
| } | |
| function parseCliArgs(argv) { | |
| const opts = { input: null, format: 'auto', projectDir: process.cwd() }; | |
| for (let i = 0; i < argv.length; i++) { | |
| const arg = argv[i]; | |
| if (arg === '--input') { | |
| opts.input = argv[++i] || null; | |
| } | |
| else if (arg === '--format') { | |
| opts.format = argv[++i] || 'auto'; | |
| } | |
| else if (arg === '--project-dir') { | |
| opts.projectDir = argv[++i] || process.cwd(); | |
| } | |
| else { | |
| throw new Error(`Unknown argument: ${arg}`); | |
| } | |
| } | |
| if (!opts.input) { | |
| throw new Error('Missing required --input <path>'); | |
| } | |
| return opts; | |
| } | |
| function main(argv) { | |
| const opts = parseCliArgs(argv); | |
| const safePath = (0, security_cjs_1.requireSafePath)(opts.input, node_path_1.default.resolve(opts.projectDir), 'ADR input path', { allowAbsolute: true }); | |
| const content = node_fs_1.default.readFileSync(safePath, 'utf8'); | |
| const parsed = parseAdrMarkdown(content, { sourcePath: opts.input ?? undefined, format: opts.format }); | |
| process.stdout.write(JSON.stringify(parsed, null, 2)); | |
| } | |
| if (require.main === module) { | |
| try { | |
| main(process.argv.slice(2)); | |
| } | |
| catch (error) { | |
| process.stderr.write(`Error: ${error.message}\n`); | |
| process.exit(1); | |
| } | |
| } | |
| module.exports = { | |
| CANONICAL_HEADERS, | |
| normalizeAdrHeader, | |
| parseAdrMarkdown, | |
| shouldRejectAdrStatus, | |
| }; | |