create-yss-spec 2.1.9 → 2.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/cli.js +82 -15
  4. package/template/.agents/skills/diagnosing-bugs/SKILL.md +2 -4
  5. package/template/.agents/skills/domain-modeling/SKILL.md +1 -1
  6. package/template/.agents/skills/grilling/SKILL.md +7 -1
  7. package/template/.agents/skills/triage/SKILL.md +2 -2
  8. package/template/.agents/skills/wait-what/SKILL.md +2 -2
  9. package/template/.agents/skills/wayfinder/SKILL.md +1 -1
  10. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +43 -91
  11. package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  12. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  13. package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  14. package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +4 -1
  15. package/template/.claude/skills/diagnosing-bugs/SKILL.md +2 -4
  16. package/template/.claude/skills/domain-modeling/SKILL.md +1 -1
  17. package/template/.claude/skills/grilling/SKILL.md +7 -1
  18. package/template/.claude/skills/triage/SKILL.md +2 -2
  19. package/template/.claude/skills/wait-what/SKILL.md +2 -2
  20. package/template/.claude/skills/wayfinder/SKILL.md +1 -1
  21. package/template/.claude/skills/yss-product-lifecycle/SKILL.md +43 -91
  22. package/template/.claude/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  23. package/template/.claude/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  24. package/template/.claude/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  25. package/template/.claude/skills/yss-product-lifecycle/references/state-model.md +4 -1
  26. package/template/.codex/skills/diagnosing-bugs/SKILL.md +2 -4
  27. package/template/.codex/skills/domain-modeling/SKILL.md +1 -1
  28. package/template/.codex/skills/grilling/SKILL.md +7 -1
  29. package/template/.codex/skills/triage/SKILL.md +2 -2
  30. package/template/.codex/skills/wait-what/SKILL.md +2 -2
  31. package/template/.codex/skills/wayfinder/SKILL.md +1 -1
  32. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +43 -91
  33. package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  34. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  35. package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  36. package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +4 -1
  37. package/template/.hermes/skills/diagnosing-bugs/SKILL.md +2 -4
  38. package/template/.hermes/skills/domain-modeling/SKILL.md +1 -1
  39. package/template/.hermes/skills/grilling/SKILL.md +7 -1
  40. package/template/.hermes/skills/triage/SKILL.md +2 -2
  41. package/template/.hermes/skills/wait-what/SKILL.md +2 -2
  42. package/template/.hermes/skills/wayfinder/SKILL.md +1 -1
  43. package/template/.hermes/skills/yss-product-lifecycle/SKILL.md +43 -91
  44. package/template/.hermes/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  45. package/template/.hermes/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  46. package/template/.hermes/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  47. package/template/.hermes/skills/yss-product-lifecycle/references/state-model.md +4 -1
  48. package/template/.pi/skills/diagnosing-bugs/SKILL.md +2 -4
  49. package/template/.pi/skills/domain-modeling/SKILL.md +1 -1
  50. package/template/.pi/skills/grilling/SKILL.md +7 -1
  51. package/template/.pi/skills/triage/SKILL.md +2 -2
  52. package/template/.pi/skills/wait-what/SKILL.md +2 -2
  53. package/template/.pi/skills/wayfinder/SKILL.md +1 -1
  54. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +43 -91
  55. package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  56. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  57. package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  58. package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +4 -1
  59. package/template/.qoder/skills/diagnosing-bugs/SKILL.md +2 -4
  60. package/template/.qoder/skills/domain-modeling/SKILL.md +1 -1
  61. package/template/.qoder/skills/grilling/SKILL.md +7 -1
  62. package/template/.qoder/skills/triage/SKILL.md +2 -2
  63. package/template/.qoder/skills/wait-what/SKILL.md +2 -2
  64. package/template/.qoder/skills/wayfinder/SKILL.md +1 -1
  65. package/template/.qoder/skills/yss-product-lifecycle/SKILL.md +43 -91
  66. package/template/.qoder/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  67. package/template/.qoder/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  68. package/template/.qoder/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  69. package/template/.qoder/skills/yss-product-lifecycle/references/state-model.md +4 -1
  70. package/template/.trae/skills/diagnosing-bugs/SKILL.md +2 -4
  71. package/template/.trae/skills/domain-modeling/SKILL.md +1 -1
  72. package/template/.trae/skills/grilling/SKILL.md +7 -1
  73. package/template/.trae/skills/triage/SKILL.md +2 -2
  74. package/template/.trae/skills/wait-what/SKILL.md +2 -2
  75. package/template/.trae/skills/wayfinder/SKILL.md +1 -1
  76. package/template/.trae/skills/yss-product-lifecycle/SKILL.md +43 -91
  77. package/template/.trae/skills/yss-product-lifecycle/references/matt-yss-adapter.md +7 -5
  78. package/template/.trae/skills/yss-product-lifecycle/references/orchestration-contract.yaml +88 -22
  79. package/template/.trae/skills/yss-product-lifecycle/references/orchestration.md +5 -4
  80. package/template/.trae/skills/yss-product-lifecycle/references/state-model.md +4 -1
  81. package/template/AGENTS.md +2 -2
  82. package/template/CONTEXT.md +7 -1
  83. package/template/docs/adr/0007-lifecycle-native-work-units.md +5 -0
  84. package/template/docs/agents/skills-maintenance.md +11 -3
  85. package/template/docs/process/MATT-POCOCK-ENGINEERING-SKILLS.md +1 -1
  86. package/template/docs/process/lifecycle-artifact-map.md +19 -15
  87. package/template/docs/process/lifecycle-registry-baseline.json +15 -1
  88. package/template/docs/process/lifecycle-registry.yaml +77 -0
  89. package/template/docs/process/schemas/frontend-implementation-evidence.schema.json +39 -0
  90. package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +56 -0
  91. package/template/docs/process/schemas/lifecycle-registry.schema.json +2 -2
  92. package/template/docs/process/templates/frontend-implementation-plan-template.yaml +16 -0
  93. package/template/docs/process/templates/frontend-implementation-verification-template.yaml +21 -0
  94. package/template/docs/process/templates/lifecycle-checkpoint-template.yaml +30 -0
  95. package/template/docs/process/templates/stage-checkpoint-template.md +9 -5
  96. package/template/scripts/lib/lifecycle-registry.mjs +9 -4
  97. package/template/scripts/lib/scenario-checks.mjs +37 -15
  98. package/template/scripts/lib/skill-supply-chain.mjs +33 -8
  99. package/template/scripts/verify-frontend-implementation-evidence +51 -0
  100. package/template/scripts/verify-lifecycle-checkpoint +37 -0
  101. package/template/scripts/verify-template +5 -2
  102. package/template/scripts/verify-upstream-skill-source +32 -0
  103. package/template/skills-lock.json +105 -55
  104. package/template.manifest.json +12 -0
  105. package/template.snapshot.json +4 -4
  106. package/template/.agents/skills/batch-grill-me/SKILL.md +0 -15
  107. package/template/.agents/skills/batch-grill-me/agents/openai.yaml +0 -5
  108. package/template/.claude/skills/batch-grill-me/SKILL.md +0 -15
  109. package/template/.claude/skills/batch-grill-me/agents/openai.yaml +0 -5
  110. package/template/.codex/skills/batch-grill-me/SKILL.md +0 -15
  111. package/template/.codex/skills/batch-grill-me/agents/openai.yaml +0 -5
  112. package/template/.hermes/skills/batch-grill-me/SKILL.md +0 -15
  113. package/template/.hermes/skills/batch-grill-me/agents/openai.yaml +0 -5
  114. package/template/.pi/skills/batch-grill-me/SKILL.md +0 -15
  115. package/template/.pi/skills/batch-grill-me/agents/openai.yaml +0 -5
  116. package/template/.qoder/skills/batch-grill-me/SKILL.md +0 -15
  117. package/template/.qoder/skills/batch-grill-me/agents/openai.yaml +0 -5
  118. package/template/.trae/skills/batch-grill-me/SKILL.md +0 -15
  119. package/template/.trae/skills/batch-grill-me/agents/openai.yaml +0 -5
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://yss.dev/schemas/lifecycle-checkpoint.schema.json",
4
+ "title": "YSS 生命周期 Checkpoint",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "mode", "status", "stage", "artifacts", "gates", "next_work_unit", "ticket_sync", "verification", "human_review", "git_checkpoint", "blockers", "rollback"],
8
+ "properties": {
9
+ "schema_version": { "const": 1 },
10
+ "mode": { "enum": ["route", "orchestrate", "resume", "audit"] },
11
+ "status": { "enum": ["routing", "running", "paused-human-gate", "blocked", "completed"] },
12
+ "stage": { "type": "string", "pattern": "^stage\\.[a-z0-9][a-z0-9-]*$" },
13
+ "artifacts": {
14
+ "type": "object",
15
+ "patternProperties": {
16
+ "^artifact\\.[a-z0-9][a-z0-9-]*$": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["status", "ref", "evidence_refs"],
20
+ "properties": {
21
+ "status": { "enum": ["missing", "draft", "ready-for-human", "approved", "stale", "not-applicable"] },
22
+ "ref": { "type": ["string", "null"] },
23
+ "evidence_refs": { "type": "array", "items": { "type": "string" } },
24
+ "stale_by": { "type": "array", "items": { "type": "string" } }
25
+ }
26
+ }
27
+ },
28
+ "additionalProperties": false
29
+ },
30
+ "gates": {
31
+ "type": "object",
32
+ "patternProperties": {
33
+ "^gate\\.[a-z0-9][a-z0-9-]*$": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "required": ["status", "reason", "evidence_refs"],
37
+ "properties": {
38
+ "status": { "enum": ["not-evaluated", "blocked", "ready-for-human", "approved", "stale", "not-applicable"] },
39
+ "reason": { "type": "string" },
40
+ "evidence_refs": { "type": "array", "items": { "type": "string" } }
41
+ }
42
+ }
43
+ },
44
+ "additionalProperties": false
45
+ },
46
+ "next_work_unit": { "type": ["string", "null"], "pattern": "^work-unit\\.[a-z0-9][a-z0-9-]*$" },
47
+ "phase_boundary": { "type": "object" },
48
+ "stage_trace": { "type": "object" },
49
+ "ticket_sync": { "type": "object" },
50
+ "verification": { "type": "object" },
51
+ "human_review": { "type": "object" },
52
+ "git_checkpoint": { "type": "object" },
53
+ "blockers": { "type": "array" },
54
+ "rollback": { "type": "array" }
55
+ }
56
+ }
@@ -28,8 +28,8 @@
28
28
  "$defs": {
29
29
  "id": { "type": "string", "pattern": "^(stage|gate|artifact|work-unit|evidence)\\.[a-z0-9][a-z0-9-]*$" },
30
30
  "stages": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "name", "goal", "exit_criteria"], "properties": { "id": { "$ref": "#/$defs/id" }, "name": { "type": "string", "minLength": 1 }, "goal": { "type": "string", "minLength": 1 }, "exit_criteria": { "type": "string", "minLength": 1 } } } },
