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.
Files changed (71) hide show
  1. package/.agents/rules/03-gemstack-security.md +2 -2
  2. package/.gemstack/state.json +7 -8
  3. package/CHANGELOG.md +87 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +113 -22
  6. package/RELEASE_NOTES.md +80 -1
  7. package/handoff.md +33 -19
  8. package/package.json +4 -3
  9. package/scripts/ci/check-package-contents.js +1 -1
  10. package/scripts/ci/check-secrets.js +84 -0
  11. package/specs/009-context-capsule/context-capsule.json +4 -4
  12. package/specs/010-agent-swarm-visual-qa/.gemstack.json +5 -0
  13. package/specs/010-agent-swarm-visual-qa/closure.json +59 -0
  14. package/specs/010-agent-swarm-visual-qa/plan.md +759 -0
  15. package/specs/010-agent-swarm-visual-qa/spec.md +842 -0
  16. package/specs/010-agent-swarm-visual-qa/swarm.json +49 -0
  17. package/specs/010-agent-swarm-visual-qa/tasks.md +873 -0
  18. package/specs/010-agent-swarm-visual-qa/visual-qa.json +41 -0
  19. package/specs/011-gemstack-2.0-hardening/.gemstack.json +5 -0
  20. package/specs/011-gemstack-2.0-hardening/closure.json +58 -0
  21. package/specs/011-gemstack-2.0-hardening/plan.md +210 -0
  22. package/specs/011-gemstack-2.0-hardening/spec.md +277 -0
  23. package/specs/011-gemstack-2.0-hardening/tasks.md +59 -0
  24. package/specs/012-gemstack-2.0-honest-evidence/.gemstack.json +5 -0
  25. package/specs/012-gemstack-2.0-honest-evidence/closure.json +58 -0
  26. package/specs/012-gemstack-2.0-honest-evidence/plan.md +202 -0
  27. package/specs/012-gemstack-2.0-honest-evidence/spec.md +222 -0
  28. package/specs/012-gemstack-2.0-honest-evidence/tasks.md +99 -0
  29. package/specs/013-gemstack-2.0-adaptable-sdd/.gemstack.json +9 -0
  30. package/specs/013-gemstack-2.0-adaptable-sdd/closure.json +58 -0
  31. package/specs/013-gemstack-2.0-adaptable-sdd/context-capsule.json +227 -0
  32. package/specs/013-gemstack-2.0-adaptable-sdd/plan.md +179 -0
  33. package/specs/013-gemstack-2.0-adaptable-sdd/spec.md +212 -0
  34. package/specs/013-gemstack-2.0-adaptable-sdd/tasks.md +90 -0
  35. package/specs/014-gemstack-2.0-context-memory/.gemstack.json +9 -0
  36. package/specs/014-gemstack-2.0-context-memory/closure.json +58 -0
  37. package/specs/014-gemstack-2.0-context-memory/plan.md +161 -0
  38. package/specs/014-gemstack-2.0-context-memory/spec.md +163 -0
  39. package/specs/014-gemstack-2.0-context-memory/tasks.md +79 -0
  40. package/src/cli.js +11 -0
  41. package/src/commands/context.js +1 -1
  42. package/src/commands/doctor.js +18 -0
  43. package/src/commands/hooks.js +98 -14
  44. package/src/commands/init.js +1 -1
  45. package/src/commands/install.js +174 -49
  46. package/src/commands/spec.js +105 -0
  47. package/src/commands/swarm.js +111 -0
  48. package/src/commands/update.js +1 -1
  49. package/src/commands/verify.js +48 -0
  50. package/src/commands/visual.js +82 -0
  51. package/src/lib/backup.js +3 -3
  52. package/src/lib/closure-context.js +9 -1
  53. package/src/lib/context-fatigue.js +165 -0
  54. package/src/lib/contract-amendments.js +109 -0
  55. package/src/lib/dependency-audit.js +202 -0
  56. package/src/lib/filesystem-safe.js +85 -15
  57. package/src/lib/memory-audit.js +121 -0
  58. package/src/lib/provider-boundary.js +5 -1
  59. package/src/lib/provider-registry.js +6 -4
  60. package/src/lib/safety-gates.js +176 -8
  61. package/src/lib/sdd-rigor.js +181 -0
  62. package/src/lib/spec-delta.js +194 -0
  63. package/src/lib/spec-merge.js +168 -0
  64. package/src/lib/swarm.js +639 -0
  65. package/src/lib/visual-qa.js +652 -0
  66. package/template/.agents/rules/03-gemstack-security.md +2 -2
  67. package/.github/workflows/main-ci.yml +0 -32
  68. package/.github/workflows/pr-ci.yml +0 -31
  69. package/.github/workflows/publish.yml +0 -52
  70. package/.github/workflows/release-readiness.yml +0 -43
  71. 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
+ };