@namewta/speculo 0.8.6 → 0.8.8
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/src/structured.js +8 -4
- package/dist/src/structured.js.map +1 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-goal-plan.md +67 -20
- package/template/canonical/canonical-specdev-grill-with-docs.md +17 -9
- package/template/canonical/canonical-specdev-orchestrate-implementation.md +2784 -0
- package/template/canonical/canonical-specdev-spec.md +19 -9
- package/template/canonical/canonical-specdev-tickets.md +58 -16
- package/template/canonical/canonical-specdev-wayfinder.md +11 -11
- package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +1 -1
- package/template/workflows/specdev/I-implement/I-implement.md +10 -5
- package/template/workflows/specdev/I-implement/execution-preflight.md +2 -0
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +1 -1
- package/template/workflows/specdev/I-init-setup/config-template.json +2 -2
- package/template/workflows/specdev/O-orchestrate-implementation/O-orchestrate-implementation.md +129 -0
- package/template/workflows/specdev/O-orchestrate-implementation/conflict-and-drift.md +18 -0
- package/template/workflows/specdev/O-orchestrate-implementation/execution-loop.md +30 -0
- package/template/workflows/specdev/O-orchestrate-implementation/implementation-evidence-template.md +39 -0
- package/template/workflows/specdev/O-orchestrate-implementation/implementation-map-template.md +50 -0
- package/template/workflows/specdev/O-orchestrate-implementation/implementation-plan-template.md +61 -0
- package/template/workflows/specdev/O-orchestrate-implementation/input-readiness.md +25 -0
- package/template/workflows/specdev/O-orchestrate-implementation/super-dag.md +27 -0
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +1 -1
- package/template/workflows/specdev/P-prototype/P-prototype.md +30 -26
- package/template/workflows/specdev/P-prototype/design-library/INDEX.md +43 -0
- package/template/workflows/specdev/P-prototype/design-library/color-and-theme.md +65 -0
- package/template/workflows/specdev/P-prototype/design-library/foundations.md +66 -0
- package/template/workflows/specdev/P-prototype/design-library/interaction-patterns.md +61 -0
- package/template/workflows/specdev/P-prototype/design-library/product-pattern-index.md +73 -0
- package/template/workflows/specdev/P-prototype/design-library/research-provenance.md +54 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/METHODOLOGY.md +90 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/README.md +31 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/extended-projects.json +241 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/projects.json +253 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/USAGE.md +40 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/design-tokens.css +145 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/report/OPEN_SOURCE_UI_RESEARCH_2026.md +519 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/PROJECT_TEMPLATE.md +29 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/README.md +9 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/claude-code-modern-clients.md +407 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/creative-ai-communication.md +574 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/data-dev-tools.md +432 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/personal-multiplatform-apps.md +185 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/productivity-collaboration.md +398 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/01-dense-ide.html +68 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/02-monochrome-console.html +18 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/03-soft-personal-ai.html +17 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/04-responsive-web.html +14 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/05-mobile-supervisor.html +16 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/06-cross-platform-workspace.html +17 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/07-local-first-content.html +9 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/08-media-first.html +17 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/README.md +49 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/ATTRIBUTION.md +9 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/landscape.jpg +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/lucide.js +20494 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/mountain.jpg +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/workspace.jpg +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/index.html +70 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package-lock.json +78 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package.json +12 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/screenshots/index-desktop.png +0 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/scripts/gallery.js +134 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/base.css +795 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/pages.css +533 -0
- package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/tests/gallery.spec.js +56 -0
- package/template/workflows/specdev/P-prototype/design-library/responsive-and-platforms.md +53 -0
- package/template/workflows/specdev/P-prototype/design-library/style-index.md +32 -0
- package/template/workflows/specdev/P-prototype/design-package.schema.json +70 -0
- package/template/workflows/specdev/P-prototype/design-system-template.md +366 -0
- package/template/workflows/specdev/P-prototype/detect-existing-style.md +53 -0
- package/template/workflows/specdev/P-prototype/generate-design-package.md +50 -0
- package/template/workflows/specdev/P-prototype/style-selection-protocol.md +41 -0
- package/template/workflows/specdev/P-prototype/tools/materialize-prototype.mjs +126 -0
- package/template/workflows/specdev/P-prototype/tools/validate-design-package.mjs +155 -0
- package/template/workflows/specdev/README.md +28 -15
- package/template/workflows/specdev/T-triage/T-triage.md +1 -1
- package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +2 -2
- package/template/workflows/specdev/W-wayfinder/local-tracker-contract.md +3 -4
- package/template/workflows/specdev/common/README.md +6 -3
- package/template/workflows/specdev/common/rules/artifact-contract.md +9 -3
- package/template/workflows/specdev/common/rules/change-completion.md +3 -0
- package/template/workflows/specdev/common/rules/deviation-control.md +2 -0
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +2 -0
- package/template/workflows/specdev/common/rules/parent-implementation-orchestration.md +27 -0
- package/template/workflows/specdev/common/rules/path-ownership.md +3 -1
- package/template/workflows/specdev/common/schemas/config.schema.json +4 -4
- package/template/workflows/specdev/common/schemas/implementation-map.schema.json +40 -0
- package/template/workflows/specdev/common/schemas/implementation-plan.schema.json +44 -0
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +4 -4
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +1 -3
- package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +5 -3
- package/template/workflows/specdev/common/tools/README.md +2 -2
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +572 -30
- package/template/workflows/specdev/P-prototype/logic-prototype.md +0 -24
- package/template/workflows/specdev/P-prototype/prototype-record-template.md +0 -46
- package/template/workflows/specdev/P-prototype/ui-prototype.md +0 -21
- package/template/workflows/specdev/common/schemas/prototype-record.schema.json +0 -24
|
@@ -22,6 +22,8 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
const DOMAIN_SCHEMA_VERSION = 3;
|
|
23
23
|
const CONFIG_SCHEMA_VERSION = 5;
|
|
24
24
|
const GOAL_PLAN_SCHEMA_VERSION = 6;
|
|
25
|
+
const IMPLEMENTATION_MAP_SCHEMA_VERSION = 1;
|
|
26
|
+
const IMPLEMENTATION_PLAN_SCHEMA_VERSION = 1;
|
|
25
27
|
const CHANGE_STATUS_SCHEMA_VERSION = 6;
|
|
26
28
|
const GLOBAL_STATUS_SCHEMA_VERSION = 5;
|
|
27
29
|
const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
|
|
@@ -29,6 +31,7 @@ const STATE_PREFIX = "{roots.state}/specdev/";
|
|
|
29
31
|
const STATE_ROOT_PREFIX = "{roots.state}/";
|
|
30
32
|
const SKILLS_PREFIX = "{roots.skills}/";
|
|
31
33
|
const COMMANDS_PREFIX = "{roots.commands}/";
|
|
34
|
+
const CHANGE_NAME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
32
35
|
|
|
33
36
|
const EXPECTED_WORKS = new Set([
|
|
34
37
|
"A-archive-and-consolidate",
|
|
@@ -37,6 +40,7 @@ const EXPECTED_WORKS = new Set([
|
|
|
37
40
|
"G-grill-with-docs",
|
|
38
41
|
"I-implement",
|
|
39
42
|
"I-init-setup",
|
|
43
|
+
"O-orchestrate-implementation",
|
|
40
44
|
"P-goal-plan",
|
|
41
45
|
"P-prototype",
|
|
42
46
|
"R-review-architecture",
|
|
@@ -104,6 +108,7 @@ const VALID_STAGES = new Set([
|
|
|
104
108
|
"review",
|
|
105
109
|
"prototype",
|
|
106
110
|
"wayfinder",
|
|
111
|
+
"orchestrate-implementation",
|
|
107
112
|
"complete",
|
|
108
113
|
]);
|
|
109
114
|
const REQUIRED_TICKET_KEYS = new Set([
|
|
@@ -156,6 +161,8 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
156
161
|
"spec.md",
|
|
157
162
|
"tickets-map.md",
|
|
158
163
|
"goal-plan.md",
|
|
164
|
+
"implementation-map.md",
|
|
165
|
+
"implementation-plan.md",
|
|
159
166
|
"ADR.md",
|
|
160
167
|
"CONTEXT.md",
|
|
161
168
|
"LOG.md",
|
|
@@ -168,6 +175,7 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
168
175
|
"architecture-review.html",
|
|
169
176
|
"wayfinder-map.md",
|
|
170
177
|
"design-tree.json",
|
|
178
|
+
"implementation-orchestration.md",
|
|
171
179
|
]);
|
|
172
180
|
const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
|
|
173
181
|
"source-issue.md",
|
|
@@ -465,10 +473,21 @@ function validateDocumentReferences(root) {
|
|
|
465
473
|
const errors = [];
|
|
466
474
|
const warnings = [];
|
|
467
475
|
const entries = walk(root);
|
|
468
|
-
const
|
|
469
|
-
|
|
476
|
+
const frozenResearchRoot = join(
|
|
477
|
+
root,
|
|
478
|
+
"P-prototype",
|
|
479
|
+
"design-library",
|
|
480
|
+
"research-snapshot",
|
|
481
|
+
);
|
|
482
|
+
const governedEntries = entries.filter(
|
|
483
|
+
(path) => path !== frozenResearchRoot && !path.startsWith(`${frozenResearchRoot}${sep}`),
|
|
484
|
+
);
|
|
485
|
+
const knownFiles = new Set(governedEntries.filter(isFile).map((path) => basename(path)));
|
|
486
|
+
const knownDirectories = new Set(governedEntries.filter(isDirectory).map((path) => basename(path)));
|
|
470
487
|
|
|
471
|
-
|
|
488
|
+
// The pinned research snapshot preserves its upstream relative links and HTML
|
|
489
|
+
// asset paths byte-for-byte. Global template link checks still validate it.
|
|
490
|
+
for (const path of governedEntries.filter(
|
|
472
491
|
(item) => isFile(item) && [".md", ".html"].includes(extname(item).toLowerCase()),
|
|
473
492
|
)) {
|
|
474
493
|
const label = toPosix(relative(root, path));
|
|
@@ -663,6 +682,8 @@ function validateExecutionContractAssets(root) {
|
|
|
663
682
|
const configSchemaPath = join(root, "common", "schemas", "config.schema.json");
|
|
664
683
|
const goalPlanSchemaPath = join(root, "common", "schemas", "goal-plan.schema.json");
|
|
665
684
|
const changeSchemaPath = join(root, "common", "schemas", "change-status.schema.json");
|
|
685
|
+
const implementationMapSchemaPath = join(root, "common", "schemas", "implementation-map.schema.json");
|
|
686
|
+
const implementationPlanSchemaPath = join(root, "common", "schemas", "implementation-plan.schema.json");
|
|
666
687
|
|
|
667
688
|
for (const path of [
|
|
668
689
|
configTemplatePath,
|
|
@@ -670,6 +691,8 @@ function validateExecutionContractAssets(root) {
|
|
|
670
691
|
configSchemaPath,
|
|
671
692
|
goalPlanSchemaPath,
|
|
672
693
|
changeSchemaPath,
|
|
694
|
+
implementationMapSchemaPath,
|
|
695
|
+
implementationPlanSchemaPath,
|
|
673
696
|
]) {
|
|
674
697
|
if (!isFile(path)) errors.push(`missing execution contract asset ${toPosix(relative(root, path))}`);
|
|
675
698
|
}
|
|
@@ -682,12 +705,13 @@ function validateExecutionContractAssets(root) {
|
|
|
682
705
|
configTemplate.execution.max_implementation_agents < 1 ||
|
|
683
706
|
!Number.isInteger(configTemplate.execution?.max_integration_attempts) ||
|
|
684
707
|
configTemplate.execution.max_integration_attempts < 1 ||
|
|
685
|
-
!Number.isInteger(configTemplate.planning?.
|
|
686
|
-
!Number.isInteger(configTemplate.planning?.
|
|
687
|
-
configTemplate.planning.
|
|
688
|
-
configTemplate.planning.
|
|
708
|
+
!Number.isInteger(configTemplate.planning?.ui_design_default_candidates) ||
|
|
709
|
+
!Number.isInteger(configTemplate.planning?.ui_design_max_candidates) ||
|
|
710
|
+
configTemplate.planning.ui_design_default_candidates < 2 ||
|
|
711
|
+
configTemplate.planning.ui_design_max_candidates > 4 ||
|
|
712
|
+
configTemplate.planning.ui_design_max_candidates < configTemplate.planning.ui_design_default_candidates
|
|
689
713
|
) {
|
|
690
|
-
errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid
|
|
714
|
+
errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid UI design candidate bounds");
|
|
691
715
|
}
|
|
692
716
|
for (const obsolete of ["auto_commit", "worktree_for_parallel", "max_parallel"]) {
|
|
693
717
|
if (JSON.stringify(configTemplate).includes(`\"${obsolete}\"`)) {
|
|
@@ -741,6 +765,29 @@ function validateExecutionContractAssets(root) {
|
|
|
741
765
|
) {
|
|
742
766
|
errors.push("change-status.schema.json must define the strict Ticket integration v6 contract");
|
|
743
767
|
}
|
|
768
|
+
|
|
769
|
+
const implementationMapSchema = JSON.parse(readText(implementationMapSchemaPath));
|
|
770
|
+
if (
|
|
771
|
+
implementationMapSchema.$id !== "urn:speculo:specdev:implementation-map:v1" ||
|
|
772
|
+
implementationMapSchema.properties?.schema_version?.const !== IMPLEMENTATION_MAP_SCHEMA_VERSION ||
|
|
773
|
+
implementationMapSchema.properties?.members?.minItems !== 2 ||
|
|
774
|
+
implementationMapSchema.properties?.tasks?.minItems !== 1 ||
|
|
775
|
+
implementationMapSchema.additionalProperties !== false
|
|
776
|
+
) {
|
|
777
|
+
errors.push("implementation-map.schema.json must define the strict parent implementation graph contract v1");
|
|
778
|
+
}
|
|
779
|
+
const implementationPlanSchema = JSON.parse(readText(implementationPlanSchemaPath));
|
|
780
|
+
if (
|
|
781
|
+
implementationPlanSchema.$id !== "urn:speculo:specdev:implementation-plan:v1" ||
|
|
782
|
+
implementationPlanSchema.properties?.schema_version?.const !== IMPLEMENTATION_PLAN_SCHEMA_VERSION ||
|
|
783
|
+
implementationPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
|
|
784
|
+
implementationPlanSchema.properties?.implementation_agent_limit?.minimum !== 1 ||
|
|
785
|
+
implementationPlanSchema.properties?.integration_attempt_limit?.minimum !== 1 ||
|
|
786
|
+
!["current", "required"].every((value) => implementationPlanSchema.properties?.ticket_workspace_policy?.enum?.includes(value)) ||
|
|
787
|
+
implementationPlanSchema.additionalProperties !== false
|
|
788
|
+
) {
|
|
789
|
+
errors.push("implementation-plan.schema.json must define the strict parent implementation projection contract v1");
|
|
790
|
+
}
|
|
744
791
|
return errors;
|
|
745
792
|
}
|
|
746
793
|
|
|
@@ -798,6 +845,13 @@ function capabilityChecks(root) {
|
|
|
798
845
|
],
|
|
799
846
|
],
|
|
800
847
|
],
|
|
848
|
+
[
|
|
849
|
+
"orchestrate-implementation",
|
|
850
|
+
[
|
|
851
|
+
join(root, "O-orchestrate-implementation", "O-orchestrate-implementation.md"),
|
|
852
|
+
["Ready Spec", "Ready Tickets", "Implementation Map", "Implementation Plan", "lead-directed", "implementation_agent_limit", "serialization", "I-implement"],
|
|
853
|
+
],
|
|
854
|
+
],
|
|
801
855
|
[
|
|
802
856
|
"implement",
|
|
803
857
|
[
|
|
@@ -816,7 +870,7 @@ function capabilityChecks(root) {
|
|
|
816
870
|
"prototype",
|
|
817
871
|
[
|
|
818
872
|
join(root, "P-prototype", "P-prototype.md"),
|
|
819
|
-
["
|
|
873
|
+
["设计定向", "风格", "design-system.md", "comparison", "HTML/CSS/JS", "design-library/INDEX.md"],
|
|
820
874
|
],
|
|
821
875
|
],
|
|
822
876
|
[
|
|
@@ -885,6 +939,13 @@ function capabilityChecks(root) {
|
|
|
885
939
|
"W-wayfinder/local-tracker-contract.md",
|
|
886
940
|
"W-wayfinder/solution-comment-template.md",
|
|
887
941
|
"R-review-architecture/architecture-report-contract.md",
|
|
942
|
+
"common/rules/parent-implementation-orchestration.md",
|
|
943
|
+
"common/schemas/implementation-map.schema.json",
|
|
944
|
+
"common/schemas/implementation-plan.schema.json",
|
|
945
|
+
"O-orchestrate-implementation/input-readiness.md",
|
|
946
|
+
"O-orchestrate-implementation/implementation-map-template.md",
|
|
947
|
+
"O-orchestrate-implementation/implementation-plan-template.md",
|
|
948
|
+
"O-orchestrate-implementation/implementation-evidence-template.md",
|
|
888
949
|
]) {
|
|
889
950
|
if (!isFile(join(root, required))) errors.push(`missing architecture/wayfinding contract ${required}`);
|
|
890
951
|
}
|
|
@@ -1196,31 +1257,78 @@ function validateReviews(change, required, errors) {
|
|
|
1196
1257
|
|
|
1197
1258
|
function validatePrototypes(change, required, errors) {
|
|
1198
1259
|
const root = join(change, "prototypes");
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1260
|
+
if (!isDirectory(root)) {
|
|
1261
|
+
if (required) errors.push("prototype stage requires a prototypes/UI-NNN/design-system.md artifact");
|
|
1262
|
+
return [];
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const validator = resolve(
|
|
1266
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
1267
|
+
"..",
|
|
1268
|
+
"..",
|
|
1269
|
+
"P-prototype",
|
|
1270
|
+
"tools",
|
|
1271
|
+
"validate-design-package.mjs",
|
|
1272
|
+
);
|
|
1273
|
+
const paths = [];
|
|
1274
|
+
let readyCount = 0;
|
|
1275
|
+
|
|
1276
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
1277
|
+
const entryPath = join(root, entry.name);
|
|
1278
|
+
const label = toPosix(relative(change, entryPath));
|
|
1279
|
+
if (entry.isSymbolicLink()) {
|
|
1280
|
+
errors.push(`${label}: prototype design directories and files must not be symlinks`);
|
|
1281
|
+
continue;
|
|
1208
1282
|
}
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
if (!new Set(["active", "answered", "blocked", "discarded"]).has(meta.status)) errors.push(`${label}: invalid prototype status`);
|
|
1213
|
-
if (typeof meta.workspace_ref !== "string" || ABSOLUTE_MACHINE_PATH_RE.test(meta.workspace_ref)) {
|
|
1214
|
-
errors.push(`${label}: workspace_ref must be a portable locator`);
|
|
1283
|
+
if (!entry.isDirectory()) {
|
|
1284
|
+
errors.push(`${label}: prototypes may only contain UI-NNN design directories`);
|
|
1285
|
+
continue;
|
|
1215
1286
|
}
|
|
1216
|
-
if (
|
|
1217
|
-
errors.push(`${label}:
|
|
1287
|
+
if (!/^UI-\d{3,}$/.test(entry.name)) {
|
|
1288
|
+
errors.push(`${label}: prototype design directory must use UI-NNN`);
|
|
1289
|
+
continue;
|
|
1218
1290
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1291
|
+
|
|
1292
|
+
const designPath = join(entryPath, "design-system.md");
|
|
1293
|
+
if (!isFile(designPath)) {
|
|
1294
|
+
errors.push(`${label}: missing design-system.md`);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
paths.push(designPath);
|
|
1298
|
+
const { meta } = parseFrontmatter(designPath);
|
|
1299
|
+
if (meta.status === "ready") readyCount += 1;
|
|
1300
|
+
try {
|
|
1301
|
+
execFileSync(process.execPath, [validator, designPath], {
|
|
1302
|
+
encoding: "utf8",
|
|
1303
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1304
|
+
});
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
const details = String(error.stderr || error.stdout || error.message)
|
|
1307
|
+
.trim()
|
|
1308
|
+
.split(/\r?\n/)
|
|
1309
|
+
.filter(Boolean);
|
|
1310
|
+
if (!details.length) errors.push(`${label}: design package validation failed`);
|
|
1311
|
+
for (const detail of details) errors.push(`${label}: ${detail}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const obsoleteRecords = walk(root).filter(
|
|
1316
|
+
(path) => isFile(path) && basename(path) === "record.md",
|
|
1317
|
+
);
|
|
1318
|
+
for (const path of obsoleteRecords) {
|
|
1319
|
+
errors.push(`${toPosix(relative(change, path))}: obsolete prototype record is forbidden; use UI-NNN/design-system.md`);
|
|
1320
|
+
}
|
|
1321
|
+
const discoveredDesigns = walk(root).filter(
|
|
1322
|
+
(path) => isFile(path) && basename(path) === "design-system.md",
|
|
1323
|
+
);
|
|
1324
|
+
for (const path of discoveredDesigns) {
|
|
1325
|
+
if (!paths.includes(path)) {
|
|
1326
|
+
errors.push(`${toPosix(relative(change, path))}: design-system.md must be directly under prototypes/UI-NNN`);
|
|
1222
1327
|
}
|
|
1223
1328
|
}
|
|
1329
|
+
if (required && readyCount === 0) {
|
|
1330
|
+
errors.push("prototype stage requires at least one ready UI design package");
|
|
1331
|
+
}
|
|
1224
1332
|
return paths;
|
|
1225
1333
|
}
|
|
1226
1334
|
|
|
@@ -2058,6 +2166,438 @@ function validateGitEvidence(repoRoot, changeStatus, errors) {
|
|
|
2058
2166
|
}
|
|
2059
2167
|
}
|
|
2060
2168
|
|
|
2169
|
+
function validateParentImplementation(change, parentStatus, stage, errors, warnings) {
|
|
2170
|
+
const required = stage === "orchestrate-implementation";
|
|
2171
|
+
const mapPath = join(change, "implementation-map.md");
|
|
2172
|
+
const planPath = join(change, "implementation-plan.md");
|
|
2173
|
+
if (!required && !isFile(mapPath) && !isFile(planPath)) return null;
|
|
2174
|
+
if (!isFile(mapPath)) errors.push("orchestrate-implementation stage requires implementation-map.md");
|
|
2175
|
+
if (!isFile(planPath)) errors.push("orchestrate-implementation stage requires implementation-plan.md");
|
|
2176
|
+
if (!isFile(mapPath) || !isFile(planPath)) return null;
|
|
2177
|
+
|
|
2178
|
+
const parentName = basename(change);
|
|
2179
|
+
const map = parseFrontmatter(mapPath);
|
|
2180
|
+
const plan = parseFrontmatter(planPath);
|
|
2181
|
+
const mapKeys = ["schema_version", "artifact", "change", "status", "revision", "members", "tasks", "dependencies", "serializations"];
|
|
2182
|
+
const planKeys = ["schema_version", "artifact", "change", "status", "source_map_revision", "orchestration", "lead", "implementation_agent_limit", "integration_attempt_limit", "ticket_workspace_policy", "integration_gate", "ready_for_execution"];
|
|
2183
|
+
for (const [label, meta, expected] of [
|
|
2184
|
+
["implementation-map.md", map.meta, mapKeys],
|
|
2185
|
+
["implementation-plan.md", plan.meta, planKeys],
|
|
2186
|
+
]) {
|
|
2187
|
+
const missing = expected.filter((key) => !(key in meta));
|
|
2188
|
+
const unexpected = Object.keys(meta).filter((key) => !expected.includes(key));
|
|
2189
|
+
if (missing.length) errors.push(`${label}: missing keys ${JSON.stringify(missing)}`);
|
|
2190
|
+
if (unexpected.length) errors.push(`${label}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
2191
|
+
}
|
|
2192
|
+
if (map.meta.schema_version !== IMPLEMENTATION_MAP_SCHEMA_VERSION || map.meta.artifact !== "implementation-map") {
|
|
2193
|
+
errors.push("implementation-map.md: artifact/schema_version must be implementation-map/1");
|
|
2194
|
+
}
|
|
2195
|
+
if (plan.meta.schema_version !== IMPLEMENTATION_PLAN_SCHEMA_VERSION || plan.meta.artifact !== "implementation-plan") {
|
|
2196
|
+
errors.push("implementation-plan.md: artifact/schema_version must be implementation-plan/1");
|
|
2197
|
+
}
|
|
2198
|
+
if (map.meta.change !== parentName || plan.meta.change !== parentName) {
|
|
2199
|
+
errors.push("parent implementation artifacts must name their containing change");
|
|
2200
|
+
}
|
|
2201
|
+
const artifactStatuses = new Set(["ready", "in_progress", "blocked", "completed"]);
|
|
2202
|
+
if (!artifactStatuses.has(map.meta.status)) errors.push(`implementation-map.md: invalid status ${map.meta.status}`);
|
|
2203
|
+
if (!artifactStatuses.has(plan.meta.status)) errors.push(`implementation-plan.md: invalid status ${plan.meta.status}`);
|
|
2204
|
+
if (!Number.isInteger(map.meta.revision) || map.meta.revision < 1) {
|
|
2205
|
+
errors.push("implementation-map.md: revision must be a positive integer");
|
|
2206
|
+
}
|
|
2207
|
+
if (plan.meta.source_map_revision !== map.meta.revision) {
|
|
2208
|
+
errors.push("implementation-plan.md: source_map_revision must equal Implementation Map revision");
|
|
2209
|
+
}
|
|
2210
|
+
if (plan.meta.orchestration !== "lead-directed" || typeof plan.meta.lead !== "string" || !plan.meta.lead.trim()) {
|
|
2211
|
+
errors.push("implementation-plan.md: lead-directed orchestration requires a recoverable Lead");
|
|
2212
|
+
}
|
|
2213
|
+
for (const key of ["implementation_agent_limit", "integration_attempt_limit"]) {
|
|
2214
|
+
if (!Number.isInteger(plan.meta[key]) || plan.meta[key] < 1) {
|
|
2215
|
+
errors.push(`implementation-plan.md: ${key} must be a positive integer`);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const validStrategy =
|
|
2219
|
+
(plan.meta.ticket_workspace_policy === "current" && plan.meta.integration_gate === "direct-parent") ||
|
|
2220
|
+
(plan.meta.ticket_workspace_policy === "required" && plan.meta.integration_gate === "candidate-merge");
|
|
2221
|
+
if (!validStrategy) {
|
|
2222
|
+
errors.push("implementation-plan.md: workspace/integration strategy must be current/direct-parent or required/candidate-merge");
|
|
2223
|
+
}
|
|
2224
|
+
const readyStatuses = new Set(["ready", "in_progress"]);
|
|
2225
|
+
if (
|
|
2226
|
+
typeof plan.meta.ready_for_execution !== "boolean" ||
|
|
2227
|
+
(plan.meta.ready_for_execution === true) !== readyStatuses.has(plan.meta.status)
|
|
2228
|
+
) {
|
|
2229
|
+
errors.push("implementation-plan.md: ready_for_execution must match status");
|
|
2230
|
+
}
|
|
2231
|
+
for (const heading of [
|
|
2232
|
+
"## 1. Members and Source Authority",
|
|
2233
|
+
"## 2. Composite Ticket Inventory",
|
|
2234
|
+
"## 3. Implementation Super-DAG",
|
|
2235
|
+
"## 4. Conflict and Serialization",
|
|
2236
|
+
"## 5. Contract and Path Coverage",
|
|
2237
|
+
"## 6. Revision Log",
|
|
2238
|
+
]) {
|
|
2239
|
+
if (!map.body.includes(heading)) errors.push(`implementation-map.md: missing '${heading}'`);
|
|
2240
|
+
}
|
|
2241
|
+
for (const heading of [
|
|
2242
|
+
"## 1. Outcome and Authority",
|
|
2243
|
+
"## 2. Ready Frontier and Waves",
|
|
2244
|
+
"## 3. Workspace and Dispatch Contract",
|
|
2245
|
+
"## 4. Repository Integration Queue",
|
|
2246
|
+
"## 5. Gates and Aggregate Verification",
|
|
2247
|
+
"## 6. Conflict, Drift and Recovery",
|
|
2248
|
+
"## 7. Progress and Decisions",
|
|
2249
|
+
]) {
|
|
2250
|
+
if (!plan.body.includes(heading)) errors.push(`implementation-plan.md: missing '${heading}'`);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const members = requireList(map.meta, "members", "implementation-map.md", errors).map(String);
|
|
2254
|
+
const tasks = requireList(map.meta, "tasks", "implementation-map.md", errors).map(String);
|
|
2255
|
+
const dependencies = requireList(map.meta, "dependencies", "implementation-map.md", errors).map(String);
|
|
2256
|
+
const serializations = requireList(map.meta, "serializations", "implementation-map.md", errors).map(String);
|
|
2257
|
+
if (members.length < 2) errors.push("implementation-map.md: parent implementation requires at least two members");
|
|
2258
|
+
if (!tasks.length) errors.push("implementation-map.md: tasks must contain the child Ticket inventory");
|
|
2259
|
+
for (const [key, values] of [["members", members], ["tasks", tasks], ["dependencies", dependencies], ["serializations", serializations]]) {
|
|
2260
|
+
if (new Set(values).size !== values.length) errors.push(`implementation-map.md: ${key} must be unique`);
|
|
2261
|
+
}
|
|
2262
|
+
if (members.includes(parentName)) errors.push("implementation-map.md: parent change cannot include itself");
|
|
2263
|
+
|
|
2264
|
+
const changesRoot = dirname(change);
|
|
2265
|
+
const memberSet = new Set(members);
|
|
2266
|
+
const memberStatuses = new Map();
|
|
2267
|
+
const ticketByTask = new Map();
|
|
2268
|
+
const expectedTasks = new Set();
|
|
2269
|
+
const expectedInternalEdges = new Set();
|
|
2270
|
+
let activeImplementations = 0;
|
|
2271
|
+
let activeCurrentWriters = 0;
|
|
2272
|
+
const integratingByRef = new Map();
|
|
2273
|
+
|
|
2274
|
+
for (const member of members) {
|
|
2275
|
+
if (!CHANGE_NAME.test(member)) {
|
|
2276
|
+
errors.push(`implementation-map.md: invalid member change name ${member}`);
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
const memberRoot = join(changesRoot, member);
|
|
2280
|
+
if (!isDirectory(memberRoot)) {
|
|
2281
|
+
errors.push(`implementation-map.md: member change does not exist: ${member}`);
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
if (isFile(join(memberRoot, "implementation-map.md"))) {
|
|
2285
|
+
errors.push(`implementation-map.md: nested parent implementation is not supported in v1: ${member}`);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
const memberErrors = [];
|
|
2289
|
+
const memberWarnings = [];
|
|
2290
|
+
const status = validateChangeStatus(join(memberRoot, ".status.json"), member, memberErrors);
|
|
2291
|
+
if (status) {
|
|
2292
|
+
memberStatuses.set(member, status);
|
|
2293
|
+
if (status.change_status === "archived") memberErrors.push("archived change cannot be an implementation member");
|
|
2294
|
+
if (status.current_work !== null && status.current_work !== "specdev/implement") {
|
|
2295
|
+
memberErrors.push(`current_work=${status.current_work} conflicts with parent implementation ownership`);
|
|
2296
|
+
}
|
|
2297
|
+
for (const worktree of Array.isArray(status.worktrees) ? status.worktrees : []) {
|
|
2298
|
+
if (worktree?.status === "active") {
|
|
2299
|
+
activeImplementations += 1;
|
|
2300
|
+
if (worktree.workspace_ref === "current") activeCurrentWriters += 1;
|
|
2301
|
+
if (plan.meta.ticket_workspace_policy === "current" && worktree.workspace_ref !== "current") {
|
|
2302
|
+
memberErrors.push(`${worktree.ticket_id}: active workspace must follow parent current policy`);
|
|
2303
|
+
}
|
|
2304
|
+
if (plan.meta.ticket_workspace_policy === "required" && worktree.workspace_ref === "current") {
|
|
2305
|
+
memberErrors.push(`${worktree.ticket_id}: active workspace must follow parent required policy`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (worktree?.status === "integrating") {
|
|
2309
|
+
const key = String(worktree.parent_branch ?? "<unknown-ref>");
|
|
2310
|
+
integratingByRef.set(key, (integratingByRef.get(key) ?? 0) + 1);
|
|
2311
|
+
}
|
|
2312
|
+
if (Number.isInteger(worktree?.integration?.attempts) && worktree.integration.attempts > plan.meta.integration_attempt_limit) {
|
|
2313
|
+
memberErrors.push(`${worktree.ticket_id}: integration attempts ${worktree.integration.attempts} exceed parent limit ${plan.meta.integration_attempt_limit}`);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const specPath = join(memberRoot, "spec.md");
|
|
2319
|
+
const childSpec = isFile(specPath) ? validateSpec(specPath, memberErrors, memberWarnings) : null;
|
|
2320
|
+
if (!childSpec) memberErrors.push("Ready Spec is required before parent creation");
|
|
2321
|
+
else {
|
|
2322
|
+
if (childSpec.meta.change !== member) memberErrors.push("spec.md: change must equal member directory name");
|
|
2323
|
+
if (childSpec.meta.status !== "ready" || childSpec.meta.ready_for_tickets !== true) {
|
|
2324
|
+
memberErrors.push("Spec must have status=ready and ready_for_tickets=true");
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
const childMapPath = join(memberRoot, "tickets-map.md");
|
|
2329
|
+
const childMap = isFile(childMapPath) ? validateMap(childMapPath, memberErrors) : null;
|
|
2330
|
+
if (!childMap) memberErrors.push("Ready Tickets Map is required before parent creation");
|
|
2331
|
+
else {
|
|
2332
|
+
if (childMap.meta.change !== member) memberErrors.push("tickets-map.md: change must equal member directory name");
|
|
2333
|
+
if (!new Set(["ready", "in_progress", "completed"]).has(childMap.meta.status)) {
|
|
2334
|
+
memberErrors.push("Tickets Map status must be ready, in_progress, or completed");
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const ticketRoot = join(memberRoot, "ticket");
|
|
2339
|
+
const ticketFiles = isDirectory(ticketRoot)
|
|
2340
|
+
? readdirSync(ticketRoot).filter((name) => name.endsWith(".md")).sort()
|
|
2341
|
+
: [];
|
|
2342
|
+
if (!ticketFiles.length) memberErrors.push("Ready Tickets are required before parent creation");
|
|
2343
|
+
const childTickets = new Map();
|
|
2344
|
+
for (const name of ticketFiles) {
|
|
2345
|
+
const artifact = validateTicket(join(ticketRoot, name), memberErrors);
|
|
2346
|
+
if (!artifact) continue;
|
|
2347
|
+
const ticketId = String(artifact.meta.id);
|
|
2348
|
+
if (artifact.meta.change !== member) memberErrors.push(`${name}: change must equal member directory name`);
|
|
2349
|
+
if (childTickets.has(ticketId)) memberErrors.push(`duplicate Ticket id ${ticketId}`);
|
|
2350
|
+
childTickets.set(ticketId, artifact);
|
|
2351
|
+
const numericId = ticketId.replace(/^T-/, "");
|
|
2352
|
+
if (!name.startsWith(`${numericId}-`)) memberErrors.push(`${name}: filename prefix must match ${ticketId}`);
|
|
2353
|
+
if (childMap && !childMap.body.includes(ticketId)) memberErrors.push(`${name}: Ticket id is absent from Tickets Map`);
|
|
2354
|
+
if (childSpec) {
|
|
2355
|
+
for (const contractId of artifact.meta.contract_ids ?? []) {
|
|
2356
|
+
if (!childSpec.body.includes(String(contractId))) memberErrors.push(`${name}: contract ${contractId} not found in Spec`);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const taskId = `${member}::${ticketId}`;
|
|
2361
|
+
expectedTasks.add(taskId);
|
|
2362
|
+
ticketByTask.set(taskId, { artifact, member, memberRoot, status });
|
|
2363
|
+
const terminal = new Set(["done", "cancelled"]).has(artifact.meta.status);
|
|
2364
|
+
if (!terminal) {
|
|
2365
|
+
if (map.meta.status === "ready" && (artifact.meta.status !== "ready" || artifact.meta.ready !== true)) {
|
|
2366
|
+
memberErrors.push(`${ticketId}: parent creation requires status=ready and ready=true`);
|
|
2367
|
+
} else if (new Set(["ready", "in_progress", "review"]).has(artifact.meta.status) && artifact.meta.ready !== true) {
|
|
2368
|
+
memberErrors.push(`${ticketId}: executable Ticket must keep ready=true`);
|
|
2369
|
+
} else if (new Set(["blocked", "deviated"]).has(artifact.meta.status) && map.meta.status !== "blocked" && plan.meta.status !== "blocked") {
|
|
2370
|
+
memberErrors.push(`${ticketId}: blocked/deviated Ticket requires blocked parent artifacts`);
|
|
2371
|
+
} else if (artifact.meta.status === "draft") {
|
|
2372
|
+
memberErrors.push(`${ticketId}: draft Ticket cannot belong to a parent implementation`);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
for (const [ticketId, artifact] of childTickets) {
|
|
2378
|
+
for (const dependency of (artifact.meta.blocked_by ?? []).map(String)) {
|
|
2379
|
+
if (!childTickets.has(dependency)) memberErrors.push(`${ticketId}: blocked_by references missing ${dependency}`);
|
|
2380
|
+
expectedInternalEdges.add(`${member}::${ticketId} <- ${member}::${dependency}`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const childGraph = new Map([...childTickets].map(([ticketId, artifact]) => [ticketId, (artifact.meta.blocked_by ?? []).map(String)]));
|
|
2384
|
+
const childCycle = findCycle(childGraph);
|
|
2385
|
+
if (childCycle) memberErrors.push(`Ticket dependency cycle: ${childCycle.join(" -> ")}`);
|
|
2386
|
+
|
|
2387
|
+
if (childSpec) {
|
|
2388
|
+
const declared = new Set(childSpec.body.match(/\bAC-\d+\b/g) ?? []);
|
|
2389
|
+
const covered = new Set([...childTickets.values()].flatMap((artifact) => (artifact.meta.contract_ids ?? []).map(String)));
|
|
2390
|
+
const uncovered = [...declared].filter((id) => !covered.has(id) && !(childMap && new RegExp(`${escapeRegExp(id)}.*\\bdeferred\\b`, "i").test(childMap.body)));
|
|
2391
|
+
if (uncovered.length) memberErrors.push(`Spec acceptance contracts are not covered by Tickets: ${JSON.stringify(uncovered.sort())}`);
|
|
2392
|
+
}
|
|
2393
|
+
if (status?.change_status === "completed") {
|
|
2394
|
+
const unfinished = [...childTickets].filter(([, artifact]) => !new Set(["done", "cancelled"]).has(artifact.meta.status)).map(([id]) => id);
|
|
2395
|
+
if (unfinished.length) memberErrors.push(`completed member has unfinished Tickets: ${JSON.stringify(unfinished)}`);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const goalPath = join(memberRoot, "goal-plan.md");
|
|
2399
|
+
if (isFile(goalPath)) {
|
|
2400
|
+
const goal = validateGoalPlan(goalPath, memberErrors);
|
|
2401
|
+
if (goal && (
|
|
2402
|
+
goal.meta.ticket_workspace_policy !== plan.meta.ticket_workspace_policy ||
|
|
2403
|
+
goal.meta.integration_gate !== plan.meta.integration_gate
|
|
2404
|
+
)) {
|
|
2405
|
+
memberErrors.push("Goal Plan workspace/integration strategy conflicts with parent Implementation Plan");
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
errors.push(...memberErrors.map((message) => `${member}: ${message}`));
|
|
2409
|
+
warnings.push(...memberWarnings.map((message) => `${member}: ${message}`));
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
const taskSet = new Set(tasks);
|
|
2413
|
+
for (const task of tasks) {
|
|
2414
|
+
if (!/^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*::T-\d{2,}$/.test(task)) {
|
|
2415
|
+
errors.push(`implementation-map.md: invalid composite task '${task}'`);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
const missingTasks = [...expectedTasks].filter((task) => !taskSet.has(task)).sort();
|
|
2419
|
+
const extraTasks = tasks.filter((task) => !expectedTasks.has(task)).sort();
|
|
2420
|
+
if (missingTasks.length || extraTasks.length) {
|
|
2421
|
+
errors.push(`implementation-map.md: tasks must exactly match child Tickets; missing=${JSON.stringify(missingTasks)} extra=${JSON.stringify(extraTasks)}`);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
const graph = new Map(tasks.map((task) => [task, []]));
|
|
2425
|
+
const dependencySet = new Set(dependencies);
|
|
2426
|
+
for (const edge of dependencies) {
|
|
2427
|
+
const match = /^([^ ]+::T-\d{2,}) <- ([^ ]+::T-\d{2,})$/.exec(edge);
|
|
2428
|
+
if (!match) {
|
|
2429
|
+
errors.push(`implementation-map.md: invalid dependency '${edge}'`);
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const [, dependent, prerequisite] = match;
|
|
2433
|
+
if (!taskSet.has(dependent) || !taskSet.has(prerequisite)) {
|
|
2434
|
+
errors.push(`implementation-map.md: dependency endpoints must be composite tasks: ${edge}`);
|
|
2435
|
+
} else if (dependent === prerequisite) {
|
|
2436
|
+
errors.push(`implementation-map.md: dependency cannot be self-referential: ${edge}`);
|
|
2437
|
+
} else {
|
|
2438
|
+
graph.get(dependent).push(prerequisite);
|
|
2439
|
+
const dependentMember = dependent.split("::")[0];
|
|
2440
|
+
const prerequisiteMember = prerequisite.split("::")[0];
|
|
2441
|
+
if (dependentMember === prerequisiteMember && !expectedInternalEdges.has(edge)) {
|
|
2442
|
+
errors.push(`implementation-map.md: same-member dependency must come from child blocked_by: ${edge}`);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
const missingInternalEdges = [...expectedInternalEdges].filter((edge) => !dependencySet.has(edge)).sort();
|
|
2447
|
+
if (missingInternalEdges.length) {
|
|
2448
|
+
errors.push(`implementation-map.md: missing child Ticket dependencies ${JSON.stringify(missingInternalEdges)}`);
|
|
2449
|
+
}
|
|
2450
|
+
const cycle = findCycle(graph);
|
|
2451
|
+
if (cycle) errors.push(`implementation super-DAG cycle: ${cycle.join(" -> ")}`);
|
|
2452
|
+
|
|
2453
|
+
const serializationPairs = new Set();
|
|
2454
|
+
for (const edge of serializations) {
|
|
2455
|
+
const match = /^([^ ]+::T-\d{2,}) <> ([^ ]+::T-\d{2,})$/.exec(edge);
|
|
2456
|
+
if (!match) {
|
|
2457
|
+
errors.push(`implementation-map.md: invalid serialization '${edge}'`);
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
const [, left, right] = match;
|
|
2461
|
+
if (!taskSet.has(left) || !taskSet.has(right)) {
|
|
2462
|
+
errors.push(`implementation-map.md: serialization endpoints must be composite tasks: ${edge}`);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
if (left === right) {
|
|
2466
|
+
errors.push(`implementation-map.md: serialization cannot be self-referential: ${edge}`);
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
const key = [left, right].sort().join("\0");
|
|
2470
|
+
if (serializationPairs.has(key)) errors.push(`implementation-map.md: duplicate unordered serialization pair: ${edge}`);
|
|
2471
|
+
serializationPairs.add(key);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
const unfinishedTasks = [...ticketByTask]
|
|
2475
|
+
.filter(([, value]) => !new Set(["done", "cancelled"]).has(value.artifact.meta.status))
|
|
2476
|
+
.map(([task]) => task);
|
|
2477
|
+
for (let index = 0; index < unfinishedTasks.length; index += 1) {
|
|
2478
|
+
const leftId = unfinishedTasks[index];
|
|
2479
|
+
const left = ticketByTask.get(leftId).artifact;
|
|
2480
|
+
for (const rightId of unfinishedTasks.slice(index + 1)) {
|
|
2481
|
+
if (leftId.split("::")[0] === rightId.split("::")[0]) continue;
|
|
2482
|
+
if (
|
|
2483
|
+
transitivelyDepends(graph, leftId, rightId) ||
|
|
2484
|
+
transitivelyDepends(graph, rightId, leftId) ||
|
|
2485
|
+
serializationPairs.has([leftId, rightId].sort().join("\0"))
|
|
2486
|
+
) continue;
|
|
2487
|
+
const right = ticketByTask.get(rightId).artifact;
|
|
2488
|
+
const overlaps = [];
|
|
2489
|
+
for (const leftPath of left.meta.writable_paths ?? []) {
|
|
2490
|
+
for (const rightPath of right.meta.writable_paths ?? []) {
|
|
2491
|
+
if (pathsOverlap(String(leftPath), String(rightPath))) overlaps.push([String(leftPath), String(rightPath)]);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (overlaps.length) {
|
|
2495
|
+
errors.push(`composite tasks ${leftId}/${rightId} have writable overlap without dependency or serialization: ${JSON.stringify(overlaps.slice(0, 3))}`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
for (const entry of readdirSync(changesRoot, { withFileTypes: true })) {
|
|
2501
|
+
if (!entry.isDirectory() || entry.name === parentName) continue;
|
|
2502
|
+
const otherMapPath = join(changesRoot, entry.name, "implementation-map.md");
|
|
2503
|
+
if (!isFile(otherMapPath)) continue;
|
|
2504
|
+
const otherMap = parseFrontmatter(otherMapPath).meta;
|
|
2505
|
+
if (!Array.isArray(otherMap.members)) continue;
|
|
2506
|
+
let otherStatus = null;
|
|
2507
|
+
try {
|
|
2508
|
+
otherStatus = JSON.parse(readText(join(changesRoot, entry.name, ".status.json"))).change_status;
|
|
2509
|
+
} catch {
|
|
2510
|
+
// Invalid competing parents still own their members conservatively.
|
|
2511
|
+
}
|
|
2512
|
+
if (new Set(["completed", "archived"]).has(otherStatus)) continue;
|
|
2513
|
+
const overlap = otherMap.members.map(String).filter((member) => memberSet.has(member));
|
|
2514
|
+
if (overlap.length) errors.push(`member changes already belong to unfinished parent implementation ${entry.name}: ${JSON.stringify(overlap)}`);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
const config = findSpecdevConfig(change);
|
|
2518
|
+
const configuredAgents = positiveConfigLimit(config, "max_implementation_agents", 0);
|
|
2519
|
+
const configuredAttempts = positiveConfigLimit(config, "max_integration_attempts", 0);
|
|
2520
|
+
if (!config || config.schema_version !== CONFIG_SCHEMA_VERSION || configuredAgents === 0 || configuredAttempts === 0) {
|
|
2521
|
+
errors.push("implementation-plan.md: SpecDev config v5 with positive execution limits is required");
|
|
2522
|
+
} else {
|
|
2523
|
+
if (plan.meta.implementation_agent_limit > configuredAgents) {
|
|
2524
|
+
errors.push(`implementation-plan.md: implementation_agent_limit ${plan.meta.implementation_agent_limit} exceeds config max_implementation_agents ${configuredAgents}`);
|
|
2525
|
+
}
|
|
2526
|
+
if (plan.meta.integration_attempt_limit > configuredAttempts) {
|
|
2527
|
+
errors.push(`implementation-plan.md: integration_attempt_limit ${plan.meta.integration_attempt_limit} exceeds config max_integration_attempts ${configuredAttempts}`);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (activeImplementations > plan.meta.implementation_agent_limit) {
|
|
2531
|
+
errors.push(`parent implementation agent limit exceeded: ${activeImplementations} active for limit ${plan.meta.implementation_agent_limit}`);
|
|
2532
|
+
}
|
|
2533
|
+
if (plan.meta.ticket_workspace_policy === "current" && activeCurrentWriters > 1) {
|
|
2534
|
+
errors.push(`parent current policy permits only one active implementation writer; found ${activeCurrentWriters}`);
|
|
2535
|
+
}
|
|
2536
|
+
for (const [ref, count] of integratingByRef) {
|
|
2537
|
+
if (count > 1) errors.push(`repository/ref integration must be serialized for ${ref}; found ${count}`);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
if (required && parentStatus && new Set(["active", "blocked"]).has(parentStatus.change_status) && parentStatus.current_work !== "specdev/orchestrate-implementation") {
|
|
2541
|
+
errors.push("parent active/blocked status must keep current_work=specdev/orchestrate-implementation");
|
|
2542
|
+
}
|
|
2543
|
+
if (parentStatus?.change_status === "completed") {
|
|
2544
|
+
const incomplete = members.filter((member) => memberStatuses.get(member)?.change_status !== "completed");
|
|
2545
|
+
if (incomplete.length) errors.push(`parent implementation is completed while members remain incomplete: ${JSON.stringify(incomplete)}`);
|
|
2546
|
+
const unfinished = [...ticketByTask].filter(([, value]) => !new Set(["done", "cancelled"]).has(value.artifact.meta.status)).map(([task]) => task);
|
|
2547
|
+
if (unfinished.length) errors.push(`completed parent implementation has unfinished composite tasks: ${JSON.stringify(unfinished)}`);
|
|
2548
|
+
if (map.meta.status !== "completed" || plan.meta.status !== "completed") {
|
|
2549
|
+
errors.push("completed parent requires completed Implementation Map and Implementation Plan");
|
|
2550
|
+
}
|
|
2551
|
+
if (
|
|
2552
|
+
parentStatus.current_work !== null ||
|
|
2553
|
+
!Array.isArray(parentStatus.blockers) || parentStatus.blockers.length > 0 ||
|
|
2554
|
+
!Array.isArray(parentStatus.deviations) || parentStatus.deviations.length > 0
|
|
2555
|
+
) {
|
|
2556
|
+
errors.push("completed parent requires null current_work and no blockers or deviations");
|
|
2557
|
+
}
|
|
2558
|
+
const activeMemberWork = [...memberStatuses]
|
|
2559
|
+
.flatMap(([member, status]) => (status.worktrees ?? []).map((worktree) => ({ member, worktree })))
|
|
2560
|
+
.filter(({ worktree }) => new Set(["planned", "active", "review", "integrating", "blocked"]).has(worktree?.status) || worktree?.integration?.status === "candidate")
|
|
2561
|
+
.map(({ member, worktree }) => `${member}::${worktree.ticket_id}`);
|
|
2562
|
+
if (activeMemberWork.length) {
|
|
2563
|
+
errors.push(`completed parent has active member worktrees or candidates: ${JSON.stringify(activeMemberWork)}`);
|
|
2564
|
+
}
|
|
2565
|
+
for (const [task, value] of ticketByTask) {
|
|
2566
|
+
if (value.artifact.meta.status !== "done") continue;
|
|
2567
|
+
if (!isFile(join(value.memberRoot, "evidence", `${value.artifact.meta.id}.md`))) {
|
|
2568
|
+
errors.push(`${task}: completed parent requires child Ticket Evidence`);
|
|
2569
|
+
}
|
|
2570
|
+
const worktree = value.status?.worktrees?.find((entry) => entry?.ticket_id === value.artifact.meta.id);
|
|
2571
|
+
if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
2572
|
+
errors.push(`${task}: completed parent requires integrated or removed child workspace record`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
const evidencePath = join(change, "evidence", "implementation-orchestration.md");
|
|
2576
|
+
if (!isFile(evidencePath)) {
|
|
2577
|
+
errors.push("completed parent requires evidence/implementation-orchestration.md");
|
|
2578
|
+
} else {
|
|
2579
|
+
const evidence = readText(evidencePath);
|
|
2580
|
+
for (const heading of [
|
|
2581
|
+
"## 1. Parent Plan and Final Revision",
|
|
2582
|
+
"## 2. Member and Ticket Completion",
|
|
2583
|
+
"## 3. Dependency and Serialization Audit",
|
|
2584
|
+
"## 4. Repository Integration Audit",
|
|
2585
|
+
"## 5. Aggregate Verification",
|
|
2586
|
+
"## 6. Contract, Drift and Deviation Audit",
|
|
2587
|
+
"## 7. Residual Risk and Boundary",
|
|
2588
|
+
]) {
|
|
2589
|
+
if (!evidence.includes(heading)) errors.push(`evidence/implementation-orchestration.md: missing '${heading}'`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
} else if (map.meta.status === "completed" || plan.meta.status === "completed") {
|
|
2593
|
+
errors.push("completed Implementation Map/Plan requires completed parent change status");
|
|
2594
|
+
}
|
|
2595
|
+
if (plan.meta.ready_for_execution === true && !new Set(["ready", "in_progress"]).has(map.meta.status)) {
|
|
2596
|
+
errors.push("ready Implementation Plan requires a ready or in_progress Implementation Map");
|
|
2597
|
+
}
|
|
2598
|
+
return { map, plan, members, tasks, memberStatuses, ticketByTask };
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2061
2601
|
function validateChange(change, stage = null, repoRoot = null) {
|
|
2062
2602
|
const errors = [];
|
|
2063
2603
|
const warnings = [];
|
|
@@ -2066,6 +2606,7 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
2066
2606
|
}
|
|
2067
2607
|
|
|
2068
2608
|
const changeStatus = validateChangeStatus(join(change, ".status.json"), basename(change), errors);
|
|
2609
|
+
validateParentImplementation(change, changeStatus, stage, errors, warnings);
|
|
2069
2610
|
if (isFile(join(change, "source-issue.md"))) {
|
|
2070
2611
|
errors.push("obsolete source-issue.md is forbidden; use source.md without compatibility fallback");
|
|
2071
2612
|
}
|
|
@@ -2085,7 +2626,7 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
2085
2626
|
validateReviews(change, stage === "review", errors);
|
|
2086
2627
|
validatePrototypes(change, stage === "prototype", errors);
|
|
2087
2628
|
|
|
2088
|
-
const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage);
|
|
2629
|
+
const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage) && !isFile(join(change, "implementation-map.md"));
|
|
2089
2630
|
const specPath = join(change, "spec.md");
|
|
2090
2631
|
const spec = isFile(specPath) || specRequired
|
|
2091
2632
|
? validateSpec(specPath, errors, warnings)
|
|
@@ -2341,6 +2882,7 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
2341
2882
|
if (
|
|
2342
2883
|
stage === "complete" &&
|
|
2343
2884
|
!ticketFiles.length &&
|
|
2885
|
+
!isFile(join(change, "implementation-map.md")) &&
|
|
2344
2886
|
!isFile(join(change, "evidence", "direct-spec.md"))
|
|
2345
2887
|
) {
|
|
2346
2888
|
errors.push("complete stage without Tickets requires evidence/direct-spec.md");
|