openxiangda-devkit-core 2.0.0-alpha.66 → 2.0.0-alpha.68
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/dist/app-spec.d.ts +47 -10
- package/dist/app-spec.d.ts.map +1 -1
- package/dist/app-spec.js +344 -40
- package/dist/app-spec.js.map +1 -1
- package/dist/application-services.d.ts +38 -7
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +21 -3
- package/dist/application-services.js.map +1 -1
- package/dist/command-registry.d.ts +4 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +6 -0
- package/dist/command-registry.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -0
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/package-compiler.d.ts.map +1 -1
- package/dist/compiler/package-compiler.js +2 -1
- package/dist/compiler/package-compiler.js.map +1 -1
- package/dist/compiler/schema-composition.d.ts +21 -0
- package/dist/compiler/schema-composition.d.ts.map +1 -0
- package/dist/compiler/schema-composition.js +185 -0
- package/dist/compiler/schema-composition.js.map +1 -0
- package/dist/deployment.d.ts +2 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +49 -4
- package/dist/deployment.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/app-spec.js
CHANGED
|
@@ -7,12 +7,13 @@ 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/
|
|
10
|
+
context: "openxiangda.appspec/context/v2",
|
|
11
11
|
};
|
|
12
12
|
export const APP_SPEC_LIMITS = {
|
|
13
13
|
maximumFiles: 128,
|
|
14
14
|
maximumFileBytes: 256 * 1024,
|
|
15
15
|
maximumTotalBytes: 2 * 1024 * 1024,
|
|
16
|
+
maximumContextBytes: 256 * 1024,
|
|
16
17
|
maximumDirectoryEntries: 256,
|
|
17
18
|
maximumHistoryDepth: 1,
|
|
18
19
|
};
|
|
@@ -126,13 +127,61 @@ export function closeAppSpecChange(input) {
|
|
|
126
127
|
}
|
|
127
128
|
mkdirSync(historyDirectory, { recursive: true });
|
|
128
129
|
const sourceContent = readFileSync(source, "utf8");
|
|
129
|
-
const
|
|
130
|
+
const effectiveContent = input.currentSpec
|
|
131
|
+
? setFrontMatterValue(sourceContent, "currentSpec", input.currentSpec)
|
|
132
|
+
: sourceContent;
|
|
133
|
+
const diagnostics = [];
|
|
134
|
+
const document = parseAppSpecDocument(input.root, source, "change", effectiveContent, diagnostics);
|
|
130
135
|
if (!document || document.id !== input.id) {
|
|
131
136
|
throw stableError("APPSPEC_ACTIVE_CHANGE_INVALID", input.id);
|
|
132
137
|
}
|
|
138
|
+
const collectionDiagnostics = [];
|
|
139
|
+
const collection = collectDocuments(input.root, collectionDiagnostics);
|
|
140
|
+
const sourcePointer = relativePath(input.root, source);
|
|
141
|
+
const relatedPaths = new Set([sourcePointer]);
|
|
142
|
+
for (const capability of collection.capabilities) {
|
|
143
|
+
if (document.references.capabilities.includes(capability.id) ||
|
|
144
|
+
capability.requirementIds.some(id => [...document.references.requirements, ...document.requirementIds].includes(id)) ||
|
|
145
|
+
capability.acceptanceIds.some(id => document.acceptanceIds.includes(id))) {
|
|
146
|
+
relatedPaths.add(capability.path);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const decision of collection.decisions) {
|
|
150
|
+
if (document.references.decisions.includes(decision.id)) {
|
|
151
|
+
relatedPaths.add(decision.path);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const collectionFatalCodes = new Set([
|
|
155
|
+
"APPSPEC_FILE_LIMIT_EXCEEDED",
|
|
156
|
+
"APPSPEC_FILE_UNREADABLE",
|
|
157
|
+
"APPSPEC_FILE_INVALID",
|
|
158
|
+
"APPSPEC_FILE_SIZE_EXCEEDED",
|
|
159
|
+
"APPSPEC_TOTAL_SIZE_EXCEEDED",
|
|
160
|
+
"APPSPEC_DIRECTORY_INVALID",
|
|
161
|
+
"APPSPEC_DIRECTORY_ENTRY_LIMIT_EXCEEDED",
|
|
162
|
+
"APPSPEC_SYMLINK_FORBIDDEN",
|
|
163
|
+
"APPSPEC_HISTORY_DEPTH_EXCEEDED",
|
|
164
|
+
]);
|
|
165
|
+
diagnostics.push(...collectionDiagnostics.filter(item => item.severity === "error" &&
|
|
166
|
+
item.path !== sourcePointer &&
|
|
167
|
+
(collectionFatalCodes.has(item.code) ||
|
|
168
|
+
(item.path !== undefined && relatedPaths.has(item.path)))));
|
|
169
|
+
validateCloseConvergence(document, collection, input.contract, diagnostics);
|
|
170
|
+
if (diagnostics.some(item => item.severity === "error")) {
|
|
171
|
+
return {
|
|
172
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
173
|
+
enabled: true,
|
|
174
|
+
advisory: true,
|
|
175
|
+
releaseGate: false,
|
|
176
|
+
archived: null,
|
|
177
|
+
status: document.status,
|
|
178
|
+
converged: false,
|
|
179
|
+
diagnostics,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
133
182
|
renameSync(source, target);
|
|
134
183
|
const closedAt = new Date().toISOString();
|
|
135
|
-
const updated = appendClosure(setFrontMatterValue(setFrontMatterValue(
|
|
184
|
+
const updated = appendClosure(setFrontMatterValue(setFrontMatterValue(effectiveContent, "status", "archived"), "closedAt", closedAt), input.summary?.trim() || "变更记录已归档;验收与发布结果以实际证据为准。", closedAt);
|
|
136
185
|
atomicReplace(target, updated);
|
|
137
186
|
return {
|
|
138
187
|
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
@@ -141,6 +190,8 @@ export function closeAppSpecChange(input) {
|
|
|
141
190
|
releaseGate: false,
|
|
142
191
|
archived: relativePath(input.root, target),
|
|
143
192
|
status: "archived",
|
|
193
|
+
converged: true,
|
|
194
|
+
diagnostics,
|
|
144
195
|
};
|
|
145
196
|
}
|
|
146
197
|
export function inspectAppSpec(root, contract, selector) {
|
|
@@ -163,18 +214,16 @@ export function inspectAppSpec(root, contract, selector) {
|
|
|
163
214
|
...collection.history,
|
|
164
215
|
...collection.decisions,
|
|
165
216
|
];
|
|
166
|
-
const
|
|
217
|
+
const index = summarizeCollection(collection);
|
|
218
|
+
const workspaceDigest = digestDocuments(allDocuments, contract);
|
|
219
|
+
const selectionDocuments = selectedDocuments(application, selected);
|
|
220
|
+
const selectionDigest = allDocuments.length === 0
|
|
167
221
|
? null
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
contract.configDigest || "",
|
|
174
|
-
contract.contractDigest || "",
|
|
175
|
-
contract.aiCatalogDigest || "",
|
|
176
|
-
].join("\n---\n"))
|
|
177
|
-
.digest("hex");
|
|
222
|
+
: digestDocuments(selectionDocuments, contract, selector ? [`selector:${selector}`] : [`index:${JSON.stringify(index)}`]);
|
|
223
|
+
const budgeted = applyContextBudget(application, selected);
|
|
224
|
+
if (budgeted.contextBudget.truncated) {
|
|
225
|
+
diagnostics.push(issue("warning", "APPSPEC_CONTEXT_BUDGET_EXCEEDED", `相关 AppSpec 正文超过 ${APP_SPEC_LIMITS.maximumContextBytes} 字节预算;已省略 ${budgeted.contextBudget.omitted.length} 份低优先级文档,可按稳定 ID 单独读取`, "appspec"));
|
|
226
|
+
}
|
|
178
227
|
return {
|
|
179
228
|
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
180
229
|
enabled: true,
|
|
@@ -183,11 +232,13 @@ export function inspectAppSpec(root, contract, selector) {
|
|
|
183
232
|
root: "appspec",
|
|
184
233
|
selector: selector || null,
|
|
185
234
|
contract,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
235
|
+
index,
|
|
236
|
+
application: budgeted.application,
|
|
237
|
+
capabilities: budgeted.capabilities,
|
|
238
|
+
activeChanges: budgeted.activeChanges,
|
|
239
|
+
archivedChanges: budgeted.archivedChanges,
|
|
240
|
+
decisions: budgeted.decisions,
|
|
241
|
+
contextBudget: budgeted.contextBudget,
|
|
191
242
|
stats: {
|
|
192
243
|
files: allDocuments.length,
|
|
193
244
|
bytes: collection.bytes,
|
|
@@ -198,7 +249,8 @@ export function inspectAppSpec(root, contract, selector) {
|
|
|
198
249
|
requirements: unique(collection.capabilities.flatMap(document => document.requirementIds)).length,
|
|
199
250
|
acceptanceScenarios: unique(collection.capabilities.flatMap(document => document.acceptanceIds)).length,
|
|
200
251
|
},
|
|
201
|
-
|
|
252
|
+
workspaceDigest,
|
|
253
|
+
selectionDigest,
|
|
202
254
|
diagnostics,
|
|
203
255
|
};
|
|
204
256
|
}
|
|
@@ -215,7 +267,9 @@ export function summarizeAppSpecContext(context) {
|
|
|
215
267
|
enabled: context.enabled,
|
|
216
268
|
mode: context.mode,
|
|
217
269
|
releaseGate: context.releaseGate,
|
|
218
|
-
|
|
270
|
+
workspaceDigest: context.workspaceDigest,
|
|
271
|
+
selectionDigest: context.selectionDigest,
|
|
272
|
+
contextBudget: context.contextBudget,
|
|
219
273
|
stats: context.stats,
|
|
220
274
|
diagnostics: {
|
|
221
275
|
errors: context.diagnostics.filter(item => item.severity === "error").length,
|
|
@@ -236,11 +290,24 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
|
|
|
236
290
|
root: "appspec",
|
|
237
291
|
selector: selector || null,
|
|
238
292
|
contract,
|
|
293
|
+
index: {
|
|
294
|
+
application: null,
|
|
295
|
+
capabilities: [],
|
|
296
|
+
activeChanges: [],
|
|
297
|
+
decisions: [],
|
|
298
|
+
history: [],
|
|
299
|
+
},
|
|
239
300
|
application: null,
|
|
240
301
|
capabilities: [],
|
|
241
302
|
activeChanges: [],
|
|
303
|
+
archivedChanges: [],
|
|
242
304
|
decisions: [],
|
|
243
|
-
|
|
305
|
+
contextBudget: {
|
|
306
|
+
maximumBytes: APP_SPEC_LIMITS.maximumContextBytes,
|
|
307
|
+
contentBytes: 0,
|
|
308
|
+
truncated: false,
|
|
309
|
+
omitted: [],
|
|
310
|
+
},
|
|
244
311
|
stats: {
|
|
245
312
|
files: 0,
|
|
246
313
|
bytes: 0,
|
|
@@ -251,7 +318,8 @@ function emptyContext(contract, selector, diagnostics = [], enabled = false) {
|
|
|
251
318
|
requirements: 0,
|
|
252
319
|
acceptanceScenarios: 0,
|
|
253
320
|
},
|
|
254
|
-
|
|
321
|
+
workspaceDigest: null,
|
|
322
|
+
selectionDigest: null,
|
|
255
323
|
diagnostics,
|
|
256
324
|
};
|
|
257
325
|
}
|
|
@@ -348,6 +416,7 @@ function validateDocuments(collection, contract, diagnostics) {
|
|
|
348
416
|
duplicateDiagnostics(collection.capabilities.flatMap(document => document.acceptanceIds.map(id => ({ ...document, id }))), document => document.id, "APPSPEC_ACCEPTANCE_ID_DUPLICATED", diagnostics);
|
|
349
417
|
const capabilityIds = new Set(collection.capabilities.map(document => document.id));
|
|
350
418
|
const requirementIds = new Set(collection.capabilities.flatMap(document => document.requirementIds));
|
|
419
|
+
const acceptanceIds = new Set(collection.capabilities.flatMap(document => document.acceptanceIds));
|
|
351
420
|
const resourceCodes = new Set(contract.resourceCodes);
|
|
352
421
|
const actionCodes = new Set(contract.actionCodes);
|
|
353
422
|
const decisionIds = new Set(collection.decisions.map(document => document.id));
|
|
@@ -376,7 +445,12 @@ function validateDocuments(collection, contract, diagnostics) {
|
|
|
376
445
|
}
|
|
377
446
|
}
|
|
378
447
|
validateReferences(document, resourceCodes, actionCodes, referenceSeverity, diagnostics);
|
|
448
|
+
validateEmbeddedCurrentSpecIds(document, requirementIds, acceptanceIds, diagnostics);
|
|
379
449
|
validateRiskSections(document, diagnostics);
|
|
450
|
+
if (finalState &&
|
|
451
|
+
currentSpecStatus(document) === "pending") {
|
|
452
|
+
diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_NOT_CONVERGED", "verified/released ChangeSpec 必须确认 currentSpec=merged 或 not-applicable", document.path));
|
|
453
|
+
}
|
|
380
454
|
if (finalState && unresolvedQuestions(document.content).length > 0) {
|
|
381
455
|
diagnostics.push(issue("error", "APPSPEC_VERIFIED_CHANGE_HAS_OPEN_QUESTIONS", "verified/released ChangeSpec 仍有未勾选问题", document.path));
|
|
382
456
|
}
|
|
@@ -385,6 +459,70 @@ function validateDocuments(collection, contract, diagnostics) {
|
|
|
385
459
|
diagnostics.push(issue("warning", "APPSPEC_ACTIVE_CHANGE_COUNT_HIGH", `当前有 ${collection.activeChanges.length} 个活跃变更;建议关闭已完成记录,避免 AI 上下文膨胀`, "appspec/changes/active"));
|
|
386
460
|
}
|
|
387
461
|
}
|
|
462
|
+
function validateCloseConvergence(document, collection, contract, diagnostics) {
|
|
463
|
+
const currentSpec = currentSpecStatus(document);
|
|
464
|
+
if (currentSpec === "pending") {
|
|
465
|
+
diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_NOT_CONVERGED", "关闭前必须确认 currentSpec=merged 或 not-applicable;该检查只约束 spec close", document.path));
|
|
466
|
+
}
|
|
467
|
+
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));
|
|
470
|
+
const decisionIds = new Set(collection.decisions.map(item => item.id));
|
|
471
|
+
for (const id of document.references.capabilities) {
|
|
472
|
+
const matches = collection.capabilities.filter(item => item.id === id);
|
|
473
|
+
if (!capabilityIds.has(id)) {
|
|
474
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_CAPABILITY_UNKNOWN", `关闭前必须建立当前能力引用 ${id}`, document.path));
|
|
475
|
+
}
|
|
476
|
+
else if (matches.length > 1) {
|
|
477
|
+
diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_DUPLICATED", `关闭前必须消除能力 ${id} 的重复定义`, document.path));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const id of document.references.requirements) {
|
|
481
|
+
const matches = collection.capabilities.filter(item => item.requirementIds.includes(id));
|
|
482
|
+
if (!requirementIds.has(id)) {
|
|
483
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `关闭前必须把需求 ${id} 合入当前 CapabilitySpec`, document.path));
|
|
484
|
+
}
|
|
485
|
+
else if (matches.length > 1) {
|
|
486
|
+
diagnostics.push(issue("error", "APPSPEC_REQUIREMENT_ID_DUPLICATED", `关闭前必须消除需求 ${id} 的重复定义`, document.path));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const id of document.references.decisions) {
|
|
490
|
+
const matches = collection.decisions.filter(item => item.id === id);
|
|
491
|
+
if (!decisionIds.has(id)) {
|
|
492
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_DECISION_UNKNOWN", `关闭前必须建立架构决策 ${id}`, document.path));
|
|
493
|
+
}
|
|
494
|
+
else if (matches.length > 1) {
|
|
495
|
+
diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_DUPLICATED", `关闭前必须消除决策 ${id} 的重复定义`, document.path));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const id of document.requirementIds) {
|
|
499
|
+
const matches = collection.capabilities.filter(item => item.requirementIds.includes(id));
|
|
500
|
+
if (matches.length > 1) {
|
|
501
|
+
diagnostics.push(issue("error", "APPSPEC_REQUIREMENT_ID_DUPLICATED", `关闭前必须消除需求 ${id} 的重复定义`, document.path));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const id of document.acceptanceIds) {
|
|
505
|
+
const matches = collection.capabilities.filter(item => item.acceptanceIds.includes(id));
|
|
506
|
+
if (matches.length > 1) {
|
|
507
|
+
diagnostics.push(issue("error", "APPSPEC_ACCEPTANCE_ID_DUPLICATED", `关闭前必须消除验收场景 ${id} 的重复定义`, document.path));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
validateEmbeddedCurrentSpecIds(document, requirementIds, acceptanceIds, diagnostics);
|
|
511
|
+
validateReferences(document, new Set(contract.resourceCodes), new Set(contract.actionCodes), "error", diagnostics);
|
|
512
|
+
validateRiskSections(document, diagnostics);
|
|
513
|
+
}
|
|
514
|
+
function validateEmbeddedCurrentSpecIds(document, currentRequirementIds, currentAcceptanceIds, diagnostics) {
|
|
515
|
+
for (const id of document.requirementIds) {
|
|
516
|
+
if (!currentRequirementIds.has(id)) {
|
|
517
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_NOT_CURRENT", `ChangeSpec 中的长期需求 ${id} 尚未进入当前 CapabilitySpec`, document.path));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const id of document.acceptanceIds) {
|
|
521
|
+
if (!currentAcceptanceIds.has(id)) {
|
|
522
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_ACCEPTANCE_NOT_CURRENT", `ChangeSpec 中的验收场景 ${id} 尚未进入当前 CapabilitySpec`, document.path));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
388
526
|
function validateReferences(document, resourceCodes, actionCodes, severity, diagnostics) {
|
|
389
527
|
for (const code of document.references.resources) {
|
|
390
528
|
if (!resourceCodes.has(code)) {
|
|
@@ -406,25 +544,45 @@ function validateRiskSections(document, diagnostics) {
|
|
|
406
544
|
if (risk === "L1")
|
|
407
545
|
return;
|
|
408
546
|
const required = [
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
547
|
+
{
|
|
548
|
+
titles: ["验收", "Acceptance"],
|
|
549
|
+
placeholders: [
|
|
550
|
+
"一个可观察的正向结果",
|
|
551
|
+
"需要时补充拒绝、异常或权限反例",
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
titles: ["数据与权限", "Data and Permissions"],
|
|
556
|
+
placeholders: ["无,或说明资源、字段、角色和数据范围变化。"],
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
titles: ["回滚", "Rollback"],
|
|
560
|
+
placeholders: ["回退声明/代码并保持旧数据可读;如不适用请说明。"],
|
|
561
|
+
},
|
|
412
562
|
];
|
|
413
563
|
if (risk === "L3") {
|
|
414
|
-
required.push(
|
|
564
|
+
required.push({
|
|
565
|
+
titles: ["失败、并发与幂等", "Failure, Concurrency, and Idempotency"],
|
|
566
|
+
placeholders: ["L3 才需要详细维护;其他变化写“无”。"],
|
|
567
|
+
}, {
|
|
568
|
+
titles: ["架构决策", "Decision Records"],
|
|
569
|
+
placeholders: ["L3 如涉及 ADR,在 front matter 的 decisions 中引用。"],
|
|
570
|
+
});
|
|
415
571
|
}
|
|
416
|
-
for (const
|
|
417
|
-
if (!
|
|
418
|
-
diagnostics.push(issue("
|
|
572
|
+
for (const section of required) {
|
|
573
|
+
if (!hasMeaningfulSection(document.content, section.titles, section.placeholders)) {
|
|
574
|
+
diagnostics.push(issue("error", "APPSPEC_RISK_SECTION_INCOMPLETE", `${risk} ChangeSpec 的“${section.titles[0]}”为空或仍是默认占位内容`, document.path));
|
|
419
575
|
}
|
|
420
576
|
}
|
|
421
577
|
}
|
|
422
578
|
function selectContext(collection, selector, diagnostics) {
|
|
423
579
|
if (!selector) {
|
|
424
580
|
return {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
581
|
+
primary: collection.app[0] || null,
|
|
582
|
+
capabilities: [],
|
|
583
|
+
activeChanges: [],
|
|
584
|
+
archivedChanges: [],
|
|
585
|
+
decisions: [],
|
|
428
586
|
};
|
|
429
587
|
}
|
|
430
588
|
const change = collection.activeChanges.find(document => document.id === selector);
|
|
@@ -432,21 +590,133 @@ function selectContext(collection, selector, diagnostics) {
|
|
|
432
590
|
const capabilityIds = new Set(change.references.capabilities);
|
|
433
591
|
const decisionIds = new Set(change.references.decisions);
|
|
434
592
|
return {
|
|
593
|
+
primary: change,
|
|
435
594
|
capabilities: collection.capabilities.filter(document => capabilityIds.has(document.id)),
|
|
436
595
|
activeChanges: [change],
|
|
596
|
+
archivedChanges: [],
|
|
597
|
+
decisions: collection.decisions.filter(document => decisionIds.has(document.id)),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const archivedChange = collection.history.find(document => document.id === selector);
|
|
601
|
+
if (archivedChange) {
|
|
602
|
+
const capabilityIds = new Set(archivedChange.references.capabilities);
|
|
603
|
+
const decisionIds = new Set(archivedChange.references.decisions);
|
|
604
|
+
return {
|
|
605
|
+
primary: archivedChange,
|
|
606
|
+
capabilities: collection.capabilities.filter(document => capabilityIds.has(document.id)),
|
|
607
|
+
activeChanges: [],
|
|
608
|
+
archivedChanges: [archivedChange],
|
|
437
609
|
decisions: collection.decisions.filter(document => decisionIds.has(document.id)),
|
|
438
610
|
};
|
|
439
611
|
}
|
|
440
612
|
const capability = collection.capabilities.find(document => document.id === selector);
|
|
441
613
|
if (capability) {
|
|
614
|
+
const activeChanges = collection.activeChanges.filter(document => document.references.capabilities.includes(capability.id));
|
|
615
|
+
const decisionIds = new Set(activeChanges.flatMap(document => document.references.decisions));
|
|
442
616
|
return {
|
|
617
|
+
primary: capability,
|
|
443
618
|
capabilities: [capability],
|
|
444
|
-
activeChanges
|
|
445
|
-
|
|
619
|
+
activeChanges,
|
|
620
|
+
archivedChanges: [],
|
|
621
|
+
decisions: collection.decisions.filter(document => decisionIds.has(document.id)),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const decision = collection.decisions.find(document => document.id === selector);
|
|
625
|
+
if (decision) {
|
|
626
|
+
return {
|
|
627
|
+
primary: decision,
|
|
628
|
+
capabilities: [],
|
|
629
|
+
activeChanges: [],
|
|
630
|
+
archivedChanges: [],
|
|
631
|
+
decisions: [decision],
|
|
446
632
|
};
|
|
447
633
|
}
|
|
448
634
|
diagnostics.push(issue("warning", "APPSPEC_SELECTOR_NOT_FOUND", `未找到 AppSpec selector ${selector},已返回应用总纲和空的相关上下文`, "appspec"));
|
|
449
|
-
return {
|
|
635
|
+
return {
|
|
636
|
+
primary: null,
|
|
637
|
+
capabilities: [],
|
|
638
|
+
activeChanges: [],
|
|
639
|
+
archivedChanges: [],
|
|
640
|
+
decisions: [],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function summarizeCollection(collection) {
|
|
644
|
+
return {
|
|
645
|
+
application: collection.app[0] ? summarize(collection.app[0]) : null,
|
|
646
|
+
capabilities: collection.capabilities.map(summarize),
|
|
647
|
+
activeChanges: collection.activeChanges.map(summarize),
|
|
648
|
+
decisions: collection.decisions.map(summarize),
|
|
649
|
+
history: collection.history.map(summarize),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function selectedDocuments(application, selected) {
|
|
653
|
+
return uniqueDocuments([
|
|
654
|
+
selected.primary,
|
|
655
|
+
application,
|
|
656
|
+
...selected.capabilities,
|
|
657
|
+
...selected.activeChanges,
|
|
658
|
+
...selected.archivedChanges,
|
|
659
|
+
...selected.decisions,
|
|
660
|
+
]);
|
|
661
|
+
}
|
|
662
|
+
function applyContextBudget(application, selected) {
|
|
663
|
+
const prioritized = selectedDocuments(application, selected);
|
|
664
|
+
const included = new Set();
|
|
665
|
+
const omitted = [];
|
|
666
|
+
let contentBytes = 0;
|
|
667
|
+
for (const document of prioritized) {
|
|
668
|
+
const bytes = Buffer.byteLength(document.content, "utf8");
|
|
669
|
+
if (contentBytes + bytes > APP_SPEC_LIMITS.maximumContextBytes) {
|
|
670
|
+
omitted.push(document);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
included.add(document.path);
|
|
674
|
+
contentBytes += bytes;
|
|
675
|
+
}
|
|
676
|
+
const keep = (documents) => documents.filter(document => included.has(document.path));
|
|
677
|
+
return {
|
|
678
|
+
application: application && included.has(application.path) ? application : null,
|
|
679
|
+
capabilities: keep(selected.capabilities),
|
|
680
|
+
activeChanges: keep(selected.activeChanges),
|
|
681
|
+
archivedChanges: keep(selected.archivedChanges),
|
|
682
|
+
decisions: keep(selected.decisions),
|
|
683
|
+
contextBudget: {
|
|
684
|
+
maximumBytes: APP_SPEC_LIMITS.maximumContextBytes,
|
|
685
|
+
contentBytes,
|
|
686
|
+
truncated: omitted.length > 0,
|
|
687
|
+
omitted: omitted.map(summarize),
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function uniqueDocuments(documents) {
|
|
692
|
+
const paths = new Set();
|
|
693
|
+
return documents.filter((document) => {
|
|
694
|
+
if (!document || paths.has(document.path))
|
|
695
|
+
return false;
|
|
696
|
+
paths.add(document.path);
|
|
697
|
+
return true;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function digestDocuments(documents, contract, extra = []) {
|
|
701
|
+
if (documents.length === 0 && extra.length === 0)
|
|
702
|
+
return null;
|
|
703
|
+
const contractIdentity = {
|
|
704
|
+
appCode: contract.appCode,
|
|
705
|
+
resourceCodes: unique(contract.resourceCodes),
|
|
706
|
+
actionCodes: unique(contract.actionCodes),
|
|
707
|
+
configDigest: contract.configDigest || null,
|
|
708
|
+
contractDigest: contract.contractDigest || null,
|
|
709
|
+
aiCatalogDigest: contract.aiCatalogDigest || null,
|
|
710
|
+
};
|
|
711
|
+
return createHash("sha256")
|
|
712
|
+
.update([
|
|
713
|
+
...[...documents]
|
|
714
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
715
|
+
.map(document => `${document.path}\n${document.content}`),
|
|
716
|
+
...extra,
|
|
717
|
+
`contract:${JSON.stringify(contractIdentity)}`,
|
|
718
|
+
].join("\n---\n"))
|
|
719
|
+
.digest("hex");
|
|
450
720
|
}
|
|
451
721
|
function parseAppSpecDocument(root, path, kind, content, diagnostics) {
|
|
452
722
|
const pointer = relativePath(root, path);
|
|
@@ -479,6 +749,12 @@ function parseAppSpecDocument(root, path, kind, content, diagnostics) {
|
|
|
479
749
|
if (!validStatuses[kind].includes(status)) {
|
|
480
750
|
diagnostics.push(issue("error", "APPSPEC_STATUS_INVALID", `${kind} status=${status} 不受支持`, pointer));
|
|
481
751
|
}
|
|
752
|
+
if (kind === "change") {
|
|
753
|
+
const currentSpec = String(frontMatter.metadata.currentSpec || "pending");
|
|
754
|
+
if (!["pending", "merged", "not-applicable"].includes(currentSpec)) {
|
|
755
|
+
diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_STATUS_INVALID", `change currentSpec=${currentSpec} 不受支持`, pointer));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
482
758
|
const h1 = content.match(/^#\s+(.+)$/m)?.[1]?.trim() || "";
|
|
483
759
|
const title = String(frontMatter.metadata.title || h1 || id);
|
|
484
760
|
const requirementIds = extractIds(content, /^###\s+(REQ-[A-Z0-9-]+)\b/gm, REQUIREMENT_ID);
|
|
@@ -610,8 +886,31 @@ function unresolvedQuestions(content) {
|
|
|
610
886
|
const section = nextHeading >= 0 ? tail.slice(0, nextHeading) : tail;
|
|
611
887
|
return section ? [...section.matchAll(/^- \[ \]\s+(.+)$/gm)].map(match => match[1]) : [];
|
|
612
888
|
}
|
|
613
|
-
function
|
|
614
|
-
|
|
889
|
+
function hasMeaningfulSection(content, alternatives, placeholders) {
|
|
890
|
+
const source = withoutHtmlComments(content);
|
|
891
|
+
for (const title of alternatives) {
|
|
892
|
+
const heading = new RegExp(`^##\\s+${escapeRegExp(title)}\\s*$`, "m").exec(source);
|
|
893
|
+
if (heading?.index === undefined)
|
|
894
|
+
continue;
|
|
895
|
+
const tail = source.slice(heading.index + heading[0].length);
|
|
896
|
+
const nextHeading = tail.search(/^##\s+/m);
|
|
897
|
+
const body = nextHeading >= 0 ? tail.slice(0, nextHeading) : tail;
|
|
898
|
+
const meaningfulLines = body
|
|
899
|
+
.split(/\r?\n/)
|
|
900
|
+
.map(line => line.trim())
|
|
901
|
+
.filter(Boolean)
|
|
902
|
+
.filter(line => !/^[-*](?:\s+\[[ xX]\])?\s*$/.test(line))
|
|
903
|
+
.filter(line => !placeholders.some(placeholder => line.includes(placeholder)));
|
|
904
|
+
if (meaningfulLines.length > 0)
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
function currentSpecStatus(document) {
|
|
910
|
+
const value = String(document.metadata.currentSpec || "pending");
|
|
911
|
+
return ["merged", "not-applicable"].includes(value)
|
|
912
|
+
? value
|
|
913
|
+
: "pending";
|
|
615
914
|
}
|
|
616
915
|
function duplicateDiagnostics(values, id, code, diagnostics) {
|
|
617
916
|
const seen = new Map();
|
|
@@ -758,6 +1057,7 @@ schema: ${APP_SPEC_SCHEMAS.change}
|
|
|
758
1057
|
id: ${input.id}
|
|
759
1058
|
title: ${JSON.stringify(input.title)}
|
|
760
1059
|
status: draft
|
|
1060
|
+
currentSpec: pending
|
|
761
1061
|
risk: ${input.risk}
|
|
762
1062
|
createdAt: ${JSON.stringify(createdAt)}
|
|
763
1063
|
capabilities: ${JSON.stringify(input.capabilities)}
|
|
@@ -881,8 +1181,11 @@ function summarize(document) {
|
|
|
881
1181
|
title: document.title,
|
|
882
1182
|
status: document.status,
|
|
883
1183
|
path: document.path,
|
|
884
|
-
|
|
885
|
-
|
|
1184
|
+
risk: document.kind === "change" && ["L1", "L2", "L3"].includes(String(document.metadata.risk))
|
|
1185
|
+
? String(document.metadata.risk)
|
|
1186
|
+
: null,
|
|
1187
|
+
requirementCount: document.requirementIds.length,
|
|
1188
|
+
acceptanceScenarioCount: document.acceptanceIds.length,
|
|
886
1189
|
};
|
|
887
1190
|
}
|
|
888
1191
|
function extractIds(content, pattern, validation) {
|
|
@@ -909,6 +1212,7 @@ function validateMetadata(kind, metadata, pointer, diagnostics) {
|
|
|
909
1212
|
"id",
|
|
910
1213
|
"title",
|
|
911
1214
|
"status",
|
|
1215
|
+
"currentSpec",
|
|
912
1216
|
"risk",
|
|
913
1217
|
"createdAt",
|
|
914
1218
|
"closedAt",
|