create-yss-spec 3.4.10 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/api/_sync-service.js +111 -18
  4. package/src/cli/args.js +1 -0
  5. package/src/cli/help.js +10 -1
  6. package/src/cli/index.js +3 -0
  7. package/src/cli/prompts.js +5 -1
  8. package/src/cli/router.js +2 -0
  9. package/src/commands/attach.js +12 -2
  10. package/src/commands/init.js +2 -0
  11. package/src/commands/skills.js +253 -0
  12. package/src/commands/sync.js +3 -0
  13. package/src/family-identity.js +1 -1
  14. package/src/template/asset-runtime.js +220 -0
  15. package/src/template/distribution-runtime.js +165 -0
  16. package/src/template/instance-runtime.js +76 -29
  17. package/src/template/ownership-policy.js +17 -1
  18. package/src/template/prune-planner.js +6 -1
  19. package/src/template/sync-planner.js +3 -0
  20. package/src/template/verification-runtime.js +56 -1
  21. package/src/validation/metadata.js +24 -1
  22. package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
  23. package/template/.agents/skills/yss-cache/SKILL.md +6 -5
  24. package/template/.agents/skills/yss-cache/references/annotations.md +3 -1
  25. package/template/.agents/skills/yss-cache/references/architecture.md +4 -2
  26. package/template/.agents/skills/yss-cache/references/configuration.md +6 -3
  27. package/template/.agents/skills/yss-cache/references/jetcache.md +21 -0
  28. package/template/.agents/skills/yss-cache/references/redis-fallback.md +5 -3
  29. package/template/.agents/skills/yss-cache/references/verification.md +10 -4
  30. package/template/.agents/skills/yss-cache/scripts/verify-cache-component.sh +26 -20
  31. package/template/.agents/skills/yss-distributed-id/SKILL.md +14 -58
  32. package/template/.agents/skills/yss-distributed-id/agents/openai.yaml +2 -2
  33. package/template/.agents/skills/yss-distributed-id/references/README.md +11 -47
  34. package/template/.agents/skills/yss-mybatis/SKILL.md +14 -2
  35. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +5 -4
  36. package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +8 -8
  37. package/template/.agents/skills/yss-prototype-stage/SKILL.md +2 -2
  38. package/template/.agents/skills/yss-repository/SKILL.md +1 -0
  39. package/template/.codex/skills/yss-cache/SKILL.md +6 -5
  40. package/template/.codex/skills/yss-cache/references/annotations.md +3 -1
  41. package/template/.codex/skills/yss-cache/references/architecture.md +4 -2
  42. package/template/.codex/skills/yss-cache/references/configuration.md +6 -3
  43. package/template/.codex/skills/yss-cache/references/jetcache.md +21 -0
  44. package/template/.codex/skills/yss-cache/references/redis-fallback.md +5 -3
  45. package/template/.codex/skills/yss-cache/references/verification.md +10 -4
  46. package/template/.codex/skills/yss-cache/scripts/verify-cache-component.sh +26 -20
  47. package/template/.codex/skills/yss-distributed-id/SKILL.md +14 -58
  48. package/template/.codex/skills/yss-distributed-id/agents/openai.yaml +2 -2
  49. package/template/.codex/skills/yss-distributed-id/references/README.md +11 -47
  50. package/template/.codex/skills/yss-mybatis/SKILL.md +14 -2
  51. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +5 -4
  52. package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +8 -8
  53. package/template/.codex/skills/yss-prototype-stage/SKILL.md +2 -2
  54. package/template/.codex/skills/yss-repository/SKILL.md +1 -0
  55. package/template/.cursor/skills/yss-cache/SKILL.md +6 -5
  56. package/template/.cursor/skills/yss-cache/references/annotations.md +3 -1
  57. package/template/.cursor/skills/yss-cache/references/architecture.md +4 -2
  58. package/template/.cursor/skills/yss-cache/references/configuration.md +6 -3
  59. package/template/.cursor/skills/yss-cache/references/jetcache.md +21 -0
  60. package/template/.cursor/skills/yss-cache/references/redis-fallback.md +5 -3
  61. package/template/.cursor/skills/yss-cache/references/verification.md +10 -4
  62. package/template/.cursor/skills/yss-cache/scripts/verify-cache-component.sh +26 -20
  63. package/template/.cursor/skills/yss-distributed-id/SKILL.md +14 -58
  64. package/template/.cursor/skills/yss-distributed-id/agents/openai.yaml +2 -2
  65. package/template/.cursor/skills/yss-distributed-id/references/README.md +11 -47
  66. package/template/.cursor/skills/yss-mybatis/SKILL.md +14 -2
  67. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +5 -4
  68. package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +8 -8
  69. package/template/.cursor/skills/yss-prototype-stage/SKILL.md +2 -2
  70. package/template/.cursor/skills/yss-repository/SKILL.md +1 -0
  71. package/template/.pi/skills/yss-cache/SKILL.md +6 -5
  72. package/template/.pi/skills/yss-cache/references/annotations.md +3 -1
  73. package/template/.pi/skills/yss-cache/references/architecture.md +4 -2
  74. package/template/.pi/skills/yss-cache/references/configuration.md +6 -3
  75. package/template/.pi/skills/yss-cache/references/jetcache.md +21 -0
  76. package/template/.pi/skills/yss-cache/references/redis-fallback.md +5 -3
  77. package/template/.pi/skills/yss-cache/references/verification.md +10 -4
  78. package/template/.pi/skills/yss-cache/scripts/verify-cache-component.sh +26 -20
  79. package/template/.pi/skills/yss-distributed-id/SKILL.md +14 -58
  80. package/template/.pi/skills/yss-distributed-id/agents/openai.yaml +2 -2
  81. package/template/.pi/skills/yss-distributed-id/references/README.md +11 -47
  82. package/template/.pi/skills/yss-mybatis/SKILL.md +14 -2
  83. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +5 -4
  84. package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +8 -8
  85. package/template/.pi/skills/yss-prototype-stage/SKILL.md +2 -2
  86. package/template/.pi/skills/yss-repository/SKILL.md +1 -0
  87. package/template/README.md +2 -5
  88. package/template/docs/agents/skill-migrations.md +4 -0
  89. package/template/docs/agents/yss-skill-registry.yaml +7 -6
  90. package/template/docs/design/README.md +0 -1
  91. package/template/docs/design/design.md +2 -2
  92. package/template/docs/design/templates/interaction-spec-template.md +1 -1
  93. package/template/docs/engineering/evidence/aliyun-artifact-resolution.json +446 -0
  94. package/template/scripts/design-md +2 -0
  95. package/template/scripts/lib/design-md.mjs +230 -0
  96. package/template/scripts/lib/skill-registry.mjs +9 -2
  97. package/template/scripts/lib/skill-supply-chain.mjs +33 -14
  98. package/template/scripts/verify-project-instance +11 -10
  99. package/template/skills-lock.json +11 -26
  100. package/template.manifest.json +18 -3
  101. package/template.snapshot.json +5 -5
  102. package/template/.agents/skills/grill-me/SKILL.md +0 -7
  103. package/template/.agents/skills/grill-me/agents/openai.yaml +0 -5
  104. package/template/.agents/skills/yss-distributed-id/assets/AutoIdInterceptor.java +0 -246
  105. package/template/.agents/skills/yss-distributed-id/assets/EnableDistributedId.java +0 -25
  106. package/template/.agents/skills/yss-distributed-id/assets/LeafConf.java +0 -36
  107. package/template/.codex/skills/grill-me/SKILL.md +0 -7
  108. package/template/.codex/skills/grill-me/agents/openai.yaml +0 -5
  109. package/template/.codex/skills/yss-distributed-id/assets/AutoIdInterceptor.java +0 -246
  110. package/template/.codex/skills/yss-distributed-id/assets/EnableDistributedId.java +0 -25
  111. package/template/.codex/skills/yss-distributed-id/assets/LeafConf.java +0 -36
  112. package/template/.cursor/skills/grill-me/SKILL.md +0 -7
  113. package/template/.cursor/skills/grill-me/agents/openai.yaml +0 -5
  114. package/template/.cursor/skills/yss-distributed-id/assets/AutoIdInterceptor.java +0 -246
  115. package/template/.cursor/skills/yss-distributed-id/assets/EnableDistributedId.java +0 -25
  116. package/template/.cursor/skills/yss-distributed-id/assets/LeafConf.java +0 -36
  117. package/template/.pi/skills/grill-me/SKILL.md +0 -7
  118. package/template/.pi/skills/grill-me/agents/openai.yaml +0 -5
  119. package/template/.pi/skills/yss-distributed-id/assets/AutoIdInterceptor.java +0 -246
  120. package/template/.pi/skills/yss-distributed-id/assets/EnableDistributedId.java +0 -25
  121. package/template/.pi/skills/yss-distributed-id/assets/LeafConf.java +0 -36
  122. package/template/docs/agents/yss-plugin-dependency-contract.md +0 -45
  123. package/template/docs/api/.gitkeep +0 -0
  124. package/template/docs/api/specs/.gitkeep +0 -0
  125. package/template/docs/architecture/.gitkeep +0 -0
  126. package/template/docs/design/facts/antdv-next/1.5.2/cli-help.txt +0 -38
  127. package/template/docs/design/facts/antdv-next/1.5.2/component-list.json +0 -434
  128. package/template/docs/design/facts/antdv-next/1.5.2/components/Alert/demo-basic.json +0 -7
  129. package/template/docs/design/facts/antdv-next/1.5.2/components/Alert/info.json +0 -246
  130. package/template/docs/design/facts/antdv-next/1.5.2/components/Alert/semantic.json +0 -40
  131. package/template/docs/design/facts/antdv-next/1.5.2/components/Alert/token.json +0 -32
  132. package/template/docs/design/facts/antdv-next/1.5.2/components/Button/demo-basic.json +0 -7
  133. package/template/docs/design/facts/antdv-next/1.5.2/components/Button/info.json +0 -190
  134. package/template/docs/design/facts/antdv-next/1.5.2/components/Button/semantic.json +0 -20
  135. package/template/docs/design/facts/antdv-next/1.5.2/components/Button/token.json +0 -256
  136. package/template/docs/design/facts/antdv-next/1.5.2/components/Input/demo-basic.json +0 -7
  137. package/template/docs/design/facts/antdv-next/1.5.2/components/Input/info.json +0 -460
  138. package/template/docs/design/facts/antdv-next/1.5.2/components/Input/semantic.json +0 -70
  139. package/template/docs/design/facts/antdv-next/1.5.2/components/Input/token.json +0 -123
  140. package/template/docs/design/facts/antdv-next/1.5.2/components/Modal/demo-basic.json +0 -7
  141. package/template/docs/design/facts/antdv-next/1.5.2/components/Modal/info.json +0 -646
  142. package/template/docs/design/facts/antdv-next/1.5.2/components/Modal/semantic.json +0 -50
  143. package/template/docs/design/facts/antdv-next/1.5.2/components/Modal/token.json +0 -130
  144. package/template/docs/design/facts/antdv-next/1.5.2/components/Select/demo-basic.json +0 -7
  145. package/template/docs/design/facts/antdv-next/1.5.2/components/Select/info.json +0 -640
  146. package/template/docs/design/facts/antdv-next/1.5.2/components/Select/semantic.json +0 -70
  147. package/template/docs/design/facts/antdv-next/1.5.2/components/Select/token.json +0 -172
  148. package/template/docs/design/facts/antdv-next/1.5.2/components/Table/demo-basic.json +0 -7
  149. package/template/docs/design/facts/antdv-next/1.5.2/components/Table/info.json +0 -1010
  150. package/template/docs/design/facts/antdv-next/1.5.2/components/Table/semantic.json +0 -70
  151. package/template/docs/design/facts/antdv-next/1.5.2/components/Table/token.json +0 -263
  152. package/template/docs/design/facts/antdv-next/1.5.2/components/Tag/demo-basic.json +0 -7
  153. package/template/docs/design/facts/antdv-next/1.5.2/components/Tag/info.json +0 -321
  154. package/template/docs/design/facts/antdv-next/1.5.2/components/Tag/semantic.json +0 -25
  155. package/template/docs/design/facts/antdv-next/1.5.2/components/Tag/token.json +0 -25
  156. package/template/docs/design/facts/antdv-next/1.5.2/design-md.json +0 -3
  157. package/template/docs/design/facts/antdv-next/1.5.2/manifest.json +0 -215
  158. package/template/docs/design/facts/antdv-next/1.5.2/resolution-probe.json +0 -5
  159. package/template/docs/design/prototypes/.gitkeep +0 -1
  160. package/template/docs/implementation/.gitkeep +0 -1
  161. package/template/docs/plan/IDEATION.md +0 -96
  162. package/template/docs/process/PDCA-SCRUM.md +0 -10
  163. package/template/docs/process/harness-executive-blueprint.md +0 -10
  164. package/template/docs/releases/.gitkeep +0 -1
  165. package/template/docs/requirements/README.md +0 -85
  166. package/template/docs/requirements/tickets/.gitkeep +0 -1
  167. package/template/docs/templates/agent-brief-template.md +0 -46
  168. package/template/docs/templates/architecture-proposal-template.md +0 -18
  169. package/template/docs/templates/implementation-plan-template.md +0 -16
  170. package/template/docs/templates/implementation-routing-template.md +0 -350
  171. package/template/docs/templates/requirement-freeze-template.md +0 -63
  172. package/template/docs/templates/risk-register-template.md +0 -11
  173. package/template/docs/templates/user-story-template.md +0 -20
  174. package/template/docs/testing/README.md +0 -110
