@dotdotgod/cli 0.1.40 → 0.1.42
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 +1 -1
- package/src/docs/traceability.mjs +87 -26
- package/src/graph/metadata.mjs +33 -6
- package/src/graph/store.mjs +1 -1
- package/src/impact/format.mjs +6 -3
- package/src/impact/report.mjs +5 -4
package/package.json
CHANGED
|
@@ -9,11 +9,12 @@ function isSecretIndexPath(path) {
|
|
|
9
9
|
|
|
10
10
|
const TRACEABILITY_PATH_FIELDS = ['implementedBy', 'verifiedBy', 'relatedDocs'];
|
|
11
11
|
const TRACEABILITY_COMMAND_FIELDS = ['verificationCommands'];
|
|
12
|
+
const CONTRACT_ALLOWED_FIELDS = new Set(['id', 'title', 'sections', ...TRACEABILITY_PATH_FIELDS, ...TRACEABILITY_COMMAND_FIELDS]);
|
|
12
13
|
export const TRACEABILITY_LINKS_START = '<!-- dotdotgod:traceability-links:start version=1 source=json-dotdotgod -->';
|
|
13
14
|
export const TRACEABILITY_LINKS_END = '<!-- dotdotgod:traceability-links:end -->';
|
|
14
15
|
|
|
15
16
|
export function traceabilityExample() {
|
|
16
|
-
return 'Expected dotdotgod traceability block:\n\n```json dotdotgod\n{\n "kind": "spec",\n "implementedBy": ["packages/..."],\n "verifiedBy": ["packages/..."],\n "relatedDocs": ["docs/..."],\n "verificationCommands": ["pnpm ..."]\n}\n```\n\nProperty guidance:\n- kind: use "spec" for behavior specs.\n- implementedBy: source/config/script files that implement this spec\'s behavior.\n- verifiedBy: test files or verification docs that check this behavior.\n- relatedDocs: docs with relevant architecture, test strategy, or product context.\n- verificationCommands: commands an agent can run to verify this behavior.';
|
|
17
|
+
return 'Expected dotdotgod traceability block:\n\n```json dotdotgod\n{\n "kind": "spec",\n "implementedBy": ["packages/..."],\n "verifiedBy": ["packages/..."],\n "relatedDocs": ["docs/..."],\n "verificationCommands": ["pnpm ..."],\n "contracts": [{\n "id": "FEATURE-BEHAVIOR-001",\n "title": "Focused behavior contract",\n "sections": ["Focused Behavior"],\n "implementedBy": ["packages/..."],\n "verifiedBy": ["packages/..."],\n "relatedDocs": ["docs/..."],\n "verificationCommands": ["pnpm ..."]\n }]\n}\n```\n\nProperty guidance:\n- kind: use "spec" for behavior specs.\n- implementedBy: source/config/script files that implement this spec\'s behavior.\n- verifiedBy: test files or verification docs that check this behavior.\n- relatedDocs: docs with relevant architecture, test strategy, or product context.\n- verificationCommands: commands an agent can run to verify this behavior.\n- contracts: optional focused behavior contracts; each contract requires non-empty id and title, may include sections as navigation hints, and may include the same path and command fields. Unknown contract fields are validation errors.';
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function lineForOffset(content, offset) {
|
|
@@ -128,6 +129,23 @@ function renderCommandList(commands) {
|
|
|
128
129
|
return ['- Verification commands:', ...commands.map((command) => ` - \`${escapeCodeSpan(command)}\``)];
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
function renderContractList(contracts) {
|
|
133
|
+
if (!Array.isArray(contracts) || contracts.length === 0) return [];
|
|
134
|
+
const lines = ['- Contracts:'];
|
|
135
|
+
for (const contract of contracts) {
|
|
136
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) continue;
|
|
137
|
+
const id = typeof contract.id === 'string' ? contract.id : '<missing-id>';
|
|
138
|
+
const title = typeof contract.title === 'string' ? contract.title : '<missing-title>';
|
|
139
|
+
const details = [];
|
|
140
|
+
if (Array.isArray(contract.sections) && contract.sections.length > 0) details.push(`sections: ${contract.sections.length}`);
|
|
141
|
+
for (const [field, label] of [['implementedBy', 'impl'], ['verifiedBy', 'verify'], ['relatedDocs', 'docs'], ['verificationCommands', 'commands']]) {
|
|
142
|
+
if (Array.isArray(contract[field]) && contract[field].length > 0) details.push(`${label}: ${contract[field].length}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(` - \`${escapeCodeSpan(id)}\` — ${escapeMarkdownText(title)}${details.length > 0 ? ` (${details.join(', ')})` : ''}`);
|
|
145
|
+
}
|
|
146
|
+
return lines;
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
export function renderTraceabilityLinks(data, root, file) {
|
|
132
150
|
const body = [
|
|
133
151
|
'### Traceability Links',
|
|
@@ -136,6 +154,7 @@ export function renderTraceabilityLinks(data, root, file) {
|
|
|
136
154
|
...renderPathList('Verified by', data?.verifiedBy, root, file),
|
|
137
155
|
...renderPathList('Related docs', data?.relatedDocs, root, file),
|
|
138
156
|
...renderCommandList(data?.verificationCommands),
|
|
157
|
+
...renderContractList(data?.contracts),
|
|
139
158
|
];
|
|
140
159
|
return `${TRACEABILITY_LINKS_START}\n<!-- generated: do not edit manually -->\n\n${body.join('\n')}\n\n${TRACEABILITY_LINKS_END}`;
|
|
141
160
|
}
|
|
@@ -168,39 +187,81 @@ export function syncTraceabilityLinksInContent(content, data, root, file) {
|
|
|
168
187
|
return { ok: true, changed: next !== content, content: next };
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return errors;
|
|
190
|
+
function validateTraceabilityPathArray(data, field, add, root, config, { required = true, displayField = field } = {}) {
|
|
191
|
+
if (data[field] === undefined && !required) return;
|
|
192
|
+
if (!Array.isArray(data[field])) {
|
|
193
|
+
add('TRACEABILITY_INVALID_FIELD', displayField, 'must be an array of local relative paths.');
|
|
194
|
+
return;
|
|
177
195
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
add('TRACEABILITY_INVALID_FIELD', field, 'must be an array of local relative paths.');
|
|
196
|
+
for (const value of data[field]) {
|
|
197
|
+
if (!isLocalRelativeTraceabilityPath(value)) {
|
|
198
|
+
add('TRACEABILITY_INVALID_PATH', displayField, `invalid local relative path: ${JSON.stringify(value)}.`);
|
|
182
199
|
continue;
|
|
183
200
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
const area = resolveMemoryArea(value, config);
|
|
190
|
-
if (area?.scope === 'local') {
|
|
191
|
-
add('TRACEABILITY_LOCAL_MEMORY_TARGET', field, `target points to local memory area "${area.id}": ${value}. Traceability must point to shared durable files, not active plans or archive memory.`);
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (!existsSync(resolve(root, value))) add('TRACEABILITY_MISSING_TARGET', field, `target does not exist: ${value}.`);
|
|
201
|
+
const area = resolveMemoryArea(value, config);
|
|
202
|
+
if (area?.scope === 'local') {
|
|
203
|
+
add('TRACEABILITY_LOCAL_MEMORY_TARGET', displayField, `target points to local memory area "${area.id}": ${value}. Traceability must point to shared durable files, not active plans or archive memory.`);
|
|
204
|
+
continue;
|
|
195
205
|
}
|
|
206
|
+
if (!existsSync(resolve(root, value))) add('TRACEABILITY_MISSING_TARGET', displayField, `target does not exist: ${value}.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateTraceabilityCommandArray(data, field, add, { required = true, displayField = field } = {}) {
|
|
211
|
+
if (data[field] === undefined && !required) return;
|
|
212
|
+
if (!Array.isArray(data[field])) {
|
|
213
|
+
add('TRACEABILITY_INVALID_FIELD', displayField, 'must be an array of executable project-local command strings.');
|
|
214
|
+
return;
|
|
196
215
|
}
|
|
197
|
-
for (const
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
for (const value of data[field]) if (typeof value !== 'string' || value.trim().length === 0) add('TRACEABILITY_INVALID_COMMAND', displayField, `invalid command: ${JSON.stringify(value)}.`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateTraceabilityContracts(data, root, add, config) {
|
|
220
|
+
if (data.contracts === undefined) return;
|
|
221
|
+
if (!Array.isArray(data.contracts)) {
|
|
222
|
+
add('TRACEABILITY_INVALID_FIELD', 'contracts', 'must be an array of focused contract objects.');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const ids = new Set();
|
|
226
|
+
for (const [index, contract] of data.contracts.entries()) {
|
|
227
|
+
const base = `contracts[${index}]`;
|
|
228
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) {
|
|
229
|
+
add('TRACEABILITY_INVALID_FIELD', base, 'must be an object with non-empty id and title fields.');
|
|
200
230
|
continue;
|
|
201
231
|
}
|
|
202
|
-
for (const
|
|
232
|
+
for (const field of Object.keys(contract)) {
|
|
233
|
+
if (!CONTRACT_ALLOWED_FIELDS.has(field)) add('TRACEABILITY_INVALID_FIELD', `${base}.${field}`, 'is not a supported contract field.');
|
|
234
|
+
}
|
|
235
|
+
if (typeof contract.id !== 'string' || contract.id.trim().length === 0) {
|
|
236
|
+
add('TRACEABILITY_INVALID_FIELD', `${base}.id`, 'must be a non-empty stable contract ID string.');
|
|
237
|
+
} else if (ids.has(contract.id)) {
|
|
238
|
+
add('TRACEABILITY_INVALID_FIELD', `${base}.id`, `duplicates contract id ${JSON.stringify(contract.id)} in this traceability block.`);
|
|
239
|
+
} else {
|
|
240
|
+
ids.add(contract.id);
|
|
241
|
+
}
|
|
242
|
+
if (typeof contract.title !== 'string' || contract.title.trim().length === 0) add('TRACEABILITY_INVALID_FIELD', `${base}.title`, 'must be a non-empty human-readable contract title.');
|
|
243
|
+
if (contract.sections !== undefined) {
|
|
244
|
+
if (!Array.isArray(contract.sections)) add('TRACEABILITY_INVALID_FIELD', `${base}.sections`, 'must be an array of same-file heading titles.');
|
|
245
|
+
else for (const [sectionIndex, section] of contract.sections.entries()) {
|
|
246
|
+
if (typeof section !== 'string' || section.trim().length === 0) add('TRACEABILITY_INVALID_FIELD', `${base}.sections[${sectionIndex}]`, 'must be a non-empty heading title string.');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const field of TRACEABILITY_PATH_FIELDS) validateTraceabilityPathArray(contract, field, add, root, config, { required: false, displayField: `${base}.${field}` });
|
|
250
|
+
for (const field of TRACEABILITY_COMMAND_FIELDS) validateTraceabilityCommandArray(contract, field, add, { required: false, displayField: `${base}.${field}` });
|
|
203
251
|
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function validateTraceabilityBlock(data, root, file, line = null, config = defaultMemoryConfig()) {
|
|
255
|
+
const errors = [];
|
|
256
|
+
const add = (code, field, message) => errors.push(traceabilityFieldError(rel(root, file), code, field, message, line));
|
|
257
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
258
|
+
add('TRACEABILITY_INVALID_JSON', null, 'Traceability block must be a JSON object.');
|
|
259
|
+
return errors;
|
|
260
|
+
}
|
|
261
|
+
if (data.kind !== 'spec') add('TRACEABILITY_INVALID_KIND', 'kind', 'must be "spec" for behavior specs.');
|
|
262
|
+
for (const field of TRACEABILITY_PATH_FIELDS) validateTraceabilityPathArray(data, field, add, root, config);
|
|
263
|
+
for (const field of TRACEABILITY_COMMAND_FIELDS) validateTraceabilityCommandArray(data, field, add);
|
|
264
|
+
validateTraceabilityContracts(data, root, add, config);
|
|
204
265
|
return errors;
|
|
205
266
|
}
|
|
206
267
|
|
package/src/graph/metadata.mjs
CHANGED
|
@@ -68,18 +68,45 @@ function addTraceabilityTarget(graph, sourceId, root, relation, targetPath, data
|
|
|
68
68
|
addEdge(graph, sourceId, targetId, relation, { confidence: 'CURATED_TRACEABILITY', ...data });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function contractNodeId(filePath, contractId) {
|
|
72
|
+
return `contract:${filePath}#${contractId}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function addTraceabilityCommands(graph, sourceId, filePath, commands, idPrefix) {
|
|
76
|
+
for (const [index, command] of (Array.isArray(commands) ? commands : []).entries()) {
|
|
77
|
+
if (typeof command !== 'string' || command.trim().length === 0) continue;
|
|
78
|
+
const id = `verification_command:${idPrefix}#${index}`;
|
|
79
|
+
addNode(graph, id, 'verification_command', { command, path: filePath });
|
|
80
|
+
addEdge(graph, sourceId, id, 'verification_command', { confidence: 'CURATED_TRACEABILITY' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addTraceabilityContracts(graph, root, fileId, filePath, contracts, config = defaultMemoryConfig()) {
|
|
85
|
+
if (!Array.isArray(contracts)) return;
|
|
86
|
+
for (const contract of contracts) {
|
|
87
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) continue;
|
|
88
|
+
if (typeof contract.id !== 'string' || contract.id.trim().length === 0) continue;
|
|
89
|
+
if (typeof contract.title !== 'string' || contract.title.trim().length === 0) continue;
|
|
90
|
+
const id = contractNodeId(filePath, contract.id);
|
|
91
|
+
const sections = Array.isArray(contract.sections) ? contract.sections.filter((section) => typeof section === 'string' && section.trim().length > 0) : undefined;
|
|
92
|
+
addNode(graph, id, 'contract', { path: filePath, contractId: contract.id, title: contract.title, sections });
|
|
93
|
+
addEdge(graph, fileId, id, 'defines_contract', { confidence: 'CURATED_TRACEABILITY' });
|
|
94
|
+
for (const target of Array.isArray(contract.implementedBy) ? contract.implementedBy : []) addTraceabilityTarget(graph, id, root, 'implemented_by', target, {}, config);
|
|
95
|
+
for (const target of Array.isArray(contract.verifiedBy) ? contract.verifiedBy : []) addTraceabilityTarget(graph, id, root, 'verified_by', target, {}, config);
|
|
96
|
+
for (const target of Array.isArray(contract.relatedDocs) ? contract.relatedDocs : []) addTraceabilityTarget(graph, id, root, 'related_doc', target, {}, config);
|
|
97
|
+
addTraceabilityCommands(graph, id, filePath, Array.isArray(contract.verificationCommands) ? contract.verificationCommands : [], id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
71
101
|
export function addTraceabilityGraph(root, fileId, content, graph, config = defaultMemoryConfig()) {
|
|
102
|
+
const filePath = fileId.replace(/^file:/, '');
|
|
72
103
|
for (const block of extractDotdotgodTraceabilityBlocks(content)) {
|
|
73
104
|
if (block.error || !block.data || block.data.kind !== 'spec') continue;
|
|
74
105
|
for (const target of block.data.implementedBy ?? []) addTraceabilityTarget(graph, fileId, root, 'implemented_by', target, {}, config);
|
|
75
106
|
for (const target of block.data.verifiedBy ?? []) addTraceabilityTarget(graph, fileId, root, 'verified_by', target, {}, config);
|
|
76
107
|
for (const target of block.data.relatedDocs ?? []) addTraceabilityTarget(graph, fileId, root, 'related_doc', target, {}, config);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const id = `verification_command:${fileId}#${index}`;
|
|
80
|
-
addNode(graph, id, 'verification_command', { command, path: fileId.replace(/^file:/, '') });
|
|
81
|
-
addEdge(graph, fileId, id, 'verification_command', { confidence: 'CURATED_TRACEABILITY' });
|
|
82
|
-
}
|
|
108
|
+
addTraceabilityCommands(graph, fileId, filePath, block.data.verificationCommands, fileId);
|
|
109
|
+
addTraceabilityContracts(graph, root, fileId, filePath, block.data.contracts, config);
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
112
|
|
package/src/graph/store.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function graphNodeShard(node) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function graphEdgeShard(edge) {
|
|
17
|
-
if (edge.relation === 'links_to' || edge.relation === 'routes_to' || edge.relation === 'contains_heading' || edge.relation === 'implemented_by' || edge.relation === 'verified_by' || edge.relation === 'related_doc' || edge.relation === 'verification_command' || SEMANTIC_RELATIONS.has(edge.relation)) return 'docs-links';
|
|
17
|
+
if (edge.relation === 'links_to' || edge.relation === 'routes_to' || edge.relation === 'contains_heading' || edge.relation === 'implemented_by' || edge.relation === 'verified_by' || edge.relation === 'related_doc' || edge.relation === 'verification_command' || edge.relation === 'defines_contract' || SEMANTIC_RELATIONS.has(edge.relation)) return 'docs-links';
|
|
18
18
|
if (edge.relation === 'declares_package' || edge.relation === 'declares_script' || edge.relation === 'declares_bin' || edge.relation === 'depends_on' || edge.relation === 'includes_resource') return 'packages';
|
|
19
19
|
return 'other';
|
|
20
20
|
}
|
package/src/impact/format.mjs
CHANGED
|
@@ -6,7 +6,7 @@ function formatCompactImpactGroup(name, group) {
|
|
|
6
6
|
return [
|
|
7
7
|
`${name}:`,
|
|
8
8
|
...items.map((item) => {
|
|
9
|
-
const label = item.path ?? item.command ?? item.name ?? item.target ?? item.id;
|
|
9
|
+
const label = item.type === 'contract' && item.path && item.contractId ? `${item.path}#${item.contractId}${item.title ? ` — ${item.title}` : ''}` : item.path ?? item.command ?? item.name ?? item.target ?? item.id;
|
|
10
10
|
const reasons = (item.reasons ?? []).slice(0, 3).join(', ');
|
|
11
11
|
return `- ${label} (${item.impactScore}; ${reasons})`;
|
|
12
12
|
}),
|
|
@@ -16,7 +16,7 @@ function formatCompactImpactGroup(name, group) {
|
|
|
16
16
|
export function formatCompactImpactOutput(payload, impact) {
|
|
17
17
|
const refreshNote = payload.metadata.cacheRefreshed ? ', refreshed' : '';
|
|
18
18
|
const lines = [`graph impact compact: ${impact.related.length} related node(s), ${impact.omittedRelated ?? 0} omitted (${payload.status.status}${refreshNote} index)`];
|
|
19
|
-
for (const name of ['docs', 'tests', 'files', 'commands', 'events', 'packageResources', 'symbols']) lines.push(...formatCompactImpactGroup(name, impact.groups[name]));
|
|
19
|
+
for (const name of ['docs', 'contracts', 'tests', 'files', 'commands', 'events', 'packageResources', 'symbols']) lines.push(...formatCompactImpactGroup(name, impact.groups[name]));
|
|
20
20
|
return lines.join('\n');
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -37,6 +37,9 @@ function formatYmlImpactItem(item) {
|
|
|
37
37
|
lines.push(` type: ${ymlScalar(item.type)}`);
|
|
38
38
|
lines.push(` score: ${ymlScalar(item.impactScore)}`);
|
|
39
39
|
lines.push(` reasons: ${ymlList((item.reasons ?? []).slice(0, 6))}`);
|
|
40
|
+
if (item.contractId) lines.push(` contract_id: ${ymlScalar(item.contractId)}`);
|
|
41
|
+
if (item.title) lines.push(` title: ${ymlScalar(item.title)}`);
|
|
42
|
+
if (Array.isArray(item.sections) && item.sections.length > 0) lines.push(` sections: ${ymlList(item.sections)}`);
|
|
40
43
|
if (item.retrieval?.area) lines.push(` area: ${ymlScalar(item.retrieval.area)}`);
|
|
41
44
|
if (item.retrieval?.role) lines.push(` role: ${ymlScalar(item.retrieval.role)}`);
|
|
42
45
|
return lines;
|
|
@@ -68,7 +71,7 @@ export function formatYmlImpactOutput(payload, impact) {
|
|
|
68
71
|
` config_source: ${ymlScalar(impact.ranking?.configSource)}`,
|
|
69
72
|
' groups:',
|
|
70
73
|
];
|
|
71
|
-
for (const name of ['docs', 'tests', 'files', 'commands', 'events', 'packageResources', 'symbols']) lines.push(...formatYmlImpactGroup(name, impact.groups[name]));
|
|
74
|
+
for (const name of ['docs', 'contracts', 'tests', 'files', 'commands', 'events', 'packageResources', 'symbols']) lines.push(...formatYmlImpactGroup(name, impact.groups[name]));
|
|
72
75
|
lines.push(' recommended_actions:');
|
|
73
76
|
lines.push(' - "review_related_docs"');
|
|
74
77
|
lines.push(' - "run_related_tests"');
|
package/src/impact/report.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export function buildImpactReport(index, changedPath, limits = {}) {
|
|
|
48
48
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
49
49
|
const seed = `file:${changedPath}`;
|
|
50
50
|
const maxRelated = limits.related ?? 25;
|
|
51
|
-
const groups = { files: { items: [], omitted: 0 }, docs: { items: [], omitted: 0 }, tests: { items: [], omitted: 0 }, commands: { items: [], omitted: 0 }, events: { items: [], omitted: 0 }, packageResources: { items: [], omitted: 0 }, symbols: { items: [], omitted: 0 } };
|
|
51
|
+
const groups = { files: { items: [], omitted: 0 }, docs: { items: [], omitted: 0 }, contracts: { items: [], omitted: 0 }, tests: { items: [], omitted: 0 }, commands: { items: [], omitted: 0 }, events: { items: [], omitted: 0 }, packageResources: { items: [], omitted: 0 }, symbols: { items: [], omitted: 0 } };
|
|
52
52
|
const relatedIds = new Set([seed]);
|
|
53
53
|
const reasons = new Map([[seed, new Set(['changed-file'])]]);
|
|
54
54
|
const addReason = (id, reason) => {
|
|
@@ -85,7 +85,8 @@ export function buildImpactReport(index, changedPath, limits = {}) {
|
|
|
85
85
|
if (area) addImpactItem(groups.docs, { ...item, area }, limits.docs ?? 10);
|
|
86
86
|
else if (isTestPath(item.path)) addImpactItem(groups.tests, item, limits.tests ?? 10);
|
|
87
87
|
else addImpactItem(groups.files, item, limits.files ?? 10);
|
|
88
|
-
} else if (item.type === '
|
|
88
|
+
} else if (item.type === 'contract') addImpactItem(groups.contracts, item, limits.contracts ?? 10);
|
|
89
|
+
else if (item.type === 'test') addImpactItem(groups.tests, item, limits.tests ?? 10);
|
|
89
90
|
else if (item.type === 'command') addImpactItem(groups.commands, item, limits.commands ?? 10);
|
|
90
91
|
else if (item.type === 'event') addImpactItem(groups.events, item, limits.events ?? 10);
|
|
91
92
|
else if (item.type === 'package_resource') addImpactItem(groups.packageResources, item, limits.packageResources ?? 10);
|
|
@@ -101,7 +102,7 @@ function compactScoreBreakdown(scoreBreakdown = {}) {
|
|
|
101
102
|
|
|
102
103
|
export function compactImpactItem(item) {
|
|
103
104
|
const compact = { id: item.id, type: item.type, impactScore: item.impactScore, reasons: (item.reasons ?? []).slice(0, 6), scoreBreakdown: compactScoreBreakdown(item.scoreBreakdown) };
|
|
104
|
-
for (const key of ['path', 'area', 'name', 'command', 'target', 'kind', 'specifier', 'title']) if (item[key] !== undefined) compact[key] = item[key];
|
|
105
|
+
for (const key of ['path', 'area', 'name', 'command', 'target', 'kind', 'specifier', 'title', 'contractId', 'sections']) if (item[key] !== undefined) compact[key] = item[key];
|
|
105
106
|
if (item.retrieval) compact.retrieval = { area: item.retrieval.area, role: item.retrieval.role, priority: item.retrieval.priority, freshness: item.retrieval.freshness };
|
|
106
107
|
return compact;
|
|
107
108
|
}
|
|
@@ -115,7 +116,7 @@ export function buildCompactImpactReport(impact, limits = {}) {
|
|
|
115
116
|
const relatedLimit = limits.related ?? 10;
|
|
116
117
|
const groupLimit = limits.groupItems ?? 5;
|
|
117
118
|
const related = (impact.related ?? []).slice(0, relatedLimit).map(compactImpactItem);
|
|
118
|
-
const groupNames = ['files', 'docs', 'tests', 'commands', 'events', 'packageResources', 'symbols'];
|
|
119
|
+
const groupNames = ['files', 'docs', 'contracts', 'tests', 'commands', 'events', 'packageResources', 'symbols'];
|
|
119
120
|
const groups = Object.fromEntries(groupNames.map((name) => [name, compactImpactGroup(impact.groups?.[name], groupLimit)]));
|
|
120
121
|
const top10 = (impact.related ?? []).filter((item) => item.id !== `file:${impact.changed}`).slice(0, 10);
|
|
121
122
|
return { changed: impact.changed, compact: true, ranking: { method: impact.ranking?.method, preset: impact.ranking?.preset, configSource: impact.ranking?.configSource }, related, groups, omittedRelated: (impact.omittedRelated ?? 0) + Math.max(0, (impact.related?.length ?? 0) - related.length), quality: { rawRelated: impact.related?.length ?? 0, compactRelated: related.length, semanticOnlyTop10: top10.filter((item) => isSemanticOnlyImpactItem(item)).length, curatedTop10: top10.filter((item) => hasCuratedImpactReason(item)).length, lowActionabilityTop10: top10.filter((item) => isLowActionabilityImpactItem(item)).length } };
|