kld-sdd 2.6.3 → 2.6.4
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 +54 -14
- package/skywalk-sdd/ontology/archive-package.cjs +35 -4
- package/skywalk-sdd/ontology/artifact-parser.cjs +119 -5
- package/skywalk-sdd/ontology/identity-index.cjs +11 -0
- package/skywalk-sdd/ontology/normalizer.cjs +11 -0
- package/skywalk-sdd/ontology/runtime.cjs +5 -0
- package/skywalk-sdd/ontology/schema.cjs +5 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +156 -0
- package/templates/openspec/continuity-resolution.example.json +32 -0
- package/templates/openspec/proposal.md +18 -0
- package/templates/openspec/spec.md +3 -0
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +5 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +10 -0
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +21 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +21 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kld-sdd",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.4",
|
|
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-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/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",
|
|
@@ -24,12 +24,17 @@ function queryFromArgs(args) {
|
|
|
24
24
|
if (typeof args['query-file'] === 'string' && args['query-file'].trim()) {
|
|
25
25
|
return fs.readFileSync(args['query-file'], 'utf8').trim();
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
return '';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function buildEndpoint(apiBase, spaceId) {
|
|
30
|
+
function buildEndpoint(apiBase, spaceId, kbId, mode) {
|
|
31
31
|
const base = String(apiBase || 'http://localhost:8090/api').replace(/\/$/, '');
|
|
32
|
-
|
|
32
|
+
const space = encodeURIComponent(spaceId);
|
|
33
|
+
const kb = encodeURIComponent(kbId);
|
|
34
|
+
if (mode === 'resolve') {
|
|
35
|
+
return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/entities/resolve`;
|
|
36
|
+
}
|
|
37
|
+
return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/context/match-requirement`;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function requestJson(url, payload, token, timeoutMs) {
|
|
@@ -81,52 +86,85 @@ function requestJson(url, payload, token, timeoutMs) {
|
|
|
81
86
|
});
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
function compactContext(data, includeCandidates) {
|
|
89
|
+
function compactContext(data, includeCandidates, mode = 'match') {
|
|
85
90
|
if (includeCandidates) return data;
|
|
86
|
-
|
|
91
|
+
const compacted = {
|
|
87
92
|
matchType: data.matchType,
|
|
93
|
+
resolution: data.resolution,
|
|
88
94
|
inheritanceAllowed: data.inheritanceAllowed,
|
|
89
95
|
reviewRequired: data.reviewRequired,
|
|
96
|
+
removedBindingCount: data.removedBindingCount,
|
|
90
97
|
advisory: data.advisory,
|
|
91
98
|
degraded: data.degraded,
|
|
92
99
|
degradationReasons: data.degradationReasons,
|
|
93
100
|
targetStage: data.targetStage,
|
|
101
|
+
reuseMode: data.reuseMode,
|
|
94
102
|
reuseBundles: data.reuseBundles,
|
|
95
103
|
answeredQuestions: data.answeredQuestions,
|
|
96
104
|
warnings: data.warnings,
|
|
97
105
|
clarificationQuestions: data.clarificationQuestions,
|
|
98
106
|
specGenerationContext: data.specGenerationContext,
|
|
107
|
+
supportEvidence: data.supportEvidence,
|
|
108
|
+
oppositionEvidence: data.oppositionEvidence,
|
|
99
109
|
};
|
|
110
|
+
// Continuity resolve 必须透出 candidates;match 默认隐藏内部候选除非 --include-candidates
|
|
111
|
+
if (mode === 'resolve') {
|
|
112
|
+
compacted.candidates = data.candidates;
|
|
113
|
+
}
|
|
114
|
+
return compacted;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
function buildPayload(args, mode) {
|
|
118
|
+
const payload = {};
|
|
103
119
|
const query = queryFromArgs(args);
|
|
120
|
+
if (mode !== 'resolve') {
|
|
121
|
+
if (!query) {
|
|
122
|
+
throw new Error('missing --query or --query-file');
|
|
123
|
+
}
|
|
124
|
+
payload.query = query;
|
|
125
|
+
payload.targetStage = args['target-stage'] || 'spec';
|
|
126
|
+
} else if (query) {
|
|
127
|
+
payload.query = query;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (args['entity-id']) payload.entityId = args['entity-id'];
|
|
131
|
+
if (args['previous-version-id']) payload.previousVersionId = args['previous-version-id'];
|
|
132
|
+
if (args['entity-type']) payload.entityType = args['entity-type'];
|
|
133
|
+
if (args['external-system']) payload.externalSystem = args['external-system'];
|
|
134
|
+
if (args['external-object-type']) payload.externalObjectType = args['external-object-type'];
|
|
135
|
+
if (args['external-id']) payload.externalId = args['external-id'];
|
|
136
|
+
if (args['canonical-key']) payload.canonicalKey = args['canonical-key'];
|
|
137
|
+
return payload;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function retrieveContext(args = parseArgs(process.argv), env = process.env) {
|
|
104
141
|
const spaceId = args['space-id'] || env.ENGINEERING_KB_SPACE_ID || '';
|
|
105
|
-
|
|
142
|
+
const kbId = args['kb-id'] || env.ENGINEERING_KB_KB_ID || '';
|
|
143
|
+
if (!spaceId || !kbId) {
|
|
106
144
|
return {
|
|
107
145
|
available: false,
|
|
108
146
|
advisory: true,
|
|
109
147
|
degraded: true,
|
|
110
148
|
reason: 'engineering_kb_not_configured',
|
|
111
|
-
hint: 'set ENGINEERING_KB_SPACE_ID
|
|
149
|
+
hint: 'set ENGINEERING_KB_SPACE_ID and ENGINEERING_KB_KB_ID; do not fall back to local archive/',
|
|
112
150
|
};
|
|
113
151
|
}
|
|
114
152
|
|
|
153
|
+
const mode = String(args.mode || 'match').trim().toLowerCase() === 'resolve' ? 'resolve' : 'match';
|
|
115
154
|
const apiBase = args['api-base'] || env.ENGINEERING_KB_API || 'http://localhost:8090/api';
|
|
116
155
|
const token = args.token || env.ENGINEERING_KB_TOKEN || '';
|
|
117
156
|
const timeoutMs = Number(args.timeout || env.ENGINEERING_KB_TIMEOUT_MS || 30000);
|
|
157
|
+
const payload = buildPayload(args, mode);
|
|
118
158
|
const data = await requestJson(
|
|
119
|
-
buildEndpoint(apiBase, spaceId),
|
|
120
|
-
|
|
121
|
-
query,
|
|
122
|
-
targetStage: args['target-stage'] || 'spec',
|
|
123
|
-
},
|
|
159
|
+
buildEndpoint(apiBase, spaceId, kbId, mode),
|
|
160
|
+
payload,
|
|
124
161
|
token,
|
|
125
162
|
timeoutMs,
|
|
126
163
|
);
|
|
127
164
|
return {
|
|
128
165
|
available: true,
|
|
129
|
-
|
|
166
|
+
mode,
|
|
167
|
+
...compactContext(data, Boolean(args['include-candidates']), mode),
|
|
130
168
|
};
|
|
131
169
|
}
|
|
132
170
|
|
|
@@ -141,6 +179,7 @@ async function main() {
|
|
|
141
179
|
degraded: true,
|
|
142
180
|
reason: 'engineering_kb_unavailable',
|
|
143
181
|
message: error.message,
|
|
182
|
+
hint: 'KB unavailable: continue degraded; never scan local archive/ for inheritance UUIDs',
|
|
144
183
|
}, null, 2));
|
|
145
184
|
if (args.strict) process.exitCode = 1;
|
|
146
185
|
}
|
|
@@ -154,6 +193,7 @@ module.exports = {
|
|
|
154
193
|
parseArgs,
|
|
155
194
|
queryFromArgs,
|
|
156
195
|
buildEndpoint,
|
|
196
|
+
buildPayload,
|
|
157
197
|
requestJson,
|
|
158
198
|
compactContext,
|
|
159
199
|
retrieveContext,
|
|
@@ -8,7 +8,7 @@ const { generateUuidV7 } = require('./id.cjs');
|
|
|
8
8
|
const PRODUCER_VERSION = require('../../package.json').version;
|
|
9
9
|
const PRODUCER = `kld-sdd@${PRODUCER_VERSION}`;
|
|
10
10
|
const MANIFEST_SCHEMA_VERSION = 'kld-sdd-archive-manifest/v2';
|
|
11
|
-
const CANONICAL_FACTS_SCHEMA_VERSION = 'kld-sdd-canonical-facts/
|
|
11
|
+
const CANONICAL_FACTS_SCHEMA_VERSION = 'kld-sdd-canonical-facts/v2';
|
|
12
12
|
const CONVERSION_REPORT_SCHEMA_VERSION = 'kld-sdd-conversion-report/v1';
|
|
13
13
|
const PROJECT_IDENTITY_SCHEMA_VERSION = 'kld-sdd-project-identity/v1';
|
|
14
14
|
const MANIFEST_PATH = 'archive-manifest.json';
|
|
@@ -100,16 +100,42 @@ function requiredText(value, field, owner) {
|
|
|
100
100
|
return normalized;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function
|
|
103
|
+
function normalizeExternalRefs(refs) {
|
|
104
|
+
if (!Array.isArray(refs)) return [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const result = [];
|
|
107
|
+
for (const ref of refs) {
|
|
108
|
+
const system = String(ref.system || ref.system_name || '').trim();
|
|
109
|
+
const objectType = String(ref.object_type || ref.objectType || '').trim().toLowerCase();
|
|
110
|
+
const externalId = String(ref.external_id || ref.externalId || '').trim();
|
|
111
|
+
if (!system || !objectType || !externalId) continue;
|
|
112
|
+
const key = `${system}\0${objectType}\0${externalId}`;
|
|
113
|
+
if (seen.has(key)) continue;
|
|
114
|
+
seen.add(key);
|
|
115
|
+
result.push({
|
|
116
|
+
system,
|
|
117
|
+
object_type: objectType,
|
|
118
|
+
external_id: externalId,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function canonicalEntity(archiveDir, entity, inheritedRequirementRefs = []) {
|
|
104
125
|
const anchorId = requiredText(entity.anchor_id || entity.id, 'anchor_id', '本体实体').toUpperCase();
|
|
105
126
|
const versionId = requiredText(
|
|
106
127
|
entity.entity_version_id || entity.version_id,
|
|
107
128
|
'entity_version_id/version_id',
|
|
108
129
|
anchorId,
|
|
109
130
|
).toLowerCase();
|
|
131
|
+
const entityType = requiredText(entity.type, 'type', anchorId);
|
|
132
|
+
let externalRefs = normalizeExternalRefs(entity.external_refs || entity.externalRefs || []);
|
|
133
|
+
if (entityType === 'Capability' && inheritedRequirementRefs.length > 0) {
|
|
134
|
+
externalRefs = normalizeExternalRefs([...externalRefs, ...inheritedRequirementRefs]);
|
|
135
|
+
}
|
|
110
136
|
return {
|
|
111
137
|
anchor_id: anchorId,
|
|
112
|
-
type:
|
|
138
|
+
type: entityType,
|
|
113
139
|
name: String(entity.name || anchorId).trim(),
|
|
114
140
|
entity_id: requiredText(entity.entity_id, 'entity_id', anchorId).toLowerCase(),
|
|
115
141
|
entity_version_id: versionId,
|
|
@@ -123,6 +149,7 @@ function canonicalEntity(archiveDir, entity) {
|
|
|
123
149
|
review_status: 'confirmed',
|
|
124
150
|
generation_role: entity.generation_role || 'current',
|
|
125
151
|
...(entity.inherited_from ? { inherited_from: entity.inherited_from } : {}),
|
|
152
|
+
...(externalRefs.length > 0 ? { external_refs: externalRefs } : {}),
|
|
126
153
|
attributes: entity.attributes && typeof entity.attributes === 'object' ? entity.attributes : {},
|
|
127
154
|
source: sourceWithVerifiedHash(archiveDir, entity.source, anchorId, true),
|
|
128
155
|
};
|
|
@@ -198,8 +225,11 @@ function buildCanonicalFacts(archiveDir, snapshot, projectId, archiveId) {
|
|
|
198
225
|
throw new Error('archive-ontology.json 必须是 confirmed 快照');
|
|
199
226
|
}
|
|
200
227
|
const changeId = requiredText(snapshot.change_id, 'change_id', 'archive-ontology.json');
|
|
228
|
+
const requirementRefs = normalizeExternalRefs(
|
|
229
|
+
snapshot.requirement_refs || snapshot.requirementRefs || [],
|
|
230
|
+
).filter((ref) => ref.object_type === 'requirement');
|
|
201
231
|
const entities = (snapshot.entities || [])
|
|
202
|
-
.map((entity) => canonicalEntity(archiveDir, entity))
|
|
232
|
+
.map((entity) => canonicalEntity(archiveDir, entity, requirementRefs))
|
|
203
233
|
.sort((left, right) => (
|
|
204
234
|
left.anchor_id.localeCompare(right.anchor_id)
|
|
205
235
|
|| left.entity_version_id.localeCompare(right.entity_version_id)
|
|
@@ -241,6 +271,7 @@ function buildCanonicalFacts(archiveDir, snapshot, projectId, archiveId) {
|
|
|
241
271
|
facts_hash: requiredText(snapshot.facts_hash, 'facts_hash', 'archive-ontology.json'),
|
|
242
272
|
source_revision: snapshot.source_revision,
|
|
243
273
|
snapshot_id: snapshot.snapshot_id,
|
|
274
|
+
...(requirementRefs.length > 0 ? { requirement_refs: requirementRefs } : {}),
|
|
244
275
|
entities,
|
|
245
276
|
relations,
|
|
246
277
|
},
|
|
@@ -7,7 +7,7 @@ const { DIAGNOSTIC_CODES, entityTypeForId } = require('./schema.cjs');
|
|
|
7
7
|
const { ensureStructuralIdentities } = require('./structural-identity.cjs');
|
|
8
8
|
|
|
9
9
|
const ENTITY_ID_PATTERN = /\b(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
|
|
10
|
-
const IDENTITY_META_PATTERN = /^\s*-\s+\*\*(entity-id|version-id|predecessor-version|delta-state)\*\*\s*[::]\s*(.*?)\s*$/i;
|
|
10
|
+
const IDENTITY_META_PATTERN = /^\s*-\s+\*\*(entity-id|version-id|predecessor-version|delta-state|external-ref)\*\*\s*[::]\s*(.*?)\s*$/i;
|
|
11
11
|
|
|
12
12
|
function toPosix(value) {
|
|
13
13
|
return String(value || '').replace(/\\/g, '/');
|
|
@@ -126,6 +126,9 @@ function sourceFor(file, line, anchorId) {
|
|
|
126
126
|
|
|
127
127
|
function addEntity(target, type, id, name, file, line, attributes = {}, identity = {}, semanticContent = '') {
|
|
128
128
|
const anchorId = String(id || '').toUpperCase();
|
|
129
|
+
const externalRefs = Array.isArray(identity.external_refs)
|
|
130
|
+
? identity.external_refs.filter(Boolean)
|
|
131
|
+
: [];
|
|
129
132
|
const entity = {
|
|
130
133
|
id: anchorId,
|
|
131
134
|
anchor_id: anchorId,
|
|
@@ -135,6 +138,7 @@ function addEntity(target, type, id, name, file, line, attributes = {}, identity
|
|
|
135
138
|
version_id: String(identity.version_id || '').trim().toLowerCase() || undefined,
|
|
136
139
|
predecessor_version_id: String(identity.predecessor_version_id || '').trim().toLowerCase() || undefined,
|
|
137
140
|
delta_state: String(identity.delta_state || '').trim().toLowerCase() || undefined,
|
|
141
|
+
...(externalRefs.length > 0 ? { external_refs: externalRefs } : {}),
|
|
138
142
|
content_hash: sha256(stripIdentityMetadata(semanticContent || `${type}\n${anchorId}\n${name}`)),
|
|
139
143
|
attributes,
|
|
140
144
|
source: sourceFor(file, line, anchorId),
|
|
@@ -162,25 +166,123 @@ function stripIdentityMetadata(value) {
|
|
|
162
166
|
return String(value || '')
|
|
163
167
|
.split('\n')
|
|
164
168
|
.filter((line) => !IDENTITY_META_PATTERN.test(line))
|
|
165
|
-
.filter((line) => !/^\s*(?:entity-id|version-id|predecessor-version|delta-state)\s*:/i.test(line))
|
|
169
|
+
.filter((line) => !/^\s*(?:entity-id|version-id|predecessor-version|delta-state|external-ref)\s*:/i.test(line))
|
|
166
170
|
.filter((line) => !/^\s*-\s+\*\*(?:realizes|implements|covers|dependsOn|constrains|约束对象|实现需求|覆盖需求|依赖)\*\*\s*[::]/i.test(line))
|
|
167
171
|
.join('\n')
|
|
168
172
|
.trim();
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
function parseExternalRefValue(value) {
|
|
176
|
+
const raw = String(value || '').trim();
|
|
177
|
+
if (!raw || /^(?:无|none|null|-)$/i.test(raw)) return null;
|
|
178
|
+
const parts = raw.split(':').map((part) => part.trim()).filter(Boolean);
|
|
179
|
+
if (parts.length < 3) return null;
|
|
180
|
+
return {
|
|
181
|
+
system: parts[0],
|
|
182
|
+
object_type: parts[1].toLowerCase(),
|
|
183
|
+
external_id: parts.slice(2).join(':'),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
171
187
|
function identityFromValues(values = {}) {
|
|
172
188
|
const optionalValue = (value) => {
|
|
173
189
|
const normalized = String(value || '').trim();
|
|
174
190
|
return /^(?:无|none|null|-)$/i.test(normalized) ? '' : normalized;
|
|
175
191
|
};
|
|
192
|
+
const externalRef = parseExternalRefValue(values['external-ref']);
|
|
176
193
|
return {
|
|
177
194
|
entity_id: optionalValue(values['entity-id']),
|
|
178
195
|
version_id: optionalValue(values['version-id']),
|
|
179
196
|
predecessor_version_id: optionalValue(values['predecessor-version']),
|
|
180
197
|
delta_state: String(values['delta-state'] || '').trim(),
|
|
198
|
+
external_refs: externalRef ? [externalRef] : [],
|
|
181
199
|
};
|
|
182
200
|
}
|
|
183
201
|
|
|
202
|
+
function flushRequirementRef(requirementRefs, currentRef) {
|
|
203
|
+
if (!currentRef) return null;
|
|
204
|
+
const system = currentRef.system;
|
|
205
|
+
const objectType = currentRef['object-type'] || currentRef.object_type;
|
|
206
|
+
const externalId = currentRef['external-id'] || currentRef.external_id;
|
|
207
|
+
if (system && objectType && externalId) {
|
|
208
|
+
requirementRefs.push({
|
|
209
|
+
system,
|
|
210
|
+
object_type: String(objectType).toLowerCase(),
|
|
211
|
+
external_id: externalId,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseStructuredFrontmatter(lines) {
|
|
218
|
+
const flat = {};
|
|
219
|
+
const requirementRefs = [];
|
|
220
|
+
const continuity = {};
|
|
221
|
+
if (!lines.length || lines[0].trim() !== '---') {
|
|
222
|
+
return { flat, requirementRefs, continuity };
|
|
223
|
+
}
|
|
224
|
+
let section = null;
|
|
225
|
+
let currentRef = null;
|
|
226
|
+
let currentCap = null;
|
|
227
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
228
|
+
const line = lines[index].replace(/\r$/, '');
|
|
229
|
+
if (line.trim() === '---') break;
|
|
230
|
+
const topKey = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
231
|
+
const listItem = line.match(/^\s+-\s+([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
232
|
+
const nested = line.match(/^\s{2,}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
233
|
+
|
|
234
|
+
if (topKey && !/^\s/.test(line)) {
|
|
235
|
+
currentRef = flushRequirementRef(requirementRefs, currentRef);
|
|
236
|
+
section = topKey[1].toLowerCase();
|
|
237
|
+
const value = unwrapScalar(stripInlineYamlComment(topKey[2]));
|
|
238
|
+
flat[section] = value;
|
|
239
|
+
currentCap = null;
|
|
240
|
+
if (section === 'continuity' && value) continuity.kind = value;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (section === 'requirement-refs' && listItem) {
|
|
245
|
+
currentRef = flushRequirementRef(requirementRefs, currentRef) || {};
|
|
246
|
+
currentRef = { [listItem[1].toLowerCase()]: unwrapScalar(stripInlineYamlComment(listItem[2])) };
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (section === 'requirement-refs' && nested) {
|
|
251
|
+
currentRef = currentRef || {};
|
|
252
|
+
currentRef[nested[1].toLowerCase()] = unwrapScalar(stripInlineYamlComment(nested[2]));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (section === 'continuity' && nested) {
|
|
257
|
+
const key = nested[1].toLowerCase().replace(/-/g, '_');
|
|
258
|
+
const value = unwrapScalar(stripInlineYamlComment(nested[2]));
|
|
259
|
+
if (key === 'base_capabilities') {
|
|
260
|
+
continuity.baseCapabilities = continuity.baseCapabilities || [];
|
|
261
|
+
currentCap = {};
|
|
262
|
+
continuity.baseCapabilities.push(currentCap);
|
|
263
|
+
} else if (currentCap && ['anchor', 'entity_id', 'current_version_id'].includes(key)) {
|
|
264
|
+
currentCap[key] = value;
|
|
265
|
+
} else {
|
|
266
|
+
continuity[key] = value;
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (section === 'continuity' && listItem) {
|
|
272
|
+
continuity.baseCapabilities = continuity.baseCapabilities || [];
|
|
273
|
+
if (!currentCap || Object.keys(currentCap).length > 0) {
|
|
274
|
+
currentCap = {};
|
|
275
|
+
continuity.baseCapabilities.push(currentCap);
|
|
276
|
+
}
|
|
277
|
+
currentCap[listItem[1].toLowerCase().replace(/-/g, '_')] = unwrapScalar(
|
|
278
|
+
stripInlineYamlComment(listItem[2]),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
flushRequirementRef(requirementRefs, currentRef);
|
|
283
|
+
return { flat, requirementRefs, continuity };
|
|
284
|
+
}
|
|
285
|
+
|
|
184
286
|
function parseIdentityBlock(lines, startIndex, endIndex = lines.length) {
|
|
185
287
|
const values = {};
|
|
186
288
|
let started = false;
|
|
@@ -217,8 +319,12 @@ function statementContentEnd(lines, startIndex) {
|
|
|
217
319
|
}
|
|
218
320
|
|
|
219
321
|
function parseProposal(target, file, lines, frontmatter) {
|
|
220
|
-
|
|
221
|
-
const
|
|
322
|
+
const structured = parseStructuredFrontmatter(lines);
|
|
323
|
+
const mergedFrontmatter = { ...structured.flat, ...frontmatter };
|
|
324
|
+
target.proposalMode = String(mergedFrontmatter.mode || '').trim().toLowerCase();
|
|
325
|
+
target.requirementRefs = structured.requirementRefs;
|
|
326
|
+
target.continuity = structured.continuity;
|
|
327
|
+
const changeId = String(mergedFrontmatter['change-id'] || '').trim().toUpperCase();
|
|
222
328
|
if (changeId) {
|
|
223
329
|
addEntity(
|
|
224
330
|
target,
|
|
@@ -228,7 +334,7 @@ function parseProposal(target, file, lines, frontmatter) {
|
|
|
228
334
|
file,
|
|
229
335
|
1,
|
|
230
336
|
{ change_name: target.changeName },
|
|
231
|
-
identityFromValues(
|
|
337
|
+
identityFromValues(mergedFrontmatter),
|
|
232
338
|
file.content,
|
|
233
339
|
);
|
|
234
340
|
target.changeId = changeId;
|
|
@@ -248,6 +354,12 @@ function parseProposal(target, file, lines, frontmatter) {
|
|
|
248
354
|
if (!match) continue;
|
|
249
355
|
const capabilityId = match[1].toUpperCase();
|
|
250
356
|
const identity = parseIdentityBlock(lines, index + 1);
|
|
357
|
+
if (structured.requirementRefs.length > 0) {
|
|
358
|
+
identity.external_refs = [
|
|
359
|
+
...(identity.external_refs || []),
|
|
360
|
+
...structured.requirementRefs,
|
|
361
|
+
];
|
|
362
|
+
}
|
|
251
363
|
const slug = unwrapScalar(match[2]);
|
|
252
364
|
addEntity(target, 'Capability', capabilityId, slug, file, index + 1, {
|
|
253
365
|
slug,
|
|
@@ -570,6 +682,8 @@ function parseChangeArtifacts(projectRoot, changeName, options = {}) {
|
|
|
570
682
|
changeId: '',
|
|
571
683
|
profile: 'simple',
|
|
572
684
|
proposalMode: '',
|
|
685
|
+
requirementRefs: [],
|
|
686
|
+
continuity: {},
|
|
573
687
|
artifacts: [],
|
|
574
688
|
files: files.map((file) => ({ path: file.relativePath, content_hash: file.contentHash })),
|
|
575
689
|
entities: [],
|
|
@@ -142,6 +142,7 @@ function recordsFor(candidate, facts, sourceRevision) {
|
|
|
142
142
|
delta_state: entity.delta_state,
|
|
143
143
|
content_hash: entity.content_hash,
|
|
144
144
|
source: entity.source,
|
|
145
|
+
external_refs: Array.isArray(entity.external_refs) ? entity.external_refs : [],
|
|
145
146
|
}));
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -263,6 +264,15 @@ function buildIdentityCatalog(projectRoot, options = {}) {
|
|
|
263
264
|
|| String(left.message || '').localeCompare(String(right.message || ''))
|
|
264
265
|
));
|
|
265
266
|
|
|
267
|
+
const byExternalRef = new Map();
|
|
268
|
+
for (const record of records) {
|
|
269
|
+
for (const ref of record.external_refs || []) {
|
|
270
|
+
const key = `${ref.system}|${ref.object_type}|${ref.external_id}`;
|
|
271
|
+
if (!byExternalRef.has(key)) byExternalRef.set(key, []);
|
|
272
|
+
byExternalRef.get(key).push(record);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
266
276
|
return {
|
|
267
277
|
records,
|
|
268
278
|
factsByRoot,
|
|
@@ -270,6 +280,7 @@ function buildIdentityCatalog(projectRoot, options = {}) {
|
|
|
270
280
|
byVersionId: groupBy(records, 'version_id'),
|
|
271
281
|
byAnchor: groupBy(records, 'anchor_id'),
|
|
272
282
|
byPredecessor: groupBy(records, 'predecessor_version_id'),
|
|
283
|
+
byExternalRef,
|
|
273
284
|
diagnostics,
|
|
274
285
|
};
|
|
275
286
|
}
|
|
@@ -20,6 +20,13 @@ function normalizeFacts(parsed) {
|
|
|
20
20
|
version_id: String(entity.version_id || '').trim().toLowerCase() || undefined,
|
|
21
21
|
predecessor_version_id: String(entity.predecessor_version_id || '').trim().toLowerCase() || undefined,
|
|
22
22
|
delta_state: String(entity.delta_state || '').trim().toLowerCase() || undefined,
|
|
23
|
+
external_refs: Array.isArray(entity.external_refs)
|
|
24
|
+
? entity.external_refs.map((ref) => ({
|
|
25
|
+
system: String(ref.system || '').trim(),
|
|
26
|
+
object_type: String(ref.object_type || ref.objectType || '').trim().toLowerCase(),
|
|
27
|
+
external_id: String(ref.external_id || ref.externalId || '').trim(),
|
|
28
|
+
})).filter((ref) => ref.system && ref.object_type && ref.external_id)
|
|
29
|
+
: undefined,
|
|
23
30
|
})).sort((left, right) => (
|
|
24
31
|
compareText(left.id, right.id)
|
|
25
32
|
|| compareText(left.type, right.type)
|
|
@@ -74,6 +81,10 @@ function normalizeFacts(parsed) {
|
|
|
74
81
|
change_id: parsed.changeId || undefined,
|
|
75
82
|
profile: parsed.profile,
|
|
76
83
|
proposal_mode: parsed.proposalMode || undefined,
|
|
84
|
+
requirement_refs: Array.isArray(parsed.requirementRefs) ? parsed.requirementRefs : undefined,
|
|
85
|
+
continuity: parsed.continuity && Object.keys(parsed.continuity).length > 0
|
|
86
|
+
? parsed.continuity
|
|
87
|
+
: undefined,
|
|
77
88
|
artifacts: (parsed.artifacts || []).slice().sort((left, right) => compareText(left.path, right.path)),
|
|
78
89
|
files: (parsed.files || []).slice().sort((left, right) => compareText(left.path, right.path)),
|
|
79
90
|
entities,
|
|
@@ -271,6 +271,11 @@ function scanChange(projectRoot, changeName, options = {}) {
|
|
|
271
271
|
profile,
|
|
272
272
|
identityHistory,
|
|
273
273
|
effectiveGraph,
|
|
274
|
+
projectRoot,
|
|
275
|
+
continuityResolutionPath: path.join(
|
|
276
|
+
parsed.changeDir,
|
|
277
|
+
'continuity-resolution.json',
|
|
278
|
+
),
|
|
274
279
|
});
|
|
275
280
|
const reviewStatus = options.markPending && validation.valid ? 'pending' : 'draft';
|
|
276
281
|
const revisionInput = {
|
|
@@ -104,6 +104,11 @@ const DIAGNOSTIC_CODES = Object.freeze({
|
|
|
104
104
|
ARCHIVE_NOT_CONFIRMED: 'SEM_ARCHIVE_NOT_CONFIRMED',
|
|
105
105
|
ARCHIVE_REVISION_MISMATCH: 'SEM_ARCHIVE_REVISION_MISMATCH',
|
|
106
106
|
INHERITED_RELATION_MISSING: 'SEM_INHERITED_RELATION_MISSING',
|
|
107
|
+
EXTERNAL_REF_INVALID: 'EXTERNAL_REF_INVALID',
|
|
108
|
+
EXTERNAL_REF_TYPE_MISMATCH: 'EXTERNAL_REF_TYPE_MISMATCH',
|
|
109
|
+
EXTERNAL_REF_CONFLICT: 'EXTERNAL_REF_CONFLICT',
|
|
110
|
+
CONTINUITY_DECISION_REQUIRED: 'CONTINUITY_DECISION_REQUIRED',
|
|
111
|
+
CONTINUITY_IDENTITY_MISMATCH: 'CONTINUITY_IDENTITY_MISMATCH',
|
|
107
112
|
});
|
|
108
113
|
|
|
109
114
|
function normalizeProfile(profile, inferredProfile = 'simple') {
|
|
@@ -340,6 +340,161 @@ function validateInheritedReferences(facts, diagnostics, identityHistory) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
|
|
344
|
+
const allowed = {
|
|
345
|
+
requirement: new Set(['Capability']),
|
|
346
|
+
scenario: new Set(['SpecificationStatement', 'AcceptanceCriterion']),
|
|
347
|
+
feature: new Set(['Capability']),
|
|
348
|
+
};
|
|
349
|
+
const continuity = facts.continuity || {};
|
|
350
|
+
const kind = String(continuity.kind || '').toLowerCase();
|
|
351
|
+
const resolutionPath = options.continuityResolutionPath
|
|
352
|
+
|| (facts.change
|
|
353
|
+
? require('path').join(
|
|
354
|
+
options.projectRoot || process.cwd(),
|
|
355
|
+
'openspec',
|
|
356
|
+
'changes',
|
|
357
|
+
facts.change,
|
|
358
|
+
'continuity-resolution.json',
|
|
359
|
+
)
|
|
360
|
+
: '');
|
|
361
|
+
let resolution = null;
|
|
362
|
+
if (resolutionPath) {
|
|
363
|
+
try {
|
|
364
|
+
const fs = require('fs');
|
|
365
|
+
if (fs.existsSync(resolutionPath)) {
|
|
366
|
+
resolution = JSON.parse(fs.readFileSync(resolutionPath, 'utf8'));
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
resolution = null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (kind === 'iteration') {
|
|
374
|
+
const caps = (facts.entities || []).filter((entity) => entity.type === 'Capability');
|
|
375
|
+
for (const capability of caps) {
|
|
376
|
+
if (!capability.entity_id || !capability.version_id) {
|
|
377
|
+
diagnostics.push(diagnostic(
|
|
378
|
+
DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
|
|
379
|
+
'error',
|
|
380
|
+
`Continuity=iteration 时 Capability 缺少 KB 回传身份: ${capability.id}`,
|
|
381
|
+
{
|
|
382
|
+
...sourceContext(capability),
|
|
383
|
+
entity_id: capability.id,
|
|
384
|
+
suggestion: '在 propose 用 entities/resolve 回填 entity-id / version-id',
|
|
385
|
+
},
|
|
386
|
+
));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!resolution || resolution.status === 'pending') {
|
|
390
|
+
diagnostics.push(diagnostic(
|
|
391
|
+
DIAGNOSTIC_CODES.CONTINUITY_DECISION_REQUIRED,
|
|
392
|
+
'error',
|
|
393
|
+
'Continuity=iteration 但 continuity-resolution.json 缺失或仍为 pending',
|
|
394
|
+
{
|
|
395
|
+
file: resolutionPath || 'continuity-resolution.json',
|
|
396
|
+
suggestion: '在 propose/spec 完成 A/B/C 决议后再 check',
|
|
397
|
+
},
|
|
398
|
+
));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (resolution && resolution.status && resolution.status !== 'pending') {
|
|
403
|
+
const decisions = [
|
|
404
|
+
...(Array.isArray(resolution.capabilities) ? resolution.capabilities : []),
|
|
405
|
+
...(Array.isArray(resolution.scenarios) ? resolution.scenarios : []),
|
|
406
|
+
];
|
|
407
|
+
const entitiesByAnchor = new Map(
|
|
408
|
+
(facts.entities || []).map((entity) => [String(entity.anchor_id || entity.id || '').toUpperCase(), entity]),
|
|
409
|
+
);
|
|
410
|
+
for (const decision of decisions) {
|
|
411
|
+
const anchor = String(decision.anchor || '').toUpperCase();
|
|
412
|
+
if (!anchor) continue;
|
|
413
|
+
const entity = entitiesByAnchor.get(anchor);
|
|
414
|
+
if (!entity) continue;
|
|
415
|
+
const decisionCode = String(decision.decision || '').toLowerCase();
|
|
416
|
+
const decidedEntityId = String(decision.entityId || decision.entity_id || '').toLowerCase();
|
|
417
|
+
const actualEntityId = String(entity.entity_id || '').toLowerCase();
|
|
418
|
+
if (
|
|
419
|
+
(decisionCode === 'reuse-existing-identity' || decisionCode === 'reuse_existing' || decisionCode === 'a')
|
|
420
|
+
&& decidedEntityId
|
|
421
|
+
&& actualEntityId
|
|
422
|
+
&& decidedEntityId !== actualEntityId
|
|
423
|
+
) {
|
|
424
|
+
diagnostics.push(diagnostic(
|
|
425
|
+
DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
|
|
426
|
+
'error',
|
|
427
|
+
`决议复用历史身份但产物仍使用新 entity_id: ${anchor}`,
|
|
428
|
+
{
|
|
429
|
+
...sourceContext(entity),
|
|
430
|
+
entity_id: entity.id,
|
|
431
|
+
suggestion: '按 continuity-resolution 回填历史 entity-id / version-id',
|
|
432
|
+
},
|
|
433
|
+
));
|
|
434
|
+
}
|
|
435
|
+
if (
|
|
436
|
+
(decisionCode === 'assign-new-anchor' || decisionCode === 'assign_new' || decisionCode === 'b')
|
|
437
|
+
&& decidedEntityId
|
|
438
|
+
&& actualEntityId
|
|
439
|
+
&& decidedEntityId === actualEntityId
|
|
440
|
+
&& String(decision.notes || '').toLowerCase().includes('new-anchor-required')
|
|
441
|
+
) {
|
|
442
|
+
diagnostics.push(diagnostic(
|
|
443
|
+
DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
|
|
444
|
+
'error',
|
|
445
|
+
`决议为新对象但仍共用旧锚点/身份: ${anchor}`,
|
|
446
|
+
{
|
|
447
|
+
...sourceContext(entity),
|
|
448
|
+
entity_id: entity.id,
|
|
449
|
+
suggestion: '更换新锚点并分配新 entity_id 后重跑 check',
|
|
450
|
+
},
|
|
451
|
+
));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const bindingKeys = new Map();
|
|
457
|
+
for (const entity of facts.entities || []) {
|
|
458
|
+
for (const ref of entity.external_refs || []) {
|
|
459
|
+
const objectType = String(ref.object_type || '').toLowerCase();
|
|
460
|
+
const allowedTypes = allowed[objectType];
|
|
461
|
+
if (!allowedTypes) {
|
|
462
|
+
diagnostics.push(diagnostic(
|
|
463
|
+
DIAGNOSTIC_CODES.EXTERNAL_REF_INVALID,
|
|
464
|
+
'error',
|
|
465
|
+
`未知 external object_type: ${objectType} (${entity.id})`,
|
|
466
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
467
|
+
));
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (!allowedTypes.has(entity.type)) {
|
|
471
|
+
diagnostics.push(diagnostic(
|
|
472
|
+
DIAGNOSTIC_CODES.EXTERNAL_REF_TYPE_MISMATCH,
|
|
473
|
+
'error',
|
|
474
|
+
`external object_type=${objectType} 与实体类型 ${entity.type} 不匹配: ${entity.id}`,
|
|
475
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
476
|
+
));
|
|
477
|
+
}
|
|
478
|
+
const key = `${ref.system}|${objectType}|${ref.external_id}|${entity.anchor_id || entity.id}`;
|
|
479
|
+
const prior = bindingKeys.get(key);
|
|
480
|
+
if (prior && prior !== entity.entity_id) {
|
|
481
|
+
diagnostics.push(diagnostic(
|
|
482
|
+
DIAGNOSTIC_CODES.EXTERNAL_REF_CONFLICT,
|
|
483
|
+
'error',
|
|
484
|
+
`同外部键+同锚点绑定了不同 entity_id: ${entity.id}`,
|
|
485
|
+
{
|
|
486
|
+
...sourceContext(entity),
|
|
487
|
+
entity_id: entity.id,
|
|
488
|
+
suggestion: '复用历史身份或更换新锚点后重开 entity_id',
|
|
489
|
+
},
|
|
490
|
+
));
|
|
491
|
+
} else if (entity.entity_id) {
|
|
492
|
+
bindingKeys.set(key, entity.entity_id);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
343
498
|
function validateTraceability(facts, options = {}) {
|
|
344
499
|
const profile = normalizeProfile(options.profile || 'auto', facts.profile || 'simple');
|
|
345
500
|
const graphEntities = options.effectiveGraph && options.effectiveGraph.effective_entities
|
|
@@ -356,6 +511,7 @@ function validateTraceability(facts, options = {}) {
|
|
|
356
511
|
const entitiesById = new Map();
|
|
357
512
|
|
|
358
513
|
validateArtifactPresence(facts, profile, diagnostics);
|
|
514
|
+
validateContinuityAndExternalRefs(facts, diagnostics, options);
|
|
359
515
|
|
|
360
516
|
if (profile === 'strict') {
|
|
361
517
|
for (const artifact of facts.artifacts || []) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "resolved",
|
|
3
|
+
"updatedAt": "2026-07-23T00:00:00.000Z",
|
|
4
|
+
"capabilities": [
|
|
5
|
+
{
|
|
6
|
+
"anchor": "CAP-ORDER-CANCEL",
|
|
7
|
+
"externalKey": {
|
|
8
|
+
"system": "requirement-mgmt",
|
|
9
|
+
"objectType": "requirement",
|
|
10
|
+
"externalId": "REQ-FI-2024-001"
|
|
11
|
+
},
|
|
12
|
+
"decision": "reuse-existing-identity",
|
|
13
|
+
"entityId": "00000000-0000-4000-8000-000000000001",
|
|
14
|
+
"currentVersionId": "00000000-0000-4000-8000-000000000011",
|
|
15
|
+
"notes": "propose 阶段 A/B/C:复用历史能力"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"scenarios": [
|
|
19
|
+
{
|
|
20
|
+
"anchor": "AC-ORDER-CANCEL-001",
|
|
21
|
+
"externalKey": {
|
|
22
|
+
"system": "requirement-mgmt",
|
|
23
|
+
"objectType": "scenario",
|
|
24
|
+
"externalId": "REQ-FI-2024-001:SCN-req-date-empty-002"
|
|
25
|
+
},
|
|
26
|
+
"decision": "reuse-existing-identity",
|
|
27
|
+
"entityId": "00000000-0000-4000-8000-000000000021",
|
|
28
|
+
"currentVersionId": "00000000-0000-4000-8000-000000000031",
|
|
29
|
+
"notes": "spec 阶段场景冲突决议"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -7,6 +7,24 @@ 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
|
+
# 外部需求键(首轮冷启动也建议写入,便于 ingest 种桥)
|
|
11
|
+
requirement-refs:
|
|
12
|
+
- system: requirement-mgmt
|
|
13
|
+
object-type: requirement
|
|
14
|
+
external-id: REQ-<DOMAIN>-<NNN>
|
|
15
|
+
# Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
|
|
16
|
+
continuity:
|
|
17
|
+
kind: new # iteration | similar-reference | new
|
|
18
|
+
kb-space-id: "" # KB space UUID(iteration 时必填)
|
|
19
|
+
kb-id: "" # KB UUID(iteration 时必填)
|
|
20
|
+
base-change-id: "" # 可选,来自 KB source/change_id
|
|
21
|
+
base-archive-id: "" # 可选溯源(KB archive_id),不是本地目录名
|
|
22
|
+
base-capabilities: []
|
|
23
|
+
# iteration 示例:
|
|
24
|
+
# base-capabilities:
|
|
25
|
+
# - anchor: CAP-<CAPABILITY>
|
|
26
|
+
# entity-id: <uuid>
|
|
27
|
+
# current-version-id: <uuid>
|
|
10
28
|
---
|
|
11
29
|
|
|
12
30
|
# proposal.md - 业务意图与上下文总览
|
|
@@ -34,6 +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>-<NNN>:SCN-<slug>
|
|
37
38
|
- **当** <!-- 触发条件 -->
|
|
38
39
|
- **预期** <!-- 预期结果 -->
|
|
39
40
|
|
|
@@ -42,6 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
42
43
|
- **version-id**: <UUID>
|
|
43
44
|
- **delta-state**: added
|
|
44
45
|
- **predecessor-version**: 无
|
|
46
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
|
|
45
47
|
- **当** <!-- 触发条件 -->
|
|
46
48
|
- **预期** <!-- 预期结果 -->
|
|
47
49
|
|
|
@@ -61,6 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
61
63
|
- **version-id**: <新 UUID>
|
|
62
64
|
- **delta-state**: <modified|added>
|
|
63
65
|
- **predecessor-version**: <modified 时填写;added 为无>
|
|
66
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
|
|
64
67
|
- **当** <!-- 触发条件 -->
|
|
65
68
|
- **预期** <!-- 预期结果 -->
|
|
66
69
|
|
|
@@ -108,13 +108,17 @@ node skywalk-sdd/log.cjs archive-docs --project=. --change=<变更名称> --reas
|
|
|
108
108
|
该命令成功后必须已经完成:
|
|
109
109
|
- 活动目录 `openspec/changes/<name>/` 被移入 `openspec/changes/archive/<日期>-<name>/`。
|
|
110
110
|
- 归档目录写入 `archive-ontology.json`、`canonical-facts.json`、`conversion-report.json` 和新版 `archive-manifest.json`。
|
|
111
|
-
- 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip
|
|
111
|
+
- 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip`(canonical-facts **v2**,含 `external_refs`),包内文件必须由 manifest 完整列举并通过 SHA-256 校验。
|
|
112
112
|
- Full Spec 的 `specs/<capability>/spec.md` 同步到 `openspec/specs/<capability>/spec.md`。
|
|
113
113
|
- archive 阶段写入 `stage_end`。
|
|
114
114
|
- 未勾选 tasks 被写入 `archive_result.task_completion`。
|
|
115
115
|
- 最终中文报告生成到 `openspec/changes/archive/<日期>-<name>/reports/<name>-report.md` 及同名 `<name>-report.html`(默认同时生成 .md 与 .html 双产物,默认归档后 archive 目录,可用 --report-output 自定义)。
|
|
116
116
|
- 执行日志 `openspec/changes/archive/<日期>-<name>/logs/execution-log.md` 随归档整目录迁移(人读审计层)。
|
|
117
117
|
|
|
118
|
+
### 5.5 收尾入库(opsx-kb-ingest)
|
|
119
|
+
|
|
120
|
+
归档 zip 生成后,提示用户用知识库技能 **`opsx-kb-ingest`** 上传;成功则写 `ingest-receipt.json`。若返回 `EXTERNAL_REF_CONFLICT`,引导回 spec/check 修正后重入,**禁止**在 KB 内现场改绑。
|
|
121
|
+
|
|
118
122
|
> 注意:`archive-docs` 成功执行后已经在内部写入 `stage_end`,因此**不要在成功的归档后再单独运行 `node skywalk-sdd/log.cjs end --command=archive ...`**。仅在第 5 步归档命令失败时,才需要运行下方的失败分支 `end`。
|
|
119
123
|
|
|
120
124
|
如果该命令失败,以失败状态结束 telemetry:
|
|
@@ -216,6 +216,16 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
|
|
|
216
216
|
|
|
217
217
|
---
|
|
218
218
|
|
|
219
|
+
## Continuity / external_ref 确定性门禁
|
|
220
|
+
|
|
221
|
+
`opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
|
|
222
|
+
|
|
223
|
+
- Continuity=`iteration` 时每个 CAP 具备 KB 回传 `entity-id` / `version-id`
|
|
224
|
+
- 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
|
|
225
|
+
- 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
|
|
226
|
+
- 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
|
|
227
|
+
- CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
|
|
228
|
+
|
|
219
229
|
## 本体语义关系门禁
|
|
220
230
|
|
|
221
231
|
在其他质量检查前必须运行:
|
|
@@ -159,6 +159,27 @@ openspec instructions proposal --change "<name>" --json
|
|
|
159
159
|
|
|
160
160
|
> 完整性检查(问题描述/目标/模块/约束 4 项)与缺失补充机制见 `./checklist.md`「§6 需求完整性检查」。发现缺失时主动询问用户补充。
|
|
161
161
|
|
|
162
|
+
### 6.5 【Continuity】需求 / Capability 身份(只到 CAP,不做场景)
|
|
163
|
+
|
|
164
|
+
在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP 编号分配前):
|
|
165
|
+
|
|
166
|
+
1. 提取 / 询问外部需求号 `REQ-*`(没有则问一次)。
|
|
167
|
+
2. 调用知识库 **`opsx-ontology-query`**(或薄封装):
|
|
168
|
+
```bash
|
|
169
|
+
node skywalk-sdd/context-client.cjs --mode=resolve \
|
|
170
|
+
--external-system=requirement-mgmt \
|
|
171
|
+
--external-object-type=requirement \
|
|
172
|
+
--external-id="<REQ-...>" \
|
|
173
|
+
--entity-type=Capability \
|
|
174
|
+
--space-id="$ENGINEERING_KB_SPACE_ID" \
|
|
175
|
+
--kb-id="$ENGINEERING_KB_KB_ID"
|
|
176
|
+
```
|
|
177
|
+
3. 按 KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP。
|
|
178
|
+
4. 写入 proposal frontmatter:`requirement-refs` + `continuity`(字段来自 KB:`kb-space-id` / `kb-id` / `base-capabilities[].entity-id` / `current-version-id`)。**禁止**写本地 archive 文件夹名作为 `base-archive`。
|
|
179
|
+
5. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `openspec/changes/<name>/continuity-resolution.json` 的 `capabilities[]`。
|
|
180
|
+
6. KB 不可用 → `degraded` 继续,**禁止**扫本地 `archive/` 抄 UUID。预期:恢复后同锚点入库可能触发 `EXTERNAL_REF_CONFLICT`。
|
|
181
|
+
7. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份。
|
|
182
|
+
|
|
162
183
|
### 7. 【交互引导】文档拆分模式选择
|
|
163
184
|
|
|
164
185
|
**❗ 必须主动询问用户,不得默认选择**。Full / Simple / Auto 三种模式的目录结构、适用场景与 AskUserQuestion 文案见 `./reference.md`「§7 文档拆分模式选择」。根据用户选择设置 `mode: full | simple`(Auto 按能力域数量判断),记录到 proposal.md 的 YAML frontmatter。
|
|
@@ -107,21 +107,31 @@ openspec list
|
|
|
107
107
|
|
|
108
108
|
> 上下文类型(需求文档 / 代码文件 / API 文档)与用途见 `./reference.md`「§3 上下文类型与用途」。
|
|
109
109
|
|
|
110
|
-
**【默认尝试】工程 Spec
|
|
110
|
+
**【默认尝试】工程 Spec 知识库上下文(场景身份主战场)**:
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
读取 proposal Continuity。对**当前 Capability** 各调一次(「全部」= 循环 N 次,不是一次大查询):
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
node skywalk-sdd/context-client.cjs
|
|
115
|
+
node skywalk-sdd/context-client.cjs \
|
|
116
|
+
--query="<当前 Capability 的自然语言需求>" \
|
|
117
|
+
--target-stage=spec \
|
|
118
|
+
--entity-id="<该 CAP 的 entity_id>" \
|
|
119
|
+
--external-system=requirement-mgmt \
|
|
120
|
+
--external-object-type=requirement \
|
|
121
|
+
--external-id="<REQ-...>" \
|
|
122
|
+
--space-id="$ENGINEERING_KB_SPACE_ID" \
|
|
123
|
+
--kb-id="$ENGINEERING_KB_KB_ID"
|
|
116
124
|
```
|
|
117
125
|
|
|
118
|
-
- 配置项:`ENGINEERING_KB_API`、`ENGINEERING_KB_SPACE_ID`、可选 `ENGINEERING_KB_TOKEN
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
- `
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
126
|
+
- 配置项:`ENGINEERING_KB_API`、`ENGINEERING_KB_SPACE_ID`、`ENGINEERING_KB_KB_ID`、可选 `ENGINEERING_KB_TOKEN`。查询权威技能是知识库 **`opsx-ontology-query`**;本脚本只是薄封装。
|
|
127
|
+
- Continuity=`iteration` 时**必须**带 `--entity-id` + external。
|
|
128
|
+
- **禁止**从本地 `archive/` 抄 UUID 当跨迭代继承源;跨迭代只认 KB current。
|
|
129
|
+
- 若返回 `available=false` / `degraded=true`,记录降级并继续,不得扫本地 archive 兜底。
|
|
130
|
+
- `INHERIT`:unchanged 写继承引用;modified 复用 entity-id + predecessor。`REFERENCE`:只参考,新开身份。
|
|
131
|
+
- reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref`(`requirement-mgmt:scenario:REQ-…:SCN-…`)。
|
|
132
|
+
- 场景级「同 SCN key + 同锚点、不同 entity_id」在写完该 CAP identity 后、确认文档前**当场问** A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`。
|
|
133
|
+
- 优先消费 `reuseBundles[].statements`;`designElements` 只作理解上下文,不能写成 Spec 的 How。
|
|
134
|
+
- 所有知识库内容均为 advisory;与用户确认 / proposal 冲突时以当前确认与 proposal 为准。
|
|
125
135
|
|
|
126
136
|
**【可选】业务知识库检索**:
|
|
127
137
|
术语含义不清且可能影响 spec 准确性时,可调用 **opsx-knowledge** skill。
|
|
@@ -203,7 +213,7 @@ node skywalk-sdd/context-client.cjs --query="<当前 Capability 的自然语言
|
|
|
203
213
|
- 新需求、场景和约束分别使用 `STMT-*`、`AC-*`、`CON-*`;分配规则是同前缀同 Capability 当前最大序号 + 1。
|
|
204
214
|
- 修改已有实体必须复用原 ID;删除实体只写 removal 语义,不得把编号分配给新实体。
|
|
205
215
|
- `added` 必须调用 `semantic-identity --delta-state=added` 生成新的实体 UUID 和版本 UUID;禁止通过复制另一需求的 UUID 创建新实体。
|
|
206
|
-
- `modified/removed` 必须从
|
|
216
|
+
- `modified/removed` 必须从 **KB**(match-requirement / resolve)取得历史 `entity-id` 和直接前序 `version-id`,调用 `semantic-identity --delta-state=<modified|removed> --entity-id=<UUID> --predecessor-version=<UUID>`;实体 UUID 复用,版本 UUID 新建。禁止把本地 archive 目录当跨迭代继承权威。
|
|
207
217
|
- AC 必须嵌套在所属 STMT 下;CON 必须通过 `**constrains**` 显式引用 STMT。
|
|
208
218
|
- 对 `reuseMode=REFERENCE` 的历史候选必须创建新的实体身份;禁止因为内容相似而复用历史 `entity-id`。
|
|
209
219
|
- 对 `reuseMode=INHERIT` 的历史事实,必须使用返回的实体与版本来源完成 unchanged/modified 身份参数校验。
|