@@ -8,6 +8,8 @@ const { treeHash } = require("../template-hash");
8
8
  const { targetPath, pathKind, normalizeRelativePath } = require("../filesystem/path-utils");
9
9
  const { validateTemplateSnapshot } = require("../validation/snapshot");
10
10
  const { validateTemplateMetadata } = require("../validation/metadata");
11
+ const { distributionForVariables, isIncludedInstancePath, selectedSkillLock, renderInstanceSkillSupplyChain, renderInstanceDesignSkillFile } = require("./distribution-runtime");
12
+ const { ASSET_PROFILE, assetPaths } = require("./asset-runtime");
11
13
  const {
12
14
  extractManagedGitignoreBlock,
13
15
  mergeManagedGitignoreBlock,
@@ -29,7 +31,7 @@ const TEMPLATE_MANIFEST_TEXT = fs.readFileSync(BUNDLED_MANIFEST_PATH, "utf8");
29
31
  const TEMPLATE_MANIFEST = JSON.parse(TEMPLATE_MANIFEST_TEXT);
30
32
  const TEMPLATE_METADATA_FILENAME = ".yss-template.json";
31
33
  const TEMPLATE_SOURCE = "github:iloveZzz/yss-spec-project-template";
32
- const METADATA_SCHEMA_VERSION = 2;
34
+ const METADATA_SCHEMA_VERSION = 3;
33
35
 
34
36
  const ROOT_EXCLUDED_ENTRIES = new Set(TEMPLATE_MANIFEST.excludeRootEntries);
35
37
  const ROOT_EXCLUDED_FILES = new Set(TEMPLATE_MANIFEST.excludeRootFiles);
@@ -47,8 +49,6 @@ const INIT_EXCLUDED_RELATIVE_PATHS = new Set(
47
49
  const RENDERED_RELATIVE_PATHS = new Set(TEMPLATE_MANIFEST.renderPaths);
48
50
  const EXAMPLE_DOC_PATHS = new Set(TEMPLATE_MANIFEST.exampleDocPaths);
49
51
 
50
- let bundledPathToLogicalPath = null;
51
-
52
52
  function sha256(value) {
53
53
  return crypto.createHash("sha256").update(value).digest("hex");
54
54
  }
@@ -79,16 +79,16 @@ function readTemplateSnapshot() {
79
79
  });
80
80
  }
