@mcoda/core 0.1.18 → 0.1.20

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.
Files changed (58) hide show
  1. package/dist/api/QaTasksApi.d.ts.map +1 -1
  2. package/dist/api/QaTasksApi.js +3 -0
  3. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  4. package/dist/prompts/PdrPrompts.js +22 -8
  5. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  6. package/dist/prompts/SdsPrompts.js +53 -34
  7. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  8. package/dist/services/backlog/BacklogService.js +3 -0
  9. package/dist/services/backlog/TaskOrderingService.d.ts +9 -0
  10. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  11. package/dist/services/backlog/TaskOrderingService.js +251 -35
  12. package/dist/services/docs/DocsService.d.ts.map +1 -1
  13. package/dist/services/docs/DocsService.js +487 -71
  14. package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts +7 -0
  15. package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts.map +1 -0
  16. package/dist/services/docs/review/gates/PdrFolderTreeGate.js +151 -0
  17. package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts +7 -0
  18. package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts.map +1 -0
  19. package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.js +109 -0
  20. package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts +7 -0
  21. package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts.map +1 -0
  22. package/dist/services/docs/review/gates/PdrTechStackRationaleGate.js +128 -0
  23. package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts +7 -0
  24. package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts.map +1 -0
  25. package/dist/services/docs/review/gates/SdsFolderTreeGate.js +153 -0
  26. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts +7 -0
  27. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -0
  28. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +109 -0
  29. package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts +7 -0
  30. package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts.map +1 -0
  31. package/dist/services/docs/review/gates/SdsTechStackRationaleGate.js +128 -0
  32. package/dist/services/estimate/EstimateService.d.ts +2 -0
  33. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  34. package/dist/services/estimate/EstimateService.js +54 -0
  35. package/dist/services/execution/QaTasksService.d.ts +6 -0
  36. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  37. package/dist/services/execution/QaTasksService.js +278 -95
  38. package/dist/services/execution/TaskSelectionService.d.ts +3 -0
  39. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  40. package/dist/services/execution/TaskSelectionService.js +33 -0
  41. package/dist/services/execution/WorkOnTasksService.d.ts +4 -0
  42. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  43. package/dist/services/execution/WorkOnTasksService.js +146 -22
  44. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  45. package/dist/services/openapi/OpenApiService.js +43 -4
  46. package/dist/services/planning/CreateTasksService.d.ts +15 -0
  47. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  48. package/dist/services/planning/CreateTasksService.js +592 -81
  49. package/dist/services/planning/RefineTasksService.d.ts +1 -0
  50. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  51. package/dist/services/planning/RefineTasksService.js +88 -2
  52. package/dist/services/review/CodeReviewService.d.ts +6 -0
  53. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  54. package/dist/services/review/CodeReviewService.js +260 -41
  55. package/dist/services/shared/ProjectGuidance.d.ts +18 -2
  56. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
  57. package/dist/services/shared/ProjectGuidance.js +535 -34
  58. package/package.json +6 -6
@@ -24,7 +24,13 @@ import { runRfpDefinitionGate } from "./review/gates/RfpDefinitionGate.js";
24
24
  import { runPdrInterfacesGate } from "./review/gates/PdrInterfacesGate.js";
25
25
  import { runPdrOwnershipGate } from "./review/gates/PdrOwnershipGate.js";
26
26
  import { runPdrOpenQuestionsGate } from "./review/gates/PdrOpenQuestionsGate.js";
27
+ import { runPdrTechStackRationaleGate } from "./review/gates/PdrTechStackRationaleGate.js";
28
+ import { runPdrFolderTreeGate } from "./review/gates/PdrFolderTreeGate.js";
29
+ import { runPdrNoUnresolvedItemsGate } from "./review/gates/PdrNoUnresolvedItemsGate.js";
27
30
  import { runSdsDecisionsGate } from "./review/gates/SdsDecisionsGate.js";
31
+ import { runSdsTechStackRationaleGate } from "./review/gates/SdsTechStackRationaleGate.js";
32
+ import { runSdsFolderTreeGate } from "./review/gates/SdsFolderTreeGate.js";
33
+ import { runSdsNoUnresolvedItemsGate } from "./review/gates/SdsNoUnresolvedItemsGate.js";
28
34
  import { runSdsPolicyTelemetryGate } from "./review/gates/SdsPolicyTelemetryGate.js";
29
35
  import { runSdsOpsGate } from "./review/gates/SdsOpsGate.js";
30
36
  import { runSdsAdaptersGate } from "./review/gates/SdsAdaptersGate.js";
