create-yss-spec 3.3.4 → 3.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/template/instance-runtime.js +8 -2
- package/src/validation/snapshot.js +7 -0
- package/template/.agents/skills/.strategic-design-skills-manifest.json +7 -5
- package/template/.agents/skills/archify/SKILL.md +1 -1
- package/template/.agents/skills/code-review/SKILL.md +6 -0
- package/template/.agents/skills/i-have-adhd/LICENSE +21 -0
- package/template/.agents/skills/i-have-adhd/SKILL.md +26 -0
- package/template/.agents/skills/i-have-adhd/agents/openai.yaml +6 -0
- package/template/.agents/skills/i-have-adhd/references/yss-adaptation.md +21 -0
- package/template/.agents/skills/i-have-adhd/tests/verify-integration.mjs +63 -0
- package/template/.agents/skills/prototype-review/SKILL.md +2 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +6 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +10 -0
- package/template/.agents/skills/yss-openapi-governance/SKILL.md +6 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +3 -1
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +12 -0
- package/template/.agents/skills/yss-prototype-stage/SKILL.md +6 -0
- package/template/.agents/skills/yss-research/SKILL.md +6 -0
- package/template/.agents/skills/yss-stage-decision/SKILL.md +19 -4
- package/template/.agents/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
- package/template/.agents/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
- package/template/.agents/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
- package/template/.agents/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
- package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
- package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
- package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
- package/template/.agents/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
- package/template/.agents/skills/yss-stage-decision/references/validation-rules.md +2 -2
- package/template/.agents/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
- package/template/.agents/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
- package/template/.agents/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
- package/template/.agents/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
- package/template/.agents/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
- package/template/.agents/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
- package/template/.agents/skills/yss-technical-design/SKILL.md +6 -0
- package/template/.codex/skills/archify/SKILL.md +1 -1
- package/template/.codex/skills/code-review/SKILL.md +6 -0
- package/template/.codex/skills/i-have-adhd/LICENSE +21 -0
- package/template/.codex/skills/i-have-adhd/SKILL.md +26 -0
- package/template/.codex/skills/i-have-adhd/agents/openai.yaml +6 -0
- package/template/.codex/skills/i-have-adhd/references/yss-adaptation.md +21 -0
- package/template/.codex/skills/i-have-adhd/tests/verify-integration.mjs +63 -0
- package/template/.codex/skills/prototype-review/SKILL.md +2 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +6 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +10 -0
- package/template/.codex/skills/yss-openapi-governance/SKILL.md +6 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +3 -1
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +12 -0
- package/template/.codex/skills/yss-prototype-stage/SKILL.md +6 -0
- package/template/.codex/skills/yss-research/SKILL.md +6 -0
- package/template/.codex/skills/yss-stage-decision/SKILL.md +19 -4
- package/template/.codex/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
- package/template/.codex/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
- package/template/.codex/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
- package/template/.codex/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
- package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
- package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
- package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
- package/template/.codex/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
- package/template/.codex/skills/yss-stage-decision/references/validation-rules.md +2 -2
- package/template/.codex/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
- package/template/.codex/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
- package/template/.codex/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
- package/template/.codex/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
- package/template/.codex/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
- package/template/.codex/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
- package/template/.codex/skills/yss-technical-design/SKILL.md +6 -0
- package/template/.cursor/skills/archify/SKILL.md +1 -1
- package/template/.cursor/skills/code-review/SKILL.md +6 -0
- package/template/.cursor/skills/i-have-adhd/LICENSE +21 -0
- package/template/.cursor/skills/i-have-adhd/SKILL.md +26 -0
- package/template/.cursor/skills/i-have-adhd/agents/openai.yaml +6 -0
- package/template/.cursor/skills/i-have-adhd/references/yss-adaptation.md +21 -0
- package/template/.cursor/skills/i-have-adhd/tests/verify-integration.mjs +63 -0
- package/template/.cursor/skills/prototype-review/SKILL.md +2 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +6 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +10 -0
- package/template/.cursor/skills/yss-openapi-governance/SKILL.md +6 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +3 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +12 -0
- package/template/.cursor/skills/yss-prototype-stage/SKILL.md +6 -0
- package/template/.cursor/skills/yss-research/SKILL.md +6 -0
- package/template/.cursor/skills/yss-stage-decision/SKILL.md +19 -4
- package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
- package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
- package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
- package/template/.cursor/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
- package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
- package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
- package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
- package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
- package/template/.cursor/skills/yss-stage-decision/references/validation-rules.md +2 -2
- package/template/.cursor/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
- package/template/.cursor/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
- package/template/.cursor/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
- package/template/.cursor/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
- package/template/.cursor/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
- package/template/.cursor/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
- package/template/.cursor/skills/yss-technical-design/SKILL.md +6 -0
- package/template/.pi/skills/archify/SKILL.md +1 -1
- package/template/.pi/skills/code-review/SKILL.md +6 -0
- package/template/.pi/skills/i-have-adhd/LICENSE +21 -0
- package/template/.pi/skills/i-have-adhd/SKILL.md +26 -0
- package/template/.pi/skills/i-have-adhd/agents/openai.yaml +6 -0
- package/template/.pi/skills/i-have-adhd/references/yss-adaptation.md +21 -0
- package/template/.pi/skills/i-have-adhd/tests/verify-integration.mjs +63 -0
- package/template/.pi/skills/prototype-review/SKILL.md +2 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +6 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +10 -0
- package/template/.pi/skills/yss-openapi-governance/SKILL.md +6 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +3 -1
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +12 -0
- package/template/.pi/skills/yss-prototype-stage/SKILL.md +6 -0
- package/template/.pi/skills/yss-research/SKILL.md +6 -0
- package/template/.pi/skills/yss-stage-decision/SKILL.md +19 -4
- package/template/.pi/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
- package/template/.pi/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
- package/template/.pi/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
- package/template/.pi/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
- package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
- package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
- package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
- package/template/.pi/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
- package/template/.pi/skills/yss-stage-decision/references/validation-rules.md +2 -2
- package/template/.pi/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
- package/template/.pi/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
- package/template/.pi/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
- package/template/.pi/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
- package/template/.pi/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
- package/template/.pi/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
- package/template/.pi/skills/yss-technical-design/SKILL.md +6 -0
- package/template/docs/agents/yss-skill-registry.yaml +22 -0
- package/template/docs/plan/templates/plan-template.md +8 -4
- package/template/docs/process/document-writing.md +77 -0
- package/template/docs/process/frontend-backend-delivery.md +7 -4
- package/template/docs/process/schemas/backend-delivery.schema.json +4 -0
- package/template/docs/process/schemas/frontend-delivery-acceptance-v1.schema.json +200 -0
- package/template/docs/process/schemas/frontend-delivery-acceptance.schema.json +37 -3
- package/template/docs/process/schemas/frontend-strategic-preflight.schema.json +32 -0
- package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +2 -1
- package/template/docs/process/schemas/strategic-consumer-feedback.schema.json +40 -0
- package/template/docs/process/schemas/strategic-design-handoff-v3.schema.json +743 -0
- package/template/docs/process/schemas/strategic-design-handoff-v4.schema.json +712 -0
- package/template/docs/process/schemas/strategic-feedback-adjudication.schema.json +42 -0
- package/template/docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json +216 -0
- package/template/docs/process/schemas/strategic-handoff-import-receipt.schema.json +39 -0
- package/template/docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json +148 -0
- package/template/docs/process/strategic-handoff-package.md +37 -4
- package/template/docs/process/templates/lifecycle-checkpoint-template.yaml +1 -0
- package/template/docs/process/templates/stage-checkpoint-template.md +1 -0
- package/template/docs/templates/examples/lifecycle-writing-examples.md +138 -0
- package/template/docs/templates/spec-template.md +16 -3
- package/template/docs/templates/vertical-slice-ticket-template.md +8 -4
- package/template/scripts/lib/approval-record.mjs +2 -1
- package/template/scripts/lib/backend-delivery.mjs +15 -3
- package/template/scripts/lib/frontend-delivery-boundary.mjs +20 -4
- package/template/scripts/lib/frontend-delivery.mjs +66 -2
- package/template/scripts/lib/profile-skill-sync.mjs +336 -0
- package/template/scripts/lib/strategic-feedback.mjs +33 -0
- package/template/scripts/lib/strategic-handoff-consumption.mjs +5 -0
- package/template/scripts/lib/strategic-handoff-io.mjs +1 -0
- package/template/scripts/lib/strategic-handoff.mjs +101 -10
- package/template/scripts/lib/user-decision-reuse.mjs +38 -0
- package/template/scripts/strategic-feedback +10 -0
- package/template/scripts/sync-profile-skills +43 -0
- package/template/scripts/verify-frontend-strategic-preflight +12 -0
- package/template/skills-lock.json +38 -20
- package/template.manifest.json +1 -0
- package/template.snapshot.json +6 -5
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
function fail(message) {
|
|
17
|
+
throw new TypeError(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safe(root, relative, label = "path") {
|
|
21
|
+
if (typeof relative !== "string" || !relative || path.isAbsolute(relative)) fail(`${label} 必须是仓库相对路径`);
|
|
22
|
+
const resolvedRoot = path.resolve(root);
|
|
23
|
+
const resolved = path.resolve(resolvedRoot, relative);
|
|
24
|
+
if (resolved === resolvedRoot || !resolved.startsWith(`${resolvedRoot}${path.sep}`)) fail(`${label} 越界: ${relative}`);
|
|
25
|
+
return resolved;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function files(directory, prefix = "") {
|
|
29
|
+
if (!existsSync(directory)) return new Map();
|
|
30
|
+
const info = lstatSync(directory);
|
|
31
|
+
if (!info.isDirectory()) fail(`技能源必须是目录: ${directory}`);
|
|
32
|
+
const result = new Map();
|
|
33
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
34
|
+
if (entry.isSymbolicLink()) fail(`跨仓同步不接受符号链接: ${path.join(directory, entry.name)}`);
|
|
35
|
+
const absolute = path.join(directory, entry.name);
|
|
36
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
for (const item of files(absolute, relative)) result.set(...item);
|
|
39
|
+
} else if (entry.isFile()) result.set(relative, readFileSync(absolute));
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function digest(value) {
|
|
45
|
+
return createHash("sha256").update(value).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sameTree(left, right) {
|
|
49
|
+
if (left.size !== right.size) return false;
|
|
50
|
+
for (const [name, content] of left) if (!right.get(name)?.equals(content)) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function treeDigest(tree) {
|
|
55
|
+
const hash = createHash("sha256");
|
|
56
|
+
for (const [name, content] of [...tree.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
|
|
57
|
+
hash.update(name);
|
|
58
|
+
hash.update("\0");
|
|
59
|
+
hash.update(content);
|
|
60
|
+
hash.update("\0");
|
|
61
|
+
}
|
|
62
|
+
return hash.digest("hex");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function patchedSource(root, profile, item) {
|
|
66
|
+
const source = safe(root, item.source ?? `.agents/skills/${item.id}`, "source");
|
|
67
|
+
const patchFile = safe(root, item.patch, "patch");
|
|
68
|
+
const sourceFiles = files(source);
|
|
69
|
+
const actualBaseline = treeDigest(sourceFiles);
|
|
70
|
+
if (actualBaseline !== item.source_tree_sha256) {
|
|
71
|
+
fail(`${profile}/${item.id} 适配基线冲突: 上游树预期 ${item.source_tree_sha256},实际 ${actualBaseline}`);
|
|
72
|
+
}
|
|
73
|
+
const temporary = mkdtempSync(path.join(tmpdir(), "profile-skill-patch-"));
|
|
74
|
+
try {
|
|
75
|
+
for (const [name, content] of sourceFiles) {
|
|
76
|
+
const target = safe(temporary, name, "patch source file");
|
|
77
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
78
|
+
writeFileSync(target, content);
|
|
79
|
+
}
|
|
80
|
+
const applied = spawnSync("git", ["apply", "--binary", "--whitespace=nowarn", patchFile], {
|
|
81
|
+
cwd: temporary,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
84
|
+
});
|
|
85
|
+
if (applied.status !== 0) fail(`${profile}/${item.id} 补丁无法重放: ${applied.stderr.trim()}`);
|
|
86
|
+
return files(temporary);
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function gitDirty(profileRoot, relativePaths) {
|
|
93
|
+
if (!relativePaths.length) return new Set();
|
|
94
|
+
const result = spawnSync("git", ["-C", profileRoot, "status", "--porcelain=v1", "--untracked-files=all", "--", ...relativePaths], {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
+
});
|
|
98
|
+
if (result.status !== 0) fail(`无法读取目标工作树状态: ${result.stderr.trim()}`);
|
|
99
|
+
return new Set(result.stdout.split("\n").filter(Boolean).map((line) => line.slice(3).replace(/^"|"$/g, "")));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dirtyMatch(dirty, relative) {
|
|
103
|
+
return [...dirty].some((entry) => entry === relative || entry.startsWith(`${relative}/`) || relative.startsWith(`${entry}/`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function transformedSource(root, profile, item) {
|
|
107
|
+
const source = safe(root, item.source ?? `.agents/skills/${item.id}`, "source");
|
|
108
|
+
const sourceFiles = files(source);
|
|
109
|
+
const desired = item.files?.length
|
|
110
|
+
? new Map(item.files.map((name) => {
|
|
111
|
+
if (!sourceFiles.has(name)) fail(`${profile}/${item.id} 适配源文件不存在: ${name}`);
|
|
112
|
+
return [name, sourceFiles.get(name)];
|
|
113
|
+
}))
|
|
114
|
+
: sourceFiles;
|
|
115
|
+
for (const replacement of item.replacements ?? []) {
|
|
116
|
+
const current = desired.get(replacement.file);
|
|
117
|
+
if (!current) fail(`${profile}/${item.id} 适配文件不存在: ${replacement.file}`);
|
|
118
|
+
const before = current.toString("utf8");
|
|
119
|
+
const count = before.split(replacement.from).length - 1;
|
|
120
|
+
const expected = replacement.count ?? 1;
|
|
121
|
+
if (count !== expected) fail(`${profile}/${item.id} 适配基线冲突: ${replacement.file} 中预期 ${expected} 处片段,实际 ${count} 处`);
|
|
122
|
+
desired.set(replacement.file, Buffer.from(before.split(replacement.from).join(replacement.to)));
|
|
123
|
+
}
|
|
124
|
+
return desired;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compareTree(profile, id, targetRelative, current, desired, classification, changes) {
|
|
128
|
+
const names = new Set([...current.keys(), ...desired.keys()]);
|
|
129
|
+
for (const name of [...names].sort()) {
|
|
130
|
+
const before = current.get(name);
|
|
131
|
+
const after = desired.get(name);
|
|
132
|
+
if (before && after && before.equals(after)) continue;
|
|
133
|
+
changes.push({
|
|
134
|
+
profile,
|
|
135
|
+
skill: id,
|
|
136
|
+
classification,
|
|
137
|
+
status: before ? (after ? "updated" : "retired") : "added",
|
|
138
|
+
path: `${targetRelative}/${name}`,
|
|
139
|
+
before_sha256: before ? digest(before) : null,
|
|
140
|
+
after_sha256: after ? digest(after) : null,
|
|
141
|
+
content: after ?? null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function loadProfileSyncConfig(root, configPath = ".template-source/profile-skill-sync.json") {
|
|
147
|
+
const absolute = safe(root, configPath, "config");
|
|
148
|
+
const config = JSON.parse(readFileSync(absolute, "utf8"));
|
|
149
|
+
if (config.schema_version !== 1 || !config.profiles || typeof config.profiles !== "object") fail("profile skill sync 配置必须是 schema v1");
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function planProfileSkillSync({ root, config, selectedProfiles, dirtyProvider = gitDirty }) {
|
|
154
|
+
const names = selectedProfiles?.length ? selectedProfiles : Object.keys(config.profiles);
|
|
155
|
+
const canonicalRoot = safe(root, config.canonical_root ?? ".agents/skills", "canonical_root");
|
|
156
|
+
const changes = [];
|
|
157
|
+
const classifications = [];
|
|
158
|
+
const issues = [];
|
|
159
|
+
for (const profile of names) {
|
|
160
|
+
const definition = config.profiles[profile];
|
|
161
|
+
if (!definition) fail(`未知 profile: ${profile}`);
|
|
162
|
+
const profileRoot = safe(root, definition.target, `${profile}.target`);
|
|
163
|
+
if (!existsSync(path.join(profileRoot, "yss-project.yaml"))) issues.push({ profile, status: "missing_dependency", path: `${definition.target}/yss-project.yaml` });
|
|
164
|
+
const classified = new Map();
|
|
165
|
+
for (const [classification, items] of [
|
|
166
|
+
["exact", (definition.exact ?? []).map(({ id }) => id)],
|
|
167
|
+
["adapted", (definition.adapted ?? []).map(({ id }) => id)],
|
|
168
|
+
["retired", definition.retired ?? []],
|
|
169
|
+
["local-only", definition.local_only ?? []],
|
|
170
|
+
["excluded", definition.excluded ?? []],
|
|
171
|
+
["upstream", definition.upstream ?? []],
|
|
172
|
+
]) {
|
|
173
|
+
for (const id of items) {
|
|
174
|
+
if (classified.has(id)) fail(`${profile}/${id} 同时声明为 ${classified.get(id)} 和 ${classification},违反单向来源规则`);
|
|
175
|
+
classified.set(id, classification);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (definition.upstream_manifest) {
|
|
179
|
+
const manifestPath = safe(root, definition.upstream_manifest, "upstream manifest");
|
|
180
|
+
if (!existsSync(manifestPath)) issues.push({ profile, status: "missing_dependency", path: definition.upstream_manifest });
|
|
181
|
+
else {
|
|
182
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
183
|
+
const declared = [...(definition.upstream ?? [])].sort();
|
|
184
|
+
const manifestIds = (manifest.skills ?? []).map(({ canonical }) => canonical).sort();
|
|
185
|
+
if (JSON.stringify(declared) !== JSON.stringify(manifestIds) || manifest.source_root !== ".agents/skills") {
|
|
186
|
+
issues.push({ profile, status: "adaptation_conflict", path: definition.upstream_manifest, message: "上游清单与 profile 单向来源声明不一致" });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const item of definition.exact ?? []) {
|
|
192
|
+
const sourceRelative = item.source ?? `.agents/skills/${item.id}`;
|
|
193
|
+
const targetRelative = item.target ?? `.agents/skills/${item.id}`;
|
|
194
|
+
const source = safe(root, sourceRelative, "source");
|
|
195
|
+
if (!existsSync(source)) {
|
|
196
|
+
issues.push({ profile, skill: item.id, status: "missing_dependency", path: sourceRelative });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
compareTree(profile, item.id, targetRelative, files(safe(profileRoot, targetRelative, "target")), files(source), "exact", changes);
|
|
200
|
+
classifications.push({ profile, skill: item.id, classification: "exact" });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const item of definition.adapted ?? []) {
|
|
204
|
+
classifications.push({ profile, skill: item.id, classification: "adapted" });
|
|
205
|
+
if (item.strategy === "profile-owned") {
|
|
206
|
+
const targetRelative = item.target ?? `.agents/skills/${item.id}`;
|
|
207
|
+
const skillFile = safe(profileRoot, `${targetRelative}/SKILL.md`, "adapted target");
|
|
208
|
+
if (!existsSync(skillFile)) issues.push({ profile, skill: item.id, status: "missing_dependency", path: `${definition.target}/${targetRelative}/SKILL.md` });
|
|
209
|
+
else {
|
|
210
|
+
const content = readFileSync(skillFile, "utf8");
|
|
211
|
+
for (const marker of item.required_markers ?? []) if (!content.includes(marker)) issues.push({ profile, skill: item.id, status: "adaptation_conflict", path: `${definition.target}/${targetRelative}/SKILL.md`, marker });
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const targetRelative = item.target ?? `.agents/skills/${item.id}`;
|
|
216
|
+
let desired;
|
|
217
|
+
try {
|
|
218
|
+
const sourceRelative = item.source ?? `.agents/skills/${item.id}`;
|
|
219
|
+
if (!existsSync(safe(root, sourceRelative, "source"))) {
|
|
220
|
+
issues.push({ profile, skill: item.id, status: "missing_dependency", path: sourceRelative });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (item.strategy === "replace") desired = transformedSource(root, profile, item);
|
|
224
|
+
else if (item.strategy === "patch") {
|
|
225
|
+
if (!item.patch || !existsSync(safe(root, item.patch, "patch"))) {
|
|
226
|
+
issues.push({ profile, skill: item.id, status: "missing_dependency", path: item.patch ?? "patch" });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
desired = patchedSource(root, profile, item);
|
|
230
|
+
} else fail(`${profile}/${item.id} 未知适配策略`);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
issues.push({ profile, skill: item.id, status: "adaptation_conflict", message: error.message });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const targetFiles = files(safe(profileRoot, targetRelative, "target"));
|
|
236
|
+
const current = item.files?.length ? new Map(item.files.filter((name) => targetFiles.has(name)).map((name) => [name, targetFiles.get(name)])) : targetFiles;
|
|
237
|
+
compareTree(profile, item.id, targetRelative, current, desired, "adapted", changes);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const item of definition.assets ?? []) {
|
|
241
|
+
const source = safe(root, item.source, "asset source");
|
|
242
|
+
const target = safe(profileRoot, item.target ?? item.source, "asset target");
|
|
243
|
+
if (!existsSync(source)) {
|
|
244
|
+
issues.push({ profile, status: "missing_dependency", path: item.source });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const before = existsSync(target) ? readFileSync(target) : null;
|
|
248
|
+
const after = readFileSync(source);
|
|
249
|
+
if (!before?.equals(after)) changes.push({ profile, classification: "exact", status: before ? "updated" : "added", path: item.target ?? item.source, before_sha256: before ? digest(before) : null, after_sha256: digest(after), content: after });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const id of definition.retired ?? []) {
|
|
253
|
+
classifications.push({ profile, skill: id, classification: "retired" });
|
|
254
|
+
const targetRelative = `.agents/skills/${id}`;
|
|
255
|
+
const current = files(safe(profileRoot, targetRelative, "retired target"));
|
|
256
|
+
for (const [name, content] of current) changes.push({ profile, skill: id, classification: "retired", status: "retired", path: `${targetRelative}/${name}`, before_sha256: digest(content), after_sha256: null, content: null });
|
|
257
|
+
}
|
|
258
|
+
for (const id of definition.local_only ?? []) classifications.push({ profile, skill: id, classification: "local-only" });
|
|
259
|
+
for (const id of definition.excluded ?? []) classifications.push({ profile, skill: id, classification: "excluded" });
|
|
260
|
+
for (const id of definition.upstream ?? []) classifications.push({ profile, skill: id, classification: "upstream" });
|
|
261
|
+
|
|
262
|
+
const targetSkillRoot = safe(profileRoot, ".agents/skills", "profile skill root");
|
|
263
|
+
if (existsSync(targetSkillRoot)) {
|
|
264
|
+
for (const entry of readdirSync(targetSkillRoot, { withFileTypes: true })) {
|
|
265
|
+
if (!entry.isDirectory() || classified.has(entry.name)) continue;
|
|
266
|
+
const sourceSkill = path.join(canonicalRoot, entry.name);
|
|
267
|
+
if (!existsSync(sourceSkill) || !lstatSync(sourceSkill).isDirectory()) continue;
|
|
268
|
+
if (!sameTree(files(sourceSkill), files(path.join(targetSkillRoot, entry.name)))) {
|
|
269
|
+
issues.push({
|
|
270
|
+
profile,
|
|
271
|
+
skill: entry.name,
|
|
272
|
+
status: "unregistered_difference",
|
|
273
|
+
path: `${definition.target}/.agents/skills/${entry.name}`,
|
|
274
|
+
message: "同名 Skill 内容不同但未登记同步分类",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const changedPaths = [...new Set(changes.filter((item) => item.profile === profile).map((item) => item.path))];
|
|
281
|
+
const dirty = dirtyProvider(profileRoot, changedPaths);
|
|
282
|
+
for (const change of changes.filter((item) => item.profile === profile && dirtyMatch(dirty, item.path))) {
|
|
283
|
+
issues.push({ profile, skill: change.skill, status: "adaptation_conflict", path: `${definition.target}/${change.path}`, message: "目标含未提交改动" });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { schema_version: 1, profiles: names, changes, classifications, issues };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function applyProfileSkillSync({ root, config, plan, write = writeFileSync, remove = rmSync }) {
|
|
290
|
+
if (plan.issues.length) fail(`同步预检失败,共 ${plan.issues.length} 项问题`);
|
|
291
|
+
const snapshots = new Map();
|
|
292
|
+
const touched = [];
|
|
293
|
+
try {
|
|
294
|
+
for (const change of plan.changes) {
|
|
295
|
+
const profileRoot = safe(root, config.profiles[change.profile].target, "profile target");
|
|
296
|
+
const target = safe(profileRoot, change.path, "change target");
|
|
297
|
+
const current = existsSync(target) ? readFileSync(target) : null;
|
|
298
|
+
const currentHash = current ? digest(current) : null;
|
|
299
|
+
if (currentHash !== change.before_sha256) fail(`目标在预览后发生变化: ${change.profile}/${change.path}`);
|
|
300
|
+
snapshots.set(target, current);
|
|
301
|
+
touched.push(target);
|
|
302
|
+
if (change.content === null) remove(target, { force: true });
|
|
303
|
+
else {
|
|
304
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
305
|
+
write(target, change.content);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const profile of plan.profiles) {
|
|
309
|
+
const profileRoot = safe(root, config.profiles[profile].target, "profile target");
|
|
310
|
+
for (const id of config.profiles[profile].retired ?? []) {
|
|
311
|
+
const retiredRoot = safe(profileRoot, `.agents/skills/${id}`, "retired skill");
|
|
312
|
+
if (!existsSync(retiredRoot)) continue;
|
|
313
|
+
if (!lstatSync(retiredRoot).isDirectory()) fail(`退役 Skill 目标不是目录: ${retiredRoot}`);
|
|
314
|
+
if (files(retiredRoot).size) fail(`退役 Skill 在预览后出现未纳管文件: ${retiredRoot}`);
|
|
315
|
+
remove(retiredRoot, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
for (const target of touched.reverse()) {
|
|
320
|
+
const before = snapshots.get(target);
|
|
321
|
+
if (before === null) remove(target, { force: true });
|
|
322
|
+
else {
|
|
323
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
324
|
+
writeFileSync(target, before);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
return { ...plan, applied: plan.changes.length };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function reportProfileSkillSync(plan) {
|
|
333
|
+
const counts = {};
|
|
334
|
+
for (const item of [...plan.changes, ...plan.issues]) counts[item.status] = (counts[item.status] ?? 0) + 1;
|
|
335
|
+
return { ...plan, changes: plan.changes.map(({ content, ...item }) => item), counts };
|
|
336
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { openBundle } from './strategic-handoff.mjs';
|
|
3
|
+
import { read, safe, ensure, hash, schema, project } from './strategic-handoff-io.mjs';
|
|
4
|
+
|
|
5
|
+
export async function verifyConsumerFeedback({root=process.cwd(),feedbackRef}={}) {
|
|
6
|
+
project(root);const file=safe(root,feedbackRef),feedback=read(file);
|
|
7
|
+
schema(feedback,'docs/process/schemas/strategic-consumer-feedback.schema.json');
|
|
8
|
+
const receipt=read(safe(root,feedback.import_receipt_ref));
|
|
9
|
+
ensure(receipt.schema_version===2,'Consumer Feedback v1 只接受 Handoff v4 的 Import Receipt v2');
|
|
10
|
+
const base=`docs/handoffs/${receipt.bundle_id}/${receipt.version}`;
|
|
11
|
+
ensure(feedback.import_receipt_ref===`${base}/import-receipt.json`&&receipt.package_ref===`${base}/package`,'Consumer Feedback 导入收据路径无效');
|
|
12
|
+
ensure(feedback.bundle.id===receipt.bundle_id&&feedback.bundle.version===receipt.version&&feedback.bundle.digest===receipt.bundle_digest,'Consumer Feedback 绑定的 bundle 身份或摘要不一致');
|
|
13
|
+
const route=receipt.routes.find(item=>item.route_id===feedback.route_id);ensure(route&&route.activation!=='not-applicable','Consumer Feedback route_id 未被当前消费者激活');
|
|
14
|
+
for(const ref of feedback.evidence_refs)ensure(readFileSync(safe(root,ref)).length>0,`Consumer Feedback evidence 为空: ${ref}`);
|
|
15
|
+
return openBundle(safe(root,receipt.package_ref),bundle=>{
|
|
16
|
+
ensure(bundle.handoff.schema_version===4&&bundle.manifest.bundle_digest===receipt.bundle_digest,'Consumer Feedback 必须绑定当前 Handoff v4 包');
|
|
17
|
+
const source=[...bundle.indexes.rules.map(item=>({id:item.rule_id,digest:item.source_digest})),...bundle.indexes.scenarios.map(item=>({id:item.scenario_id,digest:item.source_digest}))].find(item=>item.id===feedback.source.id);
|
|
18
|
+
ensure(source&&source.digest===feedback.source.digest,'Consumer Feedback source ID 或摘要不是当前包内容');
|
|
19
|
+
return {result:'feedback-verified',feedback_ref:feedbackRef,feedback_digest:hash(readFileSync(file)),severity:feedback.severity,route_id:feedback.route_id,bundle_digest:receipt.bundle_digest};
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function verifyFeedbackAdjudication({root=process.cwd(),adjudicationRef}={}) {
|
|
24
|
+
project(root);const file=safe(root,adjudicationRef),adjudication=read(file);
|
|
25
|
+
schema(adjudication,'docs/process/schemas/strategic-feedback-adjudication.schema.json');
|
|
26
|
+
const feedbackFile=safe(root,adjudication.feedback.ref);ensure(hash(readFileSync(feedbackFile))===adjudication.feedback.digest,'Feedback Adjudication 绑定的 feedback digest 已失效');
|
|
27
|
+
const verified=await verifyConsumerFeedback({root,feedbackRef:adjudication.feedback.ref});
|
|
28
|
+
for(const ref of adjudication.evidence_refs)ensure(readFileSync(safe(root,ref)).length>0,`Feedback Adjudication evidence 为空: ${ref}`);
|
|
29
|
+
if(adjudication.approval_ref)ensure(readFileSync(safe(root,adjudication.approval_ref)).length>0,'Feedback Adjudication approval_ref 为空');
|
|
30
|
+
if(adjudication.decision==='amend')return {result:'blocked',decision:'amend',requires_new_handoff:true,invalidated_bundle_digest:verified.bundle_digest,next_work_unit:adjudication.return_work_unit,target_version:adjudication.target_version};
|
|
31
|
+
if(adjudication.decision==='defer'&&verified.severity==='blocking')return {result:'blocked',decision:'defer',requires_new_handoff:false,next_work_unit:adjudication.return_work_unit,target_version:adjudication.target_version};
|
|
32
|
+
return {result:'adjudicated',decision:adjudication.decision,may_continue:true,bundle_digest:verified.bundle_digest,target_version:adjudication.target_version};
|
|
33
|
+
}
|
|
@@ -23,6 +23,11 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
|
|
|
23
23
|
const expectedBase=`docs/handoffs/${receipt.bundle_id}/${receipt.version}`;
|
|
24
24
|
ensure(binding.import_receipt_ref===`${expectedBase}/import-receipt.json` && receipt.package_ref===`${expectedBase}/package`,'导入收据路径与包身份不一致');
|
|
25
25
|
ensure(binding.bundle_digest===receipt.bundle_digest,'战术绑定与导入收据摘要不一致');
|
|
26
|
+
if(receipt.schema_version===2){
|
|
27
|
+
const capability=consumer==='frontend'?'frontend-engineering-design':'backend-technical-design';
|
|
28
|
+
const route=receipt.routes.find(item=>item.capability===capability);
|
|
29
|
+
ensure(route&&route.activation!=='not-applicable'&&binding.route_id===route.route_id,`${capability} 未绑定当前 Handoff v4 route_id`);
|
|
30
|
+
}
|
|
26
31
|
const result=spawnSync(process.execPath,[path.join(ROOT,'scripts/verify-context-reconciliation'),'--root',root,safe(root,binding.context_reconciliation_ref)],{encoding:'utf8'});
|
|
27
32
|
ensure(result.status===0,`目标术语对账未通过: ${result.error?.message || result.stderr}`);
|
|
28
33
|
const context=parseContextContract({root});
|
|
@@ -66,6 +66,7 @@ export function archive(command, source, destination) {
|
|
|
66
66
|
export function sourceApprovalPolicy(source) {
|
|
67
67
|
const roles=structuredClone(source);
|
|
68
68
|
roles.user_decision_policy ||= {gates:[]};
|
|
69
|
+
ensure(!(roles.user_decision_policy.required_capabilities || []).some(id => id !== 'strategic-decision-reuse-v1'), '接收工具不支持源用户决定策略,请升级工具后验包');
|
|
69
70
|
const policy=roles.gate_policy;
|
|
70
71
|
for(const gate of policy.product_digital_human_with_biological_veto || []) {
|
|
71
72
|
if(!(policy.biological_human||[]).includes(gate) && !(policy.digital_human_review||[]).some(x=>x.gate===gate) && !(policy.dual_digital_human||[]).some(x=>x.gate===gate)) {
|
|
@@ -12,13 +12,55 @@ 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']]);
|
|
16
|
+
const SOURCE_SCHEMAS = new Map([
|
|
17
|
+
[3,['docs/process/schemas/strategic-handoff-domain-strategy.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']],
|
|
18
|
+
[4,['docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json']],
|
|
19
|
+
]);
|
|
20
|
+
const CAPABILITIES=['backend-technical-design','frontend-engineering-design','delivery-coordination'];
|
|
21
|
+
|
|
22
|
+
function versionSchema(version, schemas, label) {
|
|
23
|
+
ensure(schemas.has(version), `${label} 未知 schema_version: ${String(version)};支持版本: ${[...schemas.keys()].join(', ')};请使用显式迁移器升级`);
|
|
24
|
+
return schemas.get(version);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateConsumerRoutes(handoff, stage) {
|
|
28
|
+
if(handoff.schema_version!==4)return;
|
|
29
|
+
const routes=new Map();
|
|
30
|
+
for(const route of handoff.consumer_routes||[]){
|
|
31
|
+
ensure(!routes.has(route.capability),`消费者能力重复: ${route.capability}`);
|
|
32
|
+
ensure(![...routes.values()].some(item=>item.route_id===route.route_id),`消费者 route_id 重复: ${route.route_id}`);
|
|
33
|
+
routes.set(route.capability,route);
|
|
34
|
+
}
|
|
35
|
+
ensure(CAPABILITIES.every(capability=>routes.has(capability)),'Handoff v4 必须完整声明 backend、frontend 与 coordination 三条消费者路由');
|
|
36
|
+
const impact=stage.impact_assessment||{};
|
|
37
|
+
const mappingByCapability=new Map();
|
|
38
|
+
for(const mapping of stage.downstream_mapping||[]){
|
|
39
|
+
ensure(CAPABILITIES.includes(mapping.consumer_capability),`未知消费者能力: ${mapping.consumer_capability}`);
|
|
40
|
+
const values=mappingByCapability.get(mapping.consumer_capability)||[];values.push(mapping);mappingByCapability.set(mapping.consumer_capability,values);
|
|
41
|
+
}
|
|
42
|
+
const expected=(capability,required)=>required?'required':(mappingByCapability.get(capability)||[]).some(mapping=>mapping.propagation!=='not-applicable')?'optional':'not-applicable';
|
|
43
|
+
const backend=routes.get('backend-technical-design'),frontend=routes.get('frontend-engineering-design'),coordination=routes.get('delivery-coordination');
|
|
44
|
+
const backendActive=Boolean(impact.backend||impact.api||impact.data),frontendActive=Boolean(impact.frontend||impact.ui);
|
|
45
|
+
ensure(backend.activation===expected(backend.capability,backendActive),'backend 消费者路由与批准的 Backend/API/Data 影响矩阵不一致');
|
|
46
|
+
ensure(frontend.activation===expected(frontend.capability,frontendActive),'frontend 消费者路由与批准的 Frontend/UI 影响矩阵不一致');
|
|
47
|
+
ensure(coordination.activation===((backend.activation!=='not-applicable'||frontend.activation!=='not-applicable')?'required':'not-applicable'),'coordination 消费者路由与交付影响不一致');
|
|
48
|
+
const activeBackend=backend.activation!=='not-applicable';
|
|
49
|
+
ensure(own(frontend.dependencies,activeBackend?[backend.route_id]:[]),'frontend 路由依赖与 backend 路由不一致');
|
|
50
|
+
ensure(own(coordination.dependencies,[...(backend.activation!=='not-applicable'?[backend.route_id]:[]),...(frontend.activation!=='not-applicable'?[frontend.route_id]:[])]),'coordination 路由依赖与消费者路由不一致');
|
|
51
|
+
for(const route of routes.values())for(const dependency of route.dependencies)ensure([...routes.values()].some(item=>item.route_id===dependency),`消费者路由依赖悬空: ${dependency}`);
|
|
52
|
+
for(const [capability,mappings] of mappingByCapability)for(const mapping of mappings){
|
|
53
|
+
const route=routes.get(capability);
|
|
54
|
+
ensure(mapping.propagation==='not-applicable'?route.activation==='not-applicable':route.activation!=='not-applicable',`downstream mapping 与消费者路由影响不一致: ${mapping.mapping_id}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
15
57
|
|
|
16
58
|
export async function sourceApproval(record, roles, root) {
|
|
17
59
|
const options={rolesDoc:roles,requireApproved:true,root,read:ref=>readFileSync(safe(root,path.relative(root,ref).split(path.sep).join('/')))};
|
|
18
60
|
if (roles.user_decision_policy.gates.includes(record.gate_id)) {
|
|
19
61
|
ensure(existsSync(path.join(ROOT,'scripts/lib/user-decision.mjs')), '接收工具不支持源用户决定策略,请升级工具后验包');
|
|
20
|
-
const {
|
|
21
|
-
|
|
62
|
+
const { assertApprovalUserDecision } = await import('./user-decision-reuse.mjs');
|
|
63
|
+
assertApprovalUserDecision(record, roles, options);
|
|
22
64
|
}
|
|
23
65
|
validateApprovalRecord(record,options);
|
|
24
66
|
}
|
|
@@ -83,7 +125,7 @@ function previewFiles(root, config) {
|
|
|
83
125
|
|
|
84
126
|
export async function inspectSource(root, handoffRef) {
|
|
85
127
|
const handoff=read(safe(root,handoffRef));
|
|
86
|
-
schema(handoff,
|
|
128
|
+
schema(handoff,versionSchema(handoff?.schema_version,HANDOFF_SCHEMAS,'Strategic Handoff'));
|
|
87
129
|
ensure(handoff.status==='approved', '交接包尚未批准');
|
|
88
130
|
const config=handoff.package_export;
|
|
89
131
|
schema(config,'docs/process/schemas/strategic-handoff-export.schema.json');
|
|
@@ -91,13 +133,15 @@ export async function inspectSource(root, handoffRef) {
|
|
|
91
133
|
snapshot(handoff.source_context_snapshot,source); checkDelta(handoff,source);
|
|
92
134
|
const strategy=read(safe(root,handoff.source.domain_strategy_ref.persisted_ref));
|
|
93
135
|
const stage=read(safe(root,handoff.source.stage_decision_package_ref.persisted_ref));
|
|
94
|
-
|
|
136
|
+
const [strategySchema,stageSchema]=versionSchema(handoff.schema_version,SOURCE_SCHEMAS,'Strategic Handoff source');
|
|
137
|
+
schemaBatch([[strategy,strategySchema],[stage,stageSchema]]);
|
|
95
138
|
snapshot(strategy.context_snapshot,source); snapshot(stage.context_snapshot,source);
|
|
96
139
|
ensure(strategy.status==='approved' && stage.status==='approved', '战略合同/方案决策包尚未批准');
|
|
97
140
|
ensure(strategy.domain_strategy_id===handoff.source.domain_strategy_ref.id && strategy.domain_version===handoff.source.domain_strategy_ref.version, '战略身份/版本不一致');
|
|
98
141
|
ensure(stage.stage_decision_id===handoff.source.stage_decision_package_ref.id && stage.package_version===handoff.source.stage_decision_package_ref.version, '阶段包身份/版本不一致');
|
|
99
142
|
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), '阶段包引用的战略内容不一致');
|
|
100
143
|
ensure(!(stage.unresolved_items || []).some(x=>x.type==='blocker'), '阶段包含 blocker');
|
|
144
|
+
validateConsumerRoutes(handoff,stage);
|
|
101
145
|
const indexes=extractTraceability(strategy);
|
|
102
146
|
const sourceRoles=read(safe(root,'docs/agents/digital-human-roles.yaml'));
|
|
103
147
|
const roles=sourceApprovalPolicy(sourceRoles);
|
|
@@ -128,6 +172,19 @@ export async function inspectSource(root, handoffRef) {
|
|
|
128
172
|
ensure(record.gate_id===approval.gate_id,`批准门禁不匹配: ${key}`);
|
|
129
173
|
ensure((record.artifact_bindings || []).some(x=>x.id===(ref.id||ref.baseline_id) && x.version===ref.version && x.digest===actual),`批准记录未绑定当前资产: ${key}`);
|
|
130
174
|
}
|
|
175
|
+
if (roles.user_decision_policy.required_capabilities?.includes('strategic-decision-reuse-v1')) {
|
|
176
|
+
const record=read(safe(root,config.approvals.handoff.record_ref));
|
|
177
|
+
const scope=read(safe(root,record.subject_ref));
|
|
178
|
+
ensure(scope.kind==='strategic-delivery-scope' && scope.schema_version===1, '交接必须绑定独立的交付范围清单');
|
|
179
|
+
const {package_export,status,...delivery}=handoff;
|
|
180
|
+
ensure(scope.delivery_ref && own(read(safe(root,scope.delivery_ref)),delivery), '交付范围清单未覆盖当前交接内容或风险');
|
|
181
|
+
ensure(scope.assets?.some(asset=>asset.ref===scope.delivery_ref && asset.digest===hash(readFileSync(safe(root,scope.delivery_ref)))), '交接内容快照缺少批准覆盖');
|
|
182
|
+
for (const [key,ref] of Object.entries(handoff.source)) {
|
|
183
|
+
const file=key==='visual_baseline_ref'?`${ref.persisted_ref}/${ref.manifest_ref}`:ref.persisted_ref;
|
|
184
|
+
const decisionBoundaries=key==='business_ticket_set_ref'?['gate.spec-baseline-approved',terminalGate]:[expectedGates[key]];
|
|
185
|
+
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
|
+
}
|
|
187
|
+
}
|
|
131
188
|
const receipt=previewFiles(root,config.prototype);
|
|
132
189
|
ensure(handoff.source.visual_baseline_ref.case_ids.every(id=>receipt.case_ids.includes(id)), '离线验证未覆盖视觉基线 case');
|
|
133
190
|
return {handoff,config,indexes,strategy,stage};
|
|
@@ -140,7 +197,7 @@ function collect(root, handoffRef, handoff, config) {
|
|
|
140
197
|
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); };
|
|
141
198
|
function refs(value,key='') {
|
|
142
199
|
if(Array.isArray(value)) { if(key==='evidence_refs') value.forEach(enqueue); else value.forEach(v=>refs(v,key)); }
|
|
143
|
-
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); }
|
|
200
|
+
else if(value && typeof value==='object') for(const [k,v]of Object.entries(value)) { if(['approval_ref','persisted_ref','user_decision_ref','decision_reuse_ref','scope_ref','subject_ref','delivery_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); }
|
|
144
201
|
}
|
|
145
202
|
let totalBytes = 0;
|
|
146
203
|
while(queue.length) {
|
|
@@ -251,12 +308,46 @@ export async function importBundle({bundle,targetRoot}) {
|
|
|
251
308
|
mkdirSync(path.dirname(dest),{recursive:true});const stage=mkdtempSync(path.join(path.dirname(dest),'.import-staging-'));
|
|
252
309
|
try {
|
|
253
310
|
cpSync(b.root,path.join(stage,'package'),{recursive:true,errorOnExist:true,force:false});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
311
|
+
let receipt;
|
|
312
|
+
if(b.handoff.schema_version===3){
|
|
313
|
+
receipt={schema_version:1,bundle_id:b.manifest.bundle_id,version:b.manifest.version,bundle_digest:b.manifest.bundle_digest,package_ref:`${rel}/package`,target_context_digest:context.document_digest,status:'pending-context-reconciliation'};
|
|
314
|
+
write(stage,'import-receipt.json',json(receipt));write(stage,'context-reconciliation-draft.json',json({schema_version:1,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,target_context_digest:context.document_digest,terms}));
|
|
315
|
+
write(stage,'tactical-traceability-draft.json',json({schema_version:1,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows}));
|
|
316
|
+
write(stage,'upstream-change-impact.json',json({schema_version:1,previous_bundle:b.manifest.previous_bundle,changed_source_ids:[...b.changes.updated,...b.changes.removed],added_source_ids:b.changes.added,status:'requires-lifecycle-reconciliation',policy:'mark-dependent-tactical-and-slice-contracts-stale; unknown-dependency-blocks-all'}));
|
|
317
|
+
}else{
|
|
318
|
+
const profileRef=path.join(target,'docs/process/harness-profile.yaml');
|
|
319
|
+
const profile=existsSync(profileRef)?read(profileRef):null;
|
|
320
|
+
const capabilities=profile?.handoff?.consumer_capabilities||CAPABILITIES;
|
|
321
|
+
ensure(Array.isArray(capabilities)&&capabilities.length&&capabilities.every(capability=>CAPABILITIES.includes(capability)),'目标 profile 未声明受支持的消费者能力');
|
|
322
|
+
const selected=b.handoff.consumer_routes.filter(route=>capabilities.includes(route.capability));
|
|
323
|
+
ensure(selected.length===capabilities.length,'目标 profile 的消费者能力在 Handoff v4 中缺失');
|
|
324
|
+
const commonRefs=[`${rel}/context-reconciliation-draft.json`,`${rel}/upstream-change-impact.json`];
|
|
325
|
+
const routeRecords=selected.map(route=>{
|
|
326
|
+
const artifactRefs=[...commonRefs];
|
|
327
|
+
if(route.activation!=='not-applicable'&&route.capability==='backend-technical-design')artifactRefs.push(`${rel}/backend-technical-traceability-draft.json`);
|
|
328
|
+
if(route.activation!=='not-applicable'&&route.capability==='frontend-engineering-design')artifactRefs.push(`${rel}/frontend-strategic-preflight-draft.json`,`${rel}/frontend-traceability-draft.json`);
|
|
329
|
+
if(route.activation!=='not-applicable'&&route.capability==='delivery-coordination')artifactRefs.push(`${rel}/consumer-status-index-draft.json`);
|
|
330
|
+
return {route_id:route.route_id,capability:route.capability,activation:route.activation,artifact_refs:artifactRefs};
|
|
331
|
+
});
|
|
332
|
+
receipt={schema_version:2,bundle_id:b.manifest.bundle_id,version:b.manifest.version,bundle_digest:b.manifest.bundle_digest,package_ref:`${rel}/package`,target_context_digest:context.document_digest,target_profile_id:profile?.profile_id||'yss-full-lifecycle',selected_consumer_capabilities:capabilities,routes:routeRecords,status:'pending-context-reconciliation'};
|
|
333
|
+
schema(receipt,'docs/process/schemas/strategic-handoff-import-receipt.schema.json');
|
|
334
|
+
write(stage,'import-receipt.json',json(receipt));
|
|
335
|
+
write(stage,'context-reconciliation-draft.json',json({schema_version:2,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,target_context_digest:context.document_digest,route_ids:selected.map(route=>route.route_id),terms}));
|
|
336
|
+
write(stage,'upstream-change-impact.json',json({schema_version:2,previous_bundle:b.manifest.previous_bundle,routes:selected.map(route=>({route_id:route.route_id,capability:route.capability,activation:route.activation,changed_source_ids:[...b.changes.updated,...b.changes.removed],added_source_ids:b.changes.added,status:'requires-lifecycle-reconciliation'})),policy:'mark-only-known-dependent-contracts-stale; unknown-dependency-or-design-basis-change-blocks-all'}));
|
|
337
|
+
const backendRoute=b.handoff.consumer_routes.find(route=>route.capability==='backend-technical-design');
|
|
338
|
+
const frontendRoute=b.handoff.consumer_routes.find(route=>route.capability==='frontend-engineering-design');
|
|
339
|
+
const coordinationRoute=b.handoff.consumer_routes.find(route=>route.capability==='delivery-coordination');
|
|
340
|
+
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
|
+
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:'',visual_baseline_ref:`${rel}/package/payload/files/${b.handoff.source.visual_baseline_ref.persisted_ref}/${b.handoff.source.visual_baseline_ref.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};
|
|
343
|
+
schema(preflight,'docs/process/schemas/frontend-strategic-preflight.schema.json');
|
|
344
|
+
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
|
+
}
|
|
346
|
+
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:[]}))}));
|
|
347
|
+
}
|
|
258
348
|
ensure(!existsSync(dest),'导入并发冲突');renameSync(stage,dest);
|
|
259
|
-
|
|
349
|
+
const traceabilityRef=b.handoff.schema_version===3?`${rel}/tactical-traceability-draft.json`:receipt.routes.find(route=>route.capability==='backend-technical-design')?.artifact_refs.find(ref=>ref.endsWith('backend-technical-traceability-draft.json'))||receipt.routes.find(route=>route.capability==='frontend-engineering-design')?.artifact_refs.find(ref=>ref.endsWith('frontend-traceability-draft.json'));
|
|
350
|
+
return {result:'imported-pending-reconciliation',receipt_ref:`${rel}/import-receipt.json`,...(traceabilityRef?{traceability_ref:traceabilityRef}:{})};
|
|
260
351
|
}finally{rmSync(stage,{recursive:true,force:true});}
|
|
261
352
|
});
|
|
262
353
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { assertUserDecisionRequirement, decisionIO, decisionDigest, decisionError } from './user-decision.mjs';
|
|
2
|
+
|
|
3
|
+
export const DECISION_REUSE_CAPABILITY = 'strategic-decision-reuse-v1';
|
|
4
|
+
const fail = detail => decisionError('user-decision-reuse-invalid', detail);
|
|
5
|
+
const sameSet = (a,b) => Array.isArray(a) && Array.isArray(b) && a.length === new Set(a).size && b.length === new Set(b).size && JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
|
|
6
|
+
|
|
7
|
+
// A reuse record is a coverage proof, never a new reply or a substitute for one.
|
|
8
|
+
export function assertApprovalUserDecision(record, roles, options = {}) {
|
|
9
|
+
const policy = roles.user_decision_policy;
|
|
10
|
+
const io = decisionIO(options);
|
|
11
|
+
if (policy.required_capabilities?.some(id => id !== DECISION_REUSE_CAPABILITY)) fail('接收工具不支持源用户决定策略,请升级');
|
|
12
|
+
if (!record.decision_reuse_ref) return assertUserDecisionRequirement({boundary:record.gate_id,subject_ref:record.subject_ref,scope:record.approval_scope,user_decision_ref:record.user_decision_ref}, {...options,rolesDoc:roles});
|
|
13
|
+
if (record.gate_id !== 'gate.strategic-design-handoff-approved' || !policy.required_capabilities?.includes(DECISION_REUSE_CAPABILITY)) fail('未登记的复用边界或能力');
|
|
14
|
+
const reuse = io.document(record.decision_reuse_ref);
|
|
15
|
+
if (reuse.schema_version !== 1 || reuse.kind !== DECISION_REUSE_CAPABILITY || reuse.target_gate !== record.gate_id || reuse.scope_ref !== record.subject_ref || !sameSet(reuse.scope, record.approval_scope)) fail('目标边界、资产或范围不匹配');
|
|
16
|
+
const manifest = io.document(reuse.scope_ref);
|
|
17
|
+
if (manifest.schema_version !== 1 || manifest.kind !== 'strategic-delivery-scope' || !Array.isArray(manifest.assets) || !manifest.assets.length || !Array.isArray(manifest.risks) || !Array.isArray(manifest.conditions)) fail('交付范围清单不完整');
|
|
18
|
+
if (!Array.isArray(reuse.requirements) || !reuse.requirements.length) fail('缺少原决定');
|
|
19
|
+
const approved = [];
|
|
20
|
+
for (const requirement of reuse.requirements) {
|
|
21
|
+
const result = assertUserDecisionRequirement(requirement,{...options,rolesDoc:roles});
|
|
22
|
+
const source = io.document(requirement.user_decision_ref);
|
|
23
|
+
for (const item of source.request.items.filter(item => result.validated.some(v => v.item_id === item.id))) approved.push({item, requirement});
|
|
24
|
+
}
|
|
25
|
+
const identities = new Set();
|
|
26
|
+
for (const asset of manifest.assets) {
|
|
27
|
+
if (!asset.ref || !asset.version || !asset.boundary || !Array.isArray(asset.scope) || !asset.scope.length || identities.has(`${asset.boundary}:${asset.ref}`)) fail('资产缺少身份、专业边界、范围或重复');
|
|
28
|
+
identities.add(`${asset.boundary}:${asset.ref}`);
|
|
29
|
+
if (decisionDigest(io.bytes(asset.ref)) !== asset.digest) fail(`资产已变化: ${asset.ref}`);
|
|
30
|
+
const covering = approved.filter(({item,requirement}) => item.boundary === asset.boundary && asset.scope.every(s => requirement.scope.includes(s)) && [item.subject,...item.basis].some(a => a.ref === asset.ref && a.version === asset.version && a.digest === asset.digest));
|
|
31
|
+
if (!covering.length) fail(`没有原决定覆盖: ${asset.ref} / ${asset.boundary}`);
|
|
32
|
+
}
|
|
33
|
+
const coveredScope = new Set(manifest.assets.flatMap(asset => asset.scope));
|
|
34
|
+
if (!reuse.scope.length || reuse.scope.some(s => !coveredScope.has(s))) fail('交付范围超出资产批准范围');
|
|
35
|
+
for (const risk of manifest.risks) if (!approved.some(({item}) => item.risks.includes(risk))) fail(`新增未接受风险: ${risk}`);
|
|
36
|
+
for (const condition of manifest.conditions) if (!approved.some(({item}) => item.next_actions.includes(condition))) fail(`新增授权条件: ${condition}`);
|
|
37
|
+
return {reused:true,manifest,validated:approved.map(({item})=>({item_id:item.id,principal_ref:item.responder_ref}))};
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { verifyConsumerFeedback, verifyFeedbackAdjudication } from './lib/strategic-feedback.mjs';
|
|
4
|
+
|
|
5
|
+
try{
|
|
6
|
+
const [command,...args]=process.argv.slice(2);const {values,positionals}=parseArgs({args,options:{root:{type:'string',default:process.cwd()}},allowPositionals:true,strict:true});
|
|
7
|
+
if(positionals.length!==1||!['verify-feedback','verify-adjudication'].includes(command))throw new TypeError('用法: scripts/strategic-feedback <verify-feedback|verify-adjudication> --root <target> <contract.json>');
|
|
8
|
+
const result=command==='verify-feedback'?await verifyConsumerFeedback({root:values.root,feedbackRef:positionals[0]}):await verifyFeedbackAdjudication({root:values.root,adjudicationRef:positionals[0]});
|
|
9
|
+
process.stdout.write(`${JSON.stringify(result,null,2)}\n`);
|
|
10
|
+
}catch(error){process.stderr.write(`${error.message}\n`);process.exitCode=1;}
|