31
- "objects": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "name", "stage", "trigger"], "properties": { "id": { "$ref": "#/$defs/id" }, "name": { "type": "string", "minLength": 1 }, "stage": { "type": "string", "pattern": "^stage\\." }, "trigger": { "type": "string", "minLength": 1 }, "evidence": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^evidence\\." } } } } },
32
- "workUnits": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "name", "scope", "input", "output", "completion"], "properties": { "id": { "$ref": "#/$defs/id" }, "name": { "type": "string", "minLength": 1 }, "scope": { "const": "template-source" }, "input": { "type": "string", "minLength": 1 }, "output": { "type": "string", "minLength": 1 }, "completion": { "type": "string", "minLength": 1 } } } },
31
+ "objects": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "name", "stage", "trigger"], "properties": { "id": { "$ref": "#/$defs/id" }, "name": { "type": "string", "minLength": 1 }, "stage": { "type": "string", "pattern": "^stage\\." }, "trigger": { "type": "string", "minLength": 1 }, "evidence": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^evidence\\." } }, "requires_gates": { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^gate\\." } } } } },
32
+ "workUnits": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "name", "scope", "input", "output", "completion"], "properties": { "id": { "$ref": "#/$defs/id" }, "name": { "type": "string", "minLength": 1 }, "scope": { "enum": ["template-source", "project-instance"] }, "input": { "type": "string", "minLength": 1 }, "output": { "type": "string", "minLength": 1 }, "completion": { "type": "string", "minLength": 1 } } } },
33
33
  "evidence": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "name", "description"], "properties": { "id": { "$ref": "#/$defs/id" }, "name": { "type": "string", "minLength": 1 }, "description": { "type": "string", "minLength": 1 } } } }
34
34
  }
35
35
  }
@@ -0,0 +1,16 @@
1
+ schema_version: 1
2
+ kind: plan
3
+ template: true
4
+ status: draft
5
+ prototype_ref: docs/.scratch/example/design/prototype.html
6
+ spec_ref: docs/.scratch/example/spec.md
7
+ route_and_page_inventory:
8
+ - route: /example
9
+ page: ExamplePage
10
+ visual_acceptance_cases:
11
+ - primary desktop flow
12
+ - narrow viewport flow
13
+ desktop_viewport: 1440x900
14
+ narrow_viewport: 390x844
15
+ state_cases: [loading, empty, error, permission, selected, key-interaction]
16
+ pnpm_commands: [pnpm lint, pnpm type-check, pnpm test]
@@ -0,0 +1,21 @@
1
+ schema_version: 1
2
+ kind: verification
3
+ template: true
4
+ status: draft
5
+ prototype_ref: docs/.scratch/example/design/prototype.html
6
+ spec_ref: docs/.scratch/example/spec.md
7
+ route_and_page_inventory:
8
+ - route: /example
9
+ page: ExamplePage
10
+ desktop_visual_evidence: [docs/.scratch/example/verification/desktop.png]
11
+ narrow_view_visual_evidence: [docs/.scratch/example/verification/narrow.png]
12
+ interaction_results: [loading, empty, error, permission, selected, key-interaction]
13
+ console_warning_check: blocked
14
+ pnpm_commands: [pnpm lint, pnpm type-check, pnpm test]
15
+ uncovered_differences: []
16
+ difference_owner: null
17
+ independent_review:
18
+ reviewer: non-implementer
19
+ candidate_digest: pending
20
+ status: pending
21
+ findings_ref: docs/.scratch/example/verification/ui-fidelity-review.md
@@ -0,0 +1,30 @@
1
+ schema_version: 1
2
+ mode: route
3
+ status: routing
4
+ stage: stage.entry-triage
5
+ artifacts: {}
6
+ gates: {}
7
+ next_work_unit: work-unit.discovery-opportunity
8
+ phase_boundary:
9
+ decision: continue
10
+ reason: initial routing
11
+ stage_trace:
12
+ stage: stage.entry-triage
13
+ upstream_refs: []
14
+ artifact_refs: []
15
+ gate_decisions: []
16
+ downstream_impacts: []
17
+ ticket_sync:
18
+ status: pending
19
+ refs: []
20
+ verification:
21
+ commands: []
22
+ evidence_refs: []
23
+ human_review:
24
+ required: false
25
+ status: not-applicable
26
+ git_checkpoint:
27
+ required: false
28
+ status: not-applicable
29
+ blockers: []
30
+ rollback: []
@@ -2,14 +2,17 @@
2
2
 
