openxiangda-devkit-core 2.0.0-alpha.12 → 2.0.0-alpha.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -8
- package/dist/app-spec.d.ts +228 -0
- package/dist/app-spec.d.ts.map +1 -0
- package/dist/app-spec.js +1443 -0
- package/dist/app-spec.js.map +1 -0
- package/dist/application-services.d.ts +996 -49
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +1348 -197
- 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.d.ts +23 -0
- package/dist/backend-image-build.d.ts.map +1 -0
- package/dist/backend-image-build.js +204 -0
- package/dist/backend-image-build.js.map +1 -0
- 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 +123 -366
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +26 -266
- package/dist/command-registry.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts +4 -0
- package/dist/compiler/ai-catalog.d.ts.map +1 -0
- package/dist/compiler/ai-catalog.js +354 -0
- package/dist/compiler/ai-catalog.js.map +1 -0
- 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/authorization-transition.d.ts +7 -0
- package/dist/compiler/authorization-transition.d.ts.map +1 -0
- package/dist/compiler/authorization-transition.js +27 -0
- package/dist/compiler/authorization-transition.js.map +1 -0
- package/dist/compiler/bundle.d.ts +34 -0
- package/dist/compiler/bundle.d.ts.map +1 -0
- package/dist/compiler/bundle.js +1910 -0
- package/dist/compiler/bundle.js.map +1 -0
- package/dist/compiler/config.d.ts +882 -0
- package/dist/compiler/config.d.ts.map +1 -0
- package/dist/compiler/config.js +4118 -0
- package/dist/compiler/config.js.map +1 -0
- package/dist/compiler/data-policy-expression.d.ts +12 -0
- package/dist/compiler/data-policy-expression.d.ts.map +1 -0
- package/dist/compiler/data-policy-expression.js +43 -0
- package/dist/compiler/data-policy-expression.js.map +1 -0
- package/dist/compiler/field-codec.d.ts +14 -0
- package/dist/compiler/field-codec.d.ts.map +1 -0
- package/dist/compiler/field-codec.js +131 -0
- package/dist/compiler/field-codec.js.map +1 -0
- package/dist/compiler/field-physical-plan.d.ts +10 -0
- package/dist/compiler/field-physical-plan.d.ts.map +1 -0
- package/dist/compiler/field-physical-plan.js +66 -0
- package/dist/compiler/field-physical-plan.js.map +1 -0
- package/dist/compiler/field-policy-path.d.ts +10 -0
- package/dist/compiler/field-policy-path.d.ts.map +1 -0
- package/dist/compiler/field-policy-path.js +283 -0
- package/dist/compiler/field-policy-path.js.map +1 -0
- package/dist/compiler/field-query-plan.d.ts +13 -0
- package/dist/compiler/field-query-plan.d.ts.map +1 -0
- package/dist/compiler/field-query-plan.js +122 -0
- package/dist/compiler/field-query-plan.js.map +1 -0
- package/dist/compiler/field-surface.d.ts +15 -0
- package/dist/compiler/field-surface.d.ts.map +1 -0
- package/dist/compiler/field-surface.js +51 -0
- package/dist/compiler/field-surface.js.map +1 -0
- package/dist/compiler/index.d.ts +6 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +6 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/package-compiler.d.ts +22 -0
- package/dist/compiler/package-compiler.d.ts.map +1 -0
- package/dist/compiler/package-compiler.js +257 -0
- package/dist/compiler/package-compiler.js.map +1 -0
- 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/compiler/schema-composition.d.ts +21 -0
- package/dist/compiler/schema-composition.d.ts.map +1 -0
- package/dist/compiler/schema-composition.js +193 -0
- package/dist/compiler/schema-composition.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- 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/connected-development.d.ts +62 -0
- package/dist/connected-development.d.ts.map +1 -0
- package/dist/connected-development.js +393 -0
- package/dist/connected-development.js.map +1 -0
- package/dist/control-plane-client.d.ts +353 -123
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +420 -141
- 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 +55 -3
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +182 -21
- 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 +188 -0
- package/dist/developer-operations.d.ts.map +1 -0
- package/dist/developer-operations.js +58 -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 +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/workflow.d.ts +83 -0
- package/dist/internal/workflow.d.ts.map +1 -0
- package/dist/internal/workflow.js +419 -0
- package/dist/internal/workflow.js.map +1 -0
- package/dist/nest-injection-contract.d.ts +4 -0
- package/dist/nest-injection-contract.d.ts.map +1 -0
- package/dist/nest-injection-contract.js +116 -0
- package/dist/nest-injection-contract.js.map +1 -0
- 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/package-compiler.d.ts +1 -1
- package/dist/package-compiler.d.ts.map +1 -1
- package/dist/package-compiler.js +1 -1
- package/dist/package-compiler.js.map +1 -1
- package/dist/session.d.ts +149 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +362 -23
- package/dist/session.js.map +1 -1
- package/dist/testing.d.ts +77 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +213 -0
- package/dist/testing.js.map +1 -0
- package/dist/toolchain-capsule.d.ts +45 -0
- package/dist/toolchain-capsule.d.ts.map +1 -0
- package/dist/toolchain-capsule.js +165 -0
- package/dist/toolchain-capsule.js.map +1 -0
- package/dist/workspace-loader.d.ts +1 -1
- package/dist/workspace-loader.d.ts.map +1 -1
- package/dist/workspace-loader.js +35 -3
- 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 +37 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +16 -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
ADDED
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, openSync, readSync, closeSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { SCHEMA_VERSIONS, } from "openxiangda-contracts";
|
|
5
|
+
export const APP_SPEC_SCHEMAS = {
|
|
6
|
+
app: "openxiangda.appspec/app/v1",
|
|
7
|
+
capability: "openxiangda.appspec/capability/v1",
|
|
8
|
+
change: "openxiangda.appspec/change/v1",
|
|
9
|
+
decision: "openxiangda.appspec/decision/v1",
|
|
10
|
+
design: "openxiangda.appspec/design/v1",
|
|
11
|
+
context: "openxiangda.appspec/context/v4",
|
|
12
|
+
};
|
|
13
|
+
export const APP_SPEC_LIMITS = {
|
|
14
|
+
maximumFiles: 128,
|
|
15
|
+
maximumFileBytes: 256 * 1024,
|
|
16
|
+
maximumTotalBytes: 2 * 1024 * 1024,
|
|
17
|
+
maximumContextBytes: 256 * 1024,
|
|
18
|
+
maximumDirectoryEntries: 256,
|
|
19
|
+
maximumHistoryDepth: 1,
|
|
20
|
+
};
|
|
21
|
+
const APP_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
22
|
+
const CHANGE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
23
|
+
const CAPABILITY_ID = /^CAP-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
24
|
+
const DECISION_ID = /^ADR-[0-9]{4}(?:-[A-Z0-9]+)*$/;
|
|
25
|
+
const REQUIREMENT_ID = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
26
|
+
const ACCEPTANCE_ID = /^AC-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
27
|
+
export function initializeAppSpec(input) {
|
|
28
|
+
if (!APP_ID.test(input.appCode)) {
|
|
29
|
+
throw stableError("APPSPEC_APP_CODE_INVALID", input.appCode);
|
|
30
|
+
}
|
|
31
|
+
const paths = appSpecPaths(input.root);
|
|
32
|
+
for (const directory of [
|
|
33
|
+
paths.root,
|
|
34
|
+
paths.capabilities,
|
|
35
|
+
paths.activeChanges,
|
|
36
|
+
paths.history,
|
|
37
|
+
paths.decisions,
|
|
38
|
+
]) {
|
|
39
|
+
mkdirSync(directory, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
const created = [];
|
|
42
|
+
const existing = [];
|
|
43
|
+
if (existsSync(paths.app)) {
|
|
44
|
+
existing.push(relativePath(input.root, paths.app));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
exclusiveWrite(paths.app, renderApplicationSpec(input.appCode, input.appName));
|
|
48
|
+
created.push(relativePath(input.root, paths.app));
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
52
|
+
enabled: true,
|
|
53
|
+
advisory: false,
|
|
54
|
+
releaseGate: true,
|
|
55
|
+
created,
|
|
56
|
+
existing,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function createAppSpecCapability(input) {
|
|
60
|
+
initializeAppSpec(input);
|
|
61
|
+
if (!CAPABILITY_ID.test(input.id)) {
|
|
62
|
+
throw stableError("APPSPEC_CAPABILITY_ID_INVALID", input.id);
|
|
63
|
+
}
|
|
64
|
+
if (!input.title.trim())
|
|
65
|
+
throw stableError("APPSPEC_TITLE_REQUIRED", input.id);
|
|
66
|
+
const path = join(appSpecPaths(input.root).capabilities, `${fileNameForId(input.id, "CAP-")}.md`);
|
|
67
|
+
exclusiveWrite(path, renderCapabilitySpec({
|
|
68
|
+
id: input.id,
|
|
69
|
+
title: input.title.trim(),
|
|
70
|
+
resources: normalized(input.resources),
|
|
71
|
+
actions: normalized(input.actions),
|
|
72
|
+
}));
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
75
|
+
enabled: true,
|
|
76
|
+
advisory: false,
|
|
77
|
+
releaseGate: true,
|
|
78
|
+
created: relativePath(input.root, path),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function createAppSpecChange(input) {
|
|
82
|
+
initializeAppSpec(input);
|
|
83
|
+
if (!CHANGE_ID.test(input.id)) {
|
|
84
|
+
throw stableError("APPSPEC_CHANGE_ID_INVALID", input.id);
|
|
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
|
+
}
|
|
89
|
+
if (!input.title.trim())
|
|
90
|
+
throw stableError("APPSPEC_TITLE_REQUIRED", input.id);
|
|
91
|
+
const risk = input.risk || "L1";
|
|
92
|
+
if (!["L1", "L2", "L3"].includes(risk)) {
|
|
93
|
+
throw stableError("APPSPEC_RISK_INVALID", String(risk));
|
|
94
|
+
}
|
|
95
|
+
const path = join(appSpecPaths(input.root).activeChanges, `${input.id}.md`);
|
|
96
|
+
exclusiveWrite(path, renderChangeSpec({
|
|
97
|
+
id: input.id,
|
|
98
|
+
title: input.title.trim(),
|
|
99
|
+
risk,
|
|
100
|
+
summary: input.summary?.trim() || "",
|
|
101
|
+
capabilities: normalized(input.capabilities),
|
|
102
|
+
requirements: normalized(input.requirements),
|
|
103
|
+
resources: normalized(input.resources),
|
|
104
|
+
actions: normalized(input.actions),
|
|
105
|
+
}));
|
|
106
|
+
return {
|
|
107
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
108
|
+
enabled: true,
|
|
109
|
+
advisory: false,
|
|
110
|
+
releaseGate: true,
|
|
111
|
+
created: relativePath(input.root, path),
|
|
112
|
+
risk,
|
|
113
|
+
status: "draft",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function closeAppSpecChange(input) {
|
|
117
|
+
if (!CHANGE_ID.test(input.id)) {
|
|
118
|
+
throw stableError("APPSPEC_CHANGE_ID_INVALID", input.id);
|
|
119
|
+
}
|
|
120
|
+
const paths = appSpecPaths(input.root);
|
|
121
|
+
const source = join(paths.activeChanges, `${input.id}.md`);
|
|
122
|
+
if (!existsSync(source)) {
|
|
123
|
+
throw stableError("APPSPEC_ACTIVE_CHANGE_NOT_FOUND", input.id);
|
|
124
|
+
}
|
|
125
|
+
assertRegularFile(source);
|
|
126
|
+
const year = String(new Date().getUTCFullYear());
|
|
127
|
+
const historyDirectory = join(paths.history, year);
|
|
128
|
+
const target = join(historyDirectory, `${input.id}.md`);
|
|
129
|
+
if (existsSync(target)) {
|
|
130
|
+
throw stableError("APPSPEC_HISTORY_CHANGE_EXISTS", relativePath(input.root, target));
|
|
131
|
+
}
|
|
132
|
+
mkdirSync(historyDirectory, { recursive: true });
|
|
133
|
+
const sourceContent = readFileSync(source, "utf8");
|
|
134
|
+
const effectiveContent = input.currentSpec
|
|
135
|
+
? setFrontMatterValue(sourceContent, "currentSpec", input.currentSpec)
|
|
136
|
+
: sourceContent;
|
|
137
|
+
const diagnostics = [];
|
|
138
|
+
const document = parseAppSpecDocument(input.root, source, "change", effectiveContent, diagnostics);
|
|
139
|
+
if (!document || document.id !== input.id) {
|
|
140
|
+
throw stableError("APPSPEC_ACTIVE_CHANGE_INVALID", input.id);
|
|
141
|
+
}
|
|
142
|
+
const collectionDiagnostics = [];
|
|
143
|
+
const collection = collectDocuments(input.root, collectionDiagnostics);
|
|
144
|
+
const sourcePointer = relativePath(input.root, source);
|
|
145
|
+
const relatedPaths = new Set([sourcePointer]);
|
|
146
|
+
for (const capability of collection.capabilities) {
|
|
147
|
+
if (document.references.capabilities.includes(capability.id) ||
|
|
148
|
+
capability.requirementIds.some(id => [...document.references.requirements, ...document.requirementIds].includes(id)) ||
|
|
149
|
+
capability.acceptanceIds.some(id => document.acceptanceIds.includes(id))) {
|
|
150
|
+
relatedPaths.add(capability.path);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const decision of collection.decisions) {
|
|
154
|
+
if (document.references.decisions.includes(decision.id)) {
|
|
155
|
+
relatedPaths.add(decision.path);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const collectionFatalCodes = new Set([
|
|
159
|
+
"APPSPEC_FILE_LIMIT_EXCEEDED",
|
|
160
|
+
"APPSPEC_FILE_UNREADABLE",
|
|
161
|
+
"APPSPEC_FILE_INVALID",
|
|
162
|
+
"APPSPEC_FILE_SIZE_EXCEEDED",
|
|
163
|
+
"APPSPEC_TOTAL_SIZE_EXCEEDED",
|
|
164
|
+
"APPSPEC_DIRECTORY_INVALID",
|
|
165
|
+
"APPSPEC_DIRECTORY_ENTRY_LIMIT_EXCEEDED",
|
|
166
|
+
"APPSPEC_SYMLINK_FORBIDDEN",
|
|
167
|
+
"APPSPEC_HISTORY_DEPTH_EXCEEDED",
|
|
168
|
+
]);
|
|
169
|
+
diagnostics.push(...collectionDiagnostics.filter(item => item.severity === "error" &&
|
|
170
|
+
item.path !== sourcePointer &&
|
|
171
|
+
(collectionFatalCodes.has(item.code) ||
|
|
172
|
+
(item.path !== undefined && relatedPaths.has(item.path)))));
|
|
173
|
+
validateCloseConvergence(document, collection, input.contract, diagnostics);
|
|
174
|
+
if (diagnostics.some(item => item.severity === "error")) {
|
|
175
|
+
return {
|
|
176
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
177
|
+
enabled: true,
|
|
178
|
+
advisory: false,
|
|
179
|
+
releaseGate: true,
|
|
180
|
+
archived: null,
|
|
181
|
+
status: document.status,
|
|
182
|
+
converged: false,
|
|
183
|
+
diagnostics,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
renameSync(source, target);
|
|
187
|
+
const closedAt = new Date().toISOString();
|
|
188
|
+
const updated = appendClosure(setFrontMatterValue(setFrontMatterValue(effectiveContent, "status", "archived"), "closedAt", closedAt), input.summary?.trim() || "变更记录已归档;验收与发布结果以实际证据为准。", closedAt);
|
|
189
|
+
atomicReplace(target, updated);
|
|
190
|
+
return {
|
|
191
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
192
|
+
enabled: true,
|
|
193
|
+
advisory: false,
|
|
194
|
+
releaseGate: true,
|
|
195
|
+
archived: relativePath(input.root, target),
|
|
196
|
+
status: "archived",
|
|
197
|
+
converged: true,
|
|
198
|
+
diagnostics,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function inspectAppSpec(root, contract, selector, historyOffset = 0) {
|
|
202
|
+
const paths = appSpecPaths(root);
|
|
203
|
+
if (!existsSync(paths.root))
|
|
204
|
+
return emptyContext(contract, selector);
|
|
205
|
+
const diagnostics = [];
|
|
206
|
+
if (!isRegularDirectory(paths.root)) {
|
|
207
|
+
diagnostics.push(issue("error", "APPSPEC_ROOT_INVALID", "appspec 必须是普通目录,不能是符号链接", "appspec"));
|
|
208
|
+
return emptyContext(contract, selector, diagnostics, true);
|
|
209
|
+
}
|
|
210
|
+
const collection = collectDocuments(root, diagnostics, selector, historyOffset);
|
|
211
|
+
const application = collection.app[0] || null;
|
|
212
|
+
validateDocuments(collection, contract, diagnostics);
|
|
213
|
+
const selected = selectContext(collection, selector, diagnostics);
|
|
214
|
+
const allDocuments = [
|
|
215
|
+
...collection.app,
|
|
216
|
+
...collection.capabilities,
|
|
217
|
+
...collection.activeChanges,
|
|
218
|
+
...collection.history,
|
|
219
|
+
...collection.decisions,
|
|
220
|
+
...collection.designs,
|
|
221
|
+
];
|
|
222
|
+
const index = summarizeCollection(collection);
|
|
223
|
+
const workspaceDigest = digestDocuments(allDocuments.filter(document => !document.path.startsWith("appspec/changes/history/")), contract);
|
|
224
|
+
const selectionDocuments = selectedDocuments(application, selected);
|
|
225
|
+
const selectionDigest = allDocuments.length === 0
|
|
226
|
+
? null
|
|
227
|
+
: digestDocuments(selectionDocuments, contract, selector ? [`selector:${selector}`] : [`index:${JSON.stringify(index)}`]);
|
|
228
|
+
const budgeted = applyContextBudget(application, selected);
|
|
229
|
+
if (budgeted.contextBudget.truncated) {
|
|
230
|
+
diagnostics.push(issue("warning", "APPSPEC_CONTEXT_BUDGET_EXCEEDED", `相关 AppSpec 正文超过 ${APP_SPEC_LIMITS.maximumContextBytes} 字节预算;已省略 ${budgeted.contextBudget.omitted.length} 份低优先级文档,可按稳定 ID 单独读取`, "appspec"));
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
234
|
+
enabled: true,
|
|
235
|
+
mode: "guided",
|
|
236
|
+
releaseGate: true,
|
|
237
|
+
root: "appspec",
|
|
238
|
+
selector: selector || null,
|
|
239
|
+
contract,
|
|
240
|
+
index,
|
|
241
|
+
application: budgeted.application,
|
|
242
|
+
capabilities: budgeted.capabilities,
|
|
243
|
+
activeChanges: budgeted.activeChanges,
|
|
244
|
+
archivedChanges: budgeted.archivedChanges,
|
|
245
|
+
decisions: budgeted.decisions,
|
|
246
|
+
designs: budgeted.designs,
|
|
247
|
+
historyPage: collection.historyPage,
|
|
248
|
+
contextBudget: budgeted.contextBudget,
|
|
249
|
+
stats: {
|
|
250
|
+
files: allDocuments.length,
|
|
251
|
+
bytes: collection.bytes,
|
|
252
|
+
capabilities: collection.capabilities.length,
|
|
253
|
+
activeChanges: collection.activeChanges.length,
|
|
254
|
+
historyChanges: collection.historyPage.total,
|
|
255
|
+
decisions: collection.decisions.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,
|
|
259
|
+
},
|
|
260
|
+
workspaceDigest,
|
|
261
|
+
selectionDigest,
|
|
262
|
+
diagnostics,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export function advisoryAppSpecDiagnostics(diagnostics) {
|
|
266
|
+
return diagnostics.map(diagnostic => ({
|
|
267
|
+
...diagnostic,
|
|
268
|
+
severity: diagnostic.severity === "error" ? "warning" : diagnostic.severity,
|
|
269
|
+
remediation: diagnostic.remediation ||
|
|
270
|
+
"运行 openxiangda spec check 查看文档诊断;普通 check 可以继续,正式发布按阶段核对需求与验收",
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
export function summarizeAppSpecContext(context) {
|
|
274
|
+
return {
|
|
275
|
+
enabled: context.enabled,
|
|
276
|
+
mode: context.mode,
|
|
277
|
+
releaseGate: context.releaseGate,
|
|
278
|
+
workspaceDigest: context.workspaceDigest,
|
|
279
|
+
selectionDigest: context.selectionDigest,
|
|
280
|
+
historyPage: context.historyPage,
|
|
281
|
+
contextBudget: context.contextBudget,
|
|
282
|
+
stats: context.stats,
|
|
283
|
+
diagnostics: {
|
|
284
|
+
errors: context.diagnostics.filter(item => item.severity === "error").length,
|
|
285
|
+
warnings: context.diagnostics.filter(item => item.severity === "warning").length,
|
|
286
|
+
info: context.diagnostics.filter(item => item.severity === "info").length,
|
|
287
|
+
},
|
|
288
|
+
nextCommand: context.enabled
|
|
289
|
+
? "openxiangda spec context --json"
|
|
290
|
+
: "openxiangda spec init",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function emptyContext(contract, selector, diagnostics = [], enabled = false) {
|
|
294
|
+
return {
|
|
295
|
+
schemaVersion: APP_SPEC_SCHEMAS.context,
|
|
296
|
+
enabled,
|
|
297
|
+
mode: "guided",
|
|
298
|
+
releaseGate: true,
|
|
299
|
+
root: "appspec",
|
|
300
|
+
selector: selector || null,
|
|
301
|
+
contract,
|
|
302
|
+
index: {
|
|
303
|
+
application: null,
|
|
304
|
+
capabilities: [],
|
|
305
|
+
activeChanges: [],
|
|
306
|
+
decisions: [],
|
|
307
|
+
designs: [],
|
|
308
|
+
history: [],
|
|
309
|
+
},
|
|
310
|
+
application: null,
|
|
311
|
+
capabilities: [],
|
|
312
|
+
activeChanges: [],
|
|
313
|
+
archivedChanges: [],
|
|
314
|
+
decisions: [],
|
|
315
|
+
designs: [],
|
|
316
|
+
historyPage: { offset: 0, limit: 50, total: 0, nextOffset: null },
|
|
317
|
+
contextBudget: {
|
|
318
|
+
maximumBytes: APP_SPEC_LIMITS.maximumContextBytes,
|
|
319
|
+
contentBytes: 0,
|
|
320
|
+
truncated: false,
|
|
321
|
+
omitted: [],
|
|
322
|
+
},
|
|
323
|
+
stats: {
|
|
324
|
+
files: 0,
|
|
325
|
+
bytes: 0,
|
|
326
|
+
capabilities: 0,
|
|
327
|
+
activeChanges: 0,
|
|
328
|
+
historyChanges: 0,
|
|
329
|
+
decisions: 0,
|
|
330
|
+
designs: 0,
|
|
331
|
+
requirements: 0,
|
|
332
|
+
acceptanceScenarios: 0,
|
|
333
|
+
},
|
|
334
|
+
workspaceDigest: null,
|
|
335
|
+
selectionDigest: null,
|
|
336
|
+
diagnostics,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
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) {
|
|
388
|
+
const paths = appSpecPaths(root);
|
|
389
|
+
const result = {
|
|
390
|
+
app: [],
|
|
391
|
+
capabilities: [],
|
|
392
|
+
activeChanges: [],
|
|
393
|
+
history: [],
|
|
394
|
+
decisions: [],
|
|
395
|
+
designs: [],
|
|
396
|
+
bytes: 0,
|
|
397
|
+
historyPage: { offset: historyOffset, limit: 50, total: 0, nextOffset: null },
|
|
398
|
+
};
|
|
399
|
+
const candidates = [];
|
|
400
|
+
if (existsSync(paths.app))
|
|
401
|
+
candidates.push({ path: paths.app, kind: "app" });
|
|
402
|
+
candidates.push(...markdownFiles(paths.capabilities, false, diagnostics, root).map(path => ({
|
|
403
|
+
path,
|
|
404
|
+
kind: "capability",
|
|
405
|
+
})), ...markdownFiles(paths.activeChanges, false, diagnostics, root).map(path => ({
|
|
406
|
+
path,
|
|
407
|
+
kind: "change",
|
|
408
|
+
})), ...markdownFiles(paths.decisions, false, diagnostics, root).map(path => ({
|
|
409
|
+
path,
|
|
410
|
+
kind: "decision",
|
|
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
|
+
}
|
|
415
|
+
if (candidates.length > APP_SPEC_LIMITS.maximumFiles) {
|
|
416
|
+
diagnostics.push(issue("error", "APPSPEC_FILE_LIMIT_EXCEEDED", `AppSpec 文件数 ${candidates.length} 超过上限 ${APP_SPEC_LIMITS.maximumFiles}`, "appspec"));
|
|
417
|
+
candidates.splice(APP_SPEC_LIMITS.maximumFiles);
|
|
418
|
+
}
|
|
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))) {
|
|
424
|
+
let stat;
|
|
425
|
+
try {
|
|
426
|
+
stat = lstatSync(candidate.path);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
diagnostics.push(issue("error", "APPSPEC_FILE_UNREADABLE", "AppSpec 文件无法读取", relativePath(root, candidate.path)));
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
433
|
+
diagnostics.push(issue("error", "APPSPEC_FILE_INVALID", "AppSpec 只允许普通 Markdown 文件,不能使用符号链接", relativePath(root, candidate.path)));
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (stat.size > APP_SPEC_LIMITS.maximumFileBytes) {
|
|
437
|
+
diagnostics.push(issue("error", "APPSPEC_FILE_SIZE_EXCEEDED", `AppSpec 单文件超过 ${APP_SPEC_LIMITS.maximumFileBytes} 字节上限`, relativePath(root, candidate.path)));
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
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) {
|
|
443
|
+
diagnostics.push(issue("error", "APPSPEC_TOTAL_SIZE_EXCEEDED", `AppSpec 总大小超过 ${APP_SPEC_LIMITS.maximumTotalBytes} 字节上限`, "appspec"));
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
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");
|
|
464
|
+
const document = parseAppSpecDocument(root, candidate.path, candidate.kind, content, diagnostics);
|
|
465
|
+
if (!document)
|
|
466
|
+
continue;
|
|
467
|
+
if (candidate.kind === "app")
|
|
468
|
+
result.app.push(document);
|
|
469
|
+
else if (candidate.kind === "capability")
|
|
470
|
+
result.capabilities.push(document);
|
|
471
|
+
else if (candidate.kind === "design")
|
|
472
|
+
result.designs.push(document);
|
|
473
|
+
else if (candidate.kind === "decision")
|
|
474
|
+
result.decisions.push(document);
|
|
475
|
+
else if (candidate.history)
|
|
476
|
+
result.history.push(document);
|
|
477
|
+
else
|
|
478
|
+
result.activeChanges.push(document);
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
function validateDocuments(collection, contract, diagnostics) {
|
|
483
|
+
if (collection.app.length !== 1) {
|
|
484
|
+
diagnostics.push(issue("error", "APPSPEC_APPLICATION_REQUIRED", "启用 AppSpec 后必须且只能存在一份 appspec/app.md", "appspec/app.md"));
|
|
485
|
+
}
|
|
486
|
+
const app = collection.app[0];
|
|
487
|
+
if (app && app.id !== contract.appCode) {
|
|
488
|
+
diagnostics.push(issue("error", "APPSPEC_APP_CODE_MISMATCH", `AppSpec app=${app.id} 与工作区 ${contract.appCode} 不一致`, app.path));
|
|
489
|
+
}
|
|
490
|
+
for (const document of collection.activeChanges) {
|
|
491
|
+
if (["archived", "cancelled"].includes(document.status)) {
|
|
492
|
+
diagnostics.push(issue("error", "APPSPEC_ACTIVE_CHANGE_STATUS_INVALID", `活跃目录中的 ChangeSpec 不能使用 ${document.status} 状态`, document.path));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const document of collection.history) {
|
|
496
|
+
if (!["archived", "cancelled"].includes(document.status)) {
|
|
497
|
+
diagnostics.push(issue("error", "APPSPEC_HISTORY_CHANGE_STATUS_INVALID", `历史目录中的 ChangeSpec 必须是 archived 或 cancelled,当前为 ${document.status}`, document.path));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
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);
|
|
503
|
+
const capabilityIds = new Set(collection.capabilities.map(document => document.id));
|
|
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));
|
|
506
|
+
const resourceCodes = new Set(contract.resourceCodes);
|
|
507
|
+
const actionCodes = new Set(contract.actionCodes);
|
|
508
|
+
const decisionIds = new Set(collection.decisions.map(document => document.id));
|
|
509
|
+
for (const document of collection.capabilities) {
|
|
510
|
+
validateReferences(document, resourceCodes, actionCodes, "error", diagnostics);
|
|
511
|
+
for (const requirement of requirementsWithoutAcceptance(document.content)) {
|
|
512
|
+
diagnostics.push(issue("warning", "APPSPEC_REQUIREMENT_ACCEPTANCE_RECOMMENDED", `需求 ${requirement} 建议至少包含一个 #### AC-* 可证伪场景`, document.path));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const document of collection.activeChanges) {
|
|
516
|
+
const finalState = ["verified", "released"].includes(document.status);
|
|
517
|
+
const referenceSeverity = finalState ? "error" : "warning";
|
|
518
|
+
for (const id of document.references.capabilities) {
|
|
519
|
+
if (!capabilityIds.has(id)) {
|
|
520
|
+
diagnostics.push(issue(referenceSeverity, "APPSPEC_CHANGE_CAPABILITY_UNKNOWN", `ChangeSpec 引用的能力 ${id} 不存在`, document.path));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const id of document.references.requirements) {
|
|
524
|
+
if (!requirementIds.has(id)) {
|
|
525
|
+
diagnostics.push(issue(referenceSeverity, "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `ChangeSpec 引用的需求 ${id} 尚未进入当前总纲或能力规格`, document.path));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const id of document.references.decisions) {
|
|
529
|
+
if (!decisionIds.has(id)) {
|
|
530
|
+
diagnostics.push(issue(referenceSeverity, "APPSPEC_CHANGE_DECISION_UNKNOWN", `ChangeSpec 引用的决策 ${id} 不存在`, document.path));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
validateReferences(document, resourceCodes, actionCodes, referenceSeverity, diagnostics);
|
|
534
|
+
validateEmbeddedCurrentSpecIds(document, requirementIds, acceptanceIds, diagnostics);
|
|
535
|
+
validateRiskSections(document, diagnostics);
|
|
536
|
+
if (finalState &&
|
|
537
|
+
currentSpecStatus(document) === "pending") {
|
|
538
|
+
diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_NOT_CONVERGED", "verified/released ChangeSpec 必须确认 currentSpec=merged 或 not-applicable", document.path));
|
|
539
|
+
}
|
|
540
|
+
if (finalState && unresolvedQuestions(document.content).length > 0) {
|
|
541
|
+
diagnostics.push(issue("error", "APPSPEC_VERIFIED_CHANGE_HAS_OPEN_QUESTIONS", "verified/released ChangeSpec 仍有未勾选问题", document.path));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (collection.activeChanges.length > 10) {
|
|
545
|
+
diagnostics.push(issue("warning", "APPSPEC_ACTIVE_CHANGE_COUNT_HIGH", `当前有 ${collection.activeChanges.length} 个活跃变更;建议关闭已完成记录,避免 AI 上下文膨胀`, "appspec/changes/active"));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
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
|
+
}
|
|
553
|
+
const currentSpec = currentSpecStatus(document);
|
|
554
|
+
if (currentSpec === "pending") {
|
|
555
|
+
diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_NOT_CONVERGED", "关闭前必须确认 currentSpec=merged 或 not-applicable;该检查只约束 spec close", document.path));
|
|
556
|
+
}
|
|
557
|
+
const capabilityIds = new Set(collection.capabilities.map(item => item.id));
|
|
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));
|
|
560
|
+
const decisionIds = new Set(collection.decisions.map(item => item.id));
|
|
561
|
+
for (const id of document.references.capabilities) {
|
|
562
|
+
const matches = collection.capabilities.filter(item => item.id === id);
|
|
563
|
+
if (!capabilityIds.has(id)) {
|
|
564
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_CAPABILITY_UNKNOWN", `关闭前必须建立当前能力引用 ${id}`, document.path));
|
|
565
|
+
}
|
|
566
|
+
else if (matches.length > 1) {
|
|
567
|
+
diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_DUPLICATED", `关闭前必须消除能力 ${id} 的重复定义`, document.path));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const id of document.references.requirements) {
|
|
571
|
+
const matches = [...collection.app, ...collection.capabilities, ...collection.designs].filter(item => item.requirementIds.includes(id));
|
|
572
|
+
if (!requirementIds.has(id)) {
|
|
573
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_UNKNOWN", `关闭前必须把需求 ${id} 合入当前总纲或能力规格`, document.path));
|
|
574
|
+
}
|
|
575
|
+
else if (matches.length > 1) {
|
|
576
|
+
diagnostics.push(issue("error", "APPSPEC_REQUIREMENT_ID_DUPLICATED", `关闭前必须消除需求 ${id} 的重复定义`, document.path));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const id of document.references.decisions) {
|
|
580
|
+
const matches = collection.decisions.filter(item => item.id === id);
|
|
581
|
+
if (!decisionIds.has(id)) {
|
|
582
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_DECISION_UNKNOWN", `关闭前必须建立架构决策 ${id}`, document.path));
|
|
583
|
+
}
|
|
584
|
+
else if (matches.length > 1) {
|
|
585
|
+
diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_DUPLICATED", `关闭前必须消除决策 ${id} 的重复定义`, document.path));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const id of document.requirementIds) {
|
|
589
|
+
const matches = [...collection.app, ...collection.capabilities, ...collection.designs].filter(item => item.requirementIds.includes(id));
|
|
590
|
+
if (matches.length > 1) {
|
|
591
|
+
diagnostics.push(issue("error", "APPSPEC_REQUIREMENT_ID_DUPLICATED", `关闭前必须消除需求 ${id} 的重复定义`, document.path));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (const id of document.acceptanceIds) {
|
|
595
|
+
const matches = [...collection.app, ...collection.capabilities, ...collection.designs].filter(item => item.acceptanceIds.includes(id));
|
|
596
|
+
if (matches.length > 1) {
|
|
597
|
+
diagnostics.push(issue("error", "APPSPEC_ACCEPTANCE_ID_DUPLICATED", `关闭前必须消除验收场景 ${id} 的重复定义`, document.path));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
validateEmbeddedCurrentSpecIds(document, requirementIds, acceptanceIds, diagnostics);
|
|
601
|
+
validateReferences(document, new Set(contract.resourceCodes), new Set(contract.actionCodes), "error", diagnostics);
|
|
602
|
+
validateRiskSections(document, diagnostics);
|
|
603
|
+
}
|
|
604
|
+
function validateEmbeddedCurrentSpecIds(document, currentRequirementIds, currentAcceptanceIds, diagnostics) {
|
|
605
|
+
for (const id of document.requirementIds) {
|
|
606
|
+
if (!currentRequirementIds.has(id)) {
|
|
607
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_REQUIREMENT_NOT_CURRENT", `ChangeSpec 中的长期需求 ${id} 尚未进入当前总纲或能力规格`, document.path));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
for (const id of document.acceptanceIds) {
|
|
611
|
+
if (!currentAcceptanceIds.has(id)) {
|
|
612
|
+
diagnostics.push(issue("error", "APPSPEC_CHANGE_ACCEPTANCE_NOT_CURRENT", `ChangeSpec 中的验收场景 ${id} 尚未进入当前总纲或能力规格`, document.path));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function validateReferences(document, resourceCodes, actionCodes, severity, diagnostics) {
|
|
617
|
+
for (const code of document.references.resources) {
|
|
618
|
+
if (!resourceCodes.has(code)) {
|
|
619
|
+
diagnostics.push(issue(severity, "APPSPEC_RESOURCE_UNKNOWN", `AppSpec 引用的资源 ${code} 不在当前编译声明中`, document.path));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const code of document.references.actions) {
|
|
623
|
+
if (!actionCodes.has(code)) {
|
|
624
|
+
diagnostics.push(issue(severity, "APPSPEC_ACTION_UNKNOWN", `AppSpec 引用的动作 ${code} 不在当前编译声明中`, document.path));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function validateRiskSections(document, diagnostics) {
|
|
629
|
+
const risk = String(document.metadata.risk || "L1");
|
|
630
|
+
if (!["L1", "L2", "L3"].includes(risk)) {
|
|
631
|
+
diagnostics.push(issue("error", "APPSPEC_RISK_INVALID", `未知风险档位 ${risk}`, document.path));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (risk === "L1")
|
|
635
|
+
return;
|
|
636
|
+
const required = [
|
|
637
|
+
{
|
|
638
|
+
titles: ["验收", "Acceptance"],
|
|
639
|
+
placeholders: [
|
|
640
|
+
"一个可观察的正向结果",
|
|
641
|
+
"需要时补充拒绝、异常或权限反例",
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
titles: ["数据与权限", "Data and Permissions"],
|
|
646
|
+
placeholders: ["无,或说明资源、字段、角色和数据范围变化。"],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
titles: ["回滚", "Rollback"],
|
|
650
|
+
placeholders: ["回退声明/代码并保持旧数据可读;如不适用请说明。"],
|
|
651
|
+
},
|
|
652
|
+
];
|
|
653
|
+
if (risk === "L3") {
|
|
654
|
+
required.push({
|
|
655
|
+
titles: ["失败、并发与幂等", "Failure, Concurrency, and Idempotency"],
|
|
656
|
+
placeholders: ["L3 才需要详细维护;其他变化写“无”。"],
|
|
657
|
+
}, {
|
|
658
|
+
titles: ["架构决策", "Decision Records"],
|
|
659
|
+
placeholders: ["L3 如涉及 ADR,在 front matter 的 decisions 中引用。"],
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
for (const section of required) {
|
|
663
|
+
if (!hasMeaningfulSection(document.content, section.titles, section.placeholders)) {
|
|
664
|
+
diagnostics.push(issue("error", "APPSPEC_RISK_SECTION_INCOMPLETE", `${risk} ChangeSpec 的“${section.titles[0]}”为空或仍是默认占位内容`, document.path));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function selectBaseContext(collection, selector, diagnostics) {
|
|
669
|
+
if (!selector) {
|
|
670
|
+
return {
|
|
671
|
+
primary: collection.app[0] || null,
|
|
672
|
+
capabilities: [],
|
|
673
|
+
activeChanges: [],
|
|
674
|
+
archivedChanges: [],
|
|
675
|
+
decisions: [],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const design = collection.designs.find(document => document.id === selector);
|
|
679
|
+
if (design)
|
|
680
|
+
return { primary: design, capabilities: [], activeChanges: [], archivedChanges: [], decisions: [] };
|
|
681
|
+
const change = collection.activeChanges.find(document => document.id === selector);
|
|
682
|
+
if (change) {
|
|
683
|
+
const capabilityIds = new Set(change.references.capabilities);
|
|
684
|
+
const decisionIds = new Set(change.references.decisions);
|
|
685
|
+
return {
|
|
686
|
+
primary: change,
|
|
687
|
+
capabilities: collection.capabilities.filter(document => capabilityIds.has(document.id)),
|
|
688
|
+
activeChanges: [change],
|
|
689
|
+
archivedChanges: [],
|
|
690
|
+
decisions: collection.decisions.filter(document => decisionIds.has(document.id)),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const archivedChange = collection.history.find(document => document.id === selector);
|
|
694
|
+
if (archivedChange) {
|
|
695
|
+
const capabilityIds = new Set(archivedChange.references.capabilities);
|
|
696
|
+
const decisionIds = new Set(archivedChange.references.decisions);
|
|
697
|
+
return {
|
|
698
|
+
primary: archivedChange,
|
|
699
|
+
capabilities: collection.capabilities.filter(document => capabilityIds.has(document.id)),
|
|
700
|
+
activeChanges: [],
|
|
701
|
+
archivedChanges: [archivedChange],
|
|
702
|
+
decisions: collection.decisions.filter(document => decisionIds.has(document.id)),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const capability = collection.capabilities.find(document => document.id === selector);
|
|
706
|
+
if (capability) {
|
|
707
|
+
const activeChanges = collection.activeChanges.filter(document => document.references.capabilities.includes(capability.id));
|
|
708
|
+
const decisionIds = new Set(activeChanges.flatMap(document => document.references.decisions));
|
|
709
|
+
return {
|
|
710
|
+
primary: capability,
|
|
711
|
+
capabilities: [capability],
|
|
712
|
+
activeChanges,
|
|
713
|
+
archivedChanges: [],
|
|
714
|
+
decisions: collection.decisions.filter(document => decisionIds.has(document.id)),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const decision = collection.decisions.find(document => document.id === selector);
|
|
718
|
+
if (decision) {
|
|
719
|
+
return {
|
|
720
|
+
primary: decision,
|
|
721
|
+
capabilities: [],
|
|
722
|
+
activeChanges: [],
|
|
723
|
+
archivedChanges: [],
|
|
724
|
+
decisions: [decision],
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
diagnostics.push(issue("warning", "APPSPEC_SELECTOR_NOT_FOUND", `未找到 AppSpec selector ${selector},已返回应用总纲和空的相关上下文`, "appspec"));
|
|
728
|
+
return {
|
|
729
|
+
primary: null,
|
|
730
|
+
capabilities: [],
|
|
731
|
+
activeChanges: [],
|
|
732
|
+
archivedChanges: [],
|
|
733
|
+
decisions: [],
|
|
734
|
+
};
|
|
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
|
+
}
|
|
766
|
+
function summarizeCollection(collection) {
|
|
767
|
+
return {
|
|
768
|
+
application: collection.app[0] ? summarize(collection.app[0]) : null,
|
|
769
|
+
capabilities: collection.capabilities.map(summarize),
|
|
770
|
+
activeChanges: collection.activeChanges.map(summarize),
|
|
771
|
+
decisions: collection.decisions.map(summarize),
|
|
772
|
+
designs: collection.designs.map(summarize),
|
|
773
|
+
history: collection.history.map(summarize),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function selectedDocuments(application, selected) {
|
|
777
|
+
return uniqueDocuments([
|
|
778
|
+
selected.primary,
|
|
779
|
+
application,
|
|
780
|
+
...selected.capabilities,
|
|
781
|
+
...selected.activeChanges,
|
|
782
|
+
...selected.archivedChanges,
|
|
783
|
+
...selected.decisions,
|
|
784
|
+
...selected.designs,
|
|
785
|
+
]);
|
|
786
|
+
}
|
|
787
|
+
function applyContextBudget(application, selected) {
|
|
788
|
+
const prioritized = selectedDocuments(application, selected);
|
|
789
|
+
const included = new Set();
|
|
790
|
+
const omitted = [];
|
|
791
|
+
let contentBytes = 0;
|
|
792
|
+
for (const document of prioritized) {
|
|
793
|
+
const bytes = Buffer.byteLength(document.content, "utf8");
|
|
794
|
+
if (contentBytes + bytes > APP_SPEC_LIMITS.maximumContextBytes) {
|
|
795
|
+
omitted.push(document);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
included.add(document.path);
|
|
799
|
+
contentBytes += bytes;
|
|
800
|
+
}
|
|
801
|
+
const keep = (documents) => documents.filter(document => included.has(document.path));
|
|
802
|
+
return {
|
|
803
|
+
application: application && included.has(application.path) ? application : null,
|
|
804
|
+
capabilities: keep(selected.capabilities),
|
|
805
|
+
activeChanges: keep(selected.activeChanges),
|
|
806
|
+
archivedChanges: keep(selected.archivedChanges),
|
|
807
|
+
decisions: keep(selected.decisions),
|
|
808
|
+
designs: keep(selected.designs),
|
|
809
|
+
contextBudget: {
|
|
810
|
+
maximumBytes: APP_SPEC_LIMITS.maximumContextBytes,
|
|
811
|
+
contentBytes,
|
|
812
|
+
truncated: omitted.length > 0,
|
|
813
|
+
omitted: omitted.map(summarize),
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function uniqueDocuments(documents) {
|
|
818
|
+
const paths = new Set();
|
|
819
|
+
return documents.filter((document) => {
|
|
820
|
+
if (!document || paths.has(document.path))
|
|
821
|
+
return false;
|
|
822
|
+
paths.add(document.path);
|
|
823
|
+
return true;
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function digestDocuments(documents, contract, extra = []) {
|
|
827
|
+
if (documents.length === 0 && extra.length === 0)
|
|
828
|
+
return null;
|
|
829
|
+
const contractIdentity = {
|
|
830
|
+
appCode: contract.appCode,
|
|
831
|
+
resourceCodes: unique(contract.resourceCodes),
|
|
832
|
+
actionCodes: unique(contract.actionCodes),
|
|
833
|
+
configDigest: contract.configDigest || null,
|
|
834
|
+
contractDigest: contract.contractDigest || null,
|
|
835
|
+
aiCatalogDigest: contract.aiCatalogDigest || null,
|
|
836
|
+
};
|
|
837
|
+
return createHash("sha256")
|
|
838
|
+
.update([
|
|
839
|
+
...[...documents]
|
|
840
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
841
|
+
.map(document => `${document.path}\n${document.content}`),
|
|
842
|
+
...extra,
|
|
843
|
+
`contract:${JSON.stringify(contractIdentity)}`,
|
|
844
|
+
].join("\n---\n"))
|
|
845
|
+
.digest("hex");
|
|
846
|
+
}
|
|
847
|
+
function parseAppSpecDocument(root, path, kind, content, diagnostics) {
|
|
848
|
+
const pointer = relativePath(root, path);
|
|
849
|
+
const frontMatter = parseFrontMatter(content, pointer, diagnostics);
|
|
850
|
+
if (!frontMatter)
|
|
851
|
+
return null;
|
|
852
|
+
validateMetadata(kind, frontMatter.metadata, pointer, diagnostics);
|
|
853
|
+
const expectedSchema = APP_SPEC_SCHEMAS[kind];
|
|
854
|
+
if (frontMatter.metadata.schema !== expectedSchema) {
|
|
855
|
+
diagnostics.push(issue("error", "APPSPEC_SCHEMA_INVALID", `期望 schema=${expectedSchema}`, pointer));
|
|
856
|
+
}
|
|
857
|
+
const id = String(kind === "app" ? frontMatter.metadata.app || "" : frontMatter.metadata.id || "");
|
|
858
|
+
const pattern = kind === "app"
|
|
859
|
+
? APP_ID
|
|
860
|
+
: kind === "capability"
|
|
861
|
+
? CAPABILITY_ID
|
|
862
|
+
: kind === "decision"
|
|
863
|
+
? DECISION_ID
|
|
864
|
+
: kind === "design" ? /^DES-[A-Z0-9]+(?:-[A-Z0-9]+)*$/ : CHANGE_ID;
|
|
865
|
+
if (!pattern.test(id)) {
|
|
866
|
+
diagnostics.push(issue("error", "APPSPEC_DOCUMENT_ID_INVALID", `无效 ${kind} id: ${id}${kind === 'decision' ? ';请使用 ADR- 加四位数字,可选大写字母/数字后缀,例如 ADR-0001 或 ADR-0001-DATA-OWNER,参见 docs appspec' : ''}`, pointer));
|
|
867
|
+
}
|
|
868
|
+
const status = String(frontMatter.metadata.status || "draft");
|
|
869
|
+
const validStatuses = {
|
|
870
|
+
app: ["active", "retired"],
|
|
871
|
+
capability: ["draft", "active", "retired"],
|
|
872
|
+
change: ["draft", "confirmed", "implementing", "verified", "released", "archived", "cancelled"],
|
|
873
|
+
design: ["draft", "confirmed", "superseded", "rejected"],
|
|
874
|
+
decision: ["proposed", "accepted", "superseded", "rejected"],
|
|
875
|
+
};
|
|
876
|
+
if (!validStatuses[kind].includes(status)) {
|
|
877
|
+
diagnostics.push(issue("error", "APPSPEC_STATUS_INVALID", `${kind} status=${status} 不受支持`, pointer));
|
|
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
|
+
}
|
|
882
|
+
if (kind === "change") {
|
|
883
|
+
const currentSpec = String(frontMatter.metadata.currentSpec || "pending");
|
|
884
|
+
if (!["pending", "merged", "not-applicable"].includes(currentSpec)) {
|
|
885
|
+
diagnostics.push(issue("error", "APPSPEC_CURRENT_SPEC_STATUS_INVALID", `change currentSpec=${currentSpec} 不受支持`, pointer));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const h1 = content.match(/^#\s+(.+)$/m)?.[1]?.trim() || "";
|
|
889
|
+
const title = String(frontMatter.metadata.title || h1 || id);
|
|
890
|
+
const requirementIds = extractIds(content, /^###\s+(REQ-[A-Z0-9-]+)\b/gm, REQUIREMENT_ID);
|
|
891
|
+
const acceptanceIds = extractIds(content, /^####\s+(AC-[A-Z0-9-]+)\b/gm, ACCEPTANCE_ID);
|
|
892
|
+
return {
|
|
893
|
+
kind,
|
|
894
|
+
id,
|
|
895
|
+
title,
|
|
896
|
+
status,
|
|
897
|
+
path: pointer,
|
|
898
|
+
metadata: frontMatter.metadata,
|
|
899
|
+
requirementIds,
|
|
900
|
+
acceptanceIds,
|
|
901
|
+
references: {
|
|
902
|
+
capabilities: metadataStrings(frontMatter.metadata, "capabilities"),
|
|
903
|
+
requirements: metadataStrings(frontMatter.metadata, "requirements"),
|
|
904
|
+
resources: metadataStrings(frontMatter.metadata, "resources"),
|
|
905
|
+
actions: metadataStrings(frontMatter.metadata, "actions"),
|
|
906
|
+
decisions: metadataStrings(frontMatter.metadata, "decisions"),
|
|
907
|
+
documents: metadataStrings(frontMatter.metadata, "documents"),
|
|
908
|
+
},
|
|
909
|
+
content,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function parseFrontMatter(content, pointer, diagnostics) {
|
|
913
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
914
|
+
if (!match) {
|
|
915
|
+
diagnostics.push(issue("error", "APPSPEC_FRONT_MATTER_REQUIRED", "AppSpec Markdown 必须以受限 YAML front matter 开头", pointer));
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const metadata = {};
|
|
919
|
+
const lines = (match[1] || "").split(/\r?\n/);
|
|
920
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
921
|
+
const raw = lines[index] || "";
|
|
922
|
+
if (!raw.trim() || raw.trimStart().startsWith("#"))
|
|
923
|
+
continue;
|
|
924
|
+
const entry = raw.match(/^([A-Za-z][A-Za-z0-9_-]*):(?:\s*(.*))?$/);
|
|
925
|
+
if (!entry) {
|
|
926
|
+
diagnostics.push(issue("error", "APPSPEC_FRONT_MATTER_INVALID", `不支持的 front matter 行: ${raw.trim()}`, pointer));
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const key = entry[1] || "";
|
|
930
|
+
const rawValue = entry[2] || "";
|
|
931
|
+
if (rawValue.trim()) {
|
|
932
|
+
metadata[key] = parseMetadataValue(rawValue.trim());
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const values = [];
|
|
936
|
+
while (index + 1 < lines.length) {
|
|
937
|
+
const next = lines[index + 1] || "";
|
|
938
|
+
const item = next.match(/^\s{2,}-\s+(.+)$/);
|
|
939
|
+
if (!item)
|
|
940
|
+
break;
|
|
941
|
+
values.push(String(parseMetadataValue(item[1] || "")));
|
|
942
|
+
index += 1;
|
|
943
|
+
}
|
|
944
|
+
metadata[key] = values;
|
|
945
|
+
}
|
|
946
|
+
return { metadata };
|
|
947
|
+
}
|
|
948
|
+
function parseMetadataValue(value) {
|
|
949
|
+
if (value === "true")
|
|
950
|
+
return true;
|
|
951
|
+
if (value === "false")
|
|
952
|
+
return false;
|
|
953
|
+
if (value === "[]")
|
|
954
|
+
return [];
|
|
955
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
956
|
+
try {
|
|
957
|
+
const parsed = JSON.parse(value);
|
|
958
|
+
if (Array.isArray(parsed))
|
|
959
|
+
return parsed.map(item => String(item));
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
return value
|
|
963
|
+
.slice(1, -1)
|
|
964
|
+
.split(",")
|
|
965
|
+
.map(item => unquote(item.trim()))
|
|
966
|
+
.filter(Boolean);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return unquote(value);
|
|
970
|
+
}
|
|
971
|
+
function unquote(value) {
|
|
972
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
973
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
974
|
+
if (value.startsWith('"')) {
|
|
975
|
+
try {
|
|
976
|
+
return String(JSON.parse(value));
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
return value.slice(1, -1);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
983
|
+
}
|
|
984
|
+
return value;
|
|
985
|
+
}
|
|
986
|
+
function metadataStrings(metadata, key) {
|
|
987
|
+
const value = metadata[key];
|
|
988
|
+
if (Array.isArray(value))
|
|
989
|
+
return normalized(value);
|
|
990
|
+
if (typeof value === "string" && value.trim())
|
|
991
|
+
return [value.trim()];
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
function requirementsWithoutAcceptance(content) {
|
|
995
|
+
const source = withoutHtmlComments(content);
|
|
996
|
+
const matches = [...source.matchAll(/^###\s+(REQ-[A-Z0-9-]+)\b/gm)];
|
|
997
|
+
const missing = [];
|
|
998
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
999
|
+
const start = matches[index]?.index || 0;
|
|
1000
|
+
const end = matches[index + 1]?.index ?? source.length;
|
|
1001
|
+
const block = source.slice(start, end);
|
|
1002
|
+
if (!/^####\s+AC-[A-Z0-9-]+\b/m.test(block)) {
|
|
1003
|
+
const id = matches[index]?.[1];
|
|
1004
|
+
if (id)
|
|
1005
|
+
missing.push(id);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return missing;
|
|
1009
|
+
}
|
|
1010
|
+
function unresolvedQuestions(content) {
|
|
1011
|
+
const source = withoutHtmlComments(content);
|
|
1012
|
+
const heading = source.match(/^##\s+(?:未确认问题|Unresolved Questions)\s*$/m);
|
|
1013
|
+
if (heading?.index === undefined)
|
|
1014
|
+
return [];
|
|
1015
|
+
const tail = source.slice(heading.index + heading[0].length);
|
|
1016
|
+
const nextHeading = tail.search(/^##\s+/m);
|
|
1017
|
+
const section = nextHeading >= 0 ? tail.slice(0, nextHeading) : tail;
|
|
1018
|
+
return section ? [...section.matchAll(/^- \[ \]\s+(.+)$/gm)].map(match => match[1]) : [];
|
|
1019
|
+
}
|
|
1020
|
+
function hasMeaningfulSection(content, alternatives, placeholders) {
|
|
1021
|
+
const source = withoutHtmlComments(content);
|
|
1022
|
+
for (const title of alternatives) {
|
|
1023
|
+
const heading = new RegExp(`^##\\s+${escapeRegExp(title)}\\s*$`, "m").exec(source);
|
|
1024
|
+
if (heading?.index === undefined)
|
|
1025
|
+
continue;
|
|
1026
|
+
const tail = source.slice(heading.index + heading[0].length);
|
|
1027
|
+
const nextHeading = tail.search(/^##\s+/m);
|
|
1028
|
+
const body = nextHeading >= 0 ? tail.slice(0, nextHeading) : tail;
|
|
1029
|
+
const meaningfulLines = body
|
|
1030
|
+
.split(/\r?\n/)
|
|
1031
|
+
.map(line => line.trim())
|
|
1032
|
+
.filter(Boolean)
|
|
1033
|
+
.filter(line => !/^[-*](?:\s+\[[ xX]\])?\s*$/.test(line))
|
|
1034
|
+
.filter(line => !placeholders.some(placeholder => line.includes(placeholder)));
|
|
1035
|
+
if (meaningfulLines.length > 0)
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
function currentSpecStatus(document) {
|
|
1041
|
+
const value = String(document.metadata.currentSpec || "pending");
|
|
1042
|
+
return ["merged", "not-applicable"].includes(value)
|
|
1043
|
+
? value
|
|
1044
|
+
: "pending";
|
|
1045
|
+
}
|
|
1046
|
+
function duplicateDiagnostics(values, id, code, diagnostics) {
|
|
1047
|
+
const seen = new Map();
|
|
1048
|
+
for (const value of values) {
|
|
1049
|
+
const key = id(value);
|
|
1050
|
+
if (!key)
|
|
1051
|
+
continue;
|
|
1052
|
+
const previous = seen.get(key);
|
|
1053
|
+
if (previous) {
|
|
1054
|
+
diagnostics.push(issue("error", code, `稳定 ID ${key} 重复,首次出现在 ${previous}`, value.path));
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
seen.set(key, value.path);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function markdownFiles(directory, recursive, diagnostics, root, depth = 0) {
|
|
1062
|
+
if (!existsSync(directory))
|
|
1063
|
+
return [];
|
|
1064
|
+
if (!isRegularDirectory(directory)) {
|
|
1065
|
+
diagnostics.push(issue("error", "APPSPEC_DIRECTORY_INVALID", "AppSpec 子目录必须是普通目录,不能是符号链接", relativePath(root, directory)));
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
const files = [];
|
|
1069
|
+
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1070
|
+
if (entries.length > APP_SPEC_LIMITS.maximumDirectoryEntries) {
|
|
1071
|
+
diagnostics.push(issue("error", "APPSPEC_DIRECTORY_ENTRY_LIMIT_EXCEEDED", `AppSpec 单目录条目数超过 ${APP_SPEC_LIMITS.maximumDirectoryEntries} 上限`, relativePath(root, directory)));
|
|
1072
|
+
entries.splice(APP_SPEC_LIMITS.maximumDirectoryEntries);
|
|
1073
|
+
}
|
|
1074
|
+
for (const entry of entries) {
|
|
1075
|
+
const path = join(directory, entry.name);
|
|
1076
|
+
if (entry.isSymbolicLink()) {
|
|
1077
|
+
diagnostics.push(issue("error", "APPSPEC_SYMLINK_FORBIDDEN", "AppSpec 不读取符号链接", relativePath(root, path)));
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (entry.isDirectory() && recursive) {
|
|
1081
|
+
if (depth >= APP_SPEC_LIMITS.maximumHistoryDepth) {
|
|
1082
|
+
diagnostics.push(issue("error", "APPSPEC_HISTORY_DEPTH_EXCEEDED", "AppSpec 历史目录只允许 history/<year>/*.md", relativePath(root, path)));
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
files.push(...markdownFiles(path, true, diagnostics, root, depth + 1));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
1089
|
+
files.push(path);
|
|
1090
|
+
else if (!entry.name.startsWith(".")) {
|
|
1091
|
+
diagnostics.push(issue("warning", "APPSPEC_UNKNOWN_ENTRY_IGNORED", "AppSpec 只读取约定目录中的 Markdown 文件", relativePath(root, path)));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return files;
|
|
1095
|
+
}
|
|
1096
|
+
function appSpecPaths(root) {
|
|
1097
|
+
const workspace = resolve(root);
|
|
1098
|
+
const appSpecRoot = resolve(workspace, "appspec");
|
|
1099
|
+
assertInside(workspace, appSpecRoot);
|
|
1100
|
+
return {
|
|
1101
|
+
root: appSpecRoot,
|
|
1102
|
+
app: join(appSpecRoot, "app.md"),
|
|
1103
|
+
capabilities: join(appSpecRoot, "capabilities"),
|
|
1104
|
+
activeChanges: join(appSpecRoot, "changes", "active"),
|
|
1105
|
+
history: join(appSpecRoot, "changes", "history"),
|
|
1106
|
+
decisions: join(appSpecRoot, "decisions"),
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function renderApplicationSpec(appCode, appName) {
|
|
1110
|
+
return `---
|
|
1111
|
+
schema: ${APP_SPEC_SCHEMAS.app}
|
|
1112
|
+
app: ${JSON.stringify(appCode)}
|
|
1113
|
+
title: ${JSON.stringify(appName)}
|
|
1114
|
+
status: active
|
|
1115
|
+
---
|
|
1116
|
+
# ${appName}
|
|
1117
|
+
|
|
1118
|
+
本文件是当前有效设计的总纲与目录。新应用先完成产品发现、详细设计与实际确认,形成评审基线后再制定实施计划;按 docs product-design 在 AppSpec 中建立相关材料。正式变更关联 ChangeSpec;测试发布前核对设计与验收计划,生产晋级前核对实际验收。资源、字段、权限和动作的实现以
|
|
1119
|
+
\`openxiangda.config.ts\` 与实时编译合同为准。
|
|
1120
|
+
|
|
1121
|
+
## 业务目标
|
|
1122
|
+
|
|
1123
|
+
<!-- 用业务语言说明应用解决的问题和可观察结果。 -->
|
|
1124
|
+
|
|
1125
|
+
## 角色
|
|
1126
|
+
|
|
1127
|
+
<!-- 只记录业务角色及其目标;稳定 role code 在能力规格中引用。 -->
|
|
1128
|
+
|
|
1129
|
+
## 范围
|
|
1130
|
+
|
|
1131
|
+
### 包含
|
|
1132
|
+
|
|
1133
|
+
### 不包含
|
|
1134
|
+
|
|
1135
|
+
## 设计资料目录
|
|
1136
|
+
|
|
1137
|
+
<!-- 引用产品、旅程、页面、视觉、权限、架构与评审的稳定 ID;详细规则各有唯一归属。 -->
|
|
1138
|
+
|
|
1139
|
+
## 能力目录
|
|
1140
|
+
|
|
1141
|
+
<!-- 复杂业务再在 capabilities/ 下增加 CAP-*;简单应用的稳定规则直接写在本文件。 -->
|
|
1142
|
+
|
|
1143
|
+
## 术语
|
|
1144
|
+
|
|
1145
|
+
## 跨能力约束
|
|
1146
|
+
|
|
1147
|
+
## 架构与数据关系
|
|
1148
|
+
|
|
1149
|
+
<!-- 写明领域边界、核心模型关系、平台能力与自定义动作的归属、状态转换与不变量。 -->
|
|
1150
|
+
|
|
1151
|
+
## 业务任务与页面
|
|
1152
|
+
|
|
1153
|
+
<!-- 按用户任务规划管理端、PC、移动入口与数据关系;辅助表不必有页面。 -->
|
|
1154
|
+
|
|
1155
|
+
## 权限矩阵与确认
|
|
1156
|
+
|
|
1157
|
+
<!-- 记录角色、页面、操作、行范围、字段和多角色组合;写明需求依据与未明确的边界。 -->
|
|
1158
|
+
|
|
1159
|
+
## 性能与容量预算
|
|
1160
|
+
|
|
1161
|
+
<!-- 记录数据量及增长、并发、分页与索引、请求次数、延迟目标和测量口径;区分估算、目标与实测。 -->
|
|
1162
|
+
|
|
1163
|
+
## 未确认问题
|
|
1164
|
+
|
|
1165
|
+
- 无。
|
|
1166
|
+
`;
|
|
1167
|
+
}
|
|
1168
|
+
function renderCapabilitySpec(input) {
|
|
1169
|
+
return `---
|
|
1170
|
+
schema: ${APP_SPEC_SCHEMAS.capability}
|
|
1171
|
+
id: ${input.id}
|
|
1172
|
+
title: ${JSON.stringify(input.title)}
|
|
1173
|
+
status: draft
|
|
1174
|
+
resources: ${JSON.stringify(input.resources)}
|
|
1175
|
+
actions: ${JSON.stringify(input.actions)}
|
|
1176
|
+
---
|
|
1177
|
+
# ${input.title}
|
|
1178
|
+
|
|
1179
|
+
## 目标与边界
|
|
1180
|
+
|
|
1181
|
+
<!-- 描述用户可观察的业务能力,不复制字段物理 Schema 或页面实现。 -->
|
|
1182
|
+
|
|
1183
|
+
## 角色与权限
|
|
1184
|
+
|
|
1185
|
+
## 当前有效需求
|
|
1186
|
+
|
|
1187
|
+
<!--
|
|
1188
|
+
### REQ-DOMAIN-001 规则名称
|
|
1189
|
+
|
|
1190
|
+
当发生某个业务条件时,系统必须产生可观察结果。
|
|
1191
|
+
|
|
1192
|
+
#### AC-DOMAIN-001-01 正向或反向场景
|
|
1193
|
+
|
|
1194
|
+
- 前置:已知条件
|
|
1195
|
+
- 操作:用户或系统执行动作
|
|
1196
|
+
- 预期:观察到明确结果
|
|
1197
|
+
-->
|
|
1198
|
+
|
|
1199
|
+
## 状态与异常
|
|
1200
|
+
|
|
1201
|
+
## 非目标
|
|
1202
|
+
`;
|
|
1203
|
+
}
|
|
1204
|
+
function renderChangeSpec(input) {
|
|
1205
|
+
const createdAt = new Date().toISOString();
|
|
1206
|
+
return `---
|
|
1207
|
+
schema: ${APP_SPEC_SCHEMAS.change}
|
|
1208
|
+
id: ${input.id}
|
|
1209
|
+
title: ${JSON.stringify(input.title)}
|
|
1210
|
+
status: draft
|
|
1211
|
+
currentSpec: pending
|
|
1212
|
+
risk: ${input.risk}
|
|
1213
|
+
createdAt: ${JSON.stringify(createdAt)}
|
|
1214
|
+
capabilities: ${JSON.stringify(input.capabilities)}
|
|
1215
|
+
requirements: ${JSON.stringify(input.requirements)}
|
|
1216
|
+
resources: ${JSON.stringify(input.resources)}
|
|
1217
|
+
actions: ${JSON.stringify(input.actions)}
|
|
1218
|
+
decisions: []
|
|
1219
|
+
---
|
|
1220
|
+
# ${input.title}
|
|
1221
|
+
|
|
1222
|
+
## 为什么
|
|
1223
|
+
|
|
1224
|
+
${input.summary || "<!-- 一两句话说明问题、证据和期望结果。 -->"}
|
|
1225
|
+
|
|
1226
|
+
## 需求依据
|
|
1227
|
+
|
|
1228
|
+
<!-- 记录用户原始需求或当前规格引用、已明确规则与假设;不代替用户编造确认。 -->
|
|
1229
|
+
|
|
1230
|
+
## 方案与影响
|
|
1231
|
+
|
|
1232
|
+
<!-- 说明能力所有者、模型/页面/权限/状态影响,引用已有架构决定。 -->
|
|
1233
|
+
|
|
1234
|
+
## 任务与实现
|
|
1235
|
+
|
|
1236
|
+
<!-- 用稳定需求 ID 关联实施任务、源码/声明路径与验收场景;实施后更新实际结果。 -->
|
|
1237
|
+
|
|
1238
|
+
## 性能与容量预算
|
|
1239
|
+
|
|
1240
|
+
<!-- 引用总纲默认预算,或记录本次数据规模、查询/分页/批量/超时/并发和延迟目标。 -->
|
|
1241
|
+
|
|
1242
|
+
## 变更
|
|
1243
|
+
|
|
1244
|
+
- ADDED:
|
|
1245
|
+
- MODIFIED:
|
|
1246
|
+
- REMOVED:
|
|
1247
|
+
- 非目标:
|
|
1248
|
+
|
|
1249
|
+
## 验收
|
|
1250
|
+
|
|
1251
|
+
- [ ] 一个可观察的正向结果
|
|
1252
|
+
- [ ] 需要时补充拒绝、异常或权限反例
|
|
1253
|
+
|
|
1254
|
+
<!-- 使用 #### AC-领域-编号 给场景稳定 ID;写明角色、前置、操作、预期和拒绝路径。测试部署后再记录实际观察。 -->
|
|
1255
|
+
|
|
1256
|
+
## 数据与权限
|
|
1257
|
+
|
|
1258
|
+
- 无,或说明资源、字段、角色和数据范围变化。
|
|
1259
|
+
|
|
1260
|
+
## 失败、并发与幂等
|
|
1261
|
+
|
|
1262
|
+
- L3 才需要详细维护;其他变化写“无”。
|
|
1263
|
+
|
|
1264
|
+
## 回滚
|
|
1265
|
+
|
|
1266
|
+
- 回退声明/代码并保持旧数据可读;如不适用请说明。
|
|
1267
|
+
|
|
1268
|
+
## 架构决策
|
|
1269
|
+
|
|
1270
|
+
- L3 如涉及 ADR,在 front matter 的 decisions 中引用。
|
|
1271
|
+
|
|
1272
|
+
## 验证与发布
|
|
1273
|
+
|
|
1274
|
+
<!-- 记录检查结果、源码提交、测试/生产运行 ID、包摘要、验收报告路径、未覆盖项和恢复入口。区分本地通过、部署成功与业务验收。 -->
|
|
1275
|
+
|
|
1276
|
+
## 交接
|
|
1277
|
+
|
|
1278
|
+
<!-- 写明持续有效规则已更新的位置、剩余问题与下一步;无需复制当前规格全文。 -->
|
|
1279
|
+
|
|
1280
|
+
## 未确认问题
|
|
1281
|
+
|
|
1282
|
+
- 无。
|
|
1283
|
+
`;
|
|
1284
|
+
}
|
|
1285
|
+
function appendClosure(content, summary, closedAt) {
|
|
1286
|
+
return `${content.trimEnd()}\n\n## 关闭记录\n\n- 时间:${closedAt}\n- 摘要:${summary}\n- 说明:归档不等于部署或生产验收;以实际 Verification/Deployment 证据为准。\n`;
|
|
1287
|
+
}
|
|
1288
|
+
function setFrontMatterValue(content, key, value) {
|
|
1289
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1290
|
+
if (!match)
|
|
1291
|
+
throw stableError("APPSPEC_FRONT_MATTER_REQUIRED", key);
|
|
1292
|
+
const encoded = JSON.stringify(value);
|
|
1293
|
+
const body = match[1] || "";
|
|
1294
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}:.*$`, "m");
|
|
1295
|
+
const nextBody = pattern.test(body)
|
|
1296
|
+
? body.replace(pattern, `${key}: ${encoded}`)
|
|
1297
|
+
: `${body.trimEnd()}\n${key}: ${encoded}`;
|
|
1298
|
+
return content.replace(match[0], `---\n${nextBody}\n---`);
|
|
1299
|
+
}
|
|
1300
|
+
function exclusiveWrite(path, content) {
|
|
1301
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1302
|
+
try {
|
|
1303
|
+
writeFileSync(path, content, { encoding: "utf8", flag: "wx" });
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
if (error.code === "EEXIST") {
|
|
1307
|
+
throw stableError("APPSPEC_DOCUMENT_EXISTS", path);
|
|
1308
|
+
}
|
|
1309
|
+
throw error;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function atomicReplace(path, content) {
|
|
1313
|
+
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
1314
|
+
exclusiveWrite(temporary, content);
|
|
1315
|
+
renameSync(temporary, path);
|
|
1316
|
+
}
|
|
1317
|
+
function assertRegularFile(path) {
|
|
1318
|
+
const stat = lstatSync(path);
|
|
1319
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1320
|
+
throw stableError("APPSPEC_FILE_INVALID", path);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function isRegularDirectory(path) {
|
|
1324
|
+
try {
|
|
1325
|
+
const stat = lstatSync(path);
|
|
1326
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function assertInside(root, path) {
|
|
1333
|
+
const base = `${resolve(root)}/`;
|
|
1334
|
+
const target = `${resolve(path)}/`;
|
|
1335
|
+
if (!target.startsWith(base))
|
|
1336
|
+
throw stableError("APPSPEC_PATH_OUTSIDE_WORKSPACE", path);
|
|
1337
|
+
}
|
|
1338
|
+
function stableError(code, detail) {
|
|
1339
|
+
return Object.assign(new Error(`${code}: ${detail}`), { code, retryable: false });
|
|
1340
|
+
}
|
|
1341
|
+
function issue(severity, code, message, path) {
|
|
1342
|
+
return {
|
|
1343
|
+
schemaVersion: SCHEMA_VERSIONS.diagnostic,
|
|
1344
|
+
code,
|
|
1345
|
+
severity,
|
|
1346
|
+
message,
|
|
1347
|
+
path,
|
|
1348
|
+
retryable: false,
|
|
1349
|
+
remediation: severity === "error"
|
|
1350
|
+
? "修正记录后运行 openxiangda spec check;普通 check 可继续,正式发布按阶段核对"
|
|
1351
|
+
: "按实际变化范围补充;确定性缺口会在相应发布阶段核对",
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function summarize(document) {
|
|
1355
|
+
return {
|
|
1356
|
+
kind: document.kind,
|
|
1357
|
+
id: document.id,
|
|
1358
|
+
title: document.title,
|
|
1359
|
+
status: document.status,
|
|
1360
|
+
path: document.path,
|
|
1361
|
+
risk: document.kind === "change" && ["L1", "L2", "L3"].includes(String(document.metadata.risk))
|
|
1362
|
+
? String(document.metadata.risk)
|
|
1363
|
+
: null,
|
|
1364
|
+
requirementCount: document.requirementIds.length,
|
|
1365
|
+
acceptanceScenarioCount: document.acceptanceIds.length,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function extractIds(content, pattern, validation) {
|
|
1369
|
+
return unique([...withoutHtmlComments(content).matchAll(pattern)]
|
|
1370
|
+
.map(match => match[1] || "")
|
|
1371
|
+
.filter(value => validation.test(value)));
|
|
1372
|
+
}
|
|
1373
|
+
function withoutHtmlComments(content) {
|
|
1374
|
+
return content.replace(/<!--[\s\S]*?-->/g, "");
|
|
1375
|
+
}
|
|
1376
|
+
function validateMetadata(kind, metadata, pointer, diagnostics) {
|
|
1377
|
+
const allowed = {
|
|
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"]),
|
|
1380
|
+
capability: new Set([
|
|
1381
|
+
"schema",
|
|
1382
|
+
"documents",
|
|
1383
|
+
"id",
|
|
1384
|
+
"title",
|
|
1385
|
+
"status",
|
|
1386
|
+
"resources",
|
|
1387
|
+
"actions",
|
|
1388
|
+
]),
|
|
1389
|
+
change: new Set([
|
|
1390
|
+
"schema",
|
|
1391
|
+
"documents",
|
|
1392
|
+
"id",
|
|
1393
|
+
"title",
|
|
1394
|
+
"status",
|
|
1395
|
+
"currentSpec",
|
|
1396
|
+
"risk",
|
|
1397
|
+
"createdAt",
|
|
1398
|
+
"closedAt",
|
|
1399
|
+
"capabilities",
|
|
1400
|
+
"requirements",
|
|
1401
|
+
"resources",
|
|
1402
|
+
"actions",
|
|
1403
|
+
"decisions",
|
|
1404
|
+
]),
|
|
1405
|
+
decision: new Set([
|
|
1406
|
+
"schema",
|
|
1407
|
+
"documents",
|
|
1408
|
+
"id",
|
|
1409
|
+
"title",
|
|
1410
|
+
"status",
|
|
1411
|
+
"date",
|
|
1412
|
+
"deciders",
|
|
1413
|
+
"supersedes",
|
|
1414
|
+
]),
|
|
1415
|
+
};
|
|
1416
|
+
const identifierKey = kind === "app" ? "app" : "id";
|
|
1417
|
+
for (const required of ["schema", identifierKey, "status"]) {
|
|
1418
|
+
if (!(required in metadata)) {
|
|
1419
|
+
diagnostics.push(issue("error", "APPSPEC_METADATA_REQUIRED", `${kind} front matter 缺少 ${required}`, pointer));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (const key of Object.keys(metadata)) {
|
|
1423
|
+
if (!allowed[kind].has(key)) {
|
|
1424
|
+
diagnostics.push(issue("error", "APPSPEC_METADATA_UNKNOWN", `${kind} front matter 不支持 ${key}`, pointer));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function normalized(values) {
|
|
1429
|
+
return unique((values || []).map(value => String(value).trim()).filter(Boolean));
|
|
1430
|
+
}
|
|
1431
|
+
function unique(values) {
|
|
1432
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
1433
|
+
}
|
|
1434
|
+
function fileNameForId(id, prefix) {
|
|
1435
|
+
return id.slice(prefix.length).toLowerCase();
|
|
1436
|
+
}
|
|
1437
|
+
function relativePath(root, path) {
|
|
1438
|
+
return relative(resolve(root), resolve(path)).replaceAll("\\", "/");
|
|
1439
|
+
}
|
|
1440
|
+
function escapeRegExp(value) {
|
|
1441
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1442
|
+
}
|
|
1443
|
+
//# sourceMappingURL=app-spec.js.map
|