create-yss-spec 3.3.7 → 3.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -13
- package/package.json +1 -1
- package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
- package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +2 -2
- package/template/.agents/skills/yss-application/SKILL.md +1 -1
- package/template/.agents/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.agents/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
- package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
- package/template/.agents/skills/yss-prototype-stage/SKILL.md +10 -1
- package/template/.agents/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.agents/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
- package/template/.agents/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
- package/template/.agents/skills/yss-technical-design/references/technical-design.schema.json +288 -10
- package/template/.agents/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
- package/template/.agents/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
- package/template/.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
- package/template/.agents/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
- package/template/.agents/skills/yss-web-controller/SKILL.md +1 -1
- package/template/.agents/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.agents/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +2 -2
- package/template/.codex/skills/yss-application/SKILL.md +1 -1
- package/template/.codex/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.codex/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
- package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
- package/template/.codex/skills/yss-prototype-stage/SKILL.md +10 -1
- package/template/.codex/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.codex/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
- package/template/.codex/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
- package/template/.codex/skills/yss-technical-design/references/technical-design.schema.json +288 -10
- package/template/.codex/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
- package/template/.codex/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
- package/template/.codex/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
- package/template/.codex/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
- package/template/.codex/skills/yss-web-controller/SKILL.md +1 -1
- package/template/.codex/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.codex/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +2 -2
- package/template/.cursor/skills/yss-application/SKILL.md +1 -1
- package/template/.cursor/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.cursor/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
- package/template/.cursor/skills/yss-prototype-stage/SKILL.md +10 -1
- package/template/.cursor/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.cursor/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
- package/template/.cursor/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
- package/template/.cursor/skills/yss-technical-design/references/technical-design.schema.json +288 -10
- package/template/.cursor/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
- package/template/.cursor/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
- package/template/.cursor/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
- package/template/.cursor/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
- package/template/.cursor/skills/yss-web-controller/SKILL.md +1 -1
- package/template/.cursor/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.cursor/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +2 -2
- package/template/.pi/skills/yss-application/SKILL.md +1 -1
- package/template/.pi/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.pi/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
- package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
- package/template/.pi/skills/yss-prototype-stage/SKILL.md +10 -1
- package/template/.pi/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.pi/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
- package/template/.pi/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
- package/template/.pi/skills/yss-technical-design/references/technical-design.schema.json +288 -10
- package/template/.pi/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
- package/template/.pi/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
- package/template/.pi/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
- package/template/.pi/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
- package/template/.pi/skills/yss-web-controller/SKILL.md +1 -1
- package/template/.pi/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
- package/template/.pi/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
- package/template/README.md +4 -6
- package/template/docs/agents/backend-architecture-profiles.md +4 -0
- package/template/docs/agents/yss-skill-registry.yaml +13 -0
- package/template/docs/design/templates/prototype-confirmation-template.md +2 -2
- package/template/docs/design/templates/prototype-review-checklist.md +2 -2
- package/template/docs/plan/templates/market-analysis-template.md +3 -3
- package/template/docs/plan/templates/user-pain-points-template.md +3 -3
- package/template/docs/process/delivery-preflight.md +72 -0
- package/template/docs/process/document-writing.md +5 -1
- package/template/docs/process/existing-backend-architecture.md +31 -0
- package/template/docs/process/existing-ui-baseline.md +40 -0
- package/template/docs/process/frontend-backend-delivery.md +19 -6
- package/template/docs/process/implementation-repo-integration.md +6 -0
- package/template/docs/process/schemas/backend-architecture-identity.schema.json +111 -14
- package/template/docs/process/schemas/delivery-preflight-input.schema.json +158 -0
- package/template/docs/process/schemas/delivery-preflight-result.schema.json +159 -0
- package/template/docs/process/schemas/digital-human-task-package.schema.json +1 -1
- package/template/docs/process/schemas/existing-ui-baseline.schema.json +365 -0
- package/template/docs/process/schemas/frontend-delivery-acceptance-v3.schema.json +308 -0
- package/template/docs/process/schemas/frontend-strategic-preflight-v2.schema.json +114 -0
- package/template/docs/process/schemas/strategic-design-handoff-v5.schema.json +932 -0
- package/template/docs/process/schemas/strategic-handoff-export-v2.schema.json +378 -0
- package/template/docs/process/strategic-handoff-package.md +21 -13
- package/template/docs/user-guide//346/212/200/346/234/257/350/256/276/350/256/241/347/224/250/346/210/267/346/214/207/345/215/227.md +43 -0
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +83 -67
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +9 -10
- package/template/docs/user-guide//350/256/276/345/244/207/345/200/237/347/224/250/350/264/257/347/251/277/346/241/210/344/276/213.md +12 -9
- package/template/scripts/backend-delivery +20 -4
- package/template/scripts/lib/approved-execution-context.mjs +106 -0
- package/template/scripts/lib/backend-architecture.mjs +12 -0
- package/template/scripts/lib/backend-delivery.mjs +10 -6
- package/template/scripts/lib/delivery-preflight.mjs +238 -0
- package/template/scripts/lib/existing-backend-architecture.mjs +139 -0
- package/template/scripts/lib/existing-ui-baseline.mjs +84 -0
- package/template/scripts/lib/frontend-delivery.mjs +23 -15
- package/template/scripts/lib/implementation-contract-compiler.mjs +19 -5
- package/template/scripts/lib/json-schema.mjs +19 -4
- package/template/scripts/lib/profile-skill-sync.mjs +26 -0
- package/template/scripts/lib/strategic-handoff-consumption.mjs +6 -5
- package/template/scripts/lib/strategic-handoff-design-targets.mjs +2 -3
- package/template/scripts/lib/strategic-handoff-io.mjs +10 -0
- package/template/scripts/lib/strategic-handoff.mjs +47 -29
- package/template/scripts/lib/task-package.mjs +11 -3
- package/template/scripts/lib/technical-design-boundary.mjs +9 -1
- package/template/scripts/lib/ui-baseline.mjs +26 -0
- package/template/scripts/lib/user-decision.mjs +4 -1
- package/template/scripts/preflight-delivery +14 -0
- package/template/scripts/strategic-handoff +19 -4
- package/template/scripts/sync-profile-skills +4 -5
- package/template/skills-lock.json +15 -15
- package/template.snapshot.json +3 -3
- package/template/docs/user-guide//346/210/230/346/234/257/350/256/276/350/256/241/345/255/220/351/241/271/347/233/256/347/224/250/346/210/267/346/211/213/345/206/214.md +0 -35
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createApprovedExecutionContext, createApprovedRecompilationContext, assertApprovedExecutionContext } from './approved-execution-context.mjs';
|
|
1
2
|
import { enforceTechnicalDesign } from './technical-design-boundary.mjs';
|
|
2
3
|
import { enforceHarnessSkillScope } from "./harness-execution-scope.mjs";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -6,7 +7,7 @@ import path from "node:path";
|
|
|
6
7
|
import { parseDocument } from "../vendor/yaml.mjs";
|
|
7
8
|
import { DEFAULT_REGISTRY, loadSkillRegistry } from "./skill-registry.mjs";
|
|
8
9
|
import { ROOT } from "./skill-supply-chain.mjs";
|
|
9
|
-
import { architectureDigest, assertArchitectureAgreement, validateArchitectureIdentity } from "./backend-architecture.mjs";
|
|
10
|
+
import { architectureDigest, assertArchitectureAgreement, validateArchitectureIdentity, verifyArchitectureEvidence } from "./backend-architecture.mjs";
|
|
10
11
|
import { enforceFrontendDelivery } from "./frontend-delivery-boundary.mjs";
|
|
11
12
|
|
|
12
13
|
export const DEFAULT_COMPILER_CONTRACT = path.join(
|
|
@@ -65,12 +66,14 @@ export function compileImplementationContract({
|
|
|
65
66
|
compilerContractDigest,
|
|
66
67
|
architecture_identity,
|
|
67
68
|
architecture_evidence,
|
|
69
|
+
approved_slice,
|
|
68
70
|
root = ROOT,
|
|
69
71
|
slice_id,
|
|
70
72
|
frontend_delivery,
|
|
71
73
|
technical_design
|
|
72
74
|
}) {
|
|
73
75
|
assertV2(registry, compilerContract);
|
|
76
|
+
const recompilationExecution=approved_slice?createApprovedRecompilationContext(approved_slice,{root}):undefined;
|
|
74
77
|
const deliveryInput = enforceFrontendDelivery({ slice_id, frontend_delivery }, { root });
|
|
75
78
|
if (!Array.isArray(recipeIds) || !Array.isArray(requiredCapabilities) || !Array.isArray(conditions)) {
|
|
76
79
|
fail("recipeIds、requiredCapabilities 与 conditions 必须是数组");
|
|
@@ -89,11 +92,11 @@ export function compileImplementationContract({
|
|
|
89
92
|
if (architectural || architecture_identity) {
|
|
90
93
|
architectureProfile = validateArchitectureIdentity(architecture_identity, registry);
|
|
91
94
|
if (!architecture_evidence?.engineering_baseline || !architecture_evidence?.repository_registration || !architecture_evidence?.manifest) fail("缺少工程基线、仓库登记或 Manifest 架构证据");
|
|
92
|
-
|
|
95
|
+
verifyArchitectureEvidence(architecture_identity, architecture_evidence, { root, registry, execution:recompilationExecution });
|
|
93
96
|
for (const recipe of orderedRecipes) if (recipe.architecture_family && recipe.architecture_family !== architecture_identity.architecture_family) fail(`Recipe ${recipe.id} 与架构族不匹配`);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
enforceTechnicalDesign({ technical_design, conditions, architecture_identity, slice_id }, { root });
|
|
99
|
+
enforceTechnicalDesign({ technical_design, conditions, architecture_identity, slice_id }, { root, execution:recompilationExecution });
|
|
97
100
|
|
|
98
101
|
const capabilitySources = new Map();
|
|
99
102
|
const orderedCapabilities = [];
|
|
@@ -175,6 +178,7 @@ export function compileImplementationContract({
|
|
|
175
178
|
...(architecture_identity ? {
|
|
176
179
|
architecture_identity: structuredClone(architecture_identity),
|
|
177
180
|
architecture_identity_digest: architectureDigest(architecture_identity),
|
|
181
|
+
...(architecture_identity.schema_version === 2 ? { architecture_evidence: structuredClone(architecture_evidence) } : {}),
|
|
178
182
|
profile_maturity: architectureProfile.maturity,
|
|
179
183
|
readiness_blockers: architectureProfile.maturity === "supported" ? [] : ["backend-profile-not-supported"],
|
|
180
184
|
skill_profiles: Object.fromEntries(orderedSkills.map((skill) => [skill, architecture_identity.architecture_profile]))
|
|
@@ -202,12 +206,17 @@ export function compileDefaultImplementationContract(input = {}) {
|
|
|
202
206
|
});
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
export function evaluateContractFreshness(contract, { registry, compilerContract, root = ROOT }) {
|
|
209
|
+
export function evaluateContractFreshness(contract, { registry, compilerContract, root = ROOT, approved_slice, readOnly = false }, execution) {
|
|
206
210
|
assertV2(registry, compilerContract);
|
|
207
211
|
if (contract?.schema_version !== 2) fail("Slice Implementation Contract schema v1 已停止支持;必须重新编译 v2 合同");
|
|
208
212
|
const resolution = contract.resolution ?? contract;
|
|
209
213
|
const reasons = [];
|
|
210
|
-
|
|
214
|
+
let trustedExecution;
|
|
215
|
+
try {
|
|
216
|
+
trustedExecution=approved_slice?createApprovedExecutionContext(approved_slice,{root,contract}):execution;
|
|
217
|
+
if(trustedExecution)assertApprovedExecutionContext(trustedExecution,{root,contract});
|
|
218
|
+
} catch(error) { reasons.push(error.code||'EXECUTION_APPROVAL_INVALID');trustedExecution=undefined; }
|
|
219
|
+
try { enforceTechnicalDesign({ ...resolution, slice_id: contract.slice_id ?? resolution.slice_id }, { root,execution:trustedExecution,readOnly }); }
|
|
211
220
|
catch { reasons.push("technical-design-stale-or-unavailable"); }
|
|
212
221
|
try { enforceFrontendDelivery(contract, { root }); }
|
|
213
222
|
catch { reasons.push("frontend-delivery-stale-or-unavailable"); }
|
|
@@ -216,6 +225,10 @@ export function evaluateContractFreshness(contract, { registry, compilerContract
|
|
|
216
225
|
if (resolution.architecture_identity) {
|
|
217
226
|
try { validateArchitectureIdentity(resolution.architecture_identity, registry); }
|
|
218
227
|
catch { reasons.push("architecture-identity-invalid"); }
|
|
228
|
+
if (resolution.architecture_identity.schema_version === 2) {
|
|
229
|
+
try { verifyArchitectureEvidence(resolution.architecture_identity, resolution.architecture_evidence, { root, registry, execution:trustedExecution }); }
|
|
230
|
+
catch (error) { reasons.push(error.code ?? "architecture-evidence-stale-or-unavailable"); }
|
|
231
|
+
}
|
|
219
232
|
if (resolution.architecture_identity_digest !== architectureDigest(resolution.architecture_identity)) reasons.push("architecture-identity-digest-changed");
|
|
220
233
|
}
|
|
221
234
|
return { freshness: reasons.length ? "stale" : "current", reasons };
|
|
@@ -225,6 +238,7 @@ export function validateExecutionResult(result, contract, current) {
|
|
|
225
238
|
if (result?.schema_version !== 2) fail("YSS Skill Execution Result schema v1 已停止支持;请迁移到 v2");
|
|
226
239
|
if (contract?.schema_version !== 2) fail("Slice Implementation Contract schema v1 已停止支持;必须重新编译 v2 合同");
|
|
227
240
|
if (!EXECUTION_STATUSES.has(result.status)) fail(`未知 execution result status: ${result.status}`);
|
|
241
|
+
if(contract.resolution?.architecture_identity?.schema_version===2&&!current?.approved_slice)return {status:'stale',blockers:['EXECUTION_APPROVAL_REQUIRED']};
|
|
228
242
|
const freshness = evaluateContractFreshness(contract, current);
|
|
229
243
|
if (freshness.freshness === "stale") return { status: "stale", blockers: freshness.reasons };
|
|
230
244
|
const consumed = result.consumed_contract ?? {};
|
|
@@ -9,7 +9,15 @@ import traceback
|
|
|
9
9
|
from jsonschema import Draft202012Validator, FormatChecker
|
|
10
10
|
validators = {}
|
|
11
11
|
results = []
|
|
12
|
-
|
|
12
|
+
expected_bytes = int(sys.argv[1])
|
|
13
|
+
payload = sys.stdin.buffer.read(expected_bytes)
|
|
14
|
+
if len(payload) != expected_bytes:
|
|
15
|
+
raise ValueError(f"JSON_SCHEMA_PROTOCOL: truncated input frame: expected {expected_bytes} bytes, received {len(payload)}")
|
|
16
|
+
try:
|
|
17
|
+
jobs = json.loads(payload.decode('utf-8'))
|
|
18
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
19
|
+
raise ValueError(f"JSON_SCHEMA_PROTOCOL: invalid UTF-8 JSON input frame: {error}") from error
|
|
20
|
+
for item in jobs:
|
|
13
21
|
try:
|
|
14
22
|
key = (item['schemaPath'], item['formatChecker'])
|
|
15
23
|
if key not in validators:
|
|
@@ -29,7 +37,8 @@ json.dump(results, sys.stdout)
|
|
|
29
37
|
`;
|
|
30
38
|
|
|
31
39
|
/** One immutable validation phase, never a cache of successful work units. */
|
|
32
|
-
export function validateJsonSchemas(items, { cwd = process.cwd() } = {}) {
|
|
40
|
+
export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30000 } = {}) {
|
|
41
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new TypeError("schema timeoutMs 必须是正整数毫秒数");
|
|
33
42
|
if (!Array.isArray(items)) throw new TypeError("schema batch 必须是数组");
|
|
34
43
|
if (!items.length) return [];
|
|
35
44
|
const jobs = items.map(({ value, schemaPath, formatChecker = true, errorStyle = "compact", label = "JSON Schema" }) => {
|
|
@@ -38,9 +47,15 @@ export function validateJsonSchemas(items, { cwd = process.cwd() } = {}) {
|
|
|
38
47
|
if (!["compact", "verbose"].includes(errorStyle)) throw new TypeError(`未知 schema errorStyle: ${errorStyle}`);
|
|
39
48
|
return { value, schemaPath: effectiveSchema, formatChecker, errorStyle };
|
|
40
49
|
});
|
|
41
|
-
|
|
50
|
+
// readall waits for EOF even after the complete JSON has arrived. A known UTF-8
|
|
51
|
+
// byte frame lets the validator finish when a transport keeps its write end open.
|
|
52
|
+
const input = Buffer.from(JSON.stringify(jobs), "utf8");
|
|
53
|
+
const result = spawnSync("python3", ["-c", validator, String(input.length)], { cwd, encoding: "utf8", input, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
|
|
54
|
+
if (result.error?.code === "ETIMEDOUT") throw new TypeError(`JSON_SCHEMA_TIMEOUT: schema validation exceeded ${timeoutMs}ms`);
|
|
42
55
|
if (result.error || result.status !== 0) throw new TypeError(result.error?.message || `${result.stdout}${result.stderr}`.trim());
|
|
43
|
-
|
|
56
|
+
let results;
|
|
57
|
+
try { results = JSON.parse(result.stdout); } catch { throw new TypeError("JSON_SCHEMA_PROTOCOL: validator returned invalid JSON"); }
|
|
58
|
+
if (!Array.isArray(results) || results.some(item => !item || typeof item.valid !== "boolean" || typeof item.error !== "string")) throw new TypeError("JSON_SCHEMA_PROTOCOL: validator returned invalid result records");
|
|
44
59
|
if (results.length !== jobs.length) throw new TypeError("schema batch 返回数量不匹配");
|
|
45
60
|
return results;
|
|
46
61
|
}
|
|
@@ -334,3 +334,29 @@ export function reportProfileSkillSync(plan) {
|
|
|
334
334
|
for (const item of [...plan.changes, ...plan.issues]) counts[item.status] = (counts[item.status] ?? 0) + 1;
|
|
335
335
|
return { ...plan, changes: plan.changes.map(({ content, ...item }) => item), counts };
|
|
336
336
|
}
|
|
337
|
+
|
|
338
|
+
export function formatProfileSkillSync(report, mode = "--dry-run") {
|
|
339
|
+
const affected = [...new Set([...report.changes, ...report.issues].map(({ profile }) => profile))];
|
|
340
|
+
const lines = [
|
|
341
|
+
`profile skills: ${report.profiles.join(", ")}`,
|
|
342
|
+
`模式:${mode === "--apply" ? "已执行同步" : mode === "--check" ? "只读检查" : "只读预览"}`,
|
|
343
|
+
`受影响 profile:${affected.join(", ") || "无"}`,
|
|
344
|
+
];
|
|
345
|
+
for (const profile of affected) {
|
|
346
|
+
const skills = [...new Set(report.changes.filter((item) => item.profile === profile && item.skill).map(({ skill }) => skill))];
|
|
347
|
+
lines.push(`${profile} 变更技能:${skills.join(", ") || "无(请查看资产差异或问题)"}`);
|
|
348
|
+
}
|
|
349
|
+
lines.push(`已有问题:${report.issues.length} 项(含冲突、缺失依赖与未登记差异)`);
|
|
350
|
+
for (const [status, count] of Object.entries(report.counts)) lines.push(`${status}: ${count}`);
|
|
351
|
+
for (const item of report.changes) lines.push(`${item.profile}: ${item.status}: ${item.path}`);
|
|
352
|
+
if (!affected.length) lines.push("本次未发现同步差异或问题;不代表项目验证已通过。");
|
|
353
|
+
else {
|
|
354
|
+
// Profile names come from configuration; quote them as literal shell arguments.
|
|
355
|
+
const profiles = affected.map((profile) => `--profile '${profile.replaceAll("'", "'\\''")}'`).join(" ");
|
|
356
|
+
lines.push(`后续同步一致性检查(待执行;有差异或冲突时先处理):scripts/sync-profile-skills --check ${profiles}`);
|
|
357
|
+
lines.push("本仓 Skill 治理验证(待执行):scripts/verify-skill-governance");
|
|
358
|
+
}
|
|
359
|
+
lines.push("文件差异、分类与 SHA-256 继续使用现有 --json 报告;本摘要不判断下游项目兼容性。");
|
|
360
|
+
const issues = report.issues.map((item) => [item.profile, item.status, item.skill, item.path, item.message, item.marker === undefined ? undefined : `marker=${item.marker}`].filter((value) => value !== undefined).join(": "));
|
|
361
|
+
return { stdout: `${lines.join("\n")}\n`, stderr: issues.length ? `${issues.join("\n")}\n` : "" };
|
|
362
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { uiBaselineSourceKeys, uiBaselineKind } from './ui-baseline.mjs';
|
|
1
2
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
@@ -12,7 +13,7 @@ const strategyBasis=value=>({
|
|
|
12
13
|
...without(value,['domain_version','approval','rule_catalog','scenarios','invariants']),
|
|
13
14
|
invariants:(value.invariants||[]).map(item=>without(item,['statement'])),
|
|
14
15
|
});
|
|
15
|
-
export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consumer='tactical'}={}) {
|
|
16
|
+
export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consumer='tactical',readOnly=false}={}) {
|
|
16
17
|
project(root);
|
|
17
18
|
ensure(['tactical','frontend'].includes(consumer),'未知战略消费者');
|
|
18
19
|
if (data.schema_version === 2 && consumer === 'tactical') ensure(['domain-driven','layered-mvc'].includes(data.architecture?.family), '未知技术设计架构');
|
|
@@ -74,15 +75,15 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
|
|
|
74
75
|
}else {ensure(['pending','conflict'].includes(row.disposition),`非法承接状态: ${id}`);mark(id,row.disposition,row);}
|
|
75
76
|
}
|
|
76
77
|
for(const[id,row]of rows)if(!known.has(id))mark(id,'removed-or-unknown-source',row);
|
|
77
|
-
for(const key of ['spec_ref','
|
|
78
|
+
for(const key of new Set(['spec_ref','business_ticket_set_ref',...uiBaselineSourceKeys(original.handoff),...uiBaselineSourceKeys(current.handoff)]))if(original.handoff.source[key]?.digest!==current.handoff.source[key]?.digest){all=true;issues.push({source_id:key,reason:'upstream-asset-stale'});}
|
|
78
79
|
// Rule changes can be scoped to declared dependencies. Boundary/decision or
|
|
79
80
|
// runnable-prototype changes need the tactical contract rebound as a whole.
|
|
80
81
|
const stageKeys=['package_version','approval','domain_strategy_ref'];
|
|
81
|
-
if(digest(strategyBasis(original.strategy))!==digest(strategyBasis(current.strategy)) || digest(without(original.stage,stageKeys))!==digest(without(current.stage,stageKeys)) || digest(original.config.prototype)!==digest(current.config.prototype)) {
|
|
82
|
+
if(digest(strategyBasis(original.strategy))!==digest(strategyBasis(current.strategy)) || digest(without(original.stage,stageKeys))!==digest(without(current.stage,stageKeys)) || uiBaselineKind(original.handoff)!==uiBaselineKind(current.handoff) || digest(original.config.prototype||null)!==digest(current.config.prototype||null)) {
|
|
82
83
|
all=true;issues.push({source_id:'strategic-design-basis',reason:'upstream-design-stale'});
|
|
83
84
|
}
|
|
84
85
|
const blockedHere=all||(sliceRef?blocked.has(sliceRef):issues.length>0);
|
|
85
86
|
return {result:blockedHere?'blocked':'verified',bundle_digest:current.manifest.bundle_digest,consumed_bundle_digest:receipt.bundle_digest,...(consumer==='frontend'?{frontend_acceptance_digest:digest(data)}:{tactical_digest:digest(data), ...(data.schema_version === 2 ? {technical_design_digest:digest(data)} : {})}),scope:sliceRef||`whole-${consumer}-design`,coverage_complete:[...known.keys()].every(id=>rows.has(id)),block_all:all,blocked_slice_refs:[...blocked].sort(),issues};
|
|
86
|
-
});
|
|
87
|
-
});
|
|
87
|
+
},{readOnly});
|
|
88
|
+
},{readOnly});
|
|
88
89
|
}
|
|
@@ -2,14 +2,13 @@ import { ensure } from './strategic-handoff-io.mjs';
|
|
|
2
2
|
|
|
3
3
|
export function designTargets(data) {
|
|
4
4
|
const design = data.schema_version === 2 ? data.design : data;
|
|
5
|
-
const catalogs = data.schema_version === 2 && data.architecture?.family === 'layered-mvc'
|
|
5
|
+
const catalogs = data.schema_version === 2 && data.design_scope === 'engineering-only' ? ['component_catalog'] : data.schema_version === 2 && data.architecture?.family === 'layered-mvc'
|
|
6
6
|
? ['module_catalog', 'use_case_catalog', 'rule_catalog', 'state_transition_catalog', 'persistence_mapping', 'integration_catalog']
|
|
7
7
|
: ['aggregate_catalog', 'entity_catalog', 'value_object_catalog', 'behavior_catalog', 'invariant_catalog', 'state_transition_catalog', 'domain_event_catalog', 'gateway_catalog'];
|
|
8
|
-
const primaryKeys = { module_catalog: 'module_id', use_case_catalog: 'use_case_id', rule_catalog: 'rule_id', state_transition_catalog: 'transition_id', persistence_mapping: 'mapping_id', integration_catalog: 'integration_id', aggregate_catalog: 'aggregate_id', entity_catalog: 'entity_id', value_object_catalog: 'value_object_id', behavior_catalog: 'behavior_id', invariant_catalog: 'invariant_id', domain_event_catalog: 'event_id', gateway_catalog: 'gateway_id' };
|
|
8
|
+
const primaryKeys = { component_catalog: 'component_id', module_catalog: 'module_id', use_case_catalog: 'use_case_id', rule_catalog: 'rule_id', state_transition_catalog: 'transition_id', persistence_mapping: 'mapping_id', integration_catalog: 'integration_id', aggregate_catalog: 'aggregate_id', entity_catalog: 'entity_id', value_object_catalog: 'value_object_id', behavior_catalog: 'behavior_id', invariant_catalog: 'invariant_id', domain_event_catalog: 'event_id', gateway_catalog: 'gateway_id' };
|
|
9
9
|
const ids = catalogs.flatMap(key => (design?.[key] || []).map(item => item[primaryKeys[key]]).filter(Boolean));
|
|
10
10
|
ensure(new Set(ids).size === ids.length, '设计对象 ID 重复');
|
|
11
11
|
const seams = design?.test_seams || [];
|
|
12
12
|
ensure(new Set(seams.map(item => item.seam_id)).size === seams.length, '测试 seam ID 重复');
|
|
13
13
|
return { ids: new Set(ids), seams: new Map(seams.map(item => [item.seam_id, item])) };
|
|
14
14
|
}
|
|
15
|
-
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, lstatSync, readdirSync, readFileSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { validateJsonSchemas } from './json-schema.mjs';
|
|
7
8
|
import { parseDocument } from '../vendor/yaml.mjs';
|
|
8
9
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
10
|
+
// A v5 package keeps the source glossary as a snapshot, never a nested CONTEXT.md.
|
|
11
|
+
// Only this fixed alias is supported, isolated to the current asynchronous reader.
|
|
12
|
+
const sourceContextSnapshots = new AsyncLocalStorage();
|
|
13
|
+
export function withSourceContextSnapshot(root, action) {
|
|
14
|
+
const roots = new Set(sourceContextSnapshots.getStore() || []);
|
|
15
|
+
roots.add(path.resolve(root));
|
|
16
|
+
return sourceContextSnapshots.run(roots, action);
|
|
17
|
+
}
|
|
9
18
|
export const ensure = (condition, message) => { if (!condition) throw new TypeError(message); };
|
|
10
19
|
export const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k])])) : value;
|
|
11
20
|
export const hash = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
@@ -23,6 +32,7 @@ export function relative(ref) {
|
|
|
23
32
|
}
|
|
24
33
|
export function safe(root, ref, { missing = false } = {}) {
|
|
25
34
|
relative(ref);
|
|
35
|
+
if (ref === 'CONTEXT.md' && sourceContextSnapshots.getStore()?.has(path.resolve(root))) ref = 'source-context.snapshot.md';
|
|
26
36
|
let current = path.resolve(root);
|
|
27
37
|
ensure(!existsSync(current) || !lstatSync(current).isSymbolicLink(), `根目录不能是 symlink: ${root}`);
|
|
28
38
|
for (const part of ref.split('/')) {
|
|
@@ -1,21 +1,22 @@
|
|
|
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, schemaBatch, 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, withSourceContextSnapshot } 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';
|
|
8
8
|
import { treeDigest } from './strategic-handoff-io.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { uiBaselineKind, uiBaselineRef, uiBaselineCaseIds, hasConsumerRoutes, validateHandoffUiBaseline } from './ui-baseline.mjs';
|
|
10
10
|
import { extractTraceability, compareIndexes } from './strategic-handoff-rules.mjs';
|
|
11
11
|
const MAX_BYTES = 512 * 1024 * 1024;
|
|
12
12
|
const own = (a,b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
13
13
|
const nonempty = x => typeof x === 'string' && x.trim();
|
|
14
14
|
const bytesDigest = (bytes, kind) => kind === 'canonical-json' ? digest(parse(bytes)) : kind === 'sha256-bytes' ? hash(bytes) : (() => { throw new TypeError(`未知摘要算法: ${kind}`); })();
|
|
15
|
-
const HANDOFF_SCHEMAS = new Map([[3,'docs/process/schemas/strategic-design-handoff-v3.schema.json'],[4,'docs/process/schemas/strategic-design-handoff-v4.schema.json']]);
|
|
15
|
+
const HANDOFF_SCHEMAS = new Map([[3,'docs/process/schemas/strategic-design-handoff-v3.schema.json'],[4,'docs/process/schemas/strategic-design-handoff-v4.schema.json'],[5,'docs/process/schemas/strategic-design-handoff-v5.schema.json']]);
|
|
16
16
|
const SOURCE_SCHEMAS = new Map([
|
|
17
17
|
[3,['docs/process/schemas/strategic-handoff-domain-strategy.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']],
|
|
18
18
|
[4,['docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json']],
|
|
19
|
+
[5,['docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json']],
|
|
19
20
|
]);
|
|
20
21
|
const CAPABILITIES=['backend-technical-design','frontend-engineering-design','delivery-coordination'];
|
|
21
22
|
|
|
@@ -25,7 +26,7 @@ function versionSchema(version, schemas, label) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function validateConsumerRoutes(handoff, stage) {
|
|
28
|
-
if(handoff
|
|
29
|
+
if(!hasConsumerRoutes(handoff))return;
|
|
29
30
|
const routes=new Map();
|
|
30
31
|
for(const route of handoff.consumer_routes||[]){
|
|
31
32
|
ensure(!routes.has(route.capability),`消费者能力重复: ${route.capability}`);
|
|
@@ -128,7 +129,8 @@ export async function inspectSource(root, handoffRef) {
|
|
|
128
129
|
schema(handoff,versionSchema(handoff?.schema_version,HANDOFF_SCHEMAS,'Strategic Handoff'));
|
|
129
130
|
ensure(handoff.status==='approved', '交接包尚未批准');
|
|
130
131
|
const config=handoff.package_export;
|
|
131
|
-
schema(config,'docs/process/schemas/strategic-handoff-export.schema.json');
|
|
132
|
+
schema(config,handoff.schema_version===5?'docs/process/schemas/strategic-handoff-export-v2.schema.json':'docs/process/schemas/strategic-handoff-export.schema.json');
|
|
133
|
+
if(handoff.schema_version===5)ensure(config.ui_baseline_kind===uiBaselineKind(handoff),'交接与导出 UI 基线类型不一致');
|
|
132
134
|
const source=parseContextSource(readFileSync(safe(root,'CONTEXT.md'),'utf8'));
|
|
133
135
|
snapshot(handoff.source_context_snapshot,source); checkDelta(handoff,source);
|
|
134
136
|
const strategy=read(safe(root,handoff.source.domain_strategy_ref.persisted_ref));
|
|
@@ -142,6 +144,7 @@ export async function inspectSource(root, handoffRef) {
|
|
|
142
144
|
ensure(stage.domain_strategy_ref.domain_strategy_id===strategy.domain_strategy_id && stage.domain_strategy_ref.domain_version===strategy.domain_version && stage.domain_strategy_ref.status==='approved' && stage.domain_strategy_ref.persisted_ref===handoff.source.domain_strategy_ref.persisted_ref && stage.domain_strategy_ref.digest===digest(strategy), '阶段包引用的战略内容不一致');
|
|
143
145
|
ensure(!(stage.unresolved_items || []).some(x=>x.type==='blocker'), '阶段包含 blocker');
|
|
144
146
|
validateConsumerRoutes(handoff,stage);
|
|
147
|
+
if(uiBaselineKind(handoff)==='existing-ui-baseline')ensure(stage.impact_assessment?.ui===false,'existing-ui-baseline 不支持 UI 影响,返回产品设计');
|
|
145
148
|
const indexes=extractTraceability(strategy);
|
|
146
149
|
const sourceRoles=read(safe(root,'docs/agents/digital-human-roles.yaml'));
|
|
147
150
|
const roles=sourceApprovalPolicy(sourceRoles);
|
|
@@ -152,22 +155,24 @@ export async function inspectSource(root, handoffRef) {
|
|
|
152
155
|
const currentPlan = (roles.gate_policy.dual_digital_human || []).some(rule => rule.gate === 'gate.plan-approved');
|
|
153
156
|
const terminalGate=currentPlan?'gate.plan-approved':countersignRuleForGate(roles.gate_policy,'gate.strategic-design-handoff-approved')?'gate.strategic-design-handoff-approved':'gate.stage-decision-package-approved';
|
|
154
157
|
const expectedGates={domain_strategy_ref:currentPlan?'gate.plan-approved':'gate.domain-strategy-approved',stage_decision_package_ref:currentPlan?'gate.plan-approved':'gate.stage-decision-package-approved',spec_ref:'gate.spec-baseline-approved',prototype_ref:currentPlan?'gate.product-design-approved':'gate.user-confirmation',visual_baseline_ref:currentPlan?'gate.product-design-approved':'gate.user-confirmation',business_ticket_set_ref:terminalGate,handoff:terminalGate};
|
|
158
|
+
expectedGates.existing_ui_baseline_ref=expectedGates.prototype_ref;
|
|
155
159
|
const bindings={...handoff.source,handoff:{id:handoff.handoff_id,version:handoff.handoff_version,persisted_ref:handoffRef,status:'approved'}};
|
|
156
160
|
for(const [key,ref] of Object.entries(bindings)) {
|
|
157
161
|
const approval=config.approvals[key];
|
|
158
162
|
ensure(approval,`缺少资产批准绑定: ${key}`);
|
|
159
163
|
ensure(approval.gate_id===expectedGates[key],`资产使用错误批准门禁: ${key}`);
|
|
160
|
-
const
|
|
164
|
+
const baselineKey=['visual_baseline_ref','existing_ui_baseline_ref'].includes(key);
|
|
161
165
|
let actual;
|
|
162
|
-
if(
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} else actual=bytesDigest(
|
|
166
|
+
if(baselineKey) {
|
|
167
|
+
const validated=await validateHandoffUiBaseline(root,handoff);actual=validated.digest;
|
|
168
|
+
if(key==='existing_ui_baseline_ref') {
|
|
169
|
+
ensure(approval.digest_kind==='sha256-bytes','既有 UI 批准必须绑定 manifest 原字节');
|
|
170
|
+
ensure(roles.user_decision_policy.gates.includes(expectedGates.existing_ui_baseline_ref),'既有 UI 承接必须使用产品设计真实用户决定策略');
|
|
171
|
+
}
|
|
172
|
+
} else actual=bytesDigest(readFileSync(safe(root,ref.persisted_ref)),approval.digest_kind);
|
|
169
173
|
if(key!=='handoff')ensure(ref.digest===actual,`源资产摘要过期: ${key}`);
|
|
170
174
|
const record=read(safe(root,approval.record_ref));
|
|
175
|
+
if(key==='existing_ui_baseline_ref')ensure(record.subject_ref===`${ref.persisted_ref}/${ref.manifest_ref}`,'既有 UI 用户决定必须以当前 manifest 为批准主体');
|
|
171
176
|
await sourceApproval(record,roles,root);
|
|
172
177
|
ensure(record.gate_id===approval.gate_id,`批准门禁不匹配: ${key}`);
|
|
173
178
|
ensure((record.artifact_bindings || []).some(x=>x.id===(ref.id||ref.baseline_id) && x.version===ref.version && x.digest===actual),`批准记录未绑定当前资产: ${key}`);
|
|
@@ -180,19 +185,22 @@ export async function inspectSource(root, handoffRef) {
|
|
|
180
185
|
ensure(scope.delivery_ref && own(read(safe(root,scope.delivery_ref)),delivery), '交付范围清单未覆盖当前交接内容或风险');
|
|
181
186
|
ensure(scope.assets?.some(asset=>asset.ref===scope.delivery_ref && asset.digest===hash(readFileSync(safe(root,scope.delivery_ref)))), '交接内容快照缺少批准覆盖');
|
|
182
187
|
for (const [key,ref] of Object.entries(handoff.source)) {
|
|
183
|
-
const file=
|
|
188
|
+
const file=['visual_baseline_ref','existing_ui_baseline_ref'].includes(key)?`${ref.persisted_ref}/${ref.manifest_ref}`:ref.persisted_ref;
|
|
184
189
|
const decisionBoundaries=key==='business_ticket_set_ref'?['gate.spec-baseline-approved',terminalGate]:[expectedGates[key]];
|
|
185
190
|
ensure(scope.assets?.some(asset=>asset.ref===file && asset.version===ref.version && decisionBoundaries.includes(asset.boundary) && asset.digest===hash(readFileSync(safe(root,file)))), `交付范围清单遗漏当前资产: ${key}`);
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
if(uiBaselineKind(handoff)==='prototype') {
|
|
194
|
+
const receipt=previewFiles(root,config.prototype);
|
|
195
|
+
ensure(uiBaselineCaseIds(handoff).every(id=>receipt.case_ids.includes(id)), '离线验证未覆盖视觉基线 case');
|
|
196
|
+
}
|
|
190
197
|
return {handoff,config,indexes,strategy,stage};
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
function collect(root, handoffRef, handoff, config) {
|
|
194
|
-
const collected=new Map(), queue=[handoffRef,'CONTEXT.md','docs/agents/digital-human-roles.yaml',...Object.values(handoff.source).map(x=>x.persisted_ref),...Object.values(config.approvals).map(x=>x.record_ref),...handoff.evidence_and_version_digests,...config.additional_files
|
|
195
|
-
if(config.prototype
|
|
201
|
+
const collected=new Map(), queue=[handoffRef,'CONTEXT.md','docs/agents/digital-human-roles.yaml',...Object.values(handoff.source).map(x=>x.persisted_ref),...Object.values(config.approvals).map(x=>x.record_ref),...handoff.evidence_and_version_digests,...config.additional_files];
|
|
202
|
+
if(config.prototype)queue.push(config.prototype.preview_root,config.prototype.verification_ref);
|
|
203
|
+
if(config.prototype?.profile==='H2')queue.push(config.prototype.source_root,config.prototype.lock_ref);
|
|
196
204
|
const symbolic=config.reference_map || {};
|
|
197
205
|
const enqueue=ref=> { if(symbolic[ref])queue.push(symbolic[ref]); else if(/^https?:\/\//i.test(ref))return; else if(ref.startsWith('evidence.'))throw new TypeError(`未解析 evidence ID: ${ref}`); else if(ref.includes('/') || /\.(?:md|yaml|json|html|png|txt|log)$/.test(ref))queue.push(ref); };
|
|
198
206
|
function refs(value,key='') {
|
|
@@ -208,20 +216,23 @@ function collect(root, handoffRef, handoff, config) {
|
|
|
208
216
|
ensure(stat.isFile() && stat.size<=MAX_BYTES,`源文件类型/大小无效: ${ref}`);
|
|
209
217
|
const bytes=readFileSync(full); collected.set(ref,bytes); totalBytes += bytes.length;
|
|
210
218
|
ensure(collected.size<=20000 && totalBytes<=MAX_BYTES,'交接包大小超限');
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
const localBaseline=uiBaselineKind(handoff)==='existing-ui-baseline'&&ref.startsWith(`${uiBaselineRef(handoff).persisted_ref}/`);
|
|
220
|
+
if(!localBaseline&&/\.(yaml|yml|json)$/.test(ref))refs(parse(bytes));
|
|
221
|
+
// The entire existing UI observation directory was validated as a byte-bound file set.
|
|
222
|
+
// Its copied source documentation is evidence, not governance dependency instructions.
|
|
223
|
+
if(!localBaseline)queue.push(...documentLinks(ref,bytes));
|
|
213
224
|
}
|
|
214
225
|
return collected;
|
|
215
226
|
}
|
|
216
227
|
function materialize(captured, manifest, destination) {
|
|
217
228
|
for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,captured.get(file.path));
|
|
218
229
|
}
|
|
219
|
-
export async function openBundle(input, action) {
|
|
220
|
-
const temp=mkdtempSync(path.join(tmpdir(),'yss-handoff-'));
|
|
230
|
+
export async function openBundle(input, action, {readOnly=false}={}) {
|
|
231
|
+
const temp=readOnly?null:mkdtempSync(path.join(tmpdir(),'yss-handoff-'));
|
|
221
232
|
try {
|
|
222
233
|
ensure(existsSync(input) && !lstatSync(input).isSymbolicLink(),'交接包不存在或是 symlink');
|
|
223
234
|
let root=path.resolve(input);
|
|
224
|
-
if(lstatSync(root).isFile()){ root=path.join(temp,'unpacked');mkdirSync(root);archive('unpack',path.resolve(input),root); }
|
|
235
|
+
if(lstatSync(root).isFile()){ ensure(!readOnly,'readonly-extraction-required: 只读验证需要已展开目录');root=path.join(temp,'unpacked');mkdirSync(root);archive('unpack',path.resolve(input),root); }
|
|
225
236
|
const manifest=read(safe(root,'manifest.json'));
|
|
226
237
|
schema(manifest,'docs/process/schemas/strategic-handoff-package.schema.json');
|
|
227
238
|
const {bundle_digest,...body}=manifest;ensure(bundle_digest===digest(body),'包清单摘要不一致');
|
|
@@ -233,7 +244,12 @@ export async function openBundle(input, action) {
|
|
|
233
244
|
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);}
|
|
234
245
|
const originals=manifest.files.filter(x=>x.original_ref).map(x=>x.original_ref);
|
|
235
246
|
ensure(new Set(originals.map(x=>x.toLowerCase())).size===originals.length,'源路径重复');
|
|
236
|
-
|
|
247
|
+
let source;
|
|
248
|
+
if(readOnly){
|
|
249
|
+
ensure(manifest.files.filter(file=>file.original_ref).every(file=>file.path===`payload/files/${file.original_ref==='CONTEXT.md'?'source-context.snapshot.md':file.original_ref}`),'readonly-source-layout-required: 原始源布局需要只读文件映射支持');
|
|
250
|
+
source=safe(root,'payload/files');
|
|
251
|
+
}else{source=path.join(temp,'source');mkdirSync(source);materialize(captured,manifest,source);}
|
|
252
|
+
const inspect=async()=>{
|
|
237
253
|
const inspected=await inspectSource(source,manifest.handoff_ref);
|
|
238
254
|
ensure(manifest.bundle_id===inspected.handoff.handoff_id && manifest.version===inspected.handoff.handoff_version,'包身份与源交接不一致');
|
|
239
255
|
ensure(own(read(safe(root,'indexes/rules.json')),inspected.indexes.rules) && own(read(safe(root,'indexes/scenarios.json')),inspected.indexes.scenarios),'规则/场景索引与源资产不一致');
|
|
@@ -250,14 +266,16 @@ export async function openBundle(input, action) {
|
|
|
250
266
|
}
|
|
251
267
|
ensure(own(changes,{previous_bundle:manifest.previous_bundle,...compareIndexes(inspected.indexes,previousIndexes)}),'版本差异与实际索引不一致');
|
|
252
268
|
return await action({root,manifest,...inspected,changes});
|
|
253
|
-
|
|
269
|
+
};
|
|
270
|
+
return readOnly?await withSourceContextSnapshot(source,inspect):await inspect();
|
|
271
|
+
} finally {if(temp)rmSync(temp,{recursive:true,force:true});}
|
|
254
272
|
}
|
|
255
273
|
export async function exportBundle({sourceRoot,handoffRef,output,zip=false,previous}) {
|
|
256
274
|
const root=project(sourceRoot); parseContextContract({root}); relative(handoffRef);
|
|
257
275
|
const current=await inspectSource(root,handoffRef);
|
|
258
276
|
let prior=null;
|
|
259
277
|
if(previous)prior=await openBundle(previous, b=>({manifest:b.manifest,indexes:b.indexes,handoff:b.handoff,rulesBytes:readFileSync(safe(b.root,'indexes/rules.json')),scenariosBytes:readFileSync(safe(b.root,'indexes/scenarios.json'))}));
|
|
260
|
-
if(prior){ensure(prior.manifest.bundle_id===current.handoff.handoff_id,'前版属于不同交接');ensure(Number(current.handoff.handoff_version.slice(1))>Number(prior.manifest.version.slice(1)),'更新必须提升交接版本');for(const[key,ref]of Object.entries(current.handoff.source)){const old=prior.handoff.source[key];if(old.digest!==ref.digest)ensure(old.version!==ref.version,`源资产内容变化必须提升版本: ${key}`);}}
|
|
278
|
+
if(prior){ensure(prior.manifest.bundle_id===current.handoff.handoff_id,'前版属于不同交接');ensure(Number(current.handoff.handoff_version.slice(1))>Number(prior.manifest.version.slice(1)),'更新必须提升交接版本');for(const[key,ref]of Object.entries(current.handoff.source)){const old=prior.handoff.source[key];if(old&&old.digest!==ref.digest)ensure(old.version!==ref.version,`源资产内容变化必须提升版本: ${key}`);}}
|
|
261
279
|
const previousBundle=prior?{bundle_id:prior.manifest.bundle_id,version:prior.manifest.version,digest:prior.manifest.bundle_digest}:null;
|
|
262
280
|
const snapshotFiles=collect(root,handoffRef,current.handoff,current.config);
|
|
263
281
|
const target=path.resolve(output);
|
|
@@ -270,7 +288,7 @@ export async function exportBundle({sourceRoot,handoffRef,output,zip=false,previ
|
|
|
270
288
|
const entries=[];
|
|
271
289
|
const put=(ref,bytes,original)=>{write(staging,ref,bytes);entries.push({path:ref,sha256:hash(bytes),size_bytes:bytes.length,...(original?{original_ref:original}:{})});};
|
|
272
290
|
for(const [ref,bytes]of [...snapshotFiles].sort(([a],[b])=>a.localeCompare(b))) {
|
|
273
|
-
const dest=ref===handoffRef?'handoff.yaml':ref==='CONTEXT.md'?'payload/source-context.snapshot.md':`payload/files/${ref}`;
|
|
291
|
+
const dest=current.handoff.schema_version===5?`payload/files/${ref==='CONTEXT.md'?'source-context.snapshot.md':ref}`:ref===handoffRef?'handoff.yaml':ref==='CONTEXT.md'?'payload/source-context.snapshot.md':`payload/files/${ref}`;
|
|
274
292
|
ensure(!dest.endsWith('/CONTEXT.md'),'只允许源根词汇快照,禁止嵌套 CONTEXT.md');put(dest,bytes,ref);
|
|
275
293
|
}
|
|
276
294
|
put('indexes/rules.json',Buffer.from(json(current.indexes.rules)));
|
|
@@ -339,8 +357,8 @@ export async function importBundle({bundle,targetRoot}) {
|
|
|
339
357
|
const coordinationRoute=b.handoff.consumer_routes.find(route=>route.capability==='delivery-coordination');
|
|
340
358
|
if(capabilities.includes('backend-technical-design')&&backendRoute.activation!=='not-applicable')write(stage,'backend-technical-traceability-draft.json',json({schema_version:2,route_id:backendRoute.route_id,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows}));
|
|
341
359
|
if(capabilities.includes('frontend-engineering-design')&&frontendRoute.activation!=='not-applicable'){
|
|
342
|
-
const preflight={schema_version:1,status:'draft',route_id:frontendRoute.route_id,capability:frontendRoute.capability,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,context_reconciliation_ref:'',
|
|
343
|
-
schema(preflight,'docs/process/schemas/frontend-strategic-preflight.schema.json');
|
|
360
|
+
const preflight={schema_version:b.handoff.schema_version===5?2:1,status:'draft',route_id:frontendRoute.route_id,capability:frontendRoute.capability,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,context_reconciliation_ref:'',...(b.handoff.schema_version===5?{ui_baseline_kind:uiBaselineKind(b.handoff),ui_baseline_ref:`${rel}/package/payload/files/${uiBaselineRef(b.handoff).persisted_ref}/${uiBaselineRef(b.handoff).manifest_ref}`}:{visual_baseline_ref:`${rel}/package/payload/files/${uiBaselineRef(b.handoff).persisted_ref}/${uiBaselineRef(b.handoff).manifest_ref}`}),source_rule_refs:rows.map(row=>row.source_id),backend_dependency:{mode:backendRoute.activation==='not-applicable'?'not-applicable':'required',route_id:backendRoute.route_id,...(backendRoute.activation==='not-applicable'?{reason:backendRoute.reason,impact_refs:backendRoute.impact_refs,evidence_refs:backendRoute.evidence_refs}:{})},ready_for_agent:false};
|
|
361
|
+
schema(preflight,b.handoff.schema_version===5?'docs/process/schemas/frontend-strategic-preflight-v2.schema.json':'docs/process/schemas/frontend-strategic-preflight.schema.json');
|
|
344
362
|
write(stage,'frontend-strategic-preflight-draft.json',json(preflight));write(stage,'frontend-traceability-draft.json',json({schema_version:1,route_id:frontendRoute.route_id,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows:rows.map(({tactical_refs,test_seam_refs,...row})=>({...row,frontend_case_refs:[]}))}));
|
|
345
363
|
}
|
|
346
364
|
if(capabilities.includes('delivery-coordination')&&coordinationRoute.activation!=='not-applicable')write(stage,'consumer-status-index-draft.json',json({schema_version:1,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,routes:b.handoff.consumer_routes.map(route=>({route_id:route.route_id,capability:route.capability,activation:route.activation,consumer_status:route.activation==='not-applicable'?'not-applicable':'pending',receipt_refs:[],acceptance_refs:[],feedback_refs:[]}))}));
|
|
@@ -179,13 +179,21 @@ function validateContract(value, registry, lifecycle) {
|
|
|
179
179
|
if (!existsSync(contractPath)) fail(`Slice Implementation Contract 不存在: ${contract.slice_contract_ref}`);
|
|
180
180
|
const document = parseYaml(readFileSync(contractPath, "utf8"), "Slice Implementation Contract");
|
|
181
181
|
const slice = document.slice_contract || document;
|
|
182
|
-
if (slice.schema_version !==
|
|
182
|
+
if (slice.schema_version !== 2 || typeof slice.slice_id !== "string" || !slice.lifecycle_refs || !slice.readiness || !Array.isArray(slice.work_units)) fail("Slice Implementation Contract 需要 schema v2 及 slice_id/lifecycle_refs/readiness/work_units");
|
|
183
183
|
if (slice.contract_id !== contract.contract_id || slice.contract_version !== contract.contract_version || slice.status !== "approved") fail("Slice Contract 必须是当前 approved 版本");
|
|
184
184
|
const sliceUnit = slice.work_units.find((item) => item && item.id === value.work_unit_id);
|
|
185
185
|
if (!sliceUnit) fail(`Slice Implementation Contract 缺少 work_unit_id: ${value.work_unit_id}`);
|
|
186
186
|
for (const field of ["role_id", "runtime_id", "task_package_ref", "contract_id", "contract_version", "allowed_write_paths"]) if (sliceUnit[field] === undefined) fail(`Slice Contract work_unit 缺少 ${field}`);
|
|
187
|
-
if (sliceUnit.role_id !== value.role_id || sliceUnit.runtime_id !== value.runtime_id || sliceUnit.contract_id !== contract.contract_id || sliceUnit.contract_version !== contract.contract_version
|
|
187
|
+
if (sliceUnit.role_id !== value.role_id || sliceUnit.runtime_id !== value.runtime_id || sliceUnit.contract_id !== contract.contract_id || sliceUnit.contract_version !== contract.contract_version) fail("任务包与 Slice Contract work_unit 不一致");
|
|
188
188
|
for (const allowed of value.allowed_write_paths) if (!sliceUnit.allowed_write_paths.includes(allowed)) fail(`allowed_write_paths 未获 Slice Contract 授权: ${allowed}`);
|
|
189
|
+
// Older packages used the work-unit ID here; new packages bind a readable dispatch file.
|
|
190
|
+
if (sliceUnit.task_package_ref !== value.work_unit_id) {
|
|
191
|
+
const dispatchPath = assertSafeRelativePath(sliceUnit.task_package_ref, "Slice work_unit.task_package_ref");
|
|
192
|
+
if (!existsSync(dispatchPath)) fail("Slice task_package_ref 不可读");
|
|
193
|
+
const dispatched = parseDocument(readFileSync(dispatchPath, "utf8")).toJS();
|
|
194
|
+
for (const field of ["task_id", "work_unit_id", "actor_id", "role_id", "runtime_id"]) if (dispatched?.[field] !== value[field]) fail("Slice task_package_ref 与当前派发身份不一致");
|
|
195
|
+
if (JSON.stringify(dispatched.contract) !== JSON.stringify(value.contract) || JSON.stringify(dispatched.allowed_write_paths) !== JSON.stringify(value.allowed_write_paths)) fail("Slice task_package_ref 与当前合同或写范围不一致");
|
|
196
|
+
}
|
|
189
197
|
if (value.execution_state === "Worker") assertImplementationDecision({ slice_contract_ref: contract.slice_contract_ref, vertical_slice_ticket_ref: slice.lifecycle_refs.ticket, user_decisions: value.user_decisions });
|
|
190
198
|
}
|
|
191
199
|
|
|
@@ -223,7 +231,7 @@ export function validateTaskPackageSet(packages, options = {}) {
|
|
|
223
231
|
packages.forEach((pkg) => validateTaskPackage(pkg, options));
|
|
224
232
|
const slicePackages = packages.filter((pkg) => pkg.contract.kind === "slice-implementation");
|
|
225
233
|
if (slicePackages.length > 0) {
|
|
226
|
-
const contracts = new Set(slicePackages.map((pkg) =>
|
|
234
|
+
const contracts = new Set(slicePackages.map((pkg) => JSON.stringify([pkg.contract.contract_id, pkg.contract.contract_version])));
|
|
227
235
|
if (contracts.size !== 1) fail("同一切片的任务包必须消费同一 contract_id/contract_version");
|
|
228
236
|
const workers = new Set(slicePackages.filter((pkg) => pkg.execution_state === "Worker").map((pkg) => pkg.actor_id));
|
|
229
237
|
slicePackages.filter((pkg) => pkg.execution_state === "Reviewer").forEach((pkg) => { if (workers.has(pkg.actor_id)) fail("Reviewer 不得与实现者使用同一 actor_id"); });
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { assertApprovedExecutionContext, approvedExecutionBinding } from './approved-execution-context.mjs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { ROOT, read, safe, ensure, hash } from './strategic-handoff-io.mjs';
|
|
5
6
|
|
|
6
7
|
// The compiler's synchronous boundary reruns the design verifier against current files.
|
|
7
|
-
export function enforceTechnicalDesign(state = {}, { root = ROOT } = {}) {
|
|
8
|
+
export function enforceTechnicalDesign(state = {}, { root = ROOT, execution, readOnly = false } = {}) {
|
|
8
9
|
const binding = state.technical_design;
|
|
9
10
|
const affected = (state.conditions || []).some(value => ['backend-technical-design-impact','tactical-domain','tactical-domain-impact','aggregate-impact','state-machine-impact','invariant-impact','consistency-impact','domain-event-impact','gateway-boundary-impact','persistence-mapping-impact'].includes(value));
|
|
10
11
|
if (!binding && !affected) return;
|
|
@@ -13,11 +14,18 @@ export function enforceTechnicalDesign(state = {}, { root = ROOT } = {}) {
|
|
|
13
14
|
const file = safe(root, binding.ref);
|
|
14
15
|
ensure(hash(readFileSync(file)) === binding.digest, '技术设计绑定摘要漂移');
|
|
15
16
|
const data = read(file);
|
|
17
|
+
if(data.design_scope==='engineering-only')ensure(!(state.conditions||[]).some(value=>['tactical-domain','tactical-domain-impact','aggregate-impact','state-machine-impact','invariant-impact','domain-event-impact','gateway-boundary-impact','persistence-mapping-impact'].includes(value)),'engineering-only 技术设计不能承接领域模型、业务状态或持久化映射影响');
|
|
16
18
|
ensure(data.status === 'approved' && data.version === binding.version, '技术设计必须为批准且绑定版本');
|
|
17
19
|
const family = data.schema_version === 1 && binding.legacy_ddd === true ? 'domain-driven' : data.architecture?.family;
|
|
18
20
|
ensure(family && family === state.architecture_identity?.architecture_family, '技术设计与实现架构不匹配');
|
|
19
21
|
const args = [path.join(ROOT, '.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs'), file, '--root', root, '--slice', state.slice_id];
|
|
20
22
|
if (binding.legacy_ddd === true) args.push('--legacy-ddd');
|
|
23
|
+
if(readOnly)args.push('--read-only');
|
|
24
|
+
if(execution){
|
|
25
|
+
assertApprovedExecutionContext(execution,{root,technicalDesign:data,architectureIdentity:state.architecture_identity,sliceId:state.slice_id});
|
|
26
|
+
const approved=approvedExecutionBinding(execution);
|
|
27
|
+
args.push('--approved-slice',approved.ref,'--approved-slice-digest',approved.digest,'--approved-slice-approval',approved.approval_ref);
|
|
28
|
+
}
|
|
21
29
|
const verified = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', timeout: 60000, maxBuffer: 2 * 1024 * 1024 });
|
|
22
30
|
ensure(verified.status === 0, `技术设计不可消费: ${verified.error?.message || verified.stderr}`);
|
|
23
31
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { ensure, read, safe, hash } from './strategic-handoff-io.mjs';
|
|
3
|
+
import { validateVisualBaseline } from '../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs';
|
|
4
|
+
import { validateExistingUiBaseline } from './existing-ui-baseline.mjs';
|
|
5
|
+
|
|
6
|
+
export function hasConsumerRoutes(handoff) { return [4,5].includes(handoff?.schema_version); }
|
|
7
|
+
export function uiBaselineKind(handoff) {
|
|
8
|
+
ensure([3,4,5].includes(handoff?.schema_version),'未知 Strategic Handoff schema_version');
|
|
9
|
+
if(handoff.schema_version<5)return 'prototype';
|
|
10
|
+
ensure(['prototype','existing-ui-baseline'].includes(handoff.ui_baseline_kind),'未知 ui_baseline_kind');
|
|
11
|
+
return handoff.ui_baseline_kind;
|
|
12
|
+
}
|
|
13
|
+
export function uiBaselineRef(handoff) {
|
|
14
|
+
const kind=uiBaselineKind(handoff),ref=handoff.source?.[kind==='prototype'?'visual_baseline_ref':'existing_ui_baseline_ref'];
|
|
15
|
+
ensure(ref,'缺少 UI 基线引用');return ref;
|
|
16
|
+
}
|
|
17
|
+
export function uiBaselineCaseIds(handoff) { return uiBaselineRef(handoff).case_ids; }
|
|
18
|
+
export function uiBaselineSourceKeys(handoff) { return uiBaselineKind(handoff)==='prototype'?['prototype_ref','visual_baseline_ref']:['existing_ui_baseline_ref']; }
|
|
19
|
+
export async function validateHandoffUiBaseline(root,handoff) {
|
|
20
|
+
const kind=uiBaselineKind(handoff),ref=uiBaselineRef(handoff),file=safe(root,`${ref.persisted_ref}/${ref.manifest_ref}`),baseline=read(file);
|
|
21
|
+
const result=kind==='prototype'?await validateVisualBaseline(baseline,{bundleRoot:safe(root,ref.persisted_ref)}):validateExistingUiBaseline(baseline,{bundleRoot:safe(root,ref.persisted_ref)});
|
|
22
|
+
ensure(!result.errors.length,`UI 基线无效: ${result.errors.join('; ')}`);
|
|
23
|
+
// The immutable existing manifest records readiness; the caller verifies its current approval chain.
|
|
24
|
+
ensure((kind==='existing-ui-baseline'||baseline.status==='approved')&&baseline.baseline_id===ref.baseline_id&&baseline.version===ref.version&&JSON.stringify([...ref.case_ids].sort())===JSON.stringify(baseline.cases.map(item=>item.case_id).sort()),'UI 基线身份不一致');
|
|
25
|
+
return {kind,ref,baseline,digest:kind==='prototype'?baseline.bundle.digest:hash(readFileSync(file))};
|
|
26
|
+
}
|
|
@@ -113,8 +113,11 @@ export function validateUserDecision(record, options = {}) {
|
|
|
113
113
|
previousTime = time;
|
|
114
114
|
if (response.decision === "approved") {
|
|
115
115
|
const explicit = source.decision === "approved" && Array.isArray(source.item_ids) && response.item_ids.every((id) => source.item_ids.includes(id));
|
|
116
|
+
const contextualAll = response.selection === "explicit-all" && items.size > 1
|
|
117
|
+
&& /是否\s*(?:明确)?(?:确认|批准|同意|接受)[^\n]{0,32}(?:以上)?全部\s*(?:\d+|[一二三四五六七八九十]+)?\s*(?:项|事项)/i.test(presentation.text)
|
|
118
|
+
&& /^(?:确认|同意|批准|接受|可以|好的|好|yes|ok)[。.!!\s]*$/i.test(source.text.trim());
|
|
116
119
|
if (!explicit && (!/(同意|批准|确认|接受|可以|好的|^好[。!!\s]*$|\b(?:approve|approved|agree|agreed|accept|yes|ok)\b)/i.test(source.text) || /(不同意|不批准|拒绝|反对|不要|需[要]?修改|请修改|暂不|reject|decline|do not approve|don't approve|disagree)/i.test(source.text))) decisionError("user-decision-ambiguous", "原文不能明确支持批准结论");
|
|
117
|
-
if (!explicit && response.selection === "explicit-all" && items.size > 1 && !/(全部|所有|\ball\b)/i.test(source.text)) decisionError("user-decision-ambiguous", "
|
|
120
|
+
if (!explicit && !contextualAll && response.selection === "explicit-all" && items.size > 1 && !/(全部|所有|\ball\b)/i.test(source.text)) decisionError("user-decision-ambiguous", "批量批准原文必须明确全部事项,或直接回复明确询问全部事项的当前展示");
|
|
118
121
|
if (!explicit && response.selection === "explicit-items" && items.size > 1 && response.item_ids.some((id) => !source.text.includes(id) && !source.text.includes(items.get(id)?.title))) decisionError("user-decision-ambiguous", "原文未指向所选事项");
|
|
119
122
|
}
|
|
120
123
|
if (response.selection === "single" && items.size !== 1) decisionError("user-decision-ambiguous", "多事项不能用单项回复");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { read } from './lib/strategic-handoff-io.mjs';
|
|
5
|
+
import { preflightDelivery, invalidPreflightInput } from './lib/delivery-preflight.mjs';
|
|
6
|
+
|
|
7
|
+
let result;
|
|
8
|
+
try {
|
|
9
|
+
const {values}=parseArgs({options:{input:{type:'string'},stage:{type:'string'},json:{type:'boolean'}},strict:true});
|
|
10
|
+
if(!values.input||!values.stage) throw new TypeError('必须提供 --input <file> 和 --stage prepare|build|export|accept');
|
|
11
|
+
result=await preflightDelivery(read(path.resolve(values.input)),{stage:values.stage,root:process.cwd()});
|
|
12
|
+
} catch(error) { result=invalidPreflightInput(error.message); }
|
|
13
|
+
process.stdout.write(`${JSON.stringify(result,null,2)}\n`);
|
|
14
|
+
process.exitCode=result.exit_code;
|