gemstack-ai 1.3.0 → 2.0.0
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/.agents/rules/03-gemstack-security.md +2 -2
- package/.gemstack/state.json +7 -8
- package/CHANGELOG.md +87 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +113 -22
- package/RELEASE_NOTES.md +80 -1
- package/handoff.md +33 -19
- package/package.json +4 -3
- package/scripts/ci/check-package-contents.js +1 -1
- package/scripts/ci/check-secrets.js +84 -0
- package/specs/009-context-capsule/context-capsule.json +4 -4
- package/specs/010-agent-swarm-visual-qa/.gemstack.json +5 -0
- package/specs/010-agent-swarm-visual-qa/closure.json +59 -0
- package/specs/010-agent-swarm-visual-qa/plan.md +759 -0
- package/specs/010-agent-swarm-visual-qa/spec.md +842 -0
- package/specs/010-agent-swarm-visual-qa/swarm.json +49 -0
- package/specs/010-agent-swarm-visual-qa/tasks.md +873 -0
- package/specs/010-agent-swarm-visual-qa/visual-qa.json +41 -0
- package/specs/011-gemstack-2.0-hardening/.gemstack.json +5 -0
- package/specs/011-gemstack-2.0-hardening/closure.json +58 -0
- package/specs/011-gemstack-2.0-hardening/plan.md +210 -0
- package/specs/011-gemstack-2.0-hardening/spec.md +277 -0
- package/specs/011-gemstack-2.0-hardening/tasks.md +59 -0
- package/specs/012-gemstack-2.0-honest-evidence/.gemstack.json +5 -0
- package/specs/012-gemstack-2.0-honest-evidence/closure.json +58 -0
- package/specs/012-gemstack-2.0-honest-evidence/plan.md +202 -0
- package/specs/012-gemstack-2.0-honest-evidence/spec.md +222 -0
- package/specs/012-gemstack-2.0-honest-evidence/tasks.md +99 -0
- package/specs/013-gemstack-2.0-adaptable-sdd/.gemstack.json +9 -0
- package/specs/013-gemstack-2.0-adaptable-sdd/closure.json +58 -0
- package/specs/013-gemstack-2.0-adaptable-sdd/context-capsule.json +227 -0
- package/specs/013-gemstack-2.0-adaptable-sdd/plan.md +179 -0
- package/specs/013-gemstack-2.0-adaptable-sdd/spec.md +212 -0
- package/specs/013-gemstack-2.0-adaptable-sdd/tasks.md +90 -0
- package/specs/014-gemstack-2.0-context-memory/.gemstack.json +9 -0
- package/specs/014-gemstack-2.0-context-memory/closure.json +58 -0
- package/specs/014-gemstack-2.0-context-memory/plan.md +161 -0
- package/specs/014-gemstack-2.0-context-memory/spec.md +163 -0
- package/specs/014-gemstack-2.0-context-memory/tasks.md +79 -0
- package/src/cli.js +11 -0
- package/src/commands/context.js +1 -1
- package/src/commands/doctor.js +18 -0
- package/src/commands/hooks.js +98 -14
- package/src/commands/init.js +1 -1
- package/src/commands/install.js +174 -49
- package/src/commands/spec.js +105 -0
- package/src/commands/swarm.js +111 -0
- package/src/commands/update.js +1 -1
- package/src/commands/verify.js +48 -0
- package/src/commands/visual.js +82 -0
- package/src/lib/backup.js +3 -3
- package/src/lib/closure-context.js +9 -1
- package/src/lib/context-fatigue.js +165 -0
- package/src/lib/contract-amendments.js +109 -0
- package/src/lib/dependency-audit.js +202 -0
- package/src/lib/filesystem-safe.js +85 -15
- package/src/lib/memory-audit.js +121 -0
- package/src/lib/provider-boundary.js +5 -1
- package/src/lib/provider-registry.js +6 -4
- package/src/lib/safety-gates.js +176 -8
- package/src/lib/sdd-rigor.js +181 -0
- package/src/lib/spec-delta.js +194 -0
- package/src/lib/spec-merge.js +168 -0
- package/src/lib/swarm.js +639 -0
- package/src/lib/visual-qa.js +652 -0
- package/template/.agents/rules/03-gemstack-security.md +2 -2
- package/.github/workflows/main-ci.yml +0 -32
- package/.github/workflows/pr-ci.yml +0 -31
- package/.github/workflows/publish.yml +0 -52
- package/.github/workflows/release-readiness.yml +0 -43
- package/gemstack-ai-1.3.0.tgz +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Incremental Spec Delta Engine (Gemstack 2.0 Sprint C)
|
|
5
|
+
* Parses and applies structured ADDED, MODIFIED, REMOVED deltas onto base specifications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts and parses a spec delta block or JSON structure.
|
|
10
|
+
* @param {string|object} input - Markdown content containing gemstack-spec-delta block or parsed delta object.
|
|
11
|
+
* @returns {object} Normalized delta object { base_spec, added, modified, removed }
|
|
12
|
+
*/
|
|
13
|
+
function parseSpecDelta(input) {
|
|
14
|
+
if (typeof input === 'object' && input !== null) {
|
|
15
|
+
return normalizeDelta(input);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof input !== 'string') {
|
|
19
|
+
throw new Error('Delta input must be a string or object');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const blockRegex = /```gemstack-spec-delta\s*([\s\S]*?)```/;
|
|
23
|
+
const match = input.match(blockRegex);
|
|
24
|
+
let parsed;
|
|
25
|
+
|
|
26
|
+
if (match) {
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(match[1].trim());
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const err = new Error(`Error parseando gemstack-spec-delta: ${e.message}`);
|
|
31
|
+
err.code = 'DELTA_PARSE_ERROR';
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// Try parsing raw JSON
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(input.trim());
|
|
38
|
+
} catch (e) {
|
|
39
|
+
const err = new Error('No se encontró bloque gemstack-spec-delta ni JSON válido');
|
|
40
|
+
err.code = 'DELTA_BLOCK_MISSING';
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalizeDelta(parsed);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeDelta(parsed) {
|
|
49
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
50
|
+
const err = new Error('Delta debe ser un objeto');
|
|
51
|
+
err.code = 'INVALID_DELTA_SCHEMA';
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
base_spec: parsed.base_spec || null,
|
|
57
|
+
added: {
|
|
58
|
+
contracts: Array.isArray(parsed.added?.contracts) ? parsed.added.contracts : [],
|
|
59
|
+
requirements: Array.isArray(parsed.added?.requirements) ? parsed.added.requirements : [],
|
|
60
|
+
tests: Array.isArray(parsed.added?.tests) ? parsed.added.tests : []
|
|
61
|
+
},
|
|
62
|
+
modified: {
|
|
63
|
+
contracts: Array.isArray(parsed.modified?.contracts) ? parsed.modified.contracts : [],
|
|
64
|
+
requirements: Array.isArray(parsed.modified?.requirements) ? parsed.modified.requirements : [],
|
|
65
|
+
tests: Array.isArray(parsed.modified?.tests) ? parsed.modified.tests : []
|
|
66
|
+
},
|
|
67
|
+
removed: {
|
|
68
|
+
contracts: Array.isArray(parsed.removed?.contracts) ? parsed.removed.contracts : [],
|
|
69
|
+
requirements: Array.isArray(parsed.removed?.requirements) ? parsed.removed.requirements : [],
|
|
70
|
+
tests: Array.isArray(parsed.removed?.tests) ? parsed.removed.tests : []
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Applies a normalized delta onto a base specification representation.
|
|
77
|
+
* @param {object} baseSpec - { contracts: [], requirements: [], tests: [] }
|
|
78
|
+
* @param {object} delta - Normalized delta from parseSpecDelta
|
|
79
|
+
* @returns {object} Merged specification representation
|
|
80
|
+
*/
|
|
81
|
+
function applySpecDelta(baseSpec, delta) {
|
|
82
|
+
if (!baseSpec || typeof baseSpec !== 'object') {
|
|
83
|
+
throw new Error('baseSpec must be an object');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const normDelta = normalizeDelta(delta);
|
|
87
|
+
|
|
88
|
+
// Clone collections
|
|
89
|
+
const contracts = Array.isArray(baseSpec.contracts) ? JSON.parse(JSON.stringify(baseSpec.contracts)) : [];
|
|
90
|
+
const requirements = Array.isArray(baseSpec.requirements) ? JSON.parse(JSON.stringify(baseSpec.requirements)) : [];
|
|
91
|
+
const tests = Array.isArray(baseSpec.tests) ? JSON.parse(JSON.stringify(baseSpec.tests)) : [];
|
|
92
|
+
|
|
93
|
+
// 1. Process REMOVED
|
|
94
|
+
for (const item of normDelta.removed.contracts) {
|
|
95
|
+
const id = typeof item === 'string' ? item : item.id;
|
|
96
|
+
const idx = contracts.findIndex(c => c.id === id);
|
|
97
|
+
if (idx === -1) {
|
|
98
|
+
const err = new Error(`No se puede remover el contrato "${id}": no existe en la especificación base.`);
|
|
99
|
+
err.code = 'DELTA_TARGET_NOT_FOUND';
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
contracts.splice(idx, 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const item of normDelta.removed.tests) {
|
|
106
|
+
const id = typeof item === 'string' ? item : item.id;
|
|
107
|
+
const idx = tests.findIndex(t => t.id === id);
|
|
108
|
+
if (idx === -1) {
|
|
109
|
+
const err = new Error(`No se puede remover el test canónico "${id}": no existe en la especificación base.`);
|
|
110
|
+
err.code = 'DELTA_TARGET_NOT_FOUND';
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
tests.splice(idx, 1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const item of normDelta.removed.requirements) {
|
|
117
|
+
const id = typeof item === 'string' ? item : item.id;
|
|
118
|
+
const idx = requirements.findIndex(r => (typeof r === 'string' ? r === id : r.id === id));
|
|
119
|
+
if (idx === -1) {
|
|
120
|
+
const err = new Error(`No se puede remover el requisito "${id}": no existe en la especificación base.`);
|
|
121
|
+
err.code = 'DELTA_TARGET_NOT_FOUND';
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
requirements.splice(idx, 1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Process MODIFIED
|
|
128
|
+
for (const item of normDelta.modified.contracts) {
|
|
129
|
+
const idx = contracts.findIndex(c => c.id === item.id);
|
|
130
|
+
if (idx === -1) {
|
|
131
|
+
const err = new Error(`No se puede modificar el contrato "${item.id}": no existe en la especificación base.`);
|
|
132
|
+
err.code = 'DELTA_TARGET_NOT_FOUND';
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
contracts[idx] = { ...contracts[idx], ...item };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const item of normDelta.modified.tests) {
|
|
139
|
+
const idx = tests.findIndex(t => t.id === item.id);
|
|
140
|
+
if (idx === -1) {
|
|
141
|
+
const err = new Error(`No se puede modificar el test "${item.id}": no existe en la especificación base.`);
|
|
142
|
+
err.code = 'DELTA_TARGET_NOT_FOUND';
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
tests[idx] = { ...tests[idx], ...item };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const item of normDelta.modified.requirements) {
|
|
149
|
+
const id = typeof item === 'string' ? item : item.id;
|
|
150
|
+
const idx = requirements.findIndex(r => (typeof r === 'string' ? r === id : r.id === id));
|
|
151
|
+
if (idx === -1) {
|
|
152
|
+
const err = new Error(`No se puede modificar el requisito "${id}": no existe en la especificación base.`);
|
|
153
|
+
err.code = 'DELTA_TARGET_NOT_FOUND';
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
requirements[idx] = typeof item === 'string' ? item : { ...requirements[idx], ...item };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. Process ADDED
|
|
160
|
+
for (const item of normDelta.added.contracts) {
|
|
161
|
+
const existing = contracts.find(c => c.id === item.id);
|
|
162
|
+
if (existing) {
|
|
163
|
+
const err = new Error(`Conflicto al agregar contrato "${item.id}": ya existe en la especificación base.`);
|
|
164
|
+
err.code = 'DELTA_ADD_CONFLICT';
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
contracts.push(item);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const item of normDelta.added.tests) {
|
|
171
|
+
const existing = tests.find(t => t.id === item.id);
|
|
172
|
+
if (existing) {
|
|
173
|
+
const err = new Error(`Conflicto al agregar test "${item.id}": ya existe en la especificación base.`);
|
|
174
|
+
err.code = 'DELTA_ADD_CONFLICT';
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
tests.push(item);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const item of normDelta.added.requirements) {
|
|
181
|
+
requirements.push(item);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
contracts,
|
|
186
|
+
requirements,
|
|
187
|
+
tests
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
parseSpecDelta,
|
|
193
|
+
applySpecDelta
|
|
194
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spec Conflict & Merge Engine (Gemstack 2.0 Sprint C)
|
|
5
|
+
* Detects contract and test ID collisions between branches/specs offline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { extractContractsBlock } = require('./contracts');
|
|
9
|
+
const { extractTestMatrixBlock } = require('./test-matrix');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes input which can be markdown string or parsed object.
|
|
13
|
+
* @param {string|object} specInput
|
|
14
|
+
* @returns {{ contracts: Array, tests: Array, requirements: Array }}
|
|
15
|
+
*/
|
|
16
|
+
function normalizeSpecRepresentation(specInput) {
|
|
17
|
+
if (typeof specInput === 'object' && specInput !== null) {
|
|
18
|
+
return {
|
|
19
|
+
contracts: Array.isArray(specInput.contracts) ? specInput.contracts : [],
|
|
20
|
+
tests: Array.isArray(specInput.tests) ? specInput.tests : [],
|
|
21
|
+
requirements: Array.isArray(specInput.requirements) ? specInput.requirements : []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof specInput === 'string') {
|
|
26
|
+
const contractsRes = extractContractsBlock(specInput);
|
|
27
|
+
const contracts = !contractsRes.isLegacy ? contractsRes.contracts : [];
|
|
28
|
+
|
|
29
|
+
const matrixRes = extractTestMatrixBlock(specInput);
|
|
30
|
+
const tests = !matrixRes.isLegacy ? matrixRes.matrix : [];
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
contracts,
|
|
34
|
+
tests,
|
|
35
|
+
requirements: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { contracts: [], tests: [], requirements: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detects conflicts between two specifications.
|
|
44
|
+
* @param {string|object} specA - Base or local spec
|
|
45
|
+
* @param {string|object} specB - Incoming or remote spec
|
|
46
|
+
* @returns {{ valid: boolean, conflicts: Array<{ type: string, id: string, reason: string, details?: any }> }}
|
|
47
|
+
*/
|
|
48
|
+
function detectSpecConflicts(specA, specB) {
|
|
49
|
+
const repA = normalizeSpecRepresentation(specA);
|
|
50
|
+
const repB = normalizeSpecRepresentation(specB);
|
|
51
|
+
|
|
52
|
+
const conflicts = [];
|
|
53
|
+
|
|
54
|
+
// 1. Detect Contract Collisions
|
|
55
|
+
const contractsMapA = new Map(repA.contracts.map(c => [c.id, c]));
|
|
56
|
+
for (const cB of repB.contracts) {
|
|
57
|
+
if (contractsMapA.has(cB.id)) {
|
|
58
|
+
const cA = contractsMapA.get(cB.id);
|
|
59
|
+
|
|
60
|
+
// Check type collision
|
|
61
|
+
if (cA.type !== cB.type) {
|
|
62
|
+
conflicts.push({
|
|
63
|
+
type: 'CONTRACT_COLLISION',
|
|
64
|
+
id: cB.id,
|
|
65
|
+
reason: `Contract "${cB.id}" has conflicting types: "${cA.type}" vs "${cB.type}".`
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check value collision based on type
|
|
71
|
+
if (cA.type === 'BOOLEAN_INVARIANT' && cA.value !== cB.value) {
|
|
72
|
+
conflicts.push({
|
|
73
|
+
type: 'CONTRACT_COLLISION',
|
|
74
|
+
id: cB.id,
|
|
75
|
+
reason: `Contract "${cB.id}" has conflicting boolean values: ${cA.value} vs ${cB.value}.`
|
|
76
|
+
});
|
|
77
|
+
} else if (cA.type === 'ENUM_SET') {
|
|
78
|
+
const setA = new Set(cA.values || []);
|
|
79
|
+
const setB = new Set(cB.values || []);
|
|
80
|
+
// If values differ materially and neither is empty
|
|
81
|
+
const diffA = [...setA].filter(x => !setB.has(x));
|
|
82
|
+
const diffB = [...setB].filter(x => !setA.has(x));
|
|
83
|
+
if (diffA.length > 0 || diffB.length > 0) {
|
|
84
|
+
conflicts.push({
|
|
85
|
+
type: 'CONTRACT_COLLISION',
|
|
86
|
+
id: cB.id,
|
|
87
|
+
reason: `Contract ENUM_SET "${cB.id}" has divergent enum values.`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Generic deep check
|
|
92
|
+
if (JSON.stringify(cA) !== JSON.stringify(cB)) {
|
|
93
|
+
conflicts.push({
|
|
94
|
+
type: 'CONTRACT_COLLISION',
|
|
95
|
+
id: cB.id,
|
|
96
|
+
reason: `Contract "${cB.id}" has incompatible configuration definitions.`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Detect Test ID Collisions with divergent criteria
|
|
104
|
+
const testsMapA = new Map(repA.tests.map(t => [t.id, t]));
|
|
105
|
+
for (const tB of repB.tests) {
|
|
106
|
+
if (testsMapA.has(tB.id)) {
|
|
107
|
+
const tA = testsMapA.get(tB.id);
|
|
108
|
+
if (
|
|
109
|
+
tA.category !== tB.category ||
|
|
110
|
+
tA.layer !== tB.layer ||
|
|
111
|
+
tA.gate !== tB.gate ||
|
|
112
|
+
tA.description !== tB.description ||
|
|
113
|
+
tA.pass_criteria !== tB.pass_criteria
|
|
114
|
+
) {
|
|
115
|
+
conflicts.push({
|
|
116
|
+
type: 'DUPLICATE_TEST_ID',
|
|
117
|
+
id: tB.id,
|
|
118
|
+
reason: `Canonical test ID "${tB.id}" is declared in both specs with divergent definitions.`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
valid: conflicts.length === 0,
|
|
126
|
+
conflicts
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Merges two specifications if no conflicts exist.
|
|
132
|
+
* @param {string|object} specA
|
|
133
|
+
* @param {string|object} specB
|
|
134
|
+
* @returns {object} Merged representation
|
|
135
|
+
*/
|
|
136
|
+
function mergeSpecs(specA, specB) {
|
|
137
|
+
const conflictReport = detectSpecConflicts(specA, specB);
|
|
138
|
+
if (!conflictReport.valid) {
|
|
139
|
+
const err = new Error(`Conflictos detectados al fusionar especificaciones: ${conflictReport.conflicts.map(c => `${c.type} (${c.id}): ${c.reason}`).join('; ')}`);
|
|
140
|
+
err.code = 'SPEC_MERGE_CONFLICT';
|
|
141
|
+
err.conflicts = conflictReport.conflicts;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const repA = normalizeSpecRepresentation(specA);
|
|
146
|
+
const repB = normalizeSpecRepresentation(specB);
|
|
147
|
+
|
|
148
|
+
// Merge contracts (union by ID)
|
|
149
|
+
const contractsMap = new Map();
|
|
150
|
+
for (const c of repA.contracts) contractsMap.set(c.id, c);
|
|
151
|
+
for (const c of repB.contracts) contractsMap.set(c.id, c);
|
|
152
|
+
|
|
153
|
+
// Merge tests (union by ID)
|
|
154
|
+
const testsMap = new Map();
|
|
155
|
+
for (const t of repA.tests) testsMap.set(t.id, t);
|
|
156
|
+
for (const t of repB.tests) testsMap.set(t.id, t);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
contracts: Array.from(contractsMap.values()),
|
|
160
|
+
tests: Array.from(testsMap.values()),
|
|
161
|
+
requirements: [...repA.requirements, ...repB.requirements]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
detectSpecConflicts,
|
|
167
|
+
mergeSpecs
|
|
168
|
+
};
|