@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.
- package/dist/api/AgentsApi.d.ts +4 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +4 -1
- package/dist/prompts/PdrPrompts.js +1 -1
- package/dist/services/docs/DocsService.d.ts +37 -0
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +537 -2
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -1
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +13 -2
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -1
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +12 -1
- package/dist/services/planning/CreateTasksService.d.ts +57 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +2491 -291
- package/dist/services/planning/SdsCoverageModel.d.ts +27 -0
- package/dist/services/planning/SdsCoverageModel.d.ts.map +1 -0
- package/dist/services/planning/SdsCoverageModel.js +138 -0
- package/dist/services/planning/SdsPreflightService.d.ts +2 -0
- package/dist/services/planning/SdsPreflightService.d.ts.map +1 -1
- package/dist/services/planning/SdsPreflightService.js +131 -37
- package/dist/services/planning/SdsStructureSignals.d.ts +24 -0
- package/dist/services/planning/SdsStructureSignals.d.ts.map +1 -0
- package/dist/services/planning/SdsStructureSignals.js +402 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts +17 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
- package/dist/services/planning/TaskSufficiencyService.js +409 -278
- 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 {
|
|
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
|
|
194
|
+
const normalized = value
|
|
193
195
|
.toLowerCase()
|
|
194
|
-
.
|
|
195
|
-
.
|
|
196
|
-
.
|
|
197
|
-
|
|
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
|
|
578
|
-
: "- Documentation impact is reviewed and no additional
|
|
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
|
|
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": "
|
|
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 =
|
|
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
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
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 = [
|
|
1194
|
-
for (const
|
|
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 (
|
|
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 &&
|
|
1605
|
+
while (idx < parts.length - 1 && this.isLikelyServiceContainerSegment(parts, idx)) {
|
|
1251
1606
|
idx += 1;
|
|
1252
1607
|
}
|
|
1253
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
|
1893
|
-
|
|
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
|
|
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
|
-
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
|
|
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) ?? "
|
|
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) =>
|
|
2572
|
-
.
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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
|
-
|
|
2658
|
-
|
|
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
|
-
|
|
2669
|
-
|
|
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
|
|
4554
|
+
title: "Integrate core dependencies and interfaces",
|
|
2731
4555
|
type: "feature",
|
|
2732
|
-
description: "Wire key
|
|
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
|
|
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
|
|
4990
|
+
title: `Integrate dependencies for ${story.title}`,
|
|
3161
4991
|
type: "feature",
|
|
3162
4992
|
description: [
|
|
3163
|
-
`Integrate dependent
|
|
3164
|
-
"Align internal/external interfaces, data
|
|
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
|
-
|
|
3265
|
-
|
|
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
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
.
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
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
|
-
|
|
3297
|
-
|
|
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
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ? "
|
|
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
|
|
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
|
|
3762
|
-
const
|
|
3763
|
-
const projectBuildMethod =
|
|
3764
|
-
const
|
|
3765
|
-
const
|
|
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: {
|
|
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
|
-
|
|
3833
|
-
|
|
3834
|
-
plan =
|
|
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
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
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
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
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: "
|
|
6029
|
+
stage: "backlog_refinement",
|
|
3946
6030
|
timestamp: new Date().toISOString(),
|
|
3947
6031
|
details: {
|
|
3948
|
-
|
|
3949
|
-
|
|
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
|
-
|
|
3961
|
-
|
|
6036
|
+
plannedGapBundleCount: sufficiencyAudit.plannedGapBundles.length,
|
|
6037
|
+
unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
|
|
3962
6038
|
},
|
|
3963
6039
|
});
|
|
3964
|
-
|
|
3965
|
-
|
|
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:
|
|
3971
|
-
storiesCreated:
|
|
3972
|
-
tasksCreated:
|
|
3973
|
-
dependenciesCreated:
|
|
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:
|
|
4041
|
-
stories:
|
|
4042
|
-
tasks:
|
|
4043
|
-
dependencies:
|
|
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;
|