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.
- package/LICENSE +21 -0
- package/README.md +3 -9
- package/dist/app-spec.d.ts +30 -12
- package/dist/app-spec.d.ts.map +1 -1
- package/dist/app-spec.js +238 -57
- package/dist/app-spec.js.map +1 -1
- package/dist/application-services.d.ts +625 -42
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +321 -85
- package/dist/application-services.js.map +1 -1
- package/dist/application-ui-contract.d.ts +5 -0
- package/dist/application-ui-contract.d.ts.map +1 -0
- package/dist/application-ui-contract.js +149 -0
- package/dist/application-ui-contract.js.map +1 -0
- package/dist/backend-image-build.js +6 -11
- package/dist/backend-image-build.js.map +1 -1
- package/dist/command-process.d.ts +14 -0
- package/dist/command-process.d.ts.map +1 -0
- package/dist/command-process.js +51 -0
- package/dist/command-process.js.map +1 -0
- package/dist/command-registry.d.ts +44 -4
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +10 -6
- package/dist/command-registry.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts.map +1 -1
- package/dist/compiler/ai-catalog.js +2 -1
- package/dist/compiler/ai-catalog.js.map +1 -1
- package/dist/compiler/application-model.d.ts +60 -0
- package/dist/compiler/application-model.d.ts.map +1 -0
- package/dist/compiler/application-model.js +141 -0
- package/dist/compiler/application-model.js.map +1 -0
- package/dist/compiler/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +196 -98
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +36 -10
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +536 -171
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/field-surface.js +1 -1
- package/dist/compiler/field-surface.js.map +1 -1
- package/dist/compiler/package-compiler.d.ts.map +1 -1
- package/dist/compiler/package-compiler.js +12 -0
- package/dist/compiler/package-compiler.js.map +1 -1
- package/dist/compiler/permission-review.d.ts +139 -0
- package/dist/compiler/permission-review.d.ts.map +1 -0
- package/dist/compiler/permission-review.js +131 -0
- package/dist/compiler/permission-review.js.map +1 -0
- package/dist/configuration-preflight.d.ts +7 -0
- package/dist/configuration-preflight.d.ts.map +1 -0
- package/dist/configuration-preflight.js +13 -0
- package/dist/configuration-preflight.js.map +1 -0
- package/dist/control-plane-client.d.ts +56 -61
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +89 -8
- package/dist/control-plane-client.js.map +1 -1
- package/dist/delivery-cache.d.ts +34 -0
- package/dist/delivery-cache.d.ts.map +1 -0
- package/dist/delivery-cache.js +186 -0
- package/dist/delivery-cache.js.map +1 -0
- package/dist/delivery-source.d.ts +17 -0
- package/dist/delivery-source.d.ts.map +1 -0
- package/dist/delivery-source.js +85 -0
- package/dist/delivery-source.js.map +1 -0
- package/dist/deployment-observer.d.ts +14 -0
- package/dist/deployment-observer.d.ts.map +1 -0
- package/dist/deployment-observer.js +68 -0
- package/dist/deployment-observer.js.map +1 -0
- package/dist/deployment.d.ts +17 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +55 -15
- package/dist/deployment.js.map +1 -1
- package/dist/design-readiness.d.ts +24 -0
- package/dist/design-readiness.d.ts.map +1 -0
- package/dist/design-readiness.js +102 -0
- package/dist/design-readiness.js.map +1 -0
- package/dist/developer-errors.d.ts +11 -0
- package/dist/developer-errors.d.ts.map +1 -0
- package/dist/developer-errors.js +16 -0
- package/dist/developer-errors.js.map +1 -0
- package/dist/developer-operations.d.ts +190 -0
- package/dist/developer-operations.d.ts.map +1 -0
- package/dist/developer-operations.js +65 -0
- package/dist/developer-operations.js.map +1 -0
- package/dist/development-lifecycle.d.ts +75 -0
- package/dist/development-lifecycle.d.ts.map +1 -0
- package/dist/development-lifecycle.js +189 -0
- package/dist/development-lifecycle.js.map +1 -0
- package/dist/documentation.d.ts +111 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +112 -0
- package/dist/documentation.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/operation-progress.d.ts +26 -0
- package/dist/operation-progress.d.ts.map +1 -0
- package/dist/operation-progress.js +92 -0
- package/dist/operation-progress.js.map +1 -0
- package/dist/optional-backend.d.ts +14 -0
- package/dist/optional-backend.d.ts.map +1 -0
- package/dist/optional-backend.js +145 -0
- package/dist/optional-backend.js.map +1 -0
- package/dist/session.d.ts +22 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +36 -0
- package/dist/session.js.map +1 -1
- package/dist/workspace-loader.d.ts.map +1 -1
- package/dist/workspace-loader.js +17 -2
- package/dist/workspace-loader.js.map +1 -1
- package/dist/workspace-operation.d.ts +3 -0
- package/dist/workspace-operation.d.ts.map +1 -0
- package/dist/workspace-operation.js +45 -0
- package/dist/workspace-operation.js.map +1 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +18 -4
- package/templates/backend/Dockerfile +20 -0
- package/templates/backend/package.json +27 -0
- package/templates/backend/src/app.module.ts +23 -0
- package/templates/backend/src/main.ts +5 -0
- 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
|
-
|
|
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:
|
|
53
|
-
releaseGate:
|
|
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:
|
|
76
|
-
releaseGate:
|
|
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:
|
|
106
|
-
releaseGate:
|
|
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:
|
|
175
|
-
releaseGate:
|
|
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:
|
|
190
|
-
releaseGate:
|
|
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: "
|
|
231
|
-
releaseGate:
|
|
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.
|
|
254
|
+
historyChanges: collection.historyPage.total,
|
|
248
255
|
decisions: collection.decisions.length,
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
"
|
|
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: "
|
|
289
|
-
releaseGate:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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
|
-
-
|
|
1044
|
-
-
|
|
1045
|
-
-
|
|
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
|
-
? "
|
|
1174
|
-
: "
|
|
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",
|