3
3
  ```yaml
4
4
  schema_version: 1
5
- mode: project-instance
5
+ mode: <route|orchestrate|resume|audit>
6
+ status: <routing|running|paused-human-gate|blocked|completed>
6
7
  stage: <lifecycle stage>
7
8
  artifacts:
8
- - path: <artifact path>
9
- status: <draft|ready-for-human|ready-for-agent|completed|blocked|not-applicable>
9
+ <stable-artifact-id>:
10
+ status: <missing|draft|ready-for-human|approved|stale|not-applicable>
11
+ ref: <readable artifact reference>
12
+ evidence_refs: []
10
13
  gates:
11
- - name: <gate name>
12
- status: <passed|failed|blocked|not-applicable>
14
+ <stable-gate-id>:
15
+ status: <not-evaluated|blocked|ready-for-human|approved|stale|not-applicable>
13
16
  reason: <why this status applies>
14
17
  phase_boundary:
15
18
  decision: <continue|clear|handoff|subagent|compact>
@@ -24,6 +27,7 @@ stage_trace:
24
27
  ticket_sync:
25
28
  status: <pending|synced|not-applicable>
26
29
  refs: []
30
+ next_work_unit: <stable work-unit id>
27
31
  verification:
28
32
  commands: []
29
33
  evidence_refs: []
@@ -128,6 +128,11 @@ export function validateRegistry(registry, { baseline = DEFAULT_BASELINE } = {})
128
128
  requireReference(ids, reference, `${gate.id}.evidence`);
129
129
  if (ids.get(reference) !== "evidence") fail(`${gate.id}.evidence 必须引用 evidence.*`);
130
130
  }
131
+ for (const reference of gate.requires_gates ?? []) {
132
+ requireReference(ids, reference, `${gate.id}.requires_gates`);
133
+ if (ids.get(reference) !== "gates") fail(`${gate.id}.requires_gates 必须引用 gate.*`);
134
+ if (reference === gate.id) fail(`${gate.id}.requires_gates 不得引用自身`);
135
+ }
131
136
  }
132
137
  for (const artifact of registry.artifacts) {
133
138
  requireReference(ids, artifact.stage, `${artifact.id}.stage`);
@@ -144,8 +149,8 @@ export function renderLifecycleStructure(registry) {
144
149
  "## 1. 主阶段", "", "| 稳定 ID | 阶段 | 目标 | 退出标准 |", "|---|---|---|---|"
145
150
  ];
146
151
  for (const stage of registry.stages) lines.push(`| \`${stage.id}\` | ${stage.name} | ${stage.goal} | ${stage.exit_criteria} |`);
147
- lines.push("", "## 2. 生命周期对象", "", "门禁是需要裁决的审查点;产物、工作单元和证据不是门禁的同义词。未命中条件的门禁记录 \`not-applicable\` 及原因,不生成空文档。", "", "### 2.1 条件门禁", "", "| 稳定 ID | 门禁 | 所属阶段 | 触发条件 | 必须留下的证据 |", "|---|---|---|---|---|");
148
- for (const gate of registry.gates) lines.push(`| \`${gate.id}\` | ${gate.name} | \`${gate.stage}\` | ${gate.trigger} | ${gate.evidence.map((id) => `\`${id}\``).join("、")} |`);
152
+ lines.push("", "## 2. 生命周期对象", "", "门禁是需要裁决的审查点;产物、工作单元和证据不是门禁的同义词。未命中条件的门禁记录 \`not-applicable\` 及原因,不生成空文档。", "", "### 2.1 条件门禁", "", "| 稳定 ID | 门禁 | 所属阶段 | 触发条件 | 前置门禁 | 必须留下的证据 |", "|---|---|---|---|---|---|");
153
+ for (const gate of registry.gates) lines.push(`| \`${gate.id}\` | ${gate.name} | \`${gate.stage}\` | ${gate.trigger} | ${(gate.requires_gates ?? []).map((id) => `\`${id}\``).join("、") || "无"} | ${gate.evidence.map((id) => `\`${id}\``).join("、")} |`);
149
154
  lines.push("", "### 2.2 生命周期产物", "", "| 稳定 ID | 产物 | 所属阶段 | 触发条件 |", "|---|---|---|---|");
150
155
  for (const artifact of registry.artifacts) lines.push(`| \`${artifact.id}\` | ${artifact.name} | \`${artifact.stage}\` | ${artifact.trigger} |`);
151
156
  lines.push("", "### 2.3 执行证据", "", "| 稳定 ID | 证据 | 说明 |", "|---|---|---|");
@@ -155,8 +160,8 @@ export function renderLifecycleStructure(registry) {
155
160
  }
156
161
 
157
162
  export function renderWorkUnits(registry) {
158
- const lines = ["<!-- lifecycle-registry:work-units:start -->", "> 此表由 `docs/process/lifecycle-registry.yaml` 生成;它只描述 `template-source` 的 Harness 工作单元。", "", "| 稳定 ID | 工作单元 | 输入 | 输出 | 完成条件 |", "|---|---|---|---|---|"];
159
- for (const unit of registry.work_units) lines.push(`| \`${unit.id}\` | ${unit.name} | ${unit.input} | ${unit.output} | ${unit.completion} |`);
163
+ const lines = ["<!-- lifecycle-registry:work-units:start -->", "> 此表由 `docs/process/lifecycle-registry.yaml` 生成;工作单元按 `scope` 区分模板维护与项目实例流程。", "", "| 稳定 ID | 范围 | 工作单元 | 输入 | 输出 | 完成条件 |", "|---|---|---|---|---|---|"];
164
+ for (const unit of registry.work_units) lines.push(`| \`${unit.id}\` | ${unit.scope} | ${unit.name} | ${unit.input} | ${unit.output} | ${unit.completion} |`);
160
165
  lines.push("<!-- lifecycle-registry:work-units:end -->");
161
166
  return `${lines.join("\n")}\n`;
162
167
  }