81
81
 
82
- function logicalTemplatePath(bundledPath) {
83
- if (bundledPathToLogicalPath === null) {
84
- const snapshot = readTemplateSnapshot();
85
- bundledPathToLogicalPath = new Map(
86
- Object.entries(snapshot.encodedPaths || {}).map(([logicalPath, encodedPath]) => [
87
- normalizeRelativePath(encodedPath),
88
- normalizeRelativePath(logicalPath),
89
- ]),
90
- );
91
- }
82
+ function logicalPathMap(snapshot) {
83
+ return new Map(
84
+ Object.entries(snapshot.encodedPaths || {}).map(([logicalPath, encodedPath]) => [
85
+ normalizeRelativePath(encodedPath),
86
+ normalizeRelativePath(logicalPath),
87
+ ]),
88
+ );
89
+ }
90
+
91
+ function logicalTemplatePath(bundledPath, bundledPathToLogicalPath) {
92
92
  return (
93
93
  bundledPathToLogicalPath.get(normalizeRelativePath(bundledPath)) ||
94
94
  normalizeRelativePath(bundledPath)
@@ -143,12 +143,35 @@ function readTargetIdentity(targetDir) {
143
143
  }
144
144
 
145
145
  function renderTemplateFile(relativePath, content, variables) {
146
+ const distribution = variables.distribution || distributionForVariables(variables, BUNDLED_TEMPLATE_ROOT);
147
+ if (relativePath === "skills-lock.json") return selectedSkillLock(content, distribution);
148
+ if (relativePath === "scripts/lib/skill-supply-chain.mjs" && distribution.mode === "selected") return renderInstanceSkillSupplyChain(content);
149
+ if (distribution.mode === "selected" && /^\.(agents|codex|cursor|pi)\/skills\/yss-design-system\/(SKILL\.md|references\/data-quality-theme\.md)$/.test(relativePath)) return renderInstanceDesignSkillFile(content);
150
+ if (relativePath === "docs/engineering/backend-platforms.json" && distribution.mode === "selected") {
151
+ return content.replaceAll(".template-source/evidence/maintenance/2026-09-18-yss-backend-components/aliyun-artifact-resolution.json", "docs/engineering/evidence/aliyun-artifact-resolution.json");
152
+ }
153
+ if (relativePath === "docs/agents/backend-architecture-profiles.md" && distribution.mode === "selected") {
154
+ return content.replace(/;依据见 `\.template-source\/evidence\/maintenance\/2026-09-12-existing-project-delivery\/maven-adapters-04\.json`/, ";适配验证证据保留在模板源,项目实例须对自身工程重新验证");
155
+ }
156
+ if (relativePath === "docs/user-guide/用户手册.md" && distribution.mode === "selected") {
157
+ if (distribution.assetProfile !== ASSET_PROFILE) {
158
+ return `# ${variables.projectName} 用户手册\n\n本仓是 \`project-instance\`,用于 ${variables.businessDomain} 的研发资产。先阅读根 [AGENTS.md](../../AGENTS.md)、[CONTEXT.md](../../CONTEXT.md) 与 [生命周期资产索引](../process/lifecycle-artifact-map.md)。\n\n阶段派发前,由 Agent 运行 \`create-yss-spec skills ensure <skill-id...> --plan\` 核对依赖,再运行 \`--apply\` 安装。旧实例继续沿用既有文件分发范围;增加平台使用 \`create-yss-spec skills runtime add <codex|cursor|pi> --plan/--apply\`。CLI 快照必须与实例记录的模板提交一致;升级 CLI 后先运行 \`create-yss-spec sync\`。\n\n项目校验运行 \`scripts/verify-project-instance\`。\n`;
159
+ }
160
+ return `# ${variables.projectName} 用户手册\n\n本仓是 \`project-instance\`,用于 ${variables.businessDomain} 的研发资产。先阅读根 [AGENTS.md](../../AGENTS.md)、[CONTEXT.md](../../CONTEXT.md) 与 [生命周期资产索引](../process/lifecycle-artifact-map.md)。\n\n初始化安装分诊与 Plan 入口资产、三项入口 Skill 与所选 Agent 平台。进入后续阶段前,运行 \`create-yss-spec assets ensure <stage-id> --plan\` 核对文件,再运行 \`--apply\` 原子安装;专项 Skill 使用 \`create-yss-spec skills ensure <skill-id...> --plan/--apply\`,会补齐该 Skill 引用的实例文件。增加平台使用 \`create-yss-spec skills runtime add <codex|cursor|pi> --plan/--apply\`。CLI 快照必须与实例记录的模板提交一致;升级 CLI 后先运行 \`create-yss-spec sync\`。\n\n项目校验运行 \`scripts/verify-project-instance\`;实例 CI 应执行该命令和项目实际的构建、测试。\n`;
161
+ }
162
+ if (distribution.mode === "selected" && relativePath === "docs/design/README.md") {
163
+ return content.replace(/^.*design-system-sync\.yaml.*\n/m, "");
164
+ }
165
+ if (distribution.mode === "selected" && relativePath.startsWith("docs/user-guide/") && relativePath.endsWith(".md")) {
166
+ return content.replaceAll("[设备借用贯穿案例](设备借用贯穿案例.md)", "[项目用户手册](用户手册.md)")
167
+ .replaceAll("本仓是 `template-source`", "模板源是 `template-source`");
168
+ }
146
169
  if (relativePath === "yss-project.yaml") {
147
170
  return convertTemplateSourceToInstance(content);
148
171
  }
149
172
 
150
173
  if (relativePath === "AGENTS.md") {
151
- return content
174
+ const rendered = content
152
175
  .replace(
153
176
  /(\*\*项目名称:\*\*\s*)\[填写\]/,
154
177
  (_, prefix) => `${prefix}${variables.projectName}`,
@@ -161,10 +184,20 @@ function renderTemplateFile(relativePath, content, variables) {
161
184
  /(\*\*团队规模:\*\*\s*)\[填写\]/,
162
185
  (_, prefix) => `${prefix}${variables.teamSize}`,
163
186
  );
187
+ if (distribution.mode !== "selected") return rendered;
188
+ const base = rendered.replace(/## 4\. \`template-source\` 模板维护路由[\s\S]*?(?=## 5\.)/, "")
189
+ .replace(/\| 影响面、\`not-applicable\`、模板维护强度 \|[^\n]*\n/, "| 影响面与 `not-applicable` | `docs/process/harness-process-tailoring.md` |\n");
190
+ const guidance = distribution.assetProfile === ASSET_PROFILE
191
+ ? "## 按需阶段资产与 Skill\n\n进入后续生命周期阶段前,运行 `create-yss-spec assets ensure <stage-id> --plan` 查看完整依赖,核对后运行 `--apply`。专项任务根据 docs/agents/yss-skill-registry.yaml 选定 Skill,运行 `create-yss-spec skills ensure <skill-id...> --plan`,核对后运行 `--apply`。缺少阶段资产时先补装,不以缺文件推定门禁不适用。若 CLI 快照与实例模板提交不一致,先运行 `create-yss-spec sync`。\n"
192
+ : "## 按需 Skill\n\n此实例继续沿用原文件分发范围。阶段派发或专项任务开始前,根据 docs/agents/yss-skill-registry.yaml 选定 Skill,运行 `create-yss-spec skills ensure <skill-id...> --plan`,核对后运行 `--apply`。若 CLI 快照与实例模板提交不一致,先运行 `create-yss-spec sync`。\n";
193
+ return `${base}\n${guidance}`;
164
194
  }
165
195
 
166
196
  if (relativePath === "README.md") {
167
- return `# ${variables.projectName}\n\n本仓库用于管理 ${variables.businessDomain} 的研发资产。\n\n- 默认 Issue Tracker:${variables.issueTracker}\n- 协作入口:[AGENTS.md](./AGENTS.md)\n- 业务词汇:[CONTEXT.md](./CONTEXT.md)\n- 用户指南:[docs/user-guide/用户手册.md](./docs/user-guide/用户手册.md)\n`;
197
+ const assetGuidance = distribution.assetProfile === ASSET_PROFILE
198
+ ? "后续阶段先运行 `create-yss-spec assets ensure <stage-id> --plan` 核对依赖,再运行 `--apply`;专项 Skill 使用 `create-yss-spec skills ensure <skill-id> --plan/--apply`。"
199
+ : "专项 Skill 使用 `create-yss-spec skills ensure <skill-id> --plan/--apply`。";
200
+ return `# ${variables.projectName}\n\n本仓库用于管理 ${variables.businessDomain} 的研发资产。\n\n- 默认 Issue Tracker:${variables.issueTracker}\n- Agent 平台:${distribution.mode === "selected" ? distribution.runtimes.join(", ") : "legacy-all"}\n- 协作入口:[AGENTS.md](./AGENTS.md)\n- 业务词汇:[CONTEXT.md](./CONTEXT.md)\n- 用户指南:[docs/user-guide/用户手册.md](./docs/user-guide/用户手册.md)\n\n项目校验:\`scripts/verify-project-instance\`。${assetGuidance}\n`;
168
201
  }
169
202
 
170
203
  if (relativePath === ".gitignore") return extractManagedGitignoreBlock(content);
@@ -178,6 +211,8 @@ function buildCopyPlan(
178
211
  variables,
179
212
  relativeDir = "",
180
213
  mode = "managed",
214
+ bundledPathToLogicalPath,
215
+ distribution = { mode: "legacy-all" },
181
216
  ) {
182
217
  const operations = [];
183
218
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
@@ -188,9 +223,10 @@ function buildCopyPlan(
188
223
  const bundledRelativePath = relativeDir
189
224
  ? path.posix.join(relativeDir, entry.name)
190
225
  : entry.name;
191
- const relativePath = logicalTemplatePath(bundledRelativePath);
226
+ const relativePath = logicalTemplatePath(bundledRelativePath, bundledPathToLogicalPath);
192
227
 
193
228
  if (shouldExcludeRelativePath(relativePath, mode)) continue;
229
+ if (!isIncludedInstancePath(relativePath, distribution)) continue;
194
230
  if (!variables.includeExampleDocs && EXAMPLE_DOC_PATHS.has(relativePath)) continue;
195
231
 
196
232
  const sourcePath = path.join(sourceDir, entry.name);
@@ -205,6 +241,8 @@ function buildCopyPlan(
205
241
  variables,
206
242
  bundledRelativePath,
207
243
  mode,
244
+ bundledPathToLogicalPath,
245
+ distribution,
208
246
  ),
209
247
  );
210
248
  continue;
@@ -235,8 +273,9 @@ function buildSyncVariables(metadata) {
235
273
  issueTracker: variables.issueTracker || "github",
236
274
  includeExampleDocs:
237
275
  variables.includeExampleDocs === undefined
238
- ? true
276
+ ? !(metadata.metadataSchemaVersion >= 3)
239
277
  : Boolean(variables.includeExampleDocs),
278
+ distribution: metadata.metadataSchemaVersion >= 3 ? metadata.distribution : { mode: "legacy-all" },
240
279
  };
241
280
  }
242
281
 
@@ -254,15 +293,18 @@ function buildDesiredManagedFile(operation, variables) {
254
293
  };
255
294
  }
256
295
 
296
+ const desiredContent = fs.readFileSync(operation.sourcePath);
257
297
  return {
258
298
  ...operation,
259
- desiredContent: fs.readFileSync(operation.sourcePath),
260
- desiredHash: fileHash(operation.sourcePath),
299
+ desiredContent,
300
+ desiredHash: sha256(desiredContent),
261
301
  };
262
302
  }
263
303
 
264
- function buildDesiredManagedOperations(targetDir, variables, mode = "managed") {
265
- return buildCopyPlan(BUNDLED_TEMPLATE_ROOT, targetDir, variables, "", mode)
304
+ function buildDesiredManagedOperations(targetDir, variables, mode = "managed", snapshot = readTemplateSnapshot()) {
305
+ const distribution = variables.distribution || distributionForVariables(variables, BUNDLED_TEMPLATE_ROOT);
306
+ const effectiveDistribution = { ...distribution, assetPaths: assetPaths(BUNDLED_TEMPLATE_ROOT, distribution) };
307
+ return buildCopyPlan(BUNDLED_TEMPLATE_ROOT, targetDir, { ...variables, distribution }, "", mode, logicalPathMap(snapshot), effectiveDistribution)
266
308
  .filter((operation) => operation.type === "copy" || operation.type === "render")
267
309
  .map((operation) => buildDesiredManagedFile(operation, variables));
268
310
  }
@@ -297,9 +339,9 @@ function adaptGitignoreOperation(
297
339
  };
298
340
  }
299
341
 
300
- function buildSyncDesiredOperations(targetDir, metadata, identity) {
342
+ function buildSyncDesiredOperations(targetDir, metadata, identity, snapshot = readTemplateSnapshot()) {
301
343
  const variables = buildSyncVariables(metadata);
302
- return buildDesiredManagedOperations(targetDir, variables, "init")
344
+ return buildDesiredManagedOperations(targetDir, variables, "init", snapshot)
303
345
  .filter((operation) => operation.relativePath !== "README.md")
304
346
  .map((operation) =>
305
347
  adaptGitignoreOperation(operation, targetDir, {
@@ -327,7 +369,7 @@ function buildSyncDesiredOperations(targetDir, metadata, identity) {
327
369
  }
328
370
 
329
371
  function buildAttachDesiredOperations(targetDir, variables, identity) {
330
- return buildDesiredManagedOperations(targetDir, variables, "managed")
372
+ return buildDesiredManagedOperations(targetDir, variables, "init")
331
373
  .filter((operation) => operation.relativePath !== "README.md")
332
374
  .map((operation) => adaptGitignoreOperation(operation, targetDir, { attach: true }))
333
375
  .map((operation) => {
@@ -396,6 +438,7 @@ function collectManagedFiles(desiredOperations) {
396
438
 
397
439
  function buildMetadata(variables, desiredOperations, timestamp = nowIsoString()) {
398
440
  const snapshot = readTemplateSnapshot();
441
+ const distribution = variables.distribution || distributionForVariables(variables, BUNDLED_TEMPLATE_ROOT);
399
442
  return {
400
443
  metadataSchemaVersion: METADATA_SCHEMA_VERSION,
401
444
  templateName: PACKAGE_MANIFEST.name,
@@ -408,6 +451,7 @@ function buildMetadata(variables, desiredOperations, timestamp = nowIsoString())
408
451
  initializedAt: timestamp,
409
452
  lastSyncedAt: timestamp,
410
453
  managedFilesManifestVersion: TEMPLATE_MANIFEST_VERSION,
454
+ distribution,
411
455
  variables: {
412
456
  projectName: variables.projectName,
413
457
  businessDomain: variables.businessDomain,
@@ -429,19 +473,21 @@ function writeTemplateMetadata(targetDir, metadata, transaction = null) {
429
473
  fs.writeFileSync(metadataPath, content, "utf8");
430
474
  }
431
475
 
432
- function buildNextSyncMetadata(metadata, syncPlan) {
433
- const snapshot = readTemplateSnapshot();
476
+ function buildNextSyncMetadata(metadata, syncPlan, {
477
+ snapshot = readTemplateSnapshot(),
478
+ currentHashes = null,
479
+ } = {}) {
434
480
  const nextManagedFiles = { ...(metadata.managedFiles || {}) };
435
481
  delete nextManagedFiles["README.md"];
436
482
  for (const relativePath of syncPlan.alreadyMissing || []) delete nextManagedFiles[relativePath];
437
483
  for (const relativePath of syncPlan.pruned || []) delete nextManagedFiles[relativePath];
438
484
  for (const operation of syncPlan.desiredOperations) {
439
485
  if (pathKind(operation.targetPath) !== "file") continue;
440
- const currentHash = fileHash(operation.targetPath);
441
- if (currentHash === operation.desiredHash) {
486
+ const currentHash = currentHashes?.[operation.relativePath] ?? fileHash(operation.targetPath);
487
+ if (currentHash === operation.desiredHash || operation.relativePath === "skills-lock.json") {
442
488
  nextManagedFiles[operation.relativePath] = {
443
489
  type: operation.type,
444
- contentHash: operation.desiredHash,
490
+ contentHash: currentHash,
445
491
  };
446
492
  }
447
493
  }
@@ -449,6 +495,7 @@ function buildNextSyncMetadata(metadata, syncPlan) {
449
495
  return {
450
496
  ...metadata,
451
497
  metadataSchemaVersion: METADATA_SCHEMA_VERSION,
498
+ distribution: metadata.metadataSchemaVersion >= 3 ? metadata.distribution : { mode: "legacy-all" },
452
499
  templateName: PACKAGE_MANIFEST.name,
453
500
  cliVersion: PACKAGE_MANIFEST.version,
454
501
  templateVersion: PACKAGE_MANIFEST.version,
@@ -124,10 +124,25 @@ function isWriteProtectedOwnership(ownership) {
124
124
  return ownership === "user-owned" || ownership === "protected";
125
125
  }
126
126
 
127
+ function projectAssetWriteViolation(relativePath) {
128
+ const path = normalizePolicyPath(relativePath);
129
+ if (path === "docs/implementation/.gitkeep" || path === "docs/requirements/tickets/.gitkeep") return null;
130
+ if (
131
+ path === "scaffold-architecture-decisions.yaml" ||
132
+ path.startsWith("docs/implementation/") ||
133
+ path.startsWith("docs/requirements/tickets/")
134
+ ) {
135
+ return `${path} 是项目合同或决策资产,CLI 同步不得写入或清理`;
136
+ }
137
+ return null;
138
+ }
139
+
127
140
  function ownershipWriteViolation(operation) {
141
+ const path = operation?.relativePath || operation?.path || "unknown";
142
+ const projectAssetViolation = projectAssetWriteViolation(path);
143
+ if (projectAssetViolation) return projectAssetViolation;
128
144
  const ownership = operation?.ownership || "managed";
129
145
  if (!isWriteProtectedOwnership(ownership)) return null;
130
- const path = operation?.relativePath || operation?.path || "unknown";
131
146
  return ownership === "protected"
132
147
  ? `${path} 被 ownership policy 标记为 protected,CLI 不得写入或覆盖`
133
148
  : `${path} 被 ownership policy 标记为 user-owned,不属于受管模板文件`;
@@ -145,5 +160,6 @@ module.exports = {
145
160
  resolveOwnership,
146
161
  isTemplateManagedOwnership,
147
162
  isWriteProtectedOwnership,
163
+ projectAssetWriteViolation,
148
164
  ownershipWriteViolation,
149
165
  };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const { isTemplateManagedOwnership } = require("./ownership-policy");
3
+ const { isTemplateManagedOwnership, projectAssetWriteViolation } = require("./ownership-policy");
4
4
  const { resolveTemplateOwnership } = require("./ownership-runtime");
5
5
 
6
6
  function classifyRemovedFiles({
@@ -29,6 +29,11 @@ function classifyRemovedFiles({
29
29
  retainedRemoved.push({ path: relativePath, reason: "缺少可信受管基线" });
30
30
  continue;
31
31
  }
32
+ const projectAssetViolation = projectAssetWriteViolation(relativePath);
33
+ if (projectAssetViolation) {
34
+ retainedRemoved.push({ path: relativePath, reason: projectAssetViolation });
35
+ continue;
36
+ }
32
37
  const recordedOwnership = record.ownership || getCurrentOwnership(relativePath);
33
38
  const currentOwnership = getCurrentOwnership(relativePath);
34
39
  if (
@@ -23,6 +23,7 @@ function classifySyncOperations({
23
23
  const conflicts = [];
24
24
  const forceableConflicts = [];
25
25
  const unsafe = [];
26
+ const currentHashes = Object.create(null);
26
27
 
27
28
  for (const operation of desiredOperations) {
28
29
  const unmanagedReason = getUnmanagedReason(operation);
@@ -53,6 +54,7 @@ function classifySyncOperations({
53
54
  }
54
55
 
55
56
  const currentHash = getFileHash(operation);
57
+ currentHashes[operation.relativePath] = currentHash;
56
58
 
57
59
  if (operation.safeSectionMerge && currentHash !== operation.desiredHash) {
58
60
  updated.push(operation);
@@ -117,6 +119,7 @@ function classifySyncOperations({
117
119
  unsafe,
118
120
  removed,
119
121
  desiredOperations,
122
+ currentHashes,
120
123
  };
121
124
  }
122
125
 
@@ -134,7 +134,61 @@ function verifyGeneratedAttach(targetDir) {
134
134
  runTemplateVerification(targetDir, "scripts/verify-project-instance", []);
135
135
  }
136
136
 
137
- function refreshGeneratedProjectInstance(targetDir) {
137
+ function readProjectSkillLock(targetDir) {
138
+ const lockPath = targetPath(targetDir, "skills-lock.json");
139
+ if (pathKind(lockPath) === "missing") return null;
140
+ const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
141
+ const isRecord = (value) => value && typeof value === "object" && !Array.isArray(value);
142
+ if (
143
+ lock.version !== 3 || !isRecord(lock.skills?.shared) ||
144
+ !isRecord(lock.skills?.platform) || !isRecord(lock.sources) ||
145
+ !Array.isArray(lock.projectionRoots)
146
+ ) {
147
+ throw new Error("现有 skills-lock.json 结构非法,无法安全保留项目技能登记");
148
+ }
149
+ return lock;
150
+ }
151
+
152
+ function preserveProjectSkillRegistrations(targetDir, previousLock, previousManagedFiles, transaction) {
153
+ if (!previousLock) return;
154
+ const lockPath = targetPath(targetDir, "skills-lock.json");
155
+ const generated = JSON.parse(fs.readFileSync(lockPath, "utf8"));
156
+ const managedPaths = Object.keys(previousManagedFiles || {});
157
+ const wasManaged = (skillPath) => managedPaths.some((ref) => ref.startsWith(`${skillPath}/`));
158
+ let changed = false;
159
+
160
+ for (const [name, record] of Object.entries(previousLock.skills.shared)) {
161
+ const skillPath = `.agents/skills/${name}`;
162
+ if (generated.skills.shared[name] || wasManaged(skillPath)) continue;
163
+ if (pathKind(targetPath(targetDir, skillPath)) !== "directory") continue;
164
+ generated.skills.shared[name] = record;
165
+ changed = true;
166
+ }
167
+ for (const [root, entries] of Object.entries(previousLock.skills.platform || {})) {
168
+ if (!generated.projectionRoots.includes(root)) continue;
169
+ for (const [name, record] of Object.entries(entries)) {
170
+ const skillPath = `${root}/${name}`;
171
+ if (generated.skills.platform?.[root]?.[name] || wasManaged(skillPath)) continue;
172
+ if (pathKind(targetPath(targetDir, skillPath)) !== "directory") continue;
173
+ generated.skills.platform ||= {};
174
+ generated.skills.platform[root] ||= {};
175
+ generated.skills.platform[root][name] = record;
176
+ changed = true;
177
+ }
178
+ }
179
+ if (changed) {
180
+ generated.sources = { ...previousLock.sources, ...generated.sources };
181
+ transaction.writeFile(lockPath, `${JSON.stringify(generated, null, 2)}\n`);
182
+ }
183
+ }
184
+
185
+ function refreshGeneratedProjectInstance(targetDir, options = {}) {
186
+ preserveProjectSkillRegistrations(
187
+ targetDir,
188
+ options.previousSkillLock,
189
+ options.previousManagedFiles,
190
+ options.transaction,
191
+ );
138
192
  runTemplateVerification(targetDir, "scripts/update-skill-lock", []);
139
193
  }
140
194
 
@@ -146,6 +200,7 @@ module.exports = {
146
200
  verifyGeneratedInstance,
147
201
  verifyGeneratedInit,
148
202
  verifyGeneratedAttach,
203
+ readProjectSkillLock,
149
204
  refreshGeneratedProjectInstance,
150
205
  verifyGeneratedProjectInstance: verifyGeneratedAttach,
151
206
  };
@@ -8,6 +8,7 @@ const OWNERSHIP_TYPES = new Set([
8
8
  "protected",
9
9
  ]);
10
10
  const MERGE_STRATEGIES = new Set(["replace-with-force", "manual"]);
11
+ const { ASSET_PROFILE, INITIAL_STAGES, STAGES } = require("../template/asset-runtime");
11
12
 
12
13
  function assertPositiveInteger(value, name) {
13
14
  if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
@@ -22,7 +23,7 @@ function assertSha256(value, name) {
22
23
  }
23
24
 
24
25
  function validateTemplateMetadata(metadata, {
25
- currentSchemaVersion = 2,
26
+ currentSchemaVersion = 3,
26
27
  templateName,
27
28
  templateSource,
28
29
  } = {}) {
@@ -35,6 +36,28 @@ function validateTemplateMetadata(metadata, {
35
36
  if (metadata.metadataSchemaVersion > currentSchemaVersion) {
36
37
  throw new Error(`不支持的模板元数据版本:${metadata.metadataSchemaVersion}`);
37
38
  }
39
+ if (metadata.metadataSchemaVersion === 3) {
40
+ const selection = metadata.distribution;
41
+ if (!selection || !["selected", "legacy-all"].includes(selection.mode)) throw new Error("模板元数据 distribution 非法");
42
+ if (selection.mode === "selected" && (
43
+ !Array.isArray(selection.runtimes) || !selection.runtimes.length ||
44
+ new Set(selection.runtimes).size !== selection.runtimes.length ||
45
+ selection.runtimes.some((runtime) => !["codex", "cursor", "pi"].includes(runtime)) ||
46
+ !Array.isArray(selection.installedSkills) ||
47
+ new Set(selection.installedSkills).size !== selection.installedSkills.length ||
48
+ selection.installedSkills.some((skill) => !/^[a-z][a-z0-9-]+$/.test(skill))
49
+ )) throw new Error("模板元数据 distribution 选择非法");
50
+ if (selection.mode === "selected" && selection.assetProfile !== undefined && (
51
+ selection.assetProfile !== ASSET_PROFILE ||
52
+ !Array.isArray(selection.installedStages) ||
53
+ INITIAL_STAGES.some((stage) => !selection.installedStages.includes(stage)) ||
54
+ new Set(selection.installedStages).size !== selection.installedStages.length ||
55
+ selection.installedStages.some((stage) => !Object.hasOwn(STAGES, stage)) ||
56
+ !Array.isArray(selection.resourceSkills) ||
57
+ new Set(selection.resourceSkills).size !== selection.resourceSkills.length ||
58
+ selection.resourceSkills.some((skill) => !selection.installedSkills.includes(skill))
59
+ )) throw new Error("模板元数据阶段资产选择非法");
60
+ }
38
61
  if (metadata.managedFiles !== undefined && (typeof metadata.managedFiles !== "object" || metadata.managedFiles === null || Array.isArray(metadata.managedFiles))) {
39
62
  throw new Error("模板元数据 managedFiles 必须是 JSON 对象");
40
63
  }
@@ -3,7 +3,7 @@
3
3
  "source": "iloveZzz/yss-harness-design-agent",
4
4
  "source_type": "git-submodule",
5
5
  "source_root": ".agents/skills",
6
- "source_revision": "9e17922a0a718e5bd1845ddab11850a2f43becad",
6
+ "source_revision": "9b579c7b954c153d2347696f53123aebaccde48f",
7
7
  "adaptation_ref": "docs/agents/strategic-design-skills-integration.md",
8
8
  "skills": [
9
9
  {
@@ -10,8 +10,8 @@ description: "接入或排查 YSS QueryCache、UpdateCache、ClearCache、TTL、
10
10
  ## 工作流
11
11
 
12
12
  1. 按 `yss-skill-source-index-refresh/references/source-location.md` 定位真实源码。当前工作区有 `.codegraph/` 时先用 CodeGraph;否则读取 [source-index.md](references/source-index.md) 后用符号或 Maven 模块搜索。
13
- 2. 识别使用模型:Spring Cache 单后端路由、Redis 故障降级,或 JetCache local + remote。不要混用三者的行为假设。
14
- 3. 明确缓存名、key、TTL、更新/删除路径、跨节点一致性和序列化兼容要求。
13
+ 2. 识别使用模型:Spring Cache 单后端路由、Redis 故障处理,或独立的 JetCache local + remote。`fail-fast`/`bypass`/`fallback` 是当前 Boot 3 Redis 契约;Boot 2 以其独立源码索引为准,不要混用两代或三种模型的行为假设。
14
+ 3. 明确缓存名、key、区域 TTL/容量/空值策略、更新/删除路径、跨节点一致性和序列化兼容要求。
15
15
  4. 先检查现有依赖、启动注解、配置和注解用法,再决定修改业务代码、配置还是组件。
16
16
  5. 修改后执行对应 reference 的验证;组件跨模块修改必须从父 reactor 构建,不要依赖本地旧 SNAPSHOT。
17
17
 
@@ -20,13 +20,14 @@ description: "接入或排查 YSS QueryCache、UpdateCache、ClearCache、TTL、
20
20
  - 新增查询缓存时同时覆盖更新、删除和状态变更的失效路径。
21
21
  - key 必须稳定并显式包含租户、机构、账套等隔离维度;不要依赖 DTO 的不稳定序列化结果。
22
22
  - 不要把 Redis fallback 当作正常二级缓存。Caffeine fallback 存在节点间不一致窗口。
23
- - 不要声称纯 Redis 后端会广播本地缓存失效。只有确认 JetCache 配置了 local、remote 和 broadcast channel 后才能作此判断。
23
+ - 不要声称纯 Redis 后端会广播本地缓存失效。JetCache 即使存在 broadcast channel 配置,也须验证实际 local/remote 实例和同步开关后才能作此判断。
24
24
  - 不要在没有双读、版本前缀、灰度清理或迁移窗口时修改 serializer、key prefix 或 cache name。
25
25
  - 不明确一致性要求时,不默认启用本地缓存或 fallback。
26
26
 
27
27
  ## 按场景读取
28
28
 
29
29
  - 模块、后端选择或多级缓存边界:[architecture.md](references/architecture.md)
30
+ - JetCache 包装器、`QuickConfig`、跨 JVM 失效与故障边界:[jetcache.md](references/jetcache.md)
30
31
  - 新增或修改缓存注解、SpEL、空 key、全量清理:[annotations.md](references/annotations.md)
31
32
  - 依赖、启动注解、TTL、Caffeine/Hazelcast、Bean backoff:[configuration.md](references/configuration.md)
32
33
  - Redis 单机、哨兵、集群、认证、SSL、超时、Jedis pool:[redis-topology.md](references/redis-topology.md)
@@ -38,10 +39,10 @@ description: "接入或排查 YSS QueryCache、UpdateCache、ClearCache、TTL、
38
39
  ## 工具
39
40
 
40
41
  - `scripts/inspect-cache-usage.sh <project-root>`:只读扫描消费项目;发现阻断缺陷返回 1,输入或环境错误返回 2。
41
- - `scripts/verify-cache-component.sh [--source-root PATH] [--consumer-root PATH --consumer-module MODULE]`:验证组件、真实 Redis、Java 8 字节码及可选消费模块。
42
+ - `scripts/verify-cache-component.sh --platform-line <boot2-java8|boot3-java17> [--source-root PATH] [--consumer-root PATH --consumer-module MODULE]`:从匹配的平台源码根干净构建,验证组件、可用时的真实 Redis、对应 Java 字节码及可选消费模块。
42
43
  - `scripts/check-skill-freshness.sh <boot2-java8|boot3-java17> [source-root]`:比较所选平台线的技能契约与当前组件源码;发现平台错配或漂移返回 1。
43
44
 
44
- 当组件源码变化后,用 `yss-skill-source-index-refresh` 刷新 [source-index.md](references/source-index.md),再运行 freshness 检查。不要手工修改生成索引。
45
+ 本 Skill 中新增的 `failure-mode`、区域策略和 `empty-key-eviction` 指引针对当前 Boot 3 组件;Boot 2 的精确配置与默认值须查其独立索引及源码。组件源码形成干净、固定的来源后,用 `yss-skill-source-index-refresh` 刷新所选平台的生成索引,再运行 freshness 检查。`source-index.md` 是平台选择页;不要手工修改生成索引或把 dirty 工作树的观察说成已核验事实。
45
46
 
46
47
  ## 平台与源码门禁
47
48
 
@@ -1,5 +1,7 @@
1
1
  # 注解契约
2
2
 
3
+ 以下 `empty-key-eviction` 可配置行为针对当前 Boot 3 / Java 17 组件;Boot 2 的空 key 处理以其独立源码索引和当前源码为准。
4
+
3
5
  ## 映射
4
6
 
5
7
  - `@QueryCache` -> Spring `CacheableOperation`。
@@ -28,7 +30,7 @@ public void delete(String tenantId, Long planId) { ... }
28
30
  ```
29
31
 
30
32
  - 未配置 key 时由 Spring KeyGenerator 生成;无参数方法会得到 `SimpleKey.EMPTY`。
31
- - 当前 YSS 拦截器将 `SimpleKey.EMPTY` 视为全量清理。需要单 key 清理时必须给出稳定 key。
33
+ - 当前 Boot 3 的 `yss.cache.empty-key-eviction=legacy-clear`(默认)把 `SimpleKey.EMPTY` 当作清区;`evict` 时只删该 key。迁移前核对原有无参数清理调用及实际属性,不能把默认行为说成所有配置下的不变量。
32
34
  - 全量清理优先显式写 `allEntries=true`,不要依赖偶然 key 形态。
33
35
  - 集合参数先稳定排序;null、大小写和空白必须归一化。
34
36
 
@@ -1,12 +1,14 @@
1
1
  # 架构与选择
2
2
 
3
+ 以下模型划分适用于接入决策;具体配置、默认值及跨节点行为按所选平台线的当前源码核验。本轮新增的 Redis 故障模式、区域策略和 JetCache 运行证据属于 Boot 3 当前组件。
4
+
3
5
  ## 三种模型
4
6
 
5
7
  | 模型 | 行为 | 一致性边界 |
6
8
  |---|---|---|
7
9
  | Spring Cache 后端路由 | `yss.cache.active-type` 在 Redis、Caffeine、Hazelcast 中选择一个 Provider | 同一时刻是单后端,不是多级缓存 |
8
10
  | Redis fallback | Redis 故障时可临时改用另一个 Provider | 是故障降级;Caffeine 会产生节点间不一致窗口 |
9
- | JetCache | 独立扩展,可配置 local + remote | 只有实际配置 broadcast channel 才能认为本地失效会同步 |
11
+ | JetCache | 独立扩展,可配置 local + remote | broadcast channel 配置本身不证明包装器已启用跨 JVM 本地失效同步 |
10
12
 
11
13
  ## 模块
12
14
 
@@ -22,4 +24,4 @@
22
24
  - 跨节点共享且不接受节点本地旧值:使用纯 Redis。
23
25
  - 读多写少并允许短暂节点差异:可选择 Caffeine。
24
26
  - Redis 故障期间业务可用性高于缓存一致性:评估后启用 fallback。
25
- - 需要 local + remote:先确认 JetCache 的 local、remote、序列化和广播配置,不要只看依赖名称。
27
+ - 需要 local + remote:按 [JetCache 专项参考](jetcache.md) 核对实际创建的 Cache、配置来源及首次创建参数,并用跨 JVM 测试证明同步,不要只看依赖或 `bootstrap.yml`。
@@ -13,20 +13,23 @@
13
13
 
14
14
  ## 公共属性
15
15
 
16
+ 以下示例及 `failure-mode`、区域策略、空值策略说明针对当前 Boot 3 / Java 17 组件。Boot 2 / Java 8 维护须先核对其独立源码索引与匹配源码,不套用这些新属性或默认值。
17
+
16
18
  ```yaml
17
19
  yss:
18
20
  cache:
19
21
  active-type: redis # redis | caffeine | hazelcast
20
22
  default-ttl: 1h
23
+ failure-mode: fail-fast # fail-fast | bypass | fallback;仅 active-type=redis
21
24
  redis:
22
25
  fallback-enabled: false
23
26
  fallback-type: caffeine
24
27
  clear-fallback-on-recovery: true
25
28
  ```
26
29
 
27
- - Redis 动态 cache 和 Caffeine 默认 TTL 使用 `yss.cache.default-ttl`。
28
- - `CacheKeyCode` 中声明的 TTL 覆盖默认 TTL。
29
- - Caffeine 的 `spring.cache.caffeine.spec` 可覆盖默认构建规格,修改前检查项目现有配置。
30
+ - `failure-mode` 默认 fail-fast;旧 `redis.fallback-enabled=true` 仍映射 fallback,同时配置为冲突值会拒绝启动。bypass 仅将 Redis 连接/资源故障的查询视为 miss,显式写入和失效仍报告错误;业务与序列化异常不降级。fallback 仅可选择已安装的 Caffeine,不是默认二级缓存。
31
+ - 区域 `yss.cache.regions[区域名]` 可逐字段设置 `ttl`、`caffeine-maximum-size`、`null-policy`。TTL 以区域值优先;Caffeine 用户显式过期策略、`CacheKeyCode` 与 `default-ttl` 的实际优先级按当前组件源码核对。`null-policy=cache` 必须指定正的区域 TTL;`skip` 不等于删除旧值,`reject` 的契约错误不能吞成 Redis 故障。
32
+ - Caffeine `spring.cache.caffeine.spec`、自定义 Builder/Loader 与区域覆盖的组合可能无法表达;核对当前组件的启动拒绝条件。自定义 CacheManager/Provider 时需证明区域策略真实生效,不能因配置存在就宣称 TTL、容量或空值策略已应用。
30
33
  - Hazelcast 不在 starter 生产依赖中;使用时显式引入模块并配置 `active-type=hazelcast`。
31
34
 
32
35
  ## 自定义 Bean
@@ -0,0 +1,21 @@
1
+ # JetCache 独立接入与验证
2
+
3
+ 本页记录当前 Boot 3 / Java 17 `yss-component-jetcache` 工作树的接入判断;Boot 2 的精确能力以其平台索引和匹配源码为准。JetCache 与 YSS Spring Cache 是两条独立入口,不继承 `yss.cache.active-type`、区域 TTL 或 Redis `failure-mode`。
4
+
5
+ ## 先确认实际入口
6
+
7
+ - 检查运行时上游 `CacheManager`、local/remote builder、应用自己的 JetCache 配置及实际创建的 area/name。依赖 JAR 中有 `bootstrap.yml` 不证明应用加载了它;无有效 local builder 时,LOCAL 请求可能在运行时失败。
8
+ - 组件兼容包装器的两参数 `getCache` 默认 TTL 为 5 分钟,重载接收 `Duration`;它不设置 `syncLocal`、loader 或刷新策略。包装器的类型映射也不证明 LOCAL 实际采用 Caffeine。需要这些能力时核对原生 `QuickConfig` 与上游管理器。
9
+ - 管理器按 area/name 复用已创建缓存。类型、TTL、`syncLocal` 等参数在第一次创建时确定,后续 `getOrCreateCache` 不会重建同名缓存;修改配置须使用新名称或明确的生命周期切换。
10
+
11
+ ## 跨 JVM 本地失效
12
+
13
+ 原生 `QuickConfig` 选择 `BOTH`、设置 `syncLocal(true)`,并为所有参与节点配置兼容的 `jetcache.remote.default.broadcastChannel`、编码及相同 area/name。包装器即使存在广播频道也不会自行开启 `syncLocal`;缺频道时不能据 `syncLocal(true)` 宣称已同步。
14
+
15
+ 验收时启动两个独立 JVM,在两端创建同名缓存,确认广播订阅实际建立后,从一端更新和删除并观察另一端本地值失效;同时用未同步的对照缓存确认失效范围。广播是异步通知,不承诺强一致或断连期间的持久补偿。
16
+
17
+ ## 故障与数据格式
18
+
19
+ - 读取 `CacheResult` 状态来区分未命中和故障;便利 `get`/`getValue` 可能将远端故障折叠为 `null`。REMOTE 与 BOTH 的故障结果也不同,不能套用主线 Redis 的 fail-fast/bypass/fallback。
20
+ - JetCache Java 编码保存 `CacheValueHolder`,与 Spring RedisCache 的 JDK 业务值及 RedisTemplate 的 Jackson 值不同。缓存名称、key 前缀或 serializer 迁移前,核对实际物理 key 和兼容方案,避免混写。
21
+ - 包装器不提供主线缓存的事务后提交协调。刷新须核对原生 loader 与 refresh policy 是否同时配置;本轮未验证多节点刷新去重、断连补偿或生产 SLA。
@@ -1,11 +1,13 @@
1
1
  # Redis 故障降级与恢复
2
2
 
3
+ 以下故障模式和脏 Redis 区域恢复顺序是当前 Boot 3 / Java 17 组件契约。Boot 2 / Java 8 的故障处理和恢复默认值须单独核对其索引及源码。
4
+
3
5
  ## 状态流
4
6
 
5
7
  ```text
6
8
  Redis 健康 -> 命令或健康检查失败 -> 标记不健康 -> 跳过 Redis
7
9
  -> 可选 fallback -> 周期健康检查 -> Redis 可连接
8
- -> 清理脏 Redis cache 与本地 fallback cache -> 恢复 Redis
10
+ -> 清理脏 Redis cache;按配置清理本地 fallback cache -> 恢复 Redis
9
11
  ```
10
12
 
11
13
  - fallback 默认关闭,默认目标是 Caffeine。
@@ -13,11 +15,11 @@ Redis 健康 -> 命令或健康检查失败 -> 标记不健康 -> 跳过 Redis
13
15
  - Redis 命令级连接故障也会立即报告不健康,不必等待下一轮检查。
14
16
  - 只读 fallback 不标记 Redis cache 为脏。
15
17
  - put、putIfAbsent、evict、clear、invalidate 和 valueLoader 产生的回退写入会标记为脏。
16
- - 恢复时先清理降级期间发生写操作的 Redis cache,再清理本地 fallback cache。
18
+ - 恢复时始终先清理降级期间发生写操作的 Redis cache;`clear-fallback-on-recovery=true` 时还清理本地 fallback cache。
17
19
  - Redis 失效失败时继续保持不健康状态并在后续检查重试,不能提前切回旧 Redis 数据。
18
20
 
19
21
  ## 一致性判断
20
22
 
21
23
  - Caffeine fallback 是每节点独立数据,不能保证跨节点读到相同值。
22
- - `clear-fallback-on-recovery=false` 会跳过恢复清理,可能重新暴露 Redis 旧值;除非业务明确接受,不要关闭。
24
+ - `clear-fallback-on-recovery=false` 只跳过本地备用缓存清理,不跳过脏 Redis 区域失效。Redis 失效失败时不切回;关闭本地清理仍须评估后续备用使用时的本地旧值窗口。
23
25
  - 强一致、锁、幂等状态或余额类数据不应依赖缓存 fallback 保证正确性。