kld-sdd 2.6.5 → 2.6.7
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/package.json +2 -2
- package/skywalk-sdd/context-client.cjs +12 -1
- package/skywalk-sdd/ontology/archive-package.cjs +95 -6
- package/skywalk-sdd/ontology/artifact-parser.cjs +50 -7
- package/skywalk-sdd/ontology/cli.cjs +19 -0
- package/skywalk-sdd/ontology/external-key.cjs +159 -0
- package/skywalk-sdd/ontology/normalizer.cjs +4 -0
- package/skywalk-sdd/ontology/schema.cjs +5 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +175 -0
- package/templates/openspec/proposal.md +5 -2
- package/templates/openspec/spec.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +3 -1
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +16 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +12 -1
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -0
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +13 -14
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +13 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +26 -15
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +19 -10
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +8 -2
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +8 -1
- package/templates/skills/kld-sdd/opsx-task/checklist.md +3 -0
- package/templates/skills/kld-sdd/opsx-tdd-anti-patterns/SKILL.md +5 -2
- package/templates/skills/kld-sdd/opsx-tdd-anti-patterns/reference.md +29 -0
- package/templates/skills/kld-sdd/opsx-tdd-core/checklist.md +4 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/multi-validation-split.md +35 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kld-sdd",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.7",
|
|
4
4
|
"description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"kld-sdd-init": "bin/kld-sdd-init.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
|
|
11
|
+
"test": "node test/external-key.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"kld",
|
|
@@ -132,7 +132,18 @@ function buildPayload(args, mode) {
|
|
|
132
132
|
if (args['entity-type']) payload.entityType = args['entity-type'];
|
|
133
133
|
if (args['external-system']) payload.externalSystem = args['external-system'];
|
|
134
134
|
if (args['external-object-type']) payload.externalObjectType = args['external-object-type'];
|
|
135
|
-
if (args['external-id'])
|
|
135
|
+
if (args['external-id']) {
|
|
136
|
+
const externalKey = require('./ontology/external-key.cjs');
|
|
137
|
+
const objectType = args['external-object-type'] || 'requirement';
|
|
138
|
+
const check = externalKey.validate(args['external-id'], objectType);
|
|
139
|
+
if (!check.ok) {
|
|
140
|
+
throw new Error(`${check.code}: ${check.message}`);
|
|
141
|
+
}
|
|
142
|
+
payload.externalId = externalKey.normalize(args['external-id'], objectType);
|
|
143
|
+
if (payload.externalObjectType) {
|
|
144
|
+
payload.externalObjectType = String(payload.externalObjectType).trim().toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
136
147
|
if (args['canonical-key']) payload.canonicalKey = args['canonical-key'];
|
|
137
148
|
return payload;
|
|
138
149
|
}
|
|
@@ -101,26 +101,107 @@ function requiredText(value, field, owner) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function normalizeExternalRefs(refs) {
|
|
104
|
+
const { normalize } = require('./external-key.cjs');
|
|
104
105
|
if (!Array.isArray(refs)) return [];
|
|
105
106
|
const seen = new Set();
|
|
106
107
|
const result = [];
|
|
107
108
|
for (const ref of refs) {
|
|
108
109
|
const system = String(ref.system || ref.system_name || '').trim();
|
|
109
110
|
const objectType = String(ref.object_type || ref.objectType || '').trim().toLowerCase();
|
|
110
|
-
const
|
|
111
|
-
if (!system || !objectType || !
|
|
111
|
+
const rawExternalId = String(ref.external_id || ref.externalId || '').trim();
|
|
112
|
+
if (!system || !objectType || !rawExternalId) continue;
|
|
113
|
+
const externalId = normalize(rawExternalId, objectType) || rawExternalId;
|
|
112
114
|
const key = `${system}\0${objectType}\0${externalId}`;
|
|
113
115
|
if (seen.has(key)) continue;
|
|
114
116
|
seen.add(key);
|
|
115
|
-
|
|
117
|
+
const entry = {
|
|
116
118
|
system,
|
|
117
119
|
object_type: objectType,
|
|
118
120
|
external_id: externalId,
|
|
119
|
-
}
|
|
121
|
+
};
|
|
122
|
+
const rawFeatureId = ref.feature_id || ref.featureId;
|
|
123
|
+
if (rawFeatureId && objectType === 'requirement') {
|
|
124
|
+
entry.feature_id = normalize(String(rawFeatureId), 'feature') || String(rawFeatureId).trim();
|
|
125
|
+
}
|
|
126
|
+
result.push(entry);
|
|
120
127
|
}
|
|
121
128
|
return result;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
function expandRequirementRefsWithFeatures(requirementRefs) {
|
|
132
|
+
const expanded = [];
|
|
133
|
+
for (const ref of requirementRefs) {
|
|
134
|
+
expanded.push(ref);
|
|
135
|
+
if (ref.object_type === 'requirement' && ref.feature_id) {
|
|
136
|
+
expanded.push({
|
|
137
|
+
system: ref.system,
|
|
138
|
+
object_type: 'feature',
|
|
139
|
+
external_id: ref.feature_id,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return normalizeExternalRefs(expanded);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertExternalKeyFinalGate(entities) {
|
|
147
|
+
const {
|
|
148
|
+
validate,
|
|
149
|
+
normalize,
|
|
150
|
+
parseScenario,
|
|
151
|
+
CODES,
|
|
152
|
+
} = require('./external-key.cjs');
|
|
153
|
+
const packageRequirementIds = new Set();
|
|
154
|
+
for (const entity of entities) {
|
|
155
|
+
for (const ref of entity.external_refs || []) {
|
|
156
|
+
if (ref.object_type === 'requirement') {
|
|
157
|
+
packageRequirementIds.add(normalize(ref.external_id, 'requirement'));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const entity of entities) {
|
|
162
|
+
const declaredFeatures = new Set();
|
|
163
|
+
const featureBindings = new Set();
|
|
164
|
+
for (const ref of entity.external_refs || []) {
|
|
165
|
+
const format = validate(ref.external_id, ref.object_type);
|
|
166
|
+
if (!format.ok) {
|
|
167
|
+
throw new Error(`${CODES.FORMAT_INVALID}: ${format.message} @ ${entity.anchor_id}`);
|
|
168
|
+
}
|
|
169
|
+
if (ref.feature_id) {
|
|
170
|
+
const featureFormat = validate(ref.feature_id, 'feature');
|
|
171
|
+
if (!featureFormat.ok) {
|
|
172
|
+
throw new Error(`${CODES.FORMAT_INVALID}: requirement.feature_id 不合规: ${featureFormat.message} @ ${entity.anchor_id}`);
|
|
173
|
+
}
|
|
174
|
+
declaredFeatures.add(normalize(ref.feature_id, 'feature'));
|
|
175
|
+
}
|
|
176
|
+
if (ref.object_type === 'feature') {
|
|
177
|
+
featureBindings.add(normalize(ref.external_id, 'feature'));
|
|
178
|
+
}
|
|
179
|
+
if (ref.object_type === 'scenario') {
|
|
180
|
+
const parts = parseScenario(ref.external_id);
|
|
181
|
+
if (!parts || !packageRequirementIds.has(parts.requirementId)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`${CODES.SCOPE_MISMATCH}: 场景键 REQ 前缀不在同包 requirement 绑定集合中: ${ref.external_id} @ ${entity.anchor_id}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const featureId of featureBindings) {
|
|
189
|
+
if (!declaredFeatures.has(featureId)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`${CODES.FEATURE_UNDECLARED}: feature 绑定无法与同实体 requirement.feature_id 配对: ${featureId} @ ${entity.anchor_id}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const featureId of declaredFeatures) {
|
|
196
|
+
if (!featureBindings.has(featureId)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`${CODES.FEATURE_UNDECLARED}: requirement 申报了 feature_id 但缺少对应 feature 绑定: ${featureId} @ ${entity.anchor_id}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
124
205
|
function canonicalEntity(archiveDir, entity, inheritedRequirementRefs = []) {
|
|
125
206
|
const anchorId = requiredText(entity.anchor_id || entity.id, 'anchor_id', '本体实体').toUpperCase();
|
|
126
207
|
const versionId = requiredText(
|
|
@@ -130,8 +211,15 @@ function canonicalEntity(archiveDir, entity, inheritedRequirementRefs = []) {
|
|
|
130
211
|
).toLowerCase();
|
|
131
212
|
const entityType = requiredText(entity.type, 'type', anchorId);
|
|
132
213
|
let externalRefs = normalizeExternalRefs(entity.external_refs || entity.externalRefs || []);
|
|
133
|
-
if (entityType === 'Capability'
|
|
134
|
-
|
|
214
|
+
if (entityType === 'Capability') {
|
|
215
|
+
const requirementSources = [
|
|
216
|
+
...externalRefs.filter((ref) => ref.object_type === 'requirement'),
|
|
217
|
+
...inheritedRequirementRefs,
|
|
218
|
+
];
|
|
219
|
+
externalRefs = normalizeExternalRefs([
|
|
220
|
+
...externalRefs,
|
|
221
|
+
...expandRequirementRefsWithFeatures(requirementSources),
|
|
222
|
+
]);
|
|
135
223
|
}
|
|
136
224
|
return {
|
|
137
225
|
anchor_id: anchorId,
|
|
@@ -237,6 +325,7 @@ function buildCanonicalFacts(archiveDir, snapshot, projectId, archiveId) {
|
|
|
237
325
|
if (entities.length === 0) {
|
|
238
326
|
throw new Error('archive-ontology.json 没有可导出的实体');
|
|
239
327
|
}
|
|
328
|
+
assertExternalKeyFinalGate(entities);
|
|
240
329
|
const entityByAnchor = new Map(entities.map((entity) => [entity.anchor_id, entity]));
|
|
241
330
|
const warningCounts = new Map();
|
|
242
331
|
const seenRelations = new Set();
|
|
@@ -201,15 +201,28 @@ function identityFromValues(values = {}) {
|
|
|
201
201
|
|
|
202
202
|
function flushRequirementRef(requirementRefs, currentRef) {
|
|
203
203
|
if (!currentRef) return null;
|
|
204
|
+
const { normalize, validate } = require('./external-key.cjs');
|
|
204
205
|
const system = currentRef.system;
|
|
205
206
|
const objectType = currentRef['object-type'] || currentRef.object_type;
|
|
206
207
|
const externalId = currentRef['external-id'] || currentRef.external_id;
|
|
208
|
+
const featureId = currentRef['feature-id'] || currentRef.feature_id;
|
|
207
209
|
if (system && objectType && externalId) {
|
|
208
|
-
|
|
210
|
+
const type = String(objectType).toLowerCase();
|
|
211
|
+
const entry = {
|
|
209
212
|
system,
|
|
210
|
-
object_type:
|
|
211
|
-
external_id: externalId,
|
|
212
|
-
}
|
|
213
|
+
object_type: type,
|
|
214
|
+
external_id: normalize(externalId, type) || String(externalId).trim(),
|
|
215
|
+
};
|
|
216
|
+
if (featureId) {
|
|
217
|
+
const featureCheck = validate(featureId, 'feature');
|
|
218
|
+
if (!featureCheck.ok) {
|
|
219
|
+
entry.feature_id_invalid = featureCheck.message;
|
|
220
|
+
entry.feature_id_raw = featureId;
|
|
221
|
+
} else {
|
|
222
|
+
entry.feature_id = normalize(featureId, 'feature');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
requirementRefs.push(entry);
|
|
213
226
|
}
|
|
214
227
|
return null;
|
|
215
228
|
}
|
|
@@ -218,8 +231,10 @@ function parseStructuredFrontmatter(lines) {
|
|
|
218
231
|
const flat = {};
|
|
219
232
|
const requirementRefs = [];
|
|
220
233
|
const continuity = {};
|
|
234
|
+
const numberingWaiver = {};
|
|
235
|
+
let numberingGate = false;
|
|
221
236
|
if (!lines.length || lines[0].trim() !== '---') {
|
|
222
|
-
return { flat, requirementRefs, continuity };
|
|
237
|
+
return { flat, requirementRefs, continuity, numberingWaiver, numberingGate };
|
|
223
238
|
}
|
|
224
239
|
let section = null;
|
|
225
240
|
let currentRef = null;
|
|
@@ -238,6 +253,13 @@ function parseStructuredFrontmatter(lines) {
|
|
|
238
253
|
flat[section] = value;
|
|
239
254
|
currentCap = null;
|
|
240
255
|
if (section === 'continuity' && value) continuity.kind = value;
|
|
256
|
+
if (section === 'numbering-waiver') {
|
|
257
|
+
numberingGate = true;
|
|
258
|
+
if (value) numberingWaiver.reason = value;
|
|
259
|
+
}
|
|
260
|
+
if (section === 'requirement-refs') {
|
|
261
|
+
numberingGate = true;
|
|
262
|
+
}
|
|
241
263
|
continue;
|
|
242
264
|
}
|
|
243
265
|
|
|
@@ -253,6 +275,12 @@ function parseStructuredFrontmatter(lines) {
|
|
|
253
275
|
continue;
|
|
254
276
|
}
|
|
255
277
|
|
|
278
|
+
if (section === 'numbering-waiver' && nested) {
|
|
279
|
+
const key = nested[1].toLowerCase().replace(/-/g, '_');
|
|
280
|
+
numberingWaiver[key] = unwrapScalar(stripInlineYamlComment(nested[2]));
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
256
284
|
if (section === 'continuity' && nested) {
|
|
257
285
|
const key = nested[1].toLowerCase().replace(/-/g, '_');
|
|
258
286
|
const value = unwrapScalar(stripInlineYamlComment(nested[2]));
|
|
@@ -280,7 +308,7 @@ function parseStructuredFrontmatter(lines) {
|
|
|
280
308
|
}
|
|
281
309
|
}
|
|
282
310
|
flushRequirementRef(requirementRefs, currentRef);
|
|
283
|
-
return { flat, requirementRefs, continuity };
|
|
311
|
+
return { flat, requirementRefs, continuity, numberingWaiver, numberingGate };
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
function parseIdentityBlock(lines, startIndex, endIndex = lines.length) {
|
|
@@ -324,6 +352,8 @@ function parseProposal(target, file, lines, frontmatter) {
|
|
|
324
352
|
target.proposalMode = String(mergedFrontmatter.mode || '').trim().toLowerCase();
|
|
325
353
|
target.requirementRefs = structured.requirementRefs;
|
|
326
354
|
target.continuity = structured.continuity;
|
|
355
|
+
target.numberingWaiver = structured.numberingWaiver || {};
|
|
356
|
+
target.numberingGate = Boolean(structured.numberingGate);
|
|
327
357
|
const changeId = String(mergedFrontmatter['change-id'] || '').trim().toUpperCase();
|
|
328
358
|
if (changeId) {
|
|
329
359
|
addEntity(
|
|
@@ -355,9 +385,20 @@ function parseProposal(target, file, lines, frontmatter) {
|
|
|
355
385
|
const capabilityId = match[1].toUpperCase();
|
|
356
386
|
const identity = parseIdentityBlock(lines, index + 1);
|
|
357
387
|
if (structured.requirementRefs.length > 0) {
|
|
388
|
+
const expanded = [];
|
|
389
|
+
for (const ref of structured.requirementRefs) {
|
|
390
|
+
expanded.push(ref);
|
|
391
|
+
if (ref.feature_id) {
|
|
392
|
+
expanded.push({
|
|
393
|
+
system: ref.system,
|
|
394
|
+
object_type: 'feature',
|
|
395
|
+
external_id: ref.feature_id,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
358
399
|
identity.external_refs = [
|
|
359
400
|
...(identity.external_refs || []),
|
|
360
|
-
...
|
|
401
|
+
...expanded,
|
|
361
402
|
];
|
|
362
403
|
}
|
|
363
404
|
const slug = unwrapScalar(match[2]);
|
|
@@ -683,6 +724,8 @@ function parseChangeArtifacts(projectRoot, changeName, options = {}) {
|
|
|
683
724
|
profile: 'simple',
|
|
684
725
|
proposalMode: '',
|
|
685
726
|
requirementRefs: [],
|
|
727
|
+
numberingWaiver: {},
|
|
728
|
+
numberingGate: false,
|
|
686
729
|
continuity: {},
|
|
687
730
|
artifacts: [],
|
|
688
731
|
files: files.map((file) => ({ path: file.relativePath, content_hash: file.contentHash })),
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
} = require('./runtime.cjs');
|
|
11
11
|
const { observeChangeArtifacts } = require('./artifact-observer.cjs');
|
|
12
12
|
const { allocateIdentity } = require('./id.cjs');
|
|
13
|
+
const externalKey = require('./external-key.cjs');
|
|
13
14
|
|
|
14
15
|
function parseArgs(argv) {
|
|
15
16
|
const result = { _: [] };
|
|
@@ -53,6 +54,7 @@ function showHelp() {
|
|
|
53
54
|
node skywalk-sdd/ontology/cli.cjs identity --delta-state=added
|
|
54
55
|
node skywalk-sdd/ontology/cli.cjs identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
|
|
55
56
|
node skywalk-sdd/ontology/cli.cjs identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
|
|
57
|
+
node skywalk-sdd/ontology/cli.cjs external-key --validate <id> --type <requirement|feature|scenario>
|
|
56
58
|
node skywalk-sdd/ontology/cli.cjs reconcile --project=. --change=<name> [--profile=...]
|
|
57
59
|
node skywalk-sdd/ontology/cli.cjs check --project=. --change=<name> [--profile=...]
|
|
58
60
|
node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
|
|
@@ -73,6 +75,23 @@ function main(argv = process.argv.slice(2)) {
|
|
|
73
75
|
console.log(JSON.stringify(allocateIdentity(args), null, 2));
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
78
|
+
if (command === 'external-key') {
|
|
79
|
+
const id = args.validate || args._[1];
|
|
80
|
+
const type = args.type || args.t;
|
|
81
|
+
if (!id || !type) {
|
|
82
|
+
throw new Error('用法: external-key --validate <id> --type <requirement|feature|scenario>');
|
|
83
|
+
}
|
|
84
|
+
const result = externalKey.validate(id, type);
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
ok: result.ok,
|
|
87
|
+
code: result.code,
|
|
88
|
+
message: result.message,
|
|
89
|
+
normalized: externalKey.normalize(id, type),
|
|
90
|
+
objectType: String(type).toLowerCase(),
|
|
91
|
+
}, null, 2));
|
|
92
|
+
if (!result.ok) process.exitCode = 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
76
95
|
const projectRoot = path.resolve(args.project || '.');
|
|
77
96
|
const changeName = args.change;
|
|
78
97
|
if (!changeName) throw new Error('缺少 --change 参数');
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* External key grammar for requirement / feature / scenario.
|
|
5
|
+
* Authority: kb-sdd docs/superpowers/specs/2026-07-24-external-numbering-kb-sdd-design.md §2.
|
|
6
|
+
* Regex and normalization MUST stay byte-for-byte aligned with kb-sdd ExternalKeyFormat.java.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const REGEX = {
|
|
10
|
+
requirement: /^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}$/,
|
|
11
|
+
feature: /^FEAT-[A-Z][A-Z0-9]{1,7}-[0-9]{3,6}$/,
|
|
12
|
+
scenario: /^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}:SCN-[a-z0-9]+(-[a-z0-9]+)*-[0-9]{3}$/,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const SCENARIO_PARTS =
|
|
16
|
+
/^(REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}):SCN-([a-z0-9]+(?:-[a-z0-9]+)*)-([0-9]{3})$/;
|
|
17
|
+
|
|
18
|
+
const SLUG_MAX_LEN = 40;
|
|
19
|
+
const SCENARIO_KEY_MAX_LEN = 120;
|
|
20
|
+
const SCN_SEQ_MAX = 999;
|
|
21
|
+
|
|
22
|
+
const CODES = {
|
|
23
|
+
FORMAT_INVALID: 'EXTERNAL_KEY_FORMAT_INVALID',
|
|
24
|
+
SCOPE_MISMATCH: 'EXTERNAL_KEY_SCOPE_MISMATCH',
|
|
25
|
+
FEATURE_UNDECLARED: 'EXTERNAL_KEY_FEATURE_UNDECLARED',
|
|
26
|
+
SEQ_REUSED: 'EXTERNAL_KEY_SEQ_REUSED',
|
|
27
|
+
WAIVER_ACTIVE: 'NUMBERING_WAIVER_ACTIVE',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function normalizeObjectType(objectType) {
|
|
31
|
+
if (objectType == null || String(objectType).trim() === '') return null;
|
|
32
|
+
return String(objectType).trim().toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalize(id, objectType) {
|
|
36
|
+
if (id == null) return null;
|
|
37
|
+
const trimmed = String(id).trim();
|
|
38
|
+
if (!trimmed) return trimmed;
|
|
39
|
+
const type = normalizeObjectType(objectType);
|
|
40
|
+
if (type === 'scenario') {
|
|
41
|
+
const colon = trimmed.indexOf(':');
|
|
42
|
+
if (colon < 0) return trimmed.toUpperCase();
|
|
43
|
+
const reqPart = trimmed.slice(0, colon).trim().toUpperCase();
|
|
44
|
+
const rest = trimmed.slice(colon + 1).trim();
|
|
45
|
+
if (/^scn-/i.test(rest)) {
|
|
46
|
+
const afterScn = rest.slice(4);
|
|
47
|
+
const lastDash = afterScn.lastIndexOf('-');
|
|
48
|
+
if (lastDash > 0) {
|
|
49
|
+
const slug = afterScn.slice(0, lastDash).toLowerCase();
|
|
50
|
+
const seq = afterScn.slice(lastDash + 1);
|
|
51
|
+
return `${reqPart}:SCN-${slug}-${seq}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `${reqPart}:${rest.toLowerCase()}`;
|
|
55
|
+
}
|
|
56
|
+
return trimmed.toUpperCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validate(id, objectType) {
|
|
60
|
+
const type = normalizeObjectType(objectType);
|
|
61
|
+
if (!type || !REGEX[type]) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
code: CODES.FORMAT_INVALID,
|
|
65
|
+
message: `未知 object_type,无法校验编号: ${objectType}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const normalized = normalize(id, type);
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
return { ok: false, code: CODES.FORMAT_INVALID, message: 'external_id 为空' };
|
|
71
|
+
}
|
|
72
|
+
if (!REGEX[type].test(normalized)) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: CODES.FORMAT_INVALID,
|
|
76
|
+
message: `编号格式不合规 (${type}): ${id} → ${normalized}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (type === 'scenario') {
|
|
80
|
+
const parts = parseScenario(normalized);
|
|
81
|
+
if (!parts) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
code: CODES.FORMAT_INVALID,
|
|
85
|
+
message: `场景键无法解析: ${normalized}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (parts.slug.length > SLUG_MAX_LEN) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
code: CODES.FORMAT_INVALID,
|
|
92
|
+
message: `场景 SLUG 超过 ${SLUG_MAX_LEN} 字符: ${parts.slug}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (normalized.length > SCENARIO_KEY_MAX_LEN) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
code: CODES.FORMAT_INVALID,
|
|
99
|
+
message: `场景键总长超过 ${SCENARIO_KEY_MAX_LEN} 字符`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, code: null, message: null, normalized };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseScenario(key) {
|
|
107
|
+
const normalized = normalize(key, 'scenario');
|
|
108
|
+
if (!normalized) return null;
|
|
109
|
+
const match = SCENARIO_PARTS.exec(normalized);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
return {
|
|
112
|
+
requirementId: match[1],
|
|
113
|
+
slug: match[2],
|
|
114
|
+
seq: Number(match[3]),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function composeScenario(reqId, slug, seq) {
|
|
119
|
+
const req = normalize(reqId, 'requirement');
|
|
120
|
+
const normalizedSlug = String(slug || '').trim().toLowerCase();
|
|
121
|
+
const n = Number(seq);
|
|
122
|
+
if (!Number.isInteger(n) || n < 1 || n > SCN_SEQ_MAX) {
|
|
123
|
+
throw new Error(`SCN 序号必须在 1..${SCN_SEQ_MAX} 之间: ${seq}`);
|
|
124
|
+
}
|
|
125
|
+
const composed = `${req}:SCN-${normalizedSlug}-${String(n).padStart(3, '0')}`;
|
|
126
|
+
const result = validate(composed, 'scenario');
|
|
127
|
+
if (!result.ok) throw new Error(result.message);
|
|
128
|
+
return composed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function nextScenarioSeq(reqId, usedKeys) {
|
|
132
|
+
const req = normalize(reqId, 'requirement');
|
|
133
|
+
let max = 0;
|
|
134
|
+
for (const key of usedKeys || []) {
|
|
135
|
+
const parts = parseScenario(key);
|
|
136
|
+
if (!parts) continue;
|
|
137
|
+
if (parts.requirementId === req) {
|
|
138
|
+
max = Math.max(max, parts.seq);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (max >= SCN_SEQ_MAX) {
|
|
142
|
+
throw new Error(`SCN 序号已用尽 (999),请回需求管理系统拆分需求: ${req}`);
|
|
143
|
+
}
|
|
144
|
+
return max + 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
REGEX,
|
|
149
|
+
CODES,
|
|
150
|
+
SLUG_MAX_LEN,
|
|
151
|
+
SCENARIO_KEY_MAX_LEN,
|
|
152
|
+
SCN_SEQ_MAX,
|
|
153
|
+
normalize,
|
|
154
|
+
validate,
|
|
155
|
+
parseScenario,
|
|
156
|
+
composeScenario,
|
|
157
|
+
nextScenarioSeq,
|
|
158
|
+
normalizeObjectType,
|
|
159
|
+
};
|
|
@@ -82,6 +82,10 @@ function normalizeFacts(parsed) {
|
|
|
82
82
|
profile: parsed.profile,
|
|
83
83
|
proposal_mode: parsed.proposalMode || undefined,
|
|
84
84
|
requirement_refs: Array.isArray(parsed.requirementRefs) ? parsed.requirementRefs : undefined,
|
|
85
|
+
numbering_waiver: parsed.numberingWaiver && Object.keys(parsed.numberingWaiver).length > 0
|
|
86
|
+
? parsed.numberingWaiver
|
|
87
|
+
: undefined,
|
|
88
|
+
numbering_gate: parsed.numberingGate ? true : undefined,
|
|
85
89
|
continuity: parsed.continuity && Object.keys(parsed.continuity).length > 0
|
|
86
90
|
? parsed.continuity
|
|
87
91
|
: undefined,
|
|
@@ -107,6 +107,11 @@ const DIAGNOSTIC_CODES = Object.freeze({
|
|
|
107
107
|
EXTERNAL_REF_INVALID: 'EXTERNAL_REF_INVALID',
|
|
108
108
|
EXTERNAL_REF_TYPE_MISMATCH: 'EXTERNAL_REF_TYPE_MISMATCH',
|
|
109
109
|
EXTERNAL_REF_CONFLICT: 'EXTERNAL_REF_CONFLICT',
|
|
110
|
+
EXTERNAL_KEY_FORMAT_INVALID: 'EXTERNAL_KEY_FORMAT_INVALID',
|
|
111
|
+
EXTERNAL_KEY_SCOPE_MISMATCH: 'EXTERNAL_KEY_SCOPE_MISMATCH',
|
|
112
|
+
EXTERNAL_KEY_FEATURE_UNDECLARED: 'EXTERNAL_KEY_FEATURE_UNDECLARED',
|
|
113
|
+
EXTERNAL_KEY_SEQ_REUSED: 'EXTERNAL_KEY_SEQ_REUSED',
|
|
114
|
+
NUMBERING_WAIVER_ACTIVE: 'NUMBERING_WAIVER_ACTIVE',
|
|
110
115
|
CONTINUITY_DECISION_REQUIRED: 'CONTINUITY_DECISION_REQUIRED',
|
|
111
116
|
CONTINUITY_IDENTITY_MISMATCH: 'CONTINUITY_IDENTITY_MISMATCH',
|
|
112
117
|
});
|
|
@@ -453,8 +453,94 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
|
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
const {
|
|
457
|
+
validate,
|
|
458
|
+
normalize,
|
|
459
|
+
parseScenario,
|
|
460
|
+
} = require('./external-key.cjs');
|
|
461
|
+
|
|
462
|
+
// 仅当 frontmatter 显式出现 requirement-refs / numbering-waiver 时启用入场门禁
|
|
463
|
+
// (新模板默认带 requirement-refs;旧夹具无该节则不强制)
|
|
464
|
+
const numberingDeclared = Boolean(facts.numbering_gate)
|
|
465
|
+
|| (facts.numbering_waiver && typeof facts.numbering_waiver === 'object');
|
|
466
|
+
const requirementRefs = Array.isArray(facts.requirement_refs) ? facts.requirement_refs : [];
|
|
467
|
+
const waiverReason = String(
|
|
468
|
+
(facts.numbering_waiver && (facts.numbering_waiver.reason || facts.numbering_waiver.Reason)) || '',
|
|
469
|
+
).trim();
|
|
470
|
+
if (numberingDeclared) {
|
|
471
|
+
if (requirementRefs.length === 0 && !waiverReason) {
|
|
472
|
+
diagnostics.push(diagnostic(
|
|
473
|
+
DIAGNOSTIC_CODES.CONTINUITY_DECISION_REQUIRED,
|
|
474
|
+
'error',
|
|
475
|
+
'缺少格式合规的 requirement-refs,且未提供 numbering-waiver.reason',
|
|
476
|
+
{
|
|
477
|
+
file: 'proposal.md',
|
|
478
|
+
suggestion: '申报 REQ 号,或填写 numbering-waiver.reason(探索性/纯内部重构)',
|
|
479
|
+
},
|
|
480
|
+
));
|
|
481
|
+
}
|
|
482
|
+
if (requirementRefs.length === 0 && waiverReason) {
|
|
483
|
+
diagnostics.push(diagnostic(
|
|
484
|
+
DIAGNOSTIC_CODES.NUMBERING_WAIVER_ACTIVE,
|
|
485
|
+
'warning',
|
|
486
|
+
`编号豁免生效:${waiverReason}(本轮不种桥)`,
|
|
487
|
+
{ file: 'proposal.md' },
|
|
488
|
+
));
|
|
489
|
+
}
|
|
490
|
+
if (requirementRefs.length > 0 && waiverReason) {
|
|
491
|
+
diagnostics.push(diagnostic(
|
|
492
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
|
|
493
|
+
'error',
|
|
494
|
+
'numbering-waiver 与非空 requirement-refs 互斥',
|
|
495
|
+
{ file: 'proposal.md', suggestion: '清空其一' },
|
|
496
|
+
));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const declaredReqIds = new Set();
|
|
501
|
+
const featureByReq = new Map();
|
|
502
|
+
for (const ref of requirementRefs) {
|
|
503
|
+
const objectType = String(ref.object_type || 'requirement').toLowerCase();
|
|
504
|
+
const format = validate(ref.external_id, objectType);
|
|
505
|
+
if (!format.ok) {
|
|
506
|
+
diagnostics.push(diagnostic(
|
|
507
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
|
|
508
|
+
'error',
|
|
509
|
+
format.message,
|
|
510
|
+
{ file: 'proposal.md', suggestion: '回需求管理系统核实/换发编号,禁止改写' },
|
|
511
|
+
));
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const normalizedReq = normalize(ref.external_id, objectType);
|
|
515
|
+
if (objectType === 'requirement') {
|
|
516
|
+
declaredReqIds.add(normalizedReq);
|
|
517
|
+
if (ref.feature_id_invalid) {
|
|
518
|
+
diagnostics.push(diagnostic(
|
|
519
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
|
|
520
|
+
'error',
|
|
521
|
+
`feature-id 不合规: ${ref.feature_id_invalid}`,
|
|
522
|
+
{ file: 'proposal.md', suggestion: '回需求管理系统核实 FEAT 号,禁止静默丢弃' },
|
|
523
|
+
));
|
|
524
|
+
} else if (ref.feature_id) {
|
|
525
|
+
if (featureByReq.has(normalizedReq) && featureByReq.get(normalizedReq) !== ref.feature_id) {
|
|
526
|
+
diagnostics.push(diagnostic(
|
|
527
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FEATURE_UNDECLARED,
|
|
528
|
+
'error',
|
|
529
|
+
`同一 Change 内同一 REQ 申报了多个 feature-id: ${normalizedReq}`,
|
|
530
|
+
{ file: 'proposal.md' },
|
|
531
|
+
));
|
|
532
|
+
}
|
|
533
|
+
featureByReq.set(normalizedReq, normalize(ref.feature_id, 'feature'));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
456
538
|
const bindingKeys = new Map();
|
|
539
|
+
const scenarioKeysInChange = new Set();
|
|
540
|
+
const removedScenarioKeys = new Set();
|
|
457
541
|
for (const entity of facts.entities || []) {
|
|
542
|
+
const declaredFeatures = new Set();
|
|
543
|
+
const featureBindings = new Set();
|
|
458
544
|
for (const ref of entity.external_refs || []) {
|
|
459
545
|
const objectType = String(ref.object_type || '').toLowerCase();
|
|
460
546
|
const allowedTypes = allowed[objectType];
|
|
@@ -475,6 +561,59 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
|
|
|
475
561
|
{ ...sourceContext(entity), entity_id: entity.id },
|
|
476
562
|
));
|
|
477
563
|
}
|
|
564
|
+
const format = validate(ref.external_id, objectType);
|
|
565
|
+
if (!format.ok) {
|
|
566
|
+
diagnostics.push(diagnostic(
|
|
567
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
|
|
568
|
+
'error',
|
|
569
|
+
`${format.message} (${entity.id})`,
|
|
570
|
+
{
|
|
571
|
+
...sourceContext(entity),
|
|
572
|
+
entity_id: entity.id,
|
|
573
|
+
suggestion: '编号不合规回需求系统换发;场景键回 opsx-spec 修正',
|
|
574
|
+
},
|
|
575
|
+
));
|
|
576
|
+
}
|
|
577
|
+
if (ref.feature_id) {
|
|
578
|
+
const featureFormat = validate(ref.feature_id, 'feature');
|
|
579
|
+
if (!featureFormat.ok) {
|
|
580
|
+
diagnostics.push(diagnostic(
|
|
581
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
|
|
582
|
+
'error',
|
|
583
|
+
`requirement.feature_id 不合规: ${featureFormat.message} (${entity.id})`,
|
|
584
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
585
|
+
));
|
|
586
|
+
} else {
|
|
587
|
+
declaredFeatures.add(normalize(ref.feature_id, 'feature'));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (objectType === 'feature') {
|
|
591
|
+
featureBindings.add(normalize(ref.external_id, 'feature'));
|
|
592
|
+
}
|
|
593
|
+
if (objectType === 'scenario') {
|
|
594
|
+
const normalizedScenario = normalize(ref.external_id, 'scenario');
|
|
595
|
+
const parts = parseScenario(normalizedScenario);
|
|
596
|
+
if (parts && declaredReqIds.size > 0 && !declaredReqIds.has(parts.requirementId)) {
|
|
597
|
+
diagnostics.push(diagnostic(
|
|
598
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_SCOPE_MISMATCH,
|
|
599
|
+
'error',
|
|
600
|
+
`场景键 REQ 前缀不在 frontmatter requirement-refs 申报集合: ${normalizedScenario}`,
|
|
601
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
602
|
+
));
|
|
603
|
+
}
|
|
604
|
+
if (scenarioKeysInChange.has(normalizedScenario)) {
|
|
605
|
+
diagnostics.push(diagnostic(
|
|
606
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_SEQ_REUSED,
|
|
607
|
+
'error',
|
|
608
|
+
`本 Change 内 SCN 键重复: ${normalizedScenario}`,
|
|
609
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
610
|
+
));
|
|
611
|
+
}
|
|
612
|
+
scenarioKeysInChange.add(normalizedScenario);
|
|
613
|
+
if (String(entity.delta_state || '').toLowerCase() === 'removed') {
|
|
614
|
+
removedScenarioKeys.add(normalizedScenario);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
478
617
|
const key = `${ref.system}|${objectType}|${ref.external_id}|${entity.anchor_id || entity.id}`;
|
|
479
618
|
const prior = bindingKeys.get(key);
|
|
480
619
|
if (prior && prior !== entity.entity_id) {
|
|
@@ -492,6 +631,42 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
|
|
|
492
631
|
bindingKeys.set(key, entity.entity_id);
|
|
493
632
|
}
|
|
494
633
|
}
|
|
634
|
+
for (const featureId of featureBindings) {
|
|
635
|
+
if (!declaredFeatures.has(featureId)) {
|
|
636
|
+
diagnostics.push(diagnostic(
|
|
637
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FEATURE_UNDECLARED,
|
|
638
|
+
'error',
|
|
639
|
+
`feature 绑定无法与同实体 requirement.feature_id 配对: ${featureId} (${entity.id})`,
|
|
640
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
641
|
+
));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const featureId of declaredFeatures) {
|
|
645
|
+
if (!featureBindings.has(featureId)) {
|
|
646
|
+
diagnostics.push(diagnostic(
|
|
647
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_FEATURE_UNDECLARED,
|
|
648
|
+
'error',
|
|
649
|
+
`requirement 申报了 feature_id 但缺少对应 feature 绑定: ${featureId} (${entity.id})`,
|
|
650
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
651
|
+
));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const entity of facts.entities || []) {
|
|
657
|
+
if (String(entity.delta_state || '').toLowerCase() === 'removed') continue;
|
|
658
|
+
for (const ref of entity.external_refs || []) {
|
|
659
|
+
if (String(ref.object_type || '').toLowerCase() !== 'scenario') continue;
|
|
660
|
+
const key = normalize(ref.external_id, 'scenario');
|
|
661
|
+
if (removedScenarioKeys.has(key) && String(entity.delta_state || '').toLowerCase() !== 'removed') {
|
|
662
|
+
diagnostics.push(diagnostic(
|
|
663
|
+
DIAGNOSTIC_CODES.EXTERNAL_KEY_SEQ_REUSED,
|
|
664
|
+
'error',
|
|
665
|
+
`复用了 removed 墓碑 SCN 号: ${key}`,
|
|
666
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
667
|
+
));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
495
670
|
}
|
|
496
671
|
}
|
|
497
672
|
|
|
@@ -7,11 +7,14 @@ delta-state: "added"
|
|
|
7
7
|
predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
|
|
8
8
|
mode: "" # full=分 Capability 产物,simple=根目录精简产物
|
|
9
9
|
test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
|
|
10
|
-
#
|
|
10
|
+
# 外部需求键(默认必填 ≥1 个合规 REQ;探索性/纯内部重构可走 numbering-waiver)
|
|
11
11
|
requirement-refs:
|
|
12
12
|
- system: requirement-mgmt
|
|
13
13
|
object-type: requirement
|
|
14
|
-
external-id: REQ-<DOMAIN>-<
|
|
14
|
+
external-id: REQ-<DOMAIN>-<YEAR>-<SEQ> # 例 REQ-FI-2024-001
|
|
15
|
+
feature-id: FEAT-<DOMAIN>-<SEQ> # 可选,需求所属功能(需求系统权威)
|
|
16
|
+
# numbering-waiver:
|
|
17
|
+
# reason: "探索性原型,本轮不种桥"
|
|
15
18
|
# Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
|
|
16
19
|
continuity:
|
|
17
20
|
kind: new # iteration | similar-reference | new
|
|
@@ -34,7 +34,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
34
34
|
- **version-id**: <UUID>
|
|
35
35
|
- **delta-state**: added
|
|
36
36
|
- **predecessor-version**: 无
|
|
37
|
-
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<
|
|
37
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
|
|
38
38
|
- **当** <!-- 触发条件 -->
|
|
39
39
|
- **预期** <!-- 预期结果 -->
|
|
40
40
|
|
|
@@ -43,7 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
43
43
|
- **version-id**: <UUID>
|
|
44
44
|
- **delta-state**: added
|
|
45
45
|
- **predecessor-version**: 无
|
|
46
|
-
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<
|
|
46
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
|
|
47
47
|
- **当** <!-- 触发条件 -->
|
|
48
48
|
- **预期** <!-- 预期结果 -->
|
|
49
49
|
|
|
@@ -63,7 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
63
63
|
- **version-id**: <新 UUID>
|
|
64
64
|
- **delta-state**: <modified|added>
|
|
65
65
|
- **predecessor-version**: <modified 时填写;added 为无>
|
|
66
|
-
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<
|
|
66
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
|
|
67
67
|
- **当** <!-- 触发条件 -->
|
|
68
68
|
- **预期** <!-- 预期结果 -->
|
|
69
69
|
|
|
@@ -211,6 +211,7 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
|
|
|
211
211
|
> 合理化预防表见 opsx-tdd-core/SKILL.md §7
|
|
212
212
|
> REFACTOR 检查点见 opsx-tdd-rules/rules/refactor-checklist.md
|
|
213
213
|
> 异常路径覆盖门禁见 opsx-tdd-rules/rules/exception-path-coverage.md
|
|
214
|
+
> TDD 节奏校验(执行后校验)见 `./checklist.md` §5e.1
|
|
214
215
|
|
|
215
216
|
**【S2.1 RED 测试质量标准】**(test-strategy=tdd 时强制):
|
|
216
217
|
|
|
@@ -282,6 +283,7 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
|
|
|
282
283
|
- **⛔ 编译检查门禁**:每完成一个任务后必须运行编译检查,编译失败禁止标记已完成。
|
|
283
284
|
- **⛔ 测试执行门禁**:根据 `test-strategy` 决定(tdd=强制, impl-first=强制补跑, none=跳过);须真实执行并留 telemetry。${HOOK_GATE_DESCRIPTION}
|
|
284
285
|
- **⛔ 必须实时更新任务状态**:每完成一个任务立即改 tasks.md,两种格式同步。
|
|
286
|
+
- **⛔ apply 结束前 checkbox 全量同步校验**:`stage_end` 前对比 telemetry `task_update` 记录数与 tasks.md `[x]` 数量,不一致则补齐(见 `./checklist.md` §5f.1)。
|
|
285
287
|
- **⛔ task_update 后必须验证 checkbox 已更新**:执行 `check-task` 确认 tasks.md 对应行已变更;未更新则手动修改。
|
|
286
288
|
- **⛔ TDD RED→GREEN 严格串行**:不适用同层并行派发;RED-N 确认失败后必须执行中断声明再进入 GREEN-N;GREEN 完成后必须通过 Scope 门禁再进入下一个 RED。
|
|
287
289
|
- **Git 只读策略**:禁止为了度量自动初始化 Git、创建分支或提交 commit;非 Git 项目用 `vcs_mode=no-git` 继续执行。
|
|
@@ -298,7 +300,7 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
|
|
|
298
300
|
|
|
299
301
|
## 渐进披露
|
|
300
302
|
|
|
301
|
-
- Read `checklist.md` 仅在执行 apply 需要校验门禁/自检时 — 含 §1.2 Check 门禁检查点、§5d/§5e 编译/测试门禁自检、§6.0 单元测试真实执行自检、§6.1 worktree 收尾前置条件、Guardrails ⛔ 强制项勾选表。
|
|
303
|
+
- Read `checklist.md` 仅在执行 apply 需要校验门禁/自检时 — 含 §1.2 Check 门禁检查点、§5d/§5e 编译/测试门禁自检、§5e.1 TDD 节奏校验、§5f.1 checkbox 全量同步校验、§6.0 单元测试真实执行自检、§6.1 worktree 收尾前置条件、Guardrails ⛔ 强制项勾选表。
|
|
302
304
|
- Read `reference.md` 仅在需要参考详细模板时 — 含 📊 Telemetry 命令模板(start/end/task_update/ai_adoption_review/worktree_finish)、§1.5 worktree 全套策略(Step 0.1-3 + record-base + 多 cap 合并顺序)、§5c 子代理派发、§5.1 AI 产出快照、§6.0 单元测试、§6.1 worktree 收尾脚本。
|
|
303
305
|
- `implementer-prompt.md` 为子代理派发提示模板(§5c 派发时组合 tasks/design/overview 上下文使用)。
|
|
304
306
|
- `worktree-setup.md` 为 worktree 快速参考(§1.5 策略的精简版,与 reference.md §1.5 完整版并存:reference=完整策略,worktree-setup=快速参考)。
|
|
@@ -47,6 +47,11 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
|
|
|
47
47
|
> 不在此内联复制,以 opsx-tdd-core/checklist.md §A 为唯一真相源。
|
|
48
48
|
> 额外补充:REFACTOR 任务还需执行 `opsx-tdd-rules/rules/refactor-checklist.md`(7 项重构检查点)。
|
|
49
49
|
|
|
50
|
+
⛔ **TDD 节奏校验**(RED→GREEN 严格串行的执行后校验):
|
|
51
|
+
- [ ] 连续的 RED-N `task_update` 与 GREEN-N `task_update` 之间有可验证的执行间隔(建议 >60 秒),若时间戳差距过小视为批量执行信号
|
|
52
|
+
- [ ] 每对 RED→GREEN 之间已执行 🔴 中断声明(`🔴 RED-N 确认失败,原因:XXX。现在进入 GREEN-N`)
|
|
53
|
+
- [ ] 若同层有多个 RED→GREEN 对,确认是逐对完成而非一次性编写多个 RED 再一次性实现多个 GREEN
|
|
54
|
+
|
|
50
55
|
### §5e.2 RED 测试质量门禁(仅 test-strategy=tdd 时,RED 任务完成后强制检查)
|
|
51
56
|
|
|
52
57
|
⛔ 核心原则(引用 opsx-tdd-quality/SKILL.md §2):Mock 边界,不 Mock 行为
|
|
@@ -79,6 +84,15 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
|
|
|
79
84
|
- [ ] 记录任务级 Telemetry(`task_update`,`--task-id=<TASK-ID>` 必填,否则 E4 指标无法计算);TDD 测试骨架任务须在 `--details-json` 带 `"task_kind":"test-skeleton"`(P3,避免红灯误判拉低 E4)
|
|
80
85
|
- [ ] ⛔ **task_update 后必须验证 checkbox 已更新**:执行 `node skywalk-sdd/index.cjs check-task --project=. --change=<变更名称> --task-id=<TASK-ID>` 确认 tasks.md 中对应行已从 `- [ ]` 变为 `- [x]`;若未更新,手动修改 tasks.md 并报告
|
|
81
86
|
|
|
87
|
+
### §5f.1 apply 结束前 checkbox 全量同步校验
|
|
88
|
+
|
|
89
|
+
> ⛔ 在 `stage_end` telemetry 记录前必须执行此校验,防止任务状态滞后到 archive 阶段。
|
|
90
|
+
|
|
91
|
+
- [ ] 对比 telemetry `task_update` 记录的已完成任务数与 tasks.md 中 `[x]` 数量,不一致则补齐
|
|
92
|
+
- [ ] tasks.md 中所有 `- [ ]` / `- [x]` 与 `**状态**: [ ]` / `[x]` 两种格式已同步
|
|
93
|
+
- [ ] 手动验证清单(如有)已勾选
|
|
94
|
+
- [ ] 文档更新项(如有)已完成或显式标注推迟
|
|
95
|
+
|
|
82
96
|
---
|
|
83
97
|
|
|
84
98
|
## §6.0 单元测试真实执行自检(`test-strategy` 非 `none`)
|
|
@@ -114,8 +128,10 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
|
|
|
114
128
|
- [ ] ⛔ **测试执行门禁**:根据 `test-strategy` 决定(tdd=强制, impl-first=强制补跑, none=跳过);须真实执行并留 telemetry,`sdd-apply-test-gate` 校验非占位数据
|
|
115
129
|
- [ ] ⛔ **RED 测试质量门禁**:见 §5e.2(引用 opsx-tdd-quality + opsx-tdd-anti-patterns,不在此内联复制)
|
|
116
130
|
- [ ] ⛔ **必须实时更新任务状态**:每完成一个任务立即改 tasks.md,两种格式(`- [ ]`→`- [x]` 与 `**状态**: [ ]`→`[x]`)同步
|
|
131
|
+
- [ ] ⛔ **apply 结束前 checkbox 全量同步校验**:见 §5f.1,`stage_end` 前对比 telemetry `task_update` 记录数与 tasks.md `[x]` 数量
|
|
117
132
|
- [ ] ⛔ **task_update 后必须验证 checkbox 已更新**:执行 `check-task` 确认 tasks.md 对应行已变更;未更新则手动修改
|
|
118
133
|
- [ ] ⛔ **TDD RED→GREEN 严格串行**:不适用同层并行派发;RED-N 确认失败后必须执行中断声明再进入 GREEN-N;GREEN 完成后必须通过 Scope 门禁再进入下一个 RED
|
|
134
|
+
- [ ] ⛔ **TDD 节奏校验**:见 §5e.1,连续 RED-N/GREEN-N 的 `task_update` 时间戳须有可验证间距
|
|
119
135
|
- [ ] **Git 只读策略**:禁止为了度量自动初始化 Git、创建分支或提交 commit;非 Git 项目用 `vcs_mode=no-git` 继续执行
|
|
120
136
|
- [ ] ⛔ **Step 0.1 隔离校验必做**:建 worktree / 建议分支名前必须完成 proposal + 跨 cap spec 依赖校验并输出报告;未通过不得按 full 并行策略拆 `kld-sdd/<change>/<cap>`
|
|
121
137
|
- [ ] **Worktree 为加速手段,非必选项**:校验通过且解耦方可多 worktree;有依赖或共享修改面则串行
|
|
@@ -97,6 +97,10 @@ openspec list
|
|
|
97
97
|
- [ ] spec.md 的需求项在 design.md 中 100% 被覆盖
|
|
98
98
|
- [ ] design.md 的设计点在 tasks.md 中 100% 被拆解
|
|
99
99
|
- [ ] 跨文档引用路径正确
|
|
100
|
+
- [ ] ⛔ **CON 覆盖一致性**:spec.md 中每个 CON 在 tasks.md 中有对应验证任务或显式声明间接覆盖;tasks.md 声明"100% 覆盖 CON"时必须可追溯
|
|
101
|
+
- [ ] ⛔ **安全/审计要求覆盖一致性**:spec.md §5.x 中的安全与审计要求在 tasks.md 中有对应任务或显式声明推迟
|
|
102
|
+
- [ ] ⛔ **AC 变体覆盖一致性**:spec.md 中含"或"条件的 AC 场景,其 RED 任务验收标准须列出所有变体的测试方法(规则见 `opsx-tdd-rules/rules/multi-validation-split.md` §AC 内"或"条件变体覆盖)
|
|
103
|
+
- [ ] ⛔ **tasks.md §4.x 验证方式表内部一致性**:§4.x 验证方式表中的测试注解/配置与任务实现步骤中的声明一致
|
|
100
104
|
|
|
101
105
|
#### 4.3 算法正确性检查
|
|
102
106
|
|
|
@@ -216,7 +220,7 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
|
|
|
216
220
|
|
|
217
221
|
---
|
|
218
222
|
|
|
219
|
-
## Continuity / external_ref
|
|
223
|
+
## Continuity / external_ref / 编号确定性门禁
|
|
220
224
|
|
|
221
225
|
`opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
|
|
222
226
|
|
|
@@ -224,6 +228,13 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
|
|
|
224
228
|
- 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
|
|
225
229
|
- 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
|
|
226
230
|
- 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
|
|
231
|
+
- **编号诊断码**(并入五维报告与 apply gate):
|
|
232
|
+
- `EXTERNAL_KEY_FORMAT_INVALID` — external_id / feature_id 不合规
|
|
233
|
+
- `EXTERNAL_KEY_SCOPE_MISMATCH` — 场景键 REQ 前缀不在 `requirement-refs`
|
|
234
|
+
- `EXTERNAL_KEY_FEATURE_UNDECLARED` — feature↔requirement.`feature_id` 配对断裂
|
|
235
|
+
- `EXTERNAL_KEY_SEQ_REUSED` — 本 Change 内 SCN 重复或复用 removed 墓碑号
|
|
236
|
+
- `NUMBERING_WAIVER_ACTIVE`(warning)— 豁免生效,本轮不种桥
|
|
237
|
+
- `requirement-refs` 为空且无有效 waiver → `CONTINUITY_DECISION_REQUIRED` 级阻断
|
|
227
238
|
- CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
|
|
228
239
|
|
|
229
240
|
## 本体语义关系门禁
|
|
@@ -36,6 +36,13 @@ description: "opsx-check 阶段日志自检清单 — 仅在 check 自检时读
|
|
|
36
36
|
- [ ] 完整性、一致性、算法正确性、可执行性、TDD合规性(仅test-strategy=tdd时)五维均已输出
|
|
37
37
|
- [ ] 报告问题对应修复建议(spec/design/task)
|
|
38
38
|
|
|
39
|
+
## D2. 一致性补充检查(SKILL.md §4.2 扩展)
|
|
40
|
+
|
|
41
|
+
- [ ] **CON 覆盖**:spec.md 中每个 CON 在 tasks.md 中有对应验证任务或显式声明间接覆盖
|
|
42
|
+
- [ ] **安全/审计要求覆盖**:spec.md §5.x 中的安全与审计要求在 tasks.md 中有对应任务或显式声明推迟
|
|
43
|
+
- [ ] **AC 变体覆盖**:spec.md 中含"或"条件的 AC 场景,其 RED 任务验收标准列出所有变体的测试方法(引用 `opsx-tdd-rules/rules/multi-validation-split.md`)
|
|
44
|
+
- [ ] **§4.x 验证方式表内部一致性**:tasks.md §4.x 验证方式表中的测试注解/配置与任务实现步骤中的声明一致
|
|
45
|
+
|
|
39
46
|
## E. TDD 合规性检查(仅 test-strategy=tdd 时)
|
|
40
47
|
|
|
41
48
|
⛔ 执行 `opsx-tdd-core/checklist.md` §B(11 项)逐项检查。
|
|
@@ -5,24 +5,10 @@ description: >-
|
|
|
5
5
|
(archive:ingest scope). Prompts for API key and multi-selects spaces/KBs into
|
|
6
6
|
local skill state. Supports upload, job status query, job list, and retry.
|
|
7
7
|
Use when ingesting new or updated knowledge archives into the ontology KB.
|
|
8
|
-
argument-hint: "[path-to-archive.zip]"
|
|
9
|
-
license: MIT
|
|
10
|
-
compatibility: Requires Engineering KB API (API Key with archive:ingest).
|
|
11
|
-
metadata:
|
|
12
|
-
author: sdd-team
|
|
13
|
-
version: "1.0"
|
|
14
|
-
source: "kb-sdd/skills/opsx-kb-ingest"
|
|
15
|
-
allowed-tools:
|
|
16
|
-
- Bash
|
|
17
|
-
- Read
|
|
18
|
-
- Write
|
|
19
|
-
- Edit
|
|
20
8
|
---
|
|
21
9
|
|
|
22
10
|
# 本体知识库 · 入库
|
|
23
11
|
|
|
24
|
-
> **部署说明**:本技能随 `kld-sdd-init` 安装到项目 skills 目录。权威源在工程知识库仓 `skills/opsx-kb-ingest`;`opsx-archive` **硬依赖**本技能完成收尾入库。
|
|
25
|
-
|
|
26
12
|
只负责**入库**(zip 上传)。鉴权只用 **API Key**(`Authorization: Bearer sk_sdd_…`),**禁止**走手机号登录。
|
|
27
13
|
|
|
28
14
|
> 控制台知识库页另有浮动 Ontology Agent(会话登录 + SSE);本 Skill 仍走 API Key,二者分开。
|
|
@@ -127,6 +113,9 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JO
|
|
|
127
113
|
- object_type 与实体类型匹配:`requirement`/`feature`→Capability;`scenario`→SpecificationStatement|AcceptanceCriterion
|
|
128
114
|
- 包内无重复 `(external key, entity_id)` 绑定对
|
|
129
115
|
- `project_id` / `spaceKey` 既有规则保留
|
|
116
|
+
- **编号格式**:三条正则(见 reference)+ 场景 SLUG≤40 / 键总长≤120;先归一化再匹配
|
|
117
|
+
- **场景作用域**:scenario 键的 REQ 前缀必须出现在同包 requirement 绑定集合
|
|
118
|
+
- **功能配对**:同实体每条 feature 绑定必须能与某 requirement 条目的 `feature_id` 精确配对;反向同理
|
|
130
119
|
|
|
131
120
|
详细字段说明 → [reference.md](reference.md)。
|
|
132
121
|
|
|
@@ -160,6 +149,16 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JO
|
|
|
160
149
|
- **改为新锚点**:回到 kld-sdd,分配新锚点与新 entity_id,重新 check → 归档 → 入库
|
|
161
150
|
3. **禁止**在 KB 内现场改绑或解绑。
|
|
162
151
|
|
|
152
|
+
若编号门禁失败:
|
|
153
|
+
|
|
154
|
+
| errorCode | 指引 |
|
|
155
|
+
|-----------|------|
|
|
156
|
+
| `EXTERNAL_REFERENCE_FORMAT_INVALID` | 编号不合规 → **回需求管理系统换发**;不得手改编号硬闯 |
|
|
157
|
+
| `EXTERNAL_REFERENCE_SCOPE_MISMATCH` | 场景键 REQ 前缀不在申报集合 → 回 kld-sdd spec 修正 external-ref |
|
|
158
|
+
| `EXTERNAL_REFERENCE_FEATURE_UNBOUND` | feature↔requirement.`feature_id` 配对断裂 → 回 propose/archive 修正线缆字段 |
|
|
159
|
+
|
|
160
|
+
**禁止**手改包内编号绕过门禁。
|
|
161
|
+
|
|
163
162
|
**硬规则**
|
|
164
163
|
|
|
165
164
|
- 无 `apiKey` 不得猜密钥、不得改走 login。
|
|
@@ -141,8 +141,21 @@ POST {base}/ingestions/{jobId}/retry
|
|
|
141
141
|
| `EXTERNAL_REF_CONFLICT` | 同外部键+同锚点已绑不同 entity_id;整包回滚;见 `report.details` |
|
|
142
142
|
| `EXTERNAL_REFERENCE_TYPE_MISMATCH` | 包内 object_type 与实体类型不匹配 |
|
|
143
143
|
| `EXTERNAL_REFERENCE_DUPLICATE_IN_PACKAGE` | 包内重复 `(external key, entity_id)` |
|
|
144
|
+
| `EXTERNAL_REFERENCE_FORMAT_INVALID` | 编号归一化后不合规(含 requirement.`feature_id`);回需求系统换发 |
|
|
145
|
+
| `EXTERNAL_REFERENCE_SCOPE_MISMATCH` | 场景键 REQ 前缀不在同包 requirement 集合;回 kld-sdd spec 修正 |
|
|
146
|
+
| `EXTERNAL_REFERENCE_FEATURE_UNBOUND` | feature 绑定与 requirement.`feature_id` 无法配对;回 propose/archive |
|
|
144
147
|
| `FACTS_SCHEMA_UNSUPPORTED` | canonical-facts schema 非 v1/v2 |
|
|
145
148
|
|
|
149
|
+
### 编号正则(与 KB 设计 §2 / ExternalKeyFormat 一致)
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
requirement: ^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}$
|
|
153
|
+
feature: ^FEAT-[A-Z][A-Z0-9]{1,7}-[0-9]{3,6}$
|
|
154
|
+
scenario: ^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}:SCN-[a-z0-9]+(-[a-z0-9]+)*-[0-9]{3}$
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
requirement 条目可选 `feature_id`(FEAT 文法);配对关系随线缆走,KB 不反查 frontmatter。
|
|
158
|
+
|
|
146
159
|
### `EXTERNAL_REF_CONFLICT` 的 report.details
|
|
147
160
|
|
|
148
161
|
```json
|
|
@@ -5,24 +5,10 @@ description: >-
|
|
|
5
5
|
entity/impact/ontology-view). Prompts for API key and multi-selects spaces/KBs into
|
|
6
6
|
local skill state. Use when looking up Spec/design facts, Spec reuse, impact, or
|
|
7
7
|
citation-backed answers from the ontology KB.
|
|
8
|
-
argument-hint: "[query or continuity intent]"
|
|
9
|
-
license: MIT
|
|
10
|
-
compatibility: Requires Engineering KB API (API Key with context:read).
|
|
11
|
-
metadata:
|
|
12
|
-
author: sdd-team
|
|
13
|
-
version: "1.0"
|
|
14
|
-
source: "kb-sdd/skills/opsx-ontology-query"
|
|
15
|
-
allowed-tools:
|
|
16
|
-
- Bash
|
|
17
|
-
- Read
|
|
18
|
-
- Write
|
|
19
|
-
- Edit
|
|
20
8
|
---
|
|
21
9
|
|
|
22
10
|
# 本体知识库 · 查询
|
|
23
11
|
|
|
24
|
-
> **部署说明**:本技能随 `kld-sdd-init` 安装到项目 skills 目录。权威源在工程知识库仓 `skills/opsx-ontology-query`;`opsx-propose` / `opsx-spec` 等流程技能**硬依赖**本技能,缺失时不得用本地 archive 兜底。
|
|
25
|
-
|
|
26
12
|
只负责**查**。鉴权只用 **API Key**(`Authorization: Bearer sk_sdd_…`),**禁止**走手机号登录。
|
|
27
13
|
|
|
28
14
|
> 控制台知识库页另有浮动 Ontology Agent(会话登录 + SSE);本 Skill 仍走 API Key,二者分开。
|
|
@@ -124,6 +110,31 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/entities/resol
|
|
|
124
110
|
|
|
125
111
|
命中时关注:`resolution=LINK_EXISTING` 且 `inheritanceAllowed=true`(可继承 n);`removedBindingCount`(已失效绑定 m);`matchType=HISTORICAL_ONLY` 表示仅有失效绑定,不可继承。
|
|
126
112
|
|
|
113
|
+
**按功能号圈能力(propose 前)**
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/entities/resolve" \
|
|
117
|
+
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
|
118
|
+
-d '{
|
|
119
|
+
"externalSystem":"requirement-mgmt",
|
|
120
|
+
"externalObjectType":"feature",
|
|
121
|
+
"externalId":"FEAT-FI-012",
|
|
122
|
+
"entityType":"Capability"
|
|
123
|
+
}'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
展示该功能下存活 / 失效能力清单,辅助勾选本次 CAP 范围;此查询只作范围参考,不改变 Continuity 判定优先级。
|
|
127
|
+
|
|
128
|
+
### 编号文法速查(权威在 KB 仓设计 §2)
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
requirement: ^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}$
|
|
132
|
+
feature: ^FEAT-[A-Z][A-Z0-9]{1,7}-[0-9]{3,6}$
|
|
133
|
+
scenario: ^REQ-…:SCN-[a-z0-9]+(-[a-z0-9]+)*-[0-9]{3}$
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
归一化:trim;REQ/FEAT 段大写;SCN slug 小写。编号五律摘要:REQ/FEAT 仅需求系统铸号;SCN 由 kld-sdd/opsx-spec 铸号;编号≠身份;归属权威在需求系统;历史绑定 append-only。
|
|
137
|
+
|
|
127
138
|
**match-requirement**
|
|
128
139
|
|
|
129
140
|
```bash
|
|
@@ -152,7 +163,7 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/context/match-
|
|
|
152
163
|
|
|
153
164
|
### 命中
|
|
154
165
|
1. **{displayName}**({entityType})@ {kbName}
|
|
155
|
-
- id / version / matchType / externalRefs
|
|
166
|
+
- id / version / matchType / externalRefs(归一化形态)…
|
|
156
167
|
- 若外部键命中:可继承 n / 已失效绑定 m
|
|
157
168
|
```
|
|
158
169
|
|
|
@@ -162,13 +162,21 @@ openspec instructions proposal --change "<name>" --json
|
|
|
162
162
|
|
|
163
163
|
> 完整性检查(问题描述/目标/模块/约束 4 项)与缺失补充机制见 `./checklist.md`「§6 需求完整性检查」。发现缺失时主动询问用户补充。
|
|
164
164
|
|
|
165
|
-
### 6.5
|
|
165
|
+
### 6.5 【编号入场 + Continuity】需求 / Capability 身份(只到 CAP,不做场景)
|
|
166
166
|
|
|
167
|
-
在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP
|
|
167
|
+
在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP 编号分配前)。**验号必须在 resolve 之前**:
|
|
168
168
|
|
|
169
|
-
1.
|
|
170
|
-
2.
|
|
171
|
-
|
|
169
|
+
1. 收集 REQ 号(含可选 `feature-id`);没有则问一次。
|
|
170
|
+
2. 逐个验号(确定性入口,禁止肉眼判正则):
|
|
171
|
+
```bash
|
|
172
|
+
node skywalk-sdd/ontology/cli.cjs external-key --validate "<REQ-...>" --type requirement
|
|
173
|
+
# 若有 feature-id:
|
|
174
|
+
node skywalk-sdd/ontology/cli.cjs external-key --validate "<FEAT-...>" --type feature
|
|
175
|
+
```
|
|
176
|
+
- REQ/FEAT 不合规 → **拒绝进入 resolve**,告知「编号不合规,请回需求管理系统核实/换发」;Agent 不得猜测、补位、改写。
|
|
177
|
+
- 用户明确说「没有外部需求号」→ 走 `numbering-waiver.reason`(必填理由);`requirement-refs` 必须为空;提示本轮不种桥。
|
|
178
|
+
3. **先加载依赖技能**:确认 `${AGENT_SKILL_DIR}/opsx-ontology-query/SKILL.md` 存在并 Read;按该技能完成 API Key / 空间与 KB 选择。未安装则停止本步。
|
|
179
|
+
4. 验号通过后才 resolve:
|
|
172
180
|
```bash
|
|
173
181
|
node skywalk-sdd/context-client.cjs --mode=resolve \
|
|
174
182
|
--external-system=requirement-mgmt \
|
|
@@ -178,11 +186,12 @@ node skywalk-sdd/context-client.cjs --mode=resolve \
|
|
|
178
186
|
--space-id="$ENGINEERING_KB_SPACE_ID" \
|
|
179
187
|
--kb-id="$ENGINEERING_KB_KB_ID"
|
|
180
188
|
```
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
5. 若申报了 feature-id:额外 `objectType=feature` resolve 一次,展示「该功能下已有能力 n 个(存活 m / 失效 k)」辅助勾选 CAP 范围;不改变 Continuity 判定优先级。
|
|
190
|
+
6. 按 KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP。
|
|
191
|
+
7. 写入 proposal frontmatter:`requirement-refs`(含 `feature-id`)/ `numbering-waiver` + `continuity`。**禁止**写本地 archive 文件夹名作为 `base-archive`。
|
|
192
|
+
8. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `continuity-resolution.json` 的 `capabilities[]`。
|
|
193
|
+
9. KB 不可用 → `degraded` 继续,**禁止**扫本地 `archive/` 抄 UUID。
|
|
194
|
+
10. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份;**不得**铸/改 REQ/FEAT 号。
|
|
186
195
|
|
|
187
196
|
### 7. 【交互引导】文档拆分模式选择
|
|
188
197
|
|
|
@@ -132,10 +132,16 @@ node skywalk-sdd/context-client.cjs \
|
|
|
132
132
|
- **禁止**从本地 `archive/` 抄 UUID 当跨迭代继承源;跨迭代只认 KB current。
|
|
133
133
|
- 若返回 `available=false` / `degraded=true`,记录降级并继续,不得扫本地 archive 兜底。
|
|
134
134
|
- `INHERIT`:unchanged 写继承引用;modified 复用 entity-id + predecessor。`REFERENCE`:只参考,新开身份。
|
|
135
|
-
- reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref
|
|
136
|
-
-
|
|
135
|
+
- reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref`(完整键 `REQ-…:SCN-<slug>-<NNN>`)。
|
|
136
|
+
- **SCN 铸号(本仓唯一铸号点)**:
|
|
137
|
+
1. 继承优先:`reuseBundle.externalRefs` 已有场景键的 unchanged/modified 场景一律沿用原 SCN 号,禁止另铸。
|
|
138
|
+
2. 新场景:`SCN-<slug>-<NNN>`;slug=kebab-case 小写 ≤40;NNN=该 REQ 命名空间内 max+1(已用集合=KB 回传 ∪ 本 Change 已写键,含 removed 墓碑)。
|
|
139
|
+
3. removed 号是墓碑:永不复用、永不重排;序号达 999 → 硬错误,回需求系统拆分需求,不扩位。
|
|
140
|
+
4. 写完立即用 `cli.cjs external-key --validate … --type scenario` 校验;REQ 前缀必须 ∈ proposal `requirement-refs`。
|
|
141
|
+
- 场景级「同 SCN key + 同锚点、不同 entity_id」当场问 A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`;决议中的 `externalKey` 必须是归一化形态。
|
|
137
142
|
- 优先消费 `reuseBundles[].statements`;`designElements` 只作理解上下文,不能写成 Spec 的 How。
|
|
138
143
|
- 所有知识库内容均为 advisory;与用户确认 / proposal 冲突时以当前确认与 proposal 为准。
|
|
144
|
+
- **禁止**铸/改 REQ/FEAT;**禁止**自动重排/回收 SCN。
|
|
139
145
|
|
|
140
146
|
**【可选】业务知识库检索**:
|
|
141
147
|
术语含义不清且可能影响 spec 准确性时,可调用 **opsx-knowledge** skill。
|
|
@@ -189,7 +189,14 @@ Simple 模式或单文件能力域下,**不要拆成多个同文件任务**。
|
|
|
189
189
|
|
|
190
190
|
### 8. 质量红线自检
|
|
191
191
|
|
|
192
|
-
> 逐项确认,完整 7 项自检清单 + TDD 合规性自检(结构符合模板 / 拓扑图已绘制 / 依赖字段已填写 / 无循环依赖 / 颗粒度 ≤5 分钟 / 100% 覆盖 design / 每任务有验收标准)见 `./checklist.md`「§8 质量红线自检 + §8.1 TDD
|
|
192
|
+
> 逐项确认,完整 7 项自检清单 + TDD 合规性自检(结构符合模板 / 拓扑图已绘制 / 依赖字段已填写 / 无循环依赖 / 颗粒度 ≤5 分钟 / 100% 覆盖 design / 每任务有验收标准)见 `./checklist.md`「§8 质量红线自检 + §8.1 TDD 合规性自检」。
|
|
193
|
+
>
|
|
194
|
+
> 额外强制项(见 `./checklist.md` §8):
|
|
195
|
+
> - ⛔ **CON 覆盖**:spec.md 中每个 CON 必须有对应验证任务或显式声明间接覆盖
|
|
196
|
+
> - ⛔ **安全/审计要求覆盖**:spec.md §5.x 中的安全与审计要求必须有对应任务或显式声明推迟
|
|
197
|
+
> - ⛔ **§4.x 验证方式表内部一致性**:§4.x 验证方式表中的测试注解/配置必须与任务实现步骤中的声明一致
|
|
198
|
+
>
|
|
199
|
+
> 如有任意一项未满足,重新生成对应章节,直至全部通过。
|
|
193
200
|
|
|
194
201
|
### 9. 确认任务并输出
|
|
195
202
|
|
|
@@ -29,6 +29,9 @@ description: opsx-task 的阶段强制检查点与自检清单。仅在执行 ta
|
|
|
29
29
|
- [ ] 每个任务颗粒度 ≤ 5 分钟
|
|
30
30
|
- [ ] 100% 覆盖 design.md 定义
|
|
31
31
|
- [ ] 每个任务都有验收标准
|
|
32
|
+
- [ ] ⛔ **约束(CON)覆盖**:spec.md 中每个 CON 必须有对应验证任务,或在任务中显式声明"通过现有 AC 间接覆盖"并说明理由;tasks.md 质量红线声明"100% 覆盖 CON"时必须可追溯
|
|
33
|
+
- [ ] ⛔ **安全/审计要求覆盖**:spec.md §5.x 中的安全与审计要求(日志记录、脱敏、告警等)必须有对应任务,或显式声明推迟到后续迭代并在任务中标注
|
|
34
|
+
- [ ] ⛔ **§4.x 验证方式表内部一致性**:§4.x 验证方式表中的测试注解/配置必须与任务实现步骤中的声明一致,不得出现"§4.1 声明 @WebMvcTest 但实现步骤允许 @SpringBootTest"的矛盾
|
|
32
35
|
|
|
33
36
|
**如有任意一项未满足,重新生成对应章节,直至全部通过。**
|
|
34
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opsx-tdd-anti-patterns
|
|
3
|
-
description: "测试反模式防护层 —
|
|
3
|
+
description: "测试反模式防护层 — 16 种反模式检测(RED 阶段 3 种 + GREEN 后 13 种),每种带门禁函数和修复方案。当编写或审查测试代码时引用本技能。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# opsx-tdd-anti-patterns — 反模式防护层
|
|
@@ -32,7 +32,7 @@ description: "测试反模式防护层 — 15 种反模式检测(RED 阶段 3
|
|
|
32
32
|
| 2 | **Mock 预定结论而非准备条件** | `when(bookMapper.countByPublisher(1L)).thenReturn(3)` 后只测 `if (count > 0) throw` — trivial 逻辑 | "我的测试是在验证完整行为链路,还是只验证一个 if 分支?" | Mock 边界依赖(Mapper)是合理的,但测试断言应验证完整行为链路(如 verify 不会执行 delete) |
|
|
33
33
|
| 3 | **Given 不是真实输入** | mock 出"这个输入会导致什么结果",而非传入真实数据让被测代码自行处理 | "我的 Given 是真实数据还是 mock 出的预定结论?" | 传入真实数据(如 `"invalid"` 字符串、`null`、空对象),让被测代码自行决定结果 |
|
|
34
34
|
|
|
35
|
-
## §4 GREEN 后反模式(
|
|
35
|
+
## §4 GREEN 后反模式(13 种)
|
|
36
36
|
|
|
37
37
|
| # | 反模式 | 问题表现 | 修复方案 |
|
|
38
38
|
|---|--------|---------|---------|
|
|
@@ -48,6 +48,7 @@ description: "测试反模式防护层 — 15 种反模式检测(RED 阶段 3
|
|
|
48
48
|
| 13 | **魔法值** | 测试中使用未解释的字面值(如 `assertEquals(42, result)` 无注释说明 42 的含义) | 使用命名常量或注释解释字面值含义 |
|
|
49
49
|
| 14 | **断言不足** | 只断言了部分结果,遗漏了关键属性(如只 assertNotNull 但不 assertEquals 具体值) | 每个测试至少有一个具体值断言(assertEquals),而非仅 assertNotNull |
|
|
50
50
|
| 15 | **缺少负面测试** | 只测试正常路径,不测试错误条件 | 每个方法至少有一个异常路径测试(见 `opsx-tdd-rules/rules/exception-path-coverage.md`) |
|
|
51
|
+
| 16 | **AC 变体覆盖不足** | AC 场景描述含"或"条件(如"缺少 A 或 B 或为空"),但测试只覆盖部分变体 | AC 中每个"或"条件变体必须有对应测试方法(规则见 `opsx-tdd-rules/rules/multi-validation-split.md` §AC 内"或"条件变体覆盖) |
|
|
51
52
|
|
|
52
53
|
## §5 门禁函数
|
|
53
54
|
|
|
@@ -82,3 +83,5 @@ AFTER GREEN(GREEN 完成后、标记通过前):
|
|
|
82
83
|
- 测试中只有 `assertNotNull` 无具体值断言
|
|
83
84
|
- 测试中存在未解释的魔法数字/字符串
|
|
84
85
|
- 正常路径有测试但异常路径无测试
|
|
86
|
+
- AC 场景描述含"或"条件但测试只覆盖部分变体
|
|
87
|
+
- 连续 RED/GREEN 的 task_update 时间戳差距过小(批量执行信号,违反 TDD 严格串行)
|
|
@@ -195,6 +195,35 @@ assertEquals("user-001", result.getUserId());
|
|
|
195
195
|
|
|
196
196
|
**修复**:每个方法至少有一个异常路径测试。规则见 `opsx-tdd-rules/rules/exception-path-coverage.md`。
|
|
197
197
|
|
|
198
|
+
## 反模式 16:AC 变体覆盖不足
|
|
199
|
+
|
|
200
|
+
**问题**:AC 场景描述含"或"条件(如"缺少 A 或 B 或为空"),但测试只覆盖部分变体,其余变体无测试守护。
|
|
201
|
+
|
|
202
|
+
**反例**:
|
|
203
|
+
```java
|
|
204
|
+
// AC-004: 请求体缺少 username 或 password 字段,或字段值为空字符串
|
|
205
|
+
// 仅测试 1/6 变体
|
|
206
|
+
@Test
|
|
207
|
+
void login_withMissingParams_returns1001() {
|
|
208
|
+
mockMvc.perform(post("/login")
|
|
209
|
+
.content("{\"username\":\"\",\"password\":\"admin123\"}"))
|
|
210
|
+
.andExpect(jsonPath("$.code").value(1001));
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**正例**:
|
|
215
|
+
```java
|
|
216
|
+
// 每个变体都有测试方法(或参数化测试)
|
|
217
|
+
@Test void login_withEmptyUsername_returns1001() { ... }
|
|
218
|
+
@Test void login_withEmptyPassword_returns1001() { ... }
|
|
219
|
+
@Test void login_withMissingUsernameField_returns1001() { ... }
|
|
220
|
+
@Test void login_withMissingPasswordField_returns1001() { ... }
|
|
221
|
+
@Test void login_withBothEmpty_returns1001() { ... }
|
|
222
|
+
@Test void login_withBothMissing_returns1001() { ... }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**修复**:AC 中每个"或"条件变体必须有对应测试方法。规则见 `opsx-tdd-rules/rules/multi-validation-split.md` §AC 内"或"条件变体覆盖。
|
|
226
|
+
|
|
198
227
|
## TDD 如何防止这些反模式
|
|
199
228
|
|
|
200
229
|
1. 先写测试 → 迫使你思考实际在测试什么
|
|
@@ -40,6 +40,9 @@ description: "opsx-tdd-core 自检清单 — TDD 执行合规自检、合规性
|
|
|
40
40
|
- [ ] 每个 RED 任务包含测试方法名(`{method}_{state}_{outcome}` 格式)
|
|
41
41
|
- [ ] 每个 GREEN 任务包含 YAGNI 围栏声明("不提前实现 [后续 RED 行为]")
|
|
42
42
|
- [ ] 每个 REFACTOR 任务列出至少 2 个具体重构点
|
|
43
|
+
- [ ] ⛔ **CON 覆盖**:spec.md 中每个 CON 在 tasks.md 中有对应验证任务或显式声明间接覆盖
|
|
44
|
+
- [ ] ⛔ **安全/审计要求覆盖**:spec.md §5.x 中的安全与审计要求在 tasks.md 中有对应任务或显式声明推迟
|
|
45
|
+
- [ ] ⛔ **AC 变体覆盖**:spec.md 中含"或"条件的 AC 场景,其 RED 任务验收标准列出所有变体的测试方法(引用 `opsx-tdd-rules/rules/multi-validation-split.md`)
|
|
43
46
|
|
|
44
47
|
## §C 完成验证清单(8 项)
|
|
45
48
|
|
|
@@ -53,5 +56,6 @@ description: "opsx-tdd-core 自检清单 — TDD 执行合规自检、合规性
|
|
|
53
56
|
- [ ] 输出纯净(无错误/警告)
|
|
54
57
|
- [ ] 测试使用真实代码(仅在不可避免时使用 mock)
|
|
55
58
|
- [ ] 边界情况和错误已覆盖
|
|
59
|
+
- [ ] ⛔ **AC "或"条件变体全覆盖**:AC 场景描述含"或"条件时,每个变体都有对应测试方法(引用 `opsx-tdd-rules/rules/multi-validation-split.md`)
|
|
56
60
|
|
|
57
61
|
> Can't check all boxes? You skipped TDD. Start over.
|
|
@@ -24,6 +24,6 @@ description: "TDD 规则库 — DAG 生成规则、Controller 策略、任务类
|
|
|
24
24
|
| `rules/green-yagni-fence.md` | GREEN 任务 YAGNI 围栏自动注入规则 | HIGH | opsx-task |
|
|
25
25
|
| `rules/green-scope-declaration.md` | GREEN 任务 Scope 声明步骤(断言清单→流程标记→仅实现属于的步骤) | HIGH | opsx-apply |
|
|
26
26
|
| `rules/des-step-annotation.md` | DES 元素步骤级标注规则(TDD 模式下标注 [GREEN-N] 归属) | MEDIUM | opsx-design |
|
|
27
|
-
| `rules/multi-validation-split.md` | 多校验条件拆分规则(每个校验条件须有独立 AC
|
|
27
|
+
| `rules/multi-validation-split.md` | 多校验条件拆分规则(每个校验条件须有独立 AC 场景;AC 内"或"条件变体须全覆盖) | MEDIUM | opsx-spec, opsx-task, opsx-check |
|
|
28
28
|
| `rules/exception-path-coverage.md` | 异常路径测试覆盖门禁(每个 orElseThrow/边界检查须有对应 RED 测试) | HIGH | opsx-task, opsx-apply, opsx-check |
|
|
29
29
|
| `rules/refactor-checklist.md` | REFACTOR 阶段检查点(public API 不变、行为保持、重构质量评估) | HIGH | opsx-apply, opsx-tdd-core |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 多校验拆分规则
|
|
2
2
|
|
|
3
3
|
> 影响等级:MEDIUM
|
|
4
|
-
> 引用方:opsx-spec(spec.md 生成)、opsx-spec/checklist.md
|
|
4
|
+
> 引用方:opsx-spec(spec.md 生成)、opsx-spec/checklist.md(自检)、opsx-task(RED 任务拆解)、opsx-check(一致性检查)
|
|
5
5
|
|
|
6
6
|
## 问题
|
|
7
7
|
|
|
@@ -34,3 +34,37 @@ AC-03: 可借数量不足(available_count = 0)
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
> 目的:每个校验条件有独立场景 → 独立 RED 测试 → 独立 GREEN 实现,避免 AI 在一个 GREEN 中实现所有校验。
|
|
37
|
+
|
|
38
|
+
## AC 内"或"条件变体覆盖
|
|
39
|
+
|
|
40
|
+
> 引用方:opsx-spec(spec.md 生成)、opsx-task(RED 任务拆解)、opsx-check(一致性检查)
|
|
41
|
+
|
|
42
|
+
### 问题
|
|
43
|
+
|
|
44
|
+
单个 AC 场景描述中包含"或"条件时(如"缺少 username **或** password 字段,或字段值为空字符串"),AI 在拆解 RED 任务时倾向于只取一个变体编写测试,导致其余变体无测试守护。
|
|
45
|
+
|
|
46
|
+
### 规则
|
|
47
|
+
|
|
48
|
+
当单个 AC 场景描述包含"或"条件时,必须检查:
|
|
49
|
+
|
|
50
|
+
1. 每个"或"条件变体是否有对应的测试方法
|
|
51
|
+
2. 若多个变体共享一个 RED 任务,RED 验收标准中必须列出所有变体的测试方法名
|
|
52
|
+
3. 变体可合并到一个 RED 任务的多个 `@Test` 方法中,但不得遗漏任何变体
|
|
53
|
+
|
|
54
|
+
### 反例(禁止)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
AC-004: 请求体缺少 username 或 password 字段,或字段值为空字符串
|
|
58
|
+
RED-4: login_withMissingParams_returns1001
|
|
59
|
+
→ 仅测试 username="" 一种变体,password="" / 缺少字段 / 双空 / 双缺共 5 种变体未覆盖
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 正例(要求)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
AC-004: 请求体缺少 username 或 password 字段,或字段值为空字符串
|
|
66
|
+
RED-4: login_withMissingParams_returns1001(含 6 个 @Test 方法或参数化测试)
|
|
67
|
+
→ username="" / password="" / 缺少 username / 缺少 password / 双空 / 双缺 均有测试
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> 目的:spec 中"或"条件描述的每个变体都有测试守护,防止参数校验逻辑回归。
|