create-yss-spec 3.1.0 → 3.1.3
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 +117 -91
- package/bin/create-yss-spec.js +11 -3
- package/package.json +9 -2
- package/src/api/_sync-service.js +191 -0
- package/src/api/index.js +22 -0
- package/src/api/project-diff.js +9 -0
- package/src/api/project-doctor.js +10 -0
- package/src/api/template-apply.js +10 -0
- package/src/api/template-plan.js +9 -0
- package/src/cli/args.js +60 -0
- package/src/cli/error-output.js +52 -0
- package/src/cli/flags.js +9 -0
- package/src/cli/help.js +95 -0
- package/src/cli/index.js +82 -0
- package/src/cli/plan-output.js +61 -0
- package/src/cli/prompts.js +91 -0
- package/src/cli/router.js +41 -0
- package/src/cli.js +5 -2253
- package/src/commands/attach.js +280 -0
- package/src/commands/diff.js +23 -0
- package/src/commands/doctor.js +417 -0
- package/src/commands/init.js +193 -0
- package/src/commands/sync.js +146 -0
- package/src/commands/update.js +14 -0
- package/src/contracts/apply-result-v1.json +45 -0
- package/src/contracts/customization-policy-v1.json +28 -0
- package/src/contracts/doctor-report-v1.json +29 -0
- package/src/contracts/error-envelope-v1.json +24 -0
- package/src/contracts/generator-policy-v1.json +24 -0
- package/src/contracts/ownership-policy-v1.json +40 -0
- package/src/contracts/plan-schema-v1.json +115 -0
- package/src/family-identity.js +365 -0
- package/src/family-runtime.js +23 -0
- package/src/filesystem/apply-plan.js +44 -0
- package/src/filesystem/copy-path.js +28 -0
- package/src/filesystem/path-utils.js +67 -0
- package/src/filesystem/transaction-runner.js +41 -0
- package/src/filesystem/transaction.js +153 -0
- package/src/git/submodule.js +115 -0
- package/src/git/worktree.js +69 -0
- package/src/template/attach-planner-runtime.js +51 -0
- package/src/template/attach-planner.js +160 -0
- package/src/template/instance-runtime.js +463 -0
- package/src/template/lifecycle-metadata.js +99 -0
- package/src/template/lifecycle-policy.js +129 -0
- package/src/template/lifecycle-runtime.js +90 -0
- package/src/template/migration-planner.js +95 -0
- package/src/template/migration-runtime.js +247 -0
- package/src/template/ownership-metadata.js +79 -0
- package/src/template/ownership-policy.js +149 -0
- package/src/template/ownership-runtime.js +109 -0
- package/src/template/plan-schema.js +36 -0
- package/src/template/sync-planner-runtime.js +57 -0
- package/src/template/sync-planner.js +206 -0
- package/src/template/verification-runtime.js +154 -0
- package/src/validation/identity.js +41 -0
- package/src/validation/metadata.js +105 -0
- package/src/validation/security.js +67 -0
- package/src/validation/snapshot.js +47 -0
- package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
- package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.agents/skills/yss-ui/SKILL.md +2 -0
- package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.claude/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.claude/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.claude/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.claude/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.claude/skills/yss-ui/SKILL.md +2 -0
- package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.codex/skills/yss-ui/SKILL.md +2 -0
- package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.cursor/skills/yss-ui/SKILL.md +2 -0
- package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.pi/skills/yss-ui/SKILL.md +2 -0
- package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.qoder/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.qoder/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.qoder/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.qoder/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.qoder/skills/yss-ui/SKILL.md +2 -0
- package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
- package/template/.trae/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.trae/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
- package/template/.trae/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
- package/template/.trae/skills/yss-product-lifecycle/SKILL.md +2 -0
- package/template/.trae/skills/yss-ui/SKILL.md +2 -0
- package/template/CONTEXT.md +2 -0
- package/template/README.md +4 -0
- package/template/docs/agents/digital-human-roles.yaml +1 -0
- package/template/docs/process/frontend-backend-delivery.md +72 -0
- package/template/docs/process/implementation-repo-integration.md +2 -0
- package/template/docs/process/lifecycle-artifact-map.md +1 -0
- package/template/docs/process/lifecycle-registry-baseline.json +2 -1
- package/template/docs/process/lifecycle-registry.yaml +5 -0
- package/template/docs/process/schemas/backend-delivery-verification.schema.json +113 -0
- package/template/docs/process/schemas/backend-delivery.schema.json +301 -0
- package/template/docs/process/schemas/digital-human-task-package.schema.json +8 -0
- package/template/docs/process/schemas/frontend-delivery-acceptance.schema.json +200 -0
- package/template/docs/process/schemas/frontend-implementation-evidence.schema.json +3 -0
- package/template/docs/process/template-verification-profiles.yaml +6 -0
- 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 +20 -206
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +63 -963
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +11 -14
- 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 +127 -0
- package/template/scripts/backend-delivery +14 -0
- package/template/scripts/fixtures/backend-delivery/revision-server.mjs +11 -0
- package/template/scripts/instantiate-harness +69 -0
- package/template/scripts/lib/backend-delivery.mjs +164 -0
- package/template/scripts/lib/frontend-delivery-boundary.mjs +49 -0
- package/template/scripts/lib/frontend-delivery.mjs +73 -0
- package/template/scripts/lib/harness-execution-scope.mjs +52 -0
- package/template/scripts/lib/implementation-contract-compiler.mjs +12 -2
- package/template/scripts/lib/lifecycle-transition.mjs +7 -0
- package/template/scripts/lib/strategic-handoff-consumption.mjs +14 -7
- package/template/scripts/lib/strategic-handoff.mjs +1 -1
- package/template/scripts/lib/task-package.mjs +7 -0
- package/template/scripts/sync-strategic-handoff-tools +3 -1
- package/template/scripts/verify-frontend-delivery +9 -0
- package/template/scripts/verify-frontend-delivery-scenarios +131 -0
- package/template/scripts/verify-frontend-implementation-evidence +2 -0
- package/template/skills-lock.json +11 -11
- package/template.manifest.json +43 -1
- package/template.snapshot.json +5 -5
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const { applyOwnershipToOperations } = require("./ownership-runtime");
|
|
7
|
+
const {
|
|
8
|
+
validateCustomizationPolicy,
|
|
9
|
+
resolveCustomizationStrategy,
|
|
10
|
+
validateGeneratorPolicy,
|
|
11
|
+
resolveGenerator,
|
|
12
|
+
} = require("./lifecycle-policy");
|
|
13
|
+
|
|
14
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "../..");
|
|
15
|
+
const MANIFEST_PATH = path.join(PACKAGE_ROOT, "template.manifest.json");
|
|
16
|
+
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
|
|
17
|
+
|
|
18
|
+
const CUSTOMIZATION_POLICY = validateCustomizationPolicy(
|
|
19
|
+
manifest.customizationPolicy || {
|
|
20
|
+
version: 1,
|
|
21
|
+
defaultStrategy: "replace-with-force",
|
|
22
|
+
rules: [],
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
const GENERATOR_POLICY = validateGeneratorPolicy(
|
|
26
|
+
manifest.generatorPolicy || { version: 1, rules: [] },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function applyLifecycleToOperations(operations = []) {
|
|
30
|
+
return applyOwnershipToOperations(operations).map((operation) => {
|
|
31
|
+
if (operation.ownership === "managed-customizable") {
|
|
32
|
+
return {
|
|
33
|
+
...operation,
|
|
34
|
+
mergeStrategy:
|
|
35
|
+
operation.mergeStrategy ||
|
|
36
|
+
resolveCustomizationStrategy(
|
|
37
|
+
operation.relativePath,
|
|
38
|
+
CUSTOMIZATION_POLICY,
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (operation.ownership === "generated") {
|
|
44
|
+
const generator = resolveGenerator(operation.relativePath, GENERATOR_POLICY);
|
|
45
|
+
return generator
|
|
46
|
+
? {
|
|
47
|
+
...operation,
|
|
48
|
+
generatorId: operation.generatorId || generator.generatorId,
|
|
49
|
+
generatorVersion:
|
|
50
|
+
operation.generatorVersion || generator.generatorVersion,
|
|
51
|
+
}
|
|
52
|
+
: operation;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return operation;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function decoratePlanWithLifecycle(plan, desiredOperations = []) {
|
|
60
|
+
const operations = applyLifecycleToOperations(desiredOperations);
|
|
61
|
+
const byPath = new Map(
|
|
62
|
+
operations.map((operation) => [operation.relativePath, operation]),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const decorate = (item) => {
|
|
66
|
+
const operation = byPath.get(item.path);
|
|
67
|
+
if (!operation) return item;
|
|
68
|
+
const result = { ...item };
|
|
69
|
+
if (operation.mergeStrategy) result.mergeStrategy = operation.mergeStrategy;
|
|
70
|
+
if (operation.generatorId) result.generatorId = operation.generatorId;
|
|
71
|
+
if (operation.generatorVersion) {
|
|
72
|
+
result.generatorVersion = operation.generatorVersion;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...plan,
|
|
79
|
+
changes: (plan.changes || []).map(decorate),
|
|
80
|
+
conflicts: (plan.conflicts || []).map(decorate),
|
|
81
|
+
unsafe: (plan.unsafe || []).map(decorate),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
CUSTOMIZATION_POLICY,
|
|
87
|
+
GENERATOR_POLICY,
|
|
88
|
+
applyLifecycleToOperations,
|
|
89
|
+
decoratePlanWithLifecycle,
|
|
90
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function createMigrationPlan(targetDir) {
|
|
4
|
+
return {
|
|
5
|
+
targetDir,
|
|
6
|
+
operations: [],
|
|
7
|
+
legacy: [],
|
|
8
|
+
conflicts: [],
|
|
9
|
+
unsafe: [],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isBlockedMigration(plan) {
|
|
14
|
+
return Boolean(
|
|
15
|
+
plan &&
|
|
16
|
+
((Array.isArray(plan.conflicts) && plan.conflicts.length > 0) ||
|
|
17
|
+
(Array.isArray(plan.unsafe) && plan.unsafe.length > 0)),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addUnsafe(plan, path, reason) {
|
|
22
|
+
plan.unsafe.push({ path, reason });
|
|
23
|
+
return plan;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function addConflict(plan, from, to, reason) {
|
|
27
|
+
plan.conflicts.push({ from, to, reason });
|
|
28
|
+
plan.legacy.push({ action: "conflict", from, to, reason });
|
|
29
|
+
return plan;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addMove(plan, from, to, reason) {
|
|
33
|
+
plan.operations.push({ kind: "move", from, to, reason });
|
|
34
|
+
plan.legacy.push({ action: "move", from, to, reason });
|
|
35
|
+
return plan;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function addRemove(plan, path, reason, metadata = {}) {
|
|
39
|
+
plan.operations.push({ kind: "remove", path, reason });
|
|
40
|
+
plan.legacy.push({
|
|
41
|
+
action: metadata.action || "remove",
|
|
42
|
+
from: path,
|
|
43
|
+
to: metadata.to ?? null,
|
|
44
|
+
reason,
|
|
45
|
+
});
|
|
46
|
+
return plan;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function addReplaceWithTemplate(plan, from, to, reason) {
|
|
50
|
+
plan.operations.push({ kind: "remove", path: from, reason });
|
|
51
|
+
plan.legacy.push({
|
|
52
|
+
action: "replace-with-template",
|
|
53
|
+
from,
|
|
54
|
+
to,
|
|
55
|
+
reason,
|
|
56
|
+
});
|
|
57
|
+
return plan;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function addRemoveDuplicate(plan, from, to, reason) {
|
|
61
|
+
plan.operations.push({
|
|
62
|
+
kind: "remove",
|
|
63
|
+
path: from,
|
|
64
|
+
reason: `${reason}(新旧内容一致)`,
|
|
65
|
+
});
|
|
66
|
+
plan.legacy.push({
|
|
67
|
+
action: "remove-duplicate",
|
|
68
|
+
from,
|
|
69
|
+
to,
|
|
70
|
+
reason,
|
|
71
|
+
});
|
|
72
|
+
return plan;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function summarizeMigrationPlan(plan) {
|
|
76
|
+
return {
|
|
77
|
+
operations: plan?.operations?.length || 0,
|
|
78
|
+
legacy: plan?.legacy?.length || 0,
|
|
79
|
+
conflicts: plan?.conflicts?.length || 0,
|
|
80
|
+
unsafe: plan?.unsafe?.length || 0,
|
|
81
|
+
blocked: isBlockedMigration(plan),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
createMigrationPlan,
|
|
87
|
+
isBlockedMigration,
|
|
88
|
+
addUnsafe,
|
|
89
|
+
addConflict,
|
|
90
|
+
addMove,
|
|
91
|
+
addRemove,
|
|
92
|
+
addReplaceWithTemplate,
|
|
93
|
+
addRemoveDuplicate,
|
|
94
|
+
summarizeMigrationPlan,
|
|
95
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
|
|
6
|
+
const { treeHash } = require("../template-hash");
|
|
7
|
+
const { targetPath, pathKind } = require("../filesystem/path-utils");
|
|
8
|
+
const {
|
|
9
|
+
createMigrationPlan,
|
|
10
|
+
addUnsafe,
|
|
11
|
+
addConflict,
|
|
12
|
+
addMove,
|
|
13
|
+
addRemove,
|
|
14
|
+
addReplaceWithTemplate,
|
|
15
|
+
addRemoveDuplicate,
|
|
16
|
+
} = require("./migration-planner");
|
|
17
|
+
|
|
18
|
+
const AGENT_SKILL_ROOTS = [
|
|
19
|
+
".agents/skills",
|
|
20
|
+
".claude/skills",
|
|
21
|
+
".codex/skills",
|
|
22
|
+
".cursor/skills",
|
|
23
|
+
".pi/skills",
|
|
24
|
+
".qoder/skills",
|
|
25
|
+
".trae/skills",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const LEGACY_SKILL_MAPPINGS = [
|
|
29
|
+
["to-prd", "to-spec"],
|
|
30
|
+
["to-issues", "to-tickets"],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const LEGACY_FILE_MAPPINGS = [
|
|
34
|
+
["docs/templates/prd-template.md", "docs/templates/spec-template.md"],
|
|
35
|
+
[
|
|
36
|
+
"docs/templates/vertical-slice-issue-template.md",
|
|
37
|
+
"docs/templates/vertical-slice-ticket-template.md",
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function sha256(value) {
|
|
42
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathContentHash(absolutePath) {
|
|
46
|
+
const kind = pathKind(absolutePath);
|
|
47
|
+
if (kind === "file") return sha256(fs.readFileSync(absolutePath));
|
|
48
|
+
if (kind === "directory") return treeHash(absolutePath);
|
|
49
|
+
if (kind === "missing") return null;
|
|
50
|
+
return sha256(`${kind}:${fs.realpathSync(absolutePath)}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pathsEqual(leftPath, rightPath) {
|
|
54
|
+
const leftKind = pathKind(leftPath);
|
|
55
|
+
const rightKind = pathKind(rightPath);
|
|
56
|
+
return (
|
|
57
|
+
leftKind === rightKind &&
|
|
58
|
+
leftKind !== "missing" &&
|
|
59
|
+
pathContentHash(leftPath) === pathContentHash(rightPath)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function managedPathPrefix(relativePath, desiredPathSet) {
|
|
64
|
+
return (
|
|
65
|
+
desiredPathSet.has(relativePath) ||
|
|
66
|
+
[...desiredPathSet].some((candidate) => candidate.startsWith(`${relativePath}/`))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addMigrationDestination(from, to, reason, desiredPathSet, plan) {
|
|
71
|
+
const source = targetPath(plan.targetDir, from);
|
|
72
|
+
const destination = targetPath(plan.targetDir, to);
|
|
73
|
+
const sourceKind = pathKind(source);
|
|
74
|
+
const destinationKind = pathKind(destination);
|
|
75
|
+
|
|
76
|
+
if (sourceKind === "missing") return;
|
|
77
|
+
|
|
78
|
+
if ([sourceKind, destinationKind].includes("other")) {
|
|
79
|
+
addUnsafe(
|
|
80
|
+
plan,
|
|
81
|
+
from,
|
|
82
|
+
`${reason}:源或目标路径是符号链接或其他不可安全判断的类型`,
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (managedPathPrefix(to, desiredPathSet)) {
|
|
88
|
+
addReplaceWithTemplate(plan, from, to, reason);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (destinationKind === "missing") {
|
|
93
|
+
addMove(plan, from, to, reason);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pathsEqual(source, destination)) {
|
|
98
|
+
addRemoveDuplicate(plan, from, to, reason);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
addConflict(plan, from, to, `${reason}:迁移目标已存在且内容不一致`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addDirectoryMigration(from, to, reason, desiredPathSet, plan) {
|
|
106
|
+
const source = targetPath(plan.targetDir, from);
|
|
107
|
+
const destination = targetPath(plan.targetDir, to);
|
|
108
|
+
const sourceKind = pathKind(source);
|
|
109
|
+
const destinationKind = pathKind(destination);
|
|
110
|
+
|
|
111
|
+
if (sourceKind === "missing") return;
|
|
112
|
+
|
|
113
|
+
if (sourceKind !== "directory" || destinationKind === "other") {
|
|
114
|
+
addUnsafe(plan, from, `${reason}:源或目标路径不是可安全遍历的目录`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (destinationKind === "file") {
|
|
119
|
+
addConflict(plan, from, to, `${reason}:迁移目标是文件而不是目录`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const entries = fs.readdirSync(source).filter((entry) => entry !== ".DS_Store");
|
|
124
|
+
if (entries.length === 0) {
|
|
125
|
+
addRemove(plan, from, reason, { action: "remove-empty", to });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const childFrom = `${from}/${entry}`;
|
|
131
|
+
const childTo = `${to}/${entry}`;
|
|
132
|
+
if (pathKind(targetPath(plan.targetDir, childFrom)) === "directory") {
|
|
133
|
+
addDirectoryMigration(childFrom, childTo, reason, desiredPathSet, plan);
|
|
134
|
+
} else {
|
|
135
|
+
addMigrationDestination(childFrom, childTo, reason, desiredPathSet, plan);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
addRemove(plan, from, reason, { action: "remove-migrated-directory", to });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildLegacyMigrationPlan(
|
|
143
|
+
targetDir,
|
|
144
|
+
desiredOperations,
|
|
145
|
+
{ checkFlatTickets = true } = {},
|
|
146
|
+
) {
|
|
147
|
+
const desiredPathSet = new Set(
|
|
148
|
+
desiredOperations.map((operation) => operation.relativePath),
|
|
149
|
+
);
|
|
150
|
+
const plan = createMigrationPlan(targetDir);
|
|
151
|
+
|
|
152
|
+
for (const [oldName, newName] of LEGACY_SKILL_MAPPINGS) {
|
|
153
|
+
for (const agentRoot of AGENT_SKILL_ROOTS) {
|
|
154
|
+
addMigrationDestination(
|
|
155
|
+
`${agentRoot}/${oldName}`,
|
|
156
|
+
`${agentRoot}/${newName}`,
|
|
157
|
+
`旧 skill ${oldName} 迁移为 ${newName}`,
|
|
158
|
+
desiredPathSet,
|
|
159
|
+
plan,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const [oldPath, newPath] of LEGACY_FILE_MAPPINGS) {
|
|
165
|
+
addMigrationDestination(
|
|
166
|
+
oldPath,
|
|
167
|
+
newPath,
|
|
168
|
+
"旧模板路径迁移",
|
|
169
|
+
desiredPathSet,
|
|
170
|
+
plan,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
addDirectoryMigration(
|
|
175
|
+
"docs/requirements/issues",
|
|
176
|
+
"docs/requirements/tickets",
|
|
177
|
+
"旧 Ticket 目录迁移",
|
|
178
|
+
desiredPathSet,
|
|
179
|
+
plan,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const requirementsPath = targetPath(targetDir, "docs/requirements");
|
|
183
|
+
if (pathKind(requirementsPath) === "directory") {
|
|
184
|
+
for (const entry of fs.readdirSync(requirementsPath, { withFileTypes: true })) {
|
|
185
|
+
if (entry.isFile() && entry.name.endsWith("-prd.md")) {
|
|
186
|
+
const oldPath = `docs/requirements/${entry.name}`;
|
|
187
|
+
const newPath = `docs/requirements/${entry.name.replace(/-prd\.md$/, "-spec.md")}`;
|
|
188
|
+
addMigrationDestination(
|
|
189
|
+
oldPath,
|
|
190
|
+
newPath,
|
|
191
|
+
"旧规格文件名迁移",
|
|
192
|
+
desiredPathSet,
|
|
193
|
+
plan,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const legacyScratch = targetPath(targetDir, ".scratch");
|
|
200
|
+
if (pathKind(legacyScratch) === "directory") {
|
|
201
|
+
for (const entry of fs.readdirSync(legacyScratch, { withFileTypes: true })) {
|
|
202
|
+
if (entry.name === ".DS_Store") continue;
|
|
203
|
+
addMigrationDestination(
|
|
204
|
+
`.scratch/${entry.name}`,
|
|
205
|
+
`docs/.scratch/${entry.name}`,
|
|
206
|
+
"根 scratch 目录迁移",
|
|
207
|
+
desiredPathSet,
|
|
208
|
+
plan,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (checkFlatTickets) {
|
|
214
|
+
const legacyDirectory = "docs/requirements/tickets";
|
|
215
|
+
const directoryPath = targetPath(targetDir, legacyDirectory);
|
|
216
|
+
if (pathKind(directoryPath) === "directory") {
|
|
217
|
+
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
|
218
|
+
const unsafeEntries = entries.filter(
|
|
219
|
+
(entry) => entry.name !== ".gitkeep" && entry.name !== ".DS_Store",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (unsafeEntries.length > 0) {
|
|
223
|
+
for (const entry of unsafeEntries) {
|
|
224
|
+
addUnsafe(
|
|
225
|
+
plan,
|
|
226
|
+
`${legacyDirectory}/${entry.name}`,
|
|
227
|
+
"扁平 Ticket 无法可靠推断功能归属,不能自动迁移",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
} else if (!desiredPathSet.has(`${legacyDirectory}/.gitkeep`)) {
|
|
231
|
+
addRemove(plan, legacyDirectory, "删除空的旧 Ticket 目录", {
|
|
232
|
+
action: "remove-empty",
|
|
233
|
+
to: null,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return plan;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
AGENT_SKILL_ROOTS,
|
|
244
|
+
LEGACY_SKILL_MAPPINGS,
|
|
245
|
+
LEGACY_FILE_MAPPINGS,
|
|
246
|
+
buildLegacyMigrationPlan,
|
|
247
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const {
|
|
5
|
+
OWNERSHIP_POLICY,
|
|
6
|
+
resolveTemplateOwnership,
|
|
7
|
+
} = require("./ownership-runtime");
|
|
8
|
+
|
|
9
|
+
function ownershipPolicyHash(policy = OWNERSHIP_POLICY) {
|
|
10
|
+
return crypto
|
|
11
|
+
.createHash("sha256")
|
|
12
|
+
.update(JSON.stringify(policy))
|
|
13
|
+
.digest("hex");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function decorateMetadataOwnership(metadata, policy = OWNERSHIP_POLICY) {
|
|
17
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
18
|
+
throw new TypeError("metadata must be an object");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const managedFiles = Object.fromEntries(
|
|
22
|
+
Object.entries(metadata.managedFiles || {}).map(([relativePath, record]) => [
|
|
23
|
+
relativePath,
|
|
24
|
+
{
|
|
25
|
+
...record,
|
|
26
|
+
ownership:
|
|
27
|
+
record?.ownership || resolveTemplateOwnership(relativePath),
|
|
28
|
+
},
|
|
29
|
+
]),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...metadata,
|
|
34
|
+
ownershipPolicyVersion: policy.version,
|
|
35
|
+
ownershipPolicyHash: ownershipPolicyHash(policy),
|
|
36
|
+
managedFiles,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ownershipPolicyDrift(metadata, policy = OWNERSHIP_POLICY) {
|
|
41
|
+
const expectedVersion = policy.version;
|
|
42
|
+
const expectedHash = ownershipPolicyHash(policy);
|
|
43
|
+
const actualVersion = metadata?.ownershipPolicyVersion;
|
|
44
|
+
const actualHash = metadata?.ownershipPolicyHash;
|
|
45
|
+
|
|
46
|
+
if (actualVersion === undefined || actualHash === undefined) {
|
|
47
|
+
return {
|
|
48
|
+
state: "missing",
|
|
49
|
+
expectedVersion,
|
|
50
|
+
expectedHash,
|
|
51
|
+
actualVersion: actualVersion ?? null,
|
|
52
|
+
actualHash: actualHash ?? null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (actualVersion !== expectedVersion || actualHash !== expectedHash) {
|
|
57
|
+
return {
|
|
58
|
+
state: "drift",
|
|
59
|
+
expectedVersion,
|
|
60
|
+
expectedHash,
|
|
61
|
+
actualVersion,
|
|
62
|
+
actualHash,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
state: "matched",
|
|
68
|
+
expectedVersion,
|
|
69
|
+
expectedHash,
|
|
70
|
+
actualVersion,
|
|
71
|
+
actualHash,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
ownershipPolicyHash,
|
|
77
|
+
decorateMetadataOwnership,
|
|
78
|
+
ownershipPolicyDrift,
|
|
79
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const OWNERSHIP_POLICY_VERSION = 1;
|
|
4
|
+
const OWNERSHIP_TYPES = Object.freeze([
|
|
5
|
+
"managed",
|
|
6
|
+
"managed-customizable",
|
|
7
|
+
"generated",
|
|
8
|
+
"user-owned",
|
|
9
|
+
"protected",
|
|
10
|
+
]);
|
|
11
|
+
const OWNERSHIP_TYPE_SET = new Set(OWNERSHIP_TYPES);
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OWNERSHIP_POLICY = Object.freeze({
|
|
14
|
+
version: OWNERSHIP_POLICY_VERSION,
|
|
15
|
+
default: "managed",
|
|
16
|
+
rules: [],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function normalizePolicyPath(value) {
|
|
20
|
+
return String(value || "")
|
|
21
|
+
.replaceAll("\\", "/")
|
|
22
|
+
.replace(/^\.\//, "")
|
|
23
|
+
.replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isOwnershipType(value) {
|
|
27
|
+
return OWNERSHIP_TYPE_SET.has(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateOwnershipPattern(pattern) {
|
|
31
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
32
|
+
throw new Error("ownership rule pattern 不能为空");
|
|
33
|
+
}
|
|
34
|
+
const normalized = normalizePolicyPath(pattern);
|
|
35
|
+
if (
|
|
36
|
+
!normalized ||
|
|
37
|
+
normalized === "." ||
|
|
38
|
+
normalized.startsWith("../") ||
|
|
39
|
+
normalized.includes("/../") ||
|
|
40
|
+
normalized.startsWith("/")
|
|
41
|
+
) {
|
|
42
|
+
throw new Error(`ownership rule pattern 越界:${pattern}`);
|
|
43
|
+
}
|
|
44
|
+
if (normalized.includes("**") && !normalized.endsWith("/**")) {
|
|
45
|
+
throw new Error(`ownership rule 仅支持末尾 /**:${pattern}`);
|
|
46
|
+
}
|
|
47
|
+
if (normalized.includes("*") && !normalized.endsWith("/*") && !normalized.endsWith("/**")) {
|
|
48
|
+
throw new Error(`ownership rule 仅支持 exact、/* 或 /**:${pattern}`);
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateOwnershipPolicy(policy) {
|
|
54
|
+
const value = policy || DEFAULT_OWNERSHIP_POLICY;
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
56
|
+
throw new Error("ownershipPolicy 必须是 JSON 对象");
|
|
57
|
+
}
|
|
58
|
+
if (value.version !== OWNERSHIP_POLICY_VERSION) {
|
|
59
|
+
throw new Error(`不支持的 ownershipPolicy version:${value.version}`);
|
|
60
|
+
}
|
|
61
|
+
if (!isOwnershipType(value.default)) {
|
|
62
|
+
throw new Error(`ownershipPolicy default 非法:${value.default}`);
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(value.rules)) {
|
|
65
|
+
throw new Error("ownershipPolicy rules 必须是数组");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const rules = value.rules.map((rule, index) => {
|
|
70
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
71
|
+
throw new Error(`ownershipPolicy rules[${index}] 必须是对象`);
|
|
72
|
+
}
|
|
73
|
+
const pattern = validateOwnershipPattern(rule.pattern);
|
|
74
|
+
if (seen.has(pattern)) {
|
|
75
|
+
throw new Error(`ownershipPolicy pattern 重复:${pattern}`);
|
|
76
|
+
}
|
|
77
|
+
seen.add(pattern);
|
|
78
|
+
if (!isOwnershipType(rule.ownership)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`ownershipPolicy rules[${index}].ownership 非法:${rule.ownership}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return { pattern, ownership: rule.ownership };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
version: OWNERSHIP_POLICY_VERSION,
|
|
88
|
+
default: value.default,
|
|
89
|
+
rules,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function matchesOwnershipPattern(relativePath, pattern) {
|
|
94
|
+
const target = normalizePolicyPath(relativePath);
|
|
95
|
+
const normalizedPattern = validateOwnershipPattern(pattern);
|
|
96
|
+
|
|
97
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
98
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
99
|
+
return target === prefix || target.startsWith(`${prefix}/`);
|
|
100
|
+
}
|
|
101
|
+
if (normalizedPattern.endsWith("/*")) {
|
|
102
|
+
const prefix = normalizedPattern.slice(0, -2);
|
|
103
|
+
if (!target.startsWith(`${prefix}/`)) return false;
|
|
104
|
+
return !target.slice(prefix.length + 1).includes("/");
|
|
105
|
+
}
|
|
106
|
+
return target === normalizedPattern;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveOwnership(relativePath, policy = DEFAULT_OWNERSHIP_POLICY) {
|
|
110
|
+
const validated = validateOwnershipPolicy(policy);
|
|
111
|
+
for (const rule of validated.rules) {
|
|
112
|
+
if (matchesOwnershipPattern(relativePath, rule.pattern)) {
|
|
113
|
+
return rule.ownership;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return validated.default;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isTemplateManagedOwnership(ownership) {
|
|
120
|
+
return ["managed", "managed-customizable", "generated"].includes(ownership);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isWriteProtectedOwnership(ownership) {
|
|
124
|
+
return ownership === "user-owned" || ownership === "protected";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ownershipWriteViolation(operation) {
|
|
128
|
+
const ownership = operation?.ownership || "managed";
|
|
129
|
+
if (!isWriteProtectedOwnership(ownership)) return null;
|
|
130
|
+
const path = operation?.relativePath || operation?.path || "unknown";
|
|
131
|
+
return ownership === "protected"
|
|
132
|
+
? `${path} 被 ownership policy 标记为 protected,CLI 不得写入或覆盖`
|
|
133
|
+
: `${path} 被 ownership policy 标记为 user-owned,不属于受管模板文件`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
OWNERSHIP_POLICY_VERSION,
|
|
138
|
+
OWNERSHIP_TYPES,
|
|
139
|
+
DEFAULT_OWNERSHIP_POLICY,
|
|
140
|
+
normalizePolicyPath,
|
|
141
|
+
isOwnershipType,
|
|
142
|
+
validateOwnershipPattern,
|
|
143
|
+
validateOwnershipPolicy,
|
|
144
|
+
matchesOwnershipPattern,
|
|
145
|
+
resolveOwnership,
|
|
146
|
+
isTemplateManagedOwnership,
|
|
147
|
+
isWriteProtectedOwnership,
|
|
148
|
+
ownershipWriteViolation,
|
|
149
|
+
};
|