create-yss-spec 3.3.0 → 3.3.1
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/package.json +3 -2
- package/src/command-runner.mjs +107 -0
- package/src/self-update.js +7 -4
- package/src/template/instance-runtime.js +1 -1
- package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
- package/template/.agents/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.agents/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.agents/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.agents/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.agents/skills/yss-research/SKILL.md +1 -1
- package/template/.agents/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/.claude/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.claude/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.claude/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.claude/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.claude/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.claude/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.claude/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.claude/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.claude/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.claude/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.claude/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.claude/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.claude/skills/yss-research/SKILL.md +1 -1
- package/template/.claude/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.claude/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/.codex/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.codex/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.codex/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.codex/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.codex/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.codex/skills/yss-research/SKILL.md +1 -1
- package/template/.codex/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/.cursor/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.cursor/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.cursor/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.cursor/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.cursor/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.cursor/skills/yss-research/SKILL.md +1 -1
- package/template/.cursor/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/.pi/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.pi/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.pi/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.pi/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.pi/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.pi/skills/yss-research/SKILL.md +1 -1
- package/template/.pi/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/.qoder/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.qoder/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.qoder/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.qoder/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.qoder/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.qoder/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.qoder/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.qoder/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.qoder/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.qoder/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.qoder/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.qoder/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.qoder/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.qoder/skills/yss-research/SKILL.md +1 -1
- package/template/.qoder/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.qoder/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/.trae/skills/competitive-intelligence/SKILL.md +4 -4
- package/template/.trae/skills/domain-modeling/CONTEXT-FORMAT.md +1 -1
- package/template/.trae/skills/grill-with-docs/SKILL.md +1 -1
- package/template/.trae/skills/setup-matt-pocock-skills/issue-tracker-local.md +1 -1
- package/template/.trae/skills/yss-ddd-scaffold-generator/scripts/run_scaffold_verification.mjs +7 -7
- package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +1 -1
- package/template/.trae/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +22 -8
- package/template/.trae/skills/yss-frontend-scaffold-generator/scripts/run_scaffold_verification.mjs +5 -6
- package/template/.trae/skills/yss-frontend-scaffold-generator/scripts/scaffold-generator.test.mjs +19 -1
- package/template/.trae/skills/yss-product-lifecycle/SKILL.md +7 -4
- package/template/.trae/skills/yss-product-lifecycle/references/artifact-dependencies.md +1 -1
- package/template/.trae/skills/yss-product-lifecycle/references/matt-yss-adapter.md +3 -3
- package/template/.trae/skills/yss-product-lifecycle/references/orchestration-contract.yaml +57 -6
- package/template/.trae/skills/yss-product-lifecycle/references/orchestration.md +11 -1
- package/template/.trae/skills/yss-research/SKILL.md +1 -1
- package/template/.trae/skills/yss-stage-decision/SKILL.md +5 -3
- package/template/.trae/skills/yss-stage-decision/references/stage-decision-package-contract.md +1 -1
- package/template/AGENTS.md +2 -2
- package/template/CONTEXT.md +3 -2
- package/template/README.md +3 -3
- package/template/docs/agents/digital-human-roles.md +1 -1
- package/template/docs/agents/digital-human-roles.yaml +11 -11
- package/template/docs/agents/issue-tracker.md +1 -1
- package/template/docs/agents/strategic-design-skills-integration.md +2 -0
- package/template/docs/architecture/README.md +1 -1
- package/template/docs/architecture/templates/business-architecture-template.md +1 -1
- package/template/docs/architecture/templates/functional-architecture-template.md +1 -1
- package/template/docs/design/README.md +1 -1
- package/template/docs/design/templates/interaction-spec-template.md +1 -1
- package/template/docs/design/templates/product-overview-design-template.md +1 -1
- package/template/docs/{discovery → plan}/IDEATION.md +5 -5
- package/template/docs/plan/README.md +9 -0
- package/template/docs/plan/entry-review.md +23 -0
- package/template/docs/plan/templates/plan-template.md +58 -0
- package/template/docs/process/harness-process-tailoring.md +1 -1
- package/template/docs/process/lifecycle-artifact-map.md +6 -6
- package/template/docs/process/lifecycle-registry-baseline.json +5 -1
- package/template/docs/process/lifecycle-registry.yaml +25 -20
- package/template/docs/process/plan-migration.md +13 -0
- package/template/docs/process/schemas/digital-human-task-package.schema.json +2 -0
- package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +3 -0
- package/template/docs/process/schemas/lifecycle-registry.schema.json +1 -1
- package/template/docs/process/template-engineering-overview.md +2 -2
- package/template/docs/process/template-verification-profiles.yaml +2 -0
- package/template/docs/process/templates/context-reconciliation-template.yaml +2 -2
- package/template/docs/process/templates/lifecycle-checkpoint-template.yaml +1 -1
- package/template/docs/requirements/README.md +2 -2
- package/template/docs/templates/local-parent-ticket-template.md +1 -1
- package/template/docs/templates/subagent-task-package-template.md +1 -1
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +1 -1
- package/template/scripts/cache-template-commit +2 -2
- package/template/scripts/fixtures/strategic-handoff/source-roles.json +7 -7
- package/template/scripts/fixtures/user-decision/plan-fixture.mjs +30 -0
- package/template/scripts/lib/backend-delivery.mjs +5 -2
- package/template/scripts/lib/command-runner.mjs +107 -0
- package/template/scripts/lib/context-reconciliation.mjs +34 -0
- package/template/scripts/lib/json-schema.mjs +42 -13
- package/template/scripts/lib/lifecycle-context-query.mjs +7 -3
- package/template/scripts/lib/lifecycle-transition.mjs +9 -3
- package/template/scripts/lib/plan-spec-entry.mjs +87 -0
- package/template/scripts/lib/scenario-checks.mjs +15 -12
- package/template/scripts/lib/skill-supply-chain.mjs +18 -2
- package/template/scripts/lib/strategic-handoff-io.mjs +5 -4
- package/template/scripts/lib/strategic-handoff.mjs +10 -9
- package/template/scripts/lib/task-package.mjs +2 -0
- package/template/scripts/lib/template-commit-cache.mjs +7 -5
- package/template/scripts/run-template-verification +9 -3
- package/template/scripts/sync-strategic-handoff-tools +9 -0
- package/template/scripts/verify-context-reconciliation +2 -48
- package/template/scripts/verify-context-reconciliation-scenarios +1 -1
- package/template/scripts/verify-lifecycle-checkpoint +2 -0
- package/template/scripts/verify-lifecycle-context-query-scenarios +28 -8
- package/template/scripts/verify-plan-spec-entry +11 -0
- package/template/scripts/verify-plan-spec-entry-scenarios +79 -0
- package/template/scripts/verify-script-performance-scenarios +56 -0
- package/template/scripts/verify-subagent-task-package-scenarios +9 -8
- package/template/scripts/verify-template-cache-scenarios +11 -1
- package/template/scripts/verify-user-decision-scenarios +4 -4
- package/template/skills-lock.json +17 -17
- package/template.manifest.json +2 -1
- package/template.snapshot.json +5 -5
- package/template/docs/discovery/README.md +0 -87
- package/template/docs/discovery/reports/.gitkeep +0 -0
- package/template/docs/discovery/reports/agent-governance-options.md +0 -71
- package/template/docs/discovery/reports/competitor-deep-dive/.gitkeep +0 -0
- package/template/docs/discovery/templates/discovery-template.md +0 -94
- /package/template/docs/{discovery → plan}/templates/competitive-analysis-template.md +0 -0
- /package/template/docs/{discovery → plan}/templates/competitive-matrix-template.md +0 -0
- /package/template/docs/{discovery → plan}/templates/market-analysis-template.md +0 -0
- /package/template/docs/{discovery → plan}/templates/user-pain-points-template.md +0 -0
|
@@ -2,21 +2,50 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const effectiveSchema = path.resolve(schemaPath);
|
|
7
|
-
if (!existsSync(effectiveSchema)) throw new TypeError(`缺少 ${label}: ${effectiveSchema}`);
|
|
8
|
-
const validator = String.raw`
|
|
5
|
+
const validator = String.raw`
|
|
9
6
|
import json
|
|
10
7
|
import sys
|
|
8
|
+
import traceback
|
|
11
9
|
from jsonschema import Draft202012Validator, FormatChecker
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
validators = {}
|
|
11
|
+
results = []
|
|
12
|
+
for item in json.load(sys.stdin):
|
|
13
|
+
try:
|
|
14
|
+
key = (item['schemaPath'], item['formatChecker'])
|
|
15
|
+
if key not in validators:
|
|
16
|
+
with open(item['schemaPath'], encoding='utf-8') as source:
|
|
17
|
+
schema = json.load(source)
|
|
18
|
+
validators[key] = Draft202012Validator(schema, format_checker=FormatChecker() if key[1] else None)
|
|
19
|
+
errors = list(validators[key].iter_errors(item['value']))
|
|
20
|
+
if item['errorStyle'] == 'verbose':
|
|
21
|
+
message = '\n'.join(str(error) for error in errors)
|
|
22
|
+
else:
|
|
23
|
+
errors.sort(key=lambda error: list(error.absolute_path))
|
|
24
|
+
message = '\n'.join(f"{'.'.join(str(part) for part in error.absolute_path) or '<root>'}: {error.message}" for error in errors)
|
|
25
|
+
results.append({'valid': not errors, 'error': message})
|
|
26
|
+
except Exception:
|
|
27
|
+
results.append({'valid': False, 'error': traceback.format_exc().strip()})
|
|
28
|
+
json.dump(results, sys.stdout)
|
|
19
29
|
`;
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
|
|
31
|
+
/** One immutable validation phase, never a cache of successful work units. */
|
|
32
|
+
export function validateJsonSchemas(items, { cwd = process.cwd() } = {}) {
|
|
33
|
+
if (!Array.isArray(items)) throw new TypeError("schema batch 必须是数组");
|
|
34
|
+
if (!items.length) return [];
|
|
35
|
+
const jobs = items.map(({ value, schemaPath, formatChecker = true, errorStyle = "compact", label = "JSON Schema" }) => {
|
|
36
|
+
const effectiveSchema = path.resolve(schemaPath);
|
|
37
|
+
if (!existsSync(effectiveSchema)) throw new TypeError(`缺少 ${label}: ${effectiveSchema}`);
|
|
38
|
+
if (!["compact", "verbose"].includes(errorStyle)) throw new TypeError(`未知 schema errorStyle: ${errorStyle}`);
|
|
39
|
+
return { value, schemaPath: effectiveSchema, formatChecker, errorStyle };
|
|
40
|
+
});
|
|
41
|
+
const result = spawnSync("python3", ["-c", validator], { cwd, encoding: "utf8", input: JSON.stringify(jobs), maxBuffer: 16 * 1024 * 1024 });
|
|
42
|
+
if (result.error || result.status !== 0) throw new TypeError(result.error?.message || `${result.stdout}${result.stderr}`.trim());
|
|
43
|
+
const results = JSON.parse(result.stdout);
|
|
44
|
+
if (results.length !== jobs.length) throw new TypeError("schema batch 返回数量不匹配");
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateJsonSchema(value, schemaPath, options = {}) {
|
|
49
|
+
const [result] = validateJsonSchemas([{ value, schemaPath, ...options }], options);
|
|
50
|
+
if (!result.valid) throw new TypeError(result.error);
|
|
22
51
|
}
|
|
@@ -70,6 +70,10 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
70
70
|
const lifecycle = validateRegistry(loadRegistry());
|
|
71
71
|
const orchestration = loadYaml(ORCHESTRATION_CONTRACT_REF, "生命周期编排合同");
|
|
72
72
|
const skillRegistry = loadSkillRegistry();
|
|
73
|
+
const planUnit = ['work-unit.plan-opportunity', 'work-unit.plan-requirements', 'work-unit.domain-strategy-design', 'work-unit.stage-decision', 'work-unit.spec-synthesis'].includes(workUnitId);
|
|
74
|
+
const loadPlan = ['stage.plan', 'stage.spec-architecture'].includes(stageId) || planUnit;
|
|
75
|
+
stageId ??= planUnit ? 'stage.plan' : undefined;
|
|
76
|
+
if (loadPlan) include = [...include, 'planning', 'grill_exit'];
|
|
73
77
|
const stage = selectById(lifecycle.stages, stageId, "阶段");
|
|
74
78
|
const workUnit = selectById(lifecycle.work_units, workUnitId, "工作单元");
|
|
75
79
|
|
|
@@ -80,8 +84,8 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
const route = workUnitId ? orchestration.work_unit_routes?.[workUnitId] ?? null : null;
|
|
83
|
-
const gates =
|
|
84
|
-
const artifacts =
|
|
87
|
+
const gates = lifecycle.gates.filter((gate) => gate.stage === stageId || (loadPlan && gate.stage === 'stage.plan'));
|
|
88
|
+
const artifacts = lifecycle.artifacts.filter((artifact) => artifact.stage === stageId || (loadPlan && artifact.stage === 'stage.plan'));
|
|
85
89
|
const evidenceIds = new Set(gates.flatMap((gate) => gate.evidence ?? []));
|
|
86
90
|
const selected = Object.fromEntries(normalizedIncludes.map((key) => [key, orchestration[key]]));
|
|
87
91
|
const transition = workUnitId ? {
|
|
@@ -124,6 +128,7 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
124
128
|
execution: {
|
|
125
129
|
mode: mode ? orchestration.modes[mode] : null,
|
|
126
130
|
selected,
|
|
131
|
+
...(loadPlan ? { plan_checks: lifecycle.stages.find(stage => stage.id === 'stage.plan').spec_entry.required_checks.map(id => ({ id, status: 'pending', evidence_refs: [] })) } : {}),
|
|
127
132
|
transition,
|
|
128
133
|
work_unit_route: route,
|
|
129
134
|
},
|
|
@@ -134,4 +139,3 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
134
139
|
.sort();
|
|
135
140
|
return canonicalize(result);
|
|
136
141
|
}
|
|
137
|
-
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { parseDocument } from "../vendor/yaml.mjs";
|
|
3
3
|
import { assertWorkUnitUserDecision, assertImplementationDecision } from "./user-decision.mjs";
|
|
4
4
|
import { enforceFrontendDelivery } from "./frontend-delivery-boundary.mjs";
|
|
5
|
+
import { validatePlanSpecEntry } from "./plan-spec-entry.mjs";
|
|
5
6
|
|
|
6
7
|
const IMPLEMENTATION_WORK_UNIT = "work-unit.slice-implementation";
|
|
7
8
|
const TICKET_DECOMPOSITION_WORK_UNIT = "work-unit.ticket-decomposition";
|
|
@@ -15,7 +16,7 @@ function deepFreeze(value) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const NEXT_ROUTES = deepFreeze({
|
|
18
|
-
"work-unit.entry-triage": ["work-unit.
|
|
19
|
+
"work-unit.entry-triage": ["work-unit.plan-opportunity", "work-unit.plan-requirements"],
|
|
19
20
|
"work-unit.ssot-update": ["work-unit.skill-projection-sync", "work-unit.intensity-aware-verification"],
|
|
20
21
|
"work-unit.skill-projection-sync": ["work-unit.template-snapshot-build", "work-unit.intensity-aware-verification"],
|
|
21
22
|
"work-unit.template-snapshot-build": ["work-unit.attach-sync-integration", "work-unit.intensity-aware-verification"],
|
|
@@ -23,8 +24,8 @@ const NEXT_ROUTES = deepFreeze({
|
|
|
23
24
|
"work-unit.intensity-aware-verification": ["work-unit.intensity-aware-review"],
|
|
24
25
|
"work-unit.intensity-aware-review": ["work-unit.release-and-rollback"],
|
|
25
26
|
"work-unit.release-and-rollback": [],
|
|
26
|
-
"work-unit.
|
|
27
|
-
"work-unit.
|
|
27
|
+
"work-unit.plan-opportunity": ["work-unit.plan-requirements", "work-unit.domain-strategy-design", "work-unit.stage-decision", "work-unit.spec-synthesis"],
|
|
28
|
+
"work-unit.plan-requirements": ["work-unit.domain-strategy-design", "work-unit.stage-decision", "work-unit.spec-synthesis"],
|
|
28
29
|
"work-unit.domain-strategy-design": ["work-unit.stage-decision", "work-unit.spec-synthesis"],
|
|
29
30
|
"work-unit.stage-decision": ["work-unit.spec-synthesis"],
|
|
30
31
|
"work-unit.spec-synthesis": ["work-unit.prototype-design", "work-unit.technical-analysis", REPOSITORY_PREPARATION_WORK_UNIT],
|
|
@@ -124,6 +125,7 @@ function validateTicketReference(ref, trackerKind) {
|
|
|
124
125
|
* by `validateWorkflowExecutionResult` before this function is called.
|
|
125
126
|
*/
|
|
126
127
|
export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, options = {}) {
|
|
128
|
+
|
|
127
129
|
if (["work-unit.technical-analysis", TICKET_DECOMPOSITION_WORK_UNIT, IMPLEMENTATION_WORK_UNIT, "work-unit.frontend-implementation-verification"].includes(nextRoute)) {
|
|
128
130
|
try { enforceFrontendDelivery(decisionState, { root: options.root, phase: nextRoute === IMPLEMENTATION_WORK_UNIT ? "implementation" : "inputs" }); }
|
|
129
131
|
catch (error) { return blockedResult(["frontend-delivery-blocked"], [error.message]); }
|
|
@@ -141,6 +143,10 @@ export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, opt
|
|
|
141
143
|
}
|
|
142
144
|
return blockedResult(signals, ["allowed_next_route"]);
|
|
143
145
|
}
|
|
146
|
+
if (nextRoute === 'work-unit.spec-synthesis' || currentWorkUnit === 'work-unit.spec-synthesis') {
|
|
147
|
+
const entry = validatePlanSpecEntry(decisionState, options);
|
|
148
|
+
if (entry.result === 'blocked' || nextRoute === 'work-unit.spec-synthesis') return entry;
|
|
149
|
+
}
|
|
144
150
|
if (nextRoute === TICKET_DECOMPOSITION_WORK_UNIT) {
|
|
145
151
|
const readiness = validateImplementationRepositoriesReady(decisionState, options);
|
|
146
152
|
if (readiness.result === "blocked") return readiness;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { verifyContextReconciliation } from './context-reconciliation.mjs';
|
|
3
|
+
import { decisionIO, decisionDigest, assertUserDecisionRequirement } from './user-decision.mjs';
|
|
4
|
+
import { validateApprovalRecord } from './approval-record.mjs';
|
|
5
|
+
|
|
6
|
+
const fail = message => { throw new TypeError(`plan-spec-entry-blocked: ${message}`); };
|
|
7
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
|
|
9
|
+
export function planEntryPolicy(options = {}) {
|
|
10
|
+
const registry = decisionIO(options).document('docs/process/lifecycle-registry.yaml');
|
|
11
|
+
const policy = registry.stages.find(stage => stage.id === 'stage.plan')?.spec_entry;
|
|
12
|
+
if (!policy?.required_checks?.length || !policy?.gate_impacts) fail('缺少 Plan 入口策略');
|
|
13
|
+
const gateIds = registry.gates.filter(gate => gate.stage === 'stage.plan').map(gate => gate.id).sort();
|
|
14
|
+
if (JSON.stringify(gateIds) !== JSON.stringify(Object.keys(policy.gate_impacts).sort())) fail('Plan 门禁触发策略覆盖不完整');
|
|
15
|
+
return policy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 自动加载只产生 pending;持久化审阅包、证据新鲜度和真实回复共同决定放行。
|
|
19
|
+
export function assertPlanSpecEntry(state, options = {}) {
|
|
20
|
+
if (!text(state?.plan_review_ref) || !text(state?.feature_id)) fail('缺少 plan_review_ref / feature_id');
|
|
21
|
+
const io = decisionIO(options);
|
|
22
|
+
const policy = planEntryPolicy(options);
|
|
23
|
+
const review = io.document(state.plan_review_ref);
|
|
24
|
+
if (review.schema_version !== 1 || review.kind !== 'plan-entry-review' || review.feature_id !== state.feature_id) fail('审阅包身份或范围不匹配');
|
|
25
|
+
if (!Array.isArray(review.basis) || !review.basis.length) fail('缺少带摘要依据');
|
|
26
|
+
const basis = new Map();
|
|
27
|
+
for (const asset of review.basis) {
|
|
28
|
+
if (!text(asset.ref) || basis.has(asset.ref) || decisionDigest(io.bytes(asset.ref)) !== asset.digest) fail(`依据缺失、重复或已变更: ${asset.ref}`);
|
|
29
|
+
basis.set(asset.ref, asset);
|
|
30
|
+
}
|
|
31
|
+
const evidence = refs => {
|
|
32
|
+
if (!Array.isArray(refs) || !refs.length || refs.some(ref => !basis.has(ref))) fail('检查项缺少当前依据');
|
|
33
|
+
};
|
|
34
|
+
for (const ref of [review.plan_ref, 'CONTEXT.md', 'docs/process/lifecycle-registry.yaml', review.context_reconciliation_ref]) {
|
|
35
|
+
if (!basis.has(ref)) fail(`必需依据未绑定: ${ref}`);
|
|
36
|
+
}
|
|
37
|
+
if (Object.keys(review.checks || {}).length !== policy.required_checks.length) fail('检查项缺失或存在未知项');
|
|
38
|
+
for (const id of policy.required_checks) {
|
|
39
|
+
const check = review.checks[id];
|
|
40
|
+
if (check?.status !== 'passed') fail(`检查未通过: ${id}`);
|
|
41
|
+
evidence(check.evidence_refs);
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(review.open_items)) fail('必须显式列出未决项');
|
|
44
|
+
for (const item of review.open_items) {
|
|
45
|
+
if (!text(item.id) || typeof item.critical !== 'boolean' || typeof item.runnable_blocker !== 'boolean') fail('未决项分类不完整');
|
|
46
|
+
if (item.status === 'resolved') { evidence(item.evidence_refs); continue; }
|
|
47
|
+
if (item.status !== 'deferred' || item.critical || item.runnable_blocker) fail(`关键问题或可执行阻塞未解决: ${item.id}`);
|
|
48
|
+
for (const field of ['noncritical_reason', 'owner', 'resolution_point', 'downstream_recipient']) if (!text(item[field])) fail(`延期项缺少 ${field}`);
|
|
49
|
+
evidence(item.evidence_refs);
|
|
50
|
+
}
|
|
51
|
+
for (const [gateId, impact] of Object.entries(policy.gate_impacts)) {
|
|
52
|
+
if (typeof review.impacts?.[impact] !== 'boolean') fail(`未评估影响面: ${impact}`);
|
|
53
|
+
const gate = review.gates?.[gateId];
|
|
54
|
+
evidence(gate?.evidence_refs);
|
|
55
|
+
if (review.impacts[impact]) {
|
|
56
|
+
if (gate.status !== 'approved' || !basis.has(gate.approval_ref) || !basis.has(gate.subject_ref)) fail(`命中门禁未批准或未绑定依据: ${gateId}`);
|
|
57
|
+
const record = io.document(gate.approval_ref);
|
|
58
|
+
if (record.gate_id !== gateId || record.subject_ref !== gate.subject_ref || !gate.approval_scope?.includes(state.feature_id) || JSON.stringify([...(gate.approval_scope || [])].sort()) !== JSON.stringify([...(record.approval_scope || [])].sort())) fail(`会签资产或范围不匹配: ${gateId}`);
|
|
59
|
+
validateApprovalRecord(record, { ...options, requireApproved: true });
|
|
60
|
+
assertUserDecisionRequirement({ boundary: gateId, subject_ref: gate.subject_ref, scope: gate.approval_scope, user_decision_ref: record.user_decision_ref }, options);
|
|
61
|
+
} else if (gate.status !== 'not-applicable' || !text(gate.reason)) fail(`未命中门禁须有原因和依据: ${gateId}`);
|
|
62
|
+
}
|
|
63
|
+
// 门禁依赖不能通过将上游标成 N/A 来跳过。
|
|
64
|
+
const registry = io.document('docs/process/lifecycle-registry.yaml');
|
|
65
|
+
for (const gate of registry.gates.filter(gate => gate.stage === 'stage.plan')) {
|
|
66
|
+
if (review.gates[gate.id]?.status === 'approved') {
|
|
67
|
+
for (const dependency of gate.requires_gates || []) if (review.gates[dependency]?.status !== 'approved') fail(`门禁依赖未批准: ${dependency}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const reconciliation = io.document(review.context_reconciliation_ref);
|
|
71
|
+
if (reconciliation.status !== 'reconciled' || reconciliation.repository_mode !== 'project-instance') fail('Context 未调和');
|
|
72
|
+
try { verifyContextReconciliation(path.resolve(io.root, review.context_reconciliation_ref), { root: io.root }); }
|
|
73
|
+
catch (error) { fail(`Context reconciliation 验证失败: ${error.message}\n`); }
|
|
74
|
+
assertUserDecisionRequirement({ boundary: 'plan-conclusion', subject_ref: state.plan_review_ref, scope: [state.feature_id], user_decision_ref: state.plan_user_decision_ref }, options);
|
|
75
|
+
return { result: 'allowed', blocking_signals: [], missing_requirements: [], evidence_refs: [state.plan_review_ref, ...basis.keys()], next_work_unit: null };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validatePlanSpecEntry(state, options = {}) {
|
|
79
|
+
try { return assertPlanSpecEntry(state, options); }
|
|
80
|
+
catch (error) { return { result: 'blocked', blocking_signals: ['plan-spec-entry-blocked'], missing_requirements: [error.message], evidence_refs: [], next_work_unit: null }; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function assertPlanCheckpoint(checkpoint, options = {}) {
|
|
84
|
+
if (checkpoint.repository_mode !== 'project-instance') return;
|
|
85
|
+
const entering = checkpoint.next_work_unit === 'work-unit.spec-synthesis' || checkpoint.stage === 'stage.spec-architecture' || checkpoint.stage_trace?.completed_work_unit === 'work-unit.spec-synthesis';
|
|
86
|
+
if (entering) assertPlanSpecEntry(checkpoint, options);
|
|
87
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { buildPlanFixture } from "../fixtures/user-decision/plan-fixture.mjs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import { buildDecisionFixture, buildImplementationFixture } from "../fixtures/user-decision/build-fixture.mjs";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -58,8 +59,8 @@ function validateInvocationBoundary(data) {
|
|
|
58
59
|
ensure(native?.default_entry === "yss-product-lifecycle" && native?.formal_artifact_owner === "yss-product-lifecycle", "生命周期原生入口未持有默认正式资产所有权");
|
|
59
60
|
ensure(native?.user_confirmation_policy_ref === "docs/agents/digital-human-roles.yaml#user_decision_policy", "生命周期人工门禁集合已漂移");
|
|
60
61
|
const routes = data.work_unit_routes;
|
|
61
|
-
ensure(routes?.["work-unit.
|
|
62
|
-
ensure(routes?.["work-unit.
|
|
62
|
+
ensure(routes?.["work-unit.plan-requirements"]?.skills?.includes("grilling") && routes?.["work-unit.plan-requirements"]?.skills?.includes("domain-modeling"), "需求分析工作单元缺少 grilling/domain-modeling");
|
|
63
|
+
ensure(routes?.["work-unit.plan-opportunity"]?.route_by?.market_or_competitor_fact === "competitive-intelligence" && routes["work-unit.plan-opportunity"].route_by.technical_or_standard_fact === "yss-research:technical-evidence" && routes["work-unit.plan-opportunity"].route_by.strategy_fact === "yss-research:strategy-evidence", "机会调研事实路由不准确");
|
|
63
64
|
const strategyResearch = routes?.["work-unit.domain-strategy-design"]?.research_contract;
|
|
64
65
|
ensure(strategyResearch?.profile === "strategy-evidence" && strategyResearch.mode_before_gate === "evidence-audited" && strategyResearch.artifact_owner === "yss-research" && strategyResearch.downstream_owner === "yss-stage-decision", "领域战略研究合同缺少 profile、门禁前审计或资产所有权边界");
|
|
65
66
|
const stageDecisionResearch = routes?.["work-unit.stage-decision"]?.research_contract;
|
|
@@ -94,7 +95,7 @@ function validateInvocationBoundary(data) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function validateWorkflowExecutionResult(payload, contract, workUnitRoutes) {
|
|
98
|
+
function validateWorkflowExecutionResult(payload, contract, workUnitRoutes, options = {}) {
|
|
98
99
|
for (const field of contract.required) ensure(Object.hasOwn(payload, field), `Workflow Execution Result 缺少 ${field}`);
|
|
99
100
|
ensure(contract.result_values.includes(payload.result), "Workflow Execution Result result 无效");
|
|
100
101
|
if (Object.hasOwn(payload, "unavailable_skill")) {
|
|
@@ -107,7 +108,7 @@ function validateWorkflowExecutionResult(payload, contract, workUnitRoutes) {
|
|
|
107
108
|
const workUnit = workUnitRoutes?.[payload.work_unit];
|
|
108
109
|
ensure(workUnit, `未知 Workflow Execution Result work_unit: ${payload.work_unit}`);
|
|
109
110
|
if (payload.result === "completed") {
|
|
110
|
-
const routeResult = validateNextRoute(payload.work_unit, payload.next_route, payload);
|
|
111
|
+
const routeResult = validateNextRoute(payload.work_unit, payload.next_route, payload, options);
|
|
111
112
|
ensure(routeResult.result === "allowed", `Workflow Execution Result next_route 非法: ${routeResult.blocking_signals.join(", ")}`);
|
|
112
113
|
ensure(payload.context_reconciliation?.status === "reconciled", "project-instance 完成态必须具有 reconciled context_reconciliation");
|
|
113
114
|
ensure(hasText(payload.context_reconciliation?.ref) && payload.evidence_refs.includes(payload.context_reconciliation.ref) && exists(payload.context_reconciliation.ref), "context_reconciliation.ref 必须可读并包含在 evidence_refs 中");
|
|
@@ -268,7 +269,9 @@ export function runScenario(name) {
|
|
|
268
269
|
process.on("exit", () => rmSync(decisionTemp, { recursive: true, force: true }));
|
|
269
270
|
const specDecision = buildDecisionFixture(path.join(decisionTemp, "spec"));
|
|
270
271
|
const implementationDecision = buildImplementationFixture(path.join(decisionTemp, "implementation"), "docs/.scratch/demo/issues/01-valid-slice.md");
|
|
272
|
+
const planFixture = buildPlanFixture(path.join(decisionTemp, 'plan'));
|
|
271
273
|
const validResult = {
|
|
274
|
+
...planFixture.state,
|
|
272
275
|
user_decisions: [specDecision.requirement],
|
|
273
276
|
result_schema: "workflow-execution-result-v1",
|
|
274
277
|
work_unit: "work-unit.spec-synthesis",
|
|
@@ -282,7 +285,7 @@ export function runScenario(name) {
|
|
|
282
285
|
next_route: "work-unit.implementation-repository-preparation",
|
|
283
286
|
blocking_signals: []
|
|
284
287
|
};
|
|
285
|
-
validateWorkflowExecutionResult(validResult, data.workflow_execution_result, data.work_unit_routes);
|
|
288
|
+
validateWorkflowExecutionResult(validResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
|
|
286
289
|
const validTicketResult = {
|
|
287
290
|
...validResult,
|
|
288
291
|
work_unit: "work-unit.ticket-decomposition",
|
|
@@ -290,7 +293,7 @@ export function runScenario(name) {
|
|
|
290
293
|
ticket_decomposition_result_ref: "docs/.scratch/demo/evidence/ticket-decomposition-result.yaml",
|
|
291
294
|
vertical_slice_ticket_ref: "docs/.scratch/demo/issues/01-valid-slice.md",
|
|
292
295
|
};
|
|
293
|
-
validateWorkflowExecutionResult(validTicketResult, data.workflow_execution_result, data.work_unit_routes);
|
|
296
|
+
validateWorkflowExecutionResult(validTicketResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
|
|
294
297
|
const validImplementationResult = {
|
|
295
298
|
...validResult,
|
|
296
299
|
...implementationDecision.state,
|
|
@@ -317,7 +320,7 @@ export function runScenario(name) {
|
|
|
317
320
|
],
|
|
318
321
|
},
|
|
319
322
|
};
|
|
320
|
-
validateWorkflowExecutionResult(validImplementationResult, data.workflow_execution_result, data.work_unit_routes);
|
|
323
|
+
validateWorkflowExecutionResult(validImplementationResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
|
|
321
324
|
for (const mutate of [
|
|
322
325
|
(item) => { item.vertical_slice_ticket_role = "ready-for-human"; },
|
|
323
326
|
(item) => { item.vertical_slice_ticket_kind = "parent-ticket"; },
|
|
@@ -329,22 +332,22 @@ export function runScenario(name) {
|
|
|
329
332
|
]) {
|
|
330
333
|
const invalid = structuredClone(validImplementationResult); mutate(invalid);
|
|
331
334
|
let rejected = false;
|
|
332
|
-
try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes); } catch { rejected = true; }
|
|
335
|
+
try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root }); } catch { rejected = true; }
|
|
333
336
|
ensure(rejected, "实现 Workflow Execution Result 的 Ticket 语义变异未被拒绝");
|
|
334
337
|
}
|
|
335
338
|
const unavailableResult = structuredClone(validResult);
|
|
336
339
|
unavailableResult.result = "blocked";
|
|
337
340
|
unavailableResult.unavailable_skill = { skill: "yss-ui", provider: "codex", fallback: "manual-review", resolution: "needs-human" };
|
|
338
341
|
unavailableResult.blocking_signals = ["missing_evidence"];
|
|
339
|
-
validateWorkflowExecutionResult(unavailableResult, data.workflow_execution_result, data.work_unit_routes);
|
|
342
|
+
validateWorkflowExecutionResult(unavailableResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
|
|
340
343
|
const malformedUnavailable = structuredClone(unavailableResult);
|
|
341
344
|
delete malformedUnavailable.unavailable_skill.fallback;
|
|
342
345
|
let unavailableRejected = false;
|
|
343
|
-
try { validateWorkflowExecutionResult(malformedUnavailable, data.workflow_execution_result, data.work_unit_routes); } catch { unavailableRejected = true; }
|
|
346
|
+
try { validateWorkflowExecutionResult(malformedUnavailable, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root }); } catch { unavailableRejected = true; }
|
|
344
347
|
ensure(unavailableRejected, "技能不可用结果缺少 fallback 时未被拒绝");
|
|
345
348
|
const compatibleResult = structuredClone(validResult);
|
|
346
349
|
compatibleResult.workflow_reference = { source: "mattpocock/skills", skill: "to-spec", invocation_mode: "reference" };
|
|
347
|
-
validateWorkflowExecutionResult(compatibleResult, data.workflow_execution_result, data.work_unit_routes);
|
|
350
|
+
validateWorkflowExecutionResult(compatibleResult, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root });
|
|
348
351
|
for (const mutate of [
|
|
349
352
|
(item) => { delete item.workflow_reference; },
|
|
350
353
|
(item) => { delete item.context_reconciliation; },
|
|
@@ -360,7 +363,7 @@ export function runScenario(name) {
|
|
|
360
363
|
]) {
|
|
361
364
|
const invalid = structuredClone(validResult); mutate(invalid);
|
|
362
365
|
let rejected = false;
|
|
363
|
-
try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes); } catch { rejected = true; }
|
|
366
|
+
try { validateWorkflowExecutionResult(invalid, data.workflow_execution_result, data.work_unit_routes, { root: planFixture.root }); } catch { rejected = true; }
|
|
364
367
|
ensure(rejected, "Workflow Execution Result 完成态变异未被拒绝");
|
|
365
368
|
}
|
|
366
369
|
let metadataRejected = false;
|
|
@@ -117,7 +117,16 @@ function loadStrategicDesignSkillManifest() {
|
|
|
117
117
|
return validateStrategicDesignSkillManifest(JSON.parse(readFileSync(STRATEGIC_DESIGN_MANIFEST_PATH, "utf8")));
|
|
118
118
|
}
|
|
119
119
|
function git(args) { return spawnSync("git", args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); }
|
|
120
|
-
function
|
|
120
|
+
function trackedPaths() {
|
|
121
|
+
const result = git(["ls-files", "-z"]);
|
|
122
|
+
if (result.status !== 0) throw new TypeError("无法读取 Git 受跟踪路径");
|
|
123
|
+
const paths = new Set();
|
|
124
|
+
for (const file of result.stdout.split("\0").filter(Boolean)) {
|
|
125
|
+
let ref = file;
|
|
126
|
+
while (ref !== ".") { paths.add(ref); ref = path.posix.dirname(ref); }
|
|
127
|
+
}
|
|
128
|
+
return paths;
|
|
129
|
+
}
|
|
121
130
|
function parseLock() { return existsSync(LOCK_PATH) ? JSON.parse(readFileSync(LOCK_PATH, "utf8")) : null; }
|
|
122
131
|
function sharedFromLock(lock) {
|
|
123
132
|
const shared = lock?.version === 3 && lock.skills?.shared;
|
|
@@ -137,6 +146,13 @@ export function unlockedCanonicalEntries(names, allowedNames, hasSkillMd = () =>
|
|
|
137
146
|
return names.filter((name) => !allowed.has(name) && !OBSOLETE.has(name) && hasSkillMd(name)).sort();
|
|
138
147
|
}
|
|
139
148
|
export function syncSkills({ check = false } = {}) {
|
|
149
|
+
let trackedSet;
|
|
150
|
+
const tracked = ref => (trackedSet ??= trackedPaths()).has(ref);
|
|
151
|
+
const sourceHashes = new Map();
|
|
152
|
+
const sourceHash = source => {
|
|
153
|
+
if (!sourceHashes.has(source)) sourceHashes.set(source, treeHash(source));
|
|
154
|
+
return sourceHashes.get(source);
|
|
155
|
+
};
|
|
140
156
|
const lock = parseLock();
|
|
141
157
|
const shared = sharedFromLock(lock);
|
|
142
158
|
const absent = shared.filter((name) => !lstatSafe(path.join(SOURCE_ROOT, name))?.isDirectory());
|
|
@@ -162,7 +178,7 @@ export function syncSkills({ check = false } = {}) {
|
|
|
162
178
|
if (info?.isSymbolicLink()) {
|
|
163
179
|
if (!existsSync(target) || realpathSync(target) !== realpathSync(source)) drift.push(`projection target mismatch: ${relative(target)}`);
|
|
164
180
|
} else if (!info?.isDirectory()) drift.push(`missing projection: ${relative(target)}`);
|
|
165
|
-
else if (
|
|
181
|
+
else if (sourceHash(source) !== treeHash(target)) drift.push(`projection drift: ${relative(target)}`);
|
|
166
182
|
} else if (info?.isSymbolicLink() && existsSync(target) && realpathSync(target) === realpathSync(source)) {
|
|
167
183
|
continue;
|
|
168
184
|
} else {
|
|
@@ -3,6 +3,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, mkdirSync, writeFileS
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { validateJsonSchemas } from './json-schema.mjs';
|
|
6
7
|
import { parseDocument } from '../vendor/yaml.mjs';
|
|
7
8
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
8
9
|
export const ensure = (condition, message) => { if (!condition) throw new TypeError(message); };
|
|
@@ -50,11 +51,11 @@ export function project(root) {
|
|
|
50
51
|
ensure(value.schema_version === 1 && value.repository_mode === 'project-instance', '只允许 project-instance 导出或导入业务包');
|
|
51
52
|
return realpathSync(root);
|
|
52
53
|
}
|
|
53
|
-
export function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
ensure(out.status === 0, `schema 校验失败: ${out.error?.message || out.stdout || out.stderr}`);
|
|
54
|
+
export function schemaBatch(items) {
|
|
55
|
+
const results = validateJsonSchemas(items.map(([value, ref]) => ({ value, schemaPath: safe(ROOT, ref), formatChecker: false, errorStyle: 'verbose' })));
|
|
56
|
+
for (const result of results) ensure(result.valid, `schema 校验失败: ${result.error}\n`);
|
|
57
57
|
}
|
|
58
|
+
export function schema(value, ref) { schemaBatch([[value, ref]]); }
|
|
58
59
|
export function archive(command, source, destination) {
|
|
59
60
|
const out = spawnSync('python3', [path.join(ROOT, 'scripts/lib/strategic-handoff-zip.py'), command, source, destination], { encoding: 'utf8' });
|
|
60
61
|
ensure(out.status === 0, `ZIP 操作失败: ${out.error?.message || out.stderr}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, cpSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
|
-
import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, archive, ROOT, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
|
|
4
|
+
import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, schemaBatch, archive, ROOT, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
|
|
5
5
|
import { parseContextSource, parseContextContract, resolveContextTermRefs } from './context-contract.mjs';
|
|
6
6
|
import { countersignRuleForGate } from './digital-human-roles.mjs';
|
|
7
7
|
import { validateApprovalRecord } from './approval-record.mjs';
|
|
@@ -91,8 +91,7 @@ export async function inspectSource(root, handoffRef) {
|
|
|
91
91
|
snapshot(handoff.source_context_snapshot,source); checkDelta(handoff,source);
|
|
92
92
|
const strategy=read(safe(root,handoff.source.domain_strategy_ref.persisted_ref));
|
|
93
93
|
const stage=read(safe(root,handoff.source.stage_decision_package_ref.persisted_ref));
|
|
94
|
-
|
|
95
|
-
schema(stage,'docs/process/schemas/strategic-handoff-stage-decision-package.schema.json');
|
|
94
|
+
schemaBatch([[strategy,'docs/process/schemas/strategic-handoff-domain-strategy.schema.json'],[stage,'docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']]);
|
|
96
95
|
snapshot(strategy.context_snapshot,source); snapshot(stage.context_snapshot,source);
|
|
97
96
|
ensure(strategy.status==='approved' && stage.status==='approved', '战略合同/方案决策包尚未批准');
|
|
98
97
|
ensure(strategy.domain_strategy_id===handoff.source.domain_strategy_ref.id && strategy.domain_version===handoff.source.domain_strategy_ref.version, '战略身份/版本不一致');
|
|
@@ -141,21 +140,22 @@ function collect(root, handoffRef, handoff, config) {
|
|
|
141
140
|
if(Array.isArray(value)) { if(key==='evidence_refs') value.forEach(enqueue); else value.forEach(v=>refs(v,key)); }
|
|
142
141
|
else if(value && typeof value==='object') for(const [k,v]of Object.entries(value)) { if(['approval_ref','persisted_ref','user_decision_ref'].includes(k)&&typeof v==='string')enqueue(v); else if(k==='ref'&&typeof v==='string'&&!/^[a-z]+:\/\//i.test(v))enqueue(v); else refs(v,k); }
|
|
143
142
|
}
|
|
143
|
+
let totalBytes = 0;
|
|
144
144
|
while(queue.length) {
|
|
145
145
|
const ref=symbolic[queue[0]]||queue[0]; queue.shift(); if(/^https?:\/\//i.test(ref))continue; relative(ref);
|
|
146
146
|
if(collected.has(ref))continue;
|
|
147
147
|
const full=safe(root,ref),stat=lstatSync(full);
|
|
148
148
|
if(stat.isDirectory()) { queue.push(...files(root,ref)); continue; }
|
|
149
149
|
ensure(stat.isFile() && stat.size<=MAX_BYTES,`源文件类型/大小无效: ${ref}`);
|
|
150
|
-
const bytes=readFileSync(full); collected.set(ref,bytes);
|
|
151
|
-
ensure(collected.size<=20000 &&
|
|
150
|
+
const bytes=readFileSync(full); collected.set(ref,bytes); totalBytes += bytes.length;
|
|
151
|
+
ensure(collected.size<=20000 && totalBytes<=MAX_BYTES,'交接包大小超限');
|
|
152
152
|
if(/\.(yaml|yml|json)$/.test(ref))refs(parse(bytes));
|
|
153
153
|
queue.push(...documentLinks(ref,bytes));
|
|
154
154
|
}
|
|
155
155
|
return collected;
|
|
156
156
|
}
|
|
157
|
-
function materialize(
|
|
158
|
-
for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,
|
|
157
|
+
function materialize(captured, manifest, destination) {
|
|
158
|
+
for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,captured.get(file.path));
|
|
159
159
|
}
|
|
160
160
|
export async function openBundle(input, action) {
|
|
161
161
|
const temp=mkdtempSync(path.join(tmpdir(),'yss-handoff-'));
|
|
@@ -170,10 +170,11 @@ export async function openBundle(input, action) {
|
|
|
170
170
|
ensure(new Set(paths.map(x=>x.toLowerCase())).size===paths.length,'包文件路径重复');
|
|
171
171
|
ensure(own([...paths,'manifest.json'].sort(),files(root).sort()),'包文件缺失或存在未登记文件');
|
|
172
172
|
ensure(manifest.files.length<=20000 && manifest.files.reduce((n,f)=>n+f.size_bytes,0)<=MAX_BYTES,'包大小超限');
|
|
173
|
-
|
|
173
|
+
const captured = new Map();
|
|
174
|
+
for(const file of manifest.files){const bytes=readFileSync(safe(root,file.path));ensure(bytes.length===file.size_bytes && hash(bytes)===file.sha256,`文件摘要不一致: ${file.path}`);if(file.original_ref)relative(file.original_ref);captured.set(file.path,bytes);}
|
|
174
175
|
const originals=manifest.files.filter(x=>x.original_ref).map(x=>x.original_ref);
|
|
175
176
|
ensure(new Set(originals.map(x=>x.toLowerCase())).size===originals.length,'源路径重复');
|
|
176
|
-
const source=path.join(temp,'source');mkdirSync(source);materialize(
|
|
177
|
+
const source=path.join(temp,'source');mkdirSync(source);materialize(captured,manifest,source);
|
|
177
178
|
const inspected=await inspectSource(source,manifest.handoff_ref);
|
|
178
179
|
ensure(manifest.bundle_id===inspected.handoff.handoff_id && manifest.version===inspected.handoff.handoff_version,'包身份与源交接不一致');
|
|
179
180
|
ensure(own(read(safe(root,'indexes/rules.json')),inspected.indexes.rules) && own(read(safe(root,'indexes/scenarios.json')),inspected.indexes.scenarios),'规则/场景索引与源资产不一致');
|
|
@@ -6,6 +6,7 @@ import { loadRegistry, ROOT } from "./lifecycle-registry.mjs";
|
|
|
6
6
|
import { loadMaintenanceCheckpoint, validateMaintenanceCheckpoint } from "./maintenance-intensity.mjs";
|
|
7
7
|
import { assertImplementationDecision } from "./user-decision.mjs";
|
|
8
8
|
import { validateNextRoute } from "./lifecycle-transition.mjs";
|
|
9
|
+
import { assertPlanSpecEntry } from "./plan-spec-entry.mjs";
|
|
9
10
|
import { enforceHarnessTaskScope } from "./harness-execution-scope.mjs";
|
|
10
11
|
import { enforceFrontendDelivery } from "./frontend-delivery-boundary.mjs";
|
|
11
12
|
import { LEGACY_TASK_PACKAGE_SCHEMA, TASK_PACKAGE_SCHEMA, validateTaskPackageSchema } from "./task-package-schema.mjs";
|
|
@@ -59,6 +60,7 @@ function validateSkillSource(value, registry) {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function validateCommon(value, registry, lifecycle) {
|
|
63
|
+
if (value.work_unit_id === 'work-unit.spec-synthesis') assertPlanSpecEntry(value);
|
|
62
64
|
const workUnit = lifecycle.work_units.find((item) => item.id === value.work_unit_id);
|
|
63
65
|
if (!workUnit && value.contract.kind !== "slice-implementation") fail(`未知 work_unit_id: ${value.work_unit_id}`);
|
|
64
66
|
const roleDefaults = taskPackageDefaults(value.role_id, registry);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runCommandSync } from "./command-runner.mjs";
|
|
1
2
|
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -7,13 +8,14 @@ import { ROOT } from "./template-verification.mjs";
|
|
|
7
8
|
|
|
8
9
|
function fail(message) { throw new TypeError(message); }
|
|
9
10
|
function ensure(condition, message) { if (!condition) fail(message); }
|
|
10
|
-
function git(args, cwd) {
|
|
11
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
12
|
-
if (result.status !== 0) fail(result.stderr.trim() || `git ${args.join(" ")} 执行失败`);
|
|
11
|
+
function git(args, cwd, timeoutMs = 0, cleanupPathsOnFailure = []) {
|
|
12
|
+
const result = args.includes("fetch") ? runCommandSync("git", args, { cwd, timeoutMs, progress: true, cleanupPathsOnFailure }) : spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
13
|
+
if (result.status !== 0) fail(result.termination || result.error?.message || result.stderr.trim() || `git ${args.join(" ")} 执行失败`);
|
|
13
14
|
return result.stdout.trim();
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function acquireTemplateCommit({ repository, commit, cacheRoot = path.join(ROOT, ".template-source/cache/remote-templates"), allowLocal = false }) {
|
|
17
|
+
export function acquireTemplateCommit({ repository, commit, cacheRoot = path.join(ROOT, ".template-source/cache/remote-templates"), allowLocal = false, timeoutMs = 0 }) {
|
|
18
|
+
ensure(Number.isFinite(timeoutMs) && timeoutMs >= 0, "timeoutMs 必须是非负毫秒数");
|
|
17
19
|
ensure(typeof repository === "string" && repository.trim(), "repository 不能为空");
|
|
18
20
|
ensure(/^[a-f0-9]{40}$/.test(commit), "模板 ref 必须是 40 位 commit");
|
|
19
21
|
if (!allowLocal) ensure(/^(?:https?:\/\/|ssh:\/\/|git@)/.test(repository), "正式模板缓存只接受远程 repository URL");
|
|
@@ -35,7 +37,7 @@ export function acquireTemplateCommit({ repository, commit, cacheRoot = path.joi
|
|
|
35
37
|
try {
|
|
36
38
|
const temporaryRepository = path.join(temporary, "repository.git");
|
|
37
39
|
git(["init", "--bare", "-q", temporaryRepository], ROOT);
|
|
38
|
-
git(["--git-dir", temporaryRepository, "fetch", "--depth=1", "--no-tags", repository, commit], ROOT);
|
|
40
|
+
git(["--git-dir", temporaryRepository, "fetch", "--depth=1", "--no-tags", repository, commit], ROOT, timeoutMs, [temporary]);
|
|
39
41
|
const actual = git(["--git-dir", temporaryRepository, "rev-parse", "FETCH_HEAD^{commit}"], ROOT);
|
|
40
42
|
ensure(actual === commit, `远程返回 commit 与请求不一致: ${actual}`);
|
|
41
43
|
writeFileSync(path.join(temporary, "metadata.json"), `${JSON.stringify({ schema_version: 1, key, repository, commit })}\n`);
|
|
@@ -23,12 +23,18 @@ function collectChangedFiles(base) {
|
|
|
23
23
|
function runCommand(command) {
|
|
24
24
|
return new Promise((resolve) => {
|
|
25
25
|
const started = Date.now();
|
|
26
|
+
process.stderr.write(`[开始] ${command}\n`);
|
|
26
27
|
const child = spawn(command, { cwd: ROOT, shell: true, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
27
28
|
let stdout = "";
|
|
28
29
|
let stderr = "";
|
|
29
30
|
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
30
31
|
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
31
|
-
child.on("
|
|
32
|
+
child.on("error", error => { stderr += `${error.message}\n`; });
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
const duration_ms = Date.now() - started;
|
|
35
|
+
process.stderr.write(`[${code === 0 ? "完成" : "失败"}] ${command} (${duration_ms}ms, exit=${code ?? 1})\n`);
|
|
36
|
+
resolve({ command, code: code ?? 1, duration_ms, stdout, stderr });
|
|
37
|
+
});
|
|
32
38
|
});
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -106,8 +112,8 @@ try {
|
|
|
106
112
|
const concurrency = Number(values.concurrency || plan.max_concurrency);
|
|
107
113
|
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4) throw new TypeError("concurrency 必须为 1..4");
|
|
108
114
|
|
|
109
|
-
process.
|
|
110
|
-
if (plan.escalation_reason) process.
|
|
115
|
+
process.stderr.write(`模板核验 profile: ${plan.requested_profile} -> ${plan.effective_profile}\n`);
|
|
116
|
+
if (plan.escalation_reason) process.stderr.write(`自动升级原因: ${plan.escalation_reason}\n`);
|
|
111
117
|
const groups = await runGroups(plan, modeResult.stdout.trim(), concurrency);
|
|
112
118
|
printResults(groups);
|
|
113
119
|
const failed = groups.flatMap((group) => group.results).find((result) => result.code !== 0);
|
|
@@ -7,10 +7,19 @@ const root=path.resolve(import.meta.dirname,'..');
|
|
|
7
7
|
const check=process.argv.includes('--check');
|
|
8
8
|
const targets=['submodules/yss-harness-design-agent','submodules/yss-harness-dev-agent','submodules/yss-harness-backend-agent','submodules/yss-harness-frontend-agent'];
|
|
9
9
|
const shared=['scripts/strategic-handoff','scripts/verify-strategic-handoff-consumption','scripts/verify-strategic-handoff-package-scenarios','scripts/verify-strategic-context-import','scripts/verify-strategic-context-import-scenarios','scripts/lib/context-contract.mjs','docs/process/strategic-handoff-package.md','docs/process/schemas/strategic-design-handoff.schema.json',...readdirSync(path.join(root,'scripts/lib')).filter(x=>x.startsWith('strategic-handoff-')||x==='strategic-handoff.mjs').map(x=>`scripts/lib/${x}`),...readdirSync(path.join(root,'docs/process/schemas')).filter(x=>x.startsWith('strategic-handoff-')).map(x=>`docs/process/schemas/${x}`),...readdirSync(path.join(root,'scripts/fixtures/strategic-handoff')).map(x=>`scripts/fixtures/strategic-handoff/${x}`)];
|
|
10
|
+
shared.push('scripts/lib/command-runner.mjs','scripts/lib/json-schema.mjs','scripts/lib/context-reconciliation.mjs','scripts/verify-context-reconciliation');
|
|
10
11
|
shared.push('scripts/instantiate-harness','scripts/backend-delivery','scripts/verify-frontend-delivery','scripts/lib/backend-delivery.mjs','scripts/lib/frontend-delivery.mjs','scripts/lib/frontend-delivery-boundary.mjs','docs/process/frontend-backend-delivery.md','docs/process/schemas/backend-delivery.schema.json','docs/process/schemas/backend-delivery-verification.schema.json','docs/process/schemas/frontend-delivery-acceptance.schema.json');
|
|
11
12
|
let differences=[];
|
|
12
13
|
// 专职执行范围由各模板持有;后端只消费 wire 校验器,不恢复原型构建技能。
|
|
13
14
|
function sync(source,dest){let bytes=readFileSync(source);if(dest.includes('/yss-harness-backend-agent/')){let content=bytes.toString();if(dest.endsWith('/scripts/lib/strategic-handoff.mjs'))content=content.replace('../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs','./visual-baseline-contract.mjs');if(dest.endsWith('/scripts/fixtures/strategic-handoff/fixture.mjs'))content=content.replace('../../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs','../../lib/visual-baseline-contract.mjs');bytes=Buffer.from(content);}if(!existsSync(dest)||!bytes.equals(readFileSync(dest))){differences.push(path.relative(root,dest));if(!check){mkdirSync(path.dirname(dest),{recursive:true});writeFileSync(dest,bytes);}}if(!check&&path.basename(path.dirname(dest))==='scripts'&&!path.extname(dest))chmodSync(dest,0o755);}
|
|
15
|
+
// CLI core owns the portable external-command runner; all runtime copies are generated here.
|
|
16
|
+
sync(path.join(root,'.template-source/cli-core/command-runner.mjs'),path.join(root,'scripts/lib/command-runner.mjs'));
|
|
17
|
+
for(const name of ['create-yss-spec','create-yss-strategic-design','create-yss-harness-dev']) {
|
|
18
|
+
const target=`submodules/${name}`;
|
|
19
|
+
if(!existsSync(path.join(root,target,'.git')))continue;
|
|
20
|
+
if(!check){const state=inspectWorkingTreeScope(root,{project_root:target,repository_scope:'git-submodule',checkout_state:'attached-branch'});if(!state.writable)throw new Error(`${target}: ${state.violation}`);}
|
|
21
|
+
sync(path.join(root,'.template-source/cli-core/command-runner.mjs'),path.join(root,target,'src/command-runner.mjs'));
|
|
22
|
+
}
|
|
14
23
|
for(const kind of ['domain-strategy','stage-decision-package'])sync(path.join(root,`.agents/skills/yss-stage-decision/references/${kind}.schema.json`),path.join(root,`docs/process/schemas/strategic-handoff-${kind}.schema.json`));
|
|
15
24
|
for(const target of targets){
|
|
16
25
|
if(!existsSync(path.join(root,target,'.git')))continue;
|