openxiangda-devkit-core 2.0.0-alpha.97 → 2.0.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -9
  3. package/dist/app-spec.d.ts +30 -12
  4. package/dist/app-spec.d.ts.map +1 -1
  5. package/dist/app-spec.js +238 -57
  6. package/dist/app-spec.js.map +1 -1
  7. package/dist/application-services.d.ts +625 -42
  8. package/dist/application-services.d.ts.map +1 -1
  9. package/dist/application-services.js +321 -85
  10. package/dist/application-services.js.map +1 -1
  11. package/dist/application-ui-contract.d.ts +5 -0
  12. package/dist/application-ui-contract.d.ts.map +1 -0
  13. package/dist/application-ui-contract.js +149 -0
  14. package/dist/application-ui-contract.js.map +1 -0
  15. package/dist/backend-image-build.js +6 -11
  16. package/dist/backend-image-build.js.map +1 -1
  17. package/dist/command-process.d.ts +14 -0
  18. package/dist/command-process.d.ts.map +1 -0
  19. package/dist/command-process.js +51 -0
  20. package/dist/command-process.js.map +1 -0
  21. package/dist/command-registry.d.ts +44 -4
  22. package/dist/command-registry.d.ts.map +1 -1
  23. package/dist/command-registry.js +10 -6
  24. package/dist/command-registry.js.map +1 -1
  25. package/dist/compiler/ai-catalog.d.ts.map +1 -1
  26. package/dist/compiler/ai-catalog.js +2 -1
  27. package/dist/compiler/ai-catalog.js.map +1 -1
  28. package/dist/compiler/application-model.d.ts +60 -0
  29. package/dist/compiler/application-model.d.ts.map +1 -0
  30. package/dist/compiler/application-model.js +141 -0
  31. package/dist/compiler/application-model.js.map +1 -0
  32. package/dist/compiler/bundle.d.ts.map +1 -1
  33. package/dist/compiler/bundle.js +196 -98
  34. package/dist/compiler/bundle.js.map +1 -1
  35. package/dist/compiler/config.d.ts +36 -10
  36. package/dist/compiler/config.d.ts.map +1 -1
  37. package/dist/compiler/config.js +536 -171
  38. package/dist/compiler/config.js.map +1 -1
  39. package/dist/compiler/field-surface.js +1 -1
  40. package/dist/compiler/field-surface.js.map +1 -1
  41. package/dist/compiler/package-compiler.d.ts.map +1 -1
  42. package/dist/compiler/package-compiler.js +12 -0
  43. package/dist/compiler/package-compiler.js.map +1 -1
  44. package/dist/compiler/permission-review.d.ts +139 -0
  45. package/dist/compiler/permission-review.d.ts.map +1 -0
  46. package/dist/compiler/permission-review.js +131 -0
  47. package/dist/compiler/permission-review.js.map +1 -0
  48. package/dist/configuration-preflight.d.ts +7 -0
  49. package/dist/configuration-preflight.d.ts.map +1 -0
  50. package/dist/configuration-preflight.js +13 -0
  51. package/dist/configuration-preflight.js.map +1 -0
  52. package/dist/control-plane-client.d.ts +56 -61
  53. package/dist/control-plane-client.d.ts.map +1 -1
  54. package/dist/control-plane-client.js +89 -8
  55. package/dist/control-plane-client.js.map +1 -1
  56. package/dist/delivery-cache.d.ts +34 -0
  57. package/dist/delivery-cache.d.ts.map +1 -0
  58. package/dist/delivery-cache.js +186 -0
  59. package/dist/delivery-cache.js.map +1 -0
  60. package/dist/delivery-source.d.ts +17 -0
  61. package/dist/delivery-source.d.ts.map +1 -0
  62. package/dist/delivery-source.js +85 -0
  63. package/dist/delivery-source.js.map +1 -0
  64. package/dist/deployment-observer.d.ts +14 -0
  65. package/dist/deployment-observer.d.ts.map +1 -0
  66. package/dist/deployment-observer.js +68 -0
  67. package/dist/deployment-observer.js.map +1 -0
  68. package/dist/deployment.d.ts +17 -1
  69. package/dist/deployment.d.ts.map +1 -1
  70. package/dist/deployment.js +55 -15
  71. package/dist/deployment.js.map +1 -1
  72. package/dist/design-readiness.d.ts +24 -0
  73. package/dist/design-readiness.d.ts.map +1 -0
  74. package/dist/design-readiness.js +102 -0
  75. package/dist/design-readiness.js.map +1 -0
  76. package/dist/developer-errors.d.ts +11 -0
  77. package/dist/developer-errors.d.ts.map +1 -0
  78. package/dist/developer-errors.js +16 -0
  79. package/dist/developer-errors.js.map +1 -0
  80. package/dist/developer-operations.d.ts +190 -0
  81. package/dist/developer-operations.d.ts.map +1 -0
  82. package/dist/developer-operations.js +65 -0
  83. package/dist/developer-operations.js.map +1 -0
  84. package/dist/development-lifecycle.d.ts +75 -0
  85. package/dist/development-lifecycle.d.ts.map +1 -0
  86. package/dist/development-lifecycle.js +189 -0
  87. package/dist/development-lifecycle.js.map +1 -0
  88. package/dist/documentation.d.ts +111 -0
  89. package/dist/documentation.d.ts.map +1 -0
  90. package/dist/documentation.js +112 -0
  91. package/dist/documentation.js.map +1 -0
  92. package/dist/index.d.ts +10 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +10 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/operation-progress.d.ts +26 -0
  97. package/dist/operation-progress.d.ts.map +1 -0
  98. package/dist/operation-progress.js +92 -0
  99. package/dist/operation-progress.js.map +1 -0
  100. package/dist/optional-backend.d.ts +14 -0
  101. package/dist/optional-backend.d.ts.map +1 -0
  102. package/dist/optional-backend.js +145 -0
  103. package/dist/optional-backend.js.map +1 -0
  104. package/dist/session.d.ts +22 -0
  105. package/dist/session.d.ts.map +1 -1
  106. package/dist/session.js +36 -0
  107. package/dist/session.js.map +1 -1
  108. package/dist/workspace-loader.d.ts.map +1 -1
  109. package/dist/workspace-loader.js +17 -2
  110. package/dist/workspace-loader.js.map +1 -1
  111. package/dist/workspace-operation.d.ts +3 -0
  112. package/dist/workspace-operation.d.ts.map +1 -0
  113. package/dist/workspace-operation.js +45 -0
  114. package/dist/workspace-operation.js.map +1 -0
  115. package/dist/workspace.d.ts.map +1 -1
  116. package/dist/workspace.js +1 -0
  117. package/dist/workspace.js.map +1 -1
  118. package/package.json +18 -4
  119. package/templates/backend/Dockerfile +20 -0
  120. package/templates/backend/package.json +27 -0
  121. package/templates/backend/src/app.module.ts +23 -0
  122. package/templates/backend/src/main.ts +5 -0
  123. package/templates/backend/tsconfig.json +9 -0