@@ -32,11 +32,14 @@ function validateMattContract(data) {
32
32
 
33
33
  function validateInvocationBoundary(data) {
34
34
  const boundary = data.matt_invocation_boundary;
35
- const expectedUserInvoked = ["ask-matt", "batch-grill-me", "grill-me", "grill-with-docs", "handoff", "implement", "improve-codebase-architecture", "loop-me", "setup-matt-pocock-skills", "setup-ts-deep-modules", "teach", "to-questionnaire", "to-spec", "to-tickets", "triage", "wait-what", "wayfinder", "writing-beats", "writing-fragments", "writing-shape"];
35
+ const expectedUserInvoked = ["ask-matt", "grill-me", "grill-with-docs", "handoff", "implement", "improve-codebase-architecture", "loop-me", "setup-matt-pocock-skills", "setup-ts-deep-modules", "teach", "to-questionnaire", "to-spec", "to-tickets", "triage", "wait-what", "wayfinder", "writing-beats", "writing-fragments", "writing-shape"];
36
+ const expectedModelInvoked = ["code-review", "codebase-design", "diagnosing-bugs", "domain-modeling", "grilling", "migrate-to-shoehorn", "prototype", "research", "resolving-merge-conflicts", "scaffold-exercises", "setup-pre-commit", "tdd", "writing-for-agents"];
37
+ const expectedLifecycleModelInvoked = ["code-review", "codebase-design", "diagnosing-bugs", "domain-modeling", "grilling", "prototype", "research", "tdd"];
36
38
  ensure(JSON.stringify(boundary?.user_invoked_skills) === JSON.stringify(expectedUserInvoked), "Matt user-invoked skills 清单不完整或已漂移");
37
39
  ensure(JSON.stringify(boundary?.lifecycle_managed_user_entries) === JSON.stringify(["setup-matt-pocock-skills", "grill-with-docs", "to-spec", "to-tickets", "implement"]), "生命周期管理的显式用户入口清单不完整");
38
40
  ensure(boundary?.lifecycle_may_invoke_user_invoked === false && boundary?.formal_artifact_owner === "explicit-user-entry", "生命周期仍可能自动调用 user-invoked skill 或产出其正式资产");
39
- ensure(JSON.stringify(boundary?.model_invoked_skills) === JSON.stringify(["grilling", "domain-modeling", "code-review"]) && boundary?.continuous_orchestration === "prepare-and-validate-only", "连续编排未被限制为允许的 model-invoked 原语与准备/校验动作");
41
+ ensure(JSON.stringify(boundary?.model_invoked_skills) === JSON.stringify(expectedModelInvoked) && JSON.stringify(boundary?.lifecycle_allowed_model_invoked_skills) === JSON.stringify(expectedLifecycleModelInvoked) && boundary?.continuous_orchestration === "compatibility-prepare-and-validate-only", "Matt invocation inventory 或生命周期 model-invoked 白名单不完整");
42
+ ensure(JSON.stringify(data.skill_source_contract?.source_revisions_required) === JSON.stringify(["mattpocock/skills"]) && data.skill_source_contract?.adaptation_ref_required_when_effective_diff === true && data.skill_source_contract?.retired_shared_skills?.includes("batch-grill-me"), "上游来源或退役 skill 供应链契约不完整");
40
43
  validateInvocationMetadata(boundary, (skill) => read(`.agents/skills/${skill}/SKILL.md`));
41
44
  const setup = data.setup_readiness;
42
45
  ensure(setup?.missing_action === "needs-human" && setup?.requested_skill === "setup-matt-pocock-skills" && setup?.resume_route === "setup-readiness" && setup?.preserves?.includes("lifecycle.status"), "readiness=missing 未形成显式用户 setup 的结构化暂停");
@@ -44,22 +47,31 @@ function validateInvocationBoundary(data) {
44
47
  ensure(data.matt_skill_result?.status === "compatibility-read-only" && data.matt_skill_result?.may_influence_routing === false && data.matt_skill_result?.normalize_to === "workflow-execution-result-v1", "旧 Matt Skill Result 未限制为只读兼容 adapter");
45
48
  ensure(result?.canonical_output_schema === "workflow-execution-result-v1" && JSON.stringify(result?.accepted_input_schemas) === JSON.stringify(["workflow-execution-result-v1"]) && result?.legacy?.["matt-skill-result-v1"]?.status === "compatibility-read-only" && result.legacy["matt-skill-result-v1"].may_influence_routing === false && result.legacy["matt-skill-result-v1"].normalize_to === "workflow-execution-result-v1", "Workflow Execution Result 未将旧 Matt Skill Result 限制为只读兼容");
46
49
  ensure(includesAll(result?.required, ["result_schema", "work_unit", "workflow_reference", "result", "evidence_refs", "changed_artifacts", "new_impacts", "stale_candidates", "next_route", "blocking_signals"]) && includesAll(result?.result_values, ["completed", "blocked", "needs-human", "failed"]) && includesAll(result?.blocking_signals, ["drift", "new_impacts", "violation", "missing_evidence", "stale_candidates"]) && includesAll(result?.completed_requires_empty, ["new_impacts", "stale_candidates"]) && includesAll(result?.completed_requires_non_empty, ["evidence_refs"]) && result?.completed_requires_readable_evidence_refs === true && result?.evidence_ref_validation === "readable-or-resolvable" && result?.completed_requires_no_blocking_signals === true && includesAll(result?.workflow_reference?.required, ["source", "skill", "invocation_mode"]), "Workflow Execution Result 的完成态证据、阻断信号或 workflow_reference 契约不完整");
47
- const units = data.lifecycle_workflow_references;
48
- ensure(units?.["work-unit.discovery-interview"]?.invocation_mode === "model-invoked" && includesAll(units["work-unit.discovery-interview"].skills, ["grilling", "domain-modeling"]), "Discovery work unit 未调用允许的原语");
49
- ensure(units?.["work-unit.code-review"]?.invocation_mode === "model-invoked" && units["work-unit.code-review"].skill === "code-review", "Code review work unit 未使用 model-invoked code-review");
50
+ const native = data.lifecycle_native_entries;
51
+ ensure(native?.default_entry === "yss-product-lifecycle" && native?.formal_artifact_owner === "yss-product-lifecycle", "生命周期原生入口未持有默认正式资产所有权");
52
+ ensure(JSON.stringify(native?.user_confirmation_required_at) === JSON.stringify(["spec-baseline", "prototype-confirmation", "openapi-freeze", "merge-or-release"]), "生命周期人工门禁集合已漂移");
53
+ const routes = data.work_unit_routes;
54
+ ensure(routes?.["work-unit.discovery-requirements"]?.skills?.includes("grilling") && routes?.["work-unit.discovery-requirements"]?.skills?.includes("domain-modeling"), "需求分析工作单元缺少 grilling/domain-modeling");
55
+ ensure(routes?.["work-unit.discovery-opportunity"]?.route_by?.market_or_competitor_fact === "competitive-intelligence" && routes["work-unit.discovery-opportunity"].route_by.technical_or_standard_fact === "research", "机会调研事实路由不准确");
56
+ ensure(routes?.["work-unit.slice-implementation"]?.skills?.includes("tdd") && routes?.["work-unit.slice-implementation"]?.skills?.includes("yss-ui"), "原生实现工作单元缺少 TDD 或 UI 路由");
57
+ ensure(routes?.["work-unit.frontend-implementation-verification"]?.skills?.includes("code-review") && routes?.["work-unit.frontend-implementation-verification"]?.applies_when === "ui_impact", "前端还原验证未绑定 UI fidelity 审查轴");
50
58
  for (const id of ["work-unit.spec-synthesis", "work-unit.ticket-decomposition", "work-unit.slice-implementation"]) {
51
- ensure(units?.[id]?.invocation_mode === "reference" && units[id].formal_artifact_owner === "explicit-user-entry", `${id} 未限制为 workflow reference 与显式正式入口`);
59
+ ensure(routes?.[id]?.native?.source === "yss-product-lifecycle" && routes[id].compatibility?.source === "mattpocock/skills" && routes[id].compatibility.formal_artifact_owner === "explicit-user-entry", `${id} 未分离原生执行定义与 Matt 兼容输入`);
60
+ }
61
+ for (const route of Object.values(routes ?? {})) {
62
+ for (const skill of route.skills ?? []) ensure(!boundary.user_invoked_skills.includes(skill), `工作单元不能自动调用 user-invoked skill: ${skill}`);
63
+ for (const skill of route.skills ?? []) if (boundary.model_invoked_skills.includes(skill)) ensure(boundary.lifecycle_allowed_model_invoked_skills.includes(skill), `工作单元调用了未进入生命周期白名单的 model-invoked skill: ${skill}`);
52
64
  }
53
65
  }
54
66
 
55
- function validateWorkflowExecutionResult(payload, contract, workUnits) {
67
+ function validateWorkflowExecutionResult(payload, contract, workUnitRoutes) {
56
68
  for (const field of contract.required) ensure(Object.hasOwn(payload, field), `Workflow Execution Result 缺少 ${field}`);
57
69
  ensure(contract.result_values.includes(payload.result), "Workflow Execution Result result 无效");
58
70
  for (const field of contract.workflow_reference.required) ensure(typeof payload.workflow_reference?.[field] === "string" && payload.workflow_reference[field].trim(), `workflow_reference.${field} 无效`);
59
- const workUnit = workUnits?.[payload.work_unit];
71
+ const workUnit = workUnitRoutes?.[payload.work_unit];
60
72
  ensure(workUnit, `未知 Workflow Execution Result work_unit: ${payload.work_unit}`);
61
- const expectedSkills = workUnit.workflow_reference ? [workUnit.workflow_reference] : workUnit.skills ?? [workUnit.skill];
62
- ensure(payload.workflow_reference.source === contract.workflow_reference.source && includesAll(expectedSkills, [payload.workflow_reference.skill]) && payload.workflow_reference.invocation_mode === workUnit.invocation_mode, "Workflow Execution Result workflow_reference 与 work_unit 不匹配");
73
+ const accepted = [workUnit.native, workUnit.compatibility].filter(Boolean);
74
+ ensure(contract.workflow_reference.allowed_sources.includes(payload.workflow_reference.source) && accepted.some((route) => route.source === payload.workflow_reference.source && route.skill === payload.workflow_reference.skill && route.invocation_mode === payload.workflow_reference.invocation_mode), "Workflow Execution Result workflow_reference 与 work_unit route 不匹配");
63
75
  if (payload.result !== "completed") return;
64
76
  for (const field of contract.completed_requires_empty) ensure(Array.isArray(payload[field]) && payload[field].length === 0, `completed 的 ${field} 必须为空`);
65
77
  for (const field of contract.completed_requires_non_empty) ensure(Array.isArray(payload[field]) && payload[field].length > 0, `completed 的 ${field} 不能为空`);
@@ -96,7 +108,7 @@ const profiles = {
96
108
  },
97
109
  matt: {
98
110
  message: "Matt/YSS 集成压力场景验证通过",
99
- files: [".agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md", ".agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml"],
111
+ files: [".agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md", ".agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml", "docs/process/templates/lifecycle-checkpoint-template.yaml", "docs/process/templates/frontend-implementation-plan-template.yaml", "docs/process/templates/frontend-implementation-verification-template.yaml"],
100
112
  markers: [[".agents/skills/yss-product-lifecycle/SKILL.md", "Workflow Execution Result"]]
101
113
  },
102
114
  prototype: {
@@ -146,6 +158,13 @@ export function runScenario(name) {
146
158
  if (name === "lifecycle") {
147
159
  const result = spawnSync("scripts/verify-lifecycle-registry", [], { cwd: root, encoding: "utf8" });
148
160
  ensure(result.status === 0, result.stderr || result.stdout);
161
+ const registry = parseDocument(read("docs/process/lifecycle-registry.yaml"), { uniqueKeys: true }).toJS({ maxAliasCount: 0 });
162
+ const releaseGate = registry.gates.find((gate) => gate.id === "gate.release-ready");
163
+ ensure(releaseGate?.requires_gates?.includes("gate.frontend-implementation-verified"), "发布就绪未依赖前端实现还原门禁");
164
+ const contract = parseDocument(read(".agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml"), { uniqueKeys: true }).toJS({ maxAliasCount: 0 });
165
+ ensure(contract.release_readiness?.conditional?.ui_impact?.includes("gate.frontend-implementation-verified") && contract.frontend_implementation_plan?.acceptance?.includes("no_template_placeholders"), "发布公式或前端计划实质校验不完整");
166
+ const templateRejected = spawnSync("scripts/verify-frontend-implementation-evidence", ["docs/process/templates/frontend-implementation-plan-template.yaml"], { cwd: root, encoding: "utf8" });
167
+ ensure(templateRejected.status !== 0 && templateRejected.stderr.includes("template: false"), "前端实现计划占位模板可冒充正式批准证据");
149
168
  }
150
169
  if (name === "matt") {
151
170
  const contract = parseDocument(read(".agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml"), { uniqueKeys: true });
@@ -156,7 +175,7 @@ export function runScenario(name) {
156
175
  const validResult = {
157
176
  result_schema: "workflow-execution-result-v1",
158
177
  work_unit: "work-unit.spec-synthesis",
159
- workflow_reference: { source: "mattpocock/skills", skill: "to-spec", invocation_mode: "reference" },
178
+ workflow_reference: { source: "yss-product-lifecycle", skill: "yss-product-lifecycle", invocation_mode: "model-invoked" },
160
179
  result: "completed",
161
180
  evidence_refs: ["docs/process/lifecycle-registry.yaml"],
162
181
  changed_artifacts: [],
@@ -165,7 +184,10 @@ export function runScenario(name) {
165
184
  next_route: "work-unit.ticket-decomposition",
166
185
  blocking_signals: []
167
186
  };
168
- validateWorkflowExecutionResult(validResult, data.workflow_execution_result, data.lifecycle_workflow_references);
187
+ validateWorkflowExecutionResult(validResult, data.workflow_execution_result, data.work_unit_routes);
188
+ const compatibleResult = structuredClone(validResult);
189
+ compatibleResult.workflow_reference = { source: "mattpocock/skills", skill: "to-spec", invocation_mode: "reference" };
190
+ validateWorkflowExecutionResult(compatibleResult, data.workflow_execution_result, data.work_unit_routes);
169
191
  for (const mutate of [
170
192
  (item) => { delete item.workflow_reference; },
171
193
  (item) => { item.evidence_refs = []; },
@@ -174,11 +196,11 @@ export function runScenario(name) {
174
196
  (item) => { item.new_impacts = ["new-api"]; },
175
197
  (item) => { item.workflow_reference.source = "untrusted/source"; },
176
198
  (item) => { item.workflow_reference.skill = "implement"; },
177
- (item) => { item.workflow_reference.invocation_mode = "model-invoked"; }
199
+ (item) => { item.workflow_reference.invocation_mode = "reference"; }
178
200
  ]) {
179
201
  const invalid = structuredClone(validResult); mutate(invalid);
180
202
  let rejected = false;
181
- try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.lifecycle_workflow_references); } catch { rejected = true; }
203
+ try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes); } catch { rejected = true; }
182
204
  ensure(rejected, "Workflow Execution Result 完成态变异未被拒绝");
183
205
  }
184
206
  let metadataRejected = false;
@@ -8,7 +8,7 @@ export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "
8
8
  const SOURCE_ROOT = path.join(ROOT, ".agents/skills");
9
9
  const LOCK_PATH = path.join(ROOT, "skills-lock.json");
10
10
  const PROJECTION_ROOTS = [".claude/skills", ".codex/skills", ".hermes/skills", ".pi/skills", ".qoder/skills", ".trae/skills"];
11
- const OBSOLETE = new Set(["to-" + "prd", "to-" + "issues", "design-an-interface", "qa", "request-refactor-plan", "ubiquitous-language", "edit-article", "obsidian-vault", "writing-great-skills", "code-review-process", "yss-domain-modeling", "yss-dir", "yss-duckdb", "yss-file", "yss-filerunner", "yss-db2mybatis", "yss-mail", "yss-mapper-dynamic", "yss-quality", "yss-sql-condition", "yss-sql-tpl", "yss-valuation", "yss-variable", "yss-openapi", "web-design-engineer", "web-video-presentation", "wireframe-prototype", "wizard", "git-guardrails-claude-code", "claude-handoff"]);
11
+ const OBSOLETE = new Set(["to-" + "prd", "to-" + "issues", "design-an-interface", "qa", "request-refactor-plan", "ubiquitous-language", "edit-article", "obsidian-vault", "writing-great-skills", "code-review-process", "yss-domain-modeling", "yss-dir", "yss-duckdb", "yss-file", "yss-filerunner", "yss-db2mybatis", "yss-mail", "yss-mapper-dynamic", "yss-quality", "yss-sql-condition", "yss-sql-tpl", "yss-valuation", "yss-variable", "yss-openapi", "web-design-engineer", "web-video-presentation", "wireframe-prototype", "wizard", "git-guardrails-claude-code", "claude-handoff", "batch-grill-me"]);
12
12
 
13
13
  function relative(target) { return path.relative(ROOT, target).replaceAll(path.sep, "/"); }
14
14
  function entries(directory) { return existsSync(directory) ? readdirSync(directory, { withFileTypes: true }) : []; }
@@ -59,7 +59,7 @@ export function syncSkills({ check = false } = {}) {
59
59
  ensureSafeProjection(projection);
60
60
  if (!check) mkdirSync(projection, { recursive: true });
61
61
  const allowed = [...shared, ...Object.keys(lock.skills?.platform?.[root] ?? {})];
62
- for (const entry of unlockedProjectionEntries(entries(projection), allowed, (name) => tracked(relative(path.join(projection, name))))) {
62
+ for (const entry of unlockedProjectionEntries(entries(projection), allowed, (name) => tracked(relative(path.join(projection, name)))).filter((entry) => !OBSOLETE.has(entry.name))) {
63
63
  const target = path.join(projection, entry.name);
64
64
  if (!check && entry.isSymbolicLink() && !existsSync(target)) rmSync(target, { force: true });
65
65
  else drift.push(`unlocked projection: ${relative(target)}`);
@@ -96,16 +96,41 @@ function priorMetadata(lock) {
96
96
  if (!["shared", "platform"].some((key) => key in skills)) return skills;
97
97
  return { ...skills.shared, ...Object.values(skills.platform ?? {}).reduce((all, group) => ({ ...all, ...group }), {}), ...Object.fromEntries(Object.entries(skills).filter(([key]) => !["shared", "platform"].includes(key))) };
98
98
  }
99
- function metadata(name, skillPath, directory, previous, canonical = false) {
99
+ function option(arguments_, name) {
100
+ const prefix = `--${name}=`;
101
+ const value = arguments_.find((argument) => argument.startsWith(prefix));
102
+ return value?.slice(prefix.length) || null;
103
+ }
104
+ function sourceRevisionMap(oldLock, arguments_) {
105
+ const sources = structuredClone(oldLock?.sources ?? {});
106
+ const revision = option(arguments_, "source-revision");
107
+ if (revision) {
108
+ const separator = revision.lastIndexOf(":");
109
+ if (separator <= 0 || separator === revision.length - 1) throw new TypeError("--source-revision 必须使用 source:revision 格式");
110
+ const source = revision.slice(0, separator);
111
+ sources[source] = { ...(sources[source] ?? {}), revision: revision.slice(separator + 1) };
112
+ }
113
+ return sources;
114
+ }
115
+ function metadata(name, skillPath, directory, previous, canonical = false, sources = {}, upstreamRoot = null) {
100
116
  const old = previous[name] ?? {};
101
117
  let recordedPath = old.skillPath ?? skillPath;
102
118
  if (canonical && /^\.(claude|codex|hermes|pi|qoder|trae)\/skills\//.test(recordedPath)) recordedPath = skillPath;
103
119
  const result = { source: old.source ?? "project", sourceType: old.sourceType ?? "local", skillPath: recordedPath, effectiveHash: treeHash(directory) };
104
- if (old.upstreamHash ?? old.computedHash) result.upstreamHash = old.upstreamHash ?? old.computedHash;
120
+ const sourceInfo = sources[result.source];
121
+ if (sourceInfo?.revision) result.sourceRevision = sourceInfo.revision;
122
+ let upstreamDirectory = null;
123
+ if (upstreamRoot && result.source === "mattpocock/skills") {
124
+ upstreamDirectory = path.join(upstreamRoot, recordedPath).replace(/\/SKILL\.md$/, "");
125
+ if (!existsSync(upstreamDirectory)) throw new TypeError(`${name} 的上游路径不存在: ${recordedPath}`);
126
+ result.upstreamHash = treeHash(upstreamDirectory);
127
+ } else if (old.upstreamHash ?? old.computedHash) result.upstreamHash = old.upstreamHash ?? old.computedHash;
128
+ if (result.source === "mattpocock/skills" && result.upstreamHash && result.effectiveHash !== result.upstreamHash) result.adaptationRef = "docs/process/MATT-POCOCK-ENGINEERING-SKILLS.md";
105
129
  return result;
106
130
  }
107
131
  export function updateSkillLock(arguments_ = process.argv.slice(2)) {
108
- const oldLock = parseLock(); const previous = priorMetadata(oldLock);
132
+ const oldLock = parseLock(); const previous = priorMetadata(oldLock); const sources = sourceRevisionMap(oldLock, arguments_); const upstreamRoot = option(arguments_, "upstream-root");
133
+ if (upstreamRoot && !existsSync(upstreamRoot)) throw new TypeError(`--upstream-root 不存在: ${upstreamRoot}`);
109
134
  const additions = arguments_.filter((arg) => arg.startsWith("--add=")).map((arg) => arg.slice(6));
110
135
  const removals = new Set(arguments_.filter((arg) => arg.startsWith("--remove=")).map((arg) => arg.slice(9)));
111
136
  const platformAdds = arguments_.filter((arg) => arg.startsWith("--add-platform=")).map((arg) => arg.slice(15).split(":", 2));
@@ -117,7 +142,7 @@ export function updateSkillLock(arguments_ = process.argv.slice(2)) {
117
142
  if (unlocked.length) throw new TypeError(`发现未登记到 skills-lock.json 的已跟踪共享 skills: ${unlocked.join(", ")}\n确认新增后运行 scripts/update-skill-lock --add=<skill-name>`);
118
143
  const targets = [".agents/skills", ...PROJECTION_ROOTS];
119
144
  const shared = Object.fromEntries(sharedNames.map((name) => {
120
- const item = metadata(name, `.agents/skills/${name}/SKILL.md`, path.join(SOURCE_ROOT, name), previous, true); item.targets = targets; return [name, item];
145
+ const item = metadata(name, `.agents/skills/${name}/SKILL.md`, path.join(SOURCE_ROOT, name), previous, true, sources, upstreamRoot); item.targets = targets; return [name, item];
121
146
  }));
122
147
  const platform = {};
123
148
  for (const root of PROJECTION_ROOTS) {
@@ -126,9 +151,9 @@ export function updateSkillLock(arguments_ = process.argv.slice(2)) {
126
151
  if (!names.length) continue;
127
152
  const absentPlatform = names.filter((name) => !lstatSafe(path.join(location, name))?.isDirectory());
128
153
  if (absentPlatform.length) throw new TypeError(`锁文件声明的平台 skills 缺少内容 (${root}): ${absentPlatform.join(", ")}`);
129
- platform[root] = Object.fromEntries(names.map((name) => { const item = metadata(name, `${root}/${name}/SKILL.md`, path.join(location, name), previous); item.targets = [root]; return [name, item]; }));
154
+ platform[root] = Object.fromEntries(names.map((name) => { const item = metadata(name, `${root}/${name}/SKILL.md`, path.join(location, name), previous, false, sources, upstreamRoot); item.targets = [root]; return [name, item]; }));
130
155
  }
131
- const manifest = { version: 3, generatedBy: "scripts/update-skill-lock", canonicalRoot: ".agents/skills", projectionRoots: PROJECTION_ROOTS, skills: { shared, platform } };
156
+ const manifest = { version: 3, generatedBy: "scripts/update-skill-lock", canonicalRoot: ".agents/skills", projectionRoots: PROJECTION_ROOTS, sources, skills: { shared, platform } };
132
157
  const rendered = `${JSON.stringify(manifest, null, 2)}\n`;
133
158
  if (arguments_.includes("--check")) { if (!existsSync(LOCK_PATH) || readFileSync(LOCK_PATH, "utf8") !== rendered) throw new TypeError("skills-lock.json is stale; run scripts/update-skill-lock"); return "skills-lock.json matches distributed skills"; }
134
159
  writeFileSync(LOCK_PATH, rendered); return `updated skills-lock.json with ${sharedNames.length} shared skills and ${Object.values(platform).reduce((sum, group) => sum + Object.keys(group).length, 0)} platform skills`;
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseDocument } from "./vendor/yaml.mjs";
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const schemaPath = path.join(root, "docs/process/schemas/frontend-implementation-evidence.schema.json");
11
+ const validator = String.raw`
12
+ import json
13
+ import sys
14
+ from jsonschema import Draft202012Validator
15
+ schema = json.load(open(sys.argv[1], encoding="utf-8"))
16
+ value = json.load(sys.stdin)
17
+ errors = sorted(Draft202012Validator(schema).iter_errors(value), key=lambda error: list(error.absolute_path))
18
+ for error in errors:
19
+ location = ".".join(str(part) for part in error.absolute_path) or "<root>"
20
+ print(f"{location}: {error.message}", file=sys.stderr)
21
+ sys.exit(1 if errors else 0)
22
+ `;
23
+
24
+ try {
25
+ const { values, positionals } = parseArgs({ options: { "allow-template": { type: "boolean" } }, allowPositionals: true, strict: true });
26
+ if (positionals.length !== 1) throw new TypeError("用法: scripts/verify-frontend-implementation-evidence [--allow-template] <file.yaml>");
27
+ const document = parseDocument(readFileSync(path.resolve(positionals[0]), "utf8"), { maxAliasCount: 0, uniqueKeys: true });
28
+ if (document.errors.length) throw new TypeError(document.errors[0].message);
29
+ const value = document.toJS({ maxAliasCount: 0 });
30
+ const result = spawnSync("python3", ["-c", validator, schemaPath], { cwd: root, encoding: "utf8", input: JSON.stringify(value) });
31
+ if (result.status !== 0) throw new TypeError(`${result.stdout}${result.stderr}`.trim());
32
+ if (!values["allow-template"] && value.template !== false) throw new TypeError("正式证据必须设置 template: false");
33
+ if (!values["allow-template"] && value.status !== "approved") throw new TypeError("正式证据必须达到 status: approved");
34
+ const serialized = JSON.stringify(value);
35
+ if (!values["allow-template"] && /REPLACE_ME|<[^>]+>|docs\/\.scratch\/example/.test(serialized)) throw new TypeError("正式证据不得保留模板占位值");
36
+ if (!values["allow-template"]) {
37
+ for (const field of ["prototype_ref", "spec_ref"]) {
38
+ const reference = value[field];
39
+ if (!/^https?:\/\//.test(reference) && !existsSync(path.resolve(root, reference))) throw new TypeError(`${field} 不可读取: ${reference}`);
40
+ }
41
+ }
42
+ if (!values["allow-template"] && value.kind === "verification") {
43
+ if (value.console_warning_check !== "pass") throw new TypeError("前端验证必须通过 console warning 检查");
44
+ if (value.uncovered_differences.length > 0 && !value.difference_owner) throw new TypeError("存在未覆盖差异时必须填写 difference_owner");
45
+ if (value.independent_review?.status !== "approved") throw new TypeError("前端验证必须通过独立 UI fidelity review");
46
+ }
47
+ process.stdout.write(`前端实现还原 ${value.kind} 验证通过\n`);
48
+ } catch (error) {
49
+ process.stderr.write(`前端实现还原证据验证失败: ${error.message}\n`);
50
+ process.exitCode = 1;
51
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseDocument } from "./vendor/yaml.mjs";
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const schemaPath = path.join(root, "docs/process/schemas/lifecycle-checkpoint.schema.json");
11
+ const validator = String.raw`
12
+ import json
13
+ import sys
14
+ from jsonschema import Draft202012Validator
15
+ schema = json.load(open(sys.argv[1], encoding="utf-8"))
16
+ value = json.load(sys.stdin)
17
+ errors = sorted(Draft202012Validator(schema).iter_errors(value), key=lambda error: list(error.absolute_path))
18
+ for error in errors:
19
+ location = ".".join(str(part) for part in error.absolute_path) or "<root>"
20
+ print(f"{location}: {error.message}", file=sys.stderr)
21
+ sys.exit(1 if errors else 0)
22
+ `;
23
+
24
+ try {
25
+ const { positionals } = parseArgs({ allowPositionals: true, strict: true });
26
+ if (positionals.length !== 1) throw new TypeError("用法: scripts/verify-lifecycle-checkpoint <checkpoint.yaml|checkpoint.json>");
27
+ const sourcePath = path.resolve(positionals[0]);
28
+ const document = parseDocument(readFileSync(sourcePath, "utf8"), { maxAliasCount: 0, uniqueKeys: true });
29
+ if (document.errors.length) throw new TypeError(document.errors[0].message);
30
+ const value = document.toJS({ maxAliasCount: 0 });
31
+ const result = spawnSync("python3", ["-c", validator, schemaPath], { cwd: root, encoding: "utf8", input: JSON.stringify(value) });
32
+ if (result.status !== 0) throw new TypeError(`${result.stdout}${result.stderr}`.trim());
33
+ process.stdout.write("生命周期 checkpoint 验证通过\n");
34
+ } catch (error) {
35
+ process.stderr.write(`生命周期 checkpoint 验证失败: ${error.message}\n`);
36
+ process.exitCode = 1;
37
+ }
@@ -5,7 +5,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
5
  cd "$ROOT_DIR"
6
6
  node -e 'const [major]=process.versions.node.split(".").map(Number); if (major < 22 || major >= 27) { console.error("模板工具需要 Node >=22 <27"); process.exit(1); }'
7
7
 
8
- required_files=(yss-project.yaml AGENTS.md CONTEXT.md skills-lock.json docs/process/lifecycle-registry.yaml docs/process/lifecycle-registry-baseline.json docs/process/maintenance-intensity.yaml scripts/repository-mode scripts/implementation-path-policy scripts/sync-skills scripts/update-skill-lock scripts/generate-lifecycle-artifacts scripts/verify-lifecycle-registry scripts/verify-maintenance-checkpoint scripts/verify-maintenance-intensity-scenarios scripts/verify-yss-dto-openapi-profile scripts/verify-yss-dto-openapi-scenarios scripts/export-yss-skills)
8
+ required_files=(yss-project.yaml AGENTS.md CONTEXT.md skills-lock.json docs/process/lifecycle-registry.yaml docs/process/lifecycle-registry-baseline.json docs/process/maintenance-intensity.yaml docs/process/schemas/lifecycle-checkpoint.schema.json docs/process/schemas/frontend-implementation-evidence.schema.json docs/process/templates/lifecycle-checkpoint-template.yaml docs/process/templates/frontend-implementation-plan-template.yaml docs/process/templates/frontend-implementation-verification-template.yaml scripts/repository-mode scripts/implementation-path-policy scripts/sync-skills scripts/update-skill-lock scripts/generate-lifecycle-artifacts scripts/verify-lifecycle-registry scripts/verify-lifecycle-checkpoint scripts/verify-frontend-implementation-evidence scripts/verify-maintenance-checkpoint scripts/verify-maintenance-intensity-scenarios scripts/verify-yss-dto-openapi-profile scripts/verify-yss-dto-openapi-scenarios scripts/export-yss-skills)
9
9
  for required in "${required_files[@]}"; do test -f "$required" || { echo "缺少模板必需文件: $required" >&2; exit 1; }; done
10
10
 
11
11
  repository_mode="$(scripts/repository-mode)"
@@ -21,6 +21,9 @@ scripts/update-skill-lock --check
21
21
  scripts/verify-yss-dto-openapi-profile
22
22
  scripts/verify-yss-dto-openapi-scenarios
23
23
  scripts/verify-lifecycle-registry
24
+ scripts/verify-lifecycle-checkpoint docs/process/templates/lifecycle-checkpoint-template.yaml
25
+ scripts/verify-frontend-implementation-evidence --allow-template docs/process/templates/frontend-implementation-plan-template.yaml
26
+ scripts/verify-frontend-implementation-evidence --allow-template docs/process/templates/frontend-implementation-verification-template.yaml
24
27
  scripts/verify-lifecycle-scenarios
25
28
  scripts/verify-maintenance-intensity-scenarios
26
29
  scripts/verify-governance-release
@@ -33,7 +36,7 @@ scripts/verify-yss-ui-scenarios
33
36
  scripts/verify-openapi-yaml-first-scenarios
34
37
  scripts/verify-openapi-json-handoff-scenarios
35
38
 
36
- for entry in scripts/sync-skills scripts/update-skill-lock scripts/generate-lifecycle-artifacts scripts/verify-lifecycle-registry scripts/verify-maintenance-checkpoint scripts/verify-maintenance-intensity-scenarios scripts/verify-yss-dto-openapi-profile scripts/verify-yss-dto-openapi-scenarios scripts/verify-lifecycle-scenarios scripts/verify-matt-yss-integration-scenarios scripts/verify-prototype-backend-scaffold-scenarios scripts/verify-yss-router-scenarios scripts/verify-openapi-yaml-first-scenarios scripts/verify-openapi-json-handoff-scenarios scripts/export-yss-skills; do node --check "$entry"; done
39
+ for entry in scripts/sync-skills scripts/update-skill-lock scripts/generate-lifecycle-artifacts scripts/verify-lifecycle-registry scripts/verify-lifecycle-checkpoint scripts/verify-frontend-implementation-evidence scripts/verify-maintenance-checkpoint scripts/verify-maintenance-intensity-scenarios scripts/verify-yss-dto-openapi-profile scripts/verify-yss-dto-openapi-scenarios scripts/verify-lifecycle-scenarios scripts/verify-matt-yss-integration-scenarios scripts/verify-prototype-backend-scaffold-scenarios scripts/verify-yss-router-scenarios scripts/verify-openapi-yaml-first-scenarios scripts/verify-openapi-json-handoff-scenarios scripts/export-yss-skills; do node --check "$entry"; done
37
40
 
38
41
  legacy_runtime="ru""by"
39
42
  script_roots=(scripts)
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { treeHash } from "./lib/skill-supply-chain.mjs";
6
+
7
+ const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
8
+ const args = process.argv.slice(2);
9
+ const sourceRootArg = args.find((arg) => arg.startsWith("--source-root="))?.slice("--source-root=".length);
10
+ if (!sourceRootArg) {
11
+ process.stderr.write("用法: scripts/verify-upstream-skill-source --source-root=<上游 checkout>\n");
12
+ process.exit(2);
13
+ }
14
+ const sourceRoot = path.resolve(sourceRootArg);
15
+ const lock = JSON.parse(readFileSync(path.join(root, "skills-lock.json"), "utf8"));
16
+ const source = "mattpocock/skills";
17
+ const revision = lock.sources?.[source]?.revision;
18
+ if (!revision) throw new TypeError(`锁文件缺少 ${source} source revision`);
19
+ const git = spawnSync("git", ["-C", sourceRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
20
+ if (git.status !== 0) throw new TypeError(`无法读取上游 checkout revision: ${sourceRoot}`);
21
+ if (git.stdout.trim() !== revision) throw new TypeError(`上游 revision 不匹配: expected=${revision} actual=${git.stdout.trim()}`);
22
+
23
+ const shared = lock.skills?.shared ?? {};
24
+ for (const [name, item] of Object.entries(shared)) {
25
+ if (item.source !== source) continue;
26
+ if (item.sourceRevision !== revision) throw new TypeError(`${name} 缺少一致的 sourceRevision`);
27
+ const directory = path.dirname(path.join(sourceRoot, item.skillPath));
28
+ if (!existsSync(directory)) throw new TypeError(`${name} 的上游路径不存在: ${item.skillPath}`);
29
+ const actualHash = treeHash(directory);
30
+ if (actualHash !== item.upstreamHash) throw new TypeError(`${name} upstreamHash 不匹配: expected=${item.upstreamHash} actual=${actualHash}`);
31
+ }
32
+ process.stdout.write(`上游 ${source}@${revision} 及其 skill hashes 验证通过\n`);