@mcoda/core 0.1.34 → 0.1.36

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 (27) hide show
  1. package/dist/api/AgentsApi.d.ts +4 -1
  2. package/dist/api/AgentsApi.d.ts.map +1 -1
  3. package/dist/api/AgentsApi.js +4 -1
  4. package/dist/prompts/PdrPrompts.js +1 -1
  5. package/dist/services/docs/DocsService.d.ts +37 -0
  6. package/dist/services/docs/DocsService.d.ts.map +1 -1
  7. package/dist/services/docs/DocsService.js +537 -2
  8. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -1
  9. package/dist/services/docs/review/gates/OpenQuestionsGate.js +13 -2
  10. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -1
  11. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +12 -1
  12. package/dist/services/planning/CreateTasksService.d.ts +57 -0
  13. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  14. package/dist/services/planning/CreateTasksService.js +2491 -291
  15. package/dist/services/planning/SdsCoverageModel.d.ts +27 -0
  16. package/dist/services/planning/SdsCoverageModel.d.ts.map +1 -0
  17. package/dist/services/planning/SdsCoverageModel.js +138 -0
  18. package/dist/services/planning/SdsPreflightService.d.ts +2 -0
  19. package/dist/services/planning/SdsPreflightService.d.ts.map +1 -1
  20. package/dist/services/planning/SdsPreflightService.js +131 -37
  21. package/dist/services/planning/SdsStructureSignals.d.ts +24 -0
  22. package/dist/services/planning/SdsStructureSignals.d.ts.map +1 -0
  23. package/dist/services/planning/SdsStructureSignals.js +402 -0
  24. package/dist/services/planning/TaskSufficiencyService.d.ts +17 -0
  25. package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
  26. package/dist/services/planning/TaskSufficiencyService.js +409 -278
  27. package/package.json +6 -6
@@ -12,7 +12,9 @@ import { classifyTask } from "../backlog/TaskOrderingHeuristics.js";
12
12
  import { TaskOrderingService } from "../backlog/TaskOrderingService.js";
13
13
  import { QaTestCommandBuilder } from "../execution/QaTestCommandBuilder.js";
14
14
  import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator, } from "./KeyHelpers.js";
15
- import { TaskSufficiencyService } from "./TaskSufficiencyService.js";
15
+ import { collectSdsCoverageSignalsFromDocs, evaluateSdsCoverage, normalizeCoverageText, } from "./SdsCoverageModel.js";
16
+ import { collectSdsImplementationSignals, extractStructuredPaths, filterImplementationStructuredPaths, headingLooksImplementationRelevant, isStructuredFilePath, normalizeHeadingCandidate, normalizeStructuredPathToken, stripManagedSdsPreflightBlock, } from "./SdsStructureSignals.js";
17
+ import { TaskSufficiencyService, } from "./TaskSufficiencyService.js";
16
18
  import { SdsPreflightService } from "./SdsPreflightService.js";
