@mcoda/core 0.1.27 → 0.1.28

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.
@@ -0,0 +1,1093 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { createEmptyArtifacts, } from "../docs/DocgenRunContext.js";
6
+ import { aggregateReviewOutcome, } from "../docs/review/ReviewTypes.js";
7
+ import { runOpenQuestionsGate } from "../docs/review/gates/OpenQuestionsGate.js";
8
+ import { runSdsNoUnresolvedItemsGate } from "../docs/review/gates/SdsNoUnresolvedItemsGate.js";
9
+ import { runSdsFolderTreeGate } from "../docs/review/gates/SdsFolderTreeGate.js";
10
+ import { runSdsTechStackRationaleGate } from "../docs/review/gates/SdsTechStackRationaleGate.js";
11
+ import { runSdsPolicyTelemetryGate } from "../docs/review/gates/SdsPolicyTelemetryGate.js";
12
+ import { runSdsOpsGate } from "../docs/review/gates/SdsOpsGate.js";
13
+ import { runSdsDecisionsGate } from "../docs/review/gates/SdsDecisionsGate.js";
14
+ import { runSdsAdaptersGate } from "../docs/review/gates/SdsAdaptersGate.js";
15
+ const SDS_SCAN_MAX_FILES = 120;
16
+ const SDS_SCAN_MAX_DEPTH = 5;
17
+ const TASKS_FOLDER_NAME = "tasks";
18
+ const PREFLIGHT_REPORT_NAME = "sds-preflight-report.json";
19
+ const OPEN_QUESTIONS_DOC_NAME = "sds-open-questions-answers.md";
20
+ const GAP_ADDENDUM_DOC_NAME = "sds-gap-remediation-addendum.md";
21
+ const MANAGED_SDS_BLOCK_START = "<!-- mcoda:sds-preflight:start -->";
22
+ const MANAGED_SDS_BLOCK_END = "<!-- mcoda:sds-preflight:end -->";
23
+ const DEFAULT_COMMIT_MESSAGE = "mcoda: apply SDS preflight remediations";
24
+ const ignoredDirs = new Set([
25
+ ".git",
26
+ ".mcoda",
27
+ ".docdex",
28
+ "node_modules",
29
+ "dist",
30
+ "build",
31
+ "coverage",
32
+ "tmp",
33
+ "temp",
34
+ ]);
35
+ const sdsFilenamePattern = /(sds|software[-_ ]design|system[-_ ]design|design[-_ ]spec|architecture)/i;
36
+ const sdsContentPattern = /(software design specification|system design specification|^#\s*sds\b)/im;
37
+ const markdownPattern = /\.(md|markdown|txt|rst)$/i;
38
+ const unresolvedTokenPattern = /\b(TBD|TBC|TODO|FIXME|to be determined|to be decided|unknown|unresolved)\b/gi;
39
+ const sdsHeadingLinePattern = /^#{1,6}\s+(.+)$/;
40
+ const sdsFolderEntryPattern = /[`'"]?([a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)+(?:\/[a-zA-Z0-9._-]+)*)[`'"]?/g;
41
+ const SDS_SIGNAL_HEADING_LIMIT = 60;
42
+ const SDS_SIGNAL_FOLDER_LIMIT = 20;
43
+ const technologyMatchers = [
44
+ { label: "Node.js", pattern: /\bnode(\.js)?\b/i },
45
+ { label: "TypeScript", pattern: /\btypescript\b|\btsconfig\b/i },
46
+ { label: "Python", pattern: /\bpython\b|\bfastapi\b|\bflask\b/i },
47
+ { label: "Go", pattern: /\bgolang\b|\bgo\b/i },
48
+ { label: "Rust", pattern: /\brust\b/i },
49
+ { label: "PostgreSQL", pattern: /\bpostgres(ql)?\b/i },
50
+ { label: "MySQL", pattern: /\bmysql\b/i },
51
+ { label: "MongoDB", pattern: /\bmongodb\b|\bmongo\b/i },
52
+ { label: "Redis", pattern: /\bredis\b/i },
53
+ { label: "Kafka", pattern: /\bkafka\b/i },
54
+ { label: "RabbitMQ", pattern: /\brabbitmq\b/i },
55
+ { label: "React", pattern: /\breact\b/i },
56
+ { label: "Next.js", pattern: /\bnext\.?js\b/i },
57
+ { label: "Docker", pattern: /\bdocker\b/i },
58
+ { label: "Kubernetes", pattern: /\bkubernetes\b|\bk8s\b/i },
59
+ ];
60
+ const adapterMatchers = [
61
+ { label: "OpenAI", pattern: /\bopenai\b/i },
62
+ { label: "Anthropic", pattern: /\banthropic\b|\bclaude\b/i },
63
+ { label: "Google Gemini", pattern: /\bgemini\b/i },
64
+ { label: "Mistral", pattern: /\bmistral\b/i },
65
+ { label: "Cohere", pattern: /\bcohere\b/i },
66
+ { label: "OpenRouter", pattern: /\bopenrouter\b/i },
67
+ { label: "Brave Search", pattern: /\bbrave\b/i },
68
+ { label: "Stripe", pattern: /\bstripe\b/i },
69
+ { label: "Twilio", pattern: /\btwilio\b/i },
70
+ { label: "SendGrid", pattern: /\bsendgrid\b/i },
71
+ { label: "Sentry", pattern: /\bsentry\b/i },
72
+ { label: "Datadog", pattern: /\bdatadog\b/i },
73
+ { label: "PostHog", pattern: /\bposthog\b/i },
74
+ { label: "Algolia", pattern: /\balgolia\b/i },
75
+ ];
76
+ const environmentMatchers = [
77
+ { label: "local", pattern: /\blocal\b|\bdev(elopment)?\b/i },
78
+ { label: "staging", pattern: /\bstaging\b|\bpre-?prod\b/i },
79
+ { label: "production", pattern: /\bprod(uction)?\b/i },
80
+ ];
81
+ const moduleNoiseTokens = new Set([
82
+ "and",
83
+ "for",
84
+ "from",
85
+ "into",
86
+ "with",
87
+ "without",
88
+ "under",
89
+ "over",
90
+ "this",
91
+ "that",
92
+ "those",
93
+ "these",
94
+ "section",
95
+ "sections",
96
+ "module",
97
+ "modules",
98
+ "system",
99
+ "design",
100
+ "specification",
101
+ ]);
102
+ const execFileAsync = promisify(execFile);
103
+ const uniqueStrings = (values) => Array.from(new Set(values.filter(Boolean)));
104
+ const normalizeQuestion = (value) => value
105
+ .toLowerCase()
106
+ .replace(/[^a-z0-9]+/g, " ")
107
+ .trim();
108
+ const issueLocationKey = (issue) => {
109
+ if (issue.location.kind === "line_range") {
110
+ return `${issue.location.path}:${issue.location.lineStart}-${issue.location.lineEnd}`;
111
+ }
112
+ return `${issue.location.path ?? ""}#${issue.location.heading}`;
113
+ };
114
+ const issueSeverityRank = {
115
+ blocker: 0,
116
+ high: 1,
117
+ medium: 2,
118
+ low: 3,
119
+ info: 4,
120
+ };
121
+ const formatIssueLocation = (issue) => {
122
+ if (issue.location.kind === "line_range") {
123
+ return `${issue.location.path}:${issue.location.lineStart}`;
124
+ }
125
+ return issue.location.path ? `${issue.location.path}#${issue.location.heading}` : issue.location.heading;
126
+ };
127
+ export class SdsPreflightService {
128
+ constructor(workspace) {
129
+ this.workspace = workspace;
130
+ }
131
+ static async create(workspace) {
132
+ return new SdsPreflightService(workspace);
133
+ }
134
+ async close() {
135
+ // Stateless service for now.
136
+ }
137
+ async walkCandidates(root, depth, collector) {
138
+ if (depth > SDS_SCAN_MAX_DEPTH)
139
+ return;
140
+ let entries = [];
141
+ try {
142
+ entries = await fs.readdir(root, { withFileTypes: true });
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ for (const entry of entries) {
148
+ if (entry.name.startsWith(".")) {
149
+ if (entry.isDirectory() && ![".github"].includes(entry.name))
150
+ continue;
151
+ }
152
+ if (entry.isDirectory()) {
153
+ if (ignoredDirs.has(entry.name))
154
+ continue;
155
+ await this.walkCandidates(path.join(root, entry.name), depth + 1, collector);
156
+ continue;
157
+ }
158
+ if (!entry.isFile())
159
+ continue;
160
+ const candidate = path.join(root, entry.name);
161
+ if (!markdownPattern.test(candidate))
162
+ continue;
163
+ collector(candidate);
164
+ }
165
+ }
166
+ async collectPathCandidates(inputPaths) {
167
+ if (!inputPaths || inputPaths.length === 0)
168
+ return [];
169
+ const resolved = [];
170
+ for (const input of inputPaths) {
171
+ if (!input || input.startsWith("docdex:"))
172
+ continue;
173
+ const fullPath = path.isAbsolute(input) ? input : path.join(this.workspace.workspaceRoot, input);
174
+ let stat;
175
+ try {
176
+ stat = await fs.stat(fullPath);
177
+ }
178
+ catch {
179
+ continue;
180
+ }
181
+ if (stat.isFile()) {
182
+ resolved.push(path.resolve(fullPath));
183
+ continue;
184
+ }
185
+ if (!stat.isDirectory())
186
+ continue;
187
+ await this.walkCandidates(path.resolve(fullPath), 0, (filePath) => {
188
+ resolved.push(path.resolve(filePath));
189
+ });
190
+ }
191
+ return uniqueStrings(resolved).slice(0, SDS_SCAN_MAX_FILES);
192
+ }
193
+ async discoverSdsPaths() {
194
+ const candidates = [
195
+ path.join(this.workspace.workspaceRoot, "docs", "sds.md"),
196
+ path.join(this.workspace.workspaceRoot, "docs", "sds", "sds.md"),
197
+ path.join(this.workspace.workspaceRoot, "docs", "software-design-specification.md"),
198
+ path.join(this.workspace.workspaceRoot, "sds.md"),
199
+ ];
200
+ const discovered = [];
201
+ for (const candidate of candidates) {
202
+ try {
203
+ const stat = await fs.stat(candidate);
204
+ if (stat.isFile())
205
+ discovered.push(path.resolve(candidate));
206
+ }
207
+ catch {
208
+ // ignore
209
+ }
210
+ }
211
+ await this.walkCandidates(this.workspace.workspaceRoot, 0, (filePath) => {
212
+ discovered.push(path.resolve(filePath));
213
+ });
214
+ return uniqueStrings(discovered).slice(0, SDS_SCAN_MAX_FILES);
215
+ }
216
+ async isLikelySdsPath(filePath) {
217
+ const baseName = path.basename(filePath);
218
+ if (sdsFilenamePattern.test(baseName))
219
+ return true;
220
+ try {
221
+ const sample = (await fs.readFile(filePath, "utf8")).slice(0, 35000);
222
+ return sdsContentPattern.test(sample);
223
+ }
224
+ catch {
225
+ return false;
226
+ }
227
+ }
228
+ async resolveSdsPaths(options) {
229
+ const explicit = await this.collectPathCandidates(options.sdsPaths);
230
+ const fromInputs = await this.collectPathCandidates(options.inputPaths);
231
+ const discovered = await this.discoverSdsPaths();
232
+ const candidatePaths = uniqueStrings([...explicit, ...fromInputs, ...discovered]).slice(0, SDS_SCAN_MAX_FILES);
233
+ const selected = [];
234
+ for (const candidate of candidatePaths) {
235
+ if (await this.isLikelySdsPath(candidate)) {
236
+ selected.push(path.resolve(candidate));
237
+ }
238
+ if (selected.length >= SDS_SCAN_MAX_FILES)
239
+ break;
240
+ }
241
+ return uniqueStrings(selected);
242
+ }
243
+ buildArtifacts(sdsPath) {
244
+ const artifacts = createEmptyArtifacts();
245
+ artifacts.sds = {
246
+ kind: "sds",
247
+ path: sdsPath,
248
+ variant: "primary",
249
+ meta: {},
250
+ };
251
+ return artifacts;
252
+ }
253
+ getGateRunners() {
254
+ return [
255
+ runOpenQuestionsGate,
256
+ runSdsNoUnresolvedItemsGate,
257
+ runSdsFolderTreeGate,
258
+ runSdsTechStackRationaleGate,
259
+ runSdsPolicyTelemetryGate,
260
+ runSdsOpsGate,
261
+ runSdsDecisionsGate,
262
+ runSdsAdaptersGate,
263
+ ];
264
+ }
265
+ dedupeIssues(issues) {
266
+ const seen = new Set();
267
+ const deduped = [];
268
+ for (const issue of issues) {
269
+ const key = `${issue.gateId}|${issue.category}|${issue.message}|${issueLocationKey(issue)}`;
270
+ if (seen.has(key))
271
+ continue;
272
+ seen.add(key);
273
+ deduped.push(issue);
274
+ }
275
+ return deduped.sort((a, b) => {
276
+ const severityDiff = issueSeverityRank[a.severity] - issueSeverityRank[b.severity];
277
+ if (severityDiff !== 0)
278
+ return severityDiff;
279
+ const gateDiff = a.gateId.localeCompare(b.gateId);
280
+ if (gateDiff !== 0)
281
+ return gateDiff;
282
+ return issueLocationKey(a).localeCompare(issueLocationKey(b));
283
+ });
284
+ }
285
+ resolveWorkspacePath(filePath) {
286
+ return path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(this.workspace.workspaceRoot, filePath);
287
+ }
288
+ collectSignalsFromContent(content) {
289
+ const lines = content.split(/\r?\n/);
290
+ const headings = [];
291
+ const folderEntries = [];
292
+ for (const line of lines) {
293
+ const trimmed = line.trim();
294
+ if (!trimmed)
295
+ continue;
296
+ const headingMatch = trimmed.match(sdsHeadingLinePattern);
297
+ if (headingMatch?.[1]) {
298
+ headings.push(headingMatch[1].replace(/#+$/, "").trim());
299
+ }
300
+ const folderMatches = [...trimmed.matchAll(new RegExp(sdsFolderEntryPattern.source, "g"))];
301
+ for (const match of folderMatches) {
302
+ const entry = (match[1] ?? "").replace(/^\.?\//, "").replace(/\/+$/, "").trim();
303
+ if (!entry || !entry.includes("/"))
304
+ continue;
305
+ folderEntries.push(entry);
306
+ }
307
+ }
308
+ const moduleDomains = uniqueStrings(headings.flatMap((heading) => heading
309
+ .replace(/^\d+(?:\.\d+)*\s+/, "")
310
+ .toLowerCase()
311
+ .split(/\s+/)
312
+ .map((token) => token.replace(/[^a-z0-9._-]+/g, ""))
313
+ .filter((token) => token.length >= 4 && !moduleNoiseTokens.has(token)))).slice(0, 10);
314
+ const technologies = technologyMatchers.filter((matcher) => matcher.pattern.test(content)).map((matcher) => matcher.label);
315
+ const adapters = adapterMatchers.filter((matcher) => matcher.pattern.test(content)).map((matcher) => matcher.label);
316
+ const environments = environmentMatchers
317
+ .filter((matcher) => matcher.pattern.test(content))
318
+ .map((matcher) => matcher.label);
319
+ return {
320
+ headings: uniqueStrings(headings).slice(0, SDS_SIGNAL_HEADING_LIMIT),
321
+ folderEntries: uniqueStrings(folderEntries).slice(0, SDS_SIGNAL_FOLDER_LIMIT),
322
+ moduleDomains,
323
+ technologies,
324
+ adapters,
325
+ environments,
326
+ };
327
+ }
328
+ async collectSignalsByPath(sourceSdsPaths) {
329
+ const signalsByPath = new Map();
330
+ for (const sdsPath of sourceSdsPaths) {
331
+ try {
332
+ const content = await fs.readFile(sdsPath, "utf8");
333
+ signalsByPath.set(path.resolve(sdsPath), this.collectSignalsFromContent(content));
334
+ }
335
+ catch {
336
+ signalsByPath.set(path.resolve(sdsPath), {
337
+ headings: [],
338
+ folderEntries: [],
339
+ moduleDomains: [],
340
+ technologies: [],
341
+ adapters: [],
342
+ environments: [],
343
+ });
344
+ }
345
+ }
346
+ return signalsByPath;
347
+ }
348
+ signalsForPath(signalsByPath, sourcePath) {
349
+ if (!sourcePath)
350
+ return undefined;
351
+ return signalsByPath.get(this.resolveWorkspacePath(sourcePath));
352
+ }
353
+ answerForQuestion(question, context) {
354
+ const combined = [question, context.target, context.heading ?? "", ...(context.signals?.headings ?? []).slice(0, 4)].join(" ");
355
+ const lower = combined.toLowerCase();
356
+ const moduleHint = context.signals?.moduleDomains?.slice(0, 3).join(", ");
357
+ const folderHint = context.signals?.folderEntries?.slice(0, 2).map((entry) => `\`${entry}\``).join(", ");
358
+ const adapterHint = context.signals?.adapters?.slice(0, 4).join(", ");
359
+ const environmentMatrix = context.signals?.environments && context.signals.environments.length > 0
360
+ ? context.signals.environments.join(", ")
361
+ : "local, staging, production";
362
+ const assumptions = [];
363
+ if (context.required) {
364
+ assumptions.push("This decision is required to unblock preflight planning gates.");
365
+ }
366
+ if (folderHint) {
367
+ assumptions.push(`Primary implementation surfaces include ${folderHint}.`);
368
+ }
369
+ if (moduleHint) {
370
+ assumptions.push(`Impacted modules include ${moduleHint}.`);
371
+ }
372
+ if (/(uncertainty|confidence|sensitivity)/i.test(lower)) {
373
+ return {
374
+ answer: "Adopt an uncertainty-first contract: compute calibrated confidence intervals, publish sensitivity analysis for top drivers, and surface uncertainty state consistently in API and UI responses.",
375
+ rationale: "This prevents false precision and keeps downstream decisions auditable when model confidence is weak.",
376
+ assumptions: uniqueStrings([
377
+ ...assumptions,
378
+ "Model evaluation flow supports calibration and confidence diagnostics.",
379
+ ]),
380
+ };
381
+ }
382
+ if (/(modular monolith|monolith|module boundaries|bounded context)/i.test(lower)) {
383
+ const moduleRoots = uniqueStrings((context.signals?.folderEntries ?? []).map((entry) => entry.split("/").slice(0, 2).join("/"))).slice(0, 4);
384
+ return {
385
+ answer: moduleRoots.length > 0
386
+ ? `Start as a modular monolith with strict internal boundaries across ${moduleRoots.map((entry) => `\`${entry}\``).join(", ")}; keep interfaces explicit and defer service extraction until runtime load and ownership seams are proven.`
387
+ : "Start as a modular monolith: define bounded internal modules with explicit interfaces, enforce dependency direction, and postpone microservice extraction until ownership seams are proven.",
388
+ rationale: "This reduces distributed-system overhead while preserving a clean extraction path when scale or team ownership requires it.",
389
+ assumptions: uniqueStrings([
390
+ ...assumptions,
391
+ "Single deployable unit is acceptable for early delivery phases.",
392
+ ]),
393
+ };
394
+ }
395
+ if (/(risk|mitigation|rollback|contingency|failure)/i.test(lower)) {
396
+ return {
397
+ answer: "Maintain a feature-level risk register with likelihood, impact, owner, mitigation, trigger signal, and explicit rollback action; every high-risk item must link to a validation or observability checkpoint.",
398
+ rationale: "Risk entries tied to concrete checkpoints improve release predictability and shorten incident recovery loops.",
399
+ assumptions: uniqueStrings([
400
+ ...assumptions,
401
+ `Deployment and operations are validated across ${environmentMatrix}.`,
402
+ ]),
403
+ };
404
+ }
405
+ if (/(training|calibration|holdout|evaluation|ethic|bias|drift|validation)/i.test(lower)) {
406
+ return {
407
+ answer: "Define a reproducible evaluation protocol: immutable dataset lineage, train/validation/holdout splits, calibration checks, drift thresholds, and explicit ethics/bias constraints enforced by release gates.",
408
+ rationale: "Reproducible evaluation and calibration controls are required to trust model outputs in production.",
409
+ assumptions: uniqueStrings([
410
+ ...assumptions,
411
+ "Data/version artifacts can be traced across training and serving runs.",
412
+ ]),
413
+ };
414
+ }
415
+ if (/(adapter|integration|provider|third[- ]party|external)/i.test(lower)) {
416
+ return {
417
+ answer: adapterHint && adapterHint.length > 0
418
+ ? `Define adapter contracts for ${adapterHint} with explicit auth, quota/rate-limit, timeout/retry strategy, and fallback behavior to secondary providers or degraded local behavior.`
419
+ : "Define adapter contracts for each external dependency with explicit auth, quota/rate-limit, timeout/retry strategy, and fallback behavior.",
420
+ rationale: "Provider contracts prevent runtime ambiguity and keep dependency failures isolated and recoverable.",
421
+ assumptions: uniqueStrings([
422
+ ...assumptions,
423
+ "External provider SLAs and failure modes are known before release.",
424
+ ]),
425
+ };
426
+ }
427
+ if (/(deploy|environment|ops|observability|telemetry|metering|policy|consent)/i.test(lower)) {
428
+ return {
429
+ answer: `Specify production-readiness controls: environment matrix (${environmentMatrix}), secrets handling, telemetry schema, metering policy, SLO/alert thresholds, and deterministic fallback behavior for policy violations or stale telemetry.`,
430
+ rationale: "Operational contracts make readiness gates deterministic and reduce release-time ambiguity.",
431
+ assumptions: uniqueStrings([
432
+ ...assumptions,
433
+ "Runtime can publish telemetry and enforcement decisions as structured events.",
434
+ ]),
435
+ };
436
+ }
437
+ return {
438
+ answer: "Resolve this as an explicit engineering decision with selected approach, rejected alternatives, and measurable verification criteria; map the decision to implementation and QA evidence.",
439
+ rationale: "Explicit decisions remove planning ambiguity and improve backlog execution quality.",
440
+ assumptions: uniqueStrings([
441
+ ...assumptions,
442
+ "Decision can be validated through deterministic tests or release checks.",
443
+ ]),
444
+ };
445
+ }
446
+ managedFolderTreeSection(signals) {
447
+ const entries = signals?.folderEntries?.slice(0, 10) ?? [];
448
+ if (entries.length > 0) {
449
+ return [
450
+ "## Folder Tree",
451
+ "```text",
452
+ ".",
453
+ ...entries.map((entry, index) => `${index === entries.length - 1 ? "└──" : "├──"} ${entry}`),
454
+ "```",
455
+ "",
456
+ ];
457
+ }
458
+ return [
459
+ "## Folder Tree",
460
+ "```text",
461
+ ".",
462
+ "├── docs/",
463
+ "├── apps/web/",
464
+ "├── services/api/",
465
+ "├── services/worker/",
466
+ "├── packages/shared/",
467
+ "├── db/migrations/",
468
+ "├── tests/",
469
+ "└── scripts/",
470
+ "```",
471
+ "",
472
+ ];
473
+ }
474
+ managedTechnologySection(signals) {
475
+ const technologies = signals?.technologies ?? [];
476
+ if (technologies.length > 0) {
477
+ return [
478
+ "## Technology Stack",
479
+ `- Chosen stack baseline: ${technologies.join(", ")}.`,
480
+ "- Alternatives considered must be recorded with trade-offs for runtime, complexity, and verification impact.",
481
+ "- Keep one explicit baseline per layer so create-tasks can generate deterministic implementation work.",
482
+ "",
483
+ ];
484
+ }
485
+ return [
486
+ "## Technology Stack",
487
+ "- Chosen stack baseline must be explicit for runtime, language, persistence, and tooling layers.",
488
+ "- Alternatives considered should be named with rationale and trade-offs.",
489
+ "- Use one baseline per layer to avoid ambiguous backlog generation.",
490
+ "",
491
+ ];
492
+ }
493
+ managedPolicyTelemetrySection(signals, scopedQuestions) {
494
+ const policyQuestionCount = scopedQuestions.filter((question) => /(policy|consent|telemetry|metering|quota|limit)/i.test(question.question)).length;
495
+ return [
496
+ "## Policy and Cache Consent",
497
+ "- Cache key policy: tenant_id + project_key + route + role.",
498
+ "- TTL tiers: hot=5m, warm=30m, cold=24h.",
499
+ "- Consent matrix: anonymous telemetry is default; identified telemetry requires explicit opt-in.",
500
+ policyQuestionCount > 0
501
+ ? `- Preflight resolved ${policyQuestionCount} policy/telemetry question(s) for this SDS file.`
502
+ : "- Preflight policy defaults are applied when SDS language is ambiguous.",
503
+ "",
504
+ "## Telemetry",
505
+ "- Telemetry schema defines event_name, timestamp, service, and request identifiers.",
506
+ "- Anonymous events contain aggregate metrics without user identity.",
507
+ "- Identified events include actor_id only when consent is granted.",
508
+ "",
509
+ "## Metering and Usage",
510
+ "- Usage metering tracks request units and compute units per tenant and feature.",
511
+ "- Rate limit and quota enforcement return deterministic limit status and retry guidance.",
512
+ "- Enforcement actions are logged for audit and operational review.",
513
+ "",
514
+ ...(signals?.adapters && signals.adapters.length > 0
515
+ ? [`- Metering coverage includes external adapter usage for ${signals.adapters.slice(0, 4).join(", ")}.`, ""]
516
+ : []),
517
+ ];
518
+ }
519
+ managedOperationsSection(signals) {
520
+ const environments = signals?.environments && signals.environments.length > 0
521
+ ? signals.environments.join(", ")
522
+ : "local, staging, production";
523
+ const moduleHint = signals?.moduleDomains?.slice(0, 4).join(", ");
524
+ return [
525
+ "## Operations and Deployment",
526
+ `- Environment matrix: ${environments}.`,
527
+ "- Secrets strategy: environment-scoped secret stores with least-privilege access.",
528
+ "- Deployment workflow: immutable build artifacts, migration checks, and controlled rollout stages.",
529
+ "",
530
+ "## Observability",
531
+ "- SLO target: 99.9% availability with p95 latency threshold of 300ms.",
532
+ "- Alert thresholds page on error-rate, saturation, and dependency failure conditions.",
533
+ moduleHint
534
+ ? `- Monitoring dashboards track module health across ${moduleHint}.`
535
+ : "- Monitoring dashboards map service health, queue depth, and critical dependency status.",
536
+ "",
537
+ "## Testing Gates",
538
+ "- Test gates require unit, integration, and validation checks before release promotion.",
539
+ "- Release validation includes contract tests, smoke tests, and rollback verification.",
540
+ "",
541
+ "## Failure Recovery and Rollback",
542
+ "- Failure modes are documented per service with runbook ownership.",
543
+ "- Recovery steps define restore order, verification checkpoints, and incident handoff.",
544
+ "- Rollback steps are deterministic and tested in staging before production rollout.",
545
+ "",
546
+ ];
547
+ }
548
+ managedAdapterSection(signals) {
549
+ const adapters = signals?.adapters ?? [];
550
+ return [
551
+ "## External Integrations and Adapter Contracts",
552
+ adapters.length > 0
553
+ ? `- Adapter contract baseline for this SDS includes: ${adapters.join(", ")}.`
554
+ : "- Adapter contract baseline must enumerate each external provider used by the project.",
555
+ "- Constraints: auth model, API key/token handling, rate limit, timeout, quota, latency budget, and pricing limits are documented per provider.",
556
+ "- Error handling: retries with bounded backoff, circuit break rules, and structured error classification are required.",
557
+ "- Fallback behavior: degrade gracefully to secondary adapters or cached responses when provider failures exceed thresholds.",
558
+ "",
559
+ ];
560
+ }
561
+ shouldReplaceIssueLine(issue) {
562
+ return (issue.gateId === "gate-open-questions" ||
563
+ issue.gateId === "gate-sds-no-unresolved-items" ||
564
+ issue.gateId === "gate-sds-explicit-decisions");
565
+ }
566
+ replacementForIssue(issue) {
567
+ if (issue.gateId === "gate-sds-explicit-decisions") {
568
+ return "- Decision: Use one selected baseline for this section and keep alternatives in an options summary.";
569
+ }
570
+ const excerptSource = issue.location.kind === "line_range" ? (issue.location.excerpt ?? issue.message) : issue.message;
571
+ const excerpt = excerptSource
572
+ .replace(/^open question requires resolution:\s*/i, "")
573
+ .replace(/^optional exploration:\s*/i, "")
574
+ .replace(/^[-*+]\s*/, "")
575
+ .replace(/^\d+\.\s*/, "")
576
+ .trim();
577
+ const resolved = this.normalizeResolvedText(excerpt || issue.message);
578
+ if (resolved)
579
+ return `- Resolved: ${resolved}`;
580
+ return "- Resolved: Explicit decision captured in the managed preflight remediation block.";
581
+ }
582
+ extractQuestionAnswers(issues, signalsByPath) {
583
+ const seen = new Set();
584
+ const answers = [];
585
+ for (const issue of issues) {
586
+ if (issue.category !== "open_questions")
587
+ continue;
588
+ const metadataQuestion = typeof issue.metadata?.question === "string" ? issue.metadata.question.trim() : "";
589
+ const excerpt = issue.location.kind === "line_range" ? issue.location.excerpt?.trim() ?? "" : "";
590
+ const rawQuestion = metadataQuestion || excerpt || issue.message;
591
+ const question = rawQuestion
592
+ .replace(/^open question requires resolution:\s*/i, "")
593
+ .replace(/^optional exploration:\s*/i, "")
594
+ .trim();
595
+ const normalized = normalizeQuestion(question);
596
+ if (!normalized || seen.has(normalized))
597
+ continue;
598
+ seen.add(normalized);
599
+ const required = typeof issue.metadata?.required === "boolean" ? issue.metadata.required : issue.severity === "high";
600
+ const target = typeof issue.metadata?.target === "string" ? issue.metadata.target : "sds";
601
+ const sourcePath = issue.location.path;
602
+ const resolved = this.answerForQuestion(question, {
603
+ target,
604
+ required,
605
+ heading: typeof issue.metadata?.heading === "string" ? issue.metadata.heading : undefined,
606
+ signals: this.signalsForPath(signalsByPath, sourcePath),
607
+ });
608
+ answers.push({
609
+ question,
610
+ normalized,
611
+ required,
612
+ target,
613
+ sourcePath,
614
+ line: issue.location.kind === "line_range" ? issue.location.lineStart : undefined,
615
+ answer: resolved.answer,
616
+ rationale: resolved.rationale,
617
+ assumptions: resolved.assumptions,
618
+ });
619
+ }
620
+ return answers;
621
+ }
622
+ issueSection(issue) {
623
+ switch (issue.gateId) {
624
+ case "gate-sds-folder-tree":
625
+ return "Folder Tree and Module Responsibilities";
626
+ case "gate-sds-tech-stack-rationale":
627
+ case "gate-sds-explicit-decisions":
628
+ return "Explicit Technology and Architecture Decisions";
629
+ case "gate-sds-policy-telemetry-metering":
630
+ return "Policy, Telemetry, and Metering";
631
+ case "gate-sds-ops-observability-testing":
632
+ return "Operations, Observability, and Testing";
633
+ case "gate-sds-external-adapters":
634
+ return "External Integrations and Adapter Contracts";
635
+ case "gate-sds-no-unresolved-items":
636
+ case "gate-open-questions":
637
+ return "Resolved Questions and Ambiguity Removal";
638
+ default:
639
+ return "Additional SDS Quality Remediations";
640
+ }
641
+ }
642
+ remediationLines(issue) {
643
+ switch (issue.gateId) {
644
+ case "gate-sds-folder-tree":
645
+ return [
646
+ "Define concrete repository tree paths for all major modules and workflows.",
647
+ "Add ownership/purpose comments for each top-level and critical nested path.",
648
+ "Ensure every required runtime, data, API, and test surface is represented.",
649
+ ];
650
+ case "gate-sds-tech-stack-rationale":
651
+ return [
652
+ "State one chosen stack baseline for each layer (runtime, persistence, interface, tooling).",
653
+ "Document alternatives considered and explicit trade-offs.",
654
+ "Tie choices to delivery constraints and verification impact.",
655
+ ];
656
+ case "gate-sds-policy-telemetry-metering":
657
+ return [
658
+ "Specify cache/policy/consent rules with deterministic enforcement behavior.",
659
+ "Define telemetry schema and anonymous vs identified handling.",
660
+ "Define metering and limit enforcement with observable outcomes.",
661
+ ];
662
+ case "gate-sds-ops-observability-testing":
663
+ return [
664
+ "Define environments, deployment rules, and secret handling.",
665
+ "Define SLO/alert thresholds and incident escalation paths.",
666
+ "Define testing/release gates and rollback procedures.",
667
+ ];
668
+ case "gate-sds-explicit-decisions":
669
+ return [
670
+ "Replace ambiguous either/or language with explicit chosen decisions.",
671
+ "Attach concise rationale and implications for implementation sequencing.",
672
+ ];
673
+ case "gate-sds-external-adapters":
674
+ return [
675
+ "Document adapter contracts for each external dependency.",
676
+ "Include rate/timeout/auth constraints, error handling, and fallback behavior.",
677
+ ];
678
+ case "gate-open-questions":
679
+ case "gate-sds-no-unresolved-items":
680
+ return [
681
+ "Convert unresolved items into explicit resolved decisions.",
682
+ "Map each decision to implementation and QA verification checkpoints.",
683
+ ];
684
+ default:
685
+ return [
686
+ "Apply the gate remediation in concrete implementation terms.",
687
+ "Add explicit verification criteria for the resolved section.",
688
+ ];
689
+ }
690
+ }
691
+ verificationLines(issue) {
692
+ switch (issue.gateId) {
693
+ case "gate-sds-folder-tree":
694
+ return [
695
+ "Create-tasks output contains implementation items for the newly defined paths.",
696
+ "Coverage report includes matching section/path signals.",
697
+ ];
698
+ case "gate-open-questions":
699
+ case "gate-sds-no-unresolved-items":
700
+ return [
701
+ "Open-question entries are represented in the generated Q&A artifact.",
702
+ "No unresolved placeholder markers remain in planning context artifacts.",
703
+ ];
704
+ default:
705
+ return [
706
+ "Generated backlog contains explicit tasks for this remediated area.",
707
+ "Sufficiency audit does not report this gap as remaining.",
708
+ ];
709
+ }
710
+ }
711
+ buildAddendum(issues) {
712
+ const now = new Date().toISOString();
713
+ const lines = [
714
+ "# SDS Gap Remediation Addendum",
715
+ "",
716
+ `Generated: ${now}`,
717
+ "",
718
+ "This addendum resolves SDS quality gaps discovered at create-tasks preflight time. Use it as planning context input alongside the primary SDS.",
719
+ "",
720
+ ];
721
+ if (issues.length === 0) {
722
+ lines.push("No unresolved SDS gaps were detected in preflight.");
723
+ return lines.join("\n");
724
+ }
725
+ const grouped = new Map();
726
+ for (const issue of issues) {
727
+ const section = this.issueSection(issue);
728
+ const current = grouped.get(section) ?? [];
729
+ current.push(issue);
730
+ grouped.set(section, current);
731
+ }
732
+ for (const [section, sectionIssues] of grouped.entries()) {
733
+ lines.push(`## ${section}`);
734
+ lines.push("");
735
+ sectionIssues.forEach((issue, index) => {
736
+ lines.push(`### Gap ${index + 1}: ${issue.message}`);
737
+ lines.push(`- Source: ${formatIssueLocation(issue)}`);
738
+ lines.push("- Remediation:");
739
+ this.remediationLines(issue).forEach((line) => lines.push(` - ${line}`));
740
+ lines.push("- Verification:");
741
+ this.verificationLines(issue).forEach((line) => lines.push(` - ${line}`));
742
+ lines.push("");
743
+ });
744
+ }
745
+ return lines.join("\n");
746
+ }
747
+ buildQuestionsDoc(questions) {
748
+ const now = new Date().toISOString();
749
+ const lines = [
750
+ "# SDS Open Questions Q&A",
751
+ "",
752
+ `Generated: ${now}`,
753
+ "",
754
+ "This document answers open questions detected in SDS preflight before task generation.",
755
+ "",
756
+ ];
757
+ if (questions.length === 0) {
758
+ lines.push("No open questions were detected.");
759
+ return lines.join("\n");
760
+ }
761
+ questions.forEach((entry, index) => {
762
+ lines.push(`## Q${index + 1}`);
763
+ lines.push(`- Question: ${entry.question}`);
764
+ lines.push(`- Required: ${entry.required ? "yes" : "no"}`);
765
+ lines.push(`- Target: ${entry.target}`);
766
+ if (entry.sourcePath) {
767
+ lines.push(`- Source: ${entry.sourcePath}${entry.line ? `:${entry.line}` : ""}`);
768
+ }
769
+ lines.push("- Answer:");
770
+ lines.push(` ${entry.answer}`);
771
+ lines.push("- Rationale:");
772
+ lines.push(` ${entry.rationale}`);
773
+ lines.push("- Assumptions:");
774
+ entry.assumptions.forEach((assumption) => lines.push(` - ${assumption}`));
775
+ lines.push("");
776
+ });
777
+ return lines.join("\n");
778
+ }
779
+ summarizeIssues(issues, gateNamesById) {
780
+ return issues.map((issue) => ({
781
+ gateId: issue.gateId,
782
+ gateName: gateNamesById.get(issue.gateId) ?? issue.gateId,
783
+ severity: issue.severity,
784
+ category: issue.category,
785
+ message: issue.message,
786
+ remediation: issue.remediation,
787
+ location: formatIssueLocation(issue),
788
+ }));
789
+ }
790
+ async collectGateResults(sourceSdsPaths) {
791
+ const gateResults = [];
792
+ const gateNamesById = new Map();
793
+ const warnings = [];
794
+ let gateFailureCount = 0;
795
+ const gateRunners = this.getGateRunners();
796
+ for (const sdsPath of sourceSdsPaths) {
797
+ const artifacts = this.buildArtifacts(sdsPath);
798
+ for (const runner of gateRunners) {
799
+ try {
800
+ const result = await runner({ artifacts });
801
+ gateResults.push(result);
802
+ gateNamesById.set(result.gateId, result.gateName);
803
+ }
804
+ catch (error) {
805
+ gateFailureCount += 1;
806
+ warnings.push(`Gate ${runner.name || "unknown"} failed for ${path.relative(this.workspace.workspaceRoot, sdsPath)}: ${error.message}`);
807
+ }
808
+ }
809
+ }
810
+ return { gateResults, gateNamesById, warnings, gateFailureCount };
811
+ }
812
+ issueMatchesPath(issue, sdsPath) {
813
+ const issuePath = issue.location.path;
814
+ if (!issuePath)
815
+ return false;
816
+ const resolvedIssuePath = this.resolveWorkspacePath(issuePath);
817
+ return resolvedIssuePath === path.resolve(sdsPath);
818
+ }
819
+ questionMatchesPath(question, sdsPath) {
820
+ if (!question.sourcePath)
821
+ return false;
822
+ return this.resolveWorkspacePath(question.sourcePath) === path.resolve(sdsPath);
823
+ }
824
+ normalizeResolvedText(value) {
825
+ return value
826
+ .replace(/\r?\n+/g, " ")
827
+ .replace(unresolvedTokenPattern, "")
828
+ .replace(/\s+/g, " ")
829
+ .trim();
830
+ }
831
+ buildLineReplacementsForPath(sdsPath, questions, issues) {
832
+ const replacements = new Map();
833
+ for (const question of questions) {
834
+ if (!this.questionMatchesPath(question, sdsPath))
835
+ continue;
836
+ if (!question.line || question.line < 1 || replacements.has(question.line))
837
+ continue;
838
+ const resolved = this.normalizeResolvedText(question.answer);
839
+ if (!resolved)
840
+ continue;
841
+ replacements.set(question.line, `- Resolved: ${resolved}`);
842
+ }
843
+ for (const issue of issues) {
844
+ if (!this.issueMatchesPath(issue, sdsPath))
845
+ continue;
846
+ if (issue.location.kind !== "line_range")
847
+ continue;
848
+ if (!this.shouldReplaceIssueLine(issue))
849
+ continue;
850
+ const line = issue.location.lineStart;
851
+ if (!line || line < 1 || replacements.has(line))
852
+ continue;
853
+ const replacement = this.replacementForIssue(issue);
854
+ if (!replacement)
855
+ continue;
856
+ replacements.set(line, replacement);
857
+ }
858
+ return replacements;
859
+ }
860
+ applyLineReplacements(content, replacements) {
861
+ if (replacements.size === 0)
862
+ return content;
863
+ const lines = content.split(/\r?\n/);
864
+ const sorted = Array.from(replacements.entries()).sort((a, b) => a[0] - b[0]);
865
+ for (const [line, replacement] of sorted) {
866
+ const index = line - 1;
867
+ if (index < 0 || index >= lines.length)
868
+ continue;
869
+ const current = (lines[index] ?? "").trim();
870
+ if (/^[-*+\d.)\s]*resolved:/i.test(current) || /^[-*+\d.)\s]*decision:/i.test(current))
871
+ continue;
872
+ lines[index] = replacement;
873
+ }
874
+ return lines.join("\n");
875
+ }
876
+ buildManagedSdsBlock(sdsPath, questions, issues, signals) {
877
+ const scopedQuestions = questions.filter((question) => this.questionMatchesPath(question, sdsPath));
878
+ const scopedIssues = issues.filter((issue) => this.issueMatchesPath(issue, sdsPath));
879
+ const lines = [
880
+ MANAGED_SDS_BLOCK_START,
881
+ "## Open Questions (Resolved)",
882
+ "",
883
+ ];
884
+ if (scopedQuestions.length === 0) {
885
+ lines.push("- Resolved: No unresolved questions remain for this SDS file in this preflight run.");
886
+ lines.push("");
887
+ }
888
+ else {
889
+ scopedQuestions.forEach((question, index) => {
890
+ const summary = this.normalizeResolvedText(question.answer);
891
+ const prefix = summary || "Explicit decision recorded in managed preflight output.";
892
+ lines.push(`- Resolved: Decision ${index + 1} for "${question.question}" => ${prefix}`);
893
+ });
894
+ lines.push("");
895
+ }
896
+ lines.push("## Resolved Decisions (mcoda preflight)");
897
+ lines.push("- Decision baseline: unresolved items are converted into explicit implementation decisions.");
898
+ lines.push("- Planning rule: each resolved decision must map to implementation and QA verification work.");
899
+ if (signals?.moduleDomains && signals.moduleDomains.length > 0) {
900
+ lines.push(`- Module scope detected for this SDS file: ${signals.moduleDomains.slice(0, 6).join(", ")}.`);
901
+ }
902
+ lines.push("");
903
+ lines.push(...this.managedFolderTreeSection(signals));
904
+ lines.push(...this.managedTechnologySection(signals));
905
+ lines.push(...this.managedPolicyTelemetrySection(signals, scopedQuestions));
906
+ lines.push(...this.managedOperationsSection(signals));
907
+ lines.push(...this.managedAdapterSection(signals));
908
+ lines.push("## Gap Remediation Summary (mcoda preflight)");
909
+ lines.push("");
910
+ if (scopedIssues.length === 0) {
911
+ lines.push("- No unresolved SDS quality gaps remained for this SDS file in this preflight run.");
912
+ lines.push("");
913
+ }
914
+ else {
915
+ scopedIssues.forEach((issue, index) => {
916
+ lines.push(`### Gap ${index + 1}: ${issue.message}`);
917
+ lines.push(`- Gate: ${issue.gateId}`);
918
+ lines.push(`- Source: ${formatIssueLocation(issue)}`);
919
+ lines.push("- Remediation:");
920
+ this.remediationLines(issue).forEach((line) => lines.push(` - ${line}`));
921
+ lines.push("");
922
+ });
923
+ }
924
+ lines.push(MANAGED_SDS_BLOCK_END);
925
+ return lines.join("\n");
926
+ }
927
+ upsertManagedSdsBlock(content, block) {
928
+ const startIndex = content.indexOf(MANAGED_SDS_BLOCK_START);
929
+ const endIndex = content.indexOf(MANAGED_SDS_BLOCK_END);
930
+ let withoutManaged = content;
931
+ if (startIndex >= 0 && endIndex > startIndex) {
932
+ const before = content.slice(0, startIndex).trimEnd();
933
+ const after = content.slice(endIndex + MANAGED_SDS_BLOCK_END.length).trimStart();
934
+ withoutManaged = [before, after].filter((segment) => segment.length > 0).join("\n\n");
935
+ }
936
+ const trimmed = withoutManaged.trim();
937
+ if (!trimmed)
938
+ return `${block}\n`;
939
+ const h1Match = trimmed.match(/^#\s+.+$/m);
940
+ if (!h1Match || typeof h1Match.index !== "number") {
941
+ return `${block}\n\n${trimmed}\n`;
942
+ }
943
+ const insertIndex = h1Match.index + h1Match[0].length;
944
+ const before = trimmed.slice(0, insertIndex).trimEnd();
945
+ const after = trimmed.slice(insertIndex).trim();
946
+ const merged = [before, block, after].filter((segment) => segment.length > 0).join("\n\n");
947
+ return `${merged.trimEnd()}\n`;
948
+ }
949
+ async applyPreflightRemediationsToSds(params) {
950
+ const appliedPaths = [];
951
+ const warnings = [];
952
+ for (const sdsPath of params.sourceSdsPaths) {
953
+ let original = "";
954
+ try {
955
+ original = await fs.readFile(sdsPath, "utf8");
956
+ }
957
+ catch (error) {
958
+ warnings.push(`Unable to read SDS for remediation ${sdsPath}: ${error.message}`);
959
+ continue;
960
+ }
961
+ let updated = this.applyLineReplacements(original, this.buildLineReplacementsForPath(sdsPath, params.questions, params.issues));
962
+ updated = this.upsertManagedSdsBlock(updated, this.buildManagedSdsBlock(sdsPath, params.questions, params.issues, params.signalsByPath.get(path.resolve(sdsPath)) ?? this.collectSignalsFromContent(original)));
963
+ if (updated === original)
964
+ continue;
965
+ try {
966
+ await fs.writeFile(sdsPath, updated, "utf8");
967
+ appliedPaths.push(path.resolve(sdsPath));
968
+ }
969
+ catch (error) {
970
+ warnings.push(`Unable to write remediated SDS ${sdsPath}: ${error.message}`);
971
+ }
972
+ }
973
+ return { appliedPaths, warnings };
974
+ }
975
+ async commitAppliedSdsChanges(paths, commitMessage) {
976
+ if (paths.length === 0)
977
+ return undefined;
978
+ const cwd = this.workspace.workspaceRoot;
979
+ try {
980
+ await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
981
+ }
982
+ catch {
983
+ return undefined;
984
+ }
985
+ const relativePaths = paths
986
+ .map((filePath) => path.relative(cwd, filePath))
987
+ .filter((relativePath) => relativePath && !relativePath.startsWith(".."));
988
+ if (relativePaths.length === 0)
989
+ return undefined;
990
+ await execFileAsync("git", ["add", "--", ...relativePaths], { cwd });
991
+ try {
992
+ await execFileAsync("git", ["commit", "-m", commitMessage || DEFAULT_COMMIT_MESSAGE, "--no-verify"], {
993
+ cwd,
994
+ env: { ...process.env, HUSKY: "0" },
995
+ });
996
+ }
997
+ catch (error) {
998
+ const stderr = String(error.stderr ?? "");
999
+ const message = error.message ?? "";
1000
+ if (/nothing to commit|no changes added to commit/i.test(`${stderr}\n${message}`)) {
1001
+ return undefined;
1002
+ }
1003
+ throw new Error(`Unable to commit SDS preflight remediations: ${message || stderr}`);
1004
+ }
1005
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd });
1006
+ const hash = stdout.trim();
1007
+ return hash.length > 0 ? hash : undefined;
1008
+ }
1009
+ async runPreflight(options) {
1010
+ const sourceSdsPaths = await this.resolveSdsPaths(options);
1011
+ if (sourceSdsPaths.length === 0) {
1012
+ throw new Error("sds-preflight requires an SDS document but none was found. Add docs/sds.md (or equivalent SDS doc) and retry.");
1013
+ }
1014
+ const warnings = [];
1015
+ let gateNamesById = new Map();
1016
+ let signalsByPath = await this.collectSignalsByPath(sourceSdsPaths);
1017
+ const initialGatePass = await this.collectGateResults(sourceSdsPaths);
1018
+ gateNamesById = initialGatePass.gateNamesById;
1019
+ warnings.push(...initialGatePass.warnings);
1020
+ let gateFailureCount = initialGatePass.gateFailureCount;
1021
+ let outcome = aggregateReviewOutcome({ gateResults: initialGatePass.gateResults });
1022
+ let issues = this.dedupeIssues(outcome.issues);
1023
+ let questions = this.extractQuestionAnswers(issues, signalsByPath);
1024
+ const applyToSds = options.applyToSds === true;
1025
+ let appliedSdsPaths = [];
1026
+ if (applyToSds) {
1027
+ const applyResult = await this.applyPreflightRemediationsToSds({
1028
+ sourceSdsPaths,
1029
+ questions,
1030
+ issues,
1031
+ signalsByPath,
1032
+ });
1033
+ appliedSdsPaths = applyResult.appliedPaths;
1034
+ warnings.push(...applyResult.warnings);
1035
+ if (appliedSdsPaths.length > 0) {
1036
+ signalsByPath = await this.collectSignalsByPath(sourceSdsPaths);
1037
+ const rerun = await this.collectGateResults(sourceSdsPaths);
1038
+ gateNamesById = rerun.gateNamesById;
1039
+ warnings.push(...rerun.warnings);
1040
+ gateFailureCount = rerun.gateFailureCount;
1041
+ outcome = aggregateReviewOutcome({ gateResults: rerun.gateResults });
1042
+ issues = this.dedupeIssues(outcome.issues);
1043
+ questions = this.extractQuestionAnswers(issues, signalsByPath);
1044
+ }
1045
+ }
1046
+ let commitHash;
1047
+ if (options.commitAppliedChanges && appliedSdsPaths.length > 0) {
1048
+ commitHash = await this.commitAppliedSdsChanges(appliedSdsPaths, options.commitMessage);
1049
+ }
1050
+ const addendum = this.buildAddendum(issues);
1051
+ const qaDoc = this.buildQuestionsDoc(questions);
1052
+ const taskDir = path.join(this.workspace.mcodaDir, TASKS_FOLDER_NAME, options.projectKey);
1053
+ const reportPath = path.join(taskDir, PREFLIGHT_REPORT_NAME);
1054
+ const openQuestionsPath = path.join(taskDir, OPEN_QUESTIONS_DOC_NAME);
1055
+ const gapAddendumPath = path.join(taskDir, GAP_ADDENDUM_DOC_NAME);
1056
+ const writeArtifacts = options.writeArtifacts !== false;
1057
+ if (writeArtifacts) {
1058
+ await fs.mkdir(taskDir, { recursive: true });
1059
+ await Promise.all([
1060
+ fs.writeFile(openQuestionsPath, qaDoc, "utf8"),
1061
+ fs.writeFile(gapAddendumPath, addendum, "utf8"),
1062
+ ]);
1063
+ }
1064
+ const qualityStatus = gateFailureCount > 0 ? "fail" : outcome.summary.status;
1065
+ const blockingIssueCount = issues.filter((issue) => issue.severity === "blocker").length + gateFailureCount;
1066
+ const requiredQuestionCount = questions.filter((question) => question.required).length;
1067
+ const result = {
1068
+ projectKey: options.projectKey,
1069
+ generatedAt: new Date().toISOString(),
1070
+ readyForPlanning: blockingIssueCount === 0 && requiredQuestionCount === 0 && qualityStatus !== "fail" && gateFailureCount === 0,
1071
+ qualityStatus,
1072
+ sourceSdsPaths,
1073
+ reportPath,
1074
+ openQuestionsPath,
1075
+ gapAddendumPath,
1076
+ generatedDocPaths: writeArtifacts ? [openQuestionsPath, gapAddendumPath] : [],
1077
+ questionCount: questions.length,
1078
+ requiredQuestionCount,
1079
+ issueCount: issues.length,
1080
+ blockingIssueCount,
1081
+ appliedToSds: applyToSds,
1082
+ appliedSdsPaths,
1083
+ commitHash,
1084
+ issues: this.summarizeIssues(issues, gateNamesById),
1085
+ questions,
1086
+ warnings: uniqueStrings(warnings),
1087
+ };
1088
+ if (writeArtifacts) {
1089
+ await fs.writeFile(reportPath, JSON.stringify(result, null, 2), "utf8");
1090
+ }
1091
+ return result;
1092
+ }
1093
+ }