@namewta/speculo 0.7.1 → 0.7.3
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/README.md +2 -1
- package/dist/src/migrations.js +604 -23
- package/dist/src/migrations.js.map +1 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +171 -277
- package/template/canonical/canonical-specdev-goal-plan.md +714 -833
- package/template/canonical/canonical-specdev-grill-with-docs.md +172 -278
- package/template/canonical/canonical-specdev-spec.md +199 -315
- package/template/canonical/canonical-specdev-tickets.md +405 -398
- package/template/canonical/canonical-specdev-wayfinder.md +170 -276
- package/template/skills/migrate-runtime-state/SKILL.md +6 -6
- package/template/skills/migrate-runtime-state/references/migration-contract.md +9 -3
- package/template/skills/migrate-runtime-state/scripts/migrate-runtime-state.mjs +322 -33
- package/template/skills/optimize-codex-config/SKILL.md +81 -0
- package/template/skills/optimize-codex-config/references/configuration-contract.md +103 -0
- package/template/skills/optimize-codex-config/references/troubleshooting.md +79 -0
- package/template/skills/optimize-codex-config/scripts/audit-codex-config.mjs +747 -0
- package/template/workflows/specdev/I-implement/I-implement.md +97 -142
- package/template/workflows/specdev/I-implement/evidence-template.md +60 -48
- package/template/workflows/specdev/I-implement/execution-preflight.md +29 -19
- package/template/workflows/specdev/I-implement/merge-conflict-protocol.md +12 -11
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +4 -5
- package/template/workflows/specdev/I-init-setup/change-status-template.json +14 -1
- package/template/workflows/specdev/I-init-setup/config-template.json +3 -5
- package/template/workflows/specdev/I-init-setup/status-template.json +1 -1
- package/template/workflows/specdev/INDEX.md +12 -8
- package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +76 -93
- package/template/workflows/specdev/P-goal-plan/completion-control.md +26 -44
- package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +43 -31
- package/template/workflows/specdev/P-goal-plan/lead-orchestration.md +34 -0
- package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +31 -42
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +42 -61
- package/template/workflows/specdev/T-tickets/T-tickets.md +6 -3
- package/template/workflows/specdev/T-tickets/ticket-readiness.md +5 -3
- package/template/workflows/specdev/T-tickets/ticket-template.md +8 -1
- package/template/workflows/specdev/T-tickets/tickets-map-template.md +5 -4
- package/template/workflows/specdev/_state/status.json +1 -1
- package/template/workflows/specdev/common/README.md +2 -2
- package/template/workflows/specdev/common/rules/change-completion.md +17 -19
- package/template/workflows/specdev/common/rules/deviation-control.md +1 -1
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +27 -37
- package/template/workflows/specdev/common/rules/path-ownership.md +21 -23
- package/template/workflows/specdev/common/rules/readiness-and-depth.md +1 -1
- package/template/workflows/specdev/common/schemas/change-status.schema.json +136 -195
- package/template/workflows/specdev/common/schemas/config.schema.json +9 -11
- package/template/workflows/specdev/common/schemas/goal-plan.schema.json +24 -6
- package/template/workflows/specdev/common/schemas/status.schema.json +7 -63
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +42 -21
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +48 -18
- package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +47 -12
- package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +34 -30
- package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +10 -23
- package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +14 -25
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +507 -117
- package/template/workflows/specdev/I-implement/delegated-evidence-template.md +0 -11
- package/template/workflows/specdev/P-goal-plan/delegated-execution-template.md +0 -33
- package/template/workflows/specdev/P-goal-plan/delegated-execution.md +0 -53
|
@@ -18,10 +18,14 @@ import {
|
|
|
18
18
|
import { dirname, basename, extname, join, relative, resolve, sep } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
21
|
+
const DOMAIN_SCHEMA_VERSION = 3;
|
|
22
|
+
const CONFIG_SCHEMA_VERSION = 4;
|
|
23
|
+
const GOAL_PLAN_SCHEMA_VERSION = 5;
|
|
24
|
+
const CHANGE_STATUS_SCHEMA_VERSION = 5;
|
|
25
|
+
const GLOBAL_STATUS_SCHEMA_VERSION = 5;
|
|
23
26
|
const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
|
|
24
27
|
const STATE_PREFIX = "{roots.state}/specdev/";
|
|
28
|
+
const STATE_ROOT_PREFIX = "{roots.state}/";
|
|
25
29
|
const SKILLS_PREFIX = "{roots.skills}/";
|
|
26
30
|
const COMMANDS_PREFIX = "{roots.commands}/";
|
|
27
31
|
|
|
@@ -55,12 +59,24 @@ const VALID_TICKET_STATUS = new Set([
|
|
|
55
59
|
const VALID_DEPTH = new Set(["lite", "standard", "deep"]);
|
|
56
60
|
const VALID_RISK = new Set(["low", "medium", "high", "critical"]);
|
|
57
61
|
const VALID_PLAN_MODES = new Set([
|
|
58
|
-
"coordination",
|
|
59
62
|
"migration",
|
|
60
63
|
"high-assurance",
|
|
61
64
|
"reference-conformance",
|
|
62
65
|
"release-coordination",
|
|
63
66
|
]);
|
|
67
|
+
const VALID_WORKTREE_STATUS = new Set([
|
|
68
|
+
"planned",
|
|
69
|
+
"active",
|
|
70
|
+
"review",
|
|
71
|
+
"integrating",
|
|
72
|
+
"integrated",
|
|
73
|
+
"removed",
|
|
74
|
+
"blocked",
|
|
75
|
+
]);
|
|
76
|
+
const VALID_INTEGRATION_STATUS = new Set(["pending", "candidate", "passed", "failed", "stale"]);
|
|
77
|
+
const VALID_INTEGRATION_METHOD = new Set([null, "fast-forward", "merge-commit"]);
|
|
78
|
+
const VALID_INTEGRATION_VERIFICATION = new Set(["pending", "passed", "failed"]);
|
|
79
|
+
const VALID_E2E_STATUS = new Set(["not-required", "pending", "passed", "failed"]);
|
|
64
80
|
const VALID_DESIGN_TREE_STATUS = new Set(["active", "consensus", "blocked"]);
|
|
65
81
|
const VALID_DESIGN_NODE_STATUS = new Set(["open", "answered", "deferred", "rejected"]);
|
|
66
82
|
const VALID_WAYFINDER_LABEL = new Set([
|
|
@@ -127,30 +143,16 @@ const REQUIRED_GOAL_PLAN_SECTIONS = new Set([
|
|
|
127
143
|
"## 5. Constraints, Risk and Recovery",
|
|
128
144
|
"## 6. Progress and Decisions",
|
|
129
145
|
]);
|
|
130
|
-
const
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"Subagent",
|
|
141
|
-
"subagent",
|
|
142
|
-
"execution_model",
|
|
143
|
-
"execution model: direct",
|
|
144
|
-
"max_correction_rounds",
|
|
145
|
-
];
|
|
146
|
-
const REQUIRED_DELEGATED_GOAL_PLAN_MARKERS = [
|
|
147
|
-
"## Delegated Execution Addendum",
|
|
148
|
-
"### Delivery Contract",
|
|
149
|
-
"### Per-Ticket Dispatch Packets",
|
|
150
|
-
"### Candidate Delivery Return and Lead Integration",
|
|
151
|
-
"native-subagent / external-web-subagent",
|
|
152
|
-
"Lead / Provider",
|
|
153
|
-
"#### Dispatch:",
|
|
146
|
+
const REQUIRED_LEAD_GOAL_PLAN_MARKERS = [
|
|
147
|
+
"### Lead Orchestration",
|
|
148
|
+
"Implementation subagents",
|
|
149
|
+
"Read-only agents",
|
|
150
|
+
"execution-time dynamic",
|
|
151
|
+
"### Ticket Workspace and Candidate Integration",
|
|
152
|
+
"source worktree 不运行 E2E",
|
|
153
|
+
"### Authorization Matrix",
|
|
154
|
+
"Implementation commit",
|
|
155
|
+
"Local candidate integration and parent update",
|
|
154
156
|
];
|
|
155
157
|
const STATE_ARTIFACT_BASENAMES = new Set([
|
|
156
158
|
"spec.md",
|
|
@@ -171,6 +173,10 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
171
173
|
]);
|
|
172
174
|
const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
|
|
173
175
|
"source-issue.md",
|
|
176
|
+
"delegated-execution.md",
|
|
177
|
+
"delegated-execution-template.md",
|
|
178
|
+
"workspace-execution-template.md",
|
|
179
|
+
"delegated-evidence-template.md",
|
|
174
180
|
"input-validation.md",
|
|
175
181
|
"vision-sections.md",
|
|
176
182
|
"execution-sections.md",
|
|
@@ -391,6 +397,19 @@ function validatePathValue(value, { allowProject = true } = {}) {
|
|
|
391
397
|
}
|
|
392
398
|
return null;
|
|
393
399
|
}
|
|
400
|
+
if (value.startsWith(STATE_ROOT_PREFIX)) {
|
|
401
|
+
const suffix = value.slice(STATE_ROOT_PREFIX.length);
|
|
402
|
+
if (
|
|
403
|
+
!suffix ||
|
|
404
|
+
suffix.startsWith("/") ||
|
|
405
|
+
suffix.startsWith("./") ||
|
|
406
|
+
suffix.startsWith("../") ||
|
|
407
|
+
suffix.includes("/../")
|
|
408
|
+
) {
|
|
409
|
+
return "state-root Path is not canonical";
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
394
413
|
for (const [prefix, name] of [[SKILLS_PREFIX, "skills"], [COMMANDS_PREFIX, "commands"]]) {
|
|
395
414
|
if (value.startsWith(prefix)) {
|
|
396
415
|
const suffix = value.slice(prefix.length);
|
|
@@ -592,7 +611,7 @@ function validateGlobalStatusAssets(root) {
|
|
|
592
611
|
!Array.isArray(data.active) ||
|
|
593
612
|
!Array.isArray(data.archived)
|
|
594
613
|
) {
|
|
595
|
-
errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status
|
|
614
|
+
errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status v5 seed`);
|
|
596
615
|
}
|
|
597
616
|
}
|
|
598
617
|
|
|
@@ -603,11 +622,11 @@ function validateGlobalStatusAssets(root) {
|
|
|
603
622
|
}
|
|
604
623
|
const schema = JSON.parse(readText(schemaPath));
|
|
605
624
|
if (
|
|
606
|
-
schema.$id !== "urn:speculo:specdev:status:
|
|
625
|
+
schema.$id !== "urn:speculo:specdev:status:v5" ||
|
|
607
626
|
schema.properties?.schema_version?.const !== GLOBAL_STATUS_SCHEMA_VERSION ||
|
|
608
627
|
schema.additionalProperties !== false
|
|
609
628
|
) {
|
|
610
|
-
errors.push("status.schema.json must define the strict SpecDev global status
|
|
629
|
+
errors.push("status.schema.json must define the strict SpecDev global status v5 index contract");
|
|
611
630
|
}
|
|
612
631
|
for (const removed of ["work_history", "completed", "result"]) {
|
|
613
632
|
if (JSON.stringify(schema).includes(`\"${removed}\"`)) {
|
|
@@ -617,6 +636,88 @@ function validateGlobalStatusAssets(root) {
|
|
|
617
636
|
return errors;
|
|
618
637
|
}
|
|
619
638
|
|
|
639
|
+
function validateExecutionContractAssets(root) {
|
|
640
|
+
const errors = [];
|
|
641
|
+
const configTemplatePath = join(root, "I-init-setup", "config-template.json");
|
|
642
|
+
const changeTemplatePath = join(root, "I-init-setup", "change-status-template.json");
|
|
643
|
+
const configSchemaPath = join(root, "common", "schemas", "config.schema.json");
|
|
644
|
+
const goalPlanSchemaPath = join(root, "common", "schemas", "goal-plan.schema.json");
|
|
645
|
+
const changeSchemaPath = join(root, "common", "schemas", "change-status.schema.json");
|
|
646
|
+
|
|
647
|
+
for (const path of [
|
|
648
|
+
configTemplatePath,
|
|
649
|
+
changeTemplatePath,
|
|
650
|
+
configSchemaPath,
|
|
651
|
+
goalPlanSchemaPath,
|
|
652
|
+
changeSchemaPath,
|
|
653
|
+
]) {
|
|
654
|
+
if (!isFile(path)) errors.push(`missing execution contract asset ${toPosix(relative(root, path))}`);
|
|
655
|
+
}
|
|
656
|
+
if (errors.length) return errors;
|
|
657
|
+
|
|
658
|
+
const configTemplate = JSON.parse(readText(configTemplatePath));
|
|
659
|
+
if (
|
|
660
|
+
configTemplate.schema_version !== CONFIG_SCHEMA_VERSION ||
|
|
661
|
+
!Number.isInteger(configTemplate.execution?.max_implementation_agents) ||
|
|
662
|
+
configTemplate.execution.max_implementation_agents < 1 ||
|
|
663
|
+
configTemplate.execution.max_implementation_agents > 3
|
|
664
|
+
) {
|
|
665
|
+
errors.push("config-template.json must define SpecDev config v4 with max_implementation_agents from 1 to 3");
|
|
666
|
+
}
|
|
667
|
+
for (const obsolete of ["auto_commit", "worktree_for_parallel", "max_parallel"]) {
|
|
668
|
+
if (JSON.stringify(configTemplate).includes(`\"${obsolete}\"`)) {
|
|
669
|
+
errors.push(`config-template.json still contains obsolete execution field ${obsolete}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const configSchema = JSON.parse(readText(configSchemaPath));
|
|
674
|
+
const limit = configSchema.properties?.execution?.properties?.max_implementation_agents;
|
|
675
|
+
if (
|
|
676
|
+
configSchema.$id !== "urn:speculo:specdev:config:v4" ||
|
|
677
|
+
configSchema.properties?.schema_version?.const !== CONFIG_SCHEMA_VERSION ||
|
|
678
|
+
limit?.minimum !== 1 ||
|
|
679
|
+
limit?.maximum !== 3 ||
|
|
680
|
+
configSchema.additionalProperties !== false
|
|
681
|
+
) {
|
|
682
|
+
errors.push("config.schema.json must define the strict SpecDev config v4 execution contract");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const goalPlanSchema = JSON.parse(readText(goalPlanSchemaPath));
|
|
686
|
+
if (
|
|
687
|
+
goalPlanSchema.$id !== "urn:speculo:specdev:goal-plan:v5" ||
|
|
688
|
+
goalPlanSchema.properties?.schema_version?.const !== GOAL_PLAN_SCHEMA_VERSION ||
|
|
689
|
+
goalPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
|
|
690
|
+
goalPlanSchema.properties?.implementation_agent_limit?.maximum !== 3 ||
|
|
691
|
+
goalPlanSchema.properties?.ticket_workspace_policy?.const !== "required" ||
|
|
692
|
+
goalPlanSchema.properties?.integration_gate?.const !== "candidate-merge" ||
|
|
693
|
+
goalPlanSchema.additionalProperties !== false
|
|
694
|
+
) {
|
|
695
|
+
errors.push("goal-plan.schema.json must define the strict Lead-directed Goal Plan v5 contract");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const changeTemplate = JSON.parse(readText(changeTemplatePath));
|
|
699
|
+
if (
|
|
700
|
+
changeTemplate.schema_version !== CHANGE_STATUS_SCHEMA_VERSION ||
|
|
701
|
+
changeTemplate.artifact !== "change-status" ||
|
|
702
|
+
!Array.isArray(changeTemplate.worktrees)
|
|
703
|
+
) {
|
|
704
|
+
errors.push("change-status-template.json must define the SpecDev change-status v4 seed");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const changeSchema = JSON.parse(readText(changeSchemaPath));
|
|
708
|
+
const worktreeRequired = new Set(changeSchema.$defs?.worktree?.required ?? []);
|
|
709
|
+
const integrationRequired = new Set(changeSchema.$defs?.integration?.required ?? []);
|
|
710
|
+
if (
|
|
711
|
+
changeSchema.$id !== "urn:speculo:specdev:change-status:v5" ||
|
|
712
|
+
!["implementation_owner", "integration_owner", "source_checkpoint", "integration"].every((key) => worktreeRequired.has(key)) ||
|
|
713
|
+
!["parent_ref", "candidate_sha", "candidate_tree_sha", "candidate_branch", "candidate_workspace_ref", "full_suite", "e2e", "promotion_status"].every((key) => integrationRequired.has(key)) ||
|
|
714
|
+
changeSchema.additionalProperties !== false
|
|
715
|
+
) {
|
|
716
|
+
errors.push("change-status.schema.json must define the strict Ticket candidate-integration v5 contract");
|
|
717
|
+
}
|
|
718
|
+
return errors;
|
|
719
|
+
}
|
|
720
|
+
|
|
620
721
|
function capabilityChecks(root) {
|
|
621
722
|
const checks = new Map([
|
|
622
723
|
[
|
|
@@ -659,14 +760,14 @@ function capabilityChecks(root) {
|
|
|
659
760
|
join(root, "P-goal-plan", "P-goal-plan.md"),
|
|
660
761
|
[
|
|
661
762
|
"Outcome",
|
|
662
|
-
"
|
|
763
|
+
"权威来源",
|
|
663
764
|
"DAG",
|
|
664
765
|
"Gate",
|
|
665
766
|
"Wave",
|
|
666
|
-
"
|
|
667
|
-
"
|
|
668
|
-
"
|
|
669
|
-
"
|
|
767
|
+
"lead-directed",
|
|
768
|
+
"implementation_agent_limit",
|
|
769
|
+
"candidate-merge",
|
|
770
|
+
"lead-orchestration.md",
|
|
670
771
|
"Definition of Done",
|
|
671
772
|
],
|
|
672
773
|
],
|
|
@@ -675,7 +776,7 @@ function capabilityChecks(root) {
|
|
|
675
776
|
"implement",
|
|
676
777
|
[
|
|
677
778
|
join(root, "I-implement", "I-implement.md"),
|
|
678
|
-
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "
|
|
779
|
+
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "parent-candidate", "subagent-delivery", "实现 commit"],
|
|
679
780
|
],
|
|
680
781
|
],
|
|
681
782
|
[
|
|
@@ -767,45 +868,30 @@ function capabilityChecks(root) {
|
|
|
767
868
|
errors.push("Wayfinder map template must query open Tickets instead of caching them");
|
|
768
869
|
}
|
|
769
870
|
|
|
770
|
-
const
|
|
771
|
-
if (isFile(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
"
|
|
777
|
-
"
|
|
778
|
-
"
|
|
779
|
-
"
|
|
780
|
-
"
|
|
781
|
-
|
|
782
|
-
"execution_model",
|
|
871
|
+
const goalPlanTemplate = join(root, "P-goal-plan", "goal-plan-template.md");
|
|
872
|
+
if (!isFile(goalPlanTemplate)) {
|
|
873
|
+
errors.push("missing Goal Plan template");
|
|
874
|
+
} else {
|
|
875
|
+
const text = readText(goalPlanTemplate);
|
|
876
|
+
for (const required of [
|
|
877
|
+
"schema_version: 5",
|
|
878
|
+
"orchestration: lead-directed",
|
|
879
|
+
"implementation_agent_limit: 3",
|
|
880
|
+
"ticket_workspace_policy: required",
|
|
881
|
+
"integration_gate: candidate-merge",
|
|
882
|
+
...REQUIRED_LEAD_GOAL_PLAN_MARKERS,
|
|
783
883
|
]) {
|
|
784
|
-
if (text.includes(
|
|
785
|
-
errors.push(`ordinary Goal Plan template contains delegated marker ${forbidden}`);
|
|
786
|
-
}
|
|
884
|
+
if (!text.includes(required)) errors.push(`Goal Plan template lost marker ${required}`);
|
|
787
885
|
}
|
|
788
886
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const sampleCore = [...REQUIRED_GOAL_PLAN_SECTIONS].join("\n");
|
|
800
|
-
const partialErrors = [];
|
|
801
|
-
validateDelegatedGoalPlan(`${sampleCore}\n## Delegated Execution Addendum\n### Delivery Contract`, "partial delegated self-check", partialErrors);
|
|
802
|
-
if (!partialErrors.length) errors.push("partial delegated Goal Plan self-check did not fail");
|
|
803
|
-
const strayRoleErrors = [];
|
|
804
|
-
validateDelegatedGoalPlan(`${sampleCore}\nLead: owner`, "stray delegated role self-check", strayRoleErrors);
|
|
805
|
-
if (!strayRoleErrors.length) errors.push("stray delegated role self-check did not fail");
|
|
806
|
-
const completeErrors = [];
|
|
807
|
-
validateDelegatedGoalPlan(`${sampleCore}\n${REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.join("\n")}`, "complete delegated self-check", completeErrors);
|
|
808
|
-
if (completeErrors.length) errors.push(...completeErrors);
|
|
887
|
+
for (const obsolete of [
|
|
888
|
+
"P-goal-plan/delegated-execution.md",
|
|
889
|
+
"P-goal-plan/delegated-execution-template.md",
|
|
890
|
+
"P-goal-plan/workspace-execution-template.md",
|
|
891
|
+
"I-implement/delegated-evidence-template.md",
|
|
892
|
+
]) {
|
|
893
|
+
if (isFile(join(root, obsolete))) errors.push(`obsolete topology asset must be removed: ${obsolete}`);
|
|
894
|
+
}
|
|
809
895
|
return errors;
|
|
810
896
|
}
|
|
811
897
|
|
|
@@ -875,6 +961,7 @@ function selfCheck(root) {
|
|
|
875
961
|
|
|
876
962
|
errors.push(...validateJsonFiles(root));
|
|
877
963
|
errors.push(...validateGlobalStatusAssets(root));
|
|
964
|
+
errors.push(...validateExecutionContractAssets(root));
|
|
878
965
|
const references = validateDocumentReferences(root);
|
|
879
966
|
errors.push(...references.errors);
|
|
880
967
|
warnings.push(...references.warnings);
|
|
@@ -887,6 +974,10 @@ function selfCheck(root) {
|
|
|
887
974
|
"P-goal-plan/governance-sections.md",
|
|
888
975
|
"P-goal-plan/lead-orchestration-protocol.md",
|
|
889
976
|
"P-goal-plan/quick-reference-table.md",
|
|
977
|
+
"P-goal-plan/delegated-execution.md",
|
|
978
|
+
"P-goal-plan/delegated-execution-template.md",
|
|
979
|
+
"P-goal-plan/workspace-execution-template.md",
|
|
980
|
+
"I-implement/delegated-evidence-template.md",
|
|
890
981
|
"schemas",
|
|
891
982
|
"tools",
|
|
892
983
|
"skills",
|
|
@@ -1112,8 +1203,8 @@ function validateSpec(path, errors, warnings) {
|
|
|
1112
1203
|
}
|
|
1113
1204
|
const { meta, body } = parseFrontmatter(path);
|
|
1114
1205
|
const label = basename(path);
|
|
1115
|
-
if (meta.artifact !== "spec" || meta.schema_version !==
|
|
1116
|
-
errors.push(`${label}: artifact/schema_version must be spec/${
|
|
1206
|
+
if (meta.artifact !== "spec" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
|
|
1207
|
+
errors.push(`${label}: artifact/schema_version must be spec/${DOMAIN_SCHEMA_VERSION}`);
|
|
1117
1208
|
}
|
|
1118
1209
|
const sources = requireList(meta, "sources", label, errors);
|
|
1119
1210
|
if (!sources.length) errors.push(`${label}: sources must contain at least one source`);
|
|
@@ -1141,8 +1232,8 @@ function validateMap(path, errors) {
|
|
|
1141
1232
|
return null;
|
|
1142
1233
|
}
|
|
1143
1234
|
const { meta, body } = parseFrontmatter(path);
|
|
1144
|
-
if (meta.artifact !== "tickets-map" || meta.schema_version !==
|
|
1145
|
-
errors.push(`${basename(path)}: artifact/schema_version must be tickets-map/${
|
|
1235
|
+
if (meta.artifact !== "tickets-map" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
|
|
1236
|
+
errors.push(`${basename(path)}: artifact/schema_version must be tickets-map/${DOMAIN_SCHEMA_VERSION}`);
|
|
1146
1237
|
}
|
|
1147
1238
|
for (const heading of [
|
|
1148
1239
|
"## 2. 执行清单",
|
|
@@ -1155,35 +1246,73 @@ function validateMap(path, errors) {
|
|
|
1155
1246
|
return { path, meta, body };
|
|
1156
1247
|
}
|
|
1157
1248
|
|
|
1158
|
-
function validateDelegatedGoalPlan(body, label, errors) {
|
|
1159
|
-
if (!DELEGATED_GOAL_PLAN_TRIGGERS.some((marker) => body.includes(marker))) return;
|
|
1160
|
-
const missing = REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.filter((marker) => !body.includes(marker));
|
|
1161
|
-
if (missing.length) {
|
|
1162
|
-
errors.push(`${label}: incomplete delegated execution addendum; missing ${JSON.stringify(missing)}`);
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
1249
|
function validateGoalPlan(path, errors) {
|
|
1167
1250
|
if (!isFile(path)) return null;
|
|
1168
1251
|
const { meta, body } = parseFrontmatter(path);
|
|
1169
|
-
|
|
1170
|
-
|
|
1252
|
+
const required = [
|
|
1253
|
+
"schema_version",
|
|
1254
|
+
"artifact",
|
|
1255
|
+
"change",
|
|
1256
|
+
"status",
|
|
1257
|
+
"modes",
|
|
1258
|
+
"orchestration",
|
|
1259
|
+
"lead",
|
|
1260
|
+
"implementation_agent_limit",
|
|
1261
|
+
"ticket_workspace_policy",
|
|
1262
|
+
"integration_gate",
|
|
1263
|
+
"ready_for_execution",
|
|
1264
|
+
];
|
|
1265
|
+
const missing = required.filter((key) => !(key in meta));
|
|
1266
|
+
if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
|
|
1267
|
+
const unexpected = Object.keys(meta).filter((key) => !required.includes(key));
|
|
1268
|
+
if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
1269
|
+
if (meta.artifact !== "goal-plan" || meta.schema_version !== GOAL_PLAN_SCHEMA_VERSION) {
|
|
1270
|
+
errors.push(`${basename(path)}: artifact/schema_version must be goal-plan/${GOAL_PLAN_SCHEMA_VERSION}`);
|
|
1171
1271
|
}
|
|
1172
1272
|
const modes = requireList(meta, "modes", basename(path), errors);
|
|
1173
1273
|
const invalidModes = modes.filter((mode) => !VALID_PLAN_MODES.has(mode));
|
|
1174
1274
|
if (invalidModes.length) {
|
|
1175
1275
|
errors.push(`${basename(path)}: invalid modes ${JSON.stringify(invalidModes)}`);
|
|
1176
1276
|
}
|
|
1177
|
-
|
|
1277
|
+
if (meta.orchestration !== "lead-directed") {
|
|
1278
|
+
errors.push(`${basename(path)}: orchestration must be lead-directed`);
|
|
1279
|
+
}
|
|
1280
|
+
if (typeof meta.lead !== "string" || !meta.lead.trim()) {
|
|
1281
|
+
errors.push(`${basename(path)}: lead must be a non-empty recoverable locator`);
|
|
1282
|
+
}
|
|
1283
|
+
if (!Number.isInteger(meta.implementation_agent_limit) || meta.implementation_agent_limit < 1 || meta.implementation_agent_limit > 3) {
|
|
1284
|
+
errors.push(`${basename(path)}: implementation_agent_limit must be an integer from 1 to 3`);
|
|
1285
|
+
}
|
|
1286
|
+
if (meta.ticket_workspace_policy !== "required") {
|
|
1287
|
+
errors.push(`${basename(path)}: ticket_workspace_policy must be required`);
|
|
1288
|
+
}
|
|
1289
|
+
if (meta.integration_gate !== "candidate-merge") {
|
|
1290
|
+
errors.push(`${basename(path)}: integration_gate must be candidate-merge`);
|
|
1291
|
+
}
|
|
1292
|
+
for (const obsolete of ["coordination_mode", "workspace_strategy", "terminal_action"]) {
|
|
1293
|
+
if (obsolete in meta) errors.push(`${basename(path)}: obsolete Goal Plan field ${obsolete} is not allowed`);
|
|
1294
|
+
}
|
|
1295
|
+
for (const obsolete of ["## Delegated Execution Addendum", "## Isolated Workspace Addendum"]) {
|
|
1296
|
+
if (body.includes(obsolete)) errors.push(`${basename(path)}: obsolete Goal Plan addendum '${obsolete}' is not allowed`);
|
|
1297
|
+
}
|
|
1298
|
+
const validReadyStates = new Set(["ready", "in_progress"]);
|
|
1299
|
+
const validNotReadyStates = new Set(["draft", "blocked", "completed"]);
|
|
1300
|
+
if (
|
|
1301
|
+
(meta.ready_for_execution === true && !validReadyStates.has(meta.status)) ||
|
|
1302
|
+
(meta.ready_for_execution === false && !validNotReadyStates.has(meta.status))
|
|
1303
|
+
) {
|
|
1304
|
+
errors.push(`${basename(path)}: ready_for_execution must match status`);
|
|
1305
|
+
}
|
|
1178
1306
|
if (meta.ready_for_execution === true) {
|
|
1179
|
-
if (!new Set(["ready", "in_progress", "completed"]).has(meta.status)) {
|
|
1180
|
-
errors.push(`${basename(path)}: ready_for_execution conflicts with status`);
|
|
1181
|
-
}
|
|
1182
1307
|
for (const heading of REQUIRED_GOAL_PLAN_SECTIONS) {
|
|
1183
1308
|
if (!body.includes(heading)) {
|
|
1184
1309
|
errors.push(`${basename(path)}: ready Goal Plan missing '${heading}'`);
|
|
1185
1310
|
}
|
|
1186
1311
|
}
|
|
1312
|
+
const missingLeadMarkers = REQUIRED_LEAD_GOAL_PLAN_MARKERS.filter((marker) => !body.includes(marker));
|
|
1313
|
+
if (missingLeadMarkers.length) {
|
|
1314
|
+
errors.push(`${basename(path)}: ready Goal Plan missing Lead/candidate markers ${JSON.stringify(missingLeadMarkers)}`);
|
|
1315
|
+
}
|
|
1187
1316
|
const assumptions = sectionBody(body, "## Assumptions");
|
|
1188
1317
|
if (assumptions && assumptions.includes("高影响") && !assumptions.includes("必须为 `false`")) {
|
|
1189
1318
|
errors.push(`${basename(path)}: high-impact assumptions cannot remain in a ready Goal Plan`);
|
|
@@ -1369,8 +1498,8 @@ function validateTicket(path, errors) {
|
|
|
1369
1498
|
errors.push(`${basename(path)}: missing frontmatter keys ${JSON.stringify(missing.sort())}`);
|
|
1370
1499
|
return null;
|
|
1371
1500
|
}
|
|
1372
|
-
if (meta.artifact !== "ticket" || meta.schema_version !==
|
|
1373
|
-
errors.push(`${basename(path)}: artifact/schema_version must be ticket/${
|
|
1501
|
+
if (meta.artifact !== "ticket" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
|
|
1502
|
+
errors.push(`${basename(path)}: artifact/schema_version must be ticket/${DOMAIN_SCHEMA_VERSION}`);
|
|
1374
1503
|
}
|
|
1375
1504
|
const ticketId = String(meta.id);
|
|
1376
1505
|
if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
|
|
@@ -1423,13 +1552,7 @@ function validateTicket(path, errors) {
|
|
|
1423
1552
|
if (unresolved && !/^无[。.]?$/.test(unresolved.trim())) {
|
|
1424
1553
|
errors.push(`${basename(path)}: Ready Ticket has unresolved questions`);
|
|
1425
1554
|
}
|
|
1426
|
-
if (
|
|
1427
|
-
!lists.writable_paths.length &&
|
|
1428
|
-
!body.includes("无代码变更") &&
|
|
1429
|
-
!body.includes("仅文档")
|
|
1430
|
-
) {
|
|
1431
|
-
errors.push(`${basename(path)}: Ready Ticket has no writable_paths and no no-code explanation`);
|
|
1432
|
-
}
|
|
1555
|
+
if (!lists.writable_paths.length) errors.push(`${basename(path)}: Ready implementation Ticket requires writable_paths`);
|
|
1433
1556
|
if (new Set(["standard", "deep"]).has(meta.planning_depth)) {
|
|
1434
1557
|
for (const heading of ["## 5. 实现契约", "## 6. 执行路线"]) {
|
|
1435
1558
|
if (!body.includes(heading)) {
|
|
@@ -1444,6 +1567,9 @@ function validateTicket(path, errors) {
|
|
|
1444
1567
|
if (!verification || !verification.includes("|")) {
|
|
1445
1568
|
errors.push(`${basename(path)}: Ready Ticket has no usable verification matrix`);
|
|
1446
1569
|
}
|
|
1570
|
+
if (!verification.includes("E2E disposition") || !verification.includes("parent-candidate")) {
|
|
1571
|
+
errors.push(`${basename(path)}: Ready Ticket must define E2E disposition and parent-candidate owner/environment`);
|
|
1572
|
+
}
|
|
1447
1573
|
const acceptance = sectionBody(body, "## 10. 验收标准").toLowerCase();
|
|
1448
1574
|
if (!acceptance.includes("- [ ]") && !acceptance.includes("- [x]")) {
|
|
1449
1575
|
errors.push(`${basename(path)}: Ready Ticket has no acceptance checklist`);
|
|
@@ -1470,6 +1596,10 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1470
1596
|
"change",
|
|
1471
1597
|
"change_status",
|
|
1472
1598
|
"current_work",
|
|
1599
|
+
"works_run",
|
|
1600
|
+
"claimed_investigations",
|
|
1601
|
+
"execution_authorization",
|
|
1602
|
+
"leadership",
|
|
1473
1603
|
"created_at",
|
|
1474
1604
|
"updated_at",
|
|
1475
1605
|
"completed_at",
|
|
@@ -1477,11 +1607,14 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1477
1607
|
"archive_path",
|
|
1478
1608
|
"blockers",
|
|
1479
1609
|
"deviations",
|
|
1610
|
+
"worktrees",
|
|
1480
1611
|
];
|
|
1481
1612
|
const missing = required.filter((key) => !(key in data));
|
|
1482
1613
|
if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
|
|
1483
|
-
|
|
1484
|
-
|
|
1614
|
+
const unexpected = Object.keys(data).filter((key) => !required.includes(key));
|
|
1615
|
+
if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
1616
|
+
if (data.schema_version !== CHANGE_STATUS_SCHEMA_VERSION || data.artifact !== "change-status") {
|
|
1617
|
+
errors.push(`${basename(path)}: artifact/schema_version must be change-status/${CHANGE_STATUS_SCHEMA_VERSION}`);
|
|
1485
1618
|
}
|
|
1486
1619
|
if (data.change !== expectedChange) {
|
|
1487
1620
|
errors.push(`${basename(path)}: change must equal directory name ${expectedChange}`);
|
|
@@ -1489,17 +1622,45 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1489
1622
|
if (!new Set(["active", "blocked", "completed", "archived"]).has(data.change_status)) {
|
|
1490
1623
|
errors.push(`${basename(path)}: invalid change_status ${data.change_status}`);
|
|
1491
1624
|
}
|
|
1492
|
-
if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations)) {
|
|
1493
|
-
errors.push(`${basename(path)}: blockers and
|
|
1494
|
-
}
|
|
1495
|
-
if (data.worktrees !== undefined && !Array.isArray(data.worktrees)) {
|
|
1496
|
-
errors.push(`${basename(path)}: worktrees must be an array when present`);
|
|
1625
|
+
if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations) || !Array.isArray(data.worktrees)) {
|
|
1626
|
+
errors.push(`${basename(path)}: blockers, deviations, and worktrees must be arrays`);
|
|
1497
1627
|
}
|
|
1628
|
+
const seenWorktreeTickets = new Set();
|
|
1498
1629
|
for (const worktree of Array.isArray(data.worktrees) ? data.worktrees : []) {
|
|
1499
1630
|
if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) {
|
|
1500
1631
|
errors.push(`${basename(path)}: invalid worktree entry`);
|
|
1501
1632
|
continue;
|
|
1502
1633
|
}
|
|
1634
|
+
const requiredWorktreeKeys = [
|
|
1635
|
+
"ticket_id",
|
|
1636
|
+
"owner",
|
|
1637
|
+
"implementation_owner",
|
|
1638
|
+
"integration_owner",
|
|
1639
|
+
"provider",
|
|
1640
|
+
"base_sha",
|
|
1641
|
+
"parent_branch",
|
|
1642
|
+
"branch",
|
|
1643
|
+
"workspace_ref",
|
|
1644
|
+
"source_checkpoint",
|
|
1645
|
+
"integration",
|
|
1646
|
+
"status",
|
|
1647
|
+
"updated_at",
|
|
1648
|
+
];
|
|
1649
|
+
const missingWorktreeKeys = requiredWorktreeKeys.filter((key) => !(key in worktree));
|
|
1650
|
+
if (missingWorktreeKeys.length) {
|
|
1651
|
+
errors.push(`${basename(path)}: worktree integration contract missing ${JSON.stringify(missingWorktreeKeys)}`);
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
const unexpectedWorktreeKeys = Object.keys(worktree).filter((key) => !requiredWorktreeKeys.includes(key));
|
|
1655
|
+
if (unexpectedWorktreeKeys.length) {
|
|
1656
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has unexpected keys ${JSON.stringify(unexpectedWorktreeKeys.sort())}`);
|
|
1657
|
+
}
|
|
1658
|
+
if (!/^T-[0-9]{2,}$/.test(String(worktree.ticket_id))) {
|
|
1659
|
+
errors.push(`${basename(path)}: invalid worktree ticket_id ${worktree.ticket_id}`);
|
|
1660
|
+
} else if (seenWorktreeTickets.has(worktree.ticket_id)) {
|
|
1661
|
+
errors.push(`${basename(path)}: duplicate worktree for ${worktree.ticket_id}`);
|
|
1662
|
+
}
|
|
1663
|
+
seenWorktreeTickets.add(worktree.ticket_id);
|
|
1503
1664
|
const ref = worktree.workspace_ref;
|
|
1504
1665
|
if (
|
|
1505
1666
|
typeof ref !== "string" ||
|
|
@@ -1509,10 +1670,209 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1509
1670
|
errors.push(`${basename(path)}: workspace_ref must be a portable non-absolute locator`);
|
|
1510
1671
|
continue;
|
|
1511
1672
|
}
|
|
1512
|
-
if (worktree.
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1673
|
+
if (!VALID_WORKTREE_STATUS.has(worktree.status)) {
|
|
1674
|
+
errors.push(`${basename(path)}: invalid worktree status ${worktree.status}`);
|
|
1675
|
+
}
|
|
1676
|
+
if (worktree.provider !== "git") {
|
|
1677
|
+
errors.push(`${basename(path)}: Ticket worktree ${worktree.ticket_id} provider must be git`);
|
|
1678
|
+
}
|
|
1679
|
+
const expectedRef = `specdev-worktree/${expectedChange}/${worktree.ticket_id}`;
|
|
1680
|
+
if (ref !== expectedRef) {
|
|
1681
|
+
errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if ("terminal_action" in worktree) {
|
|
1685
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} obsolete terminal_action is not allowed`);
|
|
1686
|
+
}
|
|
1687
|
+
const requiredIntegrationKeys = [
|
|
1688
|
+
"owner",
|
|
1689
|
+
"implementation_owner",
|
|
1690
|
+
"integration_owner",
|
|
1691
|
+
"parent_branch",
|
|
1692
|
+
"source_checkpoint",
|
|
1693
|
+
"integration",
|
|
1694
|
+
];
|
|
1695
|
+
const missingIntegrationKeys = requiredIntegrationKeys.filter((key) => !(key in worktree));
|
|
1696
|
+
if (missingIntegrationKeys.length) {
|
|
1697
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration contract missing ${JSON.stringify(missingIntegrationKeys)}`);
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
for (const key of ["owner", "implementation_owner", "integration_owner", "base_sha", "parent_branch", "branch", "updated_at"]) {
|
|
1701
|
+
if (typeof worktree[key] !== "string" || !worktree[key].trim()) {
|
|
1702
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${key} is required`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (typeof worktree.parent_branch !== "string" || !worktree.parent_branch.trim()) {
|
|
1706
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch is required`);
|
|
1707
|
+
} else if (worktree.parent_branch === worktree.branch) {
|
|
1708
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch must differ from branch`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const sourceRequired = new Set(["review", "integrating", "integrated", "removed"]).has(worktree.status);
|
|
1712
|
+
if (sourceRequired && (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())) {
|
|
1713
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_checkpoint`);
|
|
1714
|
+
} else if (
|
|
1715
|
+
worktree.source_checkpoint !== null &&
|
|
1716
|
+
(typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())
|
|
1717
|
+
) {
|
|
1718
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_checkpoint must be null or non-empty`);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const integration = worktree.integration;
|
|
1722
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration)) {
|
|
1723
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration must be an object`);
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
const requiredResultKeys = [
|
|
1727
|
+
"status",
|
|
1728
|
+
"parent_ref",
|
|
1729
|
+
"parent_before_sha",
|
|
1730
|
+
"source_sha",
|
|
1731
|
+
"candidate_sha",
|
|
1732
|
+
"candidate_tree_sha",
|
|
1733
|
+
"candidate_branch",
|
|
1734
|
+
"candidate_workspace_ref",
|
|
1735
|
+
"result_sha",
|
|
1736
|
+
"method",
|
|
1737
|
+
"conflict_paths",
|
|
1738
|
+
"verification",
|
|
1739
|
+
"full_suite",
|
|
1740
|
+
"e2e",
|
|
1741
|
+
"evidence",
|
|
1742
|
+
"attempts",
|
|
1743
|
+
"promotion_status",
|
|
1744
|
+
];
|
|
1745
|
+
const missingResultKeys = requiredResultKeys.filter((key) => !(key in integration));
|
|
1746
|
+
if (missingResultKeys.length) {
|
|
1747
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration missing ${JSON.stringify(missingResultKeys)}`);
|
|
1748
|
+
}
|
|
1749
|
+
const unexpectedResultKeys = Object.keys(integration).filter((key) => !requiredResultKeys.includes(key));
|
|
1750
|
+
if (unexpectedResultKeys.length) {
|
|
1751
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration has unexpected keys ${JSON.stringify(unexpectedResultKeys.sort())}`);
|
|
1752
|
+
}
|
|
1753
|
+
if (!VALID_INTEGRATION_STATUS.has(integration.status)) {
|
|
1754
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration status ${integration.status}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (!VALID_INTEGRATION_METHOD.has(integration.method)) {
|
|
1757
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration method ${integration.method}`);
|
|
1758
|
+
}
|
|
1759
|
+
if (!VALID_INTEGRATION_VERIFICATION.has(integration.verification)) {
|
|
1760
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration verification ${integration.verification}`);
|
|
1761
|
+
}
|
|
1762
|
+
if (!Array.isArray(integration.conflict_paths)) {
|
|
1763
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} conflict_paths must be an array`);
|
|
1764
|
+
}
|
|
1765
|
+
if (!Number.isInteger(integration.attempts) || integration.attempts < 0) {
|
|
1766
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} attempts must be a non-negative integer`);
|
|
1767
|
+
}
|
|
1768
|
+
if (
|
|
1769
|
+
typeof integration.evidence !== "string" ||
|
|
1770
|
+
!/^<Path>\{roots\.state\}\/specdev\/changes\/[^<]+\/evidence\/T-[0-9]{2,}\.md<\/Path>$/.test(integration.evidence)
|
|
1771
|
+
) {
|
|
1772
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration evidence must be a rooted Ticket Evidence path`);
|
|
1773
|
+
}
|
|
1774
|
+
if (!new Set(["pending", "applying", "applied", "failed", "stale"]).has(integration.promotion_status)) {
|
|
1775
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid promotion_status ${integration.promotion_status}`);
|
|
1776
|
+
}
|
|
1777
|
+
for (const [name, suite] of [["full_suite", integration.full_suite], ["e2e", integration.e2e]]) {
|
|
1778
|
+
if (!suite || typeof suite !== "object" || Array.isArray(suite) || typeof suite.required !== "boolean" || !VALID_E2E_STATUS.has(suite.status)) {
|
|
1779
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} contract is invalid`);
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const unexpectedSuiteKeys = Object.keys(suite).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
|
|
1783
|
+
if (unexpectedSuiteKeys.length) errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} has unexpected keys ${JSON.stringify(unexpectedSuiteKeys.sort())}`);
|
|
1784
|
+
if (suite.required === false && (suite.status !== "not-required" || typeof suite.reason !== "string" || !suite.reason.trim())) {
|
|
1785
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required ${name} needs not-required status and reason`);
|
|
1786
|
+
}
|
|
1787
|
+
if (suite.required === true && suite.status === "not-required") {
|
|
1788
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required ${name} cannot be not-required`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
const e2e = integration.e2e;
|
|
1792
|
+
if (
|
|
1793
|
+
!e2e ||
|
|
1794
|
+
typeof e2e !== "object" ||
|
|
1795
|
+
Array.isArray(e2e) ||
|
|
1796
|
+
typeof e2e.required !== "boolean" ||
|
|
1797
|
+
!VALID_E2E_STATUS.has(e2e.status)
|
|
1798
|
+
) {
|
|
1799
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e contract is invalid`);
|
|
1800
|
+
} else {
|
|
1801
|
+
const unexpectedE2eKeys = Object.keys(e2e).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
|
|
1802
|
+
if (unexpectedE2eKeys.length) {
|
|
1803
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e has unexpected keys ${JSON.stringify(unexpectedE2eKeys.sort())}`);
|
|
1804
|
+
}
|
|
1805
|
+
if (e2e.required === false && e2e.status !== "not-required") {
|
|
1806
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required E2E must use status=not-required`);
|
|
1807
|
+
}
|
|
1808
|
+
if (e2e.required === true && e2e.status === "not-required") {
|
|
1809
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required E2E cannot be not-required`);
|
|
1810
|
+
}
|
|
1811
|
+
if (
|
|
1812
|
+
e2e.required === true &&
|
|
1813
|
+
e2e.status === "passed" &&
|
|
1814
|
+
(typeof e2e.evidence !== "string" || !e2e.evidence.trim())
|
|
1815
|
+
) {
|
|
1816
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} passed required E2E needs evidence`);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const integrationLifecycleActive = new Set(["integrating", "integrated", "removed"]).has(worktree.status);
|
|
1820
|
+
if (integrationLifecycleActive) {
|
|
1821
|
+
if (typeof integration.parent_before_sha !== "string" || !integration.parent_before_sha.trim()) {
|
|
1822
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires parent_before_sha`);
|
|
1823
|
+
}
|
|
1824
|
+
if (typeof integration.source_sha !== "string" || !integration.source_sha.trim()) {
|
|
1825
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_sha`);
|
|
1826
|
+
} else if (integration.source_sha !== worktree.source_checkpoint) {
|
|
1827
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_sha must equal source_checkpoint`);
|
|
1828
|
+
}
|
|
1829
|
+
if (typeof integration.candidate_sha !== "string" || !integration.candidate_sha.trim()) {
|
|
1830
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires candidate_sha`);
|
|
1831
|
+
}
|
|
1832
|
+
const expectedCandidateBranch = `speculo/integration/${expectedChange}/${worktree.ticket_id}`;
|
|
1833
|
+
if (integration.candidate_branch !== expectedCandidateBranch) {
|
|
1834
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_branch must equal ${expectedCandidateBranch}`);
|
|
1835
|
+
}
|
|
1836
|
+
const expectedCandidateRef = `specdev-worktree/.integration/${expectedChange}/${worktree.ticket_id}`;
|
|
1837
|
+
if (integration.candidate_workspace_ref !== expectedCandidateRef) {
|
|
1838
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_workspace_ref must equal ${expectedCandidateRef}`);
|
|
1839
|
+
}
|
|
1840
|
+
if (!new Set(["fast-forward", "merge-commit"]).has(integration.method)) {
|
|
1841
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires an integration method`);
|
|
1842
|
+
}
|
|
1843
|
+
if (!Number.isInteger(integration.attempts) || integration.attempts < 1) {
|
|
1844
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires attempts >= 1`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (worktree.status === "integrating" && integration.status !== "candidate") {
|
|
1848
|
+
errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=candidate`);
|
|
1849
|
+
}
|
|
1850
|
+
if (new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
1851
|
+
if (
|
|
1852
|
+
integration.status !== "passed" ||
|
|
1853
|
+
integration.verification !== "passed" ||
|
|
1854
|
+
!new Set(["fast-forward", "merge-commit"]).has(integration.method) ||
|
|
1855
|
+
typeof integration.result_sha !== "string" ||
|
|
1856
|
+
!integration.result_sha.trim() ||
|
|
1857
|
+
integration.result_sha !== integration.candidate_sha ||
|
|
1858
|
+
!e2e ||
|
|
1859
|
+
!new Set(["not-required", "passed"]).has(e2e.status)
|
|
1860
|
+
) {
|
|
1861
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} requires passed candidate/result/E2E state`);
|
|
1862
|
+
}
|
|
1863
|
+
if (integration.method === "fast-forward") {
|
|
1864
|
+
if (integration.candidate_sha !== worktree.source_checkpoint) {
|
|
1865
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward candidate/result must equal source_checkpoint`);
|
|
1866
|
+
}
|
|
1867
|
+
if (Array.isArray(integration.conflict_paths) && integration.conflict_paths.length > 0) {
|
|
1868
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward cannot record conflict_paths`);
|
|
1869
|
+
}
|
|
1870
|
+
} else if (
|
|
1871
|
+
integration.method === "merge-commit" &&
|
|
1872
|
+
typeof integration.candidate_sha === "string" &&
|
|
1873
|
+
(integration.candidate_sha === worktree.source_checkpoint || integration.candidate_sha === integration.parent_before_sha)
|
|
1874
|
+
) {
|
|
1875
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} merge-commit candidate must be a distinct checkpoint`);
|
|
1516
1876
|
}
|
|
1517
1877
|
}
|
|
1518
1878
|
}
|
|
@@ -1624,6 +1984,25 @@ function validateChange(change, stage = null) {
|
|
|
1624
1984
|
}
|
|
1625
1985
|
}
|
|
1626
1986
|
|
|
1987
|
+
if (stage === "implement" && ticketMode && changeStatus) {
|
|
1988
|
+
const worktreesByTicket = new Map(
|
|
1989
|
+
(Array.isArray(changeStatus.worktrees) ? changeStatus.worktrees : [])
|
|
1990
|
+
.filter((entry) => entry && typeof entry === "object")
|
|
1991
|
+
.map((entry) => [String(entry.ticket_id), entry]),
|
|
1992
|
+
);
|
|
1993
|
+
for (const [ticketId, artifact] of tickets) {
|
|
1994
|
+
if (new Set(["draft", "cancelled"]).has(artifact.meta.status)) continue;
|
|
1995
|
+
if (!worktreesByTicket.has(ticketId)) {
|
|
1996
|
+
errors.push(`${ticketId}: Implement stage requires one Ticket source worktree record`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
for (const ticketId of worktreesByTicket.keys()) {
|
|
2000
|
+
if (!tickets.has(ticketId)) {
|
|
2001
|
+
errors.push(`${ticketId}: worktree record has no matching Ticket`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1627
2006
|
if (spec) {
|
|
1628
2007
|
const declaredContracts = new Set(spec.body.match(/\bAC-\d+\b/g) ?? []);
|
|
1629
2008
|
const coveredContracts = new Set(
|
|
@@ -1710,18 +2089,29 @@ function validateChange(change, stage = null) {
|
|
|
1710
2089
|
}
|
|
1711
2090
|
const text = readText(evidence);
|
|
1712
2091
|
for (const heading of [
|
|
1713
|
-
"## 2.
|
|
1714
|
-
"## 3.
|
|
1715
|
-
"## 4.
|
|
1716
|
-
"## 5.
|
|
1717
|
-
"## 6.
|
|
1718
|
-
"## 7.
|
|
1719
|
-
"## 8.
|
|
2092
|
+
"## 2. Lead Dispatch And Candidate Return",
|
|
2093
|
+
"## 3. 修改范围与路径所有权",
|
|
2094
|
+
"## 4. 验收与合同映射",
|
|
2095
|
+
"## 5. Source-worktree 验证",
|
|
2096
|
+
"## 6. 双轴审查",
|
|
2097
|
+
"## 7. Parent-candidate 集成验证",
|
|
2098
|
+
"## 8. 偏差与决策",
|
|
2099
|
+
"## 9. 残余风险与交付定位",
|
|
1720
2100
|
]) {
|
|
1721
2101
|
if (!text.includes(heading)) {
|
|
1722
2102
|
errors.push(`${toPosix(relative(change, evidence))}: missing '${heading}'`);
|
|
1723
2103
|
}
|
|
1724
2104
|
}
|
|
2105
|
+
const sourceVerification = sectionBody(text, "## 5. Source-worktree 验证");
|
|
2106
|
+
if (/E2E[^\n]*(?:\bpass(?:ed)?\b|通过)/i.test(sourceVerification) && !/不得|禁止|not-run/i.test(sourceVerification)) {
|
|
2107
|
+
errors.push(`${toPosix(relative(change, evidence))}: E2E pass cannot come from source-worktree`);
|
|
2108
|
+
}
|
|
2109
|
+
const worktree = Array.isArray(changeStatus?.worktrees)
|
|
2110
|
+
? changeStatus.worktrees.find((entry) => entry?.ticket_id === ticketId)
|
|
2111
|
+
: null;
|
|
2112
|
+
if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
2113
|
+
errors.push(`${ticketId}: status is done but no integrated or removed Ticket worktree exists`);
|
|
2114
|
+
}
|
|
1725
2115
|
}
|
|
1726
2116
|
|
|
1727
2117
|
if (goalPlan?.meta.ready_for_execution === true) {
|