package/dist/app-spec.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, openSync, readSync, closeSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { SCHEMA_VERSIONS, } from "openxiangda-contracts";
5
5
  export const APP_SPEC_SCHEMAS = {
@@ -7,7 +7,8 @@ export const APP_SPEC_SCHEMAS = {
7
7
  capability: "openxiangda.appspec/capability/v1",
8
8
  change: "openxiangda.appspec/change/v1",
9
9
  decision: "openxiangda.appspec/decision/v1",
10
- context: "openxiangda.appspec/context/v2",
10
+ design: "openxiangda.appspec/design/v1",
11
+ context: "openxiangda.appspec/context/v4",
11
12
  };
12
13
  export const APP_SPEC_LIMITS = {
13
14
  maximumFiles: 128,
@@ -49,8 +50,8 @@ export function initializeAppSpec(input) {
49
50
  return {
50
51
  schemaVersion: APP_SPEC_SCHEMAS.context,
51
52
  enabled: true,
52
- advisory: true,
53
- releaseGate: false,
53
+ advisory: false,
54
+ releaseGate: true,
54
55
  created,
55
56
  existing,
56
57
  };
@@ -72,8 +73,8 @@ export function createAppSpecCapability(input) {
72
73
  return {
73
74
  schemaVersion: APP_SPEC_SCHEMAS.context,
74
75
  enabled: true,
75
- advisory: true,
76
- releaseGate: false,
76
+ advisory: false,
77
+ releaseGate: true,
77
78
  created: relativePath(input.root, path),
78
79
  };
79
80
  }
@@ -82,6 +83,9 @@ export function createAppSpecChange(input) {
82
83
  if (!CHANGE_ID.test(input.id)) {
83
84
  throw stableError("APPSPEC_CHANGE_ID_INVALID", input.id);
84
85
  }
86
+ if (historyCandidates(input.root, input.id, 0, []).paths.some(path => basename(path) === `${input.id}.md`)) {
87
+ throw stableError('APPSPEC_HISTORY_CHANGE_EXISTS', input.id);
88
+ }
85
89
  if (!input.title.trim())
86
90
  throw stableError("APPSPEC_TITLE_REQUIRED", input.id);
87
91
  const risk = input.risk || "L1";
@@ -102,8 +106,8 @@ export function createAppSpecChange(input) {
102
106
  return {
103
107
  schemaVersion: APP_SPEC_SCHEMAS.context,
104
108
  enabled: true,
105
- advisory: true,
106
- releaseGate: false,
109
+ advisory: false,
110
+ releaseGate: true,
107
111
  created: relativePath(input.root, path),
108
112
  risk,
109
113
  status: "draft",
@@ -171,8 +175,8 @@ export function closeAppSpecChange(input) {
171
175
  return {
172
176
  schemaVersion: APP_SPEC_SCHEMAS.context,
173
177
  enabled: true,
174
- advisory: true,
175
- releaseGate: false,
178
+ advisory: false,
179
+ releaseGate: true,
176
180
  archived: null,
177
181
  status: document.status,
178
182
  converged: false,
@@ -186,15 +190,15 @@ export function closeAppSpecChange(input) {
186
190
  return {
187
191
  schemaVersion: APP_SPEC_SCHEMAS.context,
188
192
  enabled: true,
189
- advisory: true,
190
- releaseGate: false,
193
+ advisory: false,
194
+ releaseGate: true,
191
195
  archived: relativePath(input.root, target),
192
196
  status: "archived",
193
197
  converged: true,
194
198
  diagnostics,
195
199
  };
196
200
  }
197
- export function inspectAppSpec(root, contract, selector) {
201
+ export function inspectAppSpec(root, contract, selector, historyOffset = 0) {
198
202
  const paths = appSpecPaths(root);
199
203
  if (!existsSync(paths.root))
200
204
  return emptyContext(contract, selector);
@@ -203,7 +207,7 @@ export function inspectAppSpec(root, contract, selector) {
203
207
  diagnostics.push(issue("error", "APPSPEC_ROOT_INVALID", "appspec 必须是普通目录,不能是符号链接", "appspec"));
204
208
  return emptyContext(contract, selector, diagnostics, true);
205
209
  }
206
- const collection = collectDocuments(root, diagnostics);
210
+ const collection = collectDocuments(root, diagnostics, selector, historyOffset);
207
211
  const application = collection.app[0] || null;
208
212
  validateDocuments(collection, contract, diagnostics);
209
213
  const selected = selectContext(collection, selector, diagnostics);
@@ -213,9 +217,10 @@ export function inspectAppSpec(root, contract, selector) {
213
217
  ...collection.activeChanges,
214
218
  ...collection.history,
215
219
  ...collection.decisions,
220
+ ...collection.designs,
216
221
  ];
217
222
  const index = summarizeCollection(collection);
218
- const workspaceDigest = digestDocuments(allDocuments, contract);
223
+ const workspaceDigest = digestDocuments(allDocuments.filter(document => !document.path.startsWith("appspec/changes/history/")), contract);
219
224
  const selectionDocuments = selectedDocuments(application, selected);
220
225
  const selectionDigest = allDocuments.length === 0
221
226
  ? null
@@ -227,8 +232,8 @@ export function inspectAppSpec(root, contract, selector) {
227
232
  return {
228
233
  schemaVersion: APP_SPEC_SCHEMAS.context,
229
234
  enabled: true,
230
- mode: "advisory",
231
- releaseGate: false,
235
+ mode: "guided",
236
+ releaseGate: true,
232
237
  root: "appspec",
233
238
  selector: selector || null,
234
239
  contract,
@@ -238,16 +243,19 @@ export function inspectAppSpec(root, contract, selector) {
238
243
  activeChanges: budgeted.activeChanges,
239
244
  archivedChanges: budgeted.archivedChanges,
240
245
  decisions: budgeted.decisions,
246
+ designs: budgeted.designs,
247
+ historyPage: collection.historyPage,
241
248
  contextBudget: budgeted.contextBudget,
242
249
  stats: {
243
250
  files: allDocuments.length,
244
251
  bytes: collection.bytes,
245
252
  capabilities: collection.capabilities.length,
246
253
  activeChanges: collection.activeChanges.length,
247
- historyChanges: collection.history.length,
254
+ historyChanges: collection.historyPage.total,
248
255
  decisions: collection.decisions.length,
249
- requirements: unique(collection.capabilities.flatMap(document => document.requirementIds)).length,
250
- acceptanceScenarios: unique(collection.capabilities.flatMap(document => document.acceptanceIds)).length,
256
+ designs: collection.designs.length,
257
+ requirements: unique([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(document => document.requirementIds)).length,
258
+ acceptanceScenarios: unique([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(document => document.acceptanceIds)).length,
251
259
  },
252
260
  workspaceDigest,
253
261
  selectionDigest,
@@ -259,7 +267,7 @@ export function advisoryAppSpecDiagnostics(diagnostics) {
259
267
  ...diagnostic,
260
268
  severity: diagnostic.severity === "error" ? "warning" : diagnostic.severity,
261
269
  remediation: diagnostic.remediation ||
262
- "AppSpec 是辅助层;可运行 openxiangda spec check 获取完整诊断,应用 check/deploy 不受阻断",
270
+ "运行 openxiangda spec check 查看文档诊断;普通 check 可以继续,正式发布按阶段核对需求与验收",
263
271
  }));
264
272
  }
265
273
  export function summarizeAppSpecContext(context) {
@@ -269,6 +277,7 @@ export function summarizeAppSpecContext(context) {
269
277
  releaseGate: context.releaseGate,
270
278
  workspaceDigest: context.workspaceDigest,
271
279
  selectionDigest: context.selectionDigest,
280
+ historyPage: context.historyPage,
272
281
  contextBudget: context.contextBudget,
273
282
  stats: context.stats,
274
283
  diagnostics: {
@@ -285,8 +294,8 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
285
294
  return {
286
295
  schemaVersion: APP_SPEC_SCHEMAS.context,
287
296
  enabled,
288
- mode: "advisory",
289
- releaseGate: false,
297
+ mode: "guided",
298
+ releaseGate: true,
290
299
  root: "appspec",
291
300
  selector: selector || null,
292
301
  contract,
@@ -295,6 +304,7 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
295
304
  capabilities: [],
296
305
  activeChanges: [],
297
306
  decisions: [],
307
+ designs: [],
298
308
  history: [],
299
309
  },
300
310
  application: null,
@@ -302,6 +312,8 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
302
312
  activeChanges: [],
303
313
  archivedChanges: [],
304
314
  decisions: [],
315
+ designs: [],
316
+ historyPage: { offset: 0, limit: 50, total: 0, nextOffset: null },
305
317
  contextBudget: {
306
318
  maximumBytes: APP_SPEC_LIMITS.maximumContextBytes,
307
319
  contentBytes: 0,
@@ -315,6 +327,7 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
315
327
  activeChanges: 0,
316
328
  historyChanges: 0,
317
329
  decisions: 0,
330
+ designs: 0,
318
331
  requirements: 0,
319
332
  acceptanceScenarios: 0,
320
333
  },
@@ -323,7 +336,55 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
323
336
  diagnostics,
324
337
  };
325
338
  }
326
- function collectDocuments(root, diagnostics) {
339
+ function historyCandidates(root, selector, offset, diagnostics) {
340
+ if (!Number.isSafeInteger(offset) || offset < 0)
341
+ throw stableError('APPSPEC_HISTORY_OFFSET_INVALID', String(offset));
342
+ const directory = appSpecPaths(root).history;
343
+ const all = [];
344
+ let selected;
345
+ const started = Date.now();
346
+ if (existsSync(directory)) {
347
+ if (!isRegularDirectory(directory))
348
+ diagnostics.push(issue('error', 'APPSPEC_DIRECTORY_INVALID', '历史目录必须是普通目录', 'appspec/changes/history'));
349
+ else {
350
+ const years = readdirSync(directory, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
351
+ for (const year of years.slice(0, 256)) {
352
+ const path = join(directory, year.name);
353
+ if (year.isSymbolicLink()) {
354
+ diagnostics.push(issue('error', 'APPSPEC_SYMLINK_FORBIDDEN', '历史记录不能使用符号链接', relativePath(root, path)));
355
+ continue;
356
+ }
357
+ if (year.isFile() && year.name.endsWith('.md'))
358
+ all.push(path);
359
+ else if (year.isDirectory()) {
360
+ // 稳定 ID 的读取不依赖当前索引页;正文仍受单文件与上下文预算约束。
361
+ const direct = selector && CHANGE_ID.test(selector) ? join(path, `${selector}.md`) : undefined;
362
+ if (direct && existsSync(direct))
363
+ selected = direct;
364
+ if (all.length >= 100_000 || Date.now() - started > 5_000)
365
+ continue;
366
+ for (const entry of readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
367
+ if (all.length >= 100_000)
368
+ break;
369
+ const file = join(path, entry.name);
370
+ if (entry.isFile() && entry.name.endsWith('.md'))
371
+ all.push(file);
372
+ else if (entry.isSymbolicLink() || entry.isDirectory())
373
+ diagnostics.push(issue('warning', 'APPSPEC_HISTORY_ENTRY_INVALID', '历史只索引 history/<year>/*.md 普通文件', relativePath(root, file)));
374
+ }
375
+ }
376
+ }
377
+ if (years.length > 256 || all.length >= 100_000 || Date.now() - started > 5_000)
378
+ diagnostics.push(issue('warning', 'APPSPEC_HISTORY_INDEX_LIMIT', '历史索引达到读取预算;记录仍保留,可用稳定变更 ID 读取正文', 'appspec/changes/history'));
379
+ }
380
+ }
381
+ const paths = all.slice(offset, offset + 50);
382
+ const match = selected || (selector ? all.find(path => basename(path) === `${selector}.md`) : undefined);
383
+ if (match && !paths.includes(match))
384
+ paths.push(match);
385
+ return { paths, page: { offset, limit: 50, total: all.length, nextOffset: offset + 50 < all.length ? offset + 50 : null } };
386
+ }
387
+ function collectDocuments(root, diagnostics, selector, historyOffset = 0) {
327
388
  const paths = appSpecPaths(root);
328
389
  const result = {
329
390
  app: [],
@@ -331,7 +392,9 @@ function collectDocuments(root, diagnostics) {
331
392
  activeChanges: [],
332
393
  history: [],
333
394
  decisions: [],
395
+ designs: [],
334
396
  bytes: 0,
397
+ historyPage: { offset: historyOffset, limit: 50, total: 0, nextOffset: null },
335
398
  };
336
399
  const candidates = [];
337
400
  if (existsSync(paths.app))
@@ -342,19 +405,22 @@ function collectDocuments(root, diagnostics) {
342
405
  })), ...markdownFiles(paths.activeChanges, false, diagnostics, root).map(path => ({
343
406
  path,
344
407
  kind: "change",
345
- })), ...markdownFiles(paths.history, true, diagnostics, root).map(path => ({
346
- path,
347
- kind: "change",
348
- history: true,
349
408
  })), ...markdownFiles(paths.decisions, false, diagnostics, root).map(path => ({
350
409
  path,
351
410
  kind: "decision",
352
411
  })));
412
+ for (const directory of ['product', 'experience', 'design', 'reviews']) {
413
+ candidates.push(...markdownFiles(join(paths.root, directory), false, diagnostics, root).map(path => ({ path, kind: 'design' })));
414
+ }
353
415
  if (candidates.length > APP_SPEC_LIMITS.maximumFiles) {
354
416
  diagnostics.push(issue("error", "APPSPEC_FILE_LIMIT_EXCEEDED", `AppSpec 文件数 ${candidates.length} 超过上限 ${APP_SPEC_LIMITS.maximumFiles}`, "appspec"));
355
417
  candidates.splice(APP_SPEC_LIMITS.maximumFiles);
356
418
  }
357
- for (const candidate of candidates.sort((left, right) => left.path.localeCompare(right.path))) {
419
+ const history = historyCandidates(root, selector, historyOffset, diagnostics);
420
+ result.historyPage = history.page;
421
+ candidates.push(...history.paths.map(path => ({ path, kind: "change", history: true })));
422
+ let historyBytes = 0;
423
+ for (const candidate of candidates.sort((left, right) => Number(!!left.history) - Number(!!right.history) || left.path.localeCompare(right.path))) {
358
424
  let stat;
359
425
  try {
360
426
  stat = lstatSync(candidate.path);
@@ -371,12 +437,30 @@ function collectDocuments(root, diagnostics) {
371
437
  diagnostics.push(issue("error", "APPSPEC_FILE_SIZE_EXCEEDED", `AppSpec 单文件超过 ${APP_SPEC_LIMITS.maximumFileBytes} 字节上限`, relativePath(root, candidate.path)));
372
438
  continue;
373
439
  }
374
- if (result.bytes + stat.size > APP_SPEC_LIMITS.maximumTotalBytes) {
440
+ const indexOnly = candidate.history && basename(candidate.path) !== `${selector}.md`;
441
+ const readBytes = indexOnly ? Math.min(stat.size, 8192) : stat.size;
442
+ if ((candidate.history ? historyBytes : result.bytes) + readBytes > APP_SPEC_LIMITS.maximumTotalBytes) {
375
443
  diagnostics.push(issue("error", "APPSPEC_TOTAL_SIZE_EXCEEDED", `AppSpec 总大小超过 ${APP_SPEC_LIMITS.maximumTotalBytes} 字节上限`, "appspec"));
376
444
  break;
377
445
  }
378
- result.bytes += stat.size;
379
- const content = readFileSync(candidate.path, "utf8");
446
+ if (candidate.history)
447
+ historyBytes += readBytes;
448
+ else
449
+ result.bytes += readBytes;
450
+ let content;
451
+ if (indexOnly) {
452
+ const descriptor = openSync(candidate.path, 'r');
453
+ try {
454
+ const buffer = Buffer.alloc(readBytes);
455
+ const bytes = readSync(descriptor, buffer, 0, readBytes, 0);
456
+ content = buffer.subarray(0, bytes).toString('utf8').match(/^---\r?\n[\s\S]*?\r?\n---/)?.[0] || '';
457
+ }
458
+ finally {
459
+ closeSync(descriptor);
460
+ }
461
+ }
462
+ else
463
+ content = readFileSync(candidate.path, "utf8");
380
464
  const document = parseAppSpecDocument(root, candidate.path, candidate.kind, content, diagnostics);
381
465
  if (!document)
382
466
  continue;
@@ -384,6 +468,8 @@ function collectDocuments(root, diagnostics) {
384
468
  result.app.push(document);
385
469
  else if (candidate.kind === "capability")
386
470
  result.capabilities.push(document);
471
+ else if (candidate.kind === "design")
472
+ result.designs.push(document);
387
473
  else if (candidate.kind === "decision")
388
474
  result.decisions.push(document);
389
475
  else if (candidate.history)
@@ -411,12 +497,12 @@ function validateDocuments(collection, contract, diagnostics) {
411
497
  diagnostics.push(issue("error", "APPSPEC_HISTORY_CHANGE_STATUS_INVALID", `历史目录中的 ChangeSpec 必须是 archived 或 cancelled,当前为 ${document.status}`, document.path));
412
498
  }
413
499
  }
414
- duplicateDiagnostics([...collection.capabilities, ...collection.activeChanges, ...collection.history, ...collection.decisions], document => document.id, "APPSPEC_DOCUMENT_ID_DUPLICATED", diagnostics);
415
- duplicateDiagnostics(collection.capabilities.flatMap(document => document.requirementIds.map(id => ({ ...document, id }))), document => document.id, "APPSPEC_REQUIREMENT_ID_DUPLICATED", diagnostics);
416
- duplicateDiagnostics(collection.capabilities.flatMap(document => document.acceptanceIds.map(id => ({ ...document, id }))), document => document.id, "APPSPEC_ACCEPTANCE_ID_DUPLICATED", diagnostics);
500
+ duplicateDiagnostics([...collection.capabilities, ...collection.activeChanges, ...collection.history, ...collection.decisions, ...collection.designs], document => document.id, "APPSPEC_DOCUMENT_ID_DUPLICATED", diagnostics);
501
+ duplicateDiagnostics([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(document => document.requirementIds.map(id => ({ ...document, id }))), document => document.id, "APPSPEC_REQUIREMENT_ID_DUPLICATED", diagnostics);
502
+ duplicateDiagnostics([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(document => document.acceptanceIds.map(id => ({ ...document, id }))), document => document.id, "APPSPEC_ACCEPTANCE_ID_DUPLICATED", diagnostics);
417
503
  const capabilityIds = new Set(collection.capabilities.map(document => document.id));
418
- const requirementIds = new Set(collection.capabilities.flatMap(document => document.requirementIds));
419
- const acceptanceIds = new Set(collection.capabilities.flatMap(document => document.acceptanceIds));
504
+ const requirementIds = new Set([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(document => document.requirementIds));
505
+ const acceptanceIds = new Set([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(document => document.acceptanceIds));
420
506
  const resourceCodes = new Set(contract.resourceCodes);
421
507
  const actionCodes = new Set(contract.actionCodes);
422
508
  const decisionIds = new Set(collection.decisions.map(document => document.id));
@@ -436,7 +522,7 @@ function validateDocuments(collection, contract, diagnostics) {
436
522
  }
437
523
  for (const id of document.references.requirements) {
438
524
  if (!requirementIds.has(id)) {
439
- diagnostics.push(issue(referenceSeverity, "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `ChangeSpec 引用的需求 ${id} 尚未进入当前 CapabilitySpec`, document.path));
525
+ diagnostics.push(issue(referenceSeverity, "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `ChangeSpec 引用的需求 ${id} 尚未进入当前总纲或能力规格`, document.path));
440
526
  }
441
527
  }
442
528
  for (const id of document.references.decisions) {
@@ -460,13 +546,17 @@ function validateDocuments(collection, contract, diagnostics) {
460
546
  }
461
547
  }
462
548
  function validateCloseConvergence(document, collection, contract, diagnostics) {
549
+ for (const title of ['验证与发布', '交接']) {
550
+ if (!hasMeaningfulSection(document.content, [title], []))
551
+ diagnostics.push(issue('error', 'APPSPEC_HANDOFF_INCOMPLETE', `关闭前补齐“${title}”的实际结果;未发布或取消时明确原因`, `${document.path}#${title}`));
552
+ }
463
553
  const currentSpec = currentSpecStatus(document);
464
554
  if (currentSpec === "pending") {
465
555
  diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_NOT_CONVERGED", "关闭前必须确认 currentSpec=merged 或 not-applicable;该检查只约束 spec close", document.path));
466
556
  }
467
557
  const capabilityIds = new Set(collection.capabilities.map(item => item.id));
468
- const requirementIds = new Set(collection.capabilities.flatMap(item => item.requirementIds));
469
- const acceptanceIds = new Set(collection.capabilities.flatMap(item => item.acceptanceIds));
558
+ const requirementIds = new Set([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(item => item.requirementIds));
559
+ const acceptanceIds = new Set([...collection.app, ...collection.capabilities, ...collection.designs].flatMap(item => item.acceptanceIds));
470
560
  const decisionIds = new Set(collection.decisions.map(item => item.id));
471
561
  for (const id of document.references.capabilities) {
472
562
  const matches = collection.capabilities.filter(item => item.id === id);
@@ -478,9 +568,9 @@ function validateCloseConvergence(document, collection, contract, diagnostics) {
478
568
  }
479
569
  }
480
570
  for (const id of document.references.requirements) {
481
- const matches = collection.capabilities.filter(item => item.requirementIds.includes(id));
571
+ const matches = [...collection.app, ...collection.capabilities, ...collection.designs].filter(item => item.requirementIds.includes(id));
482
572
  if (!requirementIds.has(id)) {
483
- diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `关闭前必须把需求 ${id} 合入当前 CapabilitySpec`, document.path));
573
+ diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `关闭前必须把需求 ${id} 合入当前总纲或能力规格`, document.path));
484
574
  }
485
575
  else if (matches.length > 1) {
486
576
  diagnostics.push(issue("error", "APPSPEC_REQUIREMENT_ID_DUPLICATED", `关闭前必须消除需求 ${id} 的重复定义`, document.path));
@@ -496,13 +586,13 @@ function validateCloseConvergence(document, collection, contract, diagnostics) {
496
586
  }
497
587
  }
498
588
  for (const id of document.requirementIds) {
499
- const matches = collection.capabilities.filter(item => item.requirementIds.includes(id));
589
+ const matches = [...collection.app, ...collection.capabilities, ...collection.designs].filter(item => item.requirementIds.includes(id));
500
590
  if (matches.length > 1) {
501
591
  diagnostics.push(issue("error", "APPSPEC_REQUIREMENT_ID_DUPLICATED", `关闭前必须消除需求 ${id} 的重复定义`, document.path));
502
592
  }
503
593
  }
504
594
  for (const id of document.acceptanceIds) {
505
- const matches = collection.capabilities.filter(item => item.acceptanceIds.includes(id));
595
+ const matches = [...collection.app, ...collection.capabilities, ...collection.designs].filter(item => item.acceptanceIds.includes(id));
506
596
  if (matches.length > 1) {
507
597
  diagnostics.push(issue("error", "APPSPEC_ACCEPTANCE_ID_DUPLICATED", `关闭前必须消除验收场景 ${id} 的重复定义`, document.path));
508
598
  }
@@ -514,12 +604,12 @@ function validateCloseConvergence(document, collection, contract, diagnostics) {
514
604
  function validateEmbeddedCurrentSpecIds(document, currentRequirementIds, currentAcceptanceIds, diagnostics) {
515
605
  for (const id of document.requirementIds) {
516
606
  if (!currentRequirementIds.has(id)) {
517
- diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_NOT_CURRENT", `ChangeSpec 中的长期需求 ${id} 尚未进入当前 CapabilitySpec`, document.path));
607
+ diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_NOT_CURRENT", `ChangeSpec 中的长期需求 ${id} 尚未进入当前总纲或能力规格`, document.path));
518
608
  }
519
609
  }
520
610
  for (const id of document.acceptanceIds) {
521
611
  if (!currentAcceptanceIds.has(id)) {
522
- diagnostics.push(issue("error", "APPSPEC_CHANGE_ACCEPTANCE_NOT_CURRENT", `ChangeSpec 中的验收场景 ${id} 尚未进入当前 CapabilitySpec`, document.path));
612
+ diagnostics.push(issue("error", "APPSPEC_CHANGE_ACCEPTANCE_NOT_CURRENT", `ChangeSpec 中的验收场景 ${id} 尚未进入当前总纲或能力规格`, document.path));
523
613
  }
524
614
  }
525
615
  }
@@ -575,7 +665,7 @@ function validateRiskSections(document, diagnostics) {
575
665
  }
576
666
  }
577
667
  }
578
- function selectContext(collection, selector, diagnostics) {
668
+ function selectBaseContext(collection, selector, diagnostics) {
579
669
  if (!selector) {
580
670
  return {
581
671
  primary: collection.app[0] || null,
@@ -585,6 +675,9 @@ function selectContext(collection, selector, diagnostics) {
585
675
  decisions: [],
586
676
  };
587
677
  }
678
+ const design = collection.designs.find(document => document.id === selector);
679
+ if (design)
680
+ return { primary: design, capabilities: [], activeChanges: [], archivedChanges: [], decisions: [] };
588
681
  const change = collection.activeChanges.find(document => document.id === selector);
589
682
  if (change) {
590
683
  const capabilityIds = new Set(change.references.capabilities);
@@ -640,12 +733,43 @@ function selectContext(collection, selector, diagnostics) {
640
733
  decisions: [],
641
734
  };
642
735
  }
736
+ function selectContext(collection, selector, diagnostics) {
737
+ const base = selectBaseContext(collection, selector, diagnostics);
738
+ const designs = [];
739
+ const selected = { ...base, capabilities: [...base.capabilities], decisions: [...base.decisions], designs };
740
+ if (!selector)
741
+ return selected;
742
+ const available = [...collection.app, ...collection.capabilities, ...collection.decisions, ...collection.designs];
743
+ const queue = [base.primary, ...base.capabilities, ...base.decisions].filter((item) => !!item);
744
+ const seen = new Set();
745
+ for (let index = 0; index < queue.length; index++) {
746
+ const document = queue[index];
747
+ if (seen.has(document.id))
748
+ continue;
749
+ seen.add(document.id);
750
+ if (document.kind === 'design')
751
+ designs.push(document);
752
+ if (document.kind === 'capability' && !selected.capabilities.includes(document))
753
+ selected.capabilities.push(document);
754
+ if (document.kind === 'decision' && !selected.decisions.includes(document))
755
+ selected.decisions.push(document);
756
+ for (const id of document.references.documents) {
757
+ const target = available.find(item => item.id === id);
758
+ if (!target)
759
+ diagnostics.push(issue('error', 'APPSPEC_DESIGN_REFERENCE_UNKNOWN', `设计引用 ${id} 不存在;先恢复资料或修正引用`, document.path));
760
+ else if (!seen.has(id))
761
+ queue.push(target);
762
+ }
763
+ }
764
+ return selected;
765
+ }
643
766
  function summarizeCollection(collection) {
644
767
  return {
645
768
  application: collection.app[0] ? summarize(collection.app[0]) : null,
646
769
  capabilities: collection.capabilities.map(summarize),
647
770
  activeChanges: collection.activeChanges.map(summarize),
648
771
  decisions: collection.decisions.map(summarize),
772
+ designs: collection.designs.map(summarize),
649
773
  history: collection.history.map(summarize),
650
774
  };
651
775
  }
@@ -657,6 +781,7 @@ function selectedDocuments(application, selected) {
657
781
  ...selected.activeChanges,
658
782
  ...selected.archivedChanges,
659
783
  ...selected.decisions,
784
+ ...selected.designs,
660
785
  ]);
661
786
  }
662
787
  function applyContextBudget(application, selected) {
@@ -680,6 +805,7 @@ function applyContextBudget(application, selected) {
680
805
  activeChanges: keep(selected.activeChanges),
681
806
  archivedChanges: keep(selected.archivedChanges),
682
807
  decisions: keep(selected.decisions),
808
+ designs: keep(selected.designs),
683
809
  contextBudget: {
684
810
  maximumBytes: APP_SPEC_LIMITS.maximumContextBytes,
685
811
  contentBytes,
@@ -735,20 +861,24 @@ function parseAppSpecDocument(root, path, kind, content, diagnostics) {
735
861
  ? CAPABILITY_ID
736
862
  : kind === "decision"
737
863
  ? DECISION_ID
738
- : CHANGE_ID;
864
+ : kind === "design" ? /^DES-[A-Z0-9]+(?:-[A-Z0-9]+)*$/ : CHANGE_ID;
739
865
  if (!pattern.test(id)) {
740
- diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_INVALID", `无效 ${kind} id: ${id}`, pointer));
866
+ diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_INVALID", `无效 ${kind} id: ${id}${kind === 'decision' ? ';请使用 ADR- 加四位数字,可选大写字母/数字后缀,例如 ADR-0001 或 ADR-0001-DATA-OWNER,参见 docs appspec' : ''}`, pointer));
741
867
  }
742
868
  const status = String(frontMatter.metadata.status || "draft");
743
869
  const validStatuses = {
744
870
  app: ["active", "retired"],
745
871
  capability: ["draft", "active", "retired"],
746
872
  change: ["draft", "confirmed", "implementing", "verified", "released", "archived", "cancelled"],
873
+ design: ["draft", "confirmed", "superseded", "rejected"],
747
874
  decision: ["proposed", "accepted", "superseded", "rejected"],
748
875
  };
749
876
  if (!validStatuses[kind].includes(status)) {
750
877
  diagnostics.push(issue("error", "APPSPEC_STATUS_INVALID", `${kind} status=${status} 不受支持`, pointer));
751
878
  }
879
+ if (kind === 'design' && !['sources', 'product', 'journey', 'page', 'visual', 'permissions', 'architecture', 'review'].includes(String(frontMatter.metadata.type))) {
880
+ diagnostics.push(issue('error', 'APPSPEC_DESIGN_TYPE_INVALID', '设计资料需要明确 type,参见 docs product-design', pointer));
881
+ }
752
882
  if (kind === "change") {
753
883
  const currentSpec = String(frontMatter.metadata.currentSpec || "pending");
754
884
  if (!["pending", "merged", "not-applicable"].includes(currentSpec)) {
@@ -774,6 +904,7 @@ function parseAppSpecDocument(root, path, kind, content, diagnostics) {
774
904
  resources: metadataStrings(frontMatter.metadata, "resources"),
775
905
  actions: metadataStrings(frontMatter.metadata, "actions"),
776
906
  decisions: metadataStrings(frontMatter.metadata, "decisions"),
907
+ documents: metadataStrings(frontMatter.metadata, "documents"),
777
908
  },
778
909
  content,
779
910
  };
@@ -984,7 +1115,7 @@ status: active
984
1115
  ---
985
1116
  # ${appName}
986
1117
 
987
- AppSpec 是可选的业务意图辅助层,不是发布门禁。资源、字段、权限和动作仍以
1118
+ 本文件是当前有效设计的总纲与目录。新应用先完成产品发现、详细设计与实际确认,形成评审基线后再制定实施计划;按 docs product-design 在 AppSpec 中建立相关材料。正式变更关联 ChangeSpec;测试发布前核对设计与验收计划,生产晋级前核对实际验收。资源、字段、权限和动作的实现以
988
1119
  \`openxiangda.config.ts\` 与实时编译合同为准。
989
1120
 
990
1121
  ## 业务目标
@@ -1001,14 +1132,34 @@ AppSpec 是可选的业务意图辅助层,不是发布门禁。资源、字段
1001
1132
 
1002
1133
  ### 不包含
1003
1134
 
1135
+ ## 设计资料目录
1136
+
1137
+ <!-- 引用产品、旅程、页面、视觉、权限、架构与评审的稳定 ID;详细规则各有唯一归属。 -->
1138
+
1004
1139
  ## 能力目录
1005
1140
 
1006
- <!-- 复杂业务再在 capabilities/ 下增加 CAP-*;小应用可以只维护本文件。 -->
1141
+ <!-- 复杂业务再在 capabilities/ 下增加 CAP-*;简单应用的稳定规则直接写在本文件。 -->
1007
1142
 
1008
1143
  ## 术语
1009
1144
 
1010
1145
  ## 跨能力约束
1011
1146
 
1147
+ ## 架构与数据关系
1148
+
1149
+ <!-- 写明领域边界、核心模型关系、平台能力与自定义动作的归属、状态转换与不变量。 -->
1150
+
1151
+ ## 业务任务与页面
1152
+
1153
+ <!-- 按用户任务规划管理端、PC、移动入口与数据关系;辅助表不必有页面。 -->
1154
+
1155
+ ## 权限矩阵与确认
1156
+
1157
+ <!-- 记录角色、页面、操作、行范围、字段和多角色组合;写明需求依据与未明确的边界。 -->
1158
+
1159
+ ## 性能与容量预算
1160
+
1161
+ <!-- 记录数据量及增长、并发、分页与索引、请求次数、延迟目标和测量口径;区分估算、目标与实测。 -->
1162
+
1012
1163
  ## 未确认问题
1013
1164
 
1014
1165
  - 无。
@@ -1040,9 +1191,9 @@ actions: ${JSON.stringify(input.actions)}
1040
1191
 
1041
1192
  #### AC-DOMAIN-001-01 正向或反向场景
1042
1193
 
1043
- - Given 已知前置条件
1044
- - When 用户或系统执行动作
1045
- - Then 观察到明确结果
1194
+ - 前置:已知条件
1195
+ - 操作:用户或系统执行动作
1196
+ - 预期:观察到明确结果
1046
1197
  -->
1047
1198
 
1048
1199
  ## 状态与异常
@@ -1072,6 +1223,22 @@ decisions: []
1072
1223
 
1073
1224
  ${input.summary || "<!-- 一两句话说明问题、证据和期望结果。 -->"}
1074
1225
 
1226
+ ## 需求依据
1227
+
1228
+ <!-- 记录用户原始需求或当前规格引用、已明确规则与假设;不代替用户编造确认。 -->
1229
+
1230
+ ## 方案与影响
1231
+
1232
+ <!-- 说明能力所有者、模型/页面/权限/状态影响,引用已有架构决定。 -->
1233
+
1234
+ ## 任务与实现
1235
+
1236
+ <!-- 用稳定需求 ID 关联实施任务、源码/声明路径与验收场景;实施后更新实际结果。 -->
1237
+
1238
+ ## 性能与容量预算
1239
+
1240
+ <!-- 引用总纲默认预算,或记录本次数据规模、查询/分页/批量/超时/并发和延迟目标。 -->
1241
+
1075
1242
  ## 变更
1076
1243
 
1077
1244
  - ADDED:
@@ -1084,6 +1251,8 @@ ${input.summary || "<!-- 一两句话说明问题、证据和期望结果。 -->
1084
1251
  - [ ] 一个可观察的正向结果
1085
1252
  - [ ] 需要时补充拒绝、异常或权限反例
1086
1253
 
1254
+ <!-- 使用 #### AC-领域-编号 给场景稳定 ID;写明角色、前置、操作、预期和拒绝路径。测试部署后再记录实际观察。 -->
1255
+
1087
1256
  ## 数据与权限
1088
1257
 
1089
1258
  - 无,或说明资源、字段、角色和数据范围变化。
@@ -1100,6 +1269,14 @@ ${input.summary || "<!-- 一两句话说明问题、证据和期望结果。 -->
1100
1269
 
1101
1270
  - L3 如涉及 ADR,在 front matter 的 decisions 中引用。
1102
1271
 
1272
+ ## 验证与发布
1273
+
1274
+ <!-- 记录检查结果、源码提交、测试/生产运行 ID、包摘要、验收报告路径、未覆盖项和恢复入口。区分本地通过、部署成功与业务验收。 -->
1275
+
1276
+ ## 交接
1277
+
1278
+ <!-- 写明持续有效规则已更新的位置、剩余问题与下一步;无需复制当前规格全文。 -->
1279
+
1103
1280
  ## 未确认问题
1104
1281
 
1105
1282
  - 无。
@@ -1170,8 +1347,8 @@ function issue(severity, code, message, path) {
1170
1347
  path,
1171
1348
  retryable: false,
1172
1349
  remediation: severity === "error"
1173
- ? "修正 AppSpec 后重新运行 openxiangda spec check;普通应用 check/deploy 不受阻断"
1174
- : "按当前变更风险决定是否补充;AppSpec 质量建议不阻断应用交付",
1350
+ ? "修正记录后运行 openxiangda spec check;普通 check 可继续,正式发布按阶段核对"
1351
+ : "按实际变化范围补充;确定性缺口会在相应发布阶段核对",
1175
1352
  };
1176
1353
  }
1177
1354
  function summarize(document) {
@@ -1198,9 +1375,11 @@ function withoutHtmlComments(content) {
1198
1375
  }
1199
1376
  function validateMetadata(kind, metadata, pointer, diagnostics) {
1200
1377
  const allowed = {
1201
- app: new Set(["schema", "app", "title", "status"]),
1378
+ app: new Set(["schema", "app", "title", "status", "documents"]),
1379
+ design: new Set(["schema", "id", "title", "status", "type", "documents", "scope", "baselineDigest", "confirmedBy", "confirmedAt", "confirmationSource", "capabilities", "requirements", "resources", "actions", "decisions"]),
1202
1380
  capability: new Set([
1203
1381
  "schema",
1382
+ "documents",
1204
1383
  "id",
1205
1384
  "title",
1206
1385
  "status",
@@ -1209,6 +1388,7 @@ function validateMetadata(kind, metadata, pointer, diagnostics) {
1209
1388
  ]),
1210
1389
  change: new Set([
1211
1390
  "schema",
1391
+ "documents",
1212
1392
  "id",
1213
1393
  "title",
1214
1394
  "status",
@@ -1224,6 +1404,7 @@ function validateMetadata(kind, metadata, pointer, diagnostics) {
1224
1404
  ]),
1225
1405
  decision: new Set([
1226
1406
  "schema",
1407
+ "documents",
1227
1408
  "id",
1228
1409
  "title",
1229
1410
  "status",