@llm-dev-ops/agentics-cli 2.7.36 → 2.7.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +24 -70
- package/dist/commands/agents.js.map +1 -1
- package/dist/mcp/agent-event-parser.d.ts +1 -11
- package/dist/mcp/agent-event-parser.d.ts.map +1 -1
- package/dist/mcp/agent-event-parser.js +7 -153
- package/dist/mcp/agent-event-parser.js.map +1 -1
- package/dist/mcp/mcp-server.js +0 -58
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +27 -169
- package/dist/pipeline/auto-chain.js.map +1 -1
- package/dist/pipeline/local-fallback/phase5a-local-fallback.d.ts +21 -18
- package/dist/pipeline/local-fallback/phase5a-local-fallback.d.ts.map +1 -1
- package/dist/pipeline/local-fallback/phase5a-local-fallback.js +92 -397
- package/dist/pipeline/local-fallback/phase5a-local-fallback.js.map +1 -1
- package/dist/pipeline/phase2/phases/adr-generator.d.ts +29 -1
- package/dist/pipeline/phase2/phases/adr-generator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/adr-generator.js +709 -1399
- package/dist/pipeline/phase2/phases/adr-generator.js.map +1 -1
- package/dist/pipeline/phase2/phases/ddd-generator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/ddd-generator.js +7 -42
- package/dist/pipeline/phase2/phases/ddd-generator.js.map +1 -1
- package/dist/pipeline/phase2/phases/research-dossier.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/research-dossier.js +2 -33
- package/dist/pipeline/phase2/phases/research-dossier.js.map +1 -1
- package/dist/pipeline/phase2/phases/sparc-specification.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/sparc-specification.js +2 -27
- package/dist/pipeline/phase2/phases/sparc-specification.js.map +1 -1
- package/dist/pipeline/phase2/types.d.ts +19 -57
- package/dist/pipeline/phase2/types.d.ts.map +1 -1
- package/dist/pipeline/phase4-adrs/adr-index-extractor.d.ts +75 -0
- package/dist/pipeline/phase4-adrs/adr-index-extractor.d.ts.map +1 -0
- package/dist/pipeline/phase4-adrs/adr-index-extractor.js +200 -0
- package/dist/pipeline/phase4-adrs/adr-index-extractor.js.map +1 -0
- package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.d.ts.map +1 -1
- package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js +70 -68
- package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js.map +1 -1
- package/dist/pipeline/phases/adr-ddd-generator.d.ts.map +1 -1
- package/dist/pipeline/phases/adr-ddd-generator.js +48 -2
- package/dist/pipeline/phases/adr-ddd-generator.js.map +1 -1
- package/dist/pipeline/phases/prompt-generator.js +191 -80
- package/dist/pipeline/phases/prompt-generator.js.map +1 -1
- package/dist/pipeline/ruflo-phase-executor.d.ts.map +1 -1
- package/dist/pipeline/ruflo-phase-executor.js +69 -17
- package/dist/pipeline/ruflo-phase-executor.js.map +1 -1
- package/dist/pipeline/types.d.ts +14 -1
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/synthesis/ask-artifact-writer.d.ts +1 -1
- package/dist/synthesis/ask-artifact-writer.d.ts.map +1 -1
- package/dist/synthesis/ask-artifact-writer.js +9 -9
- package/dist/synthesis/ask-artifact-writer.js.map +1 -1
- package/dist/synthesis/simulation-artifact-generator.d.ts +1 -27
- package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
- package/dist/synthesis/simulation-artifact-generator.js +38 -128
- package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/ui/heartbeat.d.ts +0 -88
- package/dist/cli/ui/heartbeat.d.ts.map +0 -1
- package/dist/cli/ui/heartbeat.js +0 -158
- package/dist/cli/ui/heartbeat.js.map +0 -1
- package/dist/synthesis/agent-fleet-decomposer.d.ts +0 -124
- package/dist/synthesis/agent-fleet-decomposer.d.ts.map +0 -1
- package/dist/synthesis/agent-fleet-decomposer.js +0 -696
- package/dist/synthesis/agent-fleet-decomposer.js.map +0 -1
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
|
-
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
14
16
|
import { createSpan, endSpan, emitSpan } from '../telemetry.js';
|
|
15
17
|
import { detectTechnologyStack, writeTechStack, formatERPContextForPrompt } from './tech-stack-detector.js';
|
|
16
18
|
const DIR_MODE = 0o700;
|
|
@@ -70,176 +72,54 @@ function findDossierRefs(dossier, keywords) {
|
|
|
70
72
|
// ============================================================================
|
|
71
73
|
// ADR Markdown Renderer
|
|
72
74
|
// ============================================================================
|
|
73
|
-
/**
|
|
74
|
-
* Render a Phase2ADRRecord as a markdown document.
|
|
75
|
-
*
|
|
76
|
-
* ADR-PIPELINE-101 — when the LLM-generated record carries the rich-content
|
|
77
|
-
* fields (architectureDiagram, components, performanceTargets, references,
|
|
78
|
-
* appendices, implementationStatus), the renderer interleaves them into a
|
|
79
|
-
* structure that mirrors `docs/good_ADR_example.md`. When those fields are
|
|
80
|
-
* absent (deterministic fallback path), it produces the legacy thin format
|
|
81
|
-
* for backwards compatibility.
|
|
82
|
-
*/
|
|
83
75
|
function renderADRMarkdown(adr) {
|
|
76
|
+
// ADR-PIPELINE-100 §D1: when the LLM emitted markdown directly, write it
|
|
77
|
+
// verbatim. Structured fields are best-effort metadata, not the source of
|
|
78
|
+
// truth — re-rendering from them would lose diagrams, code samples, tables.
|
|
79
|
+
if (adr.rawMarkdown && adr.rawMarkdown.trim().length > 0) {
|
|
80
|
+
return adr.rawMarkdown.endsWith('\n') ? adr.rawMarkdown : adr.rawMarkdown + '\n';
|
|
81
|
+
}
|
|
84
82
|
const lines = [];
|
|
85
83
|
lines.push(`# ${adr.id}: ${adr.title}`);
|
|
86
84
|
lines.push('');
|
|
87
85
|
lines.push(`**Status:** ${adr.status}`);
|
|
88
86
|
lines.push(`**Date:** ${adr.date}`);
|
|
89
|
-
if (adr.authors && adr.authors.length > 0) {
|
|
90
|
-
lines.push(`**Authors:** ${adr.authors.join(', ')}`);
|
|
91
|
-
}
|
|
92
|
-
if (adr.deciders && adr.deciders.length > 0) {
|
|
93
|
-
lines.push(`**Deciders:** ${adr.deciders.join(', ')}`);
|
|
94
|
-
}
|
|
95
|
-
if (adr.supersedes) {
|
|
96
|
-
lines.push('');
|
|
97
|
-
lines.push(`**Note:** This ADR supersedes ${adr.supersedes}.`);
|
|
98
|
-
}
|
|
99
|
-
lines.push('');
|
|
100
|
-
lines.push('---');
|
|
101
87
|
lines.push('');
|
|
102
|
-
// ── Context ─────────────────────────────────────────────────────────
|
|
103
88
|
lines.push('## Context');
|
|
104
|
-
lines.push('');
|
|
105
89
|
lines.push(adr.context);
|
|
106
90
|
lines.push('');
|
|
107
|
-
// ── Decision ────────────────────────────────────────────────────────
|
|
108
91
|
lines.push('## Decision');
|
|
109
|
-
lines.push('');
|
|
110
92
|
lines.push(adr.decision);
|
|
111
93
|
lines.push('');
|
|
112
|
-
// ── Architecture diagram (if provided) ──────────────────────────────
|
|
113
|
-
if (adr.architectureDiagram && adr.architectureDiagram.trim()) {
|
|
114
|
-
lines.push('### Architecture');
|
|
115
|
-
lines.push('');
|
|
116
|
-
lines.push('```');
|
|
117
|
-
lines.push(adr.architectureDiagram.trim());
|
|
118
|
-
lines.push('```');
|
|
119
|
-
lines.push('');
|
|
120
|
-
}
|
|
121
|
-
// ── Key Components ──────────────────────────────────────────────────
|
|
122
|
-
if (adr.components && adr.components.length > 0) {
|
|
123
|
-
lines.push('## Key Components');
|
|
124
|
-
lines.push('');
|
|
125
|
-
let idx = 0;
|
|
126
|
-
for (const comp of adr.components) {
|
|
127
|
-
idx++;
|
|
128
|
-
lines.push(`### ${idx}. ${comp.name}`);
|
|
129
|
-
lines.push('');
|
|
130
|
-
lines.push(comp.description);
|
|
131
|
-
lines.push('');
|
|
132
|
-
if (comp.codeExample && comp.codeExample.code.trim()) {
|
|
133
|
-
lines.push('```' + (comp.codeExample.language || ''));
|
|
134
|
-
lines.push(comp.codeExample.code.trim());
|
|
135
|
-
lines.push('```');
|
|
136
|
-
lines.push('');
|
|
137
|
-
}
|
|
138
|
-
if (comp.configTable && comp.configTable.rows.length > 0) {
|
|
139
|
-
lines.push(renderTable(comp.configTable.headers, comp.configTable.rows));
|
|
140
|
-
lines.push('');
|
|
141
|
-
}
|
|
142
|
-
if (comp.performanceCharacteristics && comp.performanceCharacteristics.length > 0) {
|
|
143
|
-
lines.push('**Performance characteristics:**');
|
|
144
|
-
lines.push('');
|
|
145
|
-
for (const p of comp.performanceCharacteristics)
|
|
146
|
-
lines.push(`- ${p}`);
|
|
147
|
-
lines.push('');
|
|
148
|
-
}
|
|
149
|
-
if (comp.securityNotes && comp.securityNotes.length > 0) {
|
|
150
|
-
lines.push('**Security guarantees:**');
|
|
151
|
-
lines.push('');
|
|
152
|
-
for (const s of comp.securityNotes)
|
|
153
|
-
lines.push(`- ${s}`);
|
|
154
|
-
lines.push('');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// ── Performance Targets (top-level table) ───────────────────────────
|
|
159
|
-
if (adr.performanceTargets && adr.performanceTargets.rows.length > 0) {
|
|
160
|
-
lines.push('## Performance Targets');
|
|
161
|
-
lines.push('');
|
|
162
|
-
lines.push(renderTable(adr.performanceTargets.headers, adr.performanceTargets.rows));
|
|
163
|
-
lines.push('');
|
|
164
|
-
}
|
|
165
|
-
// ── Alternatives ────────────────────────────────────────────────────
|
|
166
94
|
if (adr.alternatives.length > 0) {
|
|
167
95
|
lines.push('## Alternatives Considered');
|
|
168
|
-
lines.push('');
|
|
169
96
|
for (const alt of adr.alternatives) {
|
|
170
97
|
const status = alt.rejected ? '(rejected)' : '(selected)';
|
|
171
98
|
lines.push(`- **${alt.option}** ${status}: ${alt.rationale}`);
|
|
172
99
|
}
|
|
173
100
|
lines.push('');
|
|
174
101
|
}
|
|
175
|
-
// ── Consequences ────────────────────────────────────────────────────
|
|
176
102
|
if (adr.consequences.length > 0) {
|
|
177
103
|
lines.push('## Consequences');
|
|
178
|
-
lines.push('');
|
|
179
104
|
for (const c of adr.consequences) {
|
|
180
105
|
const icon = c.type === 'positive' ? '+' : c.type === 'negative' ? '-' : '~';
|
|
181
106
|
lines.push(`- [${icon}] ${c.description}`);
|
|
182
107
|
}
|
|
183
108
|
lines.push('');
|
|
184
109
|
}
|
|
185
|
-
// ── Implementation Status ───────────────────────────────────────────
|
|
186
|
-
if (adr.implementationStatus && adr.implementationStatus.rows.length > 0) {
|
|
187
|
-
lines.push('## Implementation Status');
|
|
188
|
-
lines.push('');
|
|
189
|
-
lines.push(renderTable(adr.implementationStatus.headers, adr.implementationStatus.rows));
|
|
190
|
-
lines.push('');
|
|
191
|
-
}
|
|
192
|
-
// ── References (numbered) ───────────────────────────────────────────
|
|
193
|
-
if (adr.references && adr.references.length > 0) {
|
|
194
|
-
lines.push('## References');
|
|
195
|
-
lines.push('');
|
|
196
|
-
let r = 0;
|
|
197
|
-
for (const ref of adr.references) {
|
|
198
|
-
r++;
|
|
199
|
-
lines.push(`${r}. ${ref}`);
|
|
200
|
-
}
|
|
201
|
-
lines.push('');
|
|
202
|
-
}
|
|
203
|
-
// ── Dossier item refs (kept as a separate section so they don't bury
|
|
204
|
-
// the formal references) ───────────────────────────────────────────
|
|
205
110
|
if (adr.researchDossierItemRefs.length > 0) {
|
|
206
|
-
lines.push('##
|
|
207
|
-
lines.push('');
|
|
208
|
-
lines.push(adr.researchDossierItemRefs.map(r => `\`${r}\``).join(', '));
|
|
111
|
+
lines.push('## References');
|
|
112
|
+
lines.push(`Dossier items: ${adr.researchDossierItemRefs.join(', ')}`);
|
|
209
113
|
lines.push('');
|
|
210
114
|
}
|
|
211
|
-
// ── Appendices ──────────────────────────────────────────────────────
|
|
212
|
-
if (adr.appendices && adr.appendices.length > 0) {
|
|
213
|
-
let letterCode = 'A'.charCodeAt(0);
|
|
214
|
-
for (const apx of adr.appendices) {
|
|
215
|
-
lines.push(`## Appendix ${String.fromCharCode(letterCode)}: ${apx.title}`);
|
|
216
|
-
lines.push('');
|
|
217
|
-
lines.push(apx.body);
|
|
218
|
-
lines.push('');
|
|
219
|
-
letterCode++;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
115
|
// ADR-PIPELINE-018: Include simulation lineage when present
|
|
223
116
|
if (adr.simulationId) {
|
|
224
117
|
lines.push('## Lineage');
|
|
225
|
-
lines.push(
|
|
226
|
-
lines.push(`Originating simulation: \`${adr.simulationId}\``);
|
|
118
|
+
lines.push(`Originating simulation: ${adr.simulationId}`);
|
|
227
119
|
lines.push('');
|
|
228
120
|
}
|
|
229
121
|
return lines.join('\n');
|
|
230
122
|
}
|
|
231
|
-
/** Render a markdown table — header row + separator + data rows. */
|
|
232
|
-
function renderTable(headers, rows) {
|
|
233
|
-
const lines = [];
|
|
234
|
-
lines.push(`| ${headers.join(' | ')} |`);
|
|
235
|
-
lines.push(`|${headers.map(() => '---').join('|')}|`);
|
|
236
|
-
for (const row of rows) {
|
|
237
|
-
// Pad/truncate to header length so the table stays valid.
|
|
238
|
-
const padded = headers.map((_, i) => row[i] ?? '');
|
|
239
|
-
lines.push(`| ${padded.join(' | ')} |`);
|
|
240
|
-
}
|
|
241
|
-
return lines.join('\n');
|
|
242
|
-
}
|
|
243
123
|
// ============================================================================
|
|
244
124
|
// ADR Builder — derived from SPARC + dossier
|
|
245
125
|
// ============================================================================
|
|
@@ -250,994 +130,42 @@ function makeCons(description, type) {
|
|
|
250
130
|
return { description, type };
|
|
251
131
|
}
|
|
252
132
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
133
|
+
* Generate ADRs for a project via the LLM transports.
|
|
134
|
+
*
|
|
135
|
+
* ADR-PIPELINE-100 §D5: there is no silent SPARC-template fallback. When
|
|
136
|
+
* every LLM transport fails this returns a single loud-failure ADR record
|
|
137
|
+
* that the renderer writes as `ADR-FAIL-001.md`.
|
|
138
|
+
*
|
|
139
|
+
* Callers that want to skip the LLM and emit a failure ADR directly (the
|
|
140
|
+
* auto-chain "ensure ADRs exist" guarantee paths) should call
|
|
141
|
+
* `buildLoudFailureADR` directly rather than passing a flag here.
|
|
255
142
|
*/
|
|
256
|
-
function
|
|
257
|
-
// Services: may be on architecture.services or architecture.components or top-level services
|
|
258
|
-
const raw = sparc;
|
|
259
|
-
const arch = (raw.architecture ?? {});
|
|
260
|
-
let services = [];
|
|
261
|
-
const candidateServices = arch.services ?? arch.components ?? arch.modules ?? raw.services;
|
|
262
|
-
if (Array.isArray(candidateServices)) {
|
|
263
|
-
services = candidateServices.map((s, i) => ({
|
|
264
|
-
id: String(s.id ?? `SVC-${String(i + 1).padStart(3, '0')}`),
|
|
265
|
-
name: String(s.name ?? s.label ?? `Service${i + 1}`),
|
|
266
|
-
responsibility: String(s.responsibility ?? s.description ?? s.purpose ?? ''),
|
|
267
|
-
api: Array.isArray(s.api) ? s.api.map(String) : Array.isArray(s.endpoints) ? s.endpoints.map(String) : [],
|
|
268
|
-
dependencies: Array.isArray(s.dependencies) ? s.dependencies.map(String) : Array.isArray(s.deps) ? s.deps.map(String) : [],
|
|
269
|
-
dossierItemRefs: Array.isArray(s.dossierItemRefs) ? s.dossierItemRefs.map(String) : [],
|
|
270
|
-
}));
|
|
271
|
-
}
|
|
272
|
-
// Security: may be array of objects or array of strings
|
|
273
|
-
const ref = (raw.refinement ?? {});
|
|
274
|
-
let securityItems = [];
|
|
275
|
-
const rawSec = ref.securityConsiderations ?? ref.security ?? [];
|
|
276
|
-
if (Array.isArray(rawSec)) {
|
|
277
|
-
securityItems = rawSec.map((item, i) => {
|
|
278
|
-
if (typeof item === 'string')
|
|
279
|
-
return { id: `sec-${i + 1}`, text: item, dossierItemRefs: [] };
|
|
280
|
-
const obj = item;
|
|
281
|
-
return { id: String(obj.id ?? `sec-${i + 1}`), text: String(obj.text ?? obj.description ?? obj.name ?? ''), dossierItemRefs: [] };
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
// Performance: may be array of objects or array of strings
|
|
285
|
-
let perfItems = [];
|
|
286
|
-
const rawPerf = ref.performanceTargets ?? ref.performance ?? [];
|
|
287
|
-
if (Array.isArray(rawPerf)) {
|
|
288
|
-
perfItems = rawPerf.map((item, i) => {
|
|
289
|
-
if (typeof item === 'string')
|
|
290
|
-
return { id: `perf-${i + 1}`, text: item, dossierItemRefs: [] };
|
|
291
|
-
const obj = item;
|
|
292
|
-
return { id: String(obj.id ?? `perf-${i + 1}`), text: String(obj.text ?? obj.description ?? obj.target ?? ''), dossierItemRefs: [] };
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
const comp = (raw.completion ?? {});
|
|
296
|
-
const integrationPlan = typeof comp.integrationPlan === 'string' ? comp.integrationPlan : '';
|
|
297
|
-
return { services, securityItems, perfItems, integrationPlan };
|
|
298
|
-
}
|
|
299
|
-
function detectAlgorithmicDecisions(query, domain) {
|
|
300
|
-
const decisions = [];
|
|
301
|
-
const q = query;
|
|
302
|
-
// ── 1. Weighted / Multi-Objective Scoring Algorithm ──
|
|
303
|
-
const hasScoring = /(?:weight|score|rank|multi.?(?:objective|criteria|factor|dimension)|normali[sz]|compar(?:e|ison).*(?:score|rank|metric))/i.test(q);
|
|
304
|
-
const hasCostSpeedEmissions = /(?:cost|speed|emission|carbon|sustainab|environmental)/i.test(q) && /(?:weight|balanc|tradeoff|trade.?off|priori)/i.test(q);
|
|
305
|
-
if (hasScoring || hasCostSpeedEmissions) {
|
|
306
|
-
decisions.push({
|
|
307
|
-
title: `${domain.primary} Scoring Algorithm: weighted multi-objective normalization with configurable dimensions`,
|
|
308
|
-
context: `${domain.primary} requires comparing options across multiple competing dimensions (e.g. cost, speed, emissions). ` +
|
|
309
|
-
`Each dimension operates on a different scale and unit. Stakeholders need to adjust relative importance of dimensions without code changes. ` +
|
|
310
|
-
`The scoring algorithm embeds a non-obvious design choice: how to normalize disparate units into comparable scores.`,
|
|
311
|
-
decision: `Use min-max normalization per dimension to map all values to [0, 100], then apply configurable weights that sum to 1.0. ` +
|
|
312
|
-
`Round only the final composite score, not individual dimension scores (avoids compounding rounding error). ` +
|
|
313
|
-
`Handle edge case where all options have identical values for a dimension (return score 100 for that dimension). ` +
|
|
314
|
-
`Expose weights as configuration, not hardcoded constants, so domain experts can tune without code changes. ` +
|
|
315
|
-
`Include weights used in every output for reproducibility and audit.`,
|
|
316
|
-
alternatives: [
|
|
317
|
-
makeAlt('Min-max normalization with configurable weights', 'Well-established technique; weights are intuitive for domain experts; handles disparate units', false),
|
|
318
|
-
makeAlt('Z-score normalization', 'Better for normally distributed data but weights are less intuitive for business users', true),
|
|
319
|
-
makeAlt('Rank-based scoring', 'Loses magnitude information — a 10x cost difference scores the same as 1.1x', true),
|
|
320
|
-
makeAlt('Hardcoded scoring rules', 'Requires developer for every threshold change; not auditable', true),
|
|
321
|
-
],
|
|
322
|
-
consequences: [
|
|
323
|
-
makeCons('Domain experts can adjust priorities (cost vs speed vs emissions) without code changes', 'positive'),
|
|
324
|
-
makeCons('Scores are deterministic and reproducible given the same inputs and weights', 'positive'),
|
|
325
|
-
makeCons('Min-max normalization is sensitive to outliers — a single extreme value compresses the rest', 'negative'),
|
|
326
|
-
makeCons('Weight validation (sum to 1.0) must be enforced at both API boundary and adapter level', 'negative'),
|
|
327
|
-
],
|
|
328
|
-
keywords: ['scoring', 'weight', 'normalization', 'multi-objective', 'ranking'],
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
// ── 2. Option Filtering / Selection Strategy ──
|
|
332
|
-
const hasOptionFiltering = /(?:route|routing|modal|intermodal|multi.?modal|transport.*mode|shipping.*mode|freight|upgrade.*option|option.*filter|viab|feasib|eligible|candidate)/i.test(q);
|
|
333
|
-
const hasOptionSplit = /(?:split|distribut|allocat|percentage|ratio|mix|combination|portfolio)/i.test(q)
|
|
334
|
-
&& /(?:option|mode|modal|upgrade|investment|scenario)/i.test(q);
|
|
335
|
-
if (hasOptionFiltering || hasOptionSplit) {
|
|
336
|
-
decisions.push({
|
|
337
|
-
title: `${domain.primary} Option Selection: viability filtering with configurable thresholds and option categorization`,
|
|
338
|
-
context: `${domain.primary} involves selecting between multiple options or strategies. ` +
|
|
339
|
-
`Option viability depends on domain-specific constraints (cost, capacity, timing, regulatory requirements). ` +
|
|
340
|
-
`Combined or hybrid options require defining allocation ratios. ` +
|
|
341
|
-
`Viability thresholds embed non-obvious business knowledge and vary by context (region, property type, asset class).`,
|
|
342
|
-
decision: `Filter viable options by domain-specific thresholds (e.g. budget limits, capacity constraints, regulatory requirements). ` +
|
|
343
|
-
`Make all thresholds configurable via environment variables with sensible defaults. ` +
|
|
344
|
-
`For combined strategies, define allocation ratios as configuration (not hardcoded). ` +
|
|
345
|
-
`Generate recommendations only for viable options — do not score options that fail viability checks. ` +
|
|
346
|
-
`Document option characteristics (cost, impact, timeline, constraints) as a typed configuration object.`,
|
|
347
|
-
alternatives: [
|
|
348
|
-
makeAlt('Threshold-based viability filtering with configurable parameters', 'Fast elimination of non-viable options; thresholds tunable per context', false),
|
|
349
|
-
makeAlt('Score all options regardless of viability', 'Wastes computation; produces misleading recommendations for infeasible options', true),
|
|
350
|
-
makeAlt('Hardcoded selection rules', 'Cannot adapt to regional or contextual differences', true),
|
|
351
|
-
],
|
|
352
|
-
consequences: [
|
|
353
|
-
makeCons('Different deployments can customize thresholds without code changes', 'positive'),
|
|
354
|
-
makeCons('Option allocation ratios are transparent and auditable', 'positive'),
|
|
355
|
-
makeCons('Threshold values require domain expertise to set correctly — wrong defaults produce wrong recommendations', 'negative'),
|
|
356
|
-
],
|
|
357
|
-
keywords: ['option', 'filter', 'viability', 'threshold', 'selection', 'upgrade', 'candidate'],
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
// ── 3. Emissions / Environmental Impact Factor Sourcing ──
|
|
361
|
-
const hasEmissions = /(?:emission|carbon|co2|greenhouse|ghg|sustainab|environmental.*factor|emission.*factor|energy.*efficien|eui|energy.*usage)/i.test(q);
|
|
362
|
-
if (hasEmissions) {
|
|
363
|
-
// Detect the domain type to use appropriate examples
|
|
364
|
-
const isRealEstate = /(?:building|property|propert|real\s*estate|hvac|insulation|lighting|tenant|facility|eui)/i.test(q);
|
|
365
|
-
const emissionsUnit = isRealEstate ? 'kgCO2e/sqft/year per building type and climate zone' : 'gCO2 per unit of activity';
|
|
366
|
-
const emissionsExamples = isRealEstate
|
|
367
|
-
? 'building type, climate zone, age, HVAC system type, and insulation rating'
|
|
368
|
-
: 'region, equipment type, operational parameters, and fuel source';
|
|
369
|
-
const emissionsFrameworks = isRealEstate
|
|
370
|
-
? 'ENERGY STAR Portfolio Manager, CRREM, GRESB, EPA eGRID'
|
|
371
|
-
: 'GLEC Framework, EPA, DEFRA, GHG Protocol';
|
|
372
|
-
decisions.push({
|
|
373
|
-
title: `${domain.primary} Impact Factors: sourcing strategy with regional variation and customization`,
|
|
374
|
-
context: `${domain.primary} calculates environmental impact using emissions/energy factors (e.g. ${emissionsUnit}). ` +
|
|
375
|
-
`These factors vary significantly by ${emissionsExamples}. ` +
|
|
376
|
-
`Using global averages produces inaccurate results for specific assets or regions. ` +
|
|
377
|
-
`The sourcing strategy for these factors is a non-obvious design choice with regulatory and reporting implications.`,
|
|
378
|
-
decision: `Provide default impact factors based on established sources (e.g. ${emissionsFrameworks}). ` +
|
|
379
|
-
`Allow per-request override of factors so users can supply asset-specific or audited values. ` +
|
|
380
|
-
`Store the factors used in every calculation output for auditability. ` +
|
|
381
|
-
`Flag when default (benchmark) factors are used vs. user-supplied measured values in the output metadata. ` +
|
|
382
|
-
`Support regional factor profiles as named configurations (e.g. "EU", "North America", "APAC").`,
|
|
383
|
-
alternatives: [
|
|
384
|
-
makeAlt('Configurable factors with defaults from established frameworks', 'Balances accuracy with usability; supports regulatory audit', false),
|
|
385
|
-
makeAlt('Hardcoded global averages only', 'Inaccurate for specific assets; cannot meet regional regulatory requirements', true),
|
|
386
|
-
makeAlt('Real-time factor API integration', 'Adds external dependency and latency; factors change slowly (annually)', true),
|
|
387
|
-
],
|
|
388
|
-
consequences: [
|
|
389
|
-
makeCons('Calculations are auditable — every output shows which factors were used and their source', 'positive'),
|
|
390
|
-
makeCons('Regional deployments can use jurisdiction-specific factors', 'positive'),
|
|
391
|
-
makeCons('Default factors must be maintained and updated when source frameworks publish new versions', 'negative'),
|
|
392
|
-
],
|
|
393
|
-
keywords: ['emissions', 'carbon', 'environmental', 'factor', 'sustainability', 'energy', 'EUI'],
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
// ── 4. Priority / Constraint Threshold Model ──
|
|
397
|
-
const hasPriority = /(?:sla|service.?level|tolerance|priority|urgency|deadline|lead.?time|delivery.*time|transit.*time|timeline|disruption|impact.*level)/i.test(q);
|
|
398
|
-
const hasThresholds = /(?:threshold|viable|feasib|acceptable|constraint|buffer|slack)/i.test(q);
|
|
399
|
-
if (hasPriority || (hasThresholds && hasOptionFiltering)) {
|
|
400
|
-
const isRealEstate = /(?:building|property|propert|real\s*estate|tenant|facility)/i.test(q);
|
|
401
|
-
const priorityExamples = isRealEstate
|
|
402
|
-
? 'critical (safety/compliance), high (major efficiency gain), standard (planned improvement), low (cosmetic/minor)'
|
|
403
|
-
: 'express: 0 days tolerance, standard: 1 day, economy: 2 days';
|
|
404
|
-
const constraintType = isRealEstate
|
|
405
|
-
? 'tenant disruption level, implementation timeline, and budget constraints'
|
|
406
|
-
: 'time constraints and service level requirements';
|
|
407
|
-
decisions.push({
|
|
408
|
-
title: `${domain.primary} Priority Model: constraint-based viability thresholds with configurable levels`,
|
|
409
|
-
context: `${domain.primary} must filter and prioritize options based on ${constraintType}. Different priority levels ` +
|
|
410
|
-
`(e.g. ${priorityExamples}) have different constraint tolerances. ` +
|
|
411
|
-
`The threshold values embed domain knowledge about acceptable tradeoffs per priority class.`,
|
|
412
|
-
decision: `Define priority levels as a typed enum with associated constraint tolerance values. ` +
|
|
413
|
-
`Filter options by checking if estimated impact ≤ acceptable threshold for the given priority. ` +
|
|
414
|
-
`Make tolerance values configurable per priority level via environment variables. ` +
|
|
415
|
-
`When all options are filtered out (no viable options), return an explicit "no viable options" response rather than an empty array or 500 error.`,
|
|
416
|
-
alternatives: [
|
|
417
|
-
makeAlt('Priority-based constraint thresholds with configurable tolerances', 'Intuitive model; domain experts can adjust per-priority tolerances', false),
|
|
418
|
-
makeAlt('Single global threshold', 'Cannot differentiate high-priority from low-priority — one threshold for all', true),
|
|
419
|
-
makeAlt('No priority filtering (rank all options)', 'Returns options that violate constraints; wastes decision-maker time', true),
|
|
420
|
-
],
|
|
421
|
-
consequences: [
|
|
422
|
-
makeCons('High-priority items never recommend options that violate critical constraints', 'positive'),
|
|
423
|
-
makeCons('Empty-result edge case is handled explicitly, preventing runtime crashes', 'positive'),
|
|
424
|
-
makeCons('Priority-tolerance mapping requires business sign-off; wrong values produce wrong recommendations', 'negative'),
|
|
425
|
-
],
|
|
426
|
-
keywords: ['priority', 'SLA', 'tolerance', 'threshold', 'constraint', 'disruption', 'timeline'],
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
// ── 5. Distance / Cost Estimation Strategy ──
|
|
430
|
-
const hasDistanceEstimation = /(?:distance|haversine|geodesic|great.?circle|route.*length|estimat.*distance)/i.test(q);
|
|
431
|
-
const hasHardcodedData = /(?:hardcode|lookup|table|fallback.*default|unknown.*route|default.*distance)/i.test(q);
|
|
432
|
-
if (hasDistanceEstimation || (hasOptionFiltering && hasHardcodedData)) {
|
|
433
|
-
decisions.push({
|
|
434
|
-
title: `${domain.primary} Distance Estimation: strategy for unknown routes with fallback handling`,
|
|
435
|
-
context: `${domain.primary} needs point-to-point distances for scoring. A lookup table covers known high-volume corridors, ` +
|
|
436
|
-
`but unknown origin-destination pairs fall back to a default value. A naive default (e.g. 5000km) materially affects scoring accuracy — ` +
|
|
437
|
-
`short routes scored as long routes get wrong mode recommendations. The fallback strategy is a critical design choice.`,
|
|
438
|
-
decision: `Use haversine (great-circle) calculation as the primary fallback for unknown routes instead of a fixed default distance. ` +
|
|
439
|
-
`Maintain a lookup table for known high-volume corridors where actual road/rail distances differ significantly from great-circle. ` +
|
|
440
|
-
`Flag outputs that used estimated (non-lookup) distances so decision-makers know the accuracy level. ` +
|
|
441
|
-
`Apply a mode-specific distance multiplier to haversine results (e.g. road: 1.3x, rail: 1.2x, sea: 1.1x) to approximate actual route distances.`,
|
|
442
|
-
alternatives: [
|
|
443
|
-
makeAlt('Haversine fallback with corridor lookup table', 'Reasonable accuracy for unknown routes; exact for known corridors', false),
|
|
444
|
-
makeAlt('Fixed default distance for unknowns', 'Produces materially wrong scores — a 200km route scored as 5000km recommends sea freight', true),
|
|
445
|
-
makeAlt('External routing API (Google Maps, HERE)', 'Adds latency, cost, and external dependency for every comparison', true),
|
|
446
|
-
],
|
|
447
|
-
consequences: [
|
|
448
|
-
makeCons('Unknown routes get reasonable distance estimates instead of a wildly wrong default', 'positive'),
|
|
449
|
-
makeCons('Output transparency — decision-makers see whether distance was exact or estimated', 'positive'),
|
|
450
|
-
makeCons('Haversine underestimates road distance by ~20-40% for complex terrain; multiplier is an approximation', 'negative'),
|
|
451
|
-
],
|
|
452
|
-
keywords: ['distance', 'haversine', 'estimation', 'fallback', 'route', 'corridor'],
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
// ── 6. Comparison Immutability / Audit Chain ──
|
|
456
|
-
const hasComparison = /(?:compar(?:e|ison)|scenario.*(?:save|store|persist)|immutab|audit.*chain|tamper)/i.test(q);
|
|
457
|
-
const hasAuditTrail = /(?:audit|lineage|hash|reproducib|traceab)/i.test(q);
|
|
458
|
-
if (hasComparison && hasAuditTrail) {
|
|
459
|
-
decisions.push({
|
|
460
|
-
title: `${domain.primary} Scenario Comparison: immutability model with content-hash audit chain`,
|
|
461
|
-
context: `${domain.primary} produces scenario comparisons that inform business decisions. Once a comparison is created, ` +
|
|
462
|
-
`it must not change — decision-makers need to trust that the comparison they approved is the same one that was generated. ` +
|
|
463
|
-
`The immutability model and audit chain design embed governance requirements.`,
|
|
464
|
-
decision: `Scenario comparisons are write-once: created via POST, readable via GET, never updated or deleted. ` +
|
|
465
|
-
`Each comparison includes a content hash (SHA-256 of all inputs + weights + results) for tamper detection. ` +
|
|
466
|
-
`Link each comparison to its originating simulation ID for full lineage tracing. ` +
|
|
467
|
-
`Wrap comparison persistence in a database transaction — if the INSERT fails, the caller gets an error, not a phantom comparison. ` +
|
|
468
|
-
`Add a database index on (shipment_id, created_at) for efficient listing queries.`,
|
|
469
|
-
alternatives: [
|
|
470
|
-
makeAlt('Write-once with content hash and simulation lineage', 'Meets audit and compliance requirements; tamper-detectable', false),
|
|
471
|
-
makeAlt('Mutable comparisons', 'Cannot prove which version was approved — audit gap', true),
|
|
472
|
-
makeAlt('In-memory only comparisons', 'Lost on process restart; no audit trail', true),
|
|
473
|
-
],
|
|
474
|
-
consequences: [
|
|
475
|
-
makeCons('Every comparison is traceable back to its simulation and forward to any decisions made from it', 'positive'),
|
|
476
|
-
makeCons('Content hash enables tamper detection without blockchain overhead', 'positive'),
|
|
477
|
-
makeCons('Write-once storage grows continuously — requires archival strategy for old comparisons', 'negative'),
|
|
478
|
-
],
|
|
479
|
-
keywords: ['comparison', 'immutability', 'audit', 'hash', 'lineage', 'tamper'],
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
return decisions;
|
|
483
|
-
}
|
|
484
|
-
function buildADRsFromSPARC(sparc, dossier, scenarioQuery) {
|
|
485
|
-
const adrs = [];
|
|
486
|
-
let idx = 1;
|
|
487
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
488
|
-
const q = scenarioQuery ?? '';
|
|
489
|
-
const { services, securityItems, perfItems, integrationPlan } = normalizeSPARCForADR(sparc);
|
|
490
|
-
// ── Extract domain context from the query ──────────────────────────────
|
|
491
|
-
const domain = extractDomainContext(q);
|
|
492
|
-
const techStack = extractTechFromQuery(q);
|
|
493
|
-
// ======================================================================
|
|
494
|
-
// SPARC §S — Specification: Domain Model ADR
|
|
495
|
-
// What entities exist, how they relate, what invariants constrain them
|
|
496
|
-
// ======================================================================
|
|
497
|
-
if (services.length > 0) {
|
|
498
|
-
const entityNames = services.slice(0, 6).map(s => s.name);
|
|
499
|
-
const depsAll = services.flatMap(s => s.dependencies).filter(Boolean);
|
|
500
|
-
const uniqueDeps = [...new Set(depsAll)];
|
|
501
|
-
adrs.push({
|
|
502
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
503
|
-
title: `Domain Model: ${domain.primary} entity graph with ${entityNames.length} bounded contexts`,
|
|
504
|
-
status: 'proposed',
|
|
505
|
-
date: today,
|
|
506
|
-
context: `The ${domain.primary} domain decomposes into ${entityNames.length} bounded contexts: ${entityNames.join(', ')}. ` +
|
|
507
|
-
`${uniqueDeps.length > 0 ? 'Cross-context dependencies: ' + uniqueDeps.slice(0, 5).join(', ') + '. ' : ''}` +
|
|
508
|
-
`${domain.stakeholders ? 'Stakeholders: ' + domain.stakeholders + '. ' : ''}` +
|
|
509
|
-
`Each context must own its data and enforce its own invariants.`,
|
|
510
|
-
decision: `Model ${domain.primary} as ${entityNames.length} bounded contexts with explicit aggregate roots. ` +
|
|
511
|
-
`${entityNames.slice(0, 3).map(n => `"${n}" owns its entities and exposes a domain API`).join('; ')}. ` +
|
|
512
|
-
`Cross-context communication via domain events (not shared database). ` +
|
|
513
|
-
`${domain.erp ? 'Anti-corruption layer translates between ' + domain.erp + ' wire format and domain types.' : ''}`,
|
|
514
|
-
alternatives: [
|
|
515
|
-
makeAlt(`${entityNames.length} bounded contexts with domain events`, 'Each context evolves independently; matches ' + domain.primary + ' organizational structure', false),
|
|
516
|
-
makeAlt('Shared domain model', 'Faster initial development but all contexts coupled — any schema change ripples everywhere', true),
|
|
517
|
-
makeAlt('CRUD-only without domain model', 'Simpler but business rules scattered across layers, untestable', true),
|
|
518
|
-
],
|
|
519
|
-
consequences: [
|
|
520
|
-
makeCons(`Each ${domain.primary} context can be developed, tested, and deployed independently`, 'positive'),
|
|
521
|
-
makeCons('Domain events provide audit trail of all cross-context interactions', 'positive'),
|
|
522
|
-
makeCons('Requires upfront domain modeling investment before implementation', 'negative'),
|
|
523
|
-
makeCons(`Cross-context queries (e.g. ${entityNames.length > 1 ? entityNames[0] + ' → ' + entityNames[1] : 'aggregation'}) require API composition, not JOINs`, 'negative'),
|
|
524
|
-
],
|
|
525
|
-
sparcSectionRefs: ['specification.requirements', 'architecture.services'],
|
|
526
|
-
researchDossierItemRefs: findDossierRefs(dossier, entityNames),
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
// ======================================================================
|
|
530
|
-
// SPARC §P — Pseudocode: Simulation / Optimization / Core Algorithm ADR
|
|
531
|
-
// HOW the core computation works — not generic, specific to the domain
|
|
532
|
-
// ======================================================================
|
|
533
|
-
const hasSimulation = /simulat|scenario|what.?if|forecast|predict|model(?:ing|ed|s)/i.test(q);
|
|
534
|
-
const hasOptimization = /optimiz|minimize|maximize|tradeoff|trade.?off|pareto|quantif/i.test(q);
|
|
535
|
-
const hasGraphModeling = /graph|network|dependenc|flow|topolog|adjacen/i.test(q);
|
|
536
|
-
if (hasSimulation || hasOptimization || hasGraphModeling) {
|
|
537
|
-
const algoType = hasOptimization ? 'multi-objective optimization'
|
|
538
|
-
: hasGraphModeling ? 'graph-based dependency modeling'
|
|
539
|
-
: 'scenario simulation';
|
|
540
|
-
const rustNote = techStack.includes('Rust')
|
|
541
|
-
? `Implement the compute-intensive ${algoType} engine in Rust for performance (graph traversal, constraint solving, Monte Carlo). TypeScript orchestrates scenario configuration, result aggregation, and API exposure.`
|
|
542
|
-
: `Implement ${algoType} engine with clear separation between scenario configuration (inputs), computation (engine), and result presentation (outputs).`;
|
|
543
|
-
const simOutputs = hasOptimization
|
|
544
|
-
? 'Pareto frontier across competing objectives (cost, risk, emissions, complexity). Each point on the frontier is a decision-grade scenario with full traceoff breakdown.'
|
|
545
|
-
: 'Ranked scenario comparison with metrics per dimension. Each scenario includes confidence intervals, sensitivity analysis, and assumption documentation.';
|
|
546
|
-
adrs.push({
|
|
547
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
548
|
-
title: `${domain.primary} ${hasOptimization ? 'Optimization' : 'Simulation'} Engine: ${algoType} with immutable inputs/outputs`,
|
|
549
|
-
status: 'proposed',
|
|
550
|
-
date: today,
|
|
551
|
-
context: `The system must ${hasOptimization ? 'optimize across competing objectives' : 'simulate multiple strategies'} for ${domain.primary}. ` +
|
|
552
|
-
`Decision-makers need structured, comparative outputs — not automated execution. ` +
|
|
553
|
-
`${hasGraphModeling ? 'The domain involves network/graph structures with dependencies that affect propagation of changes. ' : ''}` +
|
|
554
|
-
`All simulation inputs must be immutable; results must be reproducible and auditable.`,
|
|
555
|
-
decision: `${rustNote} ` +
|
|
556
|
-
`Simulation accepts immutable scenario parameters, runs computation, and produces: ${simOutputs} ` +
|
|
557
|
-
`Results carry a cryptographic hash of inputs for reproducibility verification. ` +
|
|
558
|
-
`No execution occurs from simulation — results are decision-grade outputs for human review.`,
|
|
559
|
-
alternatives: [
|
|
560
|
-
makeAlt(`Dedicated ${algoType} engine with immutable I/O`, 'Auditable, reproducible, supports regulatory review', false),
|
|
561
|
-
makeAlt('Spreadsheet-based modeling', 'Insufficient for multi-dimensional analysis at enterprise scale', true),
|
|
562
|
-
makeAlt('Auto-executing optimizer', 'Violates human-in-the-loop governance requirement', true),
|
|
563
|
-
],
|
|
564
|
-
consequences: [
|
|
565
|
-
makeCons('Every simulation result is reproducible — same inputs always produce same outputs', 'positive'),
|
|
566
|
-
makeCons('Stakeholders compare scenarios side-by-side before committing resources', 'positive'),
|
|
567
|
-
makeCons(techStack.includes('Rust') ? 'Requires Rust/TypeScript FFI boundary and dual-language build pipeline' : 'Compute-intensive scenarios may require dedicated infrastructure', 'negative'),
|
|
568
|
-
makeCons('Scenario parameter space must be bounded to prevent combinatorial explosion', 'negative'),
|
|
569
|
-
],
|
|
570
|
-
sparcSectionRefs: ['pseudocode.modules', 'architecture.dataFlow'],
|
|
571
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['simulation', 'optimization', 'scenario', 'model', 'graph']),
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
// ======================================================================
|
|
575
|
-
// SPARC §P+ — Domain-Specific Algorithmic Decision ADRs
|
|
576
|
-
// Detect algorithmic patterns in the query that warrant dedicated ADRs
|
|
577
|
-
// (ADR-PIPELINE-016: each non-obvious design choice gets its own ADR)
|
|
578
|
-
// ======================================================================
|
|
579
|
-
const algorithmPatterns = detectAlgorithmicDecisions(q, domain);
|
|
580
|
-
for (const algo of algorithmPatterns) {
|
|
581
|
-
adrs.push({
|
|
582
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
583
|
-
title: algo.title,
|
|
584
|
-
status: 'proposed',
|
|
585
|
-
date: today,
|
|
586
|
-
context: algo.context,
|
|
587
|
-
decision: algo.decision,
|
|
588
|
-
alternatives: algo.alternatives,
|
|
589
|
-
consequences: algo.consequences,
|
|
590
|
-
sparcSectionRefs: ['pseudocode.modules', 'architecture.services'],
|
|
591
|
-
researchDossierItemRefs: findDossierRefs(dossier, algo.keywords),
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
// ======================================================================
|
|
595
|
-
// SPARC §A — Architecture: Per-service implementation decisions
|
|
596
|
-
// Each service gets a SPECIFIC decision, not "build as modular service"
|
|
597
|
-
// ======================================================================
|
|
598
|
-
for (const svc of services.slice(0, 6)) {
|
|
599
|
-
// Skip if the service name would duplicate the domain model or simulation ADR
|
|
600
|
-
if (idx > 2 && /domain|model|simul|optim|engine/i.test(svc.name))
|
|
601
|
-
continue;
|
|
602
|
-
const depsText = svc.dependencies.length > 0
|
|
603
|
-
? svc.dependencies.join(', ')
|
|
604
|
-
: '';
|
|
605
|
-
const responsibility = svc.responsibility || svc.name;
|
|
606
|
-
// Determine what KIND of service this is and generate a specific decision
|
|
607
|
-
const isDataIngestion = /ingest|import|extract|collect|read|fetch|sync/i.test(responsibility);
|
|
608
|
-
const isAnalysis = /analy|score|classif|evaluat|assess|rank|compar/i.test(responsibility);
|
|
609
|
-
const isOrchestration = /orchestrat|workflow|pipeline|coordinat|schedul/i.test(responsibility);
|
|
610
|
-
const isPresentation = /report|dashboard|visual|output|present|display|generat.*output/i.test(responsibility);
|
|
611
|
-
const isGovernance = /approv|govern|audit|compliance|lineage|review/i.test(responsibility);
|
|
612
|
-
let specificDecision;
|
|
613
|
-
let specificTitle;
|
|
614
|
-
let specificAlts;
|
|
615
|
-
if (isGovernance) {
|
|
616
|
-
specificTitle = `${svc.name}: Human-in-the-loop approval with immutable audit trail`;
|
|
617
|
-
specificDecision = `Implement ${svc.name} as a state machine (draft → submitted → approved/rejected → executed). ` +
|
|
618
|
-
`Every state transition is logged immutably with actor identity, timestamp, and decision rationale. ` +
|
|
619
|
-
`Approval requires authenticated user with appropriate role — no self-approval, no programmatic bypass. ` +
|
|
620
|
-
`${domain.erp ? 'Only APPROVED decisions can trigger ' + domain.erp + ' writeback.' : ''}`;
|
|
621
|
-
specificAlts = [
|
|
622
|
-
makeAlt('State machine with RBAC and immutable log', 'Meets audit and compliance requirements', false),
|
|
623
|
-
makeAlt('Simple boolean approved flag', 'No state history, no role enforcement, no audit trail', true),
|
|
624
|
-
makeAlt('Manual email-based approval', 'No programmatic enforcement, approval can be forged', true),
|
|
625
|
-
];
|
|
626
|
-
}
|
|
627
|
-
else if (isDataIngestion) {
|
|
628
|
-
specificTitle = `${svc.name}: ${domain.erp ? domain.erp + ' data ingestion' : 'External data ingestion'} with validation`;
|
|
629
|
-
specificDecision = `${svc.name} ingests data from ${depsText || (domain.erp ?? 'external sources')} through an anti-corruption layer. ` +
|
|
630
|
-
`All incoming records are validated against domain schemas before entering the system. ` +
|
|
631
|
-
`Invalid records are quarantined with error details, not silently dropped. ` +
|
|
632
|
-
`Ingestion is idempotent — re-running the same import produces identical state.`;
|
|
633
|
-
specificAlts = [
|
|
634
|
-
makeAlt('ACL with schema validation and quarantine', 'Protects domain model from external data quality issues', false),
|
|
635
|
-
makeAlt('Direct database import', 'Couples domain model to external schemas — any upstream change breaks the system', true),
|
|
636
|
-
];
|
|
637
|
-
}
|
|
638
|
-
else if (isAnalysis) {
|
|
639
|
-
specificTitle = `${svc.name}: Domain-specific ${/score|rank/i.test(responsibility) ? 'scoring' : 'analysis'} with configurable weights`;
|
|
640
|
-
specificDecision = `${svc.name} implements ${responsibility.slice(0, 100)}. ` +
|
|
641
|
-
`Scoring weights and thresholds are externalized as configuration (not hardcoded constants) so domain experts can tune them without code changes. ` +
|
|
642
|
-
`All analysis outputs include the weights used, enabling reproducibility and audit. ` +
|
|
643
|
-
`${depsText ? 'Consumes data from: ' + depsText + '.' : ''}`;
|
|
644
|
-
specificAlts = [
|
|
645
|
-
makeAlt('Configurable analysis with weight transparency', 'Domain experts can tune; results are auditable', false),
|
|
646
|
-
makeAlt('Hardcoded scoring rules', 'Faster initially but requires developer for every threshold change', true),
|
|
647
|
-
];
|
|
648
|
-
}
|
|
649
|
-
else if (isOrchestration) {
|
|
650
|
-
specificTitle = `${svc.name}: Workflow orchestration with checkpoint and resume`;
|
|
651
|
-
specificDecision = `${svc.name} coordinates: ${responsibility.slice(0, 120)}. ` +
|
|
652
|
-
`Each workflow step is checkpointed so failures resume from last successful step, not from scratch. ` +
|
|
653
|
-
`Workflow state is persisted (not in-memory) to survive process restarts. ` +
|
|
654
|
-
`${depsText ? 'Orchestrates: ' + depsText + '.' : ''}`;
|
|
655
|
-
specificAlts = [
|
|
656
|
-
makeAlt('Persistent workflow with checkpoints', 'Resilient to failures, supports long-running processes', false),
|
|
657
|
-
makeAlt('In-memory orchestration', 'Process restart loses all workflow state', true),
|
|
658
|
-
];
|
|
659
|
-
}
|
|
660
|
-
else if (isPresentation) {
|
|
661
|
-
specificTitle = `${svc.name}: Decision-grade output generation with lineage`;
|
|
662
|
-
specificDecision = `${svc.name} generates structured outputs for ${domain.stakeholders || 'leadership review'}. ` +
|
|
663
|
-
`Every output traces back to the simulation/analysis that produced it (lineage hash). ` +
|
|
664
|
-
`Outputs include: comparative metrics, tradeoff visualization data, confidence indicators, and assumption documentation. ` +
|
|
665
|
-
`${domain.erp ? 'Optionally formats approved outputs for ' + domain.erp + ' writeback.' : ''}`;
|
|
666
|
-
specificAlts = [
|
|
667
|
-
makeAlt('Structured outputs with lineage tracing', 'Decision-makers can verify what analysis produced each recommendation', false),
|
|
668
|
-
makeAlt('Free-form text reports', 'No traceability, no structured comparison', true),
|
|
669
|
-
];
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
// Generic but still domain-specific — use the actual responsibility text
|
|
673
|
-
specificTitle = `${svc.name}: ${responsibility.slice(0, 70)}`;
|
|
674
|
-
specificDecision = `Implement ${svc.name} to handle: ${responsibility}. ` +
|
|
675
|
-
`${depsText ? 'Integrates with ' + depsText + ' via typed interfaces (not direct coupling). ' : ''}` +
|
|
676
|
-
`Expose domain operations as a service API. All state mutations produce domain events for downstream consumers. ` +
|
|
677
|
-
`${techStack.includes('Rust') && /comput|optim|graph|simul/i.test(responsibility) ? 'Implement compute-intensive logic in Rust.' : ''}`;
|
|
678
|
-
specificAlts = [
|
|
679
|
-
makeAlt(`Dedicated ${svc.name} service with domain API`, `Encapsulates ${domain.primary} business rules for ${svc.name}`, false),
|
|
680
|
-
makeAlt('Inline in monolith', 'Couples ' + svc.name + ' logic to unrelated concerns', true),
|
|
681
|
-
];
|
|
682
|
-
}
|
|
683
|
-
adrs.push({
|
|
684
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
685
|
-
title: specificTitle,
|
|
686
|
-
status: 'proposed',
|
|
687
|
-
date: today,
|
|
688
|
-
context: `${domain.primary} requires: ${responsibility.slice(0, 200)}. ` +
|
|
689
|
-
`${depsText ? 'Dependencies: ' + depsText + '. ' : ''}` +
|
|
690
|
-
`This service is part of the ${domain.primary} solution.`,
|
|
691
|
-
decision: specificDecision,
|
|
692
|
-
alternatives: specificAlts,
|
|
693
|
-
consequences: [
|
|
694
|
-
makeCons(`${svc.name} business rules are testable in isolation`, 'positive'),
|
|
695
|
-
makeCons(`Clear API contract for ${svc.name} enables parallel team development`, 'positive'),
|
|
696
|
-
makeCons(`Requires explicit interface definition between ${svc.name} and its consumers`, 'negative'),
|
|
697
|
-
],
|
|
698
|
-
sparcSectionRefs: [`architecture.services.${svc.name}`],
|
|
699
|
-
researchDossierItemRefs: findDossierRefs(dossier, [svc.name, ...svc.dependencies]),
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
// ======================================================================
|
|
703
|
-
// SPARC §A — Architecture: ERP Integration ADR (if ERP detected)
|
|
704
|
-
// ======================================================================
|
|
705
|
-
if (domain.erp) {
|
|
706
|
-
adrs.push({
|
|
707
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
708
|
-
title: `${domain.erp} Integration: Anti-corruption layer with governance-gated writeback`,
|
|
709
|
-
status: 'proposed',
|
|
710
|
-
date: today,
|
|
711
|
-
context: `${domain.primary} data originates in ${domain.erp}. The AI system reads operational data for analysis/simulation ` +
|
|
712
|
-
`and optionally writes approved decisions back. Direct coupling to ${domain.erp} internals would make the domain model fragile.`,
|
|
713
|
-
decision: `Build an anti-corruption layer (ACL) between ${domain.erp} and the ${domain.primary} domain. ` +
|
|
714
|
-
`The ACL translates ${domain.erp} wire formats into domain types and vice versa. ` +
|
|
715
|
-
`Read mode is the default; writeback is gated behind APPROVED governance status. ` +
|
|
716
|
-
`Circuit breaker + sync queue handle ${domain.erp} outages. ` +
|
|
717
|
-
`All ${domain.erp} operations carry idempotency keys to prevent duplicate writes on retry.`,
|
|
718
|
-
alternatives: [
|
|
719
|
-
makeAlt(`ACL with governance-gated writeback`, `Protects ${domain.erp} integrity; enables AI-assisted decisions without risk`, false),
|
|
720
|
-
makeAlt(`Direct ${domain.erp} API calls from services`, `Tight coupling; ${domain.erp} schema changes break domain model`, true),
|
|
721
|
-
makeAlt('Full data replication to local DB', `Stale data risk; doubles storage cost; compliance concerns with data residency`, true),
|
|
722
|
-
],
|
|
723
|
-
consequences: [
|
|
724
|
-
makeCons(`${domain.erp} schema changes don't cascade to domain services`, 'positive'),
|
|
725
|
-
makeCons(`Writeback requires human approval — no accidental ${domain.erp} mutations`, 'positive'),
|
|
726
|
-
makeCons(`ACL translation layer must track ${domain.erp} API version changes`, 'negative'),
|
|
727
|
-
],
|
|
728
|
-
sparcSectionRefs: ['architecture.services'],
|
|
729
|
-
researchDossierItemRefs: findDossierRefs(dossier, [domain.erp.toLowerCase(), 'erp', 'integration']),
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
// ======================================================================
|
|
733
|
-
// SPARC §A — Architecture: Data Persistence ADR (technology-specific)
|
|
734
|
-
// ======================================================================
|
|
735
|
-
const databases = techStack.filter(t => /postgres|mysql|bigquery|mongodb|redis|dynamodb|cloud sql/i.test(t));
|
|
736
|
-
const cloudProvider = techStack.find(t => /google cloud|aws|azure|gcp/i.test(t));
|
|
737
|
-
if (databases.length > 0 || cloudProvider) {
|
|
738
|
-
adrs.push({
|
|
739
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
740
|
-
title: `Data Persistence: ${[...databases, cloudProvider].filter(Boolean).join(' + ')} for ${domain.primary}`,
|
|
741
|
-
status: 'proposed',
|
|
742
|
-
date: today,
|
|
743
|
-
context: `${domain.primary} data includes: domain entities, ${hasSimulation ? 'simulation results, ' : ''}audit trail, and operational records. ` +
|
|
744
|
-
`User specified: ${techStack.join(', ')}. ` +
|
|
745
|
-
`Audit data must be immutable. Simulation results must be reproducible.`,
|
|
746
|
-
decision: databases.map(db => {
|
|
747
|
-
if (/bigquery/i.test(db))
|
|
748
|
-
return `**BigQuery** for analytical queries, historical trend analysis, and reporting aggregations.`;
|
|
749
|
-
if (/postgres/i.test(db))
|
|
750
|
-
return `**Cloud SQL (PostgreSQL)** for transactional domain data, audit logs, and governance state. Use row-level security for multi-tenant isolation.`;
|
|
751
|
-
if (/redis/i.test(db))
|
|
752
|
-
return `**Redis** for caching frequently-read reference data and session state.`;
|
|
753
|
-
if (/mongodb/i.test(db))
|
|
754
|
-
return `**MongoDB** for flexible document storage of simulation configurations and results.`;
|
|
755
|
-
return `**${db}** for persistent domain data.`;
|
|
756
|
-
}).join(' ') +
|
|
757
|
-
(cloudProvider ? ` Deploy on ${cloudProvider} managed services to reduce operational burden.` : ''),
|
|
758
|
-
alternatives: [
|
|
759
|
-
makeAlt(`${databases.join(' + ')} on ${cloudProvider ?? 'managed cloud'}`, 'Matches specified tech stack; managed services reduce ops burden', false),
|
|
760
|
-
makeAlt('Single-database architecture', 'Simpler but cannot optimize for both transactional and analytical workloads', true),
|
|
761
|
-
makeAlt('In-memory only', 'Process restart destroys all data — compliance disqualifier', true),
|
|
762
|
-
],
|
|
763
|
-
consequences: [
|
|
764
|
-
makeCons('Polyglot persistence optimizes each data access pattern', 'positive'),
|
|
765
|
-
makeCons(`${cloudProvider ?? 'Cloud'} managed services handle backups, scaling, and failover`, 'positive'),
|
|
766
|
-
makeCons('Multiple data stores increase operational complexity and migration effort', 'negative'),
|
|
767
|
-
],
|
|
768
|
-
sparcSectionRefs: ['architecture.services'],
|
|
769
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['data', 'database', 'storage', 'persistence', ...databases.map(d => d.toLowerCase())]),
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
// ======================================================================
|
|
773
|
-
// SPARC §R — Refinement: Security & Compliance ADR
|
|
774
|
-
// ======================================================================
|
|
775
|
-
const secText = itemsToText(securityItems);
|
|
776
|
-
const hasCompliance = /audit|compliance|regulatory|governance|lineage|human.?in.?the.?loop|esg|esg/i.test(q);
|
|
777
|
-
if (secText || hasCompliance) {
|
|
778
|
-
const complianceTerms = (q.match(/audit(?:ability)?|compliance|regulatory|governance|ESG|lineage|human.?in.?the.?loop|GDPR|HIPAA|SOX/gi) ?? []);
|
|
779
|
-
const uniqueTerms = [...new Set(complianceTerms.map(t => t.toLowerCase()))];
|
|
780
|
-
adrs.push({
|
|
781
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
782
|
-
title: `Security & Compliance: ${uniqueTerms.slice(0, 3).join(', ') || 'RBAC and audit trail'} for ${domain.primary}`,
|
|
783
|
-
status: 'proposed',
|
|
784
|
-
date: today,
|
|
785
|
-
context: `${domain.primary} decisions affect ${domain.stakeholders || 'operational teams and leadership'}. ` +
|
|
786
|
-
`${secText ? 'Security requirements: ' + secText + '. ' : ''}` +
|
|
787
|
-
`${uniqueTerms.length > 0 ? 'Compliance domains: ' + uniqueTerms.join(', ') + '. ' : ''}` +
|
|
788
|
-
`The system must maintain strict auditability: who decided what, when, and based on which analysis.`,
|
|
789
|
-
decision: `Implement RBAC with domain-specific roles (not just admin/user). ` +
|
|
790
|
-
`Define roles mapped to ${domain.primary} operations: who can run simulations, who can approve decisions, who can trigger ${domain.erp ?? 'ERP'} writeback. ` +
|
|
791
|
-
`All state-changing operations produce immutable audit entries with actor identity, timestamp, and operation details. ` +
|
|
792
|
-
`${hasSimulation ? 'Simulation results are write-once with content hash for tamper detection. ' : ''}` +
|
|
793
|
-
`Human-in-the-loop is mandatory before any changes propagate to operational systems.`,
|
|
794
|
-
alternatives: [
|
|
795
|
-
makeAlt(`RBAC + immutable audit + ${hasSimulation ? 'simulation lineage' : 'operation lineage'}`, 'Meets regulatory requirements; provides full traceability', false),
|
|
796
|
-
makeAlt('Simple API key authentication', 'No role differentiation; anyone with the key can approve decisions', true),
|
|
797
|
-
makeAlt('Audit logging without RBAC', 'Records who did what but cannot prevent unauthorized actions', true),
|
|
798
|
-
],
|
|
799
|
-
consequences: [
|
|
800
|
-
makeCons(`Full decision traceability for ${uniqueTerms[0] ?? 'regulatory'} compliance`, 'positive'),
|
|
801
|
-
makeCons('No automated execution without human approval', 'positive'),
|
|
802
|
-
makeCons('Role definition and permission matrix must be maintained as organization evolves', 'negative'),
|
|
803
|
-
makeCons('Immutable audit log storage grows continuously — requires archival strategy', 'negative'),
|
|
804
|
-
],
|
|
805
|
-
sparcSectionRefs: ['refinement.securityConsiderations'],
|
|
806
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['security', 'auth', 'access', 'compliance', 'audit', 'governance']),
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
// ======================================================================
|
|
810
|
-
// SPARC §R — Refinement: Performance ADR (if Rust or compute-intensive)
|
|
811
|
-
// ======================================================================
|
|
812
|
-
const perfText = itemsToText(perfItems);
|
|
813
|
-
if (perfText || techStack.includes('Rust')) {
|
|
814
|
-
adrs.push({
|
|
815
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
816
|
-
title: `Performance: ${techStack.includes('Rust') ? 'Rust compute engine + TypeScript orchestration' : 'Optimization strategy for ' + domain.primary}`,
|
|
817
|
-
status: 'proposed',
|
|
818
|
-
date: today,
|
|
819
|
-
context: `${perfText ? 'Performance requirements: ' + perfText + '. ' : ''}` +
|
|
820
|
-
`${techStack.includes('Rust') ? 'The user specified Rust for compute-intensive components and TypeScript for orchestration. ' : ''}` +
|
|
821
|
-
`${hasSimulation ? 'Simulation/optimization workloads require bounded execution time for interactive use.' : ''}`,
|
|
822
|
-
decision: `${techStack.includes('Rust') ? 'Split computation: Rust handles graph traversal, constraint solving, and numerical optimization. TypeScript handles API routing, workflow orchestration, and result formatting. Communication via typed JSON over stdin/stdout (subprocess) or FFI (napi-rs). ' : ''}` +
|
|
823
|
-
`All compute operations have configurable timeouts. Results include execution time metadata. ` +
|
|
824
|
-
`${hasSimulation ? 'Simulation parameter space is bounded (max iterations, max graph depth) to prevent runaway computation.' : ''}`,
|
|
825
|
-
alternatives: [
|
|
826
|
-
makeAlt(techStack.includes('Rust') ? 'Rust engine + TypeScript orchestration' : 'Targeted optimization of critical paths', 'Best performance where it matters; orchestration stays in TypeScript for productivity', false),
|
|
827
|
-
makeAlt('Pure TypeScript', techStack.includes('Rust') ? 'Simpler build but 10-100x slower for graph/numeric workloads' : 'Simpler but may not meet latency targets', true),
|
|
828
|
-
],
|
|
829
|
-
consequences: [
|
|
830
|
-
makeCons(techStack.includes('Rust') ? 'Compute-intensive operations run at native speed' : 'Critical paths are optimized based on profiling data', 'positive'),
|
|
831
|
-
makeCons(techStack.includes('Rust') ? 'Dual-language build pipeline increases CI/CD complexity' : 'Optimization effort must be validated with benchmarks', 'negative'),
|
|
832
|
-
],
|
|
833
|
-
sparcSectionRefs: ['refinement.performanceTargets'],
|
|
834
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['performance', 'latency', 'throughput', 'rust', 'compute']),
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
// ======================================================================
|
|
838
|
-
// SPARC §R — Refinement: High-severity risk ADRs from dossier
|
|
839
|
-
// ======================================================================
|
|
840
|
-
const highRisks = dossier.items.filter(i => i.category === 'risk' && i.severity && /high|critical/i.test(i.severity));
|
|
841
|
-
for (const risk of highRisks.slice(0, 2)) {
|
|
842
|
-
adrs.push({
|
|
843
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
844
|
-
title: `Risk Mitigation: ${risk.title.slice(0, 70)}`,
|
|
845
|
-
status: 'proposed',
|
|
846
|
-
date: today,
|
|
847
|
-
context: risk.content,
|
|
848
|
-
decision: `Mitigate "${risk.title}" with: (1) input validation at the service boundary where this risk originates, ` +
|
|
849
|
-
`(2) monitoring and alerting for early detection of the failure mode, ` +
|
|
850
|
-
`(3) graceful degradation path that preserves data integrity when the risk materializes. ` +
|
|
851
|
-
`Document specific controls in the affected service's test suite.`,
|
|
852
|
-
alternatives: [
|
|
853
|
-
makeAlt('Active boundary controls + monitoring + degradation', 'Prevents impact; detects early; degrades gracefully', false),
|
|
854
|
-
makeAlt('Accept risk and monitor', 'Lower effort but reactive — damage occurs before detection', true),
|
|
855
|
-
],
|
|
856
|
-
consequences: [
|
|
857
|
-
makeCons(`"${risk.title.slice(0, 40)}" mitigated with layered controls`, 'positive'),
|
|
858
|
-
makeCons('Mitigation logic adds complexity to the affected service boundary', 'negative'),
|
|
859
|
-
],
|
|
860
|
-
sparcSectionRefs: [],
|
|
861
|
-
researchDossierItemRefs: [risk.id],
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
// ======================================================================
|
|
865
|
-
// SPARC §C — Completion: Deployment & Integration ADR
|
|
866
|
-
// ======================================================================
|
|
867
|
-
const deployTech = techStack.filter(t => /cloud run|cloud function|lambda|fargate|ecs|kubernetes|k8s|docker/i.test(t));
|
|
868
|
-
if (deployTech.length > 0 || cloudProvider) {
|
|
869
|
-
adrs.push({
|
|
870
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
871
|
-
title: `Deployment: ${deployTech.join(' + ') || cloudProvider || 'Cloud'} with environment-based configuration`,
|
|
872
|
-
status: 'proposed',
|
|
873
|
-
date: today,
|
|
874
|
-
context: `${domain.primary} services deploy to ${cloudProvider ?? 'cloud infrastructure'}. ` +
|
|
875
|
-
`${deployTech.length > 0 ? 'Specified: ' + deployTech.join(', ') + '. ' : ''}` +
|
|
876
|
-
`Services must be stateless (state in databases), independently deployable, and environment-configurable.`,
|
|
877
|
-
decision: `Deploy each service as a stateless container on ${deployTech[0] || cloudProvider || 'managed cloud compute'}. ` +
|
|
878
|
-
`All configuration via environment variables (database URLs, API keys, feature flags) — zero hardcoded secrets. ` +
|
|
879
|
-
`Health checks on every service endpoint. Structured JSON logging to cloud logging service. ` +
|
|
880
|
-
`${integrationPlan ? 'Integration plan: ' + integrationPlan.slice(0, 150) + '.' : ''}`,
|
|
881
|
-
alternatives: [
|
|
882
|
-
makeAlt(`${deployTech[0] || 'Serverless'} with env-based config`, 'Matches specified infrastructure; scales automatically', false),
|
|
883
|
-
makeAlt('VM-based deployment', 'More control but higher operational overhead', true),
|
|
884
|
-
],
|
|
885
|
-
consequences: [
|
|
886
|
-
makeCons('Zero-downtime deployments with rolling updates', 'positive'),
|
|
887
|
-
makeCons('Environment parity (dev/staging/prod) via configuration only', 'positive'),
|
|
888
|
-
makeCons('Cold start latency for serverless (mitigated with min instances)', 'negative'),
|
|
889
|
-
],
|
|
890
|
-
sparcSectionRefs: ['completion.integrationPlan'],
|
|
891
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['deploy', 'cloud', 'infrastructure', 'container']),
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
// ======================================================================
|
|
895
|
-
// ADR-PIPELINE-028: Guaranteed Minimum ADR Set
|
|
896
|
-
// Every pipeline run MUST produce ≥8 ADRs regardless of SPARC richness.
|
|
897
|
-
// These fire only when the conditional branches above didn't cover them.
|
|
898
|
-
// ======================================================================
|
|
899
|
-
function hasADRCovering(...keywords) {
|
|
900
|
-
return adrs.some(a => {
|
|
901
|
-
const text = `${a.title} ${a.decision}`.toLowerCase();
|
|
902
|
-
return keywords.some(kw => text.includes(kw));
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
// 1. Recommendation-only pattern (human-in-the-loop)
|
|
906
|
-
if (!hasADRCovering('human-in-the-loop', 'recommendation-only', 'no auto-execution', 'approval')) {
|
|
907
|
-
adrs.push({
|
|
908
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
909
|
-
title: `${domain.primary}: Recommendation-Only Pattern — No Automated Execution`,
|
|
910
|
-
status: 'proposed',
|
|
911
|
-
date: today,
|
|
912
|
-
context: `The ${domain.primary} system analyzes data and generates recommendations. ` +
|
|
913
|
-
`Users explicitly stated the system should not automatically enforce decisions. ` +
|
|
914
|
-
`All actions that affect operational systems require human review and approval.`,
|
|
915
|
-
decision: `The system operates in recommendation-only mode. AI analysis produces ranked options with tradeoff data. ` +
|
|
916
|
-
`No recommendation is executed automatically — every action requires explicit human approval via the governance workflow. ` +
|
|
917
|
-
`${domain.erp ? `${domain.erp} writeback is blocked until a decision reaches APPROVED status.` : 'External system writes are blocked until approval.'}`,
|
|
918
|
-
alternatives: [
|
|
919
|
-
makeAlt('Recommendation-only with human approval gate', 'Preserves decision authority; meets governance requirements', false),
|
|
920
|
-
makeAlt('Auto-execution with override option', 'Faster but removes human judgment from consequential decisions', true),
|
|
921
|
-
makeAlt('Advisory-only with no action pathway', 'Safe but provides no mechanism to act on insights', true),
|
|
922
|
-
],
|
|
923
|
-
consequences: [
|
|
924
|
-
makeCons('Decision authority stays with domain experts, not the AI system', 'positive'),
|
|
925
|
-
makeCons('Full audit trail of who approved what and why', 'positive'),
|
|
926
|
-
makeCons('Slower time-to-action compared to automated execution', 'negative'),
|
|
927
|
-
],
|
|
928
|
-
sparcSectionRefs: [],
|
|
929
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['approval', 'human', 'recommend', 'decision']),
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
// 2. ERP integration strategy
|
|
933
|
-
if (!hasADRCovering('erp', 'integration', 'anti-corruption', 'writeback') && domain.erp) {
|
|
934
|
-
adrs.push({
|
|
935
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
936
|
-
title: `${domain.erp} Integration: API-Based Read with Governance-Gated Writeback`,
|
|
937
|
-
status: 'proposed',
|
|
938
|
-
date: today,
|
|
939
|
-
context: `${domain.primary} data resides in ${domain.erp}. The system must read operational data for analysis ` +
|
|
940
|
-
`and optionally write approved decisions back. Direct coupling to ${domain.erp} internals would create fragility.`,
|
|
941
|
-
decision: `Integrate via ${domain.erp} standard APIs with an anti-corruption layer. ` +
|
|
942
|
-
`Read operations use standard API endpoints with pagination and rate limit handling. ` +
|
|
943
|
-
`Write operations are gated behind APPROVED governance status — no unapproved writes. ` +
|
|
944
|
-
`All API calls include idempotency keys to prevent duplicate operations on retry.`,
|
|
945
|
-
alternatives: [
|
|
946
|
-
makeAlt(`${domain.erp} API with ACL`, 'Decouples domain from ERP schema changes', false),
|
|
947
|
-
makeAlt('Direct database access', 'Bypasses API rate limits but couples to internal schema', true),
|
|
948
|
-
makeAlt('File-based batch export/import', 'Simple but no real-time capability and error-prone', true),
|
|
949
|
-
],
|
|
950
|
-
consequences: [
|
|
951
|
-
makeCons(`${domain.erp} schema changes don't cascade to domain logic`, 'positive'),
|
|
952
|
-
makeCons('Writeback requires explicit approval — prevents accidental data mutation', 'positive'),
|
|
953
|
-
makeCons('API rate limits may constrain data refresh frequency', 'negative'),
|
|
954
|
-
],
|
|
955
|
-
sparcSectionRefs: [],
|
|
956
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['erp', 'integration', domain.erp?.toLowerCase() ?? '']),
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
// 3. Data model and domain entity design
|
|
960
|
-
if (!hasADRCovering('domain model', 'bounded context', 'entity graph', 'data model')) {
|
|
961
|
-
adrs.push({
|
|
962
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
963
|
-
title: `${domain.primary}: Domain Model with Typed Entities and Validation`,
|
|
964
|
-
status: 'proposed',
|
|
965
|
-
date: today,
|
|
966
|
-
context: `The ${domain.primary} system must model domain entities with strict type safety ` +
|
|
967
|
-
`and validation at system boundaries. Data flows from ${domain.erp ?? 'external sources'} through analysis to decisions.`,
|
|
968
|
-
decision: `Define all domain entities as TypeScript interfaces with discriminated union types for status fields. ` +
|
|
969
|
-
`Validate all external data at ingestion boundaries using runtime schema validation. ` +
|
|
970
|
-
`Domain entities are immutable value objects — state changes produce new instances with audit trail.`,
|
|
971
|
-
alternatives: [
|
|
972
|
-
makeAlt('Typed domain model with boundary validation', 'Prevents invalid state; enables exhaustive pattern matching', false),
|
|
973
|
-
makeAlt('Loose JSON objects', 'Flexible but runtime errors from missing/wrong fields', true),
|
|
974
|
-
],
|
|
975
|
-
consequences: [
|
|
976
|
-
makeCons('Type errors caught at compile time, not runtime', 'positive'),
|
|
977
|
-
makeCons('Requires upfront type definition effort', 'negative'),
|
|
978
|
-
],
|
|
979
|
-
sparcSectionRefs: ['specification.requirements'],
|
|
980
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['data', 'model', 'entity', 'type']),
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
// 4. Scoring/ranking algorithm
|
|
984
|
-
if (!hasADRCovering('scoring', 'ranking', 'algorithm', 'weight')) {
|
|
985
|
-
adrs.push({
|
|
986
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
987
|
-
title: `${domain.primary}: Multi-Factor Scoring with Configurable Weights`,
|
|
988
|
-
status: 'proposed',
|
|
989
|
-
date: today,
|
|
990
|
-
context: `The system must prioritize and rank ${domain.primary.toLowerCase()} items by multiple criteria. ` +
|
|
991
|
-
`Different stakeholders may weight criteria differently (cost vs. sustainability vs. operational impact).`,
|
|
992
|
-
decision: `Implement a multi-factor scoring engine with externalized, configurable weights. ` +
|
|
993
|
-
`Every score includes the weight vector used, enabling reproducibility and audit. ` +
|
|
994
|
-
`Default weights are calibrated from domain benchmarks but can be overridden per analysis run.`,
|
|
995
|
-
alternatives: [
|
|
996
|
-
makeAlt('Multi-factor scoring with configurable weights', 'Transparent, auditable, domain-expert tunable', false),
|
|
997
|
-
makeAlt('Single-metric ranking', 'Simpler but ignores tradeoff complexity', true),
|
|
998
|
-
makeAlt('ML-based ranking', 'Potentially more accurate but requires training data and is less explainable', true),
|
|
999
|
-
],
|
|
1000
|
-
consequences: [
|
|
1001
|
-
makeCons('Domain experts can tune priorities without code changes', 'positive'),
|
|
1002
|
-
makeCons('Score decomposition enables "why this ranking?" explanations', 'positive'),
|
|
1003
|
-
makeCons('Weight selection requires domain expertise and periodic review', 'negative'),
|
|
1004
|
-
],
|
|
1005
|
-
sparcSectionRefs: ['pseudocode.modules'],
|
|
1006
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['score', 'rank', 'priority', 'weight', 'criteria']),
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
// 5. Audit trail and decision traceability
|
|
1010
|
-
if (!hasADRCovering('audit trail', 'traceability', 'immutable', 'audit log')) {
|
|
1011
|
-
adrs.push({
|
|
1012
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
1013
|
-
title: `${domain.primary}: Immutable Audit Trail with Decision Lineage`,
|
|
1014
|
-
status: 'proposed',
|
|
1015
|
-
date: today,
|
|
1016
|
-
context: `Decisions in ${domain.primary.toLowerCase()} affect ${domain.stakeholders || 'operational teams, compliance, and leadership'}. ` +
|
|
1017
|
-
`Regulatory and governance requirements demand a clear record of how insights are generated and decisions are made.`,
|
|
1018
|
-
decision: `Implement append-only audit trail. Every analysis run, recommendation, approval, and ${domain.erp ?? 'system'} action ` +
|
|
1019
|
-
`is logged with: timestamp, actor identity, action type, input parameters, and outcome. ` +
|
|
1020
|
-
`Audit entries are immutable — corrections create new entries referencing the original. ` +
|
|
1021
|
-
`Decision lineage traces from ${domain.erp ?? 'data source'} → analysis → recommendation → approval → action.`,
|
|
1022
|
-
alternatives: [
|
|
1023
|
-
makeAlt('Immutable append-only audit log', 'Complete traceability; meets compliance requirements', false),
|
|
1024
|
-
makeAlt('Mutable log table', 'Allows corrections but breaks audit integrity', true),
|
|
1025
|
-
makeAlt('No structured audit', 'Relies on application logs which are not queryable or tamper-evident', true),
|
|
1026
|
-
],
|
|
1027
|
-
consequences: [
|
|
1028
|
-
makeCons('Any decision can be traced back to the analysis that produced it', 'positive'),
|
|
1029
|
-
makeCons('Compliance reviewers can verify the decision chain end-to-end', 'positive'),
|
|
1030
|
-
makeCons('Audit log storage grows continuously — requires archival strategy', 'negative'),
|
|
1031
|
-
],
|
|
1032
|
-
sparcSectionRefs: [],
|
|
1033
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['audit', 'compliance', 'governance', 'trace']),
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
// 6. Monolithic prototype vs. microservice architecture
|
|
1037
|
-
if (!hasADRCovering('monolith', 'prototype', 'microservice')) {
|
|
1038
|
-
adrs.push({
|
|
1039
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
1040
|
-
title: `${domain.primary}: Monolithic Library for Prototype, Decomposable for Production`,
|
|
1041
|
-
status: 'proposed',
|
|
1042
|
-
date: today,
|
|
1043
|
-
context: `The SPARC architecture specifies multiple services. For the prototype evaluation period, ` +
|
|
1044
|
-
`a monolithic library with clean module boundaries is more appropriate than premature microservice decomposition.`,
|
|
1045
|
-
decision: `Build as a single TypeScript library with clean module boundaries (analysis/, decisions/, erp/, data/). ` +
|
|
1046
|
-
`Each module has a barrel export and communicates via typed interfaces, not shared state. ` +
|
|
1047
|
-
`This structure enables decomposition into independent services when the pilot validates the approach.`,
|
|
1048
|
-
alternatives: [
|
|
1049
|
-
makeAlt('Monolithic library with module boundaries', 'Fast to build, test, and demo; decomposable later', false),
|
|
1050
|
-
makeAlt('Microservices from day one', 'Premature complexity for a prototype; slows iteration', true),
|
|
1051
|
-
makeAlt('Single-file script', 'Fast but untestable, unmaintainable, and impossible to decompose', true),
|
|
1052
|
-
],
|
|
1053
|
-
consequences: [
|
|
1054
|
-
makeCons('Prototype can be built and demonstrated quickly', 'positive'),
|
|
1055
|
-
makeCons('Module boundaries enforce separation of concerns without deployment overhead', 'positive'),
|
|
1056
|
-
makeCons('Must resist the temptation to create cross-module shortcuts that prevent later decomposition', 'negative'),
|
|
1057
|
-
],
|
|
1058
|
-
sparcSectionRefs: ['architecture.services'],
|
|
1059
|
-
researchDossierItemRefs: [],
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
// 7. Human-in-the-loop approval workflow
|
|
1063
|
-
if (!hasADRCovering('approval workflow', 'approval chain', 'governance workflow', 'state machine')) {
|
|
1064
|
-
adrs.push({
|
|
1065
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
1066
|
-
title: `${domain.primary}: Approval Workflow with Role-Based Gates`,
|
|
1067
|
-
status: 'proposed',
|
|
1068
|
-
date: today,
|
|
1069
|
-
context: `${domain.primary} recommendations must be reviewed and approved before affecting operational systems. ` +
|
|
1070
|
-
`The approval process must support multiple approvers and record the rationale for each decision.`,
|
|
1071
|
-
decision: `Implement approval as a state machine: draft → pending_review → approved/rejected. ` +
|
|
1072
|
-
`Each transition requires an authenticated actor with appropriate role. ` +
|
|
1073
|
-
`Approval records include: approver identity, timestamp, rationale text, and the specific recommendation version approved. ` +
|
|
1074
|
-
`Only APPROVED items can trigger downstream actions (${domain.erp ?? 'ERP'} writes, policy changes).`,
|
|
1075
|
-
alternatives: [
|
|
1076
|
-
makeAlt('State machine with role-based gates', 'Enforces governance; provides audit trail for every transition', false),
|
|
1077
|
-
makeAlt('Email-based approval', 'No programmatic enforcement; approvals can be lost or forged', true),
|
|
1078
|
-
makeAlt('Auto-approve after timeout', 'Violates human-in-the-loop requirement', true),
|
|
1079
|
-
],
|
|
1080
|
-
consequences: [
|
|
1081
|
-
makeCons('Every action has a traceable approval chain', 'positive'),
|
|
1082
|
-
makeCons('Role-based gates prevent unauthorized approvals', 'positive'),
|
|
1083
|
-
makeCons('Approval process adds latency between recommendation and action', 'negative'),
|
|
1084
|
-
],
|
|
1085
|
-
sparcSectionRefs: [],
|
|
1086
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['approval', 'governance', 'workflow', 'review']),
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
// 8. Synthetic data model for prototype validation
|
|
1090
|
-
if (!hasADRCovering('synthetic data', 'seed data', 'prototype data', 'test data')) {
|
|
1091
|
-
adrs.push({
|
|
1092
|
-
id: `ADR-P2-${pad3(idx++)}`,
|
|
1093
|
-
title: `${domain.primary}: Synthetic Seed Data for Prototype Demonstration`,
|
|
1094
|
-
status: 'proposed',
|
|
1095
|
-
date: today,
|
|
1096
|
-
context: `The prototype must demonstrate the full analysis pipeline without requiring access to production ${domain.erp ?? 'ERP'} data. ` +
|
|
1097
|
-
`Seed data must be realistic enough to produce meaningful analysis results and demonstrate edge cases.`,
|
|
1098
|
-
decision: `Generate synthetic seed data that models realistic ${domain.primary.toLowerCase()} scenarios: ` +
|
|
1099
|
-
`multiple regions, varying data quality, both well-performing and problematic entities. ` +
|
|
1100
|
-
`Include edge cases that exercise all analysis branches (high/medium/low scores, different categories). ` +
|
|
1101
|
-
`Seed data is clearly labeled as synthetic and isolated from production data paths.`,
|
|
1102
|
-
alternatives: [
|
|
1103
|
-
makeAlt('Synthetic seed data with realistic distributions', 'Available immediately; exercises all code paths; no data privacy risk', false),
|
|
1104
|
-
makeAlt('Anonymized production data', 'More realistic but requires data pipeline, privacy review, and access approval', true),
|
|
1105
|
-
makeAlt('No seed data (production-only)', 'Blocks prototype demonstration until production access is granted', true),
|
|
1106
|
-
],
|
|
1107
|
-
consequences: [
|
|
1108
|
-
makeCons('Prototype can be demonstrated on day 1 without production data access', 'positive'),
|
|
1109
|
-
makeCons('Edge cases can be deliberately modeled to stress-test analysis logic', 'positive'),
|
|
1110
|
-
makeCons('Synthetic data may not capture all production data quality issues', 'negative'),
|
|
1111
|
-
],
|
|
1112
|
-
sparcSectionRefs: [],
|
|
1113
|
-
researchDossierItemRefs: findDossierRefs(dossier, ['data', 'prototype', 'seed', 'synthetic', 'test']),
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
process.stderr.write(`[adr-generator] buildADRsFromSPARC produced ${adrs.length} ADRs (minimum 8 guaranteed by ADR-PIPELINE-028)\n`);
|
|
1117
|
-
return adrs;
|
|
1118
|
-
}
|
|
1119
|
-
function extractDomainContext(query) {
|
|
1120
|
-
// Extract primary domain — look for noun phrases that describe the system's purpose
|
|
1121
|
-
let primary = 'System';
|
|
1122
|
-
const domainPatterns = [
|
|
1123
|
-
/(?:global|enterprise)\s+([\w\s-]{5,60}?)(?:\s+(?:strategy|program|platform|system|initiative))/i,
|
|
1124
|
-
/(?:help\s+(?:model|optimize|manage|simulate|build))\s+(.{10,60}?)(?:\.|,|$)/i,
|
|
1125
|
-
/(?:challenge.*?is|goal.*?is)\s+(.{10,60}?)(?:\.|,|$)/i,
|
|
1126
|
-
/\b((?:supplier|waste|emission|carbon|inventory|financial|clinical|manufacturing|procurement|supply chain)\s+[\w\s-]{3,40}?(?:optimization|management|simulation|tracking|analysis|modeling|reduction|transition))/i,
|
|
1127
|
-
];
|
|
1128
|
-
for (const pat of domainPatterns) {
|
|
1129
|
-
const m = query.match(pat);
|
|
1130
|
-
if (m) {
|
|
1131
|
-
primary = (m[1] ?? m[0]).trim().replace(/^(a|an|the)\s+/i, '');
|
|
1132
|
-
primary = primary.charAt(0).toUpperCase() + primary.slice(1);
|
|
1133
|
-
if (primary.length > 60)
|
|
1134
|
-
primary = primary.slice(0, 57) + '...';
|
|
1135
|
-
break;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (primary === 'System') {
|
|
1139
|
-
const fallback = query.match(/\b((?:global|enterprise|manufacturing|waste|carbon|emission|clinical|financial|supply|supplier|inventory)\s+\w+(?:\s+\w+){0,2})/i);
|
|
1140
|
-
if (fallback?.[1])
|
|
1141
|
-
primary = fallback[1].charAt(0).toUpperCase() + fallback[1].slice(1);
|
|
1142
|
-
}
|
|
1143
|
-
// ERP detection
|
|
1144
|
-
let erp = null;
|
|
1145
|
-
const erpPatterns = [
|
|
1146
|
-
[/\bworkday\b/i, 'Workday'],
|
|
1147
|
-
[/\boracle\s+fusion\s+cloud\b/i, 'Oracle Fusion Cloud'],
|
|
1148
|
-
[/\boracle\s+(?:erp|cloud)\b/i, 'Oracle ERP Cloud'],
|
|
1149
|
-
[/\bsap\s+s\/4\s*hana\b/i, 'SAP S/4HANA'],
|
|
1150
|
-
[/\bsap\b/i, 'SAP'],
|
|
1151
|
-
[/\bdynamics\s*365\b/i, 'Dynamics 365'],
|
|
1152
|
-
[/\bnetsuite\b/i, 'NetSuite'],
|
|
1153
|
-
[/\byardi\s+voyager\b/i, 'Yardi Voyager'],
|
|
1154
|
-
[/\byardi\b/i, 'Yardi Voyager'],
|
|
1155
|
-
];
|
|
1156
|
-
// ADR-PIPELINE-025: String-matching extraction for any system name
|
|
1157
|
-
// NOTE: The original code used require() which crashes in ESM context.
|
|
1158
|
-
// Replaced with inline string matching to avoid the module loading issue entirely.
|
|
1159
|
-
if (!erp) {
|
|
1160
|
-
const systemTriggers = [
|
|
1161
|
-
'managed through ', 'managed by ', 'managed via ', 'managed with ',
|
|
1162
|
-
'managed using ', 'managed in ', 'inside ',
|
|
1163
|
-
];
|
|
1164
|
-
const ql = query.toLowerCase();
|
|
1165
|
-
for (const trigger of systemTriggers) {
|
|
1166
|
-
const idx = ql.indexOf(trigger);
|
|
1167
|
-
if (idx === -1)
|
|
1168
|
-
continue;
|
|
1169
|
-
const after = query.slice(idx + trigger.length).trim();
|
|
1170
|
-
// Extract 1-3 words as the system name
|
|
1171
|
-
const nameMatch = after.match(/^([A-Z][\w]*(?:\s+[A-Z][\w]*){0,2})/);
|
|
1172
|
-
if (nameMatch?.[1] && nameMatch[1].length > 2) {
|
|
1173
|
-
erp = nameMatch[1].replace(/\s+ERP$/i, '').trim();
|
|
1174
|
-
break;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
for (const [pat, name] of erpPatterns) {
|
|
1179
|
-
if (pat.test(query)) {
|
|
1180
|
-
erp = name;
|
|
1181
|
-
break;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
// Stakeholders
|
|
1185
|
-
const stakeholderMatch = query.match(/(?:affect|impact|involve)\s+(.{10,120}?)(?:\.|$)/i);
|
|
1186
|
-
const matchedStakeholders = (query.match(/(?:procurement|sustainability|operations|executive|leadership|compliance|regulatory)\s+(?:team|leadership|management)/gi) ?? []).join(', ');
|
|
1187
|
-
const stakeholders = stakeholderMatch?.[1]?.trim()
|
|
1188
|
-
?? (matchedStakeholders || 'operational teams and leadership');
|
|
1189
|
-
return { primary, erp, stakeholders };
|
|
1190
|
-
}
|
|
1191
|
-
function extractTechFromQuery(query) {
|
|
1192
|
-
const techs = [];
|
|
1193
|
-
const patterns = [
|
|
1194
|
-
[/\btypescript\b/i, 'TypeScript'], [/\brust\b/i, 'Rust'], [/\bpython\b/i, 'Python'],
|
|
1195
|
-
[/\bgoogle cloud\b/i, 'Google Cloud'], [/\bcloud run\b/i, 'Cloud Run'], [/\bcloud functions?\b/i, 'Cloud Functions'],
|
|
1196
|
-
[/\baws\b/i, 'AWS'], [/\bazure\b/i, 'Azure'],
|
|
1197
|
-
[/\bpostgres(?:ql)?\b/i, 'PostgreSQL'], [/\bcloud sql\b/i, 'Cloud SQL (PostgreSQL)'],
|
|
1198
|
-
[/\bbigquery\b/i, 'BigQuery'], [/\bmongodb\b/i, 'MongoDB'], [/\bredis\b/i, 'Redis'],
|
|
1199
|
-
[/\bkafka\b/i, 'Kafka'], [/\brabbitmq\b/i, 'RabbitMQ'],
|
|
1200
|
-
[/\bkubernetes\b|\bk8s\b/i, 'Kubernetes'], [/\bdocker\b/i, 'Docker'],
|
|
1201
|
-
];
|
|
1202
|
-
for (const [pat, name] of patterns) {
|
|
1203
|
-
if (pat.test(query) && !techs.includes(name))
|
|
1204
|
-
techs.push(name);
|
|
1205
|
-
}
|
|
1206
|
-
return techs;
|
|
1207
|
-
}
|
|
1208
|
-
function itemsToText(items) {
|
|
1209
|
-
return items.slice(0, 3).map(s => {
|
|
1210
|
-
if (typeof s === 'string')
|
|
1211
|
-
return s;
|
|
1212
|
-
if (s && typeof s === 'object') {
|
|
1213
|
-
const obj = s;
|
|
1214
|
-
return String(obj['text'] ?? obj['description'] ?? JSON.stringify(s));
|
|
1215
|
-
}
|
|
1216
|
-
return String(s);
|
|
1217
|
-
}).join('; ');
|
|
1218
|
-
}
|
|
1219
|
-
export function buildPhase2ADRs(sparc, dossier, query, skipLLM = false) {
|
|
143
|
+
export async function buildPhase2ADRs(sparc, dossier, query) {
|
|
1220
144
|
if (!query) {
|
|
1221
|
-
process.stderr.write('[adr-generator] No query provided —
|
|
1222
|
-
return
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
145
|
+
process.stderr.write('[adr-generator] No query provided — emitting loud-failure ADR (ADR-PIPELINE-100 §D5)\n');
|
|
146
|
+
return [buildLoudFailureADR({
|
|
147
|
+
reason: 'No query string available — Phase 1 manifest/scenario did not surface the user prompt.',
|
|
148
|
+
phase1Status: 'missing-query',
|
|
149
|
+
dossierItemCount: dossier.items.length,
|
|
150
|
+
})];
|
|
151
|
+
}
|
|
152
|
+
const runnerResult = await tryGenerateLLMADRs(sparc, dossier, query);
|
|
153
|
+
if (runnerResult && runnerResult.length > 0)
|
|
154
|
+
return runnerResult;
|
|
155
|
+
const claudeFailureReason = 'tryGenerateLLMADRs returned no parseable markdown ADRs (timeout, claude binary missing, or empty output)';
|
|
156
|
+
const apiResult = await tryGenerateADRsViaAPI(sparc, dossier, query);
|
|
157
|
+
if (apiResult && apiResult.length > 0)
|
|
158
|
+
return apiResult;
|
|
159
|
+
const apiFailureReason = 'tryGenerateADRsViaAPI returned no parseable markdown ADRs (no ANTHROPIC_API_KEY, network, timeout, or rate-limit)';
|
|
160
|
+
process.stderr.write('[adr-generator] All LLM transports failed — emitting loud-failure ADR (ADR-PIPELINE-100 §D5)\n');
|
|
161
|
+
return [buildLoudFailureADR({
|
|
162
|
+
reason: 'All LLM transports failed to return parseable markdown ADRs.',
|
|
163
|
+
claudeFailureReason,
|
|
164
|
+
apiFailureReason,
|
|
165
|
+
dossierItemCount: dossier.items.length,
|
|
166
|
+
sparcStatus: (sparc.architecture?.services?.length ?? 0) > 0 ? 'present' : 'missing-services',
|
|
167
|
+
query,
|
|
168
|
+
})];
|
|
1241
169
|
}
|
|
1242
170
|
/**
|
|
1243
171
|
* Render a list of ADR records as a single markdown document.
|
|
@@ -1269,66 +197,42 @@ export function renderADRsAsDocument(adrs, query) {
|
|
|
1269
197
|
* one-shot subprocess that does NOT use MCP — safe to call from any context.
|
|
1270
198
|
* Uses the user's Claude Max subscription (no separate API key needed).
|
|
1271
199
|
*/
|
|
1272
|
-
function tryGenerateLLMADRs(sparc, dossier, query, phase2Dir = '') {
|
|
200
|
+
async function tryGenerateLLMADRs(sparc, dossier, query, phase2Dir = '') {
|
|
1273
201
|
try {
|
|
1274
|
-
|
|
1275
|
-
// which blocks in MCP mode. `claude --print` is safe from any context.
|
|
1276
|
-
// execFileSync imported at top of file
|
|
1277
|
-
let claudeBin = null;
|
|
1278
|
-
// Check env override first
|
|
1279
|
-
const envBin = process.env['AGENTICS_CLAUDE_BIN'];
|
|
1280
|
-
if (envBin) {
|
|
1281
|
-
claudeBin = envBin;
|
|
1282
|
-
}
|
|
1283
|
-
else {
|
|
1284
|
-
// Find claude on PATH
|
|
1285
|
-
const lookupCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
1286
|
-
for (const candidate of ['claude', 'claude-code']) {
|
|
1287
|
-
try {
|
|
1288
|
-
const found = execFileSync(lookupCmd, [candidate], {
|
|
1289
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1290
|
-
timeout: 5_000,
|
|
1291
|
-
encoding: 'utf-8',
|
|
1292
|
-
}).trim().split(/\r?\n/)[0]?.trim();
|
|
1293
|
-
if (found) {
|
|
1294
|
-
claudeBin = found;
|
|
1295
|
-
break;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
catch { /* not found, try next */ }
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
202
|
+
const claudeBin = resolveClaudeBin();
|
|
1301
203
|
if (!claudeBin) {
|
|
1302
204
|
process.stderr.write('[adr-generator] claude binary not found — cannot generate LLM ADRs\n');
|
|
1303
205
|
return null;
|
|
1304
206
|
}
|
|
1305
|
-
const
|
|
1306
|
-
const
|
|
1307
|
-
// Use --print for one-shot generation (no MCP, no interactive session)
|
|
207
|
+
const delimiter = newDocBreakDelimiter();
|
|
208
|
+
const prompt = buildADRPrompt(sparc, dossier, query, delimiter, phase2Dir);
|
|
1308
209
|
const model = process.env['AGENTICS_CLAUDE_MODEL'] || 'claude-sonnet-4-20250514';
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
//
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
210
|
+
const timeoutMs = parseAdrTimeoutMs();
|
|
211
|
+
const startMs = Date.now();
|
|
212
|
+
const result = await runStreamedClaudePrint(claudeBin, prompt, model, timeoutMs, phase2Dir);
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
process.stderr.write(`[adr-generator] Claude --print stream failed: ${result.reason} ` +
|
|
215
|
+
`(received ${result.stdoutBytes} bytes over ${result.elapsedMs}ms)\n`);
|
|
216
|
+
// ADR-PIPELINE-100 §D3: streamed output is preserved on timeout/kill, so
|
|
217
|
+
// we still try to parse what came back — if the LLM finished the first
|
|
218
|
+
// few ADRs before the cap, those are usable.
|
|
219
|
+
if (result.stdout && result.stdoutBytes > 1024) {
|
|
220
|
+
const partial = parseMarkdownADRs(result.stdout, delimiter, dossier);
|
|
221
|
+
if (partial && partial.length > 0) {
|
|
222
|
+
process.stderr.write(`[adr-generator] Salvaged ${partial.length} ADR(s) from partial stream output\n`);
|
|
223
|
+
return partial;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const adrs = parseMarkdownADRs(result.stdout, delimiter, dossier);
|
|
1326
229
|
if (!adrs || adrs.length === 0) {
|
|
1327
|
-
process.stderr.write('[adr-generator] Claude --print returned no
|
|
230
|
+
process.stderr.write('[adr-generator] Claude --print returned no parseable markdown ADRs\n');
|
|
1328
231
|
return null;
|
|
1329
232
|
}
|
|
1330
233
|
const durationMs = Date.now() - startMs;
|
|
1331
|
-
process.stderr.write(`[adr-generator] LLM-generated ${adrs.length} domain-specific ADRs via claude --print
|
|
234
|
+
process.stderr.write(`[adr-generator] LLM-generated ${adrs.length} domain-specific ADRs via streamed claude --print ` +
|
|
235
|
+
`in ${durationMs}ms (${result.stdoutBytes} bytes, markdown-direct, ADR-PIPELINE-100)\n`);
|
|
1332
236
|
return adrs;
|
|
1333
237
|
}
|
|
1334
238
|
catch (err) {
|
|
@@ -1336,20 +240,232 @@ function tryGenerateLLMADRs(sparc, dossier, query, phase2Dir = '') {
|
|
|
1336
240
|
return null;
|
|
1337
241
|
}
|
|
1338
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Locate the `claude` CLI binary for `--print` invocation.
|
|
245
|
+
* Honors `AGENTICS_CLAUDE_BIN` env override first, then probes PATH for
|
|
246
|
+
* `claude` and `claude-code`. Returns null when no binary is available.
|
|
247
|
+
*/
|
|
248
|
+
function resolveClaudeBin() {
|
|
249
|
+
const envBin = process.env['AGENTICS_CLAUDE_BIN'];
|
|
250
|
+
if (envBin)
|
|
251
|
+
return envBin;
|
|
252
|
+
const lookupCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
253
|
+
for (const candidate of ['claude', 'claude-code']) {
|
|
254
|
+
try {
|
|
255
|
+
const found = execFileSync(lookupCmd, [candidate], {
|
|
256
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
257
|
+
timeout: 5_000,
|
|
258
|
+
encoding: 'utf-8',
|
|
259
|
+
}).trim().split(/\r?\n/)[0]?.trim();
|
|
260
|
+
if (found)
|
|
261
|
+
return found;
|
|
262
|
+
}
|
|
263
|
+
catch { /* not found, try next */ }
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* ADR-PIPELINE-100 §D3: streamed `claude --print` subprocess.
|
|
269
|
+
*
|
|
270
|
+
* Replaces buffered execFileSync with `child_process.spawn` so:
|
|
271
|
+
* - stdout is collected incrementally (partial output preserved on kill)
|
|
272
|
+
* - stderr is forwarded live to the user (visible Claude Code progress)
|
|
273
|
+
* - a 10-second heartbeat reports elapsed time and bytes-received so the
|
|
274
|
+
* terminal isn't silent during long generations
|
|
275
|
+
* - a 60-second idle bound kills the process if no output has been received
|
|
276
|
+
* in a minute (catches hung subprocesses without waiting for the hard cap)
|
|
277
|
+
* - a hard timeout of `timeoutMs` (default 30 minutes) is the absolute upper
|
|
278
|
+
* bound; runaway generations are killed and reported as failures
|
|
279
|
+
*
|
|
280
|
+
* Returns `{ ok: false }` with the partial `stdout` populated when the
|
|
281
|
+
* subprocess is killed or exits non-zero. Caller may attempt to salvage
|
|
282
|
+
* partial output rather than discarding the whole run.
|
|
283
|
+
*/
|
|
284
|
+
async function runStreamedClaudePrint(claudeBin, prompt, model, timeoutMs, phase2Dir) {
|
|
285
|
+
const startMs = Date.now();
|
|
286
|
+
const idleBoundMs = parseInt(process.env['AGENTICS_ADR_IDLE_MS'] || '', 10) || 60_000;
|
|
287
|
+
const heartbeatMs = parseInt(process.env['AGENTICS_ADR_HEARTBEAT_MS'] || '', 10) || 10_000;
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
let stdout = '';
|
|
290
|
+
let stderr = '';
|
|
291
|
+
let lastDataMs = Date.now();
|
|
292
|
+
let killed = false;
|
|
293
|
+
let killReason;
|
|
294
|
+
const child = spawn(claudeBin, [
|
|
295
|
+
'--print',
|
|
296
|
+
'--output-format', 'text',
|
|
297
|
+
'--model', model,
|
|
298
|
+
prompt,
|
|
299
|
+
], {
|
|
300
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
301
|
+
env: { ...process.env, MCP_SERVER_MODE: undefined },
|
|
302
|
+
});
|
|
303
|
+
process.stderr.write(`[adr-generator] Streaming claude --print (model=${model}, timeout=${Math.floor(timeoutMs / 1000)}s, idle=${Math.floor(idleBoundMs / 1000)}s)\n`);
|
|
304
|
+
const debugLogPath = phase2Dir ? path.join(phase2Dir, 'claude-stderr.log') : null;
|
|
305
|
+
let debugStream = null;
|
|
306
|
+
if (debugLogPath) {
|
|
307
|
+
try {
|
|
308
|
+
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
309
|
+
debugStream = fs.createWriteStream(debugLogPath, { flags: 'a' });
|
|
310
|
+
}
|
|
311
|
+
catch { /* best-effort */ }
|
|
312
|
+
}
|
|
313
|
+
child.stdout?.on('data', (chunk) => {
|
|
314
|
+
const text = chunk.toString('utf-8');
|
|
315
|
+
stdout += text;
|
|
316
|
+
lastDataMs = Date.now();
|
|
317
|
+
});
|
|
318
|
+
child.stderr?.on('data', (chunk) => {
|
|
319
|
+
const text = chunk.toString('utf-8');
|
|
320
|
+
stderr += text;
|
|
321
|
+
// Forward Claude Code's stderr live so the user sees progress / warnings.
|
|
322
|
+
// ADR-PIPELINE-099 heartbeat compatibility: prefix with [claude] so the
|
|
323
|
+
// terminal scroller can identify the source.
|
|
324
|
+
process.stderr.write(`[claude] ${text}`);
|
|
325
|
+
debugStream?.write(text);
|
|
326
|
+
});
|
|
327
|
+
const heartbeat = setInterval(() => {
|
|
328
|
+
const elapsedSec = Math.floor((Date.now() - startMs) / 1000);
|
|
329
|
+
const bytes = stdout.length;
|
|
330
|
+
const idleSec = Math.floor((Date.now() - lastDataMs) / 1000);
|
|
331
|
+
process.stderr.write(`[agentics] ⏳ ADR generation streaming — ${elapsedSec}s elapsed, ${bytes} bytes received, ${idleSec}s since last byte\n`);
|
|
332
|
+
}, heartbeatMs);
|
|
333
|
+
const idleCheck = setInterval(() => {
|
|
334
|
+
if (Date.now() - lastDataMs > idleBoundMs && !killed) {
|
|
335
|
+
killed = true;
|
|
336
|
+
killReason = `idle bound exceeded (no output for ${Math.floor((Date.now() - lastDataMs) / 1000)}s)`;
|
|
337
|
+
process.stderr.write(`[adr-generator] Killing claude --print: ${killReason}\n`);
|
|
338
|
+
try {
|
|
339
|
+
child.kill('SIGTERM');
|
|
340
|
+
}
|
|
341
|
+
catch { /* already dead */ }
|
|
342
|
+
}
|
|
343
|
+
}, Math.min(heartbeatMs, 5000));
|
|
344
|
+
const hardTimeout = setTimeout(() => {
|
|
345
|
+
if (!killed) {
|
|
346
|
+
killed = true;
|
|
347
|
+
killReason = `hard timeout exceeded (${Math.floor(timeoutMs / 1000)}s)`;
|
|
348
|
+
process.stderr.write(`[adr-generator] Killing claude --print: ${killReason}\n`);
|
|
349
|
+
try {
|
|
350
|
+
child.kill('SIGTERM');
|
|
351
|
+
}
|
|
352
|
+
catch { /* already dead */ }
|
|
353
|
+
// SIGKILL backstop in case SIGTERM is ignored.
|
|
354
|
+
setTimeout(() => { try {
|
|
355
|
+
child.kill('SIGKILL');
|
|
356
|
+
}
|
|
357
|
+
catch { /* already dead */ } }, 5000);
|
|
358
|
+
}
|
|
359
|
+
}, timeoutMs);
|
|
360
|
+
child.on('error', (err) => {
|
|
361
|
+
clearInterval(heartbeat);
|
|
362
|
+
clearInterval(idleCheck);
|
|
363
|
+
clearTimeout(hardTimeout);
|
|
364
|
+
debugStream?.end();
|
|
365
|
+
resolve({
|
|
366
|
+
ok: false,
|
|
367
|
+
reason: `spawn error: ${err.message}`,
|
|
368
|
+
stdout,
|
|
369
|
+
stderr,
|
|
370
|
+
stdoutBytes: stdout.length,
|
|
371
|
+
elapsedMs: Date.now() - startMs,
|
|
372
|
+
exitCode: null,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
child.on('close', (code) => {
|
|
376
|
+
clearInterval(heartbeat);
|
|
377
|
+
clearInterval(idleCheck);
|
|
378
|
+
clearTimeout(hardTimeout);
|
|
379
|
+
debugStream?.end();
|
|
380
|
+
const elapsedMs = Date.now() - startMs;
|
|
381
|
+
const ok = !killed && code === 0;
|
|
382
|
+
resolve({
|
|
383
|
+
ok,
|
|
384
|
+
reason: ok ? undefined : (killReason ?? `claude --print exited with code ${code}`),
|
|
385
|
+
stdout,
|
|
386
|
+
stderr,
|
|
387
|
+
stdoutBytes: stdout.length,
|
|
388
|
+
elapsedMs,
|
|
389
|
+
exitCode: code,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Read `AGENTICS_ADR_TIMEOUT_MS` from env (default 30 minutes per
|
|
396
|
+
* ADR-PIPELINE-100 §D3). Clamps to [60s, 60min] to prevent both runaway runs
|
|
397
|
+
* and accidental zero/negative values.
|
|
398
|
+
*/
|
|
399
|
+
function parseAdrTimeoutMs() {
|
|
400
|
+
const raw = process.env['AGENTICS_ADR_TIMEOUT_MS'];
|
|
401
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
402
|
+
const def = 30 * 60 * 1000; // 30 min
|
|
403
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
404
|
+
return def;
|
|
405
|
+
const min = 60 * 1000; // 1 min lower bound
|
|
406
|
+
const max = 60 * 60 * 1000; // 60 min upper bound
|
|
407
|
+
return Math.max(min, Math.min(max, parsed));
|
|
408
|
+
}
|
|
1339
409
|
// ============================================================================
|
|
1340
|
-
// ADR
|
|
410
|
+
// ADR-PIPELINE-100: Markdown-direct ADR generation
|
|
1341
411
|
// ============================================================================
|
|
412
|
+
//
|
|
413
|
+
// The LLM emits a sequence of full markdown ADR documents separated by a
|
|
414
|
+
// per-run UUID delimiter. We split on the delimiter, write each chunk as its
|
|
415
|
+
// own .md file, and best-effort extract a small set of structured fields
|
|
416
|
+
// (title, status, date, phaseTags) for the index. The full markdown is the
|
|
417
|
+
// canonical artifact — we never re-serialise it from the structured fields.
|
|
418
|
+
const DOC_BREAK_PREFIX = '---ADR-DOC-BREAK-';
|
|
419
|
+
const DOC_BREAK_SUFFIX = '---';
|
|
420
|
+
/** Generate a per-run document delimiter for the LLM prompt. */
|
|
421
|
+
function newDocBreakDelimiter() {
|
|
422
|
+
return `${DOC_BREAK_PREFIX}${randomUUID()}${DOC_BREAK_SUFFIX}`;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Locate and load the canonical good-ADR exemplar shipped with the repo.
|
|
426
|
+
* Tries CWD first, then walks up from this module's location to find the
|
|
427
|
+
* `docs/templates/ADR-Good-Example.md` file (handles src/ vs dist/ layouts).
|
|
428
|
+
* Returns the empty string when not found — the prompt still works without
|
|
429
|
+
* an exemplar but the output will be lower quality.
|
|
430
|
+
*/
|
|
431
|
+
function loadGoodAdrExemplar() {
|
|
432
|
+
const candidates = [];
|
|
433
|
+
candidates.push(path.resolve(process.cwd(), 'docs/templates/ADR-Good-Example.md'));
|
|
434
|
+
try {
|
|
435
|
+
const here = fileURLToPath(import.meta.url);
|
|
436
|
+
let dir = path.dirname(here);
|
|
437
|
+
for (let i = 0; i < 8 && dir !== path.dirname(dir); i++) {
|
|
438
|
+
candidates.push(path.join(dir, 'docs', 'templates', 'ADR-Good-Example.md'));
|
|
439
|
+
dir = path.dirname(dir);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch { /* import.meta unavailable in some test contexts */ }
|
|
443
|
+
for (const p of candidates) {
|
|
444
|
+
try {
|
|
445
|
+
if (fs.existsSync(p))
|
|
446
|
+
return fs.readFileSync(p, 'utf-8');
|
|
447
|
+
}
|
|
448
|
+
catch { /* ignore unreadable candidates */ }
|
|
449
|
+
}
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
1342
452
|
/**
|
|
1343
453
|
* Build the ADR generation prompt from SPARC + dossier + query.
|
|
1344
454
|
* Shared between tryGenerateLLMADRs (Claude Code runner) and tryGenerateADRsViaAPI (direct API).
|
|
455
|
+
*
|
|
456
|
+
* ADR-PIPELINE-100 §D1: prompt instructs the LLM to emit a sequence of full
|
|
457
|
+
* markdown ADR documents separated by `delimiter`. The good-ADR exemplar is
|
|
458
|
+
* embedded verbatim as a STRUCTURAL reference, with explicit instruction that
|
|
459
|
+
* the input domain comes from the project requirements section, not the
|
|
460
|
+
* exemplar. This keeps the generator domain-agnostic.
|
|
1345
461
|
*/
|
|
1346
|
-
function buildADRPrompt(sparc, dossier, query, phase2Dir = '') {
|
|
462
|
+
function buildADRPrompt(sparc, dossier, query, delimiter, phase2Dir = '') {
|
|
1347
463
|
const services = sparc.architecture?.services ?? [];
|
|
1348
|
-
const serviceList = services.map(s => `- ${s.name}: ${s.responsibility.slice(0,
|
|
464
|
+
const serviceList = services.map(s => `- ${s.name}: ${s.responsibility.slice(0, 200)} (deps: ${s.dependencies.join(', ') || 'none'})`).join('\n');
|
|
1349
465
|
const dossierContext = dossier.items
|
|
1350
466
|
.filter(i => ['requirement', 'constraint', 'risk', 'decision-rationale'].includes(i.category))
|
|
1351
|
-
.slice(0,
|
|
1352
|
-
.map(i => `- [${i.category}] ${i.title}: ${i.content.slice(0,
|
|
467
|
+
.slice(0, 30)
|
|
468
|
+
.map(i => `- [${i.category}] ${i.title}: ${i.content.slice(0, 240)}`)
|
|
1353
469
|
.join('\n');
|
|
1354
470
|
const agentContext = phase2Dir ? loadAgenticsFleetContextForADR(phase2Dir) : '';
|
|
1355
471
|
const dossierText = dossier.items.map(i => i.content).join(' ');
|
|
@@ -1368,258 +484,419 @@ function buildADRPrompt(sparc, dossier, query, phase2Dir = '') {
|
|
|
1368
484
|
if (techStack.cacheLayer)
|
|
1369
485
|
techMentions.push(techStack.cacheLayer);
|
|
1370
486
|
const erpPromptContext = techStack.erp
|
|
1371
|
-
? `\
|
|
487
|
+
? `\n## ERP integration surface (use these EXACT paths)\n${formatERPContextForPrompt(techStack.erp)}\n`
|
|
488
|
+
: '';
|
|
489
|
+
const exemplar = loadGoodAdrExemplar();
|
|
490
|
+
const exemplarBlock = exemplar
|
|
491
|
+
? `\n## Structural exemplar (the bar to match)\n\n` +
|
|
492
|
+
`The following is a canonical "good" ADR from a different domain (vector database). ` +
|
|
493
|
+
`**Match its structure, depth, and rigour for the input domain below.** ` +
|
|
494
|
+
`Do NOT reuse its subject matter — your input is the project requirements section, ` +
|
|
495
|
+
`not the exemplar. Match these structural elements:\n\n` +
|
|
496
|
+
`- Header block: Status / Date / Authors / Deciders / Builds-on / **Implementation Phase**\n` +
|
|
497
|
+
`- Context with sub-sections and at least one comparison table\n` +
|
|
498
|
+
`- Decision section with an architecture diagram (ASCII art or mermaid)\n` +
|
|
499
|
+
`- Per-component subsections with code samples (typed signatures, struct/class definitions)\n` +
|
|
500
|
+
`- Real source-file paths consistent with the project's tech stack\n` +
|
|
501
|
+
`- Configuration parameters table (param / default / description)\n` +
|
|
502
|
+
`- Per-platform or per-mode performance characteristics where relevant\n` +
|
|
503
|
+
`- Numbered Alternatives Considered with multi-line "Rejected because:" rationales\n` +
|
|
504
|
+
`- Consequences split into Benefits, Risks (table with Probability/Impact/Mitigation), Performance Targets\n` +
|
|
505
|
+
`- Implementation Status table\n` +
|
|
506
|
+
`- Related Decisions cross-links\n` +
|
|
507
|
+
`- Revision History table\n\n` +
|
|
508
|
+
`<EXEMPLAR>\n${exemplar}\n</EXEMPLAR>\n`
|
|
1372
509
|
: '';
|
|
1373
|
-
return (`You are a senior
|
|
1374
|
-
`
|
|
510
|
+
return (`You are a senior solution architect writing Architecture Decision Records.\n\n` +
|
|
511
|
+
`Read the FULL project requirements, research findings, and detected technologies below, then write 8 to 14 ` +
|
|
512
|
+
`ADRs as full markdown documents that guide a developer on HOW to build the specific system the user described.\n\n` +
|
|
1375
513
|
`## Project Requirements\n\n${query}\n\n` +
|
|
1376
|
-
`## Services Identified\n${serviceList}\n` +
|
|
1377
|
-
`\n## Research Findings\n${dossierContext}\n` +
|
|
1378
|
-
(agentContext ?
|
|
1379
|
-
`\n## Detected Technologies\n${techMentions.join(', ') || '
|
|
514
|
+
`## Services Identified\n${serviceList || '(none — derive from requirements)'}\n` +
|
|
515
|
+
`\n## Research Findings\n${dossierContext || '(no dossier items available)'}\n` +
|
|
516
|
+
(agentContext ? agentContext + '\n' : '') +
|
|
517
|
+
`\n## Detected Technologies\n${techMentions.join(', ') || '(none specified — pick reasonable defaults and justify them)'}\n` +
|
|
1380
518
|
erpPromptContext +
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
'
|
|
1384
|
-
`
|
|
1385
|
-
|
|
1386
|
-
`
|
|
1387
|
-
`
|
|
1388
|
-
`
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
`
|
|
1396
|
-
`
|
|
1397
|
-
`
|
|
1398
|
-
`
|
|
1399
|
-
`
|
|
1400
|
-
`
|
|
1401
|
-
|
|
1402
|
-
`
|
|
1403
|
-
|
|
1404
|
-
`
|
|
1405
|
-
`
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
`
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
'
|
|
1422
|
-
|
|
1423
|
-
`A) DOMAIN-SPECIFIC DECISIONS (at least 5):\n` +
|
|
1424
|
-
` - How to model the core domain (data shape, partitioning, time semantics)\n` +
|
|
1425
|
-
` - How to implement key algorithms (scoring, anomaly detection, scenario comparison, recommendation ranking)\n` +
|
|
1426
|
-
` - How decision/simulation/optimization logic is structured\n` +
|
|
1427
|
-
` - How domain entities relate and what business rules constrain them\n` +
|
|
1428
|
-
` - How specific user-described workflows are wired end-to-end\n` +
|
|
1429
|
-
` Each non-obvious algorithmic choice gets its OWN ADR: scoring/normalization, threshold models, factor sourcing, estimation fallbacks, immutability/audit patterns.\n\n` +
|
|
1430
|
-
`B) INFRASTRUCTURE DECISIONS (3-5):\n` +
|
|
1431
|
-
` - Data persistence (which database, why, schema sketch)\n` +
|
|
1432
|
-
` - ERP integration for ${techMentions[0] || 'the named ERP'} (concrete API paths, idempotency, retry, rate limits)\n` +
|
|
1433
|
-
` - Deployment + cloud platform specifics\n` +
|
|
1434
|
-
` - Security, compliance, audit trail (RBAC, separation of duties, hash chain)\n\n` +
|
|
1435
|
-
`## Critical Rules\n\n` +
|
|
1436
|
-
`- NO generic titles like "Security Architecture", "Performance Architecture", "Service Boundary".\n` +
|
|
1437
|
-
`- Every ADR's "decision" field is 3-6 paragraphs, NEVER one sentence.\n` +
|
|
1438
|
-
`- "context" is 3-6 paragraphs explaining the problem space + current state + target state.\n` +
|
|
1439
|
-
`- "components" has 2-5 entries per ADR, each with a real code example.\n` +
|
|
1440
|
-
`- "appendices" has at least 1 entry (e.g. ERP field mapping, sample queries, threshold tables).\n` +
|
|
1441
|
-
`- Reference actual domain concepts from the brief — never generic "entities" or "services".\n` +
|
|
1442
|
-
`- If the user named technologies (${techMentions.join(', ')}), every relevant ADR cites them BY NAME with concrete API paths.\n` +
|
|
1443
|
-
`- ASCII diagrams MUST show real component names from this project, not abstract boxes labelled "Service A".\n` +
|
|
1444
|
-
`- Code examples MUST compile (or be syntactically valid pseudo-code with real types).\n` +
|
|
1445
|
-
`- References MUST include at least one industry standard / RFC / vendor doc URL where applicable.\n\n` +
|
|
1446
|
-
`Return ONLY the JSON array. No prose before or after. No markdown fences around the array.`);
|
|
519
|
+
exemplarBlock +
|
|
520
|
+
`\n## Output format (CRITICAL — read carefully)\n\n` +
|
|
521
|
+
`Emit ${techMentions.length > 0 ? '10 to 14' : '8 to 12'} ADRs as a sequence of full markdown documents.\n` +
|
|
522
|
+
`Separate consecutive ADR documents with **exactly** this delimiter on its own line:\n\n` +
|
|
523
|
+
`${delimiter}\n\n` +
|
|
524
|
+
`Do not use that string anywhere else in your output. Do not wrap the response in code fences. ` +
|
|
525
|
+
`Do not include any preamble, postscript, or explanation outside the ADR documents.\n\n` +
|
|
526
|
+
`Each ADR document MUST start with:\n\n` +
|
|
527
|
+
`\`\`\`\n` +
|
|
528
|
+
`# ADR-P2-NNN: <specific decision title>\n\n` +
|
|
529
|
+
`**Status:** Proposed\n` +
|
|
530
|
+
`**Date:** ${new Date().toISOString().slice(0, 10)}\n` +
|
|
531
|
+
`**Implementation Phase:** <foundation|domain|integration|api|deployment|operations|cross-cutting>\n` +
|
|
532
|
+
`\`\`\`\n\n` +
|
|
533
|
+
`where NNN is a zero-padded sequential number starting at 001. The Implementation Phase tag drives ` +
|
|
534
|
+
`phase-aware ADR injection in the implementation prompts (ADR-PIPELINE-100 §D4): use \`foundation\` ` +
|
|
535
|
+
`for repo scaffolding and core types, \`domain\` for domain models and business logic, \`integration\` ` +
|
|
536
|
+
`for adapters/connectors/anti-corruption layers, \`api\` for HTTP/RPC surfaces, \`deployment\` for ` +
|
|
537
|
+
`infrastructure/CI/Docker, \`operations\` for observability/logging/metrics, \`cross-cutting\` for ` +
|
|
538
|
+
`decisions that apply across all phases (security, governance, audit).\n\n` +
|
|
539
|
+
`## Coverage requirements\n\n` +
|
|
540
|
+
`The complete set of ADRs must cover BOTH categories:\n\n` +
|
|
541
|
+
`**A) Domain-specific decisions (at least 5)** — implementation of the actual business logic from the ` +
|
|
542
|
+
`requirements. Each non-obvious algorithmic choice gets its own ADR: scoring/ranking, routing/filtering, ` +
|
|
543
|
+
`factor sourcing, threshold models, estimation fallbacks, immutability/audit patterns, etc.\n\n` +
|
|
544
|
+
`**B) Infrastructure decisions (3-5)** — supporting technology: data persistence, ` +
|
|
545
|
+
`${techMentions[0] ? techMentions[0] + ' integration approach' : 'integration approach'}, deployment, security/compliance.\n\n` +
|
|
546
|
+
`## Title rules\n\n` +
|
|
547
|
+
`- GOOD: "Use Monte Carlo simulation for carbon price forecasting with configurable volatility"\n` +
|
|
548
|
+
`- BAD: "Performance Architecture"\n` +
|
|
549
|
+
`- GOOD: "Model emissions baseline as facility-level time series with business unit rollup"\n` +
|
|
550
|
+
`- BAD: "Data persistence strategy"\n\n` +
|
|
551
|
+
`Each title describes a specific decision the developer must build, not a category.\n\n` +
|
|
552
|
+
`## Quality bar\n\n` +
|
|
553
|
+
`- Reference actual domain concepts from the requirements (not generic "entities" or "services")\n` +
|
|
554
|
+
`- If the user specified technologies (${techMentions.join(', ') || 'none specified'}), reference them in relevant ADRs by name\n` +
|
|
555
|
+
`- Decision sections must be deep enough that a developer can build from them — multiple paragraphs, code samples where appropriate, file paths, named types\n` +
|
|
556
|
+
`- Alternatives Considered must include 2-4 options with substantive rejection rationales (not strawmen)\n` +
|
|
557
|
+
`- Consequences must include both benefits and risks; risks should be in a table with probability/impact/mitigation columns\n` +
|
|
558
|
+
`- Match the structural depth of the exemplar above for the user's domain\n\n` +
|
|
559
|
+
`Begin output now. First ADR's \`# ADR-P2-001:\` heading should be the very first line. ` +
|
|
560
|
+
`No preamble.`);
|
|
1447
561
|
}
|
|
1448
562
|
/**
|
|
1449
|
-
* Parse raw LLM
|
|
563
|
+
* Parse a raw LLM markdown response into Phase2ADRRecord[].
|
|
564
|
+
* ADR-PIPELINE-100 §D1: split on the per-run delimiter, then per-chunk extract
|
|
565
|
+
* a flat metadata head (id/title/status/date/phaseTags) and store the full
|
|
566
|
+
* chunk as `rawMarkdown`. Tolerant to minor heading variation.
|
|
1450
567
|
*/
|
|
1451
|
-
function
|
|
1452
|
-
|
|
568
|
+
function parseMarkdownADRs(rawText, delimiter, dossier) {
|
|
569
|
+
if (!rawText || !delimiter)
|
|
570
|
+
return null;
|
|
571
|
+
// Strip surrounding code fences if the LLM wrapped its output despite
|
|
572
|
+
// instruction not to.
|
|
1453
573
|
let cleaned = rawText.trim();
|
|
1454
574
|
if (cleaned.startsWith('```')) {
|
|
1455
|
-
cleaned = cleaned.replace(/^```
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
return null;
|
|
1469
|
-
try {
|
|
1470
|
-
const parsed = JSON.parse(arrayMatch[0]);
|
|
1471
|
-
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
1472
|
-
return null;
|
|
1473
|
-
rawAdrs = parsed;
|
|
1474
|
-
}
|
|
1475
|
-
catch {
|
|
1476
|
-
return null;
|
|
575
|
+
cleaned = cleaned.replace(/^```[a-z]*\s*\n?/i, '').replace(/\n?```\s*$/, '');
|
|
576
|
+
}
|
|
577
|
+
// Primary split: per-run UUID delimiter.
|
|
578
|
+
let chunks = cleaned
|
|
579
|
+
.split(delimiter)
|
|
580
|
+
.map(c => c.trim())
|
|
581
|
+
.filter(c => c.length > 0);
|
|
582
|
+
// Secondary split: if the model ignored the delimiter and emitted multiple
|
|
583
|
+
// ADRs separated by `# ADR-...` headers, recover by splitting on those.
|
|
584
|
+
if (chunks.length <= 1) {
|
|
585
|
+
const headerSplits = cleaned.split(/(?=^#\s+ADR[-\s])/m).map(c => c.trim()).filter(c => c.length > 0);
|
|
586
|
+
if (headerSplits.length > 1) {
|
|
587
|
+
chunks = headerSplits;
|
|
1477
588
|
}
|
|
1478
589
|
}
|
|
590
|
+
if (chunks.length === 0)
|
|
591
|
+
return null;
|
|
1479
592
|
const today = new Date().toISOString().slice(0, 10);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
593
|
+
const records = [];
|
|
594
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
595
|
+
const md = chunks[i];
|
|
596
|
+
const meta = extractAdrMetaFromMarkdown(md, i + 1);
|
|
597
|
+
// Best-effort structured-field extraction for backward-compat consumers.
|
|
598
|
+
const context = extractMdSection(md, /^##\s+context\b/im) || '';
|
|
599
|
+
const decision = extractMdSection(md, /^##\s+decision\b/im) || '';
|
|
600
|
+
const alternatives = extractAlternativesFromMd(md, decision);
|
|
601
|
+
const consequences = extractConsequencesFromMd(md);
|
|
602
|
+
records.push({
|
|
603
|
+
id: meta.id,
|
|
604
|
+
title: meta.title,
|
|
605
|
+
status: meta.status,
|
|
606
|
+
date: meta.date || today,
|
|
607
|
+
context,
|
|
608
|
+
decision,
|
|
609
|
+
alternatives,
|
|
610
|
+
consequences,
|
|
1497
611
|
sparcSectionRefs: [],
|
|
1498
|
-
researchDossierItemRefs: findDossierRefs(dossier, [
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
612
|
+
researchDossierItemRefs: findDossierRefs(dossier, [meta.title.toLowerCase()]),
|
|
613
|
+
rawMarkdown: md.endsWith('\n') ? md : md + '\n',
|
|
614
|
+
phaseTags: meta.phaseTags,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return records.length > 0 ? records : null;
|
|
1504
618
|
}
|
|
1505
619
|
/**
|
|
1506
|
-
* Extract
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
* spreads it onto the base record so missing fields stay undefined.
|
|
620
|
+
* Extract a flat metadata head (id, title, status, date, phaseTags) from a
|
|
621
|
+
* single ADR markdown chunk. Tolerant of minor variation; falls back to a
|
|
622
|
+
* generated ID if the H1 is missing or malformed.
|
|
1510
623
|
*/
|
|
1511
|
-
function
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
|
|
624
|
+
function extractAdrMetaFromMarkdown(md, fallbackIndex) {
|
|
625
|
+
// H1 like `# ADR-P2-001: Some Title` or `# ADR-PIPELINE-100: Some Title`
|
|
626
|
+
const h1Re = /^#\s+(ADR[-\w]*\d+)[:\s]+(.+?)\s*$/m;
|
|
627
|
+
const h1 = md.match(h1Re);
|
|
628
|
+
const id = h1?.[1]?.trim() || `ADR-P2-${pad3(fallbackIndex)}`;
|
|
629
|
+
let title = (h1?.[2] || '').trim();
|
|
630
|
+
if (!title) {
|
|
631
|
+
// Fallback: first H1 line, anything after the colon
|
|
632
|
+
const anyH1 = md.match(/^#\s+(.+?)\s*$/m);
|
|
633
|
+
title = anyH1?.[1]?.trim() || `Decision ${fallbackIndex}`;
|
|
634
|
+
title = title.replace(/^ADR[-\w]*\d+\s*[:\s]+/i, '').trim() || `Decision ${fallbackIndex}`;
|
|
635
|
+
}
|
|
636
|
+
const statusLine = md.match(/^\*\*Status:\*\*\s*(.+?)\s*$/im);
|
|
637
|
+
const statusText = (statusLine?.[1] || '').toLowerCase();
|
|
638
|
+
const status = statusText.includes('failed') ? 'failed'
|
|
639
|
+
: statusText.includes('deprecated') ? 'deprecated'
|
|
640
|
+
: statusText.includes('accepted') ? 'accepted'
|
|
641
|
+
: 'proposed';
|
|
642
|
+
const dateLine = md.match(/^\*\*Date:\*\*\s*(.+?)\s*$/im);
|
|
643
|
+
const date = (dateLine?.[1] || '').trim();
|
|
644
|
+
const phaseLine = md.match(/^\*\*Implementation Phase:\*\*\s*(.+?)\s*$/im);
|
|
645
|
+
const phaseTags = phaseLine?.[1]
|
|
646
|
+
? phaseLine[1].split(/[,/|]+/).map(s => s.trim().toLowerCase()).filter(Boolean)
|
|
647
|
+
: [];
|
|
648
|
+
return { id, title, status, date, phaseTags };
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Extract the body text under a heading regex until the next heading at the
|
|
652
|
+
* same or higher level (or end of document). Returns trimmed body or empty
|
|
653
|
+
* string if the heading is absent.
|
|
654
|
+
*/
|
|
655
|
+
function extractMdSection(md, headingRe) {
|
|
656
|
+
const match = headingRe.exec(md);
|
|
657
|
+
if (!match)
|
|
658
|
+
return '';
|
|
659
|
+
const headingEnd = match.index + match[0].length;
|
|
660
|
+
const after = md.slice(headingEnd);
|
|
661
|
+
// Stop at the next H1 or H2 (don't stop at H3+ which are subsections of this
|
|
662
|
+
// section).
|
|
663
|
+
const nextHeadingMatch = /^(?:#{1,2})\s+/m.exec(after);
|
|
664
|
+
const sectionBody = nextHeadingMatch ? after.slice(0, nextHeadingMatch.index) : after;
|
|
665
|
+
return sectionBody.trim();
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Best-effort alternatives extraction from a markdown ADR. Looks under
|
|
669
|
+
* `## Alternatives Considered` for either bullet items or `### N. <option>`
|
|
670
|
+
* subsections, classifying each as rejected/selected by keyword search.
|
|
671
|
+
*/
|
|
672
|
+
function extractAlternativesFromMd(md, decisionText) {
|
|
673
|
+
const section = extractMdSection(md, /^##\s+alternatives?(?:\s+considered)?\b/im);
|
|
674
|
+
if (!section) {
|
|
675
|
+
// Synthesise a single "selected approach" alternative from the decision
|
|
676
|
+
// text so backward-compat consumers see something non-empty.
|
|
677
|
+
return decisionText
|
|
678
|
+
? [makeAlt('Selected approach', decisionText.split(/\n\s*\n/)[0]?.slice(0, 400) ?? '', false)]
|
|
679
|
+
: [];
|
|
680
|
+
}
|
|
681
|
+
const alts = [];
|
|
682
|
+
// Pattern A: `### N. Title` subsections
|
|
683
|
+
const subRe = /^###\s+(?:\d+[.)]\s*)?(.+?)\s*$/gm;
|
|
684
|
+
let m;
|
|
685
|
+
const positions = [];
|
|
686
|
+
while ((m = subRe.exec(section)) !== null) {
|
|
687
|
+
positions.push({
|
|
688
|
+
start: m.index,
|
|
689
|
+
headerEnd: m.index + m[0].length,
|
|
690
|
+
option: m[1].trim(),
|
|
691
|
+
});
|
|
1518
692
|
}
|
|
1519
|
-
if (
|
|
1520
|
-
|
|
693
|
+
if (positions.length > 0) {
|
|
694
|
+
for (let i = 0; i < positions.length; i++) {
|
|
695
|
+
const cur = positions[i];
|
|
696
|
+
const nextStart = i + 1 < positions.length ? positions[i + 1].start : section.length;
|
|
697
|
+
const body = section.slice(cur.headerEnd, nextStart).trim();
|
|
698
|
+
const rejected = /\brejected\b|\bnot\s+(?:selected|chosen)\b/i.test(body);
|
|
699
|
+
alts.push(makeAlt(cur.option, body.slice(0, 600), rejected));
|
|
700
|
+
}
|
|
701
|
+
return alts;
|
|
702
|
+
}
|
|
703
|
+
// Pattern B: bullet list items
|
|
704
|
+
const bulletRe = /^\s*[-*]\s+\*\*(.+?)\*\*\s*(?:\(([^)]*)\))?:?\s*(.*)$/gm;
|
|
705
|
+
while ((m = bulletRe.exec(section)) !== null) {
|
|
706
|
+
const option = m[1].trim();
|
|
707
|
+
const annotation = (m[2] || '').toLowerCase();
|
|
708
|
+
const rationale = (m[3] || '').trim();
|
|
709
|
+
const rejected = /reject|not.*select/i.test(annotation) || /reject/i.test(rationale);
|
|
710
|
+
alts.push(makeAlt(option, rationale.slice(0, 600), rejected));
|
|
711
|
+
}
|
|
712
|
+
if (alts.length > 0)
|
|
713
|
+
return alts;
|
|
714
|
+
// Fallback: each non-empty line is an alternative
|
|
715
|
+
const lines = section.split(/\n+/).map(l => l.trim()).filter(l => l.length > 0).slice(0, 6);
|
|
716
|
+
for (const line of lines) {
|
|
717
|
+
const rejected = /reject|not.*select/i.test(line);
|
|
718
|
+
alts.push(makeAlt(line.slice(0, 200), line.slice(0, 600), rejected));
|
|
719
|
+
}
|
|
720
|
+
return alts;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Best-effort consequences extraction from a markdown ADR. Looks under
|
|
724
|
+
* `## Consequences` for bullet items or sub-sections labelled
|
|
725
|
+
* Benefits/Positive vs Risks/Negative.
|
|
726
|
+
*/
|
|
727
|
+
function extractConsequencesFromMd(md) {
|
|
728
|
+
const section = extractMdSection(md, /^##\s+consequences\b/im);
|
|
729
|
+
if (!section)
|
|
730
|
+
return [];
|
|
731
|
+
const cons = [];
|
|
732
|
+
// Pattern: bullet items with optional [+]/[-]/[~] markers
|
|
733
|
+
const bulletRe = /^\s*[-*]\s+(?:\[\s*([+\-~])\s*\]\s*)?(.+?)\s*$/gm;
|
|
734
|
+
let m;
|
|
735
|
+
while ((m = bulletRe.exec(section)) !== null) {
|
|
736
|
+
const marker = m[1];
|
|
737
|
+
const text = m[2].trim();
|
|
738
|
+
if (text.length < 8)
|
|
739
|
+
continue; // skip tiny noise lines
|
|
740
|
+
const type = marker === '+' ? 'positive'
|
|
741
|
+
: marker === '-' ? 'negative'
|
|
742
|
+
: marker === '~' ? 'neutral'
|
|
743
|
+
: /\b(benefit|advantage|enabl|reduce|improv|gain)/i.test(text) ? 'positive'
|
|
744
|
+
: /\b(risk|cost|drawback|disadvantage|limit|degrad|require.*invest)/i.test(text) ? 'negative'
|
|
745
|
+
: 'neutral';
|
|
746
|
+
cons.push(makeCons(text.slice(0, 400), type));
|
|
747
|
+
}
|
|
748
|
+
// If no bullets parsed, synthesise from sub-section headers
|
|
749
|
+
if (cons.length === 0) {
|
|
750
|
+
const benefits = extractMdSection(section, /^###\s+benefits?\b/im);
|
|
751
|
+
const risks = extractMdSection(section, /^###\s+risks?\b/im);
|
|
752
|
+
if (benefits)
|
|
753
|
+
cons.push(makeCons(benefits.split(/\n\s*\n/)[0].slice(0, 400), 'positive'));
|
|
754
|
+
if (risks)
|
|
755
|
+
cons.push(makeCons(risks.split(/\n\s*\n/)[0].slice(0, 400), 'negative'));
|
|
756
|
+
}
|
|
757
|
+
return cons;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* ADR-PIPELINE-100 §D5: build a single loud-failure ADR record describing why
|
|
761
|
+
* generation failed. The pipeline emits exactly this record (and writes its
|
|
762
|
+
* markdown to `phase4/adrs/ADR-FAIL-001.md`) when both the LLM and the Ruflo
|
|
763
|
+
* paths fail to produce content. Replaces the silent SPARC-template fallback.
|
|
764
|
+
*/
|
|
765
|
+
export function buildLoudFailureADR(args) {
|
|
766
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
767
|
+
const lines = [];
|
|
768
|
+
lines.push('# ADR-FAIL-001: ADR Generation Failed');
|
|
769
|
+
lines.push('');
|
|
770
|
+
lines.push('**Status:** Failed');
|
|
771
|
+
lines.push(`**Date:** ${today}`);
|
|
772
|
+
lines.push(`**Implementation Phase:** cross-cutting`);
|
|
773
|
+
lines.push(`**Failure mode:** ${args.reason}`);
|
|
774
|
+
lines.push('');
|
|
775
|
+
lines.push('## What happened');
|
|
776
|
+
lines.push('');
|
|
777
|
+
lines.push('Phase 4 attempted ADR generation via the configured Claude Code transport ' +
|
|
778
|
+
'(streamed `claude --print` per ADR-PIPELINE-100 §D3) and any available fallback ' +
|
|
779
|
+
'transports. All paths failed to produce usable markdown ADRs.');
|
|
780
|
+
lines.push('');
|
|
781
|
+
if (args.rufloFailureReason) {
|
|
782
|
+
lines.push(`- **Ruflo path:** ${args.rufloFailureReason}`);
|
|
1521
783
|
}
|
|
1522
|
-
if (
|
|
1523
|
-
|
|
784
|
+
if (args.claudeFailureReason) {
|
|
785
|
+
lines.push(`- **Claude Code path:** ${args.claudeFailureReason}`);
|
|
1524
786
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
out.performanceTargets = pt;
|
|
1528
|
-
const is = parseTable(raw.implementationStatus);
|
|
1529
|
-
if (is)
|
|
1530
|
-
out.implementationStatus = is;
|
|
1531
|
-
if (Array.isArray(raw.appendices)) {
|
|
1532
|
-
out.appendices = raw.appendices
|
|
1533
|
-
.filter((a) => a !== null && typeof a === 'object')
|
|
1534
|
-
.map(a => ({
|
|
1535
|
-
title: String(a['title'] ?? ''),
|
|
1536
|
-
body: String(a['body'] ?? ''),
|
|
1537
|
-
}))
|
|
1538
|
-
.filter(a => a.title && a.body);
|
|
787
|
+
if (args.apiFailureReason) {
|
|
788
|
+
lines.push(`- **Direct Anthropic API path:** ${args.apiFailureReason}`);
|
|
1539
789
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
790
|
+
lines.push('');
|
|
791
|
+
lines.push('## Pipeline state');
|
|
792
|
+
lines.push('');
|
|
793
|
+
if (args.phase1Status)
|
|
794
|
+
lines.push(`- **Phase 1 artifacts:** ${args.phase1Status}`);
|
|
795
|
+
if (typeof args.dossierItemCount === 'number')
|
|
796
|
+
lines.push(`- **Phase 2 dossier:** ${args.dossierItemCount} items`);
|
|
797
|
+
if (args.sparcStatus)
|
|
798
|
+
lines.push(`- **Phase 3 SPARC:** ${args.sparcStatus}`);
|
|
799
|
+
if (args.simulationId)
|
|
800
|
+
lines.push(`- **Simulation ID:** ${args.simulationId}`);
|
|
801
|
+
lines.push('');
|
|
802
|
+
lines.push('## Why this is the artifact (not template boilerplate)');
|
|
803
|
+
lines.push('');
|
|
804
|
+
lines.push('Per ADR-PIPELINE-100 §D5, this run emits a single loud-failure ADR ' +
|
|
805
|
+
'instead of falling back to template-substituted ADRs. The template path ' +
|
|
806
|
+
'has been classified by the user as canned garbage that cannot meet the ' +
|
|
807
|
+
'quality bar regardless of input domain. Loud failure replaces silent ' +
|
|
808
|
+
'fallback so the regression is visible at the artifact level rather than ' +
|
|
809
|
+
'hidden behind a successful exit code.');
|
|
810
|
+
lines.push('');
|
|
811
|
+
lines.push('## Next steps');
|
|
812
|
+
lines.push('');
|
|
813
|
+
lines.push('Implementation prompts (Phase 5a) and code generation (Phase 5b) cannot proceed coherently from this run.');
|
|
814
|
+
lines.push('To recover:');
|
|
815
|
+
lines.push('');
|
|
816
|
+
lines.push('1. Re-run with verbose Claude Code logging enabled: `AGENTICS_CLAUDE_DEBUG=1`');
|
|
817
|
+
lines.push('2. Inspect `phase4/claude-stderr.log` for the live transport failure reason');
|
|
818
|
+
lines.push('3. If Claude Code is unreachable, verify the subscription is active and `claude --print` works standalone');
|
|
819
|
+
lines.push('4. If timeouts persist, increase `AGENTICS_ADR_TIMEOUT_MS` (default 1800000 / 30 min)');
|
|
820
|
+
lines.push('');
|
|
821
|
+
if (args.query) {
|
|
822
|
+
lines.push('## Original query');
|
|
823
|
+
lines.push('');
|
|
824
|
+
lines.push('```');
|
|
825
|
+
lines.push(args.query.slice(0, 4000));
|
|
826
|
+
lines.push('```');
|
|
827
|
+
lines.push('');
|
|
1567
828
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
829
|
+
const rawMarkdown = lines.join('\n') + '\n';
|
|
830
|
+
return {
|
|
831
|
+
id: 'ADR-FAIL-001',
|
|
832
|
+
title: 'ADR Generation Failed',
|
|
833
|
+
status: 'failed',
|
|
834
|
+
date: today,
|
|
835
|
+
context: args.reason,
|
|
836
|
+
decision: 'Emit failure ADR rather than silent template boilerplate.',
|
|
837
|
+
alternatives: [
|
|
838
|
+
makeAlt('Loud-failure ADR (selected)', 'Surface the regression at the artifact level so users can see the failure', false),
|
|
839
|
+
makeAlt('Silent template fallback (rejected)', 'Hides the regression behind a successful exit code; produces canned output that fails the quality bar', true),
|
|
840
|
+
],
|
|
841
|
+
consequences: [
|
|
842
|
+
makeCons('Pipeline failures are visible to the user immediately', 'positive'),
|
|
843
|
+
makeCons('Downstream phases (Phase 5a/b) cannot proceed coherently from this run', 'negative'),
|
|
844
|
+
],
|
|
845
|
+
sparcSectionRefs: [],
|
|
846
|
+
researchDossierItemRefs: [],
|
|
847
|
+
simulationId: args.simulationId,
|
|
848
|
+
rawMarkdown,
|
|
849
|
+
phaseTags: ['cross-cutting'],
|
|
850
|
+
};
|
|
1584
851
|
}
|
|
1585
852
|
/**
|
|
1586
853
|
* Generate ADRs via direct Anthropic API call.
|
|
1587
854
|
* Used when Claude Code runner is unavailable (MCP mode).
|
|
1588
855
|
* Requires ANTHROPIC_API_KEY in env or ~/.agentics/credentials.json.
|
|
1589
856
|
*/
|
|
1590
|
-
function tryGenerateADRsViaAPI(sparc, dossier, query) {
|
|
1591
|
-
// Synchronous API key check — getAnthropicApiKey is async but we need sync here.
|
|
1592
|
-
// Check env var directly (most common path).
|
|
857
|
+
async function tryGenerateADRsViaAPI(sparc, dossier, query) {
|
|
1593
858
|
const apiKey = process.env['ANTHROPIC_API_KEY'] ?? null;
|
|
1594
859
|
if (!apiKey) {
|
|
1595
860
|
process.stderr.write('[adr-generator] No ANTHROPIC_API_KEY — cannot call API directly\n');
|
|
1596
861
|
return null;
|
|
1597
862
|
}
|
|
1598
863
|
try {
|
|
1599
|
-
const
|
|
864
|
+
const delimiter = newDocBreakDelimiter();
|
|
865
|
+
const prompt = buildADRPrompt(sparc, dossier, query, delimiter);
|
|
1600
866
|
const startMs = Date.now();
|
|
1601
|
-
|
|
1602
|
-
//
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
867
|
+
const timeoutMs = parseAdrTimeoutMs();
|
|
868
|
+
// ADR-PIPELINE-100 §D3: async fetch with AbortController for timeout.
|
|
869
|
+
// max_tokens widened to 32768 because markdown ADRs are substantially
|
|
870
|
+
// larger than the old JSON form.
|
|
871
|
+
const ctl = new AbortController();
|
|
872
|
+
const timer = setTimeout(() => ctl.abort('adr-timeout'), timeoutMs);
|
|
873
|
+
const heartbeatMs = parseInt(process.env['AGENTICS_ADR_HEARTBEAT_MS'] || '', 10) || 10_000;
|
|
874
|
+
const heartbeat = setInterval(() => {
|
|
875
|
+
const elapsedSec = Math.floor((Date.now() - startMs) / 1000);
|
|
876
|
+
process.stderr.write(`[agentics] ⏳ Anthropic API call in flight — ${elapsedSec}s elapsed\n`);
|
|
877
|
+
}, heartbeatMs);
|
|
878
|
+
let rawResponse = '';
|
|
879
|
+
try {
|
|
880
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
881
|
+
method: 'POST',
|
|
882
|
+
headers: {
|
|
883
|
+
'Content-Type': 'application/json',
|
|
884
|
+
'x-api-key': apiKey,
|
|
885
|
+
'anthropic-version': '2023-06-01',
|
|
886
|
+
},
|
|
887
|
+
body: JSON.stringify({
|
|
888
|
+
model: process.env['AGENTICS_ADR_MODEL'] || 'claude-sonnet-4-20250514',
|
|
889
|
+
max_tokens: 32768,
|
|
890
|
+
messages: [{ role: 'user', content: prompt }],
|
|
891
|
+
}),
|
|
892
|
+
signal: ctl.signal,
|
|
893
|
+
});
|
|
894
|
+
rawResponse = await res.text();
|
|
895
|
+
}
|
|
896
|
+
finally {
|
|
897
|
+
clearTimeout(timer);
|
|
898
|
+
clearInterval(heartbeat);
|
|
899
|
+
}
|
|
1623
900
|
const response = JSON.parse(rawResponse);
|
|
1624
901
|
if (response.error) {
|
|
1625
902
|
process.stderr.write(`[adr-generator] API error: ${response.error.message}\n`);
|
|
@@ -1630,13 +907,13 @@ function tryGenerateADRsViaAPI(sparc, dossier, query) {
|
|
|
1630
907
|
process.stderr.write('[adr-generator] API returned no text content\n');
|
|
1631
908
|
return null;
|
|
1632
909
|
}
|
|
1633
|
-
const adrs =
|
|
910
|
+
const adrs = parseMarkdownADRs(textBlock.text, delimiter, dossier);
|
|
1634
911
|
if (!adrs || adrs.length === 0) {
|
|
1635
|
-
process.stderr.write('[adr-generator] API response did not contain
|
|
912
|
+
process.stderr.write('[adr-generator] API response did not contain parseable markdown ADRs\n');
|
|
1636
913
|
return null;
|
|
1637
914
|
}
|
|
1638
915
|
const durationMs = Date.now() - startMs;
|
|
1639
|
-
process.stderr.write(`[adr-generator] API-generated ${adrs.length} domain-specific ADRs in ${durationMs}ms\n`);
|
|
916
|
+
process.stderr.write(`[adr-generator] API-generated ${adrs.length} domain-specific ADRs in ${durationMs}ms (markdown-direct, ADR-PIPELINE-100)\n`);
|
|
1640
917
|
return adrs;
|
|
1641
918
|
}
|
|
1642
919
|
catch (err) {
|
|
@@ -1678,10 +955,43 @@ export async function generateADRs(context) {
|
|
|
1678
955
|
if (techStack.erp) {
|
|
1679
956
|
process.stderr.write(`[adr-generator] Detected ERP: ${techStack.erp.name} (${techStack.erp.slug})\n`);
|
|
1680
957
|
}
|
|
1681
|
-
//
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
958
|
+
// ADR-PIPELINE-100: LLM-only chain with loud failure. No silent template
|
|
959
|
+
// fallback. Order: claude --print (subscription-backed) → Anthropic API
|
|
960
|
+
// (ANTHROPIC_API_KEY) → loud-failure ADR.
|
|
961
|
+
let adrs;
|
|
962
|
+
if (!query) {
|
|
963
|
+
adrs = [buildLoudFailureADR({
|
|
964
|
+
reason: 'No query string available — Phase 1 manifest/scenario did not surface the user prompt.',
|
|
965
|
+
phase1Status: 'missing-query',
|
|
966
|
+
dossierItemCount: dossier.items.length,
|
|
967
|
+
sparcStatus: (sparc.architecture?.services?.length ?? 0) > 0 ? 'present' : 'missing-services',
|
|
968
|
+
simulationId,
|
|
969
|
+
})];
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
const runnerResult = await tryGenerateLLMADRs(sparc, dossier, query, context.phase2Dir);
|
|
973
|
+
if (runnerResult && runnerResult.length > 0) {
|
|
974
|
+
adrs = runnerResult;
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
const apiResult = await tryGenerateADRsViaAPI(sparc, dossier, query);
|
|
978
|
+
if (apiResult && apiResult.length > 0) {
|
|
979
|
+
adrs = apiResult;
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
process.stderr.write('[adr-generator] All LLM transports failed in generateADRs — emitting loud-failure ADR (ADR-PIPELINE-100 §D5)\n');
|
|
983
|
+
adrs = [buildLoudFailureADR({
|
|
984
|
+
reason: 'All LLM transports failed to return parseable markdown ADRs.',
|
|
985
|
+
claudeFailureReason: 'tryGenerateLLMADRs returned no parseable markdown ADRs',
|
|
986
|
+
apiFailureReason: 'tryGenerateADRsViaAPI returned no parseable markdown ADRs',
|
|
987
|
+
dossierItemCount: dossier.items.length,
|
|
988
|
+
sparcStatus: (sparc.architecture?.services?.length ?? 0) > 0 ? 'present' : 'missing-services',
|
|
989
|
+
query,
|
|
990
|
+
simulationId,
|
|
991
|
+
})];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
1685
995
|
// ADR-PIPELINE-018: Stamp simulation lineage on all ADRs
|
|
1686
996
|
if (simulationId) {
|
|
1687
997
|
adrs = adrs.map(adr => ({ ...adr, simulationId }));
|