kld-sdd 2.6.2 → 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-apply/SKILL.md +2 -1
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +5 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +15 -1
- package/templates/skills/kld-sdd/opsx-check/checklist.md +5 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +21 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +21 -11
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -1
- package/templates/skills/kld-sdd/opsx-task/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-task/reference.md +7 -5
- package/templates/skills/kld-sdd/opsx-tdd-anti-patterns/SKILL.md +5 -0
- package/templates/skills/kld-sdd/opsx-tdd-core/SKILL.md +64 -4
- package/templates/skills/kld-sdd/opsx-tdd-core/checklist.md +3 -1
- package/templates/skills/kld-sdd/opsx-tdd-core/reference.md +13 -9
- package/templates/skills/kld-sdd/opsx-tdd-metrics/SKILL.md +1 -0
- package/templates/skills/kld-sdd/opsx-tdd-review/SKILL.md +39 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/controller-strategy.md +33 -11
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/dag-generation-rules.md +13 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/non-tdd-modules.md +4 -3
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/task-type-definitions.md +7 -0
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
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') {
|