17
19
  const formatBullets = (items, fallback) => {
18
20
  if (!items || items.length === 0)
@@ -162,6 +164,7 @@ const DOC_CONTEXT_SEGMENTS_PER_DOC = 8;
162
164
  const DOC_CONTEXT_FALLBACK_CHUNK_LENGTH = 480;
163
165
  const SDS_COVERAGE_HINT_HEADING_LIMIT = 24;
164
166
  const SDS_COVERAGE_REPORT_SECTION_LIMIT = 80;
167
+ const SDS_COVERAGE_REPORT_FOLDER_LIMIT = 240;
165
168
  const OPENAPI_HINT_OPERATIONS_LIMIT = 30;
166
169
  const DOCDEX_HANDLE = /^docdex:/i;
167
170
  const DOCDEX_LOCAL_HANDLE = /^docdex:local[-:/]/i;
@@ -170,7 +173,6 @@ const RELATIVE_DOC_PATH_PATTERN = /^(?:\.{1,2}\/)+[A-Za-z0-9._/-]+(?:\.[A-Za-z0-
170
173
  const FUZZY_DOC_CANDIDATE_LIMIT = 64;
171
174
  const DEPENDENCY_SCAN_LINE_LIMIT = 1400;
172
175
  const STARTUP_WAVE_SCAN_LINE_LIMIT = 4000;
173
- const VALID_AREAS = new Set(["web", "adm", "bck", "ops", "infra", "mobile"]);
174
176
  const VALID_TASK_TYPES = new Set(["feature", "bug", "chore", "spike"]);
175
177
  const VALID_EPIC_SERVICE_POLICIES = new Set(["auto-remediate", "fail"]);
176
178
  const CROSS_SERVICE_TAG = "cross_service";
@@ -189,16 +191,12 @@ const inferDocType = (filePath) => {
189
191
  const normalizeArea = (value) => {
190
192
  if (typeof value !== "string")
191
193
  return undefined;
192
- const tokens = value
194
+ const normalized = value
193
195
  .toLowerCase()
194
- .split(/[^a-z]+/)
195
- .map((token) => token.trim())
196
- .filter(Boolean);
197
- for (const token of tokens) {
198
- if (VALID_AREAS.has(token))
199
- return token;
200
- }
201
- return undefined;
196
+ .replace(/[^a-z0-9]+/g, "-")
197
+ .replace(/^-+|-+$/g, "")
198
+ .replace(/-{2,}/g, "-");
199
+ return normalized.length > 0 ? normalized.slice(0, 24) : undefined;
202
200
  };
203
201
  const normalizeTaskType = (value) => {
204
202
  if (typeof value !== "string")
@@ -252,40 +250,6 @@ const normalizeRelatedDocs = (value) => {
252
250
  }
253
251
  return normalized;
254
252
  };
255
- const extractMarkdownHeadings = (value, limit) => {
256
- if (!value)
257
- return [];
258
- const lines = value.split(/\r?\n/);
259
- const headings = [];
260
- for (let index = 0; index < lines.length; index += 1) {
261
- const line = lines[index]?.trim() ?? "";
262
- if (!line)
263
- continue;
264
- const hashHeading = line.match(/^#{1,6}\s+(.+)$/);
265
- if (hashHeading) {
266
- headings.push(hashHeading[1].trim());
267
- }
268
- else if (index + 1 < lines.length &&
269
- /^[=-]{3,}\s*$/.test((lines[index + 1] ?? "").trim()) &&
270
- !line.startsWith("-") &&
271
- !line.startsWith("*")) {
272
- headings.push(line);
273
- }
274
- else {
275
- const numberedHeading = line.match(/^(\d+(?:\.\d+)+)\s+(.+)$/);
276
- if (numberedHeading) {
277
- const headingText = `${numberedHeading[1]} ${numberedHeading[2]}`.trim();
278
- if (/[a-z]/i.test(headingText))
279
- headings.push(headingText);
280
- }
281
- }
282
- if (headings.length >= limit)
283
- break;
284
- }
285
- return uniqueStrings(headings
286
- .map((entry) => entry.replace(/[`*_]/g, "").trim())
287
- .filter(Boolean));
288
- };
289
253
  const pickDistributedIndices = (length, limit) => {
290
254
  if (length <= 0 || limit <= 0)
291
255
  return [];
@@ -574,8 +538,8 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
574
538
  ? "- Task-specific tests are added/updated and green in the task validation loop."
575
539
  : "- Verification evidence is captured in task logs/checklists for this scope.",
576
540
  relatedDocs?.length
577
- ? "- Related contracts/docs are consistent with delivered behavior."
578
- : "- Documentation impact is reviewed and no additional contract docs are required.",
541
+ ? "- Related interfaces/docs are consistent with delivered behavior."
542
+ : "- Documentation impact is reviewed and no additional interface docs are required.",
579
543
  qa?.blockers?.length ? "- Remaining QA blockers are explicit and actionable." : "- QA blockers are resolved or not present.",
580
544
  ];
581
545
  const defaultImplementationPlan = [
@@ -586,7 +550,7 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
586
550
  ];
587
551
  const defaultRisks = dependencies.length
588
552
  ? [`Delivery depends on upstream tasks: ${dependencies.join(", ")}.`]
589
- : ["Keep implementation aligned to SDS/OpenAPI contracts to avoid drift."];
553
+ : ["Keep implementation aligned to documented interfaces and dependency expectations to avoid drift."];
590
554
  return [
591
555
  `* **Task Key**: ${taskKey}`,
592
556
  "* **Objective**",
@@ -680,6 +644,51 @@ const SERVICE_PATH_CONTAINER_SEGMENTS = new Set([
680
644
  "lib",
681
645
  "src",
682
646
  ]);
647
+ const SOURCE_LIKE_PATH_SEGMENTS = new Set([
648
+ "api",
649
+ "app",
650
+ "apps",
651
+ "bin",
652
+ "cmd",
653
+ "components",
654
+ "controllers",
655
+ "handlers",
656
+ "internal",
657
+ "lib",
658
+ "libs",
659
+ "pages",
660
+ "routes",
661
+ "screens",
662
+ "server",
663
+ "servers",
664
+ "spec",
665
+ "specs",
666
+ "src",
667
+ "test",
668
+ "tests",
669
+ "ui",
670
+ "web",
671
+ ]);
672
+ const GENERIC_CONTAINER_PATH_SEGMENTS = new Set([
673
+ "adapters",
674
+ "apps",
675
+ "clients",
676
+ "consoles",
677
+ "domains",
678
+ "engines",
679
+ "features",
680
+ "modules",
681
+ "packages",
682
+ "platforms",
683
+ "plugins",
684
+ "products",
685
+ "servers",
686
+ "services",
687
+ "systems",
688
+ "tools",
689
+ "workers",
690
+ ]);
691
+ const NON_RUNTIME_STRUCTURE_ROOT_SEGMENTS = new Set(["docs", "fixtures", "runbooks", "policies", "policy"]);
683
692
  const SERVICE_NAME_STOPWORDS = new Set([
684
693
  "the",
685
694
  "a",
@@ -737,10 +746,197 @@ const SERVICE_NAME_INVALID = new Set([
737
746
  "repository",
738
747
  "codebase",
739
748
  ]);
749
+ const SERVICE_TEXT_INVALID_STARTERS = new Set([
750
+ "active",
751
+ "are",
752
+ "artifact",
753
+ "artifacts",
754
+ "be",
755
+ "been",
756
+ "being",
757
+ "block",
758
+ "blocks",
759
+ "build",
760
+ "builder",
761
+ "built",
762
+ "canonical",
763
+ "chain",
764
+ "configured",
765
+ "dedicated",
766
+ "deployment",
767
+ "discovered",
768
+ "failure",
769
+ "first",
770
+ "is",
771
+ "last",
772
+ "listing",
773
+ "mode",
774
+ "modes",
775
+ "never",
776
+ "no",
777
+ "not",
778
+ "ordered",
779
+ "owned",
780
+ "private",
781
+ "public",
782
+ "resolved",
783
+ "runtime",
784
+ "second",
785
+ "startup",
786
+ "third",
787
+ "validation",
788
+ "wave",
789
+ "waves",
790
+ "was",
791
+ "were",
792
+ ]);
793
+ const NON_RUNTIME_SERVICE_SINGLETONS = new Set([
794
+ "artifact",
795
+ "artifacts",
796
+ "compose",
797
+ "config",
798
+ "configs",
799
+ "doc",
800
+ "docs",
801
+ "interface",
802
+ "interfaces",
803
+ "key",
804
+ "keys",
805
+ "libraries",
806
+ "library",
807
+ "pdr",
808
+ "read",
809
+ "rfp",
810
+ "sds",
811
+ "script",
812
+ "scripts",
813
+ "src",
814
+ "systemd",
815
+ "test",
816
+ "tests",
817
+ "types",
818
+ "write",
819
+ ]);
820
+ const NON_RUNTIME_PATH_SERVICE_TOKENS = new Set([
821
+ "artifact",
822
+ "artifacts",
823
+ "manifest",
824
+ "manifests",
825
+ "schema",
826
+ "schemas",
827
+ "taxonomy",
828
+ "taxonomies",
829
+ ]);
740
830
  const SERVICE_LABEL_PATTERN = /\b([A-Za-z][A-Za-z0-9]*(?:[ _/-]+[A-Za-z][A-Za-z0-9]*){0,3})\s+(service|api|backend|frontend|worker|gateway|database|db|ui|client|server|adapter)\b/gi;
741
831
  const SERVICE_ARROW_PATTERN = /([A-Za-z][A-Za-z0-9 _/-]{1,80})\s*(?:->|=>|→)\s*([A-Za-z][A-Za-z0-9 _/-]{1,80})/g;
742
832
  const SERVICE_HANDLE_PATTERN = /\b((?:svc|ui|worker)-[a-z0-9-*]+)\b/gi;
743
833
  const WAVE_LABEL_PATTERN = /\bwave\s*([0-9]{1,2})\b/i;
834
+ const TOPOLOGY_HEADING_PATTERN = /\b(service|services|component|components|module|modules|interface|interfaces|runtime|runtimes|worker|workers|client|clients|gateway|gateways|server|servers|engine|engines|pipeline|pipelines|registry|registries|adapter|adapters|processor|processors|daemon|daemons|ops|operations|deployment|deployments|topology)\b/i;
835
+ const MARKDOWN_HEADING_PATTERN = /^(#{1,6})\s+(.+?)\s*$/;
836
+ const RUNTIME_COMPONENTS_HEADING_PATTERN = /\b(runtime components?|runtime topology|system components?|component topology|services?)\b/i;
837
+ const VERIFICATION_MATRIX_HEADING_PATTERN = /\b(verification matrix|validation matrix|test matrix|verification suites?)\b/i;
838
+ const ACCEPTANCE_SCENARIOS_HEADING_PATTERN = /\b(required )?acceptance scenarios?\b/i;
839
+ const BUILD_TARGET_RUNTIME_SEGMENTS = new Set([
840
+ "api",
841
+ "app",
842
+ "apps",
843
+ "bin",
844
+ "cli",
845
+ "client",
846
+ "clients",
847
+ "cmd",
848
+ "command",
849
+ "commands",
850
+ "engine",
851
+ "engines",
852
+ "feature",
853
+ "features",
854
+ "gateway",
855
+ "gateways",
856
+ "handler",
857
+ "handlers",
858
+ "module",
859
+ "modules",
860
+ "page",
861
+ "pages",
862
+ "processor",
863
+ "processors",
864
+ "route",
865
+ "routes",
866
+ "screen",
867
+ "screens",
868
+ "server",
869
+ "servers",
870
+ "service",
871
+ "services",
872
+ "src",
873
+ "ui",
874
+ "web",
875
+ "worker",
876
+ "workers",
877
+ ]);
878
+ const BUILD_TARGET_INTERFACE_SEGMENTS = new Set([
879
+ "contract",
880
+ "contracts",
881
+ "dto",
882
+ "dtos",
883
+ "interface",
884
+ "interfaces",
885
+ "proto",
886
+ "protocol",
887
+ "protocols",
888
+ "schema",
889
+ "schemas",
890
+ "spec",
891
+ "specs",
892
+ "type",
893
+ "types",
894
+ ]);
895
+ const BUILD_TARGET_DATA_SEGMENTS = new Set([
896
+ "cache",
897
+ "caches",
898
+ "data",
899
+ "db",
900
+ "ledger",
901
+ "migration",
902
+ "migrations",
903
+ "model",
904
+ "models",
905
+ "persistence",
906
+ "repository",
907
+ "repositories",
908
+ "storage",
909
+ ]);
910
+ const BUILD_TARGET_TEST_SEGMENTS = new Set([
911
+ "acceptance",
912
+ "e2e",
913
+ "integration",
914
+ "spec",
915
+ "specs",
916
+ "test",
917
+ "tests",
918
+ ]);
919
+ const BUILD_TARGET_OPS_SEGMENTS = new Set([
920
+ "deploy",
921
+ "deployment",
922
+ "deployments",
923
+ "helm",
924
+ "infra",
925
+ "k8s",
926
+ "ops",
927
+ "operation",
928
+ "operations",
929
+ "runbook",
930
+ "runbooks",
931
+ "script",
932
+ "scripts",
933
+ "systemd",
934
+ "terraform",
935
+ ]);
936
+ const BUILD_TARGET_DOC_SEGMENTS = new Set(["docs", "policy", "policies", "rfp", "pdr", "sds"]);
937
+ const MANIFEST_TARGET_BASENAME_PATTERN = /^(package\.json|pnpm-workspace\.yaml|pnpm-lock\.yaml|turbo\.json|tsconfig(?:\.[^.]+)?\.json|eslint(?:\.[^.]+)?\.(?:js|cjs|mjs|json)|prettier(?:\.[^.]+)?\.(?:js|cjs|mjs|json)|vite\.config\.[^.]+|webpack\.config\.[^.]+|rollup\.config\.[^.]+|cargo\.toml|pyproject\.toml|go\.mod|go\.sum|pom\.xml|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|requirements\.txt|poetry\.lock|foundry\.toml|hardhat\.config\.[^.]+)$/i;
938
+ const SERVICE_ARTIFACT_BASENAME_PATTERN = /(?:\.service|\.socket|\.timer|(?:^|[.-])compose\.(?:ya?ml|json)$|docker-compose\.(?:ya?ml|json)$)$/i;
939
+ const GENERIC_IMPLEMENTATION_TASK_PATTERN = /update the concrete .* modules surfaced by the sds/i;
744
940
  const nextUniqueLocalId = (prefix, existing) => {
745
941
  let index = 1;
746
942
  let candidate = `${prefix}-${index}`;
@@ -763,11 +959,21 @@ const looksLikeSdsDoc = (doc) => {
763
959
  .slice(0, 5000);
764
960
  return STRICT_SDS_CONTENT_PATTERN.test(sample);
765
961
  };
962
+ const looksLikePathishDocId = (value) => {
963
+ if (!value)
964
+ return false;
965
+ if (DOCDEX_LOCAL_HANDLE.test(value))
966
+ return false;
967
+ return (value.includes("/") ||
968
+ value.includes("\\") ||
969
+ FILE_EXTENSION_PATTERN.test(value) ||
970
+ STRICT_SDS_PATH_PATTERN.test(value.replace(/\\/g, "/").toLowerCase()));
971
+ };
766
972
  const EPIC_SCHEMA_SNIPPET = `{
767
973
  "epics": [
768
974
  {
769
975
  "localId": "e1",
770
- "area": "web|adm|bck|ops|infra|mobile",
976
+ "area": "documented-area-label",
771
977
  "title": "Epic title",
772
978
  "description": "Epic description using the epic template",
773
979
  "acceptanceCriteria": ["criterion"],
@@ -816,6 +1022,48 @@ const TASK_SCHEMA_SNIPPET = `{
816
1022
  }
817
1023
  ]
818
1024
  }`;
1025
+ const FULL_PLAN_SCHEMA_SNIPPET = `{
1026
+ "epics": [
1027
+ {
1028
+ "localId": "e1",
1029
+ "area": "documented-area-label",
1030
+ "title": "Epic title",
1031
+ "description": "Epic description using the epic template",
1032
+ "acceptanceCriteria": ["criterion"],
1033
+ "relatedDocs": ["docdex:..."],
1034
+ "priorityHint": 50,
1035
+ "serviceIds": ["backend-api"],
1036
+ "tags": ["cross_service"],
1037
+ "stories": [
1038
+ {
1039
+ "localId": "us1",
1040
+ "title": "Story title",
1041
+ "userStory": "As a ...",
1042
+ "description": "Story description using the template",
1043
+ "acceptanceCriteria": ["criterion"],
1044
+ "relatedDocs": ["docdex:..."],
1045
+ "priorityHint": 50,
1046
+ "tasks": [
1047
+ {
1048
+ "localId": "t1",
1049
+ "title": "Task title",
1050
+ "type": "feature|bug|chore|spike",
1051
+ "description": "Task description using the template",
1052
+ "estimatedStoryPoints": 3,
1053
+ "priorityHint": 50,
1054
+ "dependsOnKeys": ["t0"],
1055
+ "relatedDocs": ["docdex:..."],
1056
+ "unitTests": ["unit test description"],
1057
+ "componentTests": ["component test description"],
1058
+ "integrationTests": ["integration test description"],
1059
+ "apiTests": ["api test description"]
1060
+ }
1061
+ ]
1062
+ }
1063
+ ]
1064
+ }
1065
+ ]
1066
+ }`;
819
1067
  export class CreateTasksService {
820
1068
  constructor(workspace, deps) {
821
1069
  this.workspace = workspace;
@@ -931,7 +1179,7 @@ export class CreateTasksService {
931
1179
  if (!documents.some((doc) => looksLikeSdsDoc(doc))) {
932
1180
  throw new Error("create-tasks requires at least one SDS document. Add an SDS file (for example docs/sds.md) or pass SDS paths as input.");
933
1181
  }
934
- return this.sortDocsForPlanning(documents);
1182
+ return this.sortDocsForPlanning(this.dedupePlanningDocs(documents.map((doc) => this.sanitizeDocForPlanning(doc))));
935
1183
  }
936
1184
  normalizeDocInputForSet(input) {
937
1185
  if (input.startsWith("docdex:"))
@@ -953,8 +1201,19 @@ export class CreateTasksService {
953
1201
  }
954
1202
  return merged;
955
1203
  }
1204
+ canonicalizeDocPathKey(value) {
1205
+ const trimmed = `${value ?? ""}`.trim();
1206
+ if (!trimmed || DOCDEX_LOCAL_HANDLE.test(trimmed))
1207
+ return undefined;
1208
+ if (path.isAbsolute(trimmed))
1209
+ return path.resolve(trimmed).toLowerCase();
1210
+ if (looksLikePathishDocId(trimmed)) {
1211
+ return path.resolve(this.workspace.workspaceRoot, trimmed).toLowerCase();
1212
+ }
1213
+ return undefined;
1214
+ }
956
1215
  docIdentity(doc) {
957
- const pathKey = `${doc.path ?? ""}`.trim().toLowerCase();
1216
+ const pathKey = this.canonicalizeDocPathKey(doc.path) ?? this.canonicalizeDocPathKey(doc.id);
958
1217
  const idKey = `${doc.id ?? ""}`.trim().toLowerCase();
959
1218
  if (pathKey)
960
1219
  return `path:${pathKey}`;
@@ -978,6 +1237,61 @@ export class CreateTasksService {
978
1237
  }
979
1238
  return merged;
980
1239
  }
1240
+ sanitizeDocForPlanning(doc) {
1241
+ const content = stripManagedSdsPreflightBlock(doc.content);
1242
+ const segments = content !== doc.content
1243
+ ? []
1244
+ : (doc.segments ?? [])
1245
+ .map((segment) => {
1246
+ const sanitizedContent = stripManagedSdsPreflightBlock(segment.content ?? undefined);
1247
+ return {
1248
+ ...segment,
1249
+ content: sanitizedContent ?? segment.content,
1250
+ };
1251
+ })
1252
+ .filter((segment) => `${segment.content ?? ""}`.trim().length > 0 || `${segment.heading ?? ""}`.trim().length > 0);
1253
+ const sanitized = {
1254
+ ...doc,
1255
+ content: content ?? doc.content,
1256
+ segments,
1257
+ };
1258
+ if (looksLikeSdsDoc(sanitized) && `${sanitized.docType ?? ""}`.toUpperCase() !== "SDS") {
1259
+ sanitized.docType = "SDS";
1260
+ }
1261
+ return sanitized;
1262
+ }
1263
+ scorePlanningDoc(doc) {
1264
+ const segmentCount = doc.segments?.length ?? 0;
1265
+ const contentLength = `${doc.content ?? ""}`.length;
1266
+ return ((looksLikeSdsDoc(doc) ? 5000 : 0) +
1267
+ (doc.path ? 400 : 0) +
1268
+ segmentCount * 20 +
1269
+ Math.min(300, contentLength));
1270
+ }
1271
+ mergePlanningDocPair(current, incoming) {
1272
+ const [primary, secondary] = this.scorePlanningDoc(incoming) > this.scorePlanningDoc(current) ? [incoming, current] : [current, incoming];
1273
+ const merged = {
1274
+ ...secondary,
1275
+ ...primary,
1276
+ path: primary.path ?? secondary.path,
1277
+ title: primary.title ?? secondary.title,
1278
+ content: primary.content ?? secondary.content,
1279
+ segments: (primary.segments?.length ?? 0) > 0 ? primary.segments : secondary.segments,
1280
+ };
1281
+ if (looksLikeSdsDoc(merged) && `${merged.docType ?? ""}`.toUpperCase() !== "SDS") {
1282
+ merged.docType = "SDS";
1283
+ }
1284
+ return merged;
1285
+ }
1286
+ dedupePlanningDocs(docs) {
1287
+ const merged = new Map();
1288
+ for (const doc of docs) {
1289
+ const identity = this.docIdentity(doc);
1290
+ const existing = merged.get(identity);
1291
+ merged.set(identity, existing ? this.mergePlanningDocPair(existing, doc) : doc);
1292
+ }
1293
+ return Array.from(merged.values());
1294
+ }
981
1295
  sortDocsForPlanning(docs) {
982
1296
  return [...docs].sort((a, b) => {
983
1297
  const aIsSds = looksLikeSdsDoc(a) ? 0 : 1;
@@ -1158,47 +1472,29 @@ export class CreateTasksService {
1158
1472
  .map((entry) => entry.path);
1159
1473
  }
1160
1474
  normalizeStructurePathToken(value) {
1161
- const normalized = value
1162
- .replace(/\\/g, "/")
1163
- .replace(/^[./]+/, "")
1164
- .replace(/^\/+/, "")
1165
- .trim();
1475
+ const normalized = normalizeStructuredPathToken(value);
1166
1476
  if (!normalized)
1167
1477
  return undefined;
1168
- if (normalized.length > 140)
1169
- return undefined;
1170
- if (!normalized.includes("/"))
1171
- return undefined;
1172
- if (normalized.includes("://"))
1173
- return undefined;
1174
- if (/[\u0000-\u001f]/.test(normalized))
1175
- return undefined;
1176
- const hadTrailingSlash = /\/$/.test(normalized);
1177
- const parts = normalized.split("/").filter(Boolean);
1178
- if (parts.length < 2 && !(hadTrailingSlash && parts.length === 1))
1179
- return undefined;
1180
- if (parts.some((part) => part === "." || part === ".."))
1478
+ const root = normalized.split("/")[0]?.toLowerCase();
1479
+ if (root && DOC_SCAN_IGNORE_DIRS.has(root))
1181
1480
  return undefined;
1182
- if (parts.length === 1 && !TOP_LEVEL_STRUCTURE_PATTERN.test(parts[0]))
1183
- return undefined;
1184
- if (DOC_SCAN_IGNORE_DIRS.has(parts[0].toLowerCase()))
1185
- return undefined;
1186
- return parts.join("/");
1481
+ return normalized;
1187
1482
  }
1188
1483
  extractStructureTargets(docs) {
1189
1484
  const directories = new Set();
1190
1485
  const files = new Set();
1191
1486
  for (const doc of docs) {
1487
+ const relativeDocPath = doc.path ? path.relative(this.workspace.workspaceRoot, doc.path).replace(/\\/g, "/") : undefined;
1488
+ const localDocPath = relativeDocPath && !relativeDocPath.startsWith("..") && !path.isAbsolute(relativeDocPath)
1489
+ ? relativeDocPath
1490
+ : undefined;
1192
1491
  const segments = (doc.segments ?? []).map((segment) => segment.content).filter(Boolean).join("\n");
1193
- const corpus = [doc.title, doc.path, doc.content, segments].filter(Boolean).join("\n");
1194
- for (const match of corpus.matchAll(DOC_PATH_TOKEN_PATTERN)) {
1195
- const token = match[2];
1196
- if (!token)
1197
- continue;
1492
+ const corpus = [localDocPath, doc.content, segments].filter(Boolean).join("\n");
1493
+ for (const token of filterImplementationStructuredPaths(extractStructuredPaths(corpus, 256))) {
1198
1494
  const normalized = this.normalizeStructurePathToken(token);
1199
1495
  if (!normalized)
1200
1496
  continue;
1201
- if (FILE_EXTENSION_PATTERN.test(path.basename(normalized))) {
1497
+ if (isStructuredFilePath(path.basename(normalized))) {
1202
1498
  files.add(normalized);
1203
1499
  const parent = path.dirname(normalized).replace(/\\/g, "/");
1204
1500
  if (parent && parent !== ".")
@@ -1238,6 +1534,61 @@ export class CreateTasksService {
1238
1534
  return undefined;
1239
1535
  return candidate.length >= 2 ? candidate : undefined;
1240
1536
  }
1537
+ normalizeTextServiceName(value) {
1538
+ const candidate = this.normalizeServiceName(value);
1539
+ if (!candidate)
1540
+ return undefined;
1541
+ const tokens = candidate.split(" ").filter(Boolean);
1542
+ if (tokens.length === 0 || tokens.length > 3)
1543
+ return undefined;
1544
+ const first = tokens[0] ?? "";
1545
+ if (SERVICE_TEXT_INVALID_STARTERS.has(first))
1546
+ return undefined;
1547
+ if (tokens.length === 1) {
1548
+ if (first.length < 3)
1549
+ return undefined;
1550
+ if (SERVICE_NAME_INVALID.has(first) || NON_RUNTIME_SERVICE_SINGLETONS.has(first))
1551
+ return undefined;
1552
+ if (SERVICE_NAME_STOPWORDS.has(first))
1553
+ return undefined;
1554
+ }
1555
+ return candidate;
1556
+ }
1557
+ isLikelyServiceContainerSegment(parts, index) {
1558
+ const segment = parts[index];
1559
+ if (!segment)
1560
+ return false;
1561
+ if (SERVICE_PATH_CONTAINER_SEGMENTS.has(segment))
1562
+ return true;
1563
+ if (index !== 0)
1564
+ return false;
1565
+ const next = parts[index + 1];
1566
+ if (!next)
1567
+ return false;
1568
+ const following = parts[index + 2];
1569
+ const nextLooksSpecific = !SERVICE_PATH_CONTAINER_SEGMENTS.has(next) &&
1570
+ !NON_RUNTIME_STRUCTURE_ROOT_SEGMENTS.has(next) &&
1571
+ !SOURCE_LIKE_PATH_SEGMENTS.has(next) &&
1572
+ !isStructuredFilePath(next);
1573
+ if (!nextLooksSpecific)
1574
+ return false;
1575
+ if (GENERIC_CONTAINER_PATH_SEGMENTS.has(segment)) {
1576
+ if (!following)
1577
+ return true;
1578
+ return SOURCE_LIKE_PATH_SEGMENTS.has(following) || isStructuredFilePath(following);
1579
+ }
1580
+ return false;
1581
+ }
1582
+ normalizePathDerivedServiceName(value) {
1583
+ const candidate = this.normalizeServiceName(value);
1584
+ if (!candidate)
1585
+ return undefined;
1586
+ if (NON_RUNTIME_SERVICE_SINGLETONS.has(candidate))
1587
+ return undefined;
1588
+ if (candidate.split(" ").some((token) => NON_RUNTIME_PATH_SERVICE_TOKENS.has(token)))
1589
+ return undefined;
1590
+ return candidate;
1591
+ }
1241
1592
  deriveServiceFromPathToken(pathToken) {
1242
1593
  const parts = pathToken
1243
1594
  .replace(/\\/g, "/")
@@ -1246,11 +1597,18 @@ export class CreateTasksService {
1246
1597
  .filter(Boolean);
1247
1598
  if (!parts.length)
1248
1599
  return undefined;
1600
+ if (NON_RUNTIME_STRUCTURE_ROOT_SEGMENTS.has(parts[0] ?? ""))
1601
+ return undefined;
1602
+ if (parts.length === 1 && isStructuredFilePath(parts[0] ?? ""))
1603
+ return undefined;
1249
1604
  let idx = 0;
1250
- while (idx < parts.length - 1 && SERVICE_PATH_CONTAINER_SEGMENTS.has(parts[idx])) {
1605
+ while (idx < parts.length - 1 && this.isLikelyServiceContainerSegment(parts, idx)) {
1251
1606
  idx += 1;
1252
1607
  }
1253
- return this.normalizeServiceName(parts[idx] ?? parts[0]);
1608
+ const candidate = parts[idx] ?? parts[0];
1609
+ if (isStructuredFilePath(candidate))
1610
+ return undefined;
1611
+ return this.normalizePathDerivedServiceName(candidate);
1254
1612
  }
1255
1613
  addServiceAlias(aliases, rawValue) {
1256
1614
  const canonical = this.normalizeServiceName(rawValue);
@@ -1266,6 +1624,10 @@ export class CreateTasksService {
1266
1624
  .trim();
1267
1625
  if (alias)
1268
1626
  existing.add(alias);
1627
+ if (alias.endsWith("s") && alias.length > 3)
1628
+ existing.add(alias.slice(0, -1));
1629
+ if (!alias.endsWith("s") && alias.length > 2)
1630
+ existing.add(`${alias}s`);
1269
1631
  aliases.set(canonical, existing);
1270
1632
  return canonical;
1271
1633
  }
@@ -1275,7 +1637,7 @@ export class CreateTasksService {
1275
1637
  const mentions = new Set();
1276
1638
  for (const match of text.matchAll(SERVICE_LABEL_PATTERN)) {
1277
1639
  const phrase = `${match[1] ?? ""} ${match[2] ?? ""}`.trim();
1278
- const normalized = this.normalizeServiceName(phrase);
1640
+ const normalized = this.normalizeTextServiceName(phrase);
1279
1641
  if (normalized)
1280
1642
  mentions.add(normalized);
1281
1643
  }
@@ -1289,7 +1651,18 @@ export class CreateTasksService {
1289
1651
  }
1290
1652
  return Array.from(mentions);
1291
1653
  }
1292
- resolveServiceMentionFromPhrase(phrase, aliases) {
1654
+ deriveServiceMentionFromPathPhrase(phrase) {
1655
+ for (const match of phrase.matchAll(DOC_PATH_TOKEN_PATTERN)) {
1656
+ const token = match[2];
1657
+ if (!token)
1658
+ continue;
1659
+ const derived = this.deriveServiceFromPathToken(token);
1660
+ if (derived)
1661
+ return derived;
1662
+ }
1663
+ return undefined;
1664
+ }
1665
+ resolveServiceMentionFromPhrase(phrase, aliases, options = {}) {
1293
1666
  const normalizedPhrase = phrase
1294
1667
  .toLowerCase()
1295
1668
  .replace(/[._/-]+/g, " ")
@@ -1312,6 +1685,11 @@ export class CreateTasksService {
1312
1685
  }
1313
1686
  if (best)
1314
1687
  return best.key;
1688
+ const pathDerived = this.deriveServiceMentionFromPathPhrase(phrase);
1689
+ if (pathDerived)
1690
+ return pathDerived;
1691
+ if (!options.allowAliasRegistration)
1692
+ return undefined;
1315
1693
  const mention = this.extractServiceMentionsFromText(phrase)[0];
1316
1694
  if (!mention)
1317
1695
  return undefined;
@@ -1395,10 +1773,17 @@ export class CreateTasksService {
1395
1773
  resolved.add(canonical);
1396
1774
  }
1397
1775
  if (resolved.size === 0) {
1398
- for (const mention of this.extractServiceMentionsFromText(cell)) {
1399
- const canonical = this.addServiceAlias(aliases, mention);
1400
- if (canonical)
1401
- resolved.add(canonical);
1776
+ const normalizedCell = this.normalizeServiceLookupKey(cell);
1777
+ const haystack = normalizedCell ? ` ${normalizedCell} ` : "";
1778
+ for (const [service, names] of aliases.entries()) {
1779
+ for (const alias of names) {
1780
+ if (!alias || alias.length < 2)
1781
+ continue;
1782
+ if (!haystack.includes(` ${alias} `))
1783
+ continue;
1784
+ resolved.add(service);
1785
+ break;
1786
+ }
1402
1787
  }
1403
1788
  }
1404
1789
  return Array.from(resolved);
@@ -1441,6 +1826,31 @@ export class CreateTasksService {
1441
1826
  for (const service of resolveServicesFromCell(cells[0]))
1442
1827
  registerWave(service, waveIndex);
1443
1828
  }
1829
+ for (let index = 0; index < lines.length; index += 1) {
1830
+ const line = lines[index];
1831
+ const waveMatch = line.match(WAVE_LABEL_PATTERN);
1832
+ if (!waveMatch)
1833
+ continue;
1834
+ const waveIndex = Number.parseInt(waveMatch[1] ?? "", 10);
1835
+ if (!Number.isFinite(waveIndex))
1836
+ continue;
1837
+ const contextLines = [line];
1838
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
1839
+ const next = lines[cursor];
1840
+ if (WAVE_LABEL_PATTERN.test(next))
1841
+ break;
1842
+ if (/^#{1,6}\s+/.test(next))
1843
+ break;
1844
+ if (/^(?:[-*]|\d+[.)])\s+/.test(next))
1845
+ break;
1846
+ contextLines.push(next);
1847
+ if (contextLines.length >= 4)
1848
+ break;
1849
+ }
1850
+ for (const service of resolveServicesFromCell(contextLines.join(" "))) {
1851
+ registerWave(service, waveIndex);
1852
+ }
1853
+ }
1444
1854
  const startupWaves = Array.from(startupWavesMap.entries())
1445
1855
  .sort((a, b) => a[0] - b[0])
1446
1856
  .map(([wave, services]) => ({ wave, services: Array.from(services).sort((a, b) => a.localeCompare(b)) }));
@@ -1516,11 +1926,18 @@ export class CreateTasksService {
1516
1926
  buildServiceDependencyGraph(plan, docs) {
1517
1927
  const aliases = new Map();
1518
1928
  const dependencies = new Map();
1929
+ const sourceBackedServices = new Set();
1519
1930
  const register = (value) => {
1520
1931
  if (!value)
1521
1932
  return undefined;
1522
1933
  return this.addServiceAlias(aliases, value);
1523
1934
  };
1935
+ const registerSourceBacked = (value) => {
1936
+ const canonical = register(value);
1937
+ if (canonical)
1938
+ sourceBackedServices.add(canonical);
1939
+ return canonical;
1940
+ };
1524
1941
  const docsText = docs
1525
1942
  .map((doc) => [doc.title, doc.path, doc.content, ...(doc.segments ?? []).map((segment) => segment.content)].filter(Boolean).join("\n"))
1526
1943
  .join("\n");
@@ -1535,17 +1952,32 @@ export class CreateTasksService {
1535
1952
  }
1536
1953
  }
1537
1954
  const structureTargets = this.extractStructureTargets(docs);
1538
- for (const token of [...structureTargets.directories, ...structureTargets.files]) {
1539
- register(this.deriveServiceFromPathToken(token));
1955
+ const structureTokens = [...structureTargets.directories, ...structureTargets.files];
1956
+ for (const token of structureTokens) {
1957
+ if (!token.includes("/") &&
1958
+ !isStructuredFilePath(path.basename(token)) &&
1959
+ structureTokens.some((candidate) => candidate !== token && candidate.startsWith(`${token}/`))) {
1960
+ continue;
1961
+ }
1962
+ registerSourceBacked(this.deriveServiceFromPathToken(token));
1963
+ }
1964
+ for (const component of this.extractRuntimeComponentNames(docs)) {
1965
+ registerSourceBacked(component);
1540
1966
  }
1541
1967
  for (const match of docsText.matchAll(SERVICE_HANDLE_PATTERN))
1542
- register(match[1]);
1543
- for (const match of planText.matchAll(SERVICE_HANDLE_PATTERN))
1544
- register(match[1]);
1545
- for (const mention of this.extractServiceMentionsFromText(docsText))
1546
- register(mention);
1547
- for (const mention of this.extractServiceMentionsFromText(planText))
1548
- register(mention);
1968
+ registerSourceBacked(match[1]);
1969
+ const docsHaveRuntimeTopologySignals = sourceBackedServices.size > 0 &&
1970
+ (structureTargets.directories.length > 0 ||
1971
+ structureTargets.files.length > 0 ||
1972
+ this.collectDependencyStatements(docsText).length > 0 ||
1973
+ WAVE_LABEL_PATTERN.test(docsText) ||
1974
+ TOPOLOGY_HEADING_PATTERN.test(docsText));
1975
+ if (!docsHaveRuntimeTopologySignals) {
1976
+ for (const match of planText.matchAll(SERVICE_HANDLE_PATTERN))
1977
+ register(match[1]);
1978
+ for (const mention of this.extractServiceMentionsFromText(planText))
1979
+ register(mention);
1980
+ }
1549
1981
  const corpus = [docsText, planText].filter(Boolean);
1550
1982
  for (const text of corpus) {
1551
1983
  const statements = this.collectDependencyStatements(text);
@@ -1570,6 +2002,268 @@ export class CreateTasksService {
1570
2002
  foundationalDependencies: waveHints.foundationalDependencies,
1571
2003
  };
1572
2004
  }
2005
+ summarizeTopologySignals(docs) {
2006
+ const structureTargets = this.extractStructureTargets(docs);
2007
+ const structureServices = uniqueStrings([...structureTargets.directories, ...structureTargets.files]
2008
+ .map((token) => this.deriveServiceFromPathToken(token))
2009
+ .filter((value) => Boolean(value))
2010
+ .concat(this.extractRuntimeComponentNames(docs))).slice(0, 24);
2011
+ const topologyHeadings = this.extractSdsSectionCandidates(docs, 64)
2012
+ .filter((heading) => TOPOLOGY_HEADING_PATTERN.test(heading))
2013
+ .slice(0, 24);
2014
+ const docsText = docs
2015
+ .map((doc) => [doc.title, doc.path, doc.content, ...(doc.segments ?? []).map((segment) => segment.content)].filter(Boolean).join("\n"))
2016
+ .join("\n");
2017
+ const dependencyPairs = uniqueStrings(this.collectDependencyStatements(docsText).map((statement) => `${statement.dependent} -> ${statement.dependency}`)).slice(0, 16);
2018
+ const waveMentions = docsText
2019
+ .split(/\r?\n/)
2020
+ .map((line) => line.trim())
2021
+ .filter(Boolean)
2022
+ .filter((line) => WAVE_LABEL_PATTERN.test(line))
2023
+ .slice(0, 16);
2024
+ return {
2025
+ structureServices,
2026
+ topologyHeadings,
2027
+ dependencyPairs,
2028
+ waveMentions,
2029
+ };
2030
+ }
2031
+ buildSourceTopologyExpectation(docs) {
2032
+ const signalSummary = this.summarizeTopologySignals(docs);
2033
+ const docsOnlyGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
2034
+ return {
2035
+ runtimeBearing: docsOnlyGraph.services.length > 0 ||
2036
+ docsOnlyGraph.startupWaves.length > 0 ||
2037
+ signalSummary.dependencyPairs.length > 0,
2038
+ services: docsOnlyGraph.services,
2039
+ startupWaves: docsOnlyGraph.startupWaves.map((wave) => ({
2040
+ wave: wave.wave,
2041
+ services: [...wave.services],
2042
+ })),
2043
+ dependencyPairs: signalSummary.dependencyPairs,
2044
+ signalSummary,
2045
+ };
2046
+ }
2047
+ buildCanonicalNameInventory(docs) {
2048
+ const structureTargets = this.extractStructureTargets(docs);
2049
+ const paths = uniqueStrings([...structureTargets.directories, ...structureTargets.files]
2050
+ .map((token) => this.normalizeStructurePathToken(token))
2051
+ .filter((value) => Boolean(value))).sort((a, b) => a.length - b.length || a.localeCompare(b));
2052
+ const graph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
2053
+ const serviceAliases = new Map();
2054
+ for (const [service, aliases] of graph.aliases.entries()) {
2055
+ serviceAliases.set(service, new Set(aliases));
2056
+ }
2057
+ for (const service of graph.services) {
2058
+ const existing = serviceAliases.get(service) ?? new Set();
2059
+ existing.add(service);
2060
+ serviceAliases.set(service, existing);
2061
+ }
2062
+ return {
2063
+ paths,
2064
+ pathSet: new Set(paths),
2065
+ services: [...graph.services],
2066
+ serviceAliases,
2067
+ };
2068
+ }
2069
+ countCommonPrefixSegments(left, right) {
2070
+ let count = 0;
2071
+ while (count < left.length && count < right.length && left[count] === right[count]) {
2072
+ count += 1;
2073
+ }
2074
+ return count;
2075
+ }
2076
+ countCommonSuffixSegments(left, right, prefixFloor = 0) {
2077
+ let count = 0;
2078
+ while (count < left.length - prefixFloor &&
2079
+ count < right.length - prefixFloor &&
2080
+ left[left.length - 1 - count] === right[right.length - 1 - count]) {
2081
+ count += 1;
2082
+ }
2083
+ return count;
2084
+ }
2085
+ tokenizeCanonicalName(value) {
2086
+ return this.normalizeServiceLookupKey(value)
2087
+ .split(" ")
2088
+ .map((token) => token.trim())
2089
+ .filter(Boolean);
2090
+ }
2091
+ namesSemanticallyCollide(candidate, canonical) {
2092
+ const candidateTokens = this.tokenizeCanonicalName(candidate);
2093
+ const canonicalTokens = this.tokenizeCanonicalName(canonical);
2094
+ if (candidateTokens.length === 0 || canonicalTokens.length === 0)
2095
+ return false;
2096
+ const canonicalSet = new Set(canonicalTokens);
2097
+ const shared = candidateTokens.filter((token) => canonicalSet.has(token));
2098
+ if (shared.length === 0)
2099
+ return false;
2100
+ if (shared.length === Math.min(candidateTokens.length, canonicalTokens.length))
2101
+ return true;
2102
+ const candidateNormalized = candidateTokens.join(" ");
2103
+ const canonicalNormalized = canonicalTokens.join(" ");
2104
+ return (candidateNormalized.includes(canonicalNormalized) || canonicalNormalized.includes(candidateNormalized));
2105
+ }
2106
+ findCanonicalPathConflict(candidatePath, inventory) {
2107
+ if (!candidatePath || inventory.pathSet.has(candidatePath))
2108
+ return undefined;
2109
+ const candidateParts = candidatePath.split("/").filter(Boolean);
2110
+ let bestMatch;
2111
+ for (const canonicalPath of inventory.paths) {
2112
+ if (candidatePath === canonicalPath)
2113
+ continue;
2114
+ const canonicalParts = canonicalPath.split("/").filter(Boolean);
2115
+ const sharedPrefix = this.countCommonPrefixSegments(candidateParts, canonicalParts);
2116
+ if (sharedPrefix === 0)
2117
+ continue;
2118
+ const sharedSuffix = this.countCommonSuffixSegments(candidateParts, canonicalParts, sharedPrefix);
2119
+ if (sharedSuffix === 0)
2120
+ continue;
2121
+ const candidateCore = candidateParts.slice(sharedPrefix, candidateParts.length - sharedSuffix);
2122
+ const canonicalCore = canonicalParts.slice(sharedPrefix, canonicalParts.length - sharedSuffix);
2123
+ if (candidateCore.length !== 1 || canonicalCore.length !== 1)
2124
+ continue;
2125
+ const candidateSegment = candidateCore[0] ?? "";
2126
+ const canonicalSegment = canonicalCore[0] ?? "";
2127
+ if (!this.namesSemanticallyCollide(candidateSegment, canonicalSegment))
2128
+ continue;
2129
+ const candidateService = this.deriveServiceFromPathToken(candidatePath);
2130
+ const canonicalService = this.deriveServiceFromPathToken(canonicalPath);
2131
+ if (candidateService &&
2132
+ canonicalService &&
2133
+ candidateService !== canonicalService &&
2134
+ !this.namesSemanticallyCollide(candidateService, canonicalService)) {
2135
+ continue;
2136
+ }
2137
+ const score = sharedPrefix + sharedSuffix;
2138
+ if (!bestMatch || score > bestMatch.score || (score === bestMatch.score && canonicalPath.length > bestMatch.canonicalPath.length)) {
2139
+ bestMatch = { canonicalPath, canonicalService, score };
2140
+ }
2141
+ }
2142
+ return bestMatch ? { canonicalPath: bestMatch.canonicalPath, canonicalService: bestMatch.canonicalService } : undefined;
2143
+ }
2144
+ collectCanonicalPlanSources(plan) {
2145
+ const sources = [];
2146
+ for (const epic of plan.epics) {
2147
+ sources.push({
2148
+ location: `epic:${epic.localId}`,
2149
+ text: [epic.title, epic.description, ...(epic.acceptanceCriteria ?? []), ...(epic.serviceIds ?? []), ...(epic.tags ?? [])]
2150
+ .filter(Boolean)
2151
+ .join("\n"),
2152
+ });
2153
+ }
2154
+ for (const story of plan.stories) {
2155
+ sources.push({
2156
+ location: `story:${story.epicLocalId}/${story.localId}`,
2157
+ text: [story.title, story.userStory, story.description, ...(story.acceptanceCriteria ?? [])]
2158
+ .filter(Boolean)
2159
+ .join("\n"),
2160
+ });
2161
+ }
2162
+ for (const task of plan.tasks) {
2163
+ sources.push({
2164
+ location: `task:${task.epicLocalId}/${task.storyLocalId}/${task.localId}`,
2165
+ text: [
2166
+ task.title,
2167
+ task.description,
2168
+ ...(task.relatedDocs ?? []),
2169
+ ...(task.unitTests ?? []),
2170
+ ...(task.componentTests ?? []),
2171
+ ...(task.integrationTests ?? []),
2172
+ ...(task.apiTests ?? []),
2173
+ ]
2174
+ .filter(Boolean)
2175
+ .join("\n"),
2176
+ });
2177
+ }
2178
+ return sources.filter((source) => source.text.trim().length > 0);
2179
+ }
2180
+ assertCanonicalNameConsistency(projectKey, docs, plan) {
2181
+ const inventory = this.buildCanonicalNameInventory(docs);
2182
+ if (inventory.paths.length === 0 && inventory.services.length === 0)
2183
+ return;
2184
+ const conflicts = new Map();
2185
+ for (const source of this.collectCanonicalPlanSources(plan)) {
2186
+ const candidatePaths = uniqueStrings(filterImplementationStructuredPaths(extractStructuredPaths(source.text, 256))
2187
+ .map((token) => this.normalizeStructurePathToken(token))
2188
+ .filter((value) => Boolean(value)));
2189
+ for (const candidatePath of candidatePaths) {
2190
+ if (inventory.pathSet.has(candidatePath))
2191
+ continue;
2192
+ const conflict = this.findCanonicalPathConflict(candidatePath, inventory);
2193
+ if (!conflict)
2194
+ continue;
2195
+ const key = `${source.location}|${candidatePath}|${conflict.canonicalPath}`;
2196
+ conflicts.set(key, {
2197
+ location: source.location,
2198
+ candidate: candidatePath,
2199
+ canonical: conflict.canonicalPath,
2200
+ });
2201
+ }
2202
+ }
2203
+ if (conflicts.size === 0)
2204
+ return;
2205
+ const summary = Array.from(conflicts.values())
2206
+ .slice(0, 8)
2207
+ .map((conflict) => `${conflict.location}: ${conflict.candidate} -> ${conflict.canonical}`)
2208
+ .join("; ");
2209
+ throw new Error(`create-tasks failed canonical name validation for project "${projectKey}". Undocumented alternate implementation paths conflict with source-backed canonical names: ${summary}`);
2210
+ }
2211
+ formatTopologySignalSummary(signalSummary) {
2212
+ return uniqueStrings([
2213
+ ...signalSummary.structureServices.map((service) => `structure:${service}`),
2214
+ ...signalSummary.topologyHeadings.map((heading) => `heading:${heading}`),
2215
+ ...signalSummary.dependencyPairs.map((pair) => `dependency:${pair}`),
2216
+ ...signalSummary.waveMentions.map((wave) => `wave:${wave}`),
2217
+ ])
2218
+ .slice(0, 10)
2219
+ .join("; ");
2220
+ }
2221
+ validateTopologyExtraction(projectKey, expectation, graph) {
2222
+ const topologySignals = expectation.signalSummary;
2223
+ if (!expectation.runtimeBearing)
2224
+ return topologySignals;
2225
+ if (graph.services.length === 0) {
2226
+ throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". SDS includes runtime topology signals but no services were resolved. Signals: ${this.formatTopologySignalSummary(topologySignals) || "unavailable"}`);
2227
+ }
2228
+ const missingServices = expectation.services.filter((service) => !graph.services.includes(service));
2229
+ if (missingServices.length > 0) {
2230
+ throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". Final planning artifacts lost source-backed services: ${missingServices.slice(0, 8).join(", ")}.`);
2231
+ }
2232
+ if (expectation.startupWaves.length > 0 && graph.startupWaves.length === 0) {
2233
+ throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". SDS includes startup wave signals but no startup waves were resolved. Signals: ${topologySignals.waveMentions.slice(0, 6).join("; ")}`);
2234
+ }
2235
+ const graphServicesByWave = new Map(graph.startupWaves.map((wave) => [wave.wave, new Set(wave.services)]));
2236
+ const missingWaves = expectation.startupWaves
2237
+ .map((wave) => wave.wave)
2238
+ .filter((wave) => !graphServicesByWave.has(wave));
2239
+ if (missingWaves.length > 0) {
2240
+ throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". Final planning artifacts lost source-backed startup waves: ${missingWaves.slice(0, 8).join(", ")}.`);
2241
+ }
2242
+ const missingWaveServices = expectation.startupWaves.flatMap((wave) => {
2243
+ const actualServices = graphServicesByWave.get(wave.wave);
2244
+ return wave.services
2245
+ .filter((service) => !(actualServices?.has(service) ?? false))
2246
+ .map((service) => `wave ${wave.wave}:${service}`);
2247
+ });
2248
+ if (missingWaveServices.length > 0) {
2249
+ throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". Final planning artifacts lost source-backed startup wave services: ${missingWaveServices.slice(0, 8).join(", ")}.`);
2250
+ }
2251
+ return topologySignals;
2252
+ }
2253
+ derivePlanningArtifacts(projectKey, docs, plan, expectation = this.buildSourceTopologyExpectation(docs)) {
2254
+ const discoveryGraph = this.buildServiceDependencyGraph(plan, docs);
2255
+ const topologySignals = this.validateTopologyExtraction(projectKey, expectation, discoveryGraph);
2256
+ const serviceCatalog = this.buildServiceCatalogArtifact(projectKey, docs, discoveryGraph);
2257
+ const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
2258
+ const projectBuildPlan = this.buildProjectPlanArtifact(projectKey, docs, discoveryGraph, projectBuildMethod);
2259
+ return {
2260
+ discoveryGraph,
2261
+ topologySignals,
2262
+ serviceCatalog,
2263
+ projectBuildMethod,
2264
+ projectBuildPlan,
2265
+ };
2266
+ }
1573
2267
  normalizeServiceId(value) {
1574
2268
  const normalizedName = this.normalizeServiceName(value);
1575
2269
  if (!normalizedName)
@@ -1667,6 +2361,7 @@ export class CreateTasksService {
1667
2361
  const sourceDocs = docs
1668
2362
  .map((doc) => doc.path ?? (doc.id ? `docdex:${doc.id}` : doc.title ?? "doc"))
1669
2363
  .filter((value) => Boolean(value))
2364
+ .filter((value, index, items) => items.indexOf(value) === index)
1670
2365
  .slice(0, 24);
1671
2366
  return {
1672
2367
  projectKey,
@@ -1889,8 +2584,16 @@ export class CreateTasksService {
1889
2584
  buildProjectConstructionMethod(docs, graph) {
1890
2585
  const toLabel = (value) => value.replace(/\s+/g, "-");
1891
2586
  const structureTargets = this.extractStructureTargets(docs);
1892
- const topDirectories = structureTargets.directories.slice(0, 10);
1893
- const topFiles = structureTargets.files.slice(0, 10);
2587
+ const sourceDocPaths = new Set(docs
2588
+ .map((doc) => (doc.path ? path.relative(this.workspace.workspaceRoot, doc.path).replace(/\\/g, "/") : undefined))
2589
+ .filter((value) => Boolean(value)));
2590
+ const sourceDocDirectories = new Set(Array.from(sourceDocPaths)
2591
+ .map((docPath) => path.posix.dirname(docPath))
2592
+ .filter((dir) => dir && dir !== "."));
2593
+ const buildDirectories = structureTargets.directories.filter((dir) => !sourceDocDirectories.has(dir));
2594
+ const buildFiles = structureTargets.files.filter((file) => !sourceDocPaths.has(file));
2595
+ const topDirectories = (buildDirectories.length > 0 ? buildDirectories : structureTargets.directories).slice(0, 10);
2596
+ const topFiles = (buildFiles.length > 0 ? buildFiles : structureTargets.files).slice(0, 10);
1894
2597
  const startupWaveLines = graph.startupWaves
1895
2598
  .slice(0, 8)
1896
2599
  .map((wave) => `- Wave ${wave.wave}: ${wave.services.map(toLabel).join(", ")}`);
@@ -1915,7 +2618,9 @@ export class CreateTasksService {
1915
2618
  ...(graph.foundationalDependencies.length > 0
1916
2619
  ? graph.foundationalDependencies.map((dependency) => ` - foundation: ${dependency}`)
1917
2620
  : [" - foundation: infer runtime prerequisites from SDS deployment sections"]),
1918
- ...(startupWaveLines.length > 0 ? startupWaveLines : [" - startup waves: infer from dependency contracts"]),
2621
+ ...(startupWaveLines.length > 0
2622
+ ? startupWaveLines
2623
+ : [" - startup waves: infer from documented dependency constraints"]),
1919
2624
  "3) Implement services by dependency direction and startup wave.",
1920
2625
  ` - service order: ${serviceOrderLine}`,
1921
2626
  ...(dependencyPairs.length > 0
@@ -1929,6 +2634,7 @@ export class CreateTasksService {
1929
2634
  const sourceDocs = docs
1930
2635
  .map((doc) => doc.path ?? (doc.id ? `docdex:${doc.id}` : doc.title ?? "doc"))
1931
2636
  .filter((value) => Boolean(value))
2637
+ .filter((value, index, items) => items.indexOf(value) === index)
1932
2638
  .slice(0, 24);
1933
2639
  return {
1934
2640
  projectKey,
@@ -1941,27 +2647,591 @@ export class CreateTasksService {
1941
2647
  buildMethod,
1942
2648
  };
1943
2649
  }
1944
- orderStoryTasksByDependencies(storyTasks, serviceRank, taskServiceByScope) {
1945
- const byLocalId = new Map(storyTasks.map((task) => [task.localId, task]));
1946
- const indegree = new Map();
1947
- const outgoing = new Map();
1948
- for (const task of storyTasks) {
1949
- indegree.set(task.localId, 0);
1950
- }
1951
- for (const task of storyTasks) {
1952
- for (const dep of task.dependsOnKeys ?? []) {
1953
- if (!byLocalId.has(dep) || dep === task.localId)
1954
- continue;
1955
- indegree.set(task.localId, (indegree.get(task.localId) ?? 0) + 1);
1956
- const edges = outgoing.get(dep) ?? new Set();
1957
- edges.add(task.localId);
1958
- outgoing.set(dep, edges);
2650
+ scoreServiceUnitForText(unit, text, graph) {
2651
+ const normalizedText = this.normalizeServiceLookupKey(text);
2652
+ if (!normalizedText)
2653
+ return 0;
2654
+ const tokens = normalizedText
2655
+ .split(" ")
2656
+ .map((token) => token.trim())
2657
+ .filter((token) => token.length >= 3);
2658
+ if (tokens.length === 0)
2659
+ return 0;
2660
+ const unitCorpus = this.normalizeServiceLookupKey([
2661
+ unit.serviceName,
2662
+ ...unit.aliases,
2663
+ ...unit.directories,
2664
+ ...unit.files,
2665
+ ...unit.headings,
2666
+ ...unit.dependsOnServiceIds,
2667
+ ].join("\n"));
2668
+ const overlap = tokens.filter((token) => unitCorpus.includes(token)).length;
2669
+ let score = overlap * 10;
2670
+ const direct = this.resolveServiceMentionFromPhrase(text, graph.aliases);
2671
+ if (direct && unit.serviceName === direct)
2672
+ score += 100;
2673
+ if (normalizedText.includes(this.normalizeServiceLookupKey(unit.serviceName)))
2674
+ score += 25;
2675
+ if (unit.isFoundational)
2676
+ score += 4;
2677
+ return score;
2678
+ }
2679
+ buildSdsServiceUnits(docs, catalog, graph) {
2680
+ const units = new Map();
2681
+ const serviceById = new Map(catalog.services.map((service) => [service.id, service]));
2682
+ const serviceByName = new Map(catalog.services.map((service) => [service.name, service]));
2683
+ const orderedServiceIds = catalog.services.map((service) => service.id);
2684
+ const primaryFoundationalServiceId = catalog.services.find((service) => service.isFoundational)?.id ?? catalog.services[0]?.id;
2685
+ const opsServiceId = catalog.services.find((service) => /\b(ops|operation|deploy|infra|runtime|platform)\b/.test(this.normalizeServiceLookupKey([service.name, ...service.aliases].join(" "))))?.id ?? primaryFoundationalServiceId;
2686
+ const coverageSignals = collectSdsCoverageSignalsFromDocs(docs.map((doc) => ({ content: doc.content })), { headingLimit: 200, folderLimit: 240 });
2687
+ const structureTargets = this.extractStructureTargets(docs);
2688
+ const runtimeComponents = this.extractRuntimeComponentNames(docs);
2689
+ const ensureUnit = (serviceId) => {
2690
+ if (!serviceId)
2691
+ return undefined;
2692
+ const service = serviceById.get(serviceId);
2693
+ if (!service)
2694
+ return undefined;
2695
+ const existing = units.get(serviceId);
2696
+ if (existing)
2697
+ return existing;
2698
+ const unit = {
2699
+ serviceId: service.id,
2700
+ serviceName: service.name,
2701
+ aliases: uniqueStrings([service.name, ...service.aliases]),
2702
+ startupWave: service.startupWave,
2703
+ dependsOnServiceIds: [...service.dependsOnServiceIds],
2704
+ directories: [],
2705
+ files: [],
2706
+ headings: [],
2707
+ isFoundational: service.isFoundational,
2708
+ };
2709
+ units.set(serviceId, unit);
2710
+ return unit;
2711
+ };
2712
+ for (const serviceId of orderedServiceIds)
2713
+ ensureUnit(serviceId);
2714
+ const selectUnitForText = (text, options) => {
2715
+ const directName = this.resolveServiceMentionFromPhrase(text, graph.aliases);
2716
+ if (directName) {
2717
+ const direct = serviceByName.get(directName);
2718
+ if (direct)
2719
+ return ensureUnit(direct.id);
2720
+ }
2721
+ const pathMatch = options?.pathLike ? this.deriveServiceFromPathToken(text) : undefined;
2722
+ if (pathMatch) {
2723
+ const fromPath = serviceByName.get(pathMatch);
2724
+ if (fromPath)
2725
+ return ensureUnit(fromPath.id);
2726
+ }
2727
+ let bestUnit;
2728
+ let bestScore = 0;
2729
+ for (const unit of units.values()) {
2730
+ const score = this.scoreServiceUnitForText(unit, text, graph);
2731
+ if (score > bestScore) {
2732
+ bestScore = score;
2733
+ bestUnit = unit;
2734
+ }
2735
+ }
2736
+ if (bestUnit && bestScore > 0)
2737
+ return bestUnit;
2738
+ const normalizedText = this.normalizeServiceLookupKey(text);
2739
+ if (/\b(deploy|deployment|startup|rollback|recovery|observability|quality|release|failover|runbook|environment|secret|compute|operations?)\b/.test(normalizedText)) {
2740
+ return ensureUnit(opsServiceId);
1959
2741
  }
2742
+ return ensureUnit(primaryFoundationalServiceId);
2743
+ };
2744
+ for (const directory of structureTargets.directories) {
2745
+ const unit = selectUnitForText(directory, { pathLike: true });
2746
+ if (!unit)
2747
+ continue;
2748
+ unit.directories.push(directory);
1960
2749
  }
1961
- const priorityComparator = (a, b) => {
1962
- const classA = classifyTask({ title: a.title ?? "", description: a.description, type: a.type });
1963
- const classB = classifyTask({ title: b.title ?? "", description: b.description, type: b.type });
1964
- if (classA.foundation !== classB.foundation)
2750
+ for (const file of structureTargets.files) {
2751
+ const unit = selectUnitForText(file, { pathLike: true });
2752
+ if (!unit)
2753
+ continue;
2754
+ unit.files.push(file);
2755
+ const parent = path.dirname(file).replace(/\\/g, "/");
2756
+ if (parent && parent !== ".")
2757
+ unit.directories.push(parent);
2758
+ }
2759
+ for (const heading of coverageSignals.sectionHeadings) {
2760
+ const unit = selectUnitForText(heading);
2761
+ if (!unit)
2762
+ continue;
2763
+ unit.headings.push(normalizeHeadingCandidate(heading));
2764
+ }
2765
+ for (const component of runtimeComponents) {
2766
+ const unit = selectUnitForText(component);
2767
+ if (!unit)
2768
+ continue;
2769
+ unit.headings.push(normalizeHeadingCandidate(component));
2770
+ }
2771
+ return catalog.services
2772
+ .map((service) => units.get(service.id))
2773
+ .filter((unit) => Boolean(unit))
2774
+ .map((unit) => ({
2775
+ ...unit,
2776
+ aliases: uniqueStrings(unit.aliases).sort((a, b) => a.localeCompare(b)),
2777
+ directories: uniqueStrings(unit.directories).sort((a, b) => a.length - b.length || a.localeCompare(b)),
2778
+ files: uniqueStrings(unit.files).sort((a, b) => a.length - b.length || a.localeCompare(b)),
2779
+ headings: uniqueStrings(unit.headings),
2780
+ }));
2781
+ }
2782
+ buildSdsDrivenPlan(projectKey, docs, catalog, graph) {
2783
+ const units = this.buildSdsServiceUnits(docs, catalog, graph);
2784
+ const verificationSuites = this.extractVerificationSuites(docs);
2785
+ const acceptanceScenarios = this.extractAcceptanceScenarios(docs);
2786
+ const localIds = new Set();
2787
+ const epics = [];
2788
+ const stories = [];
2789
+ const tasks = [];
2790
+ const rootPreview = (items, fallback) => items.length > 0 ? items.slice(0, 8).map((item) => `- ${item}`).join("\n") : `- ${fallback}`;
2791
+ const chunk = (items, size) => {
2792
+ const chunks = [];
2793
+ for (let index = 0; index < items.length; index += size) {
2794
+ chunks.push(items.slice(index, index + size));
2795
+ }
2796
+ return chunks;
2797
+ };
2798
+ const toDisplayName = (value) => value
2799
+ .split(/\s+/)
2800
+ .map((token) => (token ? token[0].toUpperCase() + token.slice(1) : token))
2801
+ .join(" ");
2802
+ const defaultArea = normalizeArea(projectKey) ?? "core";
2803
+ for (const unit of units) {
2804
+ const epicLocalId = nextUniqueLocalId(`svc-${unit.serviceId}`, localIds);
2805
+ const epicTitle = `Build ${toDisplayName(unit.serviceName)}`;
2806
+ epics.push({
2807
+ localId: epicLocalId,
2808
+ area: defaultArea,
2809
+ title: epicTitle,
2810
+ description: [
2811
+ `Implement the ${unit.serviceName} build slice from the SDS.`,
2812
+ unit.startupWave !== undefined ? `Startup wave: ${unit.startupWave}.` : undefined,
2813
+ unit.dependsOnServiceIds.length > 0
2814
+ ? `Dependencies: ${unit.dependsOnServiceIds.join(", ")}.`
2815
+ : "Dependencies: foundational or none.",
2816
+ unit.headings.length > 0
2817
+ ? `Covered SDS sections: ${unit.headings.slice(0, 5).join("; ")}.`
2818
+ : undefined,
2819
+ ]
2820
+ .filter(Boolean)
2821
+ .join("\n"),
2822
+ acceptanceCriteria: [
2823
+ `${unit.serviceName} structure and implementation surfaces are represented in the backlog.`,
2824
+ `${unit.serviceName} sequencing stays aligned to SDS dependency and startup ordering.`,
2825
+ `${unit.serviceName} validation work exists for the implemented scope.`,
2826
+ ],
2827
+ relatedDocs: docs
2828
+ .map((doc) => (doc.id ? `docdex:${doc.id}` : undefined))
2829
+ .filter((value) => Boolean(value))
2830
+ .slice(0, 12),
2831
+ priorityHint: epics.length + 1,
2832
+ serviceIds: [unit.serviceId],
2833
+ tags: unit.isFoundational ? ["foundational"] : [],
2834
+ stories: [],
2835
+ });
2836
+ if (unit.directories.length > 0 || unit.files.length > 0) {
2837
+ const storyLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-structure`, localIds);
2838
+ stories.push({
2839
+ localId: storyLocalId,
2840
+ epicLocalId,
2841
+ title: `Establish ${toDisplayName(unit.serviceName)} structure`,
2842
+ userStory: `As an engineer, I need the ${unit.serviceName} scaffold in place before implementation continues.`,
2843
+ description: [
2844
+ `Create the SDS-defined folder and file scaffold for ${unit.serviceName}.`,
2845
+ "Use the documented target tree as the implementation baseline.",
2846
+ ].join("\n"),
2847
+ acceptanceCriteria: [
2848
+ `Required ${unit.serviceName} directories exist.`,
2849
+ `Required ${unit.serviceName} file entrypoints are present or explicitly queued in dependent tasks.`,
2850
+ `Follow-up tasks can reference real ${unit.serviceName} implementation paths.`,
2851
+ ],
2852
+ relatedDocs: [],
2853
+ priorityHint: 1,
2854
+ tasks: [],
2855
+ });
2856
+ const directoryChunks = chunk(unit.directories, 6);
2857
+ const fileChunks = chunk(unit.files, 5);
2858
+ if (directoryChunks.length > 0) {
2859
+ tasks.push({
2860
+ localId: nextUniqueLocalId(`svc-${unit.serviceId}-structure-task`, localIds),
2861
+ storyLocalId,
2862
+ epicLocalId,
2863
+ title: `Create ${unit.serviceName} directory scaffold`,
2864
+ type: "chore",
2865
+ description: [
2866
+ `Create the initial ${unit.serviceName} repository structure required by the SDS.`,
2867
+ "Target directories:",
2868
+ rootPreview(directoryChunks[0] ?? [], "Infer the concrete runtime directories from the SDS build tree."),
2869
+ ].join("\n"),
2870
+ estimatedStoryPoints: 2,
2871
+ priorityHint: 1,
2872
+ dependsOnKeys: [],
2873
+ relatedDocs: [],
2874
+ unitTests: [],
2875
+ componentTests: [],
2876
+ integrationTests: [],
2877
+ apiTests: [],
2878
+ });
2879
+ }
2880
+ if (fileChunks.length > 0) {
2881
+ const structureFileTargets = this.selectBuildTargets(unit, [`${unit.serviceName} structure entrypoints`], "structure", 5);
2882
+ tasks.push({
2883
+ localId: nextUniqueLocalId(`svc-${unit.serviceId}-structure-task`, localIds),
2884
+ storyLocalId,
2885
+ epicLocalId,
2886
+ title: `Create ${unit.serviceName} runtime entrypoints`,
2887
+ type: "feature",
2888
+ description: [
2889
+ `Create or stub the first concrete ${unit.serviceName} implementation files from the SDS folder tree.`,
2890
+ "Target files:",
2891
+ rootPreview(structureFileTargets.length > 0 ? structureFileTargets : fileChunks[0] ?? [], "Create the primary runtime entrypoints and implementation surfaces."),
2892
+ ].join("\n"),
2893
+ estimatedStoryPoints: 3,
2894
+ priorityHint: 2,
2895
+ dependsOnKeys: tasks.filter((task) => task.storyLocalId === storyLocalId).map((task) => task.localId),
2896
+ relatedDocs: [],
2897
+ unitTests: [],
2898
+ componentTests: [],
2899
+ integrationTests: [],
2900
+ apiTests: [],
2901
+ });
2902
+ }
2903
+ }
2904
+ if (unit.headings.length > 0 || unit.files.length > 0) {
2905
+ const storyLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-implementation`, localIds);
2906
+ stories.push({
2907
+ localId: storyLocalId,
2908
+ epicLocalId,
2909
+ title: `Implement ${toDisplayName(unit.serviceName)} capabilities`,
2910
+ userStory: `As a delivery team, we need the ${unit.serviceName} capability set implemented from the SDS.`,
2911
+ description: [
2912
+ `Implement the ${unit.serviceName} behavior defined across the SDS sections assigned to this build slice.`,
2913
+ unit.headings.length > 0 ? `Primary SDS sections: ${unit.headings.slice(0, 6).join("; ")}.` : undefined,
2914
+ ]
2915
+ .filter(Boolean)
2916
+ .join("\n"),
2917
+ acceptanceCriteria: [
2918
+ `${unit.serviceName} covers its assigned SDS capability sections.`,
2919
+ `${unit.serviceName} implementation targets are concrete and source-backed.`,
2920
+ `${unit.serviceName} leaves no generic placeholder work inside this story.`,
2921
+ ],
2922
+ relatedDocs: [],
2923
+ priorityHint: 2,
2924
+ tasks: [],
2925
+ });
2926
+ const headingChunks = chunk(unit.headings.length > 0 ? unit.headings : [`${unit.serviceName} runtime implementation`], 2);
2927
+ headingChunks.slice(0, 6).forEach((group, index) => {
2928
+ const targetSlice = this.selectBuildTargets(unit, group, "implementation", 3);
2929
+ tasks.push({
2930
+ localId: nextUniqueLocalId(`svc-${unit.serviceId}-implementation-task`, localIds),
2931
+ storyLocalId,
2932
+ epicLocalId,
2933
+ title: `Implement ${group[0] ?? unit.serviceName}`,
2934
+ type: "feature",
2935
+ description: [
2936
+ `Implement the ${unit.serviceName} scope required by these SDS sections: ${group.join("; ")}.`,
2937
+ "Primary implementation targets:",
2938
+ rootPreview(targetSlice, `Extend the SDS-defined ${unit.serviceName} runtime surfaces captured in this build slice.`),
2939
+ unit.dependsOnServiceIds.length > 0
2940
+ ? `Keep dependency direction aligned to: ${unit.dependsOnServiceIds.join(", ")}.`
2941
+ : "Keep this slice buildable without introducing undocumented dependencies.",
2942
+ ].join("\n"),
2943
+ estimatedStoryPoints: Math.min(8, 3 + Math.max(0, Math.max(1, targetSlice.length) - 1)),
2944
+ priorityHint: index + 1,
2945
+ dependsOnKeys: [],
2946
+ relatedDocs: [],
2947
+ unitTests: targetSlice.length > 0 ? [`Cover ${unit.serviceName} implementation behavior for ${targetSlice[0]}.`] : [],
2948
+ componentTests: [],
2949
+ integrationTests: [],
2950
+ apiTests: [],
2951
+ });
2952
+ });
2953
+ }
2954
+ if (unit.dependsOnServiceIds.length > 0) {
2955
+ const storyLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-integration`, localIds);
2956
+ stories.push({
2957
+ localId: storyLocalId,
2958
+ epicLocalId,
2959
+ title: `Integrate ${toDisplayName(unit.serviceName)} dependencies`,
2960
+ userStory: `As an engineer, I need ${unit.serviceName} wired to its SDS-defined dependencies and interfaces.`,
2961
+ description: [
2962
+ `Implement dependency and interface wiring for ${unit.serviceName}.`,
2963
+ `SDS dependency chain: ${unit.dependsOnServiceIds.join(", ")}.`,
2964
+ ].join("\n"),
2965
+ acceptanceCriteria: [
2966
+ `${unit.serviceName} dependency integration is explicit and ordered.`,
2967
+ `${unit.serviceName} uses only documented runtime dependencies.`,
2968
+ ],
2969
+ relatedDocs: [],
2970
+ priorityHint: 3,
2971
+ tasks: [],
2972
+ });
2973
+ unit.dependsOnServiceIds.slice(0, 4).forEach((dependencyId, index) => {
2974
+ tasks.push({
2975
+ localId: nextUniqueLocalId(`svc-${unit.serviceId}-integration-task`, localIds),
2976
+ storyLocalId,
2977
+ epicLocalId,
2978
+ title: `Wire ${unit.serviceName} to ${dependencyId}`,
2979
+ type: "feature",
2980
+ description: [
2981
+ `Implement the SDS-defined dependency direction from ${unit.serviceName} to ${dependencyId}.`,
2982
+ "Update runtime interfaces, configuration reads, or orchestration flow as required.",
2983
+ ].join("\n"),
2984
+ estimatedStoryPoints: 2,
2985
+ priorityHint: index + 1,
2986
+ dependsOnKeys: [],
2987
+ relatedDocs: [],
2988
+ unitTests: [],
2989
+ componentTests: [],
2990
+ integrationTests: [`Validate ${unit.serviceName} integration with ${dependencyId}.`],
2991
+ apiTests: [],
2992
+ });
2993
+ });
2994
+ }
2995
+ const verificationStoryLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-verification`, localIds);
2996
+ stories.push({
2997
+ localId: verificationStoryLocalId,
2998
+ epicLocalId,
2999
+ title: `Verify ${toDisplayName(unit.serviceName)} readiness`,
3000
+ userStory: `As a reviewer, I need concrete verification evidence for ${unit.serviceName}.`,
3001
+ description: [
3002
+ `Add the validation work needed to prove ${unit.serviceName} against the SDS.`,
3003
+ unit.headings.length > 0 ? `Verification should cover: ${unit.headings.slice(0, 4).join("; ")}.` : undefined,
3004
+ ]
3005
+ .filter(Boolean)
3006
+ .join("\n"),
3007
+ acceptanceCriteria: [
3008
+ `${unit.serviceName} has deterministic validation coverage.`,
3009
+ `${unit.serviceName} evidence maps back to SDS responsibilities.`,
3010
+ ],
3011
+ relatedDocs: [],
3012
+ priorityHint: 4,
3013
+ tasks: [],
3014
+ });
3015
+ tasks.push({
3016
+ localId: nextUniqueLocalId(`svc-${unit.serviceId}-verification-task`, localIds),
3017
+ storyLocalId: verificationStoryLocalId,
3018
+ epicLocalId,
3019
+ title: `Add ${unit.serviceName} focused test coverage`,
3020
+ type: "chore",
3021
+ description: [
3022
+ `Add or update the smallest deterministic validation surface that proves ${unit.serviceName}.`,
3023
+ "Cover the implemented runtime paths, dependency behavior, and SDS acceptance expectations for this build slice.",
3024
+ "Primary verification targets:",
3025
+ rootPreview(this.selectBuildTargets(unit, [...unit.headings, `${unit.serviceName} verification`], "verification", 3), `Capture service-local validation against ${unit.serviceName} runtime surfaces.`),
3026
+ ].join("\n"),
3027
+ estimatedStoryPoints: 2,
3028
+ priorityHint: 1,
3029
+ dependsOnKeys: [],
3030
+ relatedDocs: [],
3031
+ unitTests: [`Validate ${unit.serviceName} internal behavior.`],
3032
+ componentTests: unit.files.some((file) => /\b(ui|web|component|page|screen)\b/i.test(file))
3033
+ ? [`Validate ${unit.serviceName} component-level behavior.`]
3034
+ : [],
3035
+ integrationTests: [`Validate ${unit.serviceName} end-to-end dependency behavior.`],
3036
+ apiTests: [],
3037
+ });
3038
+ }
3039
+ if (verificationSuites.length > 0 || acceptanceScenarios.length > 0) {
3040
+ const releaseEpicLocalId = nextUniqueLocalId("release-verification", localIds);
3041
+ epics.push({
3042
+ localId: releaseEpicLocalId,
3043
+ area: defaultArea,
3044
+ title: "Verify Release Readiness",
3045
+ description: [
3046
+ "Execute the named verification suites and acceptance scenarios documented in the SDS release gate.",
3047
+ verificationSuites.length > 0
3048
+ ? `Named suites: ${verificationSuites.map((suite) => suite.name).slice(0, 8).join("; ")}.`
3049
+ : undefined,
3050
+ acceptanceScenarios.length > 0
3051
+ ? `Acceptance scenarios: ${acceptanceScenarios.length} documented launch scenarios must be green.`
3052
+ : undefined,
3053
+ ]
3054
+ .filter(Boolean)
3055
+ .join("\n"),
3056
+ acceptanceCriteria: [
3057
+ "Every named verification suite from the SDS is represented as executable backlog work.",
3058
+ "Every required acceptance scenario from the SDS is represented as executable backlog work.",
3059
+ "Release verification evidence maps back to the SDS release gate.",
3060
+ ],
3061
+ relatedDocs: docs
3062
+ .map((doc) => (doc.id ? `docdex:${doc.id}` : undefined))
3063
+ .filter((value) => Boolean(value))
3064
+ .slice(0, 12),
3065
+ priorityHint: epics.length + 1,
3066
+ serviceIds: uniqueStrings(units.map((unit) => unit.serviceId)).slice(0, 12),
3067
+ tags: [CROSS_SERVICE_TAG],
3068
+ stories: [],
3069
+ });
3070
+ if (verificationSuites.length > 0) {
3071
+ const verificationStoryLocalId = nextUniqueLocalId("release-verification-suites", localIds);
3072
+ stories.push({
3073
+ localId: verificationStoryLocalId,
3074
+ epicLocalId: releaseEpicLocalId,
3075
+ title: "Execute named verification suites",
3076
+ userStory: "As a release reviewer, I need every named SDS verification suite to exist as executable backlog work.",
3077
+ description: "Create the exact named verification suites from the SDS verification matrix and map them to deterministic evidence.",
3078
+ acceptanceCriteria: [
3079
+ "All named SDS verification suites have backlog tasks with explicit scope.",
3080
+ "Suite tasks reference the exact SDS suite names instead of generic test placeholders.",
3081
+ ],
3082
+ relatedDocs: [],
3083
+ priorityHint: 1,
3084
+ tasks: [],
3085
+ });
3086
+ verificationSuites.forEach((suite, index) => {
3087
+ const draft = this.buildVerificationSuiteTaskDraft(suite, "the release slice");
3088
+ tasks.push({
3089
+ localId: nextUniqueLocalId("release-verification-suite-task", localIds),
3090
+ storyLocalId: verificationStoryLocalId,
3091
+ epicLocalId: releaseEpicLocalId,
3092
+ title: `Execute suite: ${suite.name}`,
3093
+ type: "chore",
3094
+ description: draft.description,
3095
+ estimatedStoryPoints: /\b(end-to-end|acceptance|drill|integration)\b/i.test(suite.name) ? 3 : 2,
3096
+ priorityHint: index + 1,
3097
+ dependsOnKeys: [],
3098
+ relatedDocs: [],
3099
+ unitTests: draft.unitTests,
3100
+ componentTests: draft.componentTests,
3101
+ integrationTests: draft.integrationTests,
3102
+ apiTests: draft.apiTests,
3103
+ });
3104
+ });
3105
+ }
3106
+ if (acceptanceScenarios.length > 0) {
3107
+ const scenarioStoryLocalId = nextUniqueLocalId("release-acceptance-scenarios", localIds);
3108
+ stories.push({
3109
+ localId: scenarioStoryLocalId,
3110
+ epicLocalId: releaseEpicLocalId,
3111
+ title: "Run required acceptance scenarios",
3112
+ userStory: "As a launch approver, I need every required acceptance scenario executed before release.",
3113
+ description: "Create one executable backlog task per SDS acceptance scenario so launch approval is tied to concrete scenario evidence.",
3114
+ acceptanceCriteria: [
3115
+ "All numbered SDS acceptance scenarios exist as backlog tasks.",
3116
+ "Scenario tasks preserve the SDS launch wording closely enough for release review.",
3117
+ ],
3118
+ relatedDocs: [],
3119
+ priorityHint: 2,
3120
+ tasks: [],
3121
+ });
3122
+ acceptanceScenarios.forEach((scenario) => {
3123
+ tasks.push({
3124
+ localId: nextUniqueLocalId("release-acceptance-scenario-task", localIds),
3125
+ storyLocalId: scenarioStoryLocalId,
3126
+ epicLocalId: releaseEpicLocalId,
3127
+ title: `Validate acceptance scenario ${scenario.index}: ${scenario.title}`,
3128
+ type: "chore",
3129
+ description: [
3130
+ `Execute SDS acceptance scenario ${scenario.index}.`,
3131
+ scenario.details,
3132
+ "Capture deterministic pass/fail evidence and link it to the release gate.",
3133
+ ].join("\n"),
3134
+ estimatedStoryPoints: 2,
3135
+ priorityHint: scenario.index,
3136
+ dependsOnKeys: [],
3137
+ relatedDocs: [],
3138
+ unitTests: [],
3139
+ componentTests: [],
3140
+ integrationTests: [`Execute acceptance scenario ${scenario.index} end to end.`],
3141
+ apiTests: [],
3142
+ });
3143
+ });
3144
+ }
3145
+ }
3146
+ return { epics, stories, tasks };
3147
+ }
3148
+ hasStrongSdsPlanningEvidence(docs, catalog, expectation) {
3149
+ const sdsDocs = docs.filter((doc) => looksLikeSdsDoc(doc));
3150
+ if (sdsDocs.length === 0)
3151
+ return false;
3152
+ const coverageSignals = collectSdsCoverageSignalsFromDocs(docs.map((doc) => ({ content: doc.content })), { headingLimit: 200, folderLimit: 240 });
3153
+ const structureTargets = this.extractStructureTargets(docs);
3154
+ const structureSignalCount = structureTargets.directories.length + structureTargets.files.length;
3155
+ const headingSignalCount = coverageSignals.sectionHeadings.length;
3156
+ const topologySignalCount = expectation.services.length +
3157
+ expectation.startupWaves.length +
3158
+ expectation.dependencyPairs.length +
3159
+ expectation.signalSummary.topologyHeadings.length +
3160
+ expectation.signalSummary.waveMentions.length;
3161
+ return (structureSignalCount >= 4 ||
3162
+ headingSignalCount >= 8 ||
3163
+ catalog.services.length >= 2 ||
3164
+ topologySignalCount >= 4);
3165
+ }
3166
+ taskUsesOnlyWeakImplementationTargets(task, docs) {
3167
+ if (!/\bimplement\b/i.test(`${task.title} ${task.description ?? ""}`))
3168
+ return false;
3169
+ const inventory = this.buildCanonicalNameInventory(docs);
3170
+ if (!inventory.paths.some((candidate) => this.isStrongImplementationTarget(candidate)))
3171
+ return false;
3172
+ const candidateTargets = uniqueStrings(filterImplementationStructuredPaths(extractStructuredPaths(`${task.title}\n${task.description ?? ""}`, 64))
3173
+ .map((token) => this.normalizeStructurePathToken(token))
3174
+ .filter((value) => Boolean(value)));
3175
+ if (candidateTargets.length === 0)
3176
+ return false;
3177
+ return candidateTargets.every((target) => !this.isStrongImplementationTarget(target));
3178
+ }
3179
+ planLooksTooWeakForSds(plan, docs, catalog, expectation) {
3180
+ if (!this.hasStrongSdsPlanningEvidence(docs, catalog, expectation))
3181
+ return false;
3182
+ const genericTitles = plan.tasks.filter((task) => /initial planning|draft backlog|review inputs|baseline project scaffolding|integrate core dependencies|validate baseline behavior/i.test(`${task.title} ${task.description ?? ""}`)).length;
3183
+ const genericImplementationTasks = plan.tasks.filter((task) => GENERIC_IMPLEMENTATION_TASK_PATTERN.test(`${task.title} ${task.description ?? ""}`)).length;
3184
+ const weakImplementationTargetTasks = plan.tasks.filter((task) => this.taskUsesOnlyWeakImplementationTargets(task, docs)).length;
3185
+ const verificationSuites = this.extractVerificationSuites(docs);
3186
+ const acceptanceScenarios = this.extractAcceptanceScenarios(docs);
3187
+ const planCorpus = this.normalizeServiceLookupKey([
3188
+ ...plan.epics.map((epic) => `${epic.title}\n${epic.description ?? ""}`),
3189
+ ...plan.stories.map((story) => `${story.title}\n${story.description ?? ""}\n${story.userStory ?? ""}`),
3190
+ ...plan.tasks.map((task) => `${task.title}\n${task.description ?? ""}`),
3191
+ ].join("\n"));
3192
+ const coveredVerificationSuites = verificationSuites.filter((suite) => planCorpus.includes(this.normalizeServiceLookupKey(suite.name))).length;
3193
+ const coveredAcceptanceScenarios = acceptanceScenarios.filter((scenario) => planCorpus.includes(`scenario ${scenario.index}`) ||
3194
+ planCorpus.includes(this.normalizeServiceLookupKey(scenario.title))).length;
3195
+ const coveredServiceIds = new Set(plan.epics.flatMap((epic) => normalizeStringArray(epic.serviceIds)));
3196
+ return (plan.epics.length === 0 ||
3197
+ plan.stories.length === 0 ||
3198
+ plan.tasks.length === 0 ||
3199
+ genericTitles >= Math.min(2, plan.tasks.length) ||
3200
+ genericImplementationTasks > 0 ||
3201
+ weakImplementationTargetTasks > 0 ||
3202
+ (verificationSuites.length > 0 && coveredVerificationSuites === 0) ||
3203
+ (acceptanceScenarios.length > 0 && coveredAcceptanceScenarios < Math.min(3, acceptanceScenarios.length)) ||
3204
+ (genericTitles > 0 &&
3205
+ catalog.services.length > 1 &&
3206
+ coveredServiceIds.size > 0 &&
3207
+ coveredServiceIds.size < Math.min(catalog.services.length, 2)));
3208
+ }
3209
+ backlogMostlyAlignedEnough(audit) {
3210
+ if (!audit)
3211
+ return true;
3212
+ return audit.satisfied;
3213
+ }
3214
+ orderStoryTasksByDependencies(storyTasks, serviceRank, taskServiceByScope) {
3215
+ const byLocalId = new Map(storyTasks.map((task) => [task.localId, task]));
3216
+ const indegree = new Map();
3217
+ const outgoing = new Map();
3218
+ for (const task of storyTasks) {
3219
+ indegree.set(task.localId, 0);
3220
+ }
3221
+ for (const task of storyTasks) {
3222
+ for (const dep of task.dependsOnKeys ?? []) {
3223
+ if (!byLocalId.has(dep) || dep === task.localId)
3224
+ continue;
3225
+ indegree.set(task.localId, (indegree.get(task.localId) ?? 0) + 1);
3226
+ const edges = outgoing.get(dep) ?? new Set();
3227
+ edges.add(task.localId);
3228
+ outgoing.set(dep, edges);
3229
+ }
3230
+ }
3231
+ const priorityComparator = (a, b) => {
3232
+ const classA = classifyTask({ title: a.title ?? "", description: a.description, type: a.type });
3233
+ const classB = classifyTask({ title: b.title ?? "", description: b.description, type: b.type });
3234
+ if (classA.foundation !== classB.foundation)
1965
3235
  return classA.foundation ? -1 : 1;
1966
3236
  const rankA = serviceRank.get(taskServiceByScope.get(this.scopeTask(a)) ?? "") ?? Number.MAX_SAFE_INTEGER;
1967
3237
  const rankB = serviceRank.get(taskServiceByScope.get(this.scopeTask(b)) ?? "") ?? Number.MAX_SAFE_INTEGER;
@@ -2202,7 +3472,7 @@ export class CreateTasksService {
2202
3472
  .slice(0, 12);
2203
3473
  const bootstrapEpic = {
2204
3474
  localId: epicLocalId,
2205
- area: normalizeArea(projectKey) ?? "infra",
3475
+ area: normalizeArea(projectKey) ?? "core",
2206
3476
  title: "Codebase Foundation and Structure Setup",
2207
3477
  description: "Create the SDS-defined codebase scaffold first (folders/files/service boundaries) before feature implementation tasks.",
2208
3478
  acceptanceCriteria: [
@@ -2564,17 +3834,32 @@ export class CreateTasksService {
2564
3834
  for (const doc of docs) {
2565
3835
  if (!looksLikeSdsDoc(doc))
2566
3836
  continue;
3837
+ const scanLimit = Math.max(limit * 4, limit + 12);
3838
+ const contentHeadings = collectSdsImplementationSignals(doc.content ?? "", {
3839
+ headingLimit: scanLimit,
3840
+ folderLimit: 0,
3841
+ }).sectionHeadings;
2567
3842
  const segmentHeadings = (doc.segments ?? [])
2568
- .map((segment) => segment.heading?.trim())
3843
+ .map((segment) => normalizeHeadingCandidate(segment.heading?.trim() ?? ""))
2569
3844
  .filter((heading) => Boolean(heading));
2570
3845
  const segmentContentHeadings = (doc.segments ?? [])
2571
- .flatMap((segment) => extractMarkdownHeadings(segment.content ?? "", Math.max(6, Math.ceil(limit / 4))))
2572
- .slice(0, limit);
2573
- const contentHeadings = extractMarkdownHeadings(doc.content ?? "", limit);
2574
- for (const heading of [...segmentHeadings, ...segmentContentHeadings, ...contentHeadings]) {
2575
- const normalized = heading.replace(/[`*_]/g, "").trim();
3846
+ .flatMap((segment) => collectSdsImplementationSignals(segment.content ?? "", {
3847
+ headingLimit: Math.max(12, Math.ceil(scanLimit / 2)),
3848
+ folderLimit: 0,
3849
+ }).sectionHeadings)
3850
+ .slice(0, scanLimit);
3851
+ for (const heading of uniqueStrings([...contentHeadings, ...segmentHeadings, ...segmentContentHeadings])) {
3852
+ const normalized = normalizeHeadingCandidate(heading);
2576
3853
  if (!normalized)
2577
3854
  continue;
3855
+ if (!headingLooksImplementationRelevant(normalized))
3856
+ continue;
3857
+ if (/^software design specification$/i.test(normalized))
3858
+ continue;
3859
+ if (/^(?:\d+(?:\.\d+)*\.?\s*)?roles$/i.test(normalized))
3860
+ continue;
3861
+ if (sections.includes(normalized))
3862
+ continue;
2578
3863
  sections.push(normalized);
2579
3864
  if (sections.length >= limit)
2580
3865
  break;
@@ -2584,6 +3869,336 @@ export class CreateTasksService {
2584
3869
  }
2585
3870
  return uniqueStrings(sections).slice(0, limit);
2586
3871
  }
3872
+ collectSdsSections(docs) {
3873
+ const sections = [];
3874
+ for (const doc of docs) {
3875
+ if (!looksLikeSdsDoc(doc))
3876
+ continue;
3877
+ const content = stripManagedSdsPreflightBlock(doc.content ?? "") ?? "";
3878
+ const lines = content.split(/\r?\n/);
3879
+ let currentHeading;
3880
+ let currentBody = [];
3881
+ let inCodeFence = false;
3882
+ const flush = () => {
3883
+ if (!currentHeading)
3884
+ return;
3885
+ sections.push({ heading: currentHeading, body: [...currentBody] });
3886
+ };
3887
+ for (const rawLine of lines) {
3888
+ const line = rawLine ?? "";
3889
+ const trimmed = line.trim();
3890
+ if (/^```/.test(trimmed)) {
3891
+ inCodeFence = !inCodeFence;
3892
+ currentBody.push(line);
3893
+ continue;
3894
+ }
3895
+ if (!inCodeFence) {
3896
+ const headingMatch = trimmed.match(MARKDOWN_HEADING_PATTERN);
3897
+ if (headingMatch) {
3898
+ flush();
3899
+ currentHeading = normalizeHeadingCandidate(headingMatch[2] ?? "");
3900
+ currentBody = [];
3901
+ continue;
3902
+ }
3903
+ }
3904
+ if (currentHeading)
3905
+ currentBody.push(line);
3906
+ }
3907
+ flush();
3908
+ }
3909
+ return sections;
3910
+ }
3911
+ normalizeRuntimeComponentCandidate(rawValue) {
3912
+ let candidate = rawValue.trim();
3913
+ if (!candidate)
3914
+ return undefined;
3915
+ const backtickMatch = candidate.match(/`([^`]+)`/);
3916
+ if (backtickMatch?.[1]) {
3917
+ candidate = backtickMatch[1];
3918
+ }
3919
+ const colonHead = candidate.split(/:\s+/, 2)[0]?.trim();
3920
+ if (colonHead && colonHead.split(/\s+/).length <= 5) {
3921
+ candidate = colonHead;
3922
+ }
3923
+ const dashHead = candidate.split(/\s+[—-]\s+/, 2)[0]?.trim();
3924
+ if (dashHead && dashHead.split(/\s+/).length <= 5) {
3925
+ candidate = dashHead;
3926
+ }
3927
+ candidate = candidate.replace(/\([^)]*\)/g, " ").replace(/[.;,]+$/, "").trim();
3928
+ if (!candidate)
3929
+ return undefined;
3930
+ const normalized = this.normalizeTextServiceName(candidate) ?? this.normalizeServiceName(candidate);
3931
+ if (!normalized)
3932
+ return undefined;
3933
+ if (normalized.split(" ").length > 4)
3934
+ return undefined;
3935
+ return normalized;
3936
+ }
3937
+ extractRuntimeComponentNames(docs) {
3938
+ const components = new Set();
3939
+ for (const section of this.collectSdsSections(docs)) {
3940
+ if (!RUNTIME_COMPONENTS_HEADING_PATTERN.test(section.heading))
3941
+ continue;
3942
+ let inCodeFence = false;
3943
+ for (const rawLine of section.body) {
3944
+ const trimmed = rawLine.trim();
3945
+ if (/^```/.test(trimmed)) {
3946
+ inCodeFence = !inCodeFence;
3947
+ continue;
3948
+ }
3949
+ if (inCodeFence)
3950
+ continue;
3951
+ const listMatch = trimmed.match(/^(?:[-*]|\d+[.)])\s+(.+)$/);
3952
+ if (!listMatch?.[1])
3953
+ continue;
3954
+ const candidate = this.normalizeRuntimeComponentCandidate(listMatch[1]);
3955
+ if (candidate)
3956
+ components.add(candidate);
3957
+ }
3958
+ }
3959
+ return Array.from(components);
3960
+ }
3961
+ extractVerificationSuites(docs) {
3962
+ const suites = [];
3963
+ const seen = new Set();
3964
+ for (const section of this.collectSdsSections(docs)) {
3965
+ if (!VERIFICATION_MATRIX_HEADING_PATTERN.test(section.heading))
3966
+ continue;
3967
+ for (const rawLine of section.body) {
3968
+ const trimmed = rawLine.trim();
3969
+ if (!trimmed.startsWith("|"))
3970
+ continue;
3971
+ if (/^\|\s*-+\s*\|/i.test(trimmed))
3972
+ continue;
3973
+ const cells = trimmed
3974
+ .split("|")
3975
+ .map((cell) => cell.trim())
3976
+ .filter(Boolean);
3977
+ if (cells.length < 2)
3978
+ continue;
3979
+ if (/verification suite/i.test(cells[0] ?? ""))
3980
+ continue;
3981
+ const name = normalizeHeadingCandidate(cells[0] ?? "");
3982
+ if (!name)
3983
+ continue;
3984
+ const key = this.normalizeServiceLookupKey(name);
3985
+ if (!key || seen.has(key))
3986
+ continue;
3987
+ seen.add(key);
3988
+ suites.push({
3989
+ name,
3990
+ scope: cells[1] || undefined,
3991
+ sourceCoverage: cells[2] || undefined,
3992
+ });
3993
+ }
3994
+ }
3995
+ return suites;
3996
+ }
3997
+ extractAcceptanceScenarios(docs) {
3998
+ const scenarios = [];
3999
+ const seen = new Set();
4000
+ for (const section of this.collectSdsSections(docs)) {
4001
+ if (!ACCEPTANCE_SCENARIOS_HEADING_PATTERN.test(section.heading))
4002
+ continue;
4003
+ for (const rawLine of section.body) {
4004
+ const trimmed = rawLine.trim();
4005
+ const match = trimmed.match(/^(\d+)\.\s+(.+)$/);
4006
+ if (!match?.[1] || !match[2])
4007
+ continue;
4008
+ const index = Number.parseInt(match[1], 10);
4009
+ if (!Number.isFinite(index) || seen.has(index))
4010
+ continue;
4011
+ const details = match[2].trim();
4012
+ const title = normalizeHeadingCandidate(details.split(/:\s+/, 2)[0] ?? details) || `Scenario ${index}`;
4013
+ scenarios.push({ index, title, details });
4014
+ seen.add(index);
4015
+ }
4016
+ }
4017
+ return scenarios.sort((left, right) => left.index - right.index);
4018
+ }
4019
+ classifyBuildTarget(target) {
4020
+ const normalized = this.normalizeStructurePathToken(target) ?? target.replace(/\\/g, "/").trim();
4021
+ const segments = normalized
4022
+ .toLowerCase()
4023
+ .split("/")
4024
+ .map((segment) => segment.trim())
4025
+ .filter(Boolean);
4026
+ const basename = segments[segments.length - 1] ?? normalized.toLowerCase();
4027
+ const isFile = isStructuredFilePath(basename);
4028
+ const isServiceArtifact = SERVICE_ARTIFACT_BASENAME_PATTERN.test(basename);
4029
+ if (segments.some((segment) => BUILD_TARGET_DOC_SEGMENTS.has(segment))) {
4030
+ return { normalized, basename, segments, isFile, kind: "doc", isServiceArtifact };
4031
+ }
4032
+ if (MANIFEST_TARGET_BASENAME_PATTERN.test(basename) || isServiceArtifact) {
4033
+ return { normalized, basename, segments, isFile, kind: "manifest", isServiceArtifact };
4034
+ }
4035
+ if (segments.some((segment) => BUILD_TARGET_TEST_SEGMENTS.has(segment))) {
4036
+ return { normalized, basename, segments, isFile, kind: "test", isServiceArtifact };
4037
+ }
4038
+ if (segments.some((segment) => BUILD_TARGET_OPS_SEGMENTS.has(segment))) {
4039
+ return { normalized, basename, segments, isFile, kind: "ops", isServiceArtifact };
4040
+ }
4041
+ if (segments.some((segment) => BUILD_TARGET_INTERFACE_SEGMENTS.has(segment))) {
4042
+ return { normalized, basename, segments, isFile, kind: "interface", isServiceArtifact };
4043
+ }
4044
+ if (segments.some((segment) => BUILD_TARGET_DATA_SEGMENTS.has(segment))) {
4045
+ return { normalized, basename, segments, isFile, kind: "data", isServiceArtifact };
4046
+ }
4047
+ if (segments.some((segment) => BUILD_TARGET_RUNTIME_SEGMENTS.has(segment))) {
4048
+ return { normalized, basename, segments, isFile, kind: "runtime", isServiceArtifact };
4049
+ }
4050
+ return { normalized, basename, segments, isFile, kind: "unknown", isServiceArtifact };
4051
+ }
4052
+ isStrongImplementationTarget(target) {
4053
+ const classification = this.classifyBuildTarget(target);
4054
+ return (classification.kind === "runtime" ||
4055
+ classification.kind === "interface" ||
4056
+ classification.kind === "data" ||
4057
+ classification.kind === "test" ||
4058
+ classification.kind === "ops");
4059
+ }
4060
+ deriveBuildFocusProfile(texts) {
4061
+ const corpus = this.normalizeServiceLookupKey(texts.join("\n"));
4062
+ return {
4063
+ wantsOps: /\b(deploy|deployment|startup|release|rollback|recovery|rotation|drill|runbook|failover|proxy|operations?|runtime)\b/.test(corpus),
4064
+ wantsVerification: /\b(verify|verification|acceptance|scenario|quality|suite|test|tests|matrix|gate|drill)\b/.test(corpus),
4065
+ wantsData: /\b(data|storage|cache|ledger|pipeline|db|database|persistence)\b/.test(corpus),
4066
+ wantsInterface: /\b(contract|policy|provider|gateway|rpc|api|interface|schema|oracle|protocol)\b/.test(corpus),
4067
+ };
4068
+ }
4069
+ selectBuildTargets(unit, focusTexts, purpose, limit) {
4070
+ const candidates = uniqueStrings([...unit.files, ...unit.directories])
4071
+ .map((candidate) => this.normalizeStructurePathToken(candidate) ?? candidate.replace(/\\/g, "/").trim())
4072
+ .filter(Boolean);
4073
+ if (candidates.length === 0)
4074
+ return [];
4075
+ const focusCorpus = this.normalizeServiceLookupKey([unit.serviceName, ...unit.aliases, ...focusTexts].filter(Boolean).join("\n"));
4076
+ const focusTokens = focusCorpus
4077
+ .split(" ")
4078
+ .map((token) => token.trim())
4079
+ .filter((token) => token.length >= 3);
4080
+ const focusProfile = this.deriveBuildFocusProfile(focusTexts);
4081
+ const scored = candidates
4082
+ .map((target) => {
4083
+ const classification = this.classifyBuildTarget(target);
4084
+ const normalizedTarget = this.normalizeServiceLookupKey(target.replace(/\//g, " "));
4085
+ const overlap = focusTokens.filter((token) => normalizedTarget.includes(token)).length;
4086
+ let score = overlap * 25 + (classification.isFile ? 12 : 0);
4087
+ if (purpose === "structure") {
4088
+ if (classification.kind === "runtime" || classification.kind === "interface")
4089
+ score += 90;
4090
+ else if (classification.kind === "data")
4091
+ score += 75;
4092
+ else if (classification.kind === "ops")
4093
+ score += 30;
4094
+ else if (classification.kind === "manifest")
4095
+ score += 10;
4096
+ else if (classification.kind === "doc")
4097
+ score -= 80;
4098
+ }
4099
+ else if (purpose === "implementation") {
4100
+ if (classification.kind === "runtime")
4101
+ score += classification.isFile ? 170 : 140;
4102
+ else if (classification.kind === "interface")
4103
+ score += classification.isFile ? 160 : 135;
4104
+ else if (classification.kind === "data")
4105
+ score += classification.isFile ? 150 : 125;
4106
+ else if (classification.kind === "test")
4107
+ score += 70;
4108
+ else if (classification.kind === "ops")
4109
+ score += focusProfile.wantsOps ? 140 : 25;
4110
+ else if (classification.kind === "manifest")
4111
+ score -= 140;
4112
+ else if (classification.kind === "doc")
4113
+ score -= 180;
4114
+ }
4115
+ else {
4116
+ if (classification.kind === "test")
4117
+ score += 170;
4118
+ else if (classification.kind === "runtime")
4119
+ score += 120;
4120
+ else if (classification.kind === "interface")
4121
+ score += 105;
4122
+ else if (classification.kind === "data")
4123
+ score += 95;
4124
+ else if (classification.kind === "ops")
4125
+ score += focusProfile.wantsOps ? 160 : 90;
4126
+ else if (classification.kind === "manifest")
4127
+ score -= 120;
4128
+ else if (classification.kind === "doc")
4129
+ score -= 180;
4130
+ }
4131
+ if (focusProfile.wantsOps && classification.kind === "ops")
4132
+ score += 60;
4133
+ if (focusProfile.wantsVerification && classification.kind === "test")
4134
+ score += 60;
4135
+ if (focusProfile.wantsData && classification.kind === "data")
4136
+ score += 55;
4137
+ if (focusProfile.wantsInterface && classification.kind === "interface")
4138
+ score += 55;
4139
+ if (focusProfile.wantsInterface && classification.kind === "runtime")
4140
+ score += 20;
4141
+ return { target, classification, score };
4142
+ })
4143
+ .sort((left, right) => right.score - left.score || left.target.length - right.target.length || left.target.localeCompare(right.target));
4144
+ const strongExists = scored.some((entry) => entry.score > 0 &&
4145
+ (entry.classification.kind === "runtime" ||
4146
+ entry.classification.kind === "interface" ||
4147
+ entry.classification.kind === "data" ||
4148
+ entry.classification.kind === "test" ||
4149
+ entry.classification.kind === "ops"));
4150
+ const filtered = scored.filter((entry) => {
4151
+ if (entry.score <= 0)
4152
+ return false;
4153
+ if (!strongExists)
4154
+ return true;
4155
+ if (purpose === "structure")
4156
+ return entry.classification.kind !== "doc";
4157
+ if (entry.classification.kind === "manifest")
4158
+ return false;
4159
+ return entry.classification.kind !== "doc";
4160
+ });
4161
+ const ranked = (filtered.length > 0 ? filtered : scored.filter((entry) => entry.score > 0)).map((entry) => entry.target);
4162
+ return uniqueStrings(ranked).slice(0, Math.max(1, limit));
4163
+ }
4164
+ buildVerificationSuiteTaskDraft(suite, serviceName) {
4165
+ const normalized = this.normalizeServiceLookupKey([suite.name, suite.scope, suite.sourceCoverage].filter(Boolean).join(" "));
4166
+ const unitTests = [];
4167
+ const componentTests = [];
4168
+ const integrationTests = [];
4169
+ const apiTests = [];
4170
+ if (/\bunit\b/.test(normalized)) {
4171
+ unitTests.push(`Execute the named suite "${suite.name}" for ${serviceName}.`);
4172
+ }
4173
+ if (/\b(component|ui|render|client)\b/.test(normalized)) {
4174
+ componentTests.push(`Execute the named suite "${suite.name}" against the ${serviceName} surface.`);
4175
+ }
4176
+ if (/\b(integration|acceptance|end to end|end-to-end|drill|replay|failover)\b/.test(normalized)) {
4177
+ integrationTests.push(`Execute the named suite "${suite.name}" end to end for ${serviceName}.`);
4178
+ }
4179
+ if (/\b(api|gateway|rpc|provider)\b/.test(normalized)) {
4180
+ apiTests.push(`Execute the named suite "${suite.name}" against the ${serviceName} API/provider surface.`);
4181
+ }
4182
+ if (unitTests.length === 0 &&
4183
+ componentTests.length === 0 &&
4184
+ integrationTests.length === 0 &&
4185
+ apiTests.length === 0) {
4186
+ integrationTests.push(`Execute the named suite "${suite.name}" and capture deterministic evidence.`);
4187
+ }
4188
+ return {
4189
+ description: [
4190
+ `Implement and wire the named verification suite "${suite.name}" for ${serviceName}.`,
4191
+ suite.scope ? `Scope: ${suite.scope}` : undefined,
4192
+ suite.sourceCoverage ? `Source coverage: ${suite.sourceCoverage}` : undefined,
4193
+ ]
4194
+ .filter(Boolean)
4195
+ .join("\n"),
4196
+ unitTests,
4197
+ componentTests,
4198
+ integrationTests,
4199
+ apiTests,
4200
+ };
4201
+ }
2587
4202
  buildSdsCoverageHints(docs) {
2588
4203
  const hints = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_HINT_HEADING_LIMIT);
2589
4204
  if (hints.length === 0)
@@ -2654,8 +4269,213 @@ export class CreateTasksService {
2654
4269
  }
2655
4270
  return { docSummary: blocks.join("\n\n") || "(no docs)", warnings };
2656
4271
  }
2657
- buildPrompt(projectKey, docs, projectBuildMethod, serviceCatalog, options) {
2658
- const docSummary = docs.map((doc, idx) => describeDoc(doc, idx)).join("\n");
4272
+ buildCreateTasksAgentMission(projectKey) {
4273
+ return [
4274
+ `You are the orchestration agent for mcoda create-tasks on project ${projectKey}.`,
4275
+ "Your job in this run is to turn the SDS and supporting docs into an executable backlog that is enough to build the documented product.",
4276
+ "You must understand the services/tools that need to exist, define implementation epics, define the user stories needed to finish each epic, and define the concrete tasks needed to finish each story.",
4277
+ "Keep every phase aligned to the SDS folder tree, runtime topology, dependency order, startup waves, verification suites, acceptance scenarios, and named implementation targets.",
4278
+ "Use only canonical documented names for services, modules, interfaces, commands, schemas, files, and runtime artifacts.",
4279
+ "Do not invent stack choices, rename documented targets, emit placeholder work, or defer SDS gaps to a later manual pass.",
4280
+ "If coverage gaps remain, refine the backlog itself until the backlog is enough to build the product.",
4281
+ ].join("\n");
4282
+ }
4283
+ schemaSnippetForAction(action) {
4284
+ switch (action) {
4285
+ case "epics":
4286
+ return EPIC_SCHEMA_SNIPPET;
4287
+ case "stories":
4288
+ return STORY_SCHEMA_SNIPPET;
4289
+ case "tasks":
4290
+ return TASK_SCHEMA_SNIPPET;
4291
+ case "full_plan":
4292
+ return FULL_PLAN_SCHEMA_SNIPPET;
4293
+ default:
4294
+ return FULL_PLAN_SCHEMA_SNIPPET;
4295
+ }
4296
+ }
4297
+ buildPlanOutline(plan, options) {
4298
+ const maxEpics = options?.maxEpics ?? 16;
4299
+ const maxStoriesPerEpic = options?.maxStoriesPerEpic ?? 8;
4300
+ const maxTasksPerStory = options?.maxTasksPerStory ?? 10;
4301
+ const summary = {
4302
+ epics: plan.epics.slice(0, maxEpics).map((epic) => ({
4303
+ localId: epic.localId,
4304
+ title: epic.title,
4305
+ area: epic.area,
4306
+ serviceIds: normalizeStringArray(epic.serviceIds),
4307
+ tags: normalizeStringArray(epic.tags),
4308
+ acceptanceCriteria: (epic.acceptanceCriteria ?? []).slice(0, 8),
4309
+ stories: plan.stories
4310
+ .filter((story) => story.epicLocalId === epic.localId)
4311
+ .slice(0, maxStoriesPerEpic)
4312
+ .map((story) => ({
4313
+ localId: story.localId,
4314
+ title: story.title,
4315
+ userStory: story.userStory,
4316
+ acceptanceCriteria: (story.acceptanceCriteria ?? []).slice(0, 8),
4317
+ tasks: plan.tasks
4318
+ .filter((task) => task.epicLocalId === epic.localId && task.storyLocalId === story.localId)
4319
+ .slice(0, maxTasksPerStory)
4320
+ .map((task) => ({
4321
+ localId: task.localId,
4322
+ title: task.title,
4323
+ type: task.type,
4324
+ dependsOnKeys: normalizeStringArray(task.dependsOnKeys),
4325
+ description: task.description,
4326
+ unitTests: normalizeStringArray(task.unitTests),
4327
+ componentTests: normalizeStringArray(task.componentTests),
4328
+ integrationTests: normalizeStringArray(task.integrationTests),
4329
+ apiTests: normalizeStringArray(task.apiTests),
4330
+ })),
4331
+ })),
4332
+ })),
4333
+ };
4334
+ return JSON.stringify(summary, null, 2);
4335
+ }
4336
+ parseFullPlan(output, options) {
4337
+ const parsed = extractJson(output);
4338
+ if (!parsed || !Array.isArray(parsed.epics) || parsed.epics.length === 0) {
4339
+ throw new Error("Agent did not return a full backlog plan in expected format");
4340
+ }
4341
+ return this.materializePlanFromSeed(parsed, options);
4342
+ }
4343
+ async normalizeGeneratedPlan(params) {
4344
+ const normalizedPlanEpics = this.alignEpicsToServiceCatalog(params.plan.epics, params.serviceCatalog, params.unknownEpicServicePolicy);
4345
+ for (const warning of normalizedPlanEpics.warnings) {
4346
+ await this.jobService.appendLog(params.jobId, `[create-tasks] ${warning}\n`);
4347
+ }
4348
+ let plan = {
4349
+ ...params.plan,
4350
+ epics: normalizedPlanEpics.epics.map((epic, index) => ({
4351
+ ...epic,
4352
+ localId: epic.localId ?? `e${index + 1}`,
4353
+ stories: [],
4354
+ })),
4355
+ };
4356
+ plan = this.enforceStoryScopedDependencies(plan);
4357
+ plan = this.injectStructureBootstrapPlan(plan, params.docs, params.serviceCatalog.projectKey);
4358
+ plan = this.enforceStoryScopedDependencies(plan);
4359
+ this.validatePlanLocalIdentifiers(plan);
4360
+ plan = this.applyServiceDependencySequencing(plan, params.docs);
4361
+ plan = this.enforceStoryScopedDependencies(plan);
4362
+ this.validatePlanLocalIdentifiers(plan);
4363
+ this.derivePlanningArtifacts(params.serviceCatalog.projectKey, params.docs, plan, params.sourceTopologyExpectation);
4364
+ return plan;
4365
+ }
4366
+ buildRefinementPrompt(params) {
4367
+ const serviceCatalogSummary = this.buildServiceCatalogPromptSummary(params.serviceCatalog);
4368
+ const planOutline = this.buildPlanOutline(params.currentPlan, params.options);
4369
+ const refinementLimits = [
4370
+ params.options.maxEpics ? `- Limit epics to ${params.options.maxEpics}.` : "",
4371
+ params.options.maxStoriesPerEpic ? `- Limit stories per epic to ${params.options.maxStoriesPerEpic}.` : "",
4372
+ params.options.maxTasksPerStory ? `- Limit tasks per story to ${params.options.maxTasksPerStory}.` : "",
4373
+ ]
4374
+ .filter(Boolean)
4375
+ .join("\n");
4376
+ const plannedGapBundles = params.audit.plannedGapBundles.length > 0
4377
+ ? JSON.stringify(params.audit.plannedGapBundles.slice(0, 48), null, 2)
4378
+ : "[]";
4379
+ return [
4380
+ this.buildCreateTasksAgentMission(params.projectKey),
4381
+ `Refinement iteration ${params.iteration}. The current backlog did not yet satisfy the SDS sufficiency audit.`,
4382
+ "Return a complete replacement backlog as valid JSON only matching:",
4383
+ FULL_PLAN_SCHEMA_SNIPPET,
4384
+ "Refinement rules:",
4385
+ "- Return the full revised backlog, not a delta.",
4386
+ "- Preserve already-good backlog slices unless a stricter SDS-aligned replacement is required.",
4387
+ "- Every epic must map to one or more serviceIds from the phase-0 service catalog.",
4388
+ "- Every story must contain concrete tasks, and every task must stay scoped to its own story.",
4389
+ "- Every task must be implementation-concrete, name real targets when the SDS exposes them, and include unit/component/integration/api test arrays ([] when not applicable).",
4390
+ "- Fix the specific missing SDS coverage items listed below. Do not claim coverage unless the backlog contains executable work for them.",
4391
+ "- Maintain dependency-first sequencing from foundational/runtime prerequisites through verification and acceptance evidence.",
4392
+ refinementLimits || "- Use reasonable scope without over-generating backlog items.",
4393
+ "Why this revision is required:",
4394
+ formatBullets(params.reasons, "SDS coverage gaps remain."),
4395
+ "Current backlog outline:",
4396
+ planOutline,
4397
+ "Remaining section headings:",
4398
+ formatBullets(params.audit.remainingSectionHeadings, "none"),
4399
+ "Remaining folder entries:",
4400
+ formatBullets(params.audit.remainingFolderEntries, "none"),
4401
+ "Actionable gap bundles (anchor + concrete implementation targets):",
4402
+ plannedGapBundles,
4403
+ "Project construction method:",
4404
+ params.projectBuildMethod,
4405
+ "Phase 0 service catalog (allowed serviceIds):",
4406
+ serviceCatalogSummary,
4407
+ "Docs available:",
4408
+ params.docSummary || "- (no docs provided; propose sensible refinements).",
4409
+ ].join("\n\n");
4410
+ }
4411
+ async refinePlanWithAgent(params) {
4412
+ const prompt = this.buildRefinementPrompt({
4413
+ projectKey: params.projectKey,
4414
+ currentPlan: params.currentPlan,
4415
+ audit: params.audit,
4416
+ reasons: params.reasons,
4417
+ docSummary: params.docSummary,
4418
+ projectBuildMethod: params.projectBuildMethod,
4419
+ serviceCatalog: params.serviceCatalog,
4420
+ options: params.options,
4421
+ iteration: params.iteration,
4422
+ });
4423
+ const { output } = await this.invokeAgentWithRetry(params.agent, prompt, "full_plan", params.agentStream, params.jobId, params.commandRunId, {
4424
+ refinementIteration: params.iteration,
4425
+ remainingGapCount: params.audit.remainingGaps.total,
4426
+ remainingSectionCount: params.audit.remainingSectionHeadings.length,
4427
+ remainingFolderCount: params.audit.remainingFolderEntries.length,
4428
+ });
4429
+ const refinedPlan = this.parseFullPlan(output, params.options);
4430
+ return this.normalizeGeneratedPlan({
4431
+ plan: refinedPlan,
4432
+ docs: params.docs,
4433
+ serviceCatalog: params.serviceCatalog,
4434
+ sourceTopologyExpectation: params.sourceTopologyExpectation,
4435
+ unknownEpicServicePolicy: params.unknownEpicServicePolicy,
4436
+ jobId: params.jobId,
4437
+ });
4438
+ }
4439
+ async runTaskSufficiencyAudit(params) {
4440
+ if (!this.taskSufficiencyFactory) {
4441
+ return { warnings: [] };
4442
+ }
4443
+ let audit;
4444
+ let error;
4445
+ let closeError;
4446
+ try {
4447
+ const sufficiencyService = await this.taskSufficiencyFactory(this.workspace);
4448
+ try {
4449
+ audit = await sufficiencyService.runAudit({
4450
+ workspace: params.workspace,
4451
+ projectKey: params.projectKey,
4452
+ sourceCommand: params.sourceCommand,
4453
+ dryRun: params.dryRun,
4454
+ });
4455
+ }
4456
+ finally {
4457
+ try {
4458
+ await sufficiencyService.close();
4459
+ }
4460
+ catch (caught) {
4461
+ closeError = caught?.message ?? String(caught);
4462
+ await this.jobService.appendLog(params.jobId, `Task sufficiency audit close warning: ${closeError}\n`);
4463
+ }
4464
+ }
4465
+ }
4466
+ catch (caught) {
4467
+ error = caught?.message ?? String(caught);
4468
+ }
4469
+ return {
4470
+ audit,
4471
+ error,
4472
+ warnings: uniqueStrings([
4473
+ ...(audit?.warnings ?? []),
4474
+ ...(closeError ? [`Task sufficiency audit close warning: ${closeError}`] : []),
4475
+ ]),
4476
+ };
4477
+ }
4478
+ buildPrompt(projectKey, docSummary, projectBuildMethod, serviceCatalog, options) {
2659
4479
  const serviceCatalogSummary = this.buildServiceCatalogPromptSummary(serviceCatalog);
2660
4480
  const limits = [
2661
4481
  options.maxEpics ? `Limit epics to ${options.maxEpics}.` : "",
@@ -2665,18 +4485,22 @@ export class CreateTasksService {
2665
4485
  .filter(Boolean)
2666
4486
  .join(" ");
2667
4487
  const prompt = [
2668
- `You are assisting in phase 1 of 3 for project ${projectKey}: generate epics only.`,
2669
- "Process is strict and direct: build plan -> epics -> stories -> tasks.",
4488
+ this.buildCreateTasksAgentMission(projectKey),
4489
+ `You are assisting in phase 1 of 3 for project ${projectKey}: understand the documented services/tools and generate epics only.`,
4490
+ "Process is strict and direct: understand services/tools -> epics -> stories -> tasks -> sufficiency refinement.",
2670
4491
  "This step outputs only epics derived from the build plan and docs.",
2671
4492
  "Return strictly valid JSON (no prose) matching:",
2672
4493
  EPIC_SCHEMA_SNIPPET,
2673
4494
  "Rules:",
4495
+ "- First reason through which documented services/tools must be created or changed, then express that understanding through executable implementation epics.",
2674
4496
  "- Do NOT include final slugs; the system will assign keys.",
2675
4497
  "- Use docdex handles when referencing docs.",
2676
4498
  "- acceptanceCriteria must be an array of strings (5-10 items).",
2677
4499
  "- Keep epics actionable and implementation-oriented; avoid glossary/admin-only epics.",
2678
4500
  "- Prefer dependency-first sequencing: foundational setup epics before dependent feature epics.",
2679
4501
  "- Keep output derived from docs; do not assume stacks unless docs state them.",
4502
+ "- Use canonical documented names for modules, services, interfaces, commands, schemas, and files exactly as they appear in Docs and the project construction method.",
4503
+ "- Do not rename explicit documented targets or replace them with invented alternatives.",
2680
4504
  "- serviceIds is required and must contain one or more ids from the phase-0 service catalog below.",
2681
4505
  `- If an epic spans multiple services, include tag \"${CROSS_SERVICE_TAG}\" in tags.`,
2682
4506
  "Project construction method to follow:",
@@ -2727,9 +4551,9 @@ export class CreateTasksService {
2727
4551
  },
2728
4552
  {
2729
4553
  localId: "task-2",
2730
- title: "Integrate core contracts and dependencies",
4554
+ title: "Integrate core dependencies and interfaces",
2731
4555
  type: "feature",
2732
- description: "Wire key contracts/interfaces and dependency paths so core behavior can execute end-to-end.",
4556
+ description: "Wire key dependencies, interfaces, and integration paths so core behavior can execute end-to-end.",
2733
4557
  estimatedStoryPoints: 3,
2734
4558
  priorityHint: 20,
2735
4559
  dependsOnKeys: ["task-1"],
@@ -2884,7 +4708,7 @@ export class CreateTasksService {
2884
4708
  const attempt = 2;
2885
4709
  const fixPrompt = [
2886
4710
  "Rewrite the previous response into valid JSON matching the expected schema.",
2887
- `Schema hint:\n${action === "epics" ? EPIC_SCHEMA_SNIPPET : action === "stories" ? STORY_SCHEMA_SNIPPET : TASK_SCHEMA_SNIPPET}`,
4711
+ `Schema hint:\n${this.schemaSnippetForAction(action)}`,
2888
4712
  "Return JSON only; no prose.",
2889
4713
  `Original content:\n${output}`,
2890
4714
  ].join("\n\n");
@@ -2988,8 +4812,9 @@ export class CreateTasksService {
2988
4812
  }))
2989
4813
  .filter((e) => e.title);
2990
4814
  }
2991
- async generateStoriesForEpic(agent, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
4815
+ async generateStoriesForEpic(agent, projectKey, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
2992
4816
  const prompt = [
4817
+ this.buildCreateTasksAgentMission(projectKey),
2993
4818
  `Generate user stories for epic "${epic.title}" (phase 2 of 3).`,
2994
4819
  "This phase is stories-only. Do not generate tasks yet.",
2995
4820
  "Return JSON only matching:",
@@ -2999,7 +4824,9 @@ export class CreateTasksService {
2999
4824
  "- acceptanceCriteria must be an array of strings.",
3000
4825
  "- Use docdex handles when citing docs.",
3001
4826
  "- Keep stories direct and implementation-oriented; avoid placeholder-only narrative sections.",
4827
+ "- Define the minimum set of user stories that, when completed, will finish this epic according to the SDS and construction method.",
3002
4828
  "- Keep story sequencing aligned with the project construction method.",
4829
+ "- Preserve canonical documented names for modules, services, interfaces, commands, schemas, and files exactly as written.",
3003
4830
  `Epic context (key=${epic.key ?? epic.localId ?? "TBD"}):`,
3004
4831
  epic.description ?? "(no description provided)",
3005
4832
  `Epic serviceIds: ${(epic.serviceIds ?? []).join(", ") || "(not provided)"}`,
@@ -3028,7 +4855,7 @@ export class CreateTasksService {
3028
4855
  }))
3029
4856
  .filter((s) => s.title);
3030
4857
  }
3031
- async generateTasksForStory(agent, epic, story, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
4858
+ async generateTasksForStory(agent, projectKey, epic, story, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
3032
4859
  const parseTestList = (value) => {
3033
4860
  if (!Array.isArray(value))
3034
4861
  return [];
@@ -3038,6 +4865,7 @@ export class CreateTasksService {
3038
4865
  .filter(Boolean);
3039
4866
  };
3040
4867
  const prompt = [
4868
+ this.buildCreateTasksAgentMission(projectKey),
3041
4869
  `Generate tasks for story "${story.title}" (Epic: ${epic.title}, phase 3 of 3).`,
3042
4870
  "This phase is tasks-only for the given story.",
3043
4871
  "Return JSON only matching:",
@@ -3056,8 +4884,10 @@ export class CreateTasksService {
3056
4884
  "- Start from prerequisite codebase setup: add structure/bootstrap tasks before feature tasks when missing.",
3057
4885
  "- Keep dependencies strictly inside this story; never reference tasks from other stories/epics.",
3058
4886
  "- Order tasks from foundational prerequisites to dependents based on documented dependency direction and startup constraints.",
4887
+ "- Generate the concrete work that would actually complete this story in one automated backlog pass; do not leave implied implementation gaps behind.",
3059
4888
  "- Avoid placeholder wording (TBD, TODO, to be defined, generic follow-up phrases).",
3060
4889
  "- Avoid documentation-only or glossary-only tasks unless story acceptance explicitly requires them.",
4890
+ "- Preserve canonical documented names for modules, services, interfaces, commands, schemas, and files exactly as written.",
3061
4891
  "- Use docdex handles when citing docs.",
3062
4892
  "- If OPENAPI_HINTS are present in Docs, align tasks with hinted service/capability/stage/test_requirements.",
3063
4893
  "- If SDS_COVERAGE_HINTS are present in Docs, cover the relevant SDS sections in implementation tasks.",
@@ -3157,11 +4987,11 @@ export class CreateTasksService {
3157
4987
  },
3158
4988
  {
3159
4989
  localId: "t-fallback-2",
3160
- title: `Integrate contracts for ${story.title}`,
4990
+ title: `Integrate dependencies for ${story.title}`,
3161
4991
  type: "feature",
3162
4992
  description: [
3163
- `Integrate dependent contracts/interfaces for "${story.title}" after core scope implementation.`,
3164
- "Align internal/external interfaces, data contracts, and dependency wiring with SDS/OpenAPI context.",
4993
+ `Integrate dependent interfaces and runtime dependencies for "${story.title}" after core scope implementation.`,
4994
+ "Align internal/external interfaces, data shapes, and dependency wiring with the documented context.",
3165
4995
  "Record dependency rationale and compatibility constraints in the task output.",
3166
4996
  ].join("\n"),
3167
4997
  estimatedStoryPoints: 3,
@@ -3193,7 +5023,7 @@ export class CreateTasksService {
3193
5023
  },
3194
5024
  ];
3195
5025
  }
3196
- async generatePlanFromAgent(epics, agent, docSummary, options) {
5026
+ async generatePlanFromAgent(projectKey, epics, agent, docSummary, options) {
3197
5027
  const planEpics = epics.map((epic, idx) => ({
3198
5028
  ...epic,
3199
5029
  localId: epic.localId ?? `e${idx + 1}`,
@@ -3205,7 +5035,7 @@ export class CreateTasksService {
3205
5035
  let stories = [];
3206
5036
  let usedFallbackStories = false;
3207
5037
  try {
3208
- stories = await this.generateStoriesForEpic(agent, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
5038
+ stories = await this.generateStoriesForEpic(agent, projectKey, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
3209
5039
  }
3210
5040
  catch (error) {
3211
5041
  usedFallbackStories = true;
@@ -3238,7 +5068,7 @@ export class CreateTasksService {
3238
5068
  }
3239
5069
  else {
3240
5070
  try {
3241
- tasks = await this.generateTasksForStory(agent, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
5071
+ tasks = await this.generateTasksForStory(agent, projectKey, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
3242
5072
  }
3243
5073
  catch (error) {
3244
5074
  await this.jobService.appendLog(options.jobId, `Task generation failed for story "${story.title}" (${storyScope}). Using deterministic fallback tasks. Reason: ${error.message ?? String(error)}\n`);
@@ -3261,50 +5091,130 @@ export class CreateTasksService {
3261
5091
  }
3262
5092
  return { epics: planEpics, stories: planStories, tasks: planTasks };
3263
5093
  }
3264
- buildSdsCoverageReport(projectKey, docs, plan) {
3265
- const sections = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_REPORT_SECTION_LIMIT);
3266
- const normalize = (value) => value
3267
- .toLowerCase()
3268
- .replace(/[`*_]/g, "")
3269
- .replace(/[^a-z0-9\s/-]+/g, " ")
3270
- .replace(/\s+/g, " ")
3271
- .trim();
3272
- const planCorpus = normalize([
5094
+ buildCoverageCorpus(plan) {
5095
+ return normalizeCoverageText([
3273
5096
  ...plan.epics.map((epic) => `${epic.title} ${epic.description ?? ""} ${(epic.acceptanceCriteria ?? []).join(" ")}`),
3274
5097
  ...plan.stories.map((story) => `${story.title} ${story.userStory ?? ""} ${story.description ?? ""} ${(story.acceptanceCriteria ?? []).join(" ")}`),
3275
5098
  ...plan.tasks.map((task) => `${task.title} ${task.description ?? ""}`),
3276
5099
  ].join("\n"));
3277
- const matched = [];
3278
- const unmatched = [];
3279
- for (const section of sections) {
3280
- const normalizedSection = normalize(section);
3281
- if (!normalizedSection)
3282
- continue;
3283
- const keywords = normalizedSection
3284
- .split(/\s+/)
3285
- .filter((token) => token.length >= 4)
3286
- .slice(0, 6);
3287
- const hasDirectMatch = normalizedSection.length >= 6 && planCorpus.includes(normalizedSection);
3288
- const hasKeywordMatch = keywords.some((keyword) => planCorpus.includes(keyword));
3289
- if (hasDirectMatch || hasKeywordMatch) {
3290
- matched.push(section);
3291
- }
3292
- else {
3293
- unmatched.push(section);
5100
+ }
5101
+ collectCoverageAnchorsFromBacklog(backlog) {
5102
+ const anchors = new Set();
5103
+ for (const task of backlog.tasks) {
5104
+ const sufficiencyAudit = task.metadata?.sufficiencyAudit;
5105
+ const anchor = typeof sufficiencyAudit?.anchor === "string" ? sufficiencyAudit.anchor.trim() : "";
5106
+ if (anchor)
5107
+ anchors.add(anchor);
5108
+ if (Array.isArray(sufficiencyAudit?.anchors)) {
5109
+ for (const value of sufficiencyAudit.anchors) {
5110
+ if (typeof value !== "string" || value.trim().length === 0)
5111
+ continue;
5112
+ anchors.add(value.trim());
5113
+ }
3294
5114
  }
3295
5115
  }
3296
- const totalSections = matched.length + unmatched.length;
3297
- const coverageRatio = totalSections === 0 ? 1 : matched.length / totalSections;
5116
+ return anchors;
5117
+ }
5118
+ assertCoverageConsistency(projectKey, report, expected) {
5119
+ const sort = (values) => [...values].sort((left, right) => left.localeCompare(right));
5120
+ const sameSectionGaps = JSON.stringify(sort(report.missingSectionHeadings)) === JSON.stringify(sort(expected.missingSectionHeadings));
5121
+ const sameFolderGaps = JSON.stringify(sort(report.missingFolderEntries)) === JSON.stringify(sort(expected.missingFolderEntries));
5122
+ if (report.totalSignals !== expected.totalSignals ||
5123
+ report.coverageRatio !== expected.coverageRatio ||
5124
+ !sameSectionGaps ||
5125
+ !sameFolderGaps) {
5126
+ throw new Error(`create-tasks produced inconsistent coverage artifacts for project "${projectKey}". coverage-report.json diverged from task sufficiency coverage.`);
5127
+ }
5128
+ }
5129
+ assertPlanningArtifactConsistency(projectKey, buildPlan, serviceCatalog) {
5130
+ const sort = (values) => [...values].sort((left, right) => left.localeCompare(right));
5131
+ const catalogSourceDocs = sort(uniqueStrings(serviceCatalog.sourceDocs));
5132
+ const buildPlanSourceDocs = sort(uniqueStrings(buildPlan.sourceDocs));
5133
+ if (JSON.stringify(catalogSourceDocs) !== JSON.stringify(buildPlanSourceDocs)) {
5134
+ throw new Error(`create-tasks produced inconsistent planning artifacts for project "${projectKey}". build-plan.json and services.json disagree on source docs.`);
5135
+ }
5136
+ const catalogServiceNames = serviceCatalog.services.map((service) => service.name);
5137
+ const catalogServiceIds = serviceCatalog.services.map((service) => service.id);
5138
+ const expectedBuildPlanServiceNames = catalogServiceNames.slice(0, buildPlan.services.length);
5139
+ const expectedBuildPlanServiceIds = catalogServiceIds.slice(0, buildPlan.serviceIds.length);
5140
+ if (JSON.stringify(buildPlan.services) !== JSON.stringify(expectedBuildPlanServiceNames) ||
5141
+ JSON.stringify(buildPlan.serviceIds) !== JSON.stringify(expectedBuildPlanServiceIds)) {
5142
+ throw new Error(`create-tasks produced inconsistent planning artifacts for project "${projectKey}". build-plan.json and services.json disagree on service identity ordering.`);
5143
+ }
5144
+ const catalogServicesByName = new Map(serviceCatalog.services.map((service) => [service.name, service]));
5145
+ const unknownWaveServices = buildPlan.startupWaves.flatMap((wave) => wave.services
5146
+ .filter((serviceName) => !catalogServicesByName.has(serviceName))
5147
+ .map((serviceName) => `wave ${wave.wave}:${serviceName}`));
5148
+ if (unknownWaveServices.length > 0) {
5149
+ throw new Error(`create-tasks produced inconsistent planning artifacts for project "${projectKey}". build-plan.json references services missing from services.json: ${unknownWaveServices.slice(0, 8).join(", ")}.`);
5150
+ }
5151
+ }
5152
+ async loadExpectedCoverageFromSufficiencyReport(reportPath) {
5153
+ if (!reportPath)
5154
+ return undefined;
5155
+ let raw;
5156
+ try {
5157
+ raw = await fs.readFile(reportPath, "utf8");
5158
+ }
5159
+ catch (error) {
5160
+ const message = error?.message ?? String(error);
5161
+ throw new Error(`create-tasks failed to load task sufficiency coverage report from "${reportPath}": ${message}`);
5162
+ }
5163
+ let parsed;
5164
+ try {
5165
+ parsed = JSON.parse(raw);
5166
+ }
5167
+ catch (error) {
5168
+ const message = error?.message ?? String(error);
5169
+ throw new Error(`create-tasks failed to parse task sufficiency coverage report from "${reportPath}": ${message}`);
5170
+ }
5171
+ const finalCoverage = parsed.finalCoverage;
5172
+ if (!finalCoverage ||
5173
+ typeof finalCoverage.coverageRatio !== "number" ||
5174
+ typeof finalCoverage.totalSignals !== "number" ||
5175
+ !Array.isArray(finalCoverage.missingSectionHeadings) ||
5176
+ !Array.isArray(finalCoverage.missingFolderEntries)) {
5177
+ throw new Error(`create-tasks failed to load task sufficiency coverage report from "${reportPath}": finalCoverage is incomplete.`);
5178
+ }
5179
+ return {
5180
+ coverageRatio: finalCoverage.coverageRatio,
5181
+ totalSignals: finalCoverage.totalSignals,
5182
+ missingSectionHeadings: finalCoverage.missingSectionHeadings.filter((value) => typeof value === "string"),
5183
+ missingFolderEntries: finalCoverage.missingFolderEntries.filter((value) => typeof value === "string"),
5184
+ };
5185
+ }
5186
+ buildSdsCoverageReport(projectKey, docs, plan, existingAnchors = new Set()) {
5187
+ const coverageSignals = collectSdsCoverageSignalsFromDocs(docs, {
5188
+ headingLimit: SDS_COVERAGE_REPORT_SECTION_LIMIT,
5189
+ folderLimit: SDS_COVERAGE_REPORT_FOLDER_LIMIT,
5190
+ });
5191
+ const coverage = evaluateSdsCoverage(this.buildCoverageCorpus(plan), {
5192
+ sectionHeadings: coverageSignals.sectionHeadings,
5193
+ folderEntries: coverageSignals.folderEntries,
5194
+ }, existingAnchors);
5195
+ const matchedSections = coverageSignals.sectionHeadings.filter((heading) => !coverage.missingSectionHeadings.includes(heading));
5196
+ const matchedFolderEntries = coverageSignals.folderEntries.filter((entry) => !coverage.missingFolderEntries.includes(entry));
3298
5197
  return {
3299
5198
  projectKey,
3300
5199
  generatedAt: new Date().toISOString(),
3301
- totalSections,
3302
- matched,
3303
- unmatched,
3304
- coverageRatio: Number(coverageRatio.toFixed(4)),
3305
- notes: totalSections === 0
3306
- ? ["No SDS section headings detected; coverage defaults to 1.0."]
3307
- : ["Coverage is heading-based heuristic match between SDS sections and generated epic/story/task corpus."],
5200
+ totalSignals: coverage.totalSignals,
5201
+ totalSections: coverageSignals.sectionHeadings.length,
5202
+ totalFolderEntries: coverageSignals.folderEntries.length,
5203
+ rawSectionSignals: coverageSignals.rawSectionHeadings.length,
5204
+ rawFolderSignals: coverageSignals.rawFolderEntries.length,
5205
+ skippedHeadingSignals: coverageSignals.skippedHeadingSignals,
5206
+ skippedFolderSignals: coverageSignals.skippedFolderSignals,
5207
+ matched: matchedSections,
5208
+ unmatched: coverage.missingSectionHeadings,
5209
+ matchedSections,
5210
+ missingSectionHeadings: coverage.missingSectionHeadings,
5211
+ matchedFolderEntries,
5212
+ missingFolderEntries: coverage.missingFolderEntries,
5213
+ existingAnchorsCount: existingAnchors.size,
5214
+ coverageRatio: coverage.coverageRatio,
5215
+ notes: coverage.totalSignals === 0
5216
+ ? ["No actionable SDS implementation signals detected; coverage defaults to 1.0."]
5217
+ : ["Coverage uses the same heading and folder signal model as task-sufficiency-audit."],
3308
5218
  };
3309
5219
  }
3310
5220
  async acquirePlanArtifactLock(baseDir, options) {
@@ -3365,11 +5275,171 @@ export class CreateTasksService {
3365
5275
  }
3366
5276
  }
3367
5277
  }
3368
- async writePlanArtifacts(projectKey, plan, docSummary, docs, buildPlan, serviceCatalog) {
5278
+ splitPersistedAcceptanceCriteria(value) {
5279
+ if (!value)
5280
+ return [];
5281
+ return uniqueStrings(value
5282
+ .split(/\r?\n/)
5283
+ .map((line) => line.replace(/^[-*]\s+/, "").trim())
5284
+ .filter(Boolean));
5285
+ }
5286
+ async loadPersistedBacklog(projectId) {
5287
+ const repoLike = this.workspaceRepo;
5288
+ if (typeof repoLike.getDb !== "function") {
5289
+ const epics = Array.isArray(repoLike.epics)
5290
+ ? repoLike.epics.filter((row) => row.projectId === projectId)
5291
+ : [];
5292
+ const stories = Array.isArray(repoLike.stories)
5293
+ ? repoLike.stories.filter((row) => row.projectId === projectId)
5294
+ : [];
5295
+ const tasks = Array.isArray(repoLike.tasks)
5296
+ ? repoLike.tasks.filter((row) => row.projectId === projectId)
5297
+ : [];
5298
+ const taskIds = new Set(tasks.map((task) => task.id));
5299
+ const dependencies = Array.isArray(repoLike.deps)
5300
+ ? repoLike.deps.filter((row) => taskIds.has(row.taskId))
5301
+ : [];
5302
+ return { epics, stories, tasks, dependencies };
5303
+ }
5304
+ const db = repoLike.getDb();
5305
+ const epicRows = await db.all(`SELECT id, project_id, key, title, description, story_points_total, priority, metadata_json, created_at, updated_at
5306
+ FROM epics
5307
+ WHERE project_id = ?
5308
+ ORDER BY COALESCE(priority, 2147483647), datetime(created_at), key`, projectId);
5309
+ const storyRows = await db.all(`SELECT id, project_id, epic_id, key, title, description, acceptance_criteria, story_points_total, priority, metadata_json, created_at, updated_at
5310
+ FROM user_stories
5311
+ WHERE project_id = ?
5312
+ ORDER BY COALESCE(priority, 2147483647), datetime(created_at), key`, projectId);
5313
+ const taskRows = await db.all(`SELECT id, project_id, epic_id, user_story_id, key, title, description, type, status, story_points, priority,
5314
+ assigned_agent_id, assignee_human, vcs_branch, vcs_base_branch, vcs_last_commit_sha, metadata_json,
5315
+ openapi_version_at_creation, created_at, updated_at
5316
+ FROM tasks
5317
+ WHERE project_id = ?
5318
+ ORDER BY COALESCE(priority, 2147483647), datetime(created_at), key`, projectId);
5319
+ const epics = epicRows.map((row) => ({
5320
+ id: row.id,
5321
+ projectId: row.project_id,
5322
+ key: row.key,
5323
+ title: row.title,
5324
+ description: row.description,
5325
+ storyPointsTotal: row.story_points_total ?? null,
5326
+ priority: row.priority ?? null,
5327
+ metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
5328
+ createdAt: row.created_at,
5329
+ updatedAt: row.updated_at,
5330
+ }));
5331
+ const stories = storyRows.map((row) => ({
5332
+ id: row.id,
5333
+ projectId: row.project_id,
5334
+ epicId: row.epic_id,
5335
+ key: row.key,
5336
+ title: row.title,
5337
+ description: row.description,
5338
+ acceptanceCriteria: row.acceptance_criteria ?? null,
5339
+ storyPointsTotal: row.story_points_total ?? null,
5340
+ priority: row.priority ?? null,
5341
+ metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
5342
+ createdAt: row.created_at,
5343
+ updatedAt: row.updated_at,
5344
+ }));
5345
+ const tasks = taskRows.map((row) => ({
5346
+ id: row.id,
5347
+ projectId: row.project_id,
5348
+ epicId: row.epic_id,
5349
+ userStoryId: row.user_story_id,
5350
+ key: row.key,
5351
+ title: row.title,
5352
+ description: row.description,
5353
+ type: row.type ?? null,
5354
+ status: row.status,
5355
+ storyPoints: row.story_points ?? null,
5356
+ priority: row.priority ?? null,
5357
+ assignedAgentId: row.assigned_agent_id ?? null,
5358
+ assigneeHuman: row.assignee_human ?? null,
5359
+ vcsBranch: row.vcs_branch ?? null,
5360
+ vcsBaseBranch: row.vcs_base_branch ?? null,
5361
+ vcsLastCommitSha: row.vcs_last_commit_sha ?? null,
5362
+ metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
5363
+ openapiVersionAtCreation: row.openapi_version_at_creation ?? null,
5364
+ createdAt: row.created_at,
5365
+ updatedAt: row.updated_at,
5366
+ }));
5367
+ const dependencies = typeof repoLike.getTaskDependencies === "function"
5368
+ ? await repoLike.getTaskDependencies(tasks.map((task) => task.id))
5369
+ : [];
5370
+ return { epics, stories, tasks, dependencies };
5371
+ }
5372
+ buildPlanFromPersistedBacklog(backlog) {
5373
+ const storyById = new Map(backlog.stories.map((story) => [story.id, story]));
5374
+ const epicById = new Map(backlog.epics.map((epic) => [epic.id, epic]));
5375
+ const taskById = new Map(backlog.tasks.map((task) => [task.id, task]));
5376
+ const dependencyKeysByTaskId = new Map();
5377
+ for (const dependency of backlog.dependencies) {
5378
+ const current = dependencyKeysByTaskId.get(dependency.taskId) ?? [];
5379
+ const dependsOn = taskById.get(dependency.dependsOnTaskId)?.key;
5380
+ if (dependsOn && !current.includes(dependsOn))
5381
+ current.push(dependsOn);
5382
+ dependencyKeysByTaskId.set(dependency.taskId, current);
5383
+ }
5384
+ return {
5385
+ epics: backlog.epics.map((epic) => {
5386
+ const metadata = (epic.metadata ?? {});
5387
+ return {
5388
+ localId: epic.key,
5389
+ area: epic.key.split("-")[0]?.toLowerCase() || "proj",
5390
+ title: epic.title,
5391
+ description: epic.description,
5392
+ acceptanceCriteria: [],
5393
+ relatedDocs: normalizeRelatedDocs(metadata.doc_links),
5394
+ priorityHint: epic.priority ?? undefined,
5395
+ serviceIds: normalizeStringArray(metadata.service_ids),
5396
+ tags: normalizeStringArray(metadata.tags),
5397
+ stories: [],
5398
+ };
5399
+ }),
5400
+ stories: backlog.stories.map((story) => {
5401
+ const metadata = (story.metadata ?? {});
5402
+ return {
5403
+ localId: story.key,
5404
+ epicLocalId: epicById.get(story.epicId)?.key ?? story.epicId,
5405
+ title: story.title,
5406
+ userStory: undefined,
5407
+ description: story.description,
5408
+ acceptanceCriteria: this.splitPersistedAcceptanceCriteria(story.acceptanceCriteria),
5409
+ relatedDocs: normalizeRelatedDocs(metadata.doc_links),
5410
+ priorityHint: story.priority ?? undefined,
5411
+ tasks: [],
5412
+ };
5413
+ }),
5414
+ tasks: backlog.tasks.map((task) => {
5415
+ const metadata = (task.metadata ?? {});
5416
+ const testRequirements = (metadata.test_requirements ?? {});
5417
+ return {
5418
+ localId: task.key,
5419
+ epicLocalId: epicById.get(task.epicId)?.key ?? task.epicId,
5420
+ storyLocalId: storyById.get(task.userStoryId)?.key ?? task.userStoryId,
5421
+ title: task.title,
5422
+ type: task.type ?? "feature",
5423
+ description: task.description,
5424
+ estimatedStoryPoints: task.storyPoints ?? undefined,
5425
+ priorityHint: task.priority ?? undefined,
5426
+ dependsOnKeys: dependencyKeysByTaskId.get(task.id) ?? [],
5427
+ relatedDocs: normalizeRelatedDocs(metadata.doc_links),
5428
+ unitTests: normalizeStringArray(testRequirements.unit),
5429
+ componentTests: normalizeStringArray(testRequirements.component),
5430
+ integrationTests: normalizeStringArray(testRequirements.integration),
5431
+ apiTests: normalizeStringArray(testRequirements.api),
5432
+ qa: isPlainObject(metadata.qa) ? metadata.qa : undefined,
5433
+ };
5434
+ }),
5435
+ };
5436
+ }
5437
+ async writePlanArtifacts(projectKey, plan, docSummary, docs, buildPlan, serviceCatalog, options) {
3369
5438
  const baseDir = path.join(this.workspace.mcodaDir, "tasks", projectKey);
3370
5439
  await fs.mkdir(baseDir, { recursive: true });
3371
5440
  const releaseLock = await this.acquirePlanArtifactLock(baseDir);
3372
5441
  try {
5442
+ this.assertCanonicalNameConsistency(projectKey, docs, plan);
3373
5443
  const write = async (file, data) => {
3374
5444
  const target = path.join(baseDir, file);
3375
5445
  await this.writeJsonArtifactAtomic(target, data);
@@ -3387,7 +5457,12 @@ export class CreateTasksService {
3387
5457
  await write("epics.json", plan.epics);
3388
5458
  await write("stories.json", plan.stories);
3389
5459
  await write("tasks.json", plan.tasks);
3390
- await write("coverage-report.json", this.buildSdsCoverageReport(projectKey, docs, plan));
5460
+ this.assertPlanningArtifactConsistency(projectKey, buildPlan, serviceCatalog);
5461
+ const coverageReport = this.buildSdsCoverageReport(projectKey, docs, plan, options?.existingCoverageAnchors ?? new Set());
5462
+ if (options?.expectedCoverage) {
5463
+ this.assertCoverageConsistency(projectKey, coverageReport, options.expectedCoverage);
5464
+ }
5465
+ await write("coverage-report.json", coverageReport);
3391
5466
  }
3392
5467
  finally {
3393
5468
  await releaseLock();
@@ -3656,6 +5731,8 @@ export class CreateTasksService {
3656
5731
  });
3657
5732
  let sdsPreflight;
3658
5733
  let sdsPreflightError;
5734
+ let sdsPreflightBlockingReasons = [];
5735
+ let continueAfterSdsPreflightWarnings = false;
3659
5736
  if (this.sdsPreflightFactory) {
3660
5737
  let sdsPreflightCloseError;
3661
5738
  try {
@@ -3667,7 +5744,7 @@ export class CreateTasksService {
3667
5744
  inputPaths: options.inputs,
3668
5745
  sdsPaths: options.inputs,
3669
5746
  writeArtifacts: true,
3670
- applyToSds: true,
5747
+ applyToSds: options.sdsPreflightApplyToSds === true,
3671
5748
  commitAppliedChanges: options.sdsPreflightCommit === true,
3672
5749
  commitMessage: options.sdsPreflightCommitMessage,
3673
5750
  });
@@ -3727,12 +5804,15 @@ export class CreateTasksService {
3727
5804
  }
3728
5805
  if (blockingReasons.length > 0) {
3729
5806
  sdsPreflightError = blockingReasons.join(" ");
5807
+ sdsPreflightBlockingReasons = [...blockingReasons];
5808
+ continueAfterSdsPreflightWarnings = true;
5809
+ await this.jobService.appendLog(job.id, `SDS preflight reported planning warnings but create-tasks will continue with remediation context: ${blockingReasons.join(" ")} Report: ${sdsPreflight.reportPath}\n`);
3730
5810
  }
3731
5811
  await this.jobService.writeCheckpoint(job.id, {
3732
5812
  stage: "sds_preflight",
3733
5813
  timestamp: new Date().toISOString(),
3734
5814
  details: {
3735
- status: blockingReasons.length > 0 ? "blocked" : "succeeded",
5815
+ status: blockingReasons.length > 0 ? "continued_with_warnings" : "succeeded",
3736
5816
  error: sdsPreflightError,
3737
5817
  readyForPlanning: sdsPreflight.readyForPlanning,
3738
5818
  qualityStatus: sdsPreflight.qualityStatus,
@@ -3747,28 +5827,46 @@ export class CreateTasksService {
3747
5827
  appliedToSds: sdsPreflight.appliedToSds,
3748
5828
  appliedSdsCount: sdsPreflight.appliedSdsPaths.length,
3749
5829
  commitHash: sdsPreflight.commitHash,
5830
+ blockingReasons,
5831
+ continuedWithWarnings: continueAfterSdsPreflightWarnings,
3750
5832
  warnings: preflightWarnings,
3751
5833
  },
3752
5834
  });
3753
- if (blockingReasons.length > 0) {
3754
- throw new Error(`create-tasks blocked by SDS preflight. ${blockingReasons.join(" ")} Report: ${sdsPreflight.reportPath}`);
3755
- }
3756
5835
  }
3757
- const preflightDocInputs = this.mergeDocInputs(options.inputs, sdsPreflight ? [...sdsPreflight.sourceSdsPaths, ...sdsPreflight.generatedDocPaths] : []);
5836
+ const preflightGeneratedDocInputs = sdsPreflight && (!sdsPreflight.appliedToSds || continueAfterSdsPreflightWarnings)
5837
+ ? sdsPreflight.generatedDocPaths
5838
+ : [];
5839
+ const preflightDocInputs = this.mergeDocInputs(options.inputs, sdsPreflight ? [...sdsPreflight.sourceSdsPaths, ...preflightGeneratedDocInputs] : []);
3758
5840
  const docs = await this.prepareDocs(preflightDocInputs);
3759
5841
  const { docSummary, warnings: indexedDocWarnings } = this.buildDocContext(docs);
3760
5842
  const docWarnings = uniqueStrings([...(sdsPreflight?.warnings ?? []), ...indexedDocWarnings]);
3761
- const discoveryGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
3762
- const serviceCatalog = this.buildServiceCatalogArtifact(options.projectKey, docs, discoveryGraph);
3763
- const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
3764
- const projectBuildPlan = this.buildProjectPlanArtifact(options.projectKey, docs, discoveryGraph, projectBuildMethod);
3765
- const { prompt } = this.buildPrompt(options.projectKey, docs, projectBuildMethod, serviceCatalog, options);
5843
+ const sourceTopologyExpectation = this.buildSourceTopologyExpectation(docs);
5844
+ const initialArtifacts = this.derivePlanningArtifacts(options.projectKey, docs, { epics: [], stories: [], tasks: [] }, sourceTopologyExpectation);
5845
+ const { discoveryGraph, topologySignals, serviceCatalog, projectBuildMethod, projectBuildPlan } = initialArtifacts;
5846
+ const sdsDrivenPlan = this.buildSdsDrivenPlan(options.projectKey, docs, serviceCatalog, discoveryGraph);
5847
+ const deterministicFallbackPlan = this.hasStrongSdsPlanningEvidence(docs, serviceCatalog, sourceTopologyExpectation)
5848
+ ? sdsDrivenPlan
5849
+ : this.materializePlanFromSeed(this.fallbackPlan(options.projectKey, docs), {
5850
+ maxEpics: options.maxEpics,
5851
+ maxStoriesPerEpic: options.maxStoriesPerEpic,
5852
+ maxTasksPerStory: options.maxTasksPerStory,
5853
+ });
5854
+ const { prompt } = this.buildPrompt(options.projectKey, docSummary, projectBuildMethod, serviceCatalog, options);
3766
5855
  const qaPreflight = await this.buildQaPreflight();
3767
5856
  const qaOverrides = this.buildQaOverrides(options);
3768
5857
  await this.jobService.writeCheckpoint(job.id, {
3769
5858
  stage: "docs_indexed",
3770
5859
  timestamp: new Date().toISOString(),
3771
- details: { count: docs.length, warnings: docWarnings, startupWaves: discoveryGraph.startupWaves.slice(0, 8) },
5860
+ details: {
5861
+ count: docs.length,
5862
+ warnings: docWarnings,
5863
+ startupWaves: discoveryGraph.startupWaves.slice(0, 8),
5864
+ topologySignals: {
5865
+ structureServices: topologySignals.structureServices.slice(0, 8),
5866
+ topologyHeadings: topologySignals.topologyHeadings.slice(0, 8),
5867
+ waveMentions: topologySignals.waveMentions.slice(0, 4),
5868
+ },
5869
+ },
3772
5870
  });
3773
5871
  await this.jobService.writeCheckpoint(job.id, {
3774
5872
  stage: "build_plan_defined",
@@ -3814,7 +5912,7 @@ export class CreateTasksService {
3814
5912
  timestamp: new Date().toISOString(),
3815
5913
  details: { epics: epics.length, source: "agent" },
3816
5914
  });
3817
- plan = await this.generatePlanFromAgent(epics, agent, docSummary, {
5915
+ plan = await this.generatePlanFromAgent(options.projectKey, epics, agent, docSummary, {
3818
5916
  agentStream,
3819
5917
  jobId: job.id,
3820
5918
  commandRunId: commandRun.id,
@@ -3829,38 +5927,41 @@ export class CreateTasksService {
3829
5927
  /unknown service ids|phase-0 service references/i.test(fallbackReason)) {
3830
5928
  throw error;
3831
5929
  }
3832
- planSource = "fallback";
3833
- await this.jobService.appendLog(job.id, `Agent planning failed, using deterministic fallback plan: ${fallbackReason}\n`);
3834
- plan = this.materializePlanFromSeed(this.fallbackPlan(options.projectKey, docs), {
3835
- maxEpics: options.maxEpics,
3836
- maxStoriesPerEpic: options.maxStoriesPerEpic,
3837
- maxTasksPerStory: options.maxTasksPerStory,
3838
- });
5930
+ await this.jobService.appendLog(job.id, `Agent planning failed, using deterministic planner fallback: ${fallbackReason}\n`);
5931
+ planSource = deterministicFallbackPlan === sdsDrivenPlan ? "sds" : "fallback";
5932
+ plan = deterministicFallbackPlan;
3839
5933
  await this.jobService.writeCheckpoint(job.id, {
3840
5934
  stage: "epics_generated",
3841
5935
  timestamp: new Date().toISOString(),
3842
5936
  details: { epics: plan.epics.length, source: planSource, reason: fallbackReason },
3843
5937
  });
3844
5938
  }
3845
- const normalizedPlanEpics = this.alignEpicsToServiceCatalog(plan.epics, serviceCatalog, unknownEpicServicePolicy);
3846
- for (const warning of normalizedPlanEpics.warnings) {
3847
- await this.jobService.appendLog(job.id, `[create-tasks] ${warning}\n`);
5939
+ plan = await this.normalizeGeneratedPlan({
5940
+ plan,
5941
+ docs,
5942
+ serviceCatalog,
5943
+ sourceTopologyExpectation,
5944
+ unknownEpicServicePolicy,
5945
+ jobId: job.id,
5946
+ });
5947
+ if (this.planLooksTooWeakForSds(plan, docs, serviceCatalog, sourceTopologyExpectation)) {
5948
+ fallbackReason = [
5949
+ fallbackReason,
5950
+ `generated backlog was too weak for SDS-first acceptance (epics=${plan.epics.length}, tasks=${plan.tasks.length})`,
5951
+ ]
5952
+ .filter(Boolean)
5953
+ .join("; ");
5954
+ planSource = "sds";
5955
+ plan = await this.normalizeGeneratedPlan({
5956
+ plan: sdsDrivenPlan,
5957
+ docs,
5958
+ serviceCatalog,
5959
+ sourceTopologyExpectation,
5960
+ unknownEpicServicePolicy,
5961
+ jobId: job.id,
5962
+ });
5963
+ await this.jobService.appendLog(job.id, `create-tasks replaced the weak generated backlog with the SDS-first deterministic plan. Reason: ${fallbackReason}\n`);
3848
5964
  }
3849
- plan = {
3850
- ...plan,
3851
- epics: normalizedPlanEpics.epics.map((epic, index) => ({
3852
- ...epic,
3853
- localId: epic.localId ?? `e${index + 1}`,
3854
- stories: [],
3855
- })),
3856
- };
3857
- plan = this.enforceStoryScopedDependencies(plan);
3858
- plan = this.injectStructureBootstrapPlan(plan, docs, options.projectKey);
3859
- plan = this.enforceStoryScopedDependencies(plan);
3860
- this.validatePlanLocalIdentifiers(plan);
3861
- plan = this.applyServiceDependencySequencing(plan, docs);
3862
- plan = this.enforceStoryScopedDependencies(plan);
3863
- this.validatePlanLocalIdentifiers(plan);
3864
5965
  await this.jobService.writeCheckpoint(job.id, {
3865
5966
  stage: "stories_generated",
3866
5967
  timestamp: new Date().toISOString(),
@@ -3886,91 +5987,185 @@ export class CreateTasksService {
3886
5987
  await this.seedPriorities(options.projectKey);
3887
5988
  let sufficiencyAudit;
3888
5989
  let sufficiencyAuditError;
3889
- if (this.taskSufficiencyFactory) {
3890
- let sufficiencyCloseError;
3891
- try {
3892
- const sufficiencyService = await this.taskSufficiencyFactory(this.workspace);
3893
- try {
3894
- sufficiencyAudit = await sufficiencyService.runAudit({
3895
- workspace: options.workspace,
3896
- projectKey: options.projectKey,
3897
- sourceCommand: "create-tasks",
3898
- });
3899
- }
3900
- finally {
3901
- try {
3902
- await sufficiencyService.close();
3903
- }
3904
- catch (closeError) {
3905
- sufficiencyCloseError = closeError?.message ?? String(closeError);
3906
- await this.jobService.appendLog(job.id, `Task sufficiency audit close warning: ${sufficiencyCloseError}\n`);
3907
- }
3908
- }
3909
- }
3910
- catch (error) {
3911
- sufficiencyAuditError = error?.message ?? String(error);
3912
- }
5990
+ const sufficiencyWarnings = [];
5991
+ let refinementAttempt = 0;
5992
+ while (true) {
5993
+ const auditResult = await this.runTaskSufficiencyAudit({
5994
+ workspace: options.workspace,
5995
+ projectKey: options.projectKey,
5996
+ sourceCommand: "create-tasks",
5997
+ dryRun: true,
5998
+ jobId: job.id,
5999
+ });
6000
+ sufficiencyAudit = auditResult.audit;
6001
+ sufficiencyAuditError = auditResult.error;
6002
+ sufficiencyWarnings.push(...auditResult.warnings);
3913
6003
  if (!sufficiencyAudit) {
3914
- const message = `create-tasks blocked: task sufficiency audit failed (${sufficiencyAuditError ?? "unknown error"}).`;
3915
- await this.jobService.writeCheckpoint(job.id, {
3916
- stage: "task_sufficiency_audit",
3917
- timestamp: new Date().toISOString(),
3918
- details: {
3919
- status: "failed",
3920
- error: message,
3921
- jobId: undefined,
3922
- commandRunId: undefined,
3923
- satisfied: false,
3924
- dryRun: undefined,
3925
- totalTasksAdded: undefined,
3926
- totalTasksUpdated: undefined,
3927
- finalCoverageRatio: undefined,
3928
- reportPath: undefined,
3929
- remainingSectionCount: undefined,
3930
- remainingFolderCount: undefined,
3931
- remainingGapCount: undefined,
3932
- warnings: [],
3933
- },
3934
- });
3935
- throw new Error(message);
6004
+ break;
3936
6005
  }
3937
- const sufficiencyWarnings = uniqueStrings([
3938
- ...(sufficiencyAudit.warnings ?? []),
3939
- ...(sufficiencyCloseError ? [`Task sufficiency audit close warning: ${sufficiencyCloseError}`] : []),
3940
- ]);
3941
- if (!sufficiencyAudit.satisfied) {
3942
- sufficiencyAuditError = `SDS coverage target not reached (coverage=${sufficiencyAudit.finalCoverageRatio}, remaining gaps=${sufficiencyAudit.remainingGaps.total}).`;
6006
+ if (sufficiencyAudit.satisfied) {
6007
+ sufficiencyAuditError = undefined;
6008
+ break;
3943
6009
  }
6010
+ const refinementReasons = [
6011
+ `SDS coverage target not reached (coverage=${sufficiencyAudit.finalCoverageRatio}, remaining gaps=${sufficiencyAudit.remainingGaps.total}).`,
6012
+ ...(sufficiencyAudit.remainingSectionHeadings.length > 0
6013
+ ? [
6014
+ `Remaining section headings: ${sufficiencyAudit.remainingSectionHeadings.slice(0, 12).join(", ")}`,
6015
+ ]
6016
+ : []),
6017
+ ...(sufficiencyAudit.remainingFolderEntries.length > 0
6018
+ ? [
6019
+ `Remaining folder entries: ${sufficiencyAudit.remainingFolderEntries.slice(0, 12).join(", ")}`,
6020
+ ]
6021
+ : []),
6022
+ ];
6023
+ if (!agent || refinementAttempt >= CreateTasksService.MAX_AGENT_REFINEMENT_ATTEMPTS) {
6024
+ sufficiencyAuditError = refinementReasons[0];
6025
+ break;
6026
+ }
6027
+ refinementAttempt += 1;
3944
6028
  await this.jobService.writeCheckpoint(job.id, {
3945
- stage: "task_sufficiency_audit",
6029
+ stage: "backlog_refinement",
3946
6030
  timestamp: new Date().toISOString(),
3947
6031
  details: {
3948
- status: sufficiencyAudit.satisfied ? "succeeded" : "blocked",
3949
- error: sufficiencyAuditError,
3950
- jobId: sufficiencyAudit.jobId,
3951
- commandRunId: sufficiencyAudit.commandRunId,
3952
- satisfied: sufficiencyAudit.satisfied,
3953
- dryRun: sufficiencyAudit.dryRun,
3954
- totalTasksAdded: sufficiencyAudit.totalTasksAdded,
3955
- totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
3956
- finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
3957
- reportPath: sufficiencyAudit.reportPath,
6032
+ iteration: refinementAttempt,
6033
+ remainingGapCount: sufficiencyAudit.remainingGaps.total,
3958
6034
  remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
3959
6035
  remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
3960
- remainingGapCount: sufficiencyAudit.remainingGaps.total,
3961
- warnings: sufficiencyWarnings,
6036
+ plannedGapBundleCount: sufficiencyAudit.plannedGapBundles.length,
6037
+ unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
3962
6038
  },
3963
6039
  });
3964
- if (!sufficiencyAudit.satisfied) {
3965
- throw new Error(`create-tasks blocked: task sufficiency audit did not reach full coverage. Report: ${sufficiencyAudit.reportPath}`);
6040
+ try {
6041
+ plan = await this.refinePlanWithAgent({
6042
+ agent,
6043
+ currentPlan: plan,
6044
+ audit: sufficiencyAudit,
6045
+ reasons: refinementReasons,
6046
+ docs,
6047
+ docSummary,
6048
+ projectKey: options.projectKey,
6049
+ projectBuildMethod,
6050
+ serviceCatalog,
6051
+ sourceTopologyExpectation,
6052
+ unknownEpicServicePolicy,
6053
+ options,
6054
+ agentStream,
6055
+ jobId: job.id,
6056
+ commandRunId: commandRun.id,
6057
+ iteration: refinementAttempt,
6058
+ });
6059
+ planSource = "agent";
6060
+ await this.persistPlanToDb(project.id, options.projectKey, plan, job.id, commandRun.id, {
6061
+ force: true,
6062
+ resetKeys: true,
6063
+ qaPreflight,
6064
+ qaOverrides,
6065
+ });
6066
+ await this.seedPriorities(options.projectKey);
6067
+ await this.jobService.appendLog(job.id, `create-tasks refinement iteration ${refinementAttempt} replaced the backlog with ${plan.epics.length} epics, ${plan.stories.length} stories, and ${plan.tasks.length} tasks.\n`);
3966
6068
  }
6069
+ catch (error) {
6070
+ const message = error?.message ?? String(error);
6071
+ await this.jobService.appendLog(job.id, `create-tasks refinement iteration ${refinementAttempt} failed: ${message}\n`);
6072
+ sufficiencyAuditError = refinementReasons[0];
6073
+ if (refinementAttempt >= CreateTasksService.MAX_AGENT_REFINEMENT_ATTEMPTS) {
6074
+ break;
6075
+ }
6076
+ }
6077
+ }
6078
+ if (!sufficiencyAudit) {
6079
+ const message = `create-tasks blocked: task sufficiency audit failed (${sufficiencyAuditError ?? "unknown error"}).`;
6080
+ await this.jobService.writeCheckpoint(job.id, {
6081
+ stage: "task_sufficiency_audit",
6082
+ timestamp: new Date().toISOString(),
6083
+ details: {
6084
+ status: "failed",
6085
+ error: message,
6086
+ jobId: undefined,
6087
+ commandRunId: undefined,
6088
+ satisfied: false,
6089
+ dryRun: undefined,
6090
+ totalTasksAdded: undefined,
6091
+ totalTasksUpdated: undefined,
6092
+ finalCoverageRatio: undefined,
6093
+ reportPath: undefined,
6094
+ remainingSectionCount: undefined,
6095
+ remainingFolderCount: undefined,
6096
+ remainingGapCount: undefined,
6097
+ plannedGapBundleCount: undefined,
6098
+ warnings: [],
6099
+ },
6100
+ });
6101
+ throw new Error(message);
6102
+ }
6103
+ const uniqueSufficiencyWarnings = uniqueStrings(sufficiencyWarnings);
6104
+ sufficiencyAudit = {
6105
+ ...sufficiencyAudit,
6106
+ warnings: uniqueSufficiencyWarnings,
6107
+ };
6108
+ if (!sufficiencyAudit.satisfied) {
6109
+ sufficiencyAuditError = `SDS coverage target not reached (coverage=${sufficiencyAudit.finalCoverageRatio}, remaining gaps=${sufficiencyAudit.remainingGaps.total}).`;
6110
+ }
6111
+ await this.jobService.writeCheckpoint(job.id, {
6112
+ stage: "task_sufficiency_audit",
6113
+ timestamp: new Date().toISOString(),
6114
+ details: {
6115
+ status: sufficiencyAudit.satisfied ? "succeeded" : "blocked",
6116
+ error: sufficiencyAuditError,
6117
+ jobId: sufficiencyAudit.jobId,
6118
+ commandRunId: sufficiencyAudit.commandRunId,
6119
+ satisfied: sufficiencyAudit.satisfied,
6120
+ dryRun: sufficiencyAudit.dryRun,
6121
+ totalTasksAdded: sufficiencyAudit.totalTasksAdded,
6122
+ totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
6123
+ finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
6124
+ reportPath: sufficiencyAudit.reportPath,
6125
+ remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
6126
+ remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
6127
+ remainingGapCount: sufficiencyAudit.remainingGaps.total,
6128
+ plannedGapBundleCount: sufficiencyAudit.plannedGapBundles.length,
6129
+ unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
6130
+ warnings: uniqueSufficiencyWarnings,
6131
+ },
6132
+ });
6133
+ if ((sufficiencyAudit?.totalTasksAdded ?? 0) > 0) {
6134
+ await this.seedPriorities(options.projectKey);
6135
+ }
6136
+ const finalBacklog = await this.loadPersistedBacklog(project.id);
6137
+ const finalPlan = this.buildPlanFromPersistedBacklog(finalBacklog);
6138
+ const finalArtifacts = this.derivePlanningArtifacts(options.projectKey, docs, finalPlan, sourceTopologyExpectation);
6139
+ const finalCoverageAnchors = this.collectCoverageAnchorsFromBacklog(finalBacklog);
6140
+ const expectedCoverage = await this.loadExpectedCoverageFromSufficiencyReport(sufficiencyAudit?.reportPath);
6141
+ await this.writePlanArtifacts(options.projectKey, finalPlan, docSummary, docs, finalArtifacts.projectBuildPlan, finalArtifacts.serviceCatalog, {
6142
+ existingCoverageAnchors: finalCoverageAnchors,
6143
+ expectedCoverage,
6144
+ });
6145
+ await this.jobService.writeCheckpoint(job.id, {
6146
+ stage: "plan_refreshed",
6147
+ timestamp: new Date().toISOString(),
6148
+ details: {
6149
+ folder,
6150
+ epics: finalBacklog.epics.length,
6151
+ stories: finalBacklog.stories.length,
6152
+ tasks: finalBacklog.tasks.length,
6153
+ dependencies: finalBacklog.dependencies.length,
6154
+ services: finalArtifacts.serviceCatalog.services.length,
6155
+ startupWaves: finalArtifacts.projectBuildPlan.startupWaves.length,
6156
+ acceptedWithResidualSectionGaps: false,
6157
+ },
6158
+ });
6159
+ const acceptedWithResidualSectionGaps = false;
6160
+ if (sufficiencyAudit && !sufficiencyAudit.satisfied) {
6161
+ throw new Error(`create-tasks blocked: task sufficiency audit did not reach full coverage. Report: ${sufficiencyAudit.reportPath}`);
3967
6162
  }
3968
6163
  await this.jobService.updateJobStatus(job.id, "completed", {
3969
6164
  payload: {
3970
- epicsCreated: epicRows.length,
3971
- storiesCreated: storyRows.length,
3972
- tasksCreated: taskRows.length,
3973
- dependenciesCreated: dependencyRows.length,
6165
+ epicsCreated: finalBacklog.epics.length,
6166
+ storiesCreated: finalBacklog.stories.length,
6167
+ tasksCreated: finalBacklog.tasks.length,
6168
+ dependenciesCreated: finalBacklog.dependencies.length,
3974
6169
  docs: docSummary,
3975
6170
  planFolder: folder,
3976
6171
  planSource,
@@ -3990,6 +6185,8 @@ export class CreateTasksService {
3990
6185
  reportPath: sdsPreflight.reportPath,
3991
6186
  openQuestionsPath: sdsPreflight.openQuestionsPath,
3992
6187
  gapAddendumPath: sdsPreflight.gapAddendumPath,
6188
+ blockingReasons: sdsPreflightBlockingReasons,
6189
+ continuedWithWarnings: continueAfterSdsPreflightWarnings,
3993
6190
  warnings: sdsPreflight.warnings,
3994
6191
  }
3995
6192
  : undefined,
@@ -4006,6 +6203,8 @@ export class CreateTasksService {
4006
6203
  remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
4007
6204
  remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
4008
6205
  remainingGapCount: sufficiencyAudit.remainingGaps.total,
6206
+ unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
6207
+ acceptedWithResidualSectionGaps,
4009
6208
  warnings: sufficiencyAudit.warnings,
4010
6209
  }
4011
6210
  : undefined,
@@ -4037,10 +6236,10 @@ export class CreateTasksService {
4037
6236
  return {
4038
6237
  jobId: job.id,
4039
6238
  commandRunId: commandRun.id,
4040
- epics: epicRows,
4041
- stories: storyRows,
4042
- tasks: taskRows,
4043
- dependencies: dependencyRows,
6239
+ epics: finalBacklog.epics,
6240
+ stories: finalBacklog.stories,
6241
+ tasks: finalBacklog.tasks,
6242
+ dependencies: finalBacklog.dependencies,
4044
6243
  };
4045
6244
  }
4046
6245
  catch (error) {
@@ -4203,3 +6402,4 @@ export class CreateTasksService {
4203
6402
  }
4204
6403
  CreateTasksService.MAX_BUSY_RETRIES = 6;
4205
6404
  CreateTasksService.BUSY_BACKOFF_MS = 500;
6405
+ CreateTasksService.MAX_AGENT_REFINEMENT_ATTEMPTS = 3;