@@ -69,7 +75,13 @@ const BUILD_READY_ONLY_GATES = new Set([
69
75
  "gate-rfp-definition-coverage",
70
76
  "gate-pdr-interfaces-pipeline",
71
77
  "gate-pdr-ownership-consent-flow",
78
+ "gate-pdr-tech-stack-rationale",
79
+ "gate-pdr-folder-tree",
80
+ "gate-pdr-no-unresolved-items",
72
81
  "gate-sds-explicit-decisions",
82
+ "gate-sds-tech-stack-rationale",
83
+ "gate-sds-folder-tree",
84
+ "gate-sds-no-unresolved-items",
73
85
  "gate-sds-policy-telemetry-metering",
74
86
  "gate-sds-ops-observability-testing",
75
87
  "gate-sds-external-adapters",
@@ -88,12 +100,16 @@ const readPromptIfExists = async (workspace, relative) => {
88
100
  const PDR_REQUIRED_HEADINGS = [
89
101
  ["Introduction"],
90
102
  ["Scope"],
103
+ ["Goals", "Goals & Success Metrics", "Success Metrics"],
91
104
  ["Technology Stack", "Tech Stack"],
92
105
  ["Requirements", "Requirements & Constraints"],
93
106
  ["Architecture", "Architecture Overview"],
94
107
  ["Interfaces", "Interfaces / APIs"],
108
+ ["Delivery", "Delivery & Dependency Sequencing", "Dependency Sequencing"],
109
+ ["Target Folder Tree", "Folder Tree", "Directory Structure", "Repository Structure"],
95
110
  ["Non-Functional", "Non-Functional Requirements"],
96
111
  ["Risks", "Risks & Mitigations"],
112
+ ["Resolved Decisions"],
97
113
  ["Open Questions"],
98
114
  ["Acceptance Criteria"],
99
115
  ];
@@ -124,20 +140,25 @@ const ensureSectionContent = (draft, title, fallback) => {
124
140
  const validateSdsDraft = (draft) => {
125
141
  if (!draft || draft.trim().length < 100)
126
142
  return false;
127
- const required = [
128
- "Introduction",
129
- "Scope",
130
- "Architecture",
131
- "Components",
132
- "Data Model",
133
- "Interfaces",
134
- "Non-Functional",
135
- "Security",
136
- "Failure",
137
- "Risks",
138
- "Open Questions",
143
+ const headings = draft
144
+ .split(/\r?\n/)
145
+ .map((line) => line.trim())
146
+ .filter((line) => /^#{1,6}\s+/.test(line))
147
+ .map((line) => line.replace(/^#{1,6}\s+/, "").toLowerCase());
148
+ const requiredGroups = [
149
+ ["introduction", "purpose"],
150
+ ["scope"],
151
+ ["architecture"],
152
+ ["data"],
153
+ ["interface"],
154
+ ["security"],
155
+ ["failure", "recovery", "rollback"],
156
+ ["risk"],
157
+ ["operations", "observability", "quality"],
158
+ ["open questions"],
159
+ ["acceptance criteria"],
139
160
  ];
140
- return required.every((section) => new RegExp(`^#{1,6}\\s+[^\\n]*${section}\\b`, "im").test(draft));
161
+ return requiredGroups.every((group) => headings.some((heading) => group.some((term) => heading.includes(term))));
141
162
  };
142
163
  const slugify = (value) => value
143
164
  .toLowerCase()
@@ -192,6 +213,113 @@ const contextIndicatesMlStack = (context) => {
192
213
  const resolveTechStackFallback = (context) => {
193
214
  return contextIndicatesMlStack(context) ? ML_TECH_STACK_FALLBACK : DEFAULT_TECH_STACK_FALLBACK;
194
215
  };
216
+ const DEFAULT_PDR_FOLDER_TREE_BLOCK = [
217
+ "```text",
218
+ ".",
219
+ "├── docs/ # product, design, and architecture docs",
220
+ "│ ├── rfp/ # requirement sources",
221
+ "│ ├── pdr/ # product design reviews",
222
+ "│ └── sds/ # software design specifications",
223
+ "├── packages/ # application/service modules",
224
+ "│ ├── cli/ # command and operator interfaces",
225
+ "│ ├── core/ # business/domain services",
226
+ "│ └── integrations/ # provider adapters",
227
+ "├── openapi/ # API contracts",
228
+ "├── db/ # schema, migrations, seed data",
229
+ "├── deploy/ # runtime manifests/compose/k8s",
230
+ "├── tests/ # unit/integration/e2e checks",
231
+ "└── scripts/ # automation and release scripts",
232
+ "```",
233
+ ].join("\n");
234
+ const normalizePdrResolvedEntry = (line) => {
235
+ const stripped = line
236
+ .trim()
237
+ .replace(/^[-*+]\s+/, "")
238
+ .replace(/^\d+[.)]\s+/, "")
239
+ .trim();
240
+ if (!stripped)
241
+ return undefined;
242
+ if (/no unresolved questions remain|no open questions remain/i.test(stripped)) {
243
+ return "Resolved: No unresolved questions remain.";
244
+ }
245
+ const withoutPrefix = stripped.replace(/^resolved:\s*/i, "").trim();
246
+ const withoutQuestions = withoutPrefix.replace(/\?+$/, "").trim();
247
+ if (!withoutQuestions)
248
+ return undefined;
249
+ const withTerminal = /[.!?]$/.test(withoutQuestions) ? withoutQuestions : `${withoutQuestions}.`;
250
+ return `Resolved: ${withTerminal}`;
251
+ };
252
+ const enforcePdrResolvedOpenQuestionsContract = (draft) => {
253
+ const section = extractSection(draft, "Open Questions") ??
254
+ extractSection(draft, "Open Questions (Resolved)");
255
+ if (!section)
256
+ return draft;
257
+ const resolvedEntries = section.body
258
+ .split(/\r?\n/)
259
+ .map(normalizePdrResolvedEntry)
260
+ .filter((value) => Boolean(value));
261
+ const uniqueEntries = Array.from(new Map(resolvedEntries.map((entry) => [entry.toLowerCase(), entry])).values());
262
+ const body = uniqueEntries.length > 0
263
+ ? uniqueEntries.map((entry) => `- ${entry}`).join("\n")
264
+ : "- Resolved: No unresolved questions remain.";
265
+ return replaceSection(draft, "Open Questions", body);
266
+ };
267
+ const enforcePdrTechStackContract = (draft) => {
268
+ const section = extractSection(draft, "Technology Stack") ??
269
+ extractSection(draft, "Tech Stack");
270
+ if (!section)
271
+ return draft;
272
+ const body = cleanBody(section.body ?? "");
273
+ const additions = [];
274
+ if (!/chosen stack|selected stack|primary stack|we use/i.test(body)) {
275
+ additions.push("- Chosen stack: declare runtime, language, persistence, and tooling baseline.");
276
+ }
277
+ if (!/alternatives? considered|options? considered|alternative/i.test(body)) {
278
+ additions.push("- Alternatives considered: list realistic options evaluated but not selected.");
279
+ }
280
+ if (!/rationale|trade[- ]?off|because|why/i.test(body)) {
281
+ additions.push("- Rationale: explain why the selected stack is preferred for delivery and operations.");
282
+ }
283
+ if (additions.length === 0)
284
+ return draft;
285
+ const merged = [body, ...additions].filter(Boolean).join("\n");
286
+ return replaceSection(draft, "Technology Stack", merged);
287
+ };
288
+ const enforcePdrFolderTreeContract = (draft) => {
289
+ const section = extractSection(draft, "Target Folder Tree") ??
290
+ extractSection(draft, "Folder Tree");
291
+ if (!section)
292
+ return draft;
293
+ const body = section.body ?? "";
294
+ const treeBlock = body.match(/```(?:text)?\s*([\s\S]*?)```/i)?.[1]?.trim();
295
+ const treeEntries = treeBlock?.split(/\r?\n/).filter((line) => {
296
+ const trimmed = line.trim();
297
+ if (!trimmed)
298
+ return false;
299
+ if (trimmed === ".")
300
+ return true;
301
+ if (/^[├└│]/.test(trimmed))
302
+ return true;
303
+ return /[A-Za-z0-9_.-]+\/?/.test(trimmed);
304
+ }).length ?? 0;
305
+ const hasResponsibilityHints = treeBlock
306
+ ? /#|responsibilit|owner|module|service|tests?|scripts?/i.test(treeBlock)
307
+ : false;
308
+ const hasFence = /```(?:text)?[\s\S]*?```/i.test(body);
309
+ if (hasFence && treeEntries >= 8 && hasResponsibilityHints)
310
+ return draft;
311
+ const mergedBody = cleanBody(body).length > 0
312
+ ? `${cleanBody(body)}\n\n${DEFAULT_PDR_FOLDER_TREE_BLOCK}`
313
+ : DEFAULT_PDR_FOLDER_TREE_BLOCK;
314
+ return replaceSection(draft, "Target Folder Tree", mergedBody);
315
+ };
316
+ const applyPdrHardContracts = (draft) => {
317
+ let updated = draft;
318
+ updated = enforcePdrTechStackContract(updated);
319
+ updated = enforcePdrFolderTreeContract(updated);
320
+ updated = enforcePdrResolvedOpenQuestionsContract(updated);
321
+ return updated;
322
+ };
195
323
  class DocContextAssembler {
196
324
  constructor(docdex, workspace) {
197
325
  this.docdex = docdex;
@@ -518,8 +646,13 @@ const buildRunPrompt = (context, projectKey, prompts, runbook) => {
518
646
  docdexNote,
519
647
  [
520
648
  "Return markdown with exactly these sections as H2 headings, one time each:",
521
- "Introduction, Scope, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria",
649
+ "Introduction, Scope, Goals & Success Metrics, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Delivery & Dependency Sequencing, Target Folder Tree, Non-Functional Requirements, Risks & Mitigations, Resolved Decisions, Open Questions, Acceptance Criteria",
522
650
  "Do not use bold headings; use `##` headings only. Do not repeat sections.",
651
+ "Quality requirements:",
652
+ "- Produce implementation-ready, self-consistent content (no TODO/TBD/maybe placeholders).",
653
+ "- Include chosen stack, alternatives considered, and rationale in Technology Stack.",
654
+ "- Include a fenced `text` folder tree with responsibilities in Target Folder Tree.",
655
+ "- Keep Open Questions resolved-only (lines beginning with `Resolved:`).",
523
656
  ].join("\n"),
524
657
  runbookPrompt,
525
658
  ]
@@ -548,6 +681,14 @@ const buildSdsRunPrompt = (context, projectKey, prompts, runbook, template) => {
548
681
  `Context summary: ${context.summary}`,
549
682
  blocks,
550
683
  docdexNote,
684
+ [
685
+ "SDS quality requirements:",
686
+ "- Produce implementation-ready, self-consistent content (no TODO/TBD/maybe placeholders).",
687
+ "- Make architecture and stack choices explicit; include alternatives considered with rationale.",
688
+ "- Include a detailed folder tree in a fenced text block with file/folder responsibilities.",
689
+ "- Keep Open Questions resolved-only (lines beginning with 'Resolved:').",
690
+ "- Keep terminology, API contracts, data model, security, deployment, and operations aligned.",
691
+ ].join("\n"),
551
692
  runbook,
552
693
  ]
553
694
  .filter(Boolean)
@@ -561,20 +702,39 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
561
702
  { title: "Introduction", fallback: `This PDR summarizes project ${projectKey ?? "N/A"} based on ${rfpSource}.` },
562
703
  {
563
704
  title: "Scope",
564
- fallback: "In-scope: todo CRUD (title required; optional description, due date, priority), status toggle, filters/sort/search, bulk complete/delete, keyboard shortcuts, responsive UI, offline/localStorage. Out-of-scope: multi-user/auth/sync/backends, notifications/reminders, team features, heavy UI kits.",
705
+ fallback: "In-scope: capabilities, interfaces, and delivery outcomes explicitly defined in the RFP/context. Out-of-scope: speculative features, undocumented integrations, and requirements without source grounding.",
706
+ },
707
+ {
708
+ title: "Goals & Success Metrics",
709
+ fallback: "- Goal: deliver prioritized product outcomes from the RFP with a production-viable implementation path.\n- Success metric: core user workflows are complete and testable with defined acceptance criteria.\n- Success metric: release blockers are reduced to zero unresolved items before build-ready handoff.",
565
710
  },
566
711
  { title: "Technology Stack", fallback: techStackFallback },
567
712
  {
568
713
  title: "Requirements & Constraints",
569
714
  fallback: context.bullets.map((b) => `- ${b}`).join("\n") ||
570
- "- Data model, UX flows, keyboard shortcuts, and offline localStorage persistence per RFP.",
715
+ "- Capture functional, contract, data, security, compliance, and operational constraints from the source context.",
571
716
  },
572
717
  { title: "Architecture Overview", fallback: "Describe the system architecture, components, and interactions." },
573
718
  { title: "Interfaces / APIs", fallback: "List key interfaces and constraints. Do not invent endpoints." },
719
+ {
720
+ title: "Delivery & Dependency Sequencing",
721
+ fallback: "- Define foundational capabilities first, then dependent services/features.\n- Sequence work by dependency direction (providers before consumers).\n- Identify readiness gates and handoff criteria between phases.",
722
+ },
723
+ {
724
+ title: "Target Folder Tree",
725
+ fallback: DEFAULT_PDR_FOLDER_TREE_BLOCK,
726
+ },
574
727
  { title: "Non-Functional Requirements", fallback: "- Performance, reliability, compliance, and operational needs." },
575
728
  { title: "Risks & Mitigations", fallback: "- Enumerate risks from the RFP and proposed mitigations." },
576
- { title: "Open Questions", fallback: "- Outstanding questions to clarify with stakeholders." },
577
- { title: "Acceptance Criteria", fallback: "- Add/edit/delete todos persists offline; filters/sorts/search <100ms for 500 items; shortcuts (`n`, Ctrl/Cmd+Enter) work; bulk actions confirm/undo; responsive and accessible (WCAG AA basics)." },
729
+ {
730
+ title: "Resolved Decisions",
731
+ fallback: "- Decision: architecture and contract baselines are fixed for this implementation cycle.\n- Decision: dependency sequencing and release gates are mandatory.",
732
+ },
733
+ { title: "Open Questions", fallback: "- Resolved: No unresolved questions remain." },
734
+ {
735
+ title: "Acceptance Criteria",
736
+ fallback: "- Functional flows defined by the RFP are implemented and testable.\n- Interface and contract assumptions are explicit, validated, and traceable.\n- Operational and quality gates (tests, observability, release readiness) are satisfied for build-ready handoff.",
737
+ },
578
738
  ];
579
739
  const parts = [];
580
740
  parts.push(`# Product Design Review${projectKey ? `: ${projectKey}` : ""}`);
@@ -584,7 +744,7 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
584
744
  let body = cleaned && cleaned.length > 0 ? cleaned : cleanBody(section.fallback);
585
745
  if (section.title === "Interfaces / APIs" && (context.openapi?.length ?? 0) === 0) {
586
746
  const scrubbed = stripInventedEndpoints(body);
587
- const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (authentication/identity, data access, integrations, eventing, analytics/observability).";
747
+ const openApiFallback = "No OpenAPI excerpts available. Document required interfaces as explicit contracts/assumptions without inventing endpoint paths.";
588
748
  if (!scrubbed || scrubbed.length === 0 || /endpoint/i.test(scrubbed)) {
589
749
  body = cleanBody(openApiFallback);
590
750
  }
@@ -592,7 +752,7 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
592
752
  body = scrubbed;
593
753
  }
594
754
  if (!/openapi/i.test(body)) {
595
- body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
755
+ body = `${body}\n- No OpenAPI excerpts available; avoid inventing endpoint paths.`;
596
756
  }
597
757
  }
598
758
  parts.push(`## ${section.title}`);
@@ -600,7 +760,7 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
600
760
  }
601
761
  parts.push("## Source RFP");
602
762
  parts.push(rfpSource);
603
- return parts.join("\n\n");
763
+ return applyPdrHardContracts(parts.join("\n\n"));
604
764
  };
605
765
  const tidyPdrDraft = async (draft, agent, invoke) => {
606
766
  const prompt = [
@@ -608,7 +768,7 @@ const tidyPdrDraft = async (draft, agent, invoke) => {
608
768
  draft,
609
769
  "",
610
770
  "Requirements:",
611
- "- Keep exactly one instance of each H2 section: Introduction, Scope, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria, Source RFP.",
771
+ "- Keep exactly one instance of each H2 section: Introduction, Scope, Goals & Success Metrics, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Delivery & Dependency Sequencing, Target Folder Tree, Non-Functional Requirements, Risks & Mitigations, Resolved Decisions, Open Questions, Acceptance Criteria, Source RFP.",
612
772
  "- Remove duplicate sections, bold headings posing as sections, placeholder sentences, and repeated bullet blocks. If the same idea appears twice, keep the richer/longer version and drop the restatement.",
613
773
  "- Do not add new sections or reorder the required outline.",
614
774
  "- Keep content concise and aligned to the headings. Do not alter semantics.",
@@ -628,48 +788,195 @@ const PDR_ENRICHMENT_SECTIONS = [
628
788
  {
629
789
  title: "Architecture Overview",
630
790
  guidance: [
631
- "List concrete modules/components: UI shells (list/detail), state/store, persistence adapter (localStorage), keyboard/shortcut handler, bulk selection manager, search/filter/sort utilities.",
632
- "Describe data flow (load -> store -> UI render; user actions -> store mutate -> persist).",
633
- "Call out offline-first behavior and how persistence errors are handled.",
791
+ "List concrete components/services/modules from the source context and define responsibilities and boundaries for each.",
792
+ "Describe primary data/control flows across those components and include failure-handling boundaries.",
793
+ "State operational assumptions explicitly (readiness, dependencies, and degradation behavior) without domain-specific bias.",
634
794
  ],
635
795
  },
636
796
  {
637
797
  title: "Requirements & Constraints",
638
798
  guidance: [
639
- "Spell out data model fields (id, title, description?, status enum, dueDate format/timezone, priority enum order, createdAt/updatedAt, selection flag).",
640
- "Define localStorage key naming, schema versioning, and migration approach.",
641
- "Include accessibility expectations (keyboard focus, ARIA basics) and bundle size/perf targets.",
799
+ "Define domain entities, invariants, validation constraints, compatibility rules, and evolution/migration expectations where applicable.",
800
+ "Capture security/compliance constraints and any operational limitations explicitly tied to the source context.",
801
+ "Quantify measurable quality expectations (performance, reliability, accessibility, etc.) when available from context.",
642
802
  ],
643
803
  },
644
804
  {
645
805
  title: "Interfaces / APIs",
646
806
  guidance: [
647
- "Define internal contracts: store API (add/update/delete/toggle/filter/search), persistence adapter API (load/save/validate), shortcut map (keys -> actions), bulk action contract, optional export/import shape.",
648
- "Clarify validation rules (required title, length limits, due date handling).",
807
+ "Define internal and external interfaces/contracts, including responsibilities, inputs/outputs, and error semantics.",
808
+ "When OpenAPI is missing, state bounded interface assumptions and avoid inventing concrete endpoint paths.",
809
+ ],
810
+ },
811
+ {
812
+ title: "Delivery & Dependency Sequencing",
813
+ guidance: [
814
+ "Describe foundational-to-dependent delivery order and why that order reduces rework.",
815
+ "Identify hard dependencies, readiness criteria, and phase handoff checks.",
816
+ ],
817
+ },
818
+ {
819
+ title: "Target Folder Tree",
820
+ guidance: [
821
+ "Provide a fenced text tree with directories/files and short responsibility comments.",
822
+ "Include docs, source modules, contracts, database, deploy, tests, and scripts paths.",
649
823
  ],
650
824
  },
651
825
  {
652
826
  title: "Non-Functional Requirements",
653
827
  guidance: [
654
- "Quantify perf (<100ms for 500 items), bundle size goal, offline expectations, and accessibility targets (focus order, contrast).",
655
- "Reliability: handling storage quota/corruption, error surfacing.",
828
+ "Quantify performance/reliability/security/operability targets that are justified by source context.",
829
+ "Include observability and failure-containment expectations required for production readiness.",
656
830
  ],
657
831
  },
658
832
  {
659
833
  title: "Risks & Mitigations",
660
834
  guidance: [
661
- "Cover localStorage limits/corruption, keyboard conflicts, bulk delete accidents, schema drift, and mobile usability.",
662
- "Provide specific mitigations (validation, confirmations/undo, migrations, debounced search, accessible shortcuts).",
835
+ "Enumerate concrete delivery/architecture/operational risks derived from source context.",
836
+ "Provide explicit mitigations, fallback behavior, and verification checks for each high-impact risk.",
663
837
  ],
664
838
  },
665
839
  {
666
840
  title: "Open Questions",
667
841
  guidance: [
668
- "Resolve defaults: sort/tie-breakers, priority order, initial filters, due date format/timezone.",
669
- "Ask about export/import needs, accessibility targets, theming/branding, undo/confirm patterns.",
842
+ "Convert unresolved items to resolved decisions with explicit outcomes.",
843
+ "Keep entries in `Resolved: ...` form and remove TODO/TBD language.",
670
844
  ],
671
845
  },
672
846
  ];
847
+ const DEFAULT_SDS_SECTION_OUTLINE = [
848
+ "0. Introduction, Document Governance, and Change Policy",
849
+ "1. Purpose and Scope",
850
+ "2. System Boundaries and Non-Goals",
851
+ "3. Core Decisions (Baseline)",
852
+ "4. Platform Model and Technology Stack",
853
+ "5. Service Architecture and Dependency Contracts",
854
+ "6. Data Architecture and Ownership",
855
+ "7. Eventing, APIs, and Interface Contracts",
856
+ "8. Security, IAM, and Compliance",
857
+ "9. Risk and Control Model",
858
+ "10. Compute, Deployment, and Startup Sequencing",
859
+ "11. Target Folder Tree (Expanded with File Responsibilities)",
860
+ "12. Operations, Observability, and Quality Gates",
861
+ "13. External Integrations and Adapter Contracts",
862
+ "14. Policy, Telemetry, and Metering",
863
+ "15. Failure Modes, Recovery, and Rollback",
864
+ "16. Assumptions and Constraints",
865
+ "17. Resolved Decisions",
866
+ "18. Open Questions (Resolved)",
867
+ "19. Acceptance Criteria and Verification Plan",
868
+ ];
869
+ const DEFAULT_SDS_FOLDER_TREE_BLOCK = [
870
+ "```text",
871
+ ".",
872
+ "├── docs/ # product and architecture docs",
873
+ "│ ├── rfp/ # requirement sources",
874
+ "│ ├── pdr/ # product design reviews",
875
+ "│ └── sds/ # software design specifications",
876
+ "├── packages/ # source modules/services",
877
+ "│ ├── cli/ # command interfaces",
878
+ "│ ├── core/ # business/application services",
879
+ "│ └── integrations/ # external adapters/providers",
880
+ "├── openapi/ # API contracts",
881
+ "├── db/ # schema and migrations",
882
+ "├── deploy/ # compose/k8s/runtime manifests",
883
+ "├── tests/ # unit/integration/e2e suites",
884
+ "└── scripts/ # build/release/ops automation",
885
+ "```",
886
+ ].join("\n");
887
+ const findSdsSectionTitle = (sections, pattern, fallback) => sections.find((section) => pattern.test(section)) ?? fallback;
888
+ const ensureTrailingPeriod = (value) => {
889
+ const trimmed = value.trim();
890
+ if (!trimmed)
891
+ return trimmed;
892
+ if (/[.!?]$/.test(trimmed))
893
+ return trimmed;
894
+ return `${trimmed}.`;
895
+ };
896
+ const normalizeResolvedEntry = (line) => {
897
+ const stripped = line
898
+ .trim()
899
+ .replace(/^[-*+]\s+/, "")
900
+ .replace(/^\d+[.)]\s+/, "")
901
+ .trim();
902
+ if (!stripped)
903
+ return undefined;
904
+ if (/no unresolved questions remain|no open questions remain/i.test(stripped)) {
905
+ return "Resolved: No unresolved questions remain.";
906
+ }
907
+ const withoutPrefix = stripped.replace(/^resolved:\s*/i, "").trim();
908
+ const withoutQuestions = withoutPrefix.replace(/\?+$/, "").trim();
909
+ if (!withoutQuestions)
910
+ return undefined;
911
+ return `Resolved: ${ensureTrailingPeriod(withoutQuestions)}`;
912
+ };
913
+ const enforceResolvedOpenQuestionsContract = (draft, sections) => {
914
+ const title = findSdsSectionTitle(sections, /open questions?/i, "Open Questions (Resolved)");
915
+ const section = extractSection(draft, title);
916
+ if (!section)
917
+ return draft;
918
+ const resolvedEntries = section.body
919
+ .split(/\r?\n/)
920
+ .map(normalizeResolvedEntry)
921
+ .filter((value) => Boolean(value));
922
+ const deduped = Array.from(new Set(resolvedEntries.map((entry) => entry.toLowerCase()))).map((lower) => resolvedEntries.find((entry) => entry.toLowerCase() === lower));
923
+ const body = deduped.length > 0
924
+ ? deduped.map((entry) => `- ${entry}`).join("\n")
925
+ : "- Resolved: No unresolved questions remain.";
926
+ return replaceSection(draft, title, body);
927
+ };
928
+ const enforceTechStackContract = (draft, sections) => {
929
+ const title = findSdsSectionTitle(sections, /platform model|technology stack|tech stack/i, sections[0] ?? "Architecture");
930
+ const section = extractSection(draft, title);
931
+ if (!section)
932
+ return draft;
933
+ const body = cleanBody(section.body ?? "");
934
+ const additions = [];
935
+ if (!/chosen stack|selected stack|primary stack|we use/i.test(body)) {
936
+ additions.push("- Chosen stack: declare the selected runtime, language, persistence, and tooling baseline.");
937
+ }
938
+ if (!/alternatives? considered|options? considered|alternative/i.test(body)) {
939
+ additions.push("- Alternatives considered: list realistic options that were evaluated but not selected.");
940
+ }
941
+ if (!/rationale|trade[- ]?off|because|why/i.test(body)) {
942
+ additions.push("- Rationale: document why the selected stack is preferred for delivery, operations, and maintenance.");
943
+ }
944
+ if (additions.length === 0)
945
+ return draft;
946
+ const merged = [body, ...additions].filter(Boolean).join("\n");
947
+ return replaceSection(draft, title, merged);
948
+ };
949
+ const enforceFolderTreeContract = (draft, sections) => {
950
+ const title = findSdsSectionTitle(sections, /folder tree|directory structure|repository structure|target structure/i, "Target Folder Tree (Expanded with File Responsibilities)");
951
+ const section = extractSection(draft, title);
952
+ if (!section)
953
+ return draft;
954
+ const body = section.body ?? "";
955
+ const treeBlock = body.match(/```(?:text)?\s*([\s\S]*?)```/i)?.[1]?.trim();
956
+ const treeEntries = treeBlock?.split(/\r?\n/).filter((line) => {
957
+ const trimmed = line.trim();
958
+ if (!trimmed)
959
+ return false;
960
+ if (trimmed === ".")
961
+ return true;
962
+ if (/^[├└│]/.test(trimmed))
963
+ return true;
964
+ return /[A-Za-z0-9_.-]+\/?/.test(trimmed);
965
+ }).length ?? 0;
966
+ const hasResponsibilityHints = treeBlock ? /#|responsibilit|owner|module|service|tests?|scripts?/i.test(treeBlock) : false;
967
+ const hasFence = /```(?:text)?[\s\S]*?```/i.test(body);
968
+ if (hasFence && treeEntries >= 8 && hasResponsibilityHints)
969
+ return draft;
970
+ const mergedBody = cleanBody(body).length > 0 ? `${cleanBody(body)}\n\n${DEFAULT_SDS_FOLDER_TREE_BLOCK}` : DEFAULT_SDS_FOLDER_TREE_BLOCK;
971
+ return replaceSection(draft, title, mergedBody);
972
+ };
973
+ const applySdsHardContracts = (draft, sections) => {
974
+ let updated = draft;
975
+ updated = enforceTechStackContract(updated, sections);
976
+ updated = enforceFolderTreeContract(updated, sections);
977
+ updated = enforceResolvedOpenQuestionsContract(updated, sections);
978
+ return updated;
979
+ };
673
980
  const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
674
981
  const normalized = draft.trim();
675
982
  const templateHeadings = template
@@ -677,21 +984,7 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
677
984
  .map((line) => line.trim())
678
985
  .filter((line) => /^#{1,6}\s+/.test(line))
679
986
  .map((line) => line.replace(/^#{1,6}\s+/, "").trim());
680
- const defaultSections = [
681
- "Introduction",
682
- "Goals & Scope",
683
- "Architecture Overview",
684
- "Components & Responsibilities",
685
- "Data Model & Persistence",
686
- "Interfaces & Contracts",
687
- "Non-Functional Requirements",
688
- "Security & Compliance",
689
- "Failure Modes & Resilience",
690
- "Risks & Mitigations",
691
- "Assumptions",
692
- "Open Questions",
693
- "Acceptance Criteria",
694
- ];
987
+ const defaultSections = DEFAULT_SDS_SECTION_OUTLINE;
695
988
  const sections = templateHeadings.length ? templateHeadings : defaultSections;
696
989
  const cues = extractBullets(context.pdrs[0]?.content ?? context.rfp?.content ?? "", 10);
697
990
  const assumptionFallback = context.warnings.length > 0
@@ -699,6 +992,137 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
699
992
  : "- Document assumptions and dependencies.";
700
993
  const fallbackFor = (section) => {
701
994
  const key = section.toLowerCase();
995
+ if (key.includes("governance") || key.includes("change policy")) {
996
+ return [
997
+ "- Versioning: major/minor SDS revisions are tracked with implementation impact notes.",
998
+ "- Change control: architectural, schema, API, and security changes require explicit decision entries.",
999
+ "- Review cadence: update this SDS before task generation and before release hardening.",
1000
+ ].join("\n");
1001
+ }
1002
+ if (key.includes("purpose") || key.includes("scope")) {
1003
+ return cues.length ? cues.map((c) => `- ${c}`).join("\n") : "- Scope and objectives derived from PDR/RFP.";
1004
+ }
1005
+ if (key.includes("boundaries") || key.includes("non-goals")) {
1006
+ return [
1007
+ "- In-scope capabilities are limited to documented product outcomes and owned interfaces.",
1008
+ "- Out-of-scope paths are explicitly excluded to prevent accidental scope drift.",
1009
+ ].join("\n");
1010
+ }
1011
+ if (key.includes("core decisions")) {
1012
+ return [
1013
+ "- Runtime and language decisions are explicit and finalized for this delivery phase.",
1014
+ "- Data ownership boundaries and source-of-truth services are fixed.",
1015
+ "- API contract authority and compatibility policy are fixed before implementation.",
1016
+ ].join("\n");
1017
+ }
1018
+ if (key.includes("platform model") || key.includes("technology stack")) {
1019
+ return [
1020
+ "- Chosen stack: TypeScript services, relational persistence, and deterministic CI/CD workflows.",
1021
+ "- Alternatives considered: Python-first service core and JVM stack; rejected for this phase due to operational complexity and delivery latency.",
1022
+ "- Rationale: the chosen stack aligns with current team skills, release constraints, and maintainability goals.",
1023
+ ].join("\n");
1024
+ }
1025
+ if (key.includes("service architecture") || key.includes("dependency contracts")) {
1026
+ return [
1027
+ "- Define service boundaries, ownership, and contract direction (provider -> consumer).",
1028
+ "- Define startup dependency sequencing (foundational services first, dependent services after readiness).",
1029
+ "- Include health/readiness contracts and failure containment boundaries per service.",
1030
+ ].join("\n");
1031
+ }
1032
+ if (key.includes("data architecture") || key.includes("ownership")) {
1033
+ return [
1034
+ "- Define primary entities, write ownership, read models, and migration strategy.",
1035
+ "- Document retention, auditability, and schema evolution rules.",
1036
+ ].join("\n");
1037
+ }
1038
+ if (key.includes("eventing") || key.includes("interfaces") || key.includes("api")) {
1039
+ return [
1040
+ "- Define synchronous API contracts, async events, and schema compatibility rules.",
1041
+ "- Bind all external/public operations to OpenAPI references when available.",
1042
+ ].join("\n");
1043
+ }
1044
+ if (key.includes("security") || key.includes("iam") || key.includes("compliance")) {
1045
+ return [
1046
+ "- Document authentication, authorization, secret handling, and audit trails.",
1047
+ "- Define compliance boundaries for data handling and privileged operations.",
1048
+ ].join("\n");
1049
+ }
1050
+ if (key.includes("risk and control")) {
1051
+ return [
1052
+ "- Define risk gates, escalation paths, and release veto conditions.",
1053
+ "- Define controls for data quality, rollback triggers, and emergency stop criteria.",
1054
+ ].join("\n");
1055
+ }
1056
+ if (key.includes("compute") || key.includes("deployment") || key.includes("startup sequencing")) {
1057
+ return [
1058
+ "- Define runtime topology, environment contracts, and deployment wave order.",
1059
+ "- Define startup/readiness dependencies and rollback-safe rollout strategy.",
1060
+ ].join("\n");
1061
+ }
1062
+ if (key.includes("folder tree")) {
1063
+ return [
1064
+ "```text",
1065
+ ".",
1066
+ "├── docs/ # product and architecture docs",
1067
+ "│ ├── rfp/ # requirement sources",
1068
+ "│ ├── pdr/ # product design reviews",
1069
+ "│ └── sds/ # software design specifications",
1070
+ "├── packages/ # source modules/services",
1071
+ "│ ├── cli/ # command interfaces",
1072
+ "│ ├── core/ # business/application services",
1073
+ "│ └── integrations/ # external adapters/providers",
1074
+ "├── openapi/ # API contracts",
1075
+ "├── db/ # schema and migrations",
1076
+ "├── deploy/ # compose/k8s/runtime manifests",
1077
+ "├── tests/ # unit/integration/e2e suites",
1078
+ "└── scripts/ # build/release/ops automation",
1079
+ "```",
1080
+ ].join("\n");
1081
+ }
1082
+ if (key.includes("operations") || key.includes("observability") || key.includes("quality")) {
1083
+ return [
1084
+ "- Define SLOs with alert thresholds and runbook actions for breaches.",
1085
+ "- Define required test gates (unit/component/integration/e2e) before promotion.",
1086
+ "- Define operational dashboards, logging standards, and incident drill cadence.",
1087
+ ].join("\n");
1088
+ }
1089
+ if (key.includes("external integrations") || key.includes("adapter")) {
1090
+ return [
1091
+ "- For each external provider, document contract, rate limit/quota constraints, and timeout budgets.",
1092
+ "- Document adapter error handling, retry/backoff policy, and fallback behavior.",
1093
+ ].join("\n");
1094
+ }
1095
+ if (key.includes("policy") || key.includes("telemetry") || key.includes("metering")) {
1096
+ return [
1097
+ "- Policy: define cache key construction, TTL tiers, and consent matrix handling.",
1098
+ "- Telemetry: define schema for anonymous and identified events, including validation rules.",
1099
+ "- Metering: define usage collection, rate limits, quota enforcement, and billing/audit traces.",
1100
+ ].join("\n");
1101
+ }
1102
+ if (key.includes("failure") || key.includes("recovery") || key.includes("rollback")) {
1103
+ return [
1104
+ "- Enumerate failure modes, detection signals, rollback triggers, and recovery playbooks.",
1105
+ "- Define RTO/RPO or equivalent recovery objectives and escalation policy.",
1106
+ ].join("\n");
1107
+ }
1108
+ if (key.includes("assumption"))
1109
+ return assumptionFallback;
1110
+ if (key.includes("resolved decisions")) {
1111
+ return [
1112
+ "- Decision: Architecture, stack, and contract baselines are fixed for this implementation cycle.",
1113
+ "- Decision: Dependency sequencing and release gates are mandatory and deterministic.",
1114
+ ].join("\n");
1115
+ }
1116
+ if (key.includes("open question")) {
1117
+ return "- Resolved: No unresolved questions remain; implementation blockers are closed.";
1118
+ }
1119
+ if (key.includes("acceptance") || key.includes("verification")) {
1120
+ return [
1121
+ "- All required sections are complete, internally consistent, and traceable to source context.",
1122
+ "- Deployment and rollback procedures are validated in CI with reproducible artifacts.",
1123
+ "- Test gates pass and release readiness checks are green.",
1124
+ ].join("\n");
1125
+ }
702
1126
  if (key.includes("goal") || key.includes("scope")) {
703
1127
  return cues.length ? cues.map((c) => `- ${c}`).join("\n") : "- Goals and scope derived from PDR/RFP.";
704
1128
  }
@@ -718,15 +1142,13 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
718
1142
  return "- Failure modes, detection, rollback, and recovery paths.";
719
1143
  if (key.includes("risk"))
720
1144
  return "- Enumerate major risks and proposed mitigations.";
721
- if (key.includes("assumption"))
722
- return assumptionFallback;
723
1145
  if (key.includes("question"))
724
- return "- Outstanding questions and clarifications required.";
1146
+ return "- Resolved: No unresolved questions remain.";
725
1147
  if (key.includes("acceptance"))
726
1148
  return "- Criteria for sign-off and verification.";
727
1149
  if (key.includes("introduction"))
728
1150
  return `SDS for ${projectKey ?? "project"} derived from available PDR/RFP context.`;
729
- return "- TBD";
1151
+ return "- Provide explicit implementation-ready decisions, ownership, and verification details.";
730
1152
  };
731
1153
  const hasHeading = (title) => new RegExp(`^#{1,6}\\s+${title}\\b`, "im").test(normalized);
732
1154
  const parts = [];
@@ -758,7 +1180,7 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
758
1180
  structured = replaceSection(structured, interfaceTitle, body);
759
1181
  }
760
1182
  }
761
- return structured;
1183
+ return applySdsHardContracts(structured, sections);
762
1184
  };
763
1185
  const getSdsSections = (template) => {
764
1186
  const templateHeadings = template
@@ -766,21 +1188,7 @@ const getSdsSections = (template) => {
766
1188
  .map((line) => line.trim())
767
1189
  .filter((line) => /^#{1,6}\s+/.test(line))
768
1190
  .map((line) => line.replace(/^#{1,6}\s+/, "").trim());
769
- const defaultSections = [
770
- "Introduction",
771
- "Goals & Scope",
772
- "Architecture Overview",
773
- "Components & Responsibilities",
774
- "Data Model & Persistence",
775
- "Interfaces & Contracts",
776
- "Non-Functional Requirements",
777
- "Security & Compliance",
778
- "Failure Modes & Resilience",
779
- "Risks & Mitigations",
780
- "Assumptions",
781
- "Open Questions",
782
- "Acceptance Criteria",
783
- ];
1191
+ const defaultSections = DEFAULT_SDS_SECTION_OUTLINE;
784
1192
  const sections = templateHeadings.length ? templateHeadings : defaultSections;
785
1193
  const seen = new Set();
786
1194
  const unique = sections.filter((title) => {
@@ -1681,12 +2089,18 @@ export class DocsService {
1681
2089
  })));
1682
2090
  addResult(await runGate("gate-pdr-interfaces", "PDR Interfaces", () => runPdrInterfacesGate({ artifacts: runContext.artifacts })));
1683
2091
  addResult(await runGate("gate-pdr-ownership", "PDR Ownership", () => runPdrOwnershipGate({ artifacts: runContext.artifacts })));
2092
+ addResult(await runGate("gate-pdr-tech-stack-rationale", "PDR Tech Stack Rationale", () => runPdrTechStackRationaleGate({ artifacts: runContext.artifacts })));
2093
+ addResult(await runGate("gate-pdr-folder-tree", "PDR Folder Tree", () => runPdrFolderTreeGate({ artifacts: runContext.artifacts })));
2094
+ addResult(await runGate("gate-pdr-no-unresolved-items", "PDR No Unresolved Items", () => runPdrNoUnresolvedItemsGate({ artifacts: runContext.artifacts })));
1684
2095
  const openQuestionsEnabled = resolveOpenQuestions;
1685
2096
  addResult(await runGate("gate-pdr-open-questions", "PDR Open Questions", () => runPdrOpenQuestionsGate({
1686
2097
  artifacts: runContext.artifacts,
1687
2098
  enabled: openQuestionsEnabled,
1688
2099
  })));
1689
2100
  addResult(await runGate("gate-sds-explicit-decisions", "SDS Explicit Decisions", () => runSdsDecisionsGate({ artifacts: runContext.artifacts })));
2101
+ addResult(await runGate("gate-sds-tech-stack-rationale", "SDS Tech Stack Rationale", () => runSdsTechStackRationaleGate({ artifacts: runContext.artifacts })));
2102
+ addResult(await runGate("gate-sds-folder-tree", "SDS Folder Tree", () => runSdsFolderTreeGate({ artifacts: runContext.artifacts })));
2103
+ addResult(await runGate("gate-sds-no-unresolved-items", "SDS No Unresolved Items", () => runSdsNoUnresolvedItemsGate({ artifacts: runContext.artifacts })));
1690
2104
  addResult(await runGate("gate-sds-policy-telemetry", "SDS Policy Telemetry", () => runSdsPolicyTelemetryGate({ artifacts: runContext.artifacts })));
1691
2105
  addResult(await runGate("gate-sds-ops-observability-testing", "SDS Ops/Observability/Testing", () => runSdsOpsGate({ artifacts: runContext.artifacts })));
1692
2106
  addResult(await runGate("gate-sds-external-adapters", "SDS External Adapters", () => runSdsAdaptersGate({ artifacts: runContext.artifacts })));
@@ -2900,6 +3314,7 @@ export class DocsService {
2900
3314
  }
2901
3315
  }
2902
3316
  }
3317
+ draft = ensureStructuredDraft(draft, options.projectKey, context, context.rfp.path ?? context.rfp.id ?? "RFP");
2903
3318
  await this.jobService.writeCheckpoint(job.id, {
2904
3319
  stage: "draft_completed",
2905
3320
  timestamp: new Date().toISOString(),
@@ -3352,6 +3767,7 @@ export class DocsService {
3352
3767
  warnings.push(`Iterative SDS refinement failed; keeping first draft. ${String(error)}`);
3353
3768
  }
3354
3769
  }
3770
+ draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
3355
3771
  await this.jobService.writeCheckpoint(job.id, {
3356
3772
  stage: "draft_completed",
3357
3773
  timestamp: new Date().toISOString(),