@namewta/speculo 0.6.0 → 0.7.0
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 +11 -11
- package/dist/src/cli.js +36 -128
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -4
- package/dist/src/index.js +173 -151
- package/dist/src/index.js.map +1 -1
- package/package.json +6 -4
- package/template/.speculo/README.md +4 -0
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +133 -141
- package/template/canonical/canonical-specdev-goal-plan.md +394 -377
- package/template/canonical/canonical-specdev-grill-with-docs.md +241 -178
- package/template/canonical/canonical-specdev-spec.md +132 -122
- package/template/canonical/canonical-specdev-tickets.md +176 -156
- package/template/canonical/canonical-specdev-wayfinder.md +106 -108
- package/template/commands/archive-and-consolidate.md +10 -8
- package/template/commands/handoff.md +2 -0
- package/template/commands/retro.md +3 -3
- package/template/commands/status.md +5 -4
- package/template/skills/archive-and-consolidate/SKILL.md +5 -9
- package/template/skills/archive-and-consolidate/assets/archive-plan-template.md +1 -1
- package/template/skills/archive-and-consolidate/references/archive-rules.md +4 -4
- package/template/skills/archive-and-consolidate/references/consolidation-rules.md +7 -8
- package/template/skills/archive-and-consolidate/references/knowledge-graduation.md +5 -2
- package/template/skills/github-npm-ops/SKILL.md +4 -2
- package/template/skills/github-npm-ops/references/issue-transport.md +26 -0
- package/template/skills/github-npm-ops/references/preflight-checklist.md +1 -1
- package/template/skills/github-npm-ops/scripts/issue-transport.mjs +227 -0
- package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +31 -145
- package/template/workflows/specdev/A-archive-and-consolidate/consolidation-interview.md +4 -6
- package/template/workflows/specdev/C-code-review/C-code-review.md +42 -0
- package/template/workflows/specdev/C-code-review/code-review-template.md +42 -0
- package/template/workflows/specdev/D-diagnose-bugs/D-diagnose-bugs.md +45 -48
- package/template/workflows/specdev/D-diagnose-bugs/diagnosis-template.md +44 -40
- package/template/workflows/specdev/D-diagnose-bugs/feedback-loop.md +41 -0
- package/template/workflows/specdev/D-diagnose-bugs/hypothesis-and-instrumentation.md +32 -0
- package/template/workflows/specdev/D-diagnose-bugs/scripts/hitl-loop.template.sh +26 -0
- package/template/workflows/specdev/E-engineering-cognitive-mentor/E-engineering-cognitive-mentor.md +3 -3
- package/template/workflows/specdev/E-engineering-cognitive-mentor/persistence-and-resume.md +5 -20
- package/template/workflows/specdev/G-grill-with-docs/G-grill-with-docs.md +17 -10
- package/template/workflows/specdev/G-grill-with-docs/adr-format.md +31 -17
- package/template/workflows/specdev/G-grill-with-docs/context-format.md +9 -29
- package/template/workflows/specdev/G-grill-with-docs/domain-modeling-rules.md +10 -5
- package/template/workflows/specdev/G-grill-with-docs/stakeholder-questionnaire.md +45 -0
- package/template/workflows/specdev/I-implement/I-implement.md +29 -14
- package/template/workflows/specdev/I-implement/delegated-evidence-template.md +11 -0
- package/template/workflows/specdev/I-implement/evidence-template.md +24 -10
- package/template/workflows/specdev/I-implement/execution-preflight.md +4 -4
- package/template/workflows/specdev/I-implement/merge-conflict-protocol.md +20 -0
- package/template/workflows/specdev/I-implement/tdd-mocking.md +19 -0
- package/template/workflows/specdev/I-implement/tdd-rules.md +14 -12
- package/template/workflows/specdev/I-implement/tdd-test-design.md +25 -0
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +12 -9
- package/template/workflows/specdev/I-init-setup/config-template.json +1 -1
- package/template/workflows/specdev/I-init-setup/domain-layout-template.md +2 -2
- package/template/workflows/specdev/I-init-setup/status-template.json +2 -3
- package/template/workflows/specdev/I-init-setup/tracking-template.md +3 -0
- package/template/workflows/specdev/INDEX.md +64 -26
- package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +48 -38
- package/template/workflows/specdev/P-goal-plan/completion-control.md +19 -53
- package/template/workflows/specdev/P-goal-plan/delegated-execution-template.md +33 -0
- package/template/workflows/specdev/P-goal-plan/delegated-execution.md +53 -0
- package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +9 -40
- package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +16 -68
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +20 -48
- package/template/workflows/specdev/P-prototype/P-prototype.md +46 -0
- package/template/workflows/specdev/P-prototype/logic-prototype.md +24 -0
- package/template/workflows/specdev/P-prototype/prototype-record-template.md +46 -0
- package/template/workflows/specdev/P-prototype/ui-prototype.md +21 -0
- package/template/workflows/specdev/R-review-architecture/R-review-architecture.md +1 -1
- package/template/workflows/specdev/S-spec/S-spec.md +2 -1
- package/template/workflows/specdev/T-tickets/T-tickets.md +3 -2
- package/template/workflows/specdev/T-tickets/ticket-readiness.md +1 -1
- package/template/workflows/specdev/T-tickets/ticket-template.md +1 -1
- package/template/workflows/specdev/T-tickets/tickets-map-template.md +1 -1
- package/template/workflows/specdev/T-triage/T-triage.md +65 -26
- package/template/workflows/specdev/T-triage/intake-protocol.md +32 -0
- package/template/workflows/specdev/T-triage/reconcile-protocol.md +34 -0
- package/template/workflows/specdev/T-triage/source-template.md +30 -0
- package/template/workflows/specdev/T-triage/triage-template.md +38 -24
- package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +3 -1
- package/template/workflows/specdev/_state/status.json +2 -3
- package/template/workflows/specdev/common/README.md +9 -2
- package/template/workflows/specdev/common/rules/artifact-contract.md +20 -9
- package/template/workflows/specdev/common/rules/change-completion.md +33 -0
- package/template/workflows/specdev/common/rules/deviation-control.md +2 -2
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +1 -1
- package/template/workflows/specdev/common/rules/path-ownership.md +3 -3
- package/template/workflows/specdev/common/schemas/change-status.schema.json +43 -0
- package/template/workflows/specdev/common/schemas/code-review.schema.json +22 -0
- package/template/workflows/specdev/common/schemas/diagnosis.schema.json +19 -0
- package/template/workflows/specdev/common/schemas/prototype-record.schema.json +24 -0
- package/template/workflows/specdev/common/schemas/source.schema.json +20 -0
- package/template/workflows/specdev/common/schemas/status.schema.json +20 -78
- package/template/workflows/specdev/common/schemas/triage.schema.json +21 -0
- package/template/workflows/specdev/common/skills/code-review/SKILL.md +49 -0
- package/template/workflows/specdev/common/skills/code-review/references/fowler-smells.md +18 -0
- package/template/workflows/specdev/common/skills/code-review/references/reviewer-contracts.md +13 -0
- package/template/workflows/specdev/common/skills/code-review/references/source-discovery.md +22 -0
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +12 -9
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +18 -13
- package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +8 -6
- package/template/workflows/specdev/common/skills/research/SKILL.md +38 -26
- package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +3 -5
- package/template/workflows/specdev/common/tools/README.md +3 -0
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +496 -30
- package/dist/src/migrate.d.ts +0 -38
- package/dist/src/migrate.js +0 -642
- package/dist/src/migrate.js.map +0 -1
- package/dist/src/skills-mirror.d.ts +0 -38
- package/dist/src/skills-mirror.js +0 -160
- package/dist/src/skills-mirror.js.map +0 -1
- package/template/workflows/specdev/A-archive-and-consolidate/archive-checklist.md +0 -15
- package/template/workflows/specdev/A-archive-and-consolidate/knowledge-promotion-rules.md +0 -32
- package/template/workflows/specdev/I-implement/code-review-process.md +0 -17
- package/template/workflows/specdev/I-implement/tdd-examples.md +0 -14
- package/template/workflows/specdev/I-init-setup/status-labels-template.md +0 -55
|
@@ -19,17 +19,22 @@ import { dirname, basename, extname, join, relative, resolve, sep } from "node:p
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
21
|
const SCHEMA_VERSION = 3;
|
|
22
|
+
const GLOBAL_STATUS_SCHEMA_VERSION = 4;
|
|
22
23
|
const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
|
|
23
24
|
const STATE_PREFIX = "{roots.state}/specdev/";
|
|
25
|
+
const SKILLS_PREFIX = "{roots.skills}/";
|
|
26
|
+
const COMMANDS_PREFIX = "{roots.commands}/";
|
|
24
27
|
|
|
25
28
|
const EXPECTED_WORKS = new Set([
|
|
26
29
|
"A-archive-and-consolidate",
|
|
30
|
+
"C-code-review",
|
|
27
31
|
"D-diagnose-bugs",
|
|
28
32
|
"E-engineering-cognitive-mentor",
|
|
29
33
|
"G-grill-with-docs",
|
|
30
34
|
"I-implement",
|
|
31
35
|
"I-init-setup",
|
|
32
36
|
"P-goal-plan",
|
|
37
|
+
"P-prototype",
|
|
33
38
|
"R-review-architecture",
|
|
34
39
|
"S-spec",
|
|
35
40
|
"T-tickets",
|
|
@@ -72,6 +77,19 @@ const VALID_WAYFINDER_RESOLUTION = new Set([
|
|
|
72
77
|
"superseded",
|
|
73
78
|
"cancelled",
|
|
74
79
|
]);
|
|
80
|
+
const VALID_STAGES = new Set([
|
|
81
|
+
"triage",
|
|
82
|
+
"diagnosis",
|
|
83
|
+
"grill",
|
|
84
|
+
"spec",
|
|
85
|
+
"tickets",
|
|
86
|
+
"goal-plan",
|
|
87
|
+
"implement",
|
|
88
|
+
"review",
|
|
89
|
+
"prototype",
|
|
90
|
+
"wayfinder",
|
|
91
|
+
"complete",
|
|
92
|
+
]);
|
|
75
93
|
const REQUIRED_TICKET_KEYS = new Set([
|
|
76
94
|
"schema_version",
|
|
77
95
|
"artifact",
|
|
@@ -109,6 +127,31 @@ const REQUIRED_GOAL_PLAN_SECTIONS = new Set([
|
|
|
109
127
|
"## 5. Constraints, Risk and Recovery",
|
|
110
128
|
"## 6. Progress and Decisions",
|
|
111
129
|
]);
|
|
130
|
+
const DELEGATED_GOAL_PLAN_TRIGGERS = [
|
|
131
|
+
"## Delegated Execution Addendum",
|
|
132
|
+
"### Delivery Contract",
|
|
133
|
+
"### Per-Ticket Dispatch Packets",
|
|
134
|
+
"### Candidate Delivery Return and Lead Integration",
|
|
135
|
+
"Lead / Provider",
|
|
136
|
+
"Lead:",
|
|
137
|
+
"Lead:",
|
|
138
|
+
"Worker:",
|
|
139
|
+
"Worker:",
|
|
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:",
|
|
154
|
+
];
|
|
112
155
|
const STATE_ARTIFACT_BASENAMES = new Set([
|
|
113
156
|
"spec.md",
|
|
114
157
|
"tickets-map.md",
|
|
@@ -120,13 +163,14 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
120
163
|
".status.json",
|
|
121
164
|
"triage.md",
|
|
122
165
|
"diagnosis.md",
|
|
123
|
-
"source
|
|
166
|
+
"source.md",
|
|
124
167
|
"architecture-review.md",
|
|
125
168
|
"architecture-review.html",
|
|
126
169
|
"wayfinder-map.md",
|
|
127
170
|
"design-tree.json",
|
|
128
171
|
]);
|
|
129
172
|
const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
|
|
173
|
+
"source-issue.md",
|
|
130
174
|
"input-validation.md",
|
|
131
175
|
"vision-sections.md",
|
|
132
176
|
"execution-sections.md",
|
|
@@ -347,8 +391,23 @@ function validatePathValue(value, { allowProject = true } = {}) {
|
|
|
347
391
|
}
|
|
348
392
|
return null;
|
|
349
393
|
}
|
|
394
|
+
for (const [prefix, name] of [[SKILLS_PREFIX, "skills"], [COMMANDS_PREFIX, "commands"]]) {
|
|
395
|
+
if (value.startsWith(prefix)) {
|
|
396
|
+
const suffix = value.slice(prefix.length);
|
|
397
|
+
if (
|
|
398
|
+
!suffix ||
|
|
399
|
+
suffix.startsWith("/") ||
|
|
400
|
+
suffix.startsWith("./") ||
|
|
401
|
+
suffix.startsWith("../") ||
|
|
402
|
+
suffix.includes("/../")
|
|
403
|
+
) {
|
|
404
|
+
return `${name} Path is not canonical`;
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
350
409
|
if (value.includes("{roots.")) {
|
|
351
|
-
return "unknown or incomplete root variable
|
|
410
|
+
return "unknown or incomplete root variable";
|
|
352
411
|
}
|
|
353
412
|
if (!allowProject) return "this field requires a rooted workflow or state Path";
|
|
354
413
|
if (
|
|
@@ -410,6 +469,17 @@ function validateDocumentReferences(root) {
|
|
|
410
469
|
);
|
|
411
470
|
}
|
|
412
471
|
}
|
|
472
|
+
} else if (payload.startsWith(SKILLS_PREFIX) || payload.startsWith(COMMANDS_PREFIX)) {
|
|
473
|
+
const isSkill = payload.startsWith(SKILLS_PREFIX);
|
|
474
|
+
const prefix = isSkill ? SKILLS_PREFIX : COMMANDS_PREFIX;
|
|
475
|
+
const referenced = payload.slice(prefix.length);
|
|
476
|
+
if (!PLACEHOLDER_RE.test(referenced)) {
|
|
477
|
+
const templateRoot = resolve(root, "..", "..");
|
|
478
|
+
const target = join(templateRoot, isSkill ? "skills" : "commands", referenced);
|
|
479
|
+
if (!existsSync(target)) {
|
|
480
|
+
errors.push(`${label}:${lineNumber(text, match.index)}: broken public Path ${payload}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
413
483
|
}
|
|
414
484
|
}
|
|
415
485
|
|
|
@@ -499,6 +569,54 @@ function validateJsonFiles(root) {
|
|
|
499
569
|
return errors;
|
|
500
570
|
}
|
|
501
571
|
|
|
572
|
+
function validateGlobalStatusAssets(root) {
|
|
573
|
+
const errors = [];
|
|
574
|
+
const paths = [join(root, "I-init-setup", "status-template.json")];
|
|
575
|
+
if (isDirectory(join(root, "_state"))) {
|
|
576
|
+
paths.unshift(join(root, "_state", "status.json"));
|
|
577
|
+
}
|
|
578
|
+
for (const path of paths) {
|
|
579
|
+
if (!isFile(path)) {
|
|
580
|
+
errors.push(`missing global status seed ${toPosix(relative(root, path))}`);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const data = JSON.parse(readText(path));
|
|
584
|
+
const keys = Object.keys(data).sort();
|
|
585
|
+
const expected = ["active", "archived", "schema_version", "workflow"];
|
|
586
|
+
if (JSON.stringify(keys) !== JSON.stringify(expected)) {
|
|
587
|
+
errors.push(`${toPosix(relative(root, path))}: global status must contain only ${expected.join(", ")}`);
|
|
588
|
+
}
|
|
589
|
+
if (
|
|
590
|
+
data.schema_version !== GLOBAL_STATUS_SCHEMA_VERSION ||
|
|
591
|
+
data.workflow !== "specdev" ||
|
|
592
|
+
!Array.isArray(data.active) ||
|
|
593
|
+
!Array.isArray(data.archived)
|
|
594
|
+
) {
|
|
595
|
+
errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status v4 seed`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const schemaPath = join(root, "common", "schemas", "status.schema.json");
|
|
600
|
+
if (!isFile(schemaPath)) {
|
|
601
|
+
errors.push("missing common/schemas/status.schema.json");
|
|
602
|
+
return errors;
|
|
603
|
+
}
|
|
604
|
+
const schema = JSON.parse(readText(schemaPath));
|
|
605
|
+
if (
|
|
606
|
+
schema.$id !== "urn:speculo:specdev:status:v4" ||
|
|
607
|
+
schema.properties?.schema_version?.const !== GLOBAL_STATUS_SCHEMA_VERSION ||
|
|
608
|
+
schema.additionalProperties !== false
|
|
609
|
+
) {
|
|
610
|
+
errors.push("status.schema.json must define the strict SpecDev global status v4 contract");
|
|
611
|
+
}
|
|
612
|
+
for (const removed of ["work_history", "completed", "result"]) {
|
|
613
|
+
if (JSON.stringify(schema).includes(`\"${removed}\"`)) {
|
|
614
|
+
errors.push(`status.schema.json still contains removed global field ${removed}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return errors;
|
|
618
|
+
}
|
|
619
|
+
|
|
502
620
|
function capabilityChecks(root) {
|
|
503
621
|
const checks = new Map([
|
|
504
622
|
[
|
|
@@ -508,9 +626,9 @@ function capabilityChecks(root) {
|
|
|
508
626
|
[
|
|
509
627
|
"tracking-template",
|
|
510
628
|
"domain-layout-template",
|
|
511
|
-
"status-labels-template",
|
|
512
629
|
"config-template",
|
|
513
630
|
"change-status-template",
|
|
631
|
+
"specdev-worktree/",
|
|
514
632
|
],
|
|
515
633
|
],
|
|
516
634
|
],
|
|
@@ -518,7 +636,7 @@ function capabilityChecks(root) {
|
|
|
518
636
|
"triage",
|
|
519
637
|
[
|
|
520
638
|
join(root, "T-triage", "T-triage.md"),
|
|
521
|
-
["source
|
|
639
|
+
["source.md", "intake", "reconcile", "唯一权威", "远程写入为零"],
|
|
522
640
|
],
|
|
523
641
|
],
|
|
524
642
|
[
|
|
@@ -545,9 +663,10 @@ function capabilityChecks(root) {
|
|
|
545
663
|
"DAG",
|
|
546
664
|
"Gate",
|
|
547
665
|
"Wave",
|
|
548
|
-
"
|
|
549
|
-
"
|
|
550
|
-
"
|
|
666
|
+
"普通 Goal Plan",
|
|
667
|
+
"委派 Goal Plan",
|
|
668
|
+
"每次向用户询问",
|
|
669
|
+
"delegated-execution.md",
|
|
551
670
|
"Definition of Done",
|
|
552
671
|
],
|
|
553
672
|
],
|
|
@@ -559,6 +678,20 @@ function capabilityChecks(root) {
|
|
|
559
678
|
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "delegated execution", "subagent-delivery", "提交"],
|
|
560
679
|
],
|
|
561
680
|
],
|
|
681
|
+
[
|
|
682
|
+
"code-review",
|
|
683
|
+
[
|
|
684
|
+
join(root, "C-code-review", "C-code-review.md"),
|
|
685
|
+
["git diff <fixed>...<head>", "固定点", "标准轴", "规范轴", "未合并排名", "reviews/CR-###.md"],
|
|
686
|
+
],
|
|
687
|
+
],
|
|
688
|
+
[
|
|
689
|
+
"prototype",
|
|
690
|
+
[
|
|
691
|
+
join(root, "P-prototype", "P-prototype.md"),
|
|
692
|
+
["一个问题", "Logic", "UI", "临时 branch/worktree", "promotion target", "main"],
|
|
693
|
+
],
|
|
694
|
+
],
|
|
562
695
|
[
|
|
563
696
|
"wayfinder",
|
|
564
697
|
[
|
|
@@ -577,21 +710,21 @@ function capabilityChecks(root) {
|
|
|
577
710
|
"archive",
|
|
578
711
|
[
|
|
579
712
|
join(root, "A-archive-and-consolidate", "A-archive-and-consolidate.md"),
|
|
580
|
-
["
|
|
713
|
+
["archive-single", "dry-run", "external_action", "consolidate-from-code", "archive-and-consolidate"],
|
|
581
714
|
],
|
|
582
715
|
],
|
|
583
716
|
[
|
|
584
717
|
"grill",
|
|
585
718
|
[
|
|
586
719
|
join(root, "G-grill-with-docs", "G-grill-with-docs.md"),
|
|
587
|
-
["完整 frontier", "design-tree.json", "LOG.md", "CONTEXT.md", "ADR.md", "推荐答案"],
|
|
720
|
+
["完整 frontier", "design-tree.json", "LOG.md", "CONTEXT.md", "ADR.md", "推荐答案", "永久 namespace 对 G 只读"],
|
|
588
721
|
],
|
|
589
722
|
],
|
|
590
723
|
[
|
|
591
724
|
"diagnosis",
|
|
592
725
|
[
|
|
593
726
|
join(root, "D-diagnose-bugs", "D-diagnose-bugs.md"),
|
|
594
|
-
["
|
|
727
|
+
["红灯", "反馈回路", "最小化", "可证伪", "根因", "回归测试"],
|
|
595
728
|
],
|
|
596
729
|
],
|
|
597
730
|
]);
|
|
@@ -633,6 +766,46 @@ function capabilityChecks(root) {
|
|
|
633
766
|
if (isFile(mapTemplate) && /^## (?:开放 Tickets|调查清单)/m.test(readText(mapTemplate))) {
|
|
634
767
|
errors.push("Wayfinder map template must query open Tickets instead of caching them");
|
|
635
768
|
}
|
|
769
|
+
|
|
770
|
+
const ordinaryGoalPlanTemplate = join(root, "P-goal-plan", "goal-plan-template.md");
|
|
771
|
+
if (isFile(ordinaryGoalPlanTemplate)) {
|
|
772
|
+
const text = readText(ordinaryGoalPlanTemplate);
|
|
773
|
+
for (const forbidden of [
|
|
774
|
+
"Lead",
|
|
775
|
+
"Subagent",
|
|
776
|
+
"subagent",
|
|
777
|
+
"Provider",
|
|
778
|
+
"Delivery Contract",
|
|
779
|
+
"Dispatch Packet",
|
|
780
|
+
"Worker",
|
|
781
|
+
"max_correction_rounds",
|
|
782
|
+
"execution_model",
|
|
783
|
+
]) {
|
|
784
|
+
if (text.includes(forbidden)) {
|
|
785
|
+
errors.push(`ordinary Goal Plan template contains delegated marker ${forbidden}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const delegatedTemplate = join(root, "P-goal-plan", "delegated-execution-template.md");
|
|
791
|
+
if (!isFile(delegatedTemplate)) {
|
|
792
|
+
errors.push("missing delegated Goal Plan template");
|
|
793
|
+
} else {
|
|
794
|
+
const text = readText(delegatedTemplate);
|
|
795
|
+
const missing = REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.filter((marker) => !text.includes(marker));
|
|
796
|
+
if (missing.length) errors.push(`delegated Goal Plan template lost markers: ${JSON.stringify(missing)}`);
|
|
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);
|
|
636
809
|
return errors;
|
|
637
810
|
}
|
|
638
811
|
|
|
@@ -701,6 +874,7 @@ function selfCheck(root) {
|
|
|
701
874
|
}
|
|
702
875
|
|
|
703
876
|
errors.push(...validateJsonFiles(root));
|
|
877
|
+
errors.push(...validateGlobalStatusAssets(root));
|
|
704
878
|
const references = validateDocumentReferences(root);
|
|
705
879
|
errors.push(...references.errors);
|
|
706
880
|
warnings.push(...references.warnings);
|
|
@@ -748,6 +922,189 @@ function validatePathList(values, label, key, errors) {
|
|
|
748
922
|
}
|
|
749
923
|
}
|
|
750
924
|
|
|
925
|
+
function validateSource(path, expectedChange, errors) {
|
|
926
|
+
if (!isFile(path)) {
|
|
927
|
+
errors.push("missing source artifact");
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
const { meta, body } = parseFrontmatter(path);
|
|
931
|
+
const required = [
|
|
932
|
+
"schema_version",
|
|
933
|
+
"artifact",
|
|
934
|
+
"change",
|
|
935
|
+
"source_type",
|
|
936
|
+
"canonical_locator",
|
|
937
|
+
"captured_at",
|
|
938
|
+
"content_sha256",
|
|
939
|
+
"remote_state",
|
|
940
|
+
"close_capability",
|
|
941
|
+
];
|
|
942
|
+
const missing = required.filter((key) => !(key in meta));
|
|
943
|
+
if (missing.length) errors.push(`source.md: missing keys ${JSON.stringify(missing)}`);
|
|
944
|
+
if (meta.schema_version !== 1 || meta.artifact !== "source") {
|
|
945
|
+
errors.push("source.md: artifact/schema_version must be source/1");
|
|
946
|
+
}
|
|
947
|
+
if (meta.change !== expectedChange) errors.push("source.md: change must equal directory name");
|
|
948
|
+
if (!new Set(["conversation", "pasted", "local-file", "url", "github-issue", "github-pr"]).has(meta.source_type)) {
|
|
949
|
+
errors.push(`source.md: invalid source_type ${meta.source_type}`);
|
|
950
|
+
}
|
|
951
|
+
if (!/^[a-f0-9]{64}$/.test(String(meta.content_sha256 ?? ""))) {
|
|
952
|
+
errors.push("source.md: content_sha256 must be a SHA-256 digest");
|
|
953
|
+
}
|
|
954
|
+
if (!new Set(["open", "closed", "unknown", "not-applicable"]).has(meta.remote_state)) {
|
|
955
|
+
errors.push(`source.md: invalid remote_state ${meta.remote_state}`);
|
|
956
|
+
}
|
|
957
|
+
if (!new Set(["supported", "unsupported", "not-applicable"]).has(meta.close_capability)) {
|
|
958
|
+
errors.push(`source.md: invalid close_capability ${meta.close_capability}`);
|
|
959
|
+
}
|
|
960
|
+
if (typeof meta.canonical_locator === "string" && ABSOLUTE_MACHINE_PATH_RE.test(meta.canonical_locator)) {
|
|
961
|
+
errors.push("source.md: canonical_locator must not be a machine absolute path");
|
|
962
|
+
}
|
|
963
|
+
for (const heading of ["## Capture Metadata", "## Original Content", "## Source Comments"]) {
|
|
964
|
+
if (!body.includes(heading)) errors.push(`source.md: missing '${heading}'`);
|
|
965
|
+
}
|
|
966
|
+
return { path, meta, body };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function validateTriage(path, expectedChange, errors) {
|
|
970
|
+
if (!isFile(path)) {
|
|
971
|
+
errors.push("missing triage artifact");
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const { meta, body } = parseFrontmatter(path);
|
|
975
|
+
const required = [
|
|
976
|
+
"schema_version",
|
|
977
|
+
"artifact",
|
|
978
|
+
"change",
|
|
979
|
+
"mode",
|
|
980
|
+
"source",
|
|
981
|
+
"classification",
|
|
982
|
+
"risk",
|
|
983
|
+
"route",
|
|
984
|
+
"ready_for_implementation",
|
|
985
|
+
"external_action",
|
|
986
|
+
"updated_at",
|
|
987
|
+
];
|
|
988
|
+
const missing = required.filter((key) => !(key in meta));
|
|
989
|
+
if (missing.length) errors.push(`triage.md: missing keys ${JSON.stringify(missing)}`);
|
|
990
|
+
if (meta.schema_version !== 1 || meta.artifact !== "triage") {
|
|
991
|
+
errors.push("triage.md: artifact/schema_version must be triage/1");
|
|
992
|
+
}
|
|
993
|
+
if (meta.change !== expectedChange) errors.push("triage.md: change must equal directory name");
|
|
994
|
+
if (!new Set(["intake", "reconcile"]).has(meta.mode)) errors.push(`triage.md: invalid mode ${meta.mode}`);
|
|
995
|
+
if (!VALID_RISK.has(meta.risk)) errors.push(`triage.md: invalid risk ${meta.risk}`);
|
|
996
|
+
if (typeof meta.route !== "string" || !meta.route.startsWith("specdev/")) {
|
|
997
|
+
errors.push("triage.md: route must be a specdev work id");
|
|
998
|
+
}
|
|
999
|
+
if (typeof meta.ready_for_implementation !== "boolean") {
|
|
1000
|
+
errors.push("triage.md: ready_for_implementation must be boolean");
|
|
1001
|
+
}
|
|
1002
|
+
if (!new Set(["not-applicable", "pending-close", "closed", "close-failed", "waived"]).has(meta.external_action)) {
|
|
1003
|
+
errors.push(`triage.md: invalid external_action ${meta.external_action}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (!String(meta.source ?? "").includes("/source.md</Path>")) {
|
|
1006
|
+
errors.push("triage.md: source must reference the local source.md artifact");
|
|
1007
|
+
}
|
|
1008
|
+
for (const heading of ["## 当前判定", "## 未知项", "## 路由", "## 外部动作"]) {
|
|
1009
|
+
if (!body.includes(heading)) errors.push(`triage.md: missing '${heading}'`);
|
|
1010
|
+
}
|
|
1011
|
+
return { path, meta, body };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function validateDiagnosis(path, expectedChange, errors) {
|
|
1015
|
+
if (!isFile(path)) {
|
|
1016
|
+
errors.push("missing diagnosis artifact");
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
const { meta, body } = parseFrontmatter(path);
|
|
1020
|
+
if (meta.schema_version !== 1 || meta.artifact !== "diagnosis") {
|
|
1021
|
+
errors.push("diagnosis.md: artifact/schema_version must be diagnosis/1");
|
|
1022
|
+
}
|
|
1023
|
+
if (meta.change !== expectedChange) errors.push("diagnosis.md: change must equal directory name");
|
|
1024
|
+
if (!new Set(["reproducing", "minimizing", "testing-hypotheses", "root-cause-confirmed", "blocked"]).has(meta.status)) {
|
|
1025
|
+
errors.push(`diagnosis.md: invalid status ${meta.status}`);
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof meta.feedback_loop_ready !== "boolean") {
|
|
1028
|
+
errors.push("diagnosis.md: feedback_loop_ready must be boolean");
|
|
1029
|
+
}
|
|
1030
|
+
if (meta.feedback_loop_ready) {
|
|
1031
|
+
if (!String(meta.red_command ?? "").trim() || !String(meta.red_evidence ?? "").trim()) {
|
|
1032
|
+
errors.push("diagnosis.md: ready feedback loop requires red_command and red_evidence");
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
const hypotheses = sectionBody(body, "## 4. 假设与证伪");
|
|
1036
|
+
const dataRows = hypotheses
|
|
1037
|
+
.split(/\r?\n/)
|
|
1038
|
+
.filter((line) => /^\|/.test(line.trim()))
|
|
1039
|
+
.filter((line) => !/^\|\s*(?:排名|-)/.test(line.trim()));
|
|
1040
|
+
if (dataRows.length) errors.push("diagnosis.md: hypotheses must be empty before red evidence");
|
|
1041
|
+
}
|
|
1042
|
+
if (!new Set(["pending", "clean", "registered"]).has(meta.cleanup_status)) {
|
|
1043
|
+
errors.push(`diagnosis.md: invalid cleanup_status ${meta.cleanup_status}`);
|
|
1044
|
+
}
|
|
1045
|
+
for (const heading of ["## 2. 红灯反馈回路", "## 3. 最小复现", "## 4. 假设与证伪", "## 5. 已确认根因", "## 6. 修复契约", "## 7. 清理"]) {
|
|
1046
|
+
if (!body.includes(heading)) errors.push(`diagnosis.md: missing '${heading}'`);
|
|
1047
|
+
}
|
|
1048
|
+
return { path, meta, body };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function validateReviews(change, required, errors) {
|
|
1052
|
+
const root = join(change, "reviews");
|
|
1053
|
+
const paths = isDirectory(root)
|
|
1054
|
+
? readdirSync(root).filter((name) => /^CR-\d{3,}\.md$/.test(name)).sort().map((name) => join(root, name))
|
|
1055
|
+
: [];
|
|
1056
|
+
if (required && !paths.length) errors.push("review stage requires a CR-###.md report");
|
|
1057
|
+
for (const path of paths) {
|
|
1058
|
+
const { meta, body } = parseFrontmatter(path);
|
|
1059
|
+
if (meta.schema_version !== 1 || meta.artifact !== "code-review") {
|
|
1060
|
+
errors.push(`${basename(path)}: artifact/schema_version must be code-review/1`);
|
|
1061
|
+
}
|
|
1062
|
+
if (meta.change !== basename(change)) errors.push(`${basename(path)}: change must equal directory name`);
|
|
1063
|
+
if (!/^CR-\d{3,}$/.test(String(meta.review_id ?? ""))) errors.push(`${basename(path)}: invalid review_id`);
|
|
1064
|
+
if (!/^[a-f0-9]{7,40}$/i.test(String(meta.fixed_point ?? "")) || !/^[a-f0-9]{7,40}$/i.test(String(meta.head ?? ""))) {
|
|
1065
|
+
errors.push(`${basename(path)}: fixed_point and head must be commit SHAs`);
|
|
1066
|
+
}
|
|
1067
|
+
if (meta.fixed_point === meta.head) errors.push(`${basename(path)}: fixed_point and head must differ`);
|
|
1068
|
+
if (!new Set(["approved", "request-changes", "blocked"]).has(meta.status)) errors.push(`${basename(path)}: invalid status`);
|
|
1069
|
+
if (!new Set(["pass", "request-changes", "skipped"]).has(meta.standards_result)) errors.push(`${basename(path)}: invalid standards_result`);
|
|
1070
|
+
if (!new Set(["pass", "request-changes", "skipped"]).has(meta.specification_result)) errors.push(`${basename(path)}: invalid specification_result`);
|
|
1071
|
+
for (const heading of ["## Fixed Input", "## 标准", "## 规范", "## Summary"]) {
|
|
1072
|
+
if (!body.includes(heading)) errors.push(`${basename(path)}: missing '${heading}'`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return paths;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function validatePrototypes(change, required, errors) {
|
|
1079
|
+
const root = join(change, "prototypes");
|
|
1080
|
+
const paths = isDirectory(root)
|
|
1081
|
+
? walk(root).filter((path) => isFile(path) && basename(path) === "record.md").sort()
|
|
1082
|
+
: [];
|
|
1083
|
+
if (required && !paths.length) errors.push("prototype stage requires a prototypes/<id>/record.md artifact");
|
|
1084
|
+
for (const path of paths) {
|
|
1085
|
+
const { meta, body } = parseFrontmatter(path);
|
|
1086
|
+
const label = toPosix(relative(change, path));
|
|
1087
|
+
if (meta.schema_version !== 1 || meta.artifact !== "prototype-record") {
|
|
1088
|
+
errors.push(`${label}: artifact/schema_version must be prototype-record/1`);
|
|
1089
|
+
}
|
|
1090
|
+
if (meta.change !== basename(change)) errors.push(`${label}: change must equal directory name`);
|
|
1091
|
+
if (!/^PROTO-\d{3,}$/.test(String(meta.prototype_id ?? ""))) errors.push(`${label}: invalid prototype_id`);
|
|
1092
|
+
if (!new Set(["logic", "ui"]).has(meta.branch)) errors.push(`${label}: invalid prototype branch`);
|
|
1093
|
+
if (!new Set(["active", "answered", "blocked", "discarded"]).has(meta.status)) errors.push(`${label}: invalid prototype status`);
|
|
1094
|
+
if (typeof meta.workspace_ref !== "string" || ABSOLUTE_MACHINE_PATH_RE.test(meta.workspace_ref)) {
|
|
1095
|
+
errors.push(`${label}: workspace_ref must be a portable locator`);
|
|
1096
|
+
}
|
|
1097
|
+
if (meta.status === "answered" && (!String(meta.winner ?? "").trim() || !String(meta.promotion_target ?? "").trim())) {
|
|
1098
|
+
errors.push(`${label}: answered prototype requires winner and promotion_target`);
|
|
1099
|
+
}
|
|
1100
|
+
if (!new Set(["pending", "clean", "registered"]).has(meta.cleanup_status)) errors.push(`${label}: invalid cleanup_status`);
|
|
1101
|
+
for (const heading of ["## Question and Assumption", "## Run and Assets", "## Evaluation", "## Promotion and Cleanup"]) {
|
|
1102
|
+
if (!body.includes(heading)) errors.push(`${label}: missing '${heading}'`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return paths;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
751
1108
|
function validateSpec(path, errors, warnings) {
|
|
752
1109
|
if (!isFile(path)) {
|
|
753
1110
|
warnings.push("Spec is missing; contract traceability cannot be fully checked");
|
|
@@ -798,6 +1155,14 @@ function validateMap(path, errors) {
|
|
|
798
1155
|
return { path, meta, body };
|
|
799
1156
|
}
|
|
800
1157
|
|
|
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
|
+
|
|
801
1166
|
function validateGoalPlan(path, errors) {
|
|
802
1167
|
if (!isFile(path)) return null;
|
|
803
1168
|
const { meta, body } = parseFrontmatter(path);
|
|
@@ -809,6 +1174,7 @@ function validateGoalPlan(path, errors) {
|
|
|
809
1174
|
if (invalidModes.length) {
|
|
810
1175
|
errors.push(`${basename(path)}: invalid modes ${JSON.stringify(invalidModes)}`);
|
|
811
1176
|
}
|
|
1177
|
+
validateDelegatedGoalPlan(body, basename(path), errors);
|
|
812
1178
|
if (meta.ready_for_execution === true) {
|
|
813
1179
|
if (!new Set(["ready", "in_progress", "completed"]).has(meta.status)) {
|
|
814
1180
|
errors.push(`${basename(path)}: ready_for_execution conflicts with status`);
|
|
@@ -1126,6 +1492,30 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1126
1492
|
if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations)) {
|
|
1127
1493
|
errors.push(`${basename(path)}: blockers and deviations must be arrays`);
|
|
1128
1494
|
}
|
|
1495
|
+
if (data.worktrees !== undefined && !Array.isArray(data.worktrees)) {
|
|
1496
|
+
errors.push(`${basename(path)}: worktrees must be an array when present`);
|
|
1497
|
+
}
|
|
1498
|
+
for (const worktree of Array.isArray(data.worktrees) ? data.worktrees : []) {
|
|
1499
|
+
if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) {
|
|
1500
|
+
errors.push(`${basename(path)}: invalid worktree entry`);
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
const ref = worktree.workspace_ref;
|
|
1504
|
+
if (
|
|
1505
|
+
typeof ref !== "string" ||
|
|
1506
|
+
ref.startsWith("/") ||
|
|
1507
|
+
/^[A-Za-z]:[\\/]/.test(ref)
|
|
1508
|
+
) {
|
|
1509
|
+
errors.push(`${basename(path)}: workspace_ref must be a portable non-absolute locator`);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (worktree.provider === "git") {
|
|
1513
|
+
const expectedRef = `specdev-worktree/${worktree.ticket_id}`;
|
|
1514
|
+
if (ref !== expectedRef) {
|
|
1515
|
+
errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1129
1519
|
if (data.change_status === "archived") {
|
|
1130
1520
|
if (data.archived !== true) errors.push(`${basename(path)}: archived status requires archived=true`);
|
|
1131
1521
|
const inner = typeof data.archive_path === "string" ? stripPathTag(data.archive_path) : null;
|
|
@@ -1138,7 +1528,7 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1138
1528
|
return data;
|
|
1139
1529
|
}
|
|
1140
1530
|
|
|
1141
|
-
function validateChange(change) {
|
|
1531
|
+
function validateChange(change, stage = null) {
|
|
1142
1532
|
const errors = [];
|
|
1143
1533
|
const warnings = [];
|
|
1144
1534
|
if (!isDirectory(change)) {
|
|
@@ -1146,11 +1536,48 @@ function validateChange(change) {
|
|
|
1146
1536
|
}
|
|
1147
1537
|
|
|
1148
1538
|
const changeStatus = validateChangeStatus(join(change, ".status.json"), basename(change), errors);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1539
|
+
if (isFile(join(change, "source-issue.md"))) {
|
|
1540
|
+
errors.push("obsolete source-issue.md is forbidden; use source.md without compatibility fallback");
|
|
1541
|
+
}
|
|
1542
|
+
const sourceRequired = stage === "triage";
|
|
1543
|
+
const sourcePath = join(change, "source.md");
|
|
1544
|
+
const source = isFile(sourcePath) || sourceRequired
|
|
1545
|
+
? validateSource(sourcePath, basename(change), errors)
|
|
1546
|
+
: null;
|
|
1547
|
+
const triagePath = join(change, "triage.md");
|
|
1548
|
+
const triage = isFile(triagePath) || sourceRequired
|
|
1549
|
+
? validateTriage(triagePath, basename(change), errors)
|
|
1550
|
+
: null;
|
|
1551
|
+
const diagnosisPath = join(change, "diagnosis.md");
|
|
1552
|
+
if (isFile(diagnosisPath) || stage === "diagnosis") {
|
|
1553
|
+
validateDiagnosis(diagnosisPath, basename(change), errors);
|
|
1554
|
+
}
|
|
1555
|
+
validateReviews(change, stage === "review", errors);
|
|
1556
|
+
validatePrototypes(change, stage === "prototype", errors);
|
|
1557
|
+
|
|
1558
|
+
const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage);
|
|
1559
|
+
const specPath = join(change, "spec.md");
|
|
1560
|
+
const spec = isFile(specPath) || specRequired
|
|
1561
|
+
? validateSpec(specPath, errors, warnings)
|
|
1562
|
+
: null;
|
|
1563
|
+
const ticketMode = isDirectory(join(change, "ticket"));
|
|
1564
|
+
const mapRequired = new Set(["tickets", "goal-plan"]).has(stage) || (stage === "implement" && ticketMode);
|
|
1565
|
+
const mapPath = join(change, "tickets-map.md");
|
|
1566
|
+
const ticketsMap = isFile(mapPath) || mapRequired ? validateMap(mapPath, errors) : null;
|
|
1567
|
+
const goalPlanPath = join(change, "goal-plan.md");
|
|
1568
|
+
const goalPlan = isFile(goalPlanPath) || stage === "goal-plan"
|
|
1569
|
+
? validateGoalPlan(goalPlanPath, errors)
|
|
1570
|
+
: null;
|
|
1152
1571
|
validateDesignTree(join(change, "design-tree.json"), change, errors);
|
|
1153
1572
|
validateWayfinder(change, errors);
|
|
1573
|
+
if (stage === "grill") {
|
|
1574
|
+
for (const name of ["design-tree.json", "LOG.md", "CONTEXT.md", "ADR.md"]) {
|
|
1575
|
+
if (!isFile(join(change, name))) errors.push(`grill stage requires ${name}`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (stage === "wayfinder" && !isFile(join(change, "wayfinder-map.md"))) {
|
|
1579
|
+
errors.push("wayfinder stage requires wayfinder-map.md");
|
|
1580
|
+
}
|
|
1154
1581
|
for (const artifact of [spec, ticketsMap, goalPlan]) {
|
|
1155
1582
|
if (artifact && artifact.meta.change !== basename(change)) {
|
|
1156
1583
|
errors.push(`${basename(artifact.path)}: change must equal directory name ${basename(change)}`);
|
|
@@ -1158,13 +1585,17 @@ function validateChange(change) {
|
|
|
1158
1585
|
}
|
|
1159
1586
|
|
|
1160
1587
|
const ticketDir = join(change, "ticket");
|
|
1161
|
-
|
|
1162
|
-
const ticketFiles =
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1588
|
+
const ticketsRequired = new Set(["tickets", "goal-plan"]).has(stage) || (stage === "implement" && ticketMode);
|
|
1589
|
+
const ticketFiles = isDirectory(ticketDir)
|
|
1590
|
+
? readdirSync(ticketDir)
|
|
1591
|
+
.filter((name) => name.endsWith(".md"))
|
|
1592
|
+
.sort()
|
|
1593
|
+
.map((name) => join(ticketDir, name))
|
|
1594
|
+
: [];
|
|
1595
|
+
if (ticketsRequired && !isDirectory(ticketDir)) errors.push("missing Ticket directory");
|
|
1596
|
+
if (ticketsRequired && !ticketFiles.length) errors.push("Ticket directory contains no Markdown tickets");
|
|
1597
|
+
if (stage === "implement" && !ticketMode && !isFile(join(change, "evidence", "direct-spec.md"))) {
|
|
1598
|
+
errors.push("Direct Spec implement stage requires evidence/direct-spec.md");
|
|
1168
1599
|
}
|
|
1169
1600
|
|
|
1170
1601
|
const tickets = new Map();
|
|
@@ -1263,7 +1694,7 @@ function validateChange(change) {
|
|
|
1263
1694
|
);
|
|
1264
1695
|
} else {
|
|
1265
1696
|
warnings.push(
|
|
1266
|
-
`concurrent Ready Tickets ${leftId}/${rightId} share owned paths;
|
|
1697
|
+
`concurrent Ready Tickets ${leftId}/${rightId} share owned paths; an explicit owner or serialization is required`,
|
|
1267
1698
|
);
|
|
1268
1699
|
}
|
|
1269
1700
|
}
|
|
@@ -1282,9 +1713,10 @@ function validateChange(change) {
|
|
|
1282
1713
|
"## 2. 修改范围",
|
|
1283
1714
|
"## 3. 验收与合同映射",
|
|
1284
1715
|
"## 4. 验证执行",
|
|
1285
|
-
"## 5.
|
|
1286
|
-
"## 6.
|
|
1287
|
-
"## 7.
|
|
1716
|
+
"## 5. 双轴审查",
|
|
1717
|
+
"## 6. 路径所有权审计",
|
|
1718
|
+
"## 7. 偏差与决策",
|
|
1719
|
+
"## 8. 残余风险与后续",
|
|
1288
1720
|
]) {
|
|
1289
1721
|
if (!text.includes(heading)) {
|
|
1290
1722
|
errors.push(`${toPosix(relative(change, evidence))}: missing '${heading}'`);
|
|
@@ -1324,6 +1756,23 @@ function validateChange(change) {
|
|
|
1324
1756
|
if (changeStatus.change_status === "archived") {
|
|
1325
1757
|
warnings.push("validating an archived change in place; normally it lives under the archive root");
|
|
1326
1758
|
}
|
|
1759
|
+
if (stage === "complete" && changeStatus.change_status !== "completed") {
|
|
1760
|
+
errors.push("complete stage requires change_status=completed");
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (
|
|
1764
|
+
stage === "complete" &&
|
|
1765
|
+
!ticketFiles.length &&
|
|
1766
|
+
!isFile(join(change, "evidence", "direct-spec.md"))
|
|
1767
|
+
) {
|
|
1768
|
+
errors.push("complete stage without Tickets requires evidence/direct-spec.md");
|
|
1769
|
+
}
|
|
1770
|
+
if (
|
|
1771
|
+
stage === "complete" &&
|
|
1772
|
+
triage &&
|
|
1773
|
+
new Set(["pending-close", "close-failed"]).has(triage.meta.external_action)
|
|
1774
|
+
) {
|
|
1775
|
+
errors.push(`complete stage cannot archive external_action=${triage.meta.external_action}`);
|
|
1327
1776
|
}
|
|
1328
1777
|
|
|
1329
1778
|
for (const path of walk(change).filter((item) => isFile(item) && extname(item) === ".md")) {
|
|
@@ -1352,22 +1801,39 @@ function printResults(errors, warnings) {
|
|
|
1352
1801
|
}
|
|
1353
1802
|
|
|
1354
1803
|
function usage() {
|
|
1355
|
-
console.error("Usage: node validate-specdev.mjs <change-directory> | --self-check");
|
|
1804
|
+
console.error("Usage: node validate-specdev.mjs [--stage <stage>] <change-directory> | --self-check");
|
|
1356
1805
|
return 2;
|
|
1357
1806
|
}
|
|
1358
1807
|
|
|
1359
1808
|
function main(argv) {
|
|
1360
|
-
|
|
1361
|
-
|
|
1809
|
+
let selfCheckRequested = false;
|
|
1810
|
+
let stage = null;
|
|
1811
|
+
const positional = [];
|
|
1812
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1813
|
+
const arg = argv[index];
|
|
1814
|
+
if (arg === "--self-check") {
|
|
1815
|
+
selfCheckRequested = true;
|
|
1816
|
+
} else if (arg === "--stage") {
|
|
1817
|
+
stage = argv[index + 1] ?? null;
|
|
1818
|
+
index += 1;
|
|
1819
|
+
} else if (arg.startsWith("--stage=")) {
|
|
1820
|
+
stage = arg.slice("--stage=".length);
|
|
1821
|
+
} else if (arg.startsWith("--")) {
|
|
1822
|
+
return usage();
|
|
1823
|
+
} else {
|
|
1824
|
+
positional.push(arg);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (stage !== null && !VALID_STAGES.has(stage)) return usage();
|
|
1362
1828
|
if (selfCheckRequested) {
|
|
1363
|
-
if (positional.length) return usage();
|
|
1829
|
+
if (positional.length || stage !== null) return usage();
|
|
1364
1830
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
1365
1831
|
const root = resolve(scriptDirectory, "..", "..");
|
|
1366
1832
|
const result = selfCheck(root);
|
|
1367
1833
|
return printResults(result.errors, result.warnings);
|
|
1368
1834
|
}
|
|
1369
1835
|
if (positional.length === 1) {
|
|
1370
|
-
const result = validateChange(resolve(positional[0]));
|
|
1836
|
+
const result = validateChange(resolve(positional[0]), stage);
|
|
1371
1837
|
return printResults(result.errors, result.warnings);
|
|
1372
1838
|
}
|
|
1373
1839
|
return usage();
|