gemstack-ai 1.4.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 +10 -11
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -13
- package/RELEASE_NOTES.md +40 -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/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 +3 -0
- 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/update.js +1 -1
- package/src/commands/verify.js +10 -0
- package/src/lib/backup.js +3 -3
- 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 +2 -2
- package/src/lib/visual-qa.js +162 -9
- 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.4.0.tgz +0 -0
|
@@ -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
|
+
};
|
package/src/lib/swarm.js
CHANGED
|
@@ -234,7 +234,7 @@ function planSwarmWaves(tasks) {
|
|
|
234
234
|
const nextRemaining = [];
|
|
235
235
|
|
|
236
236
|
for (const task of remaining) {
|
|
237
|
-
const taskWrites = (task.write_set || []).map(normalizePath);
|
|
237
|
+
const taskWrites = (task.write_set || []).map(p => normalizePath(p));
|
|
238
238
|
let collides = false;
|
|
239
239
|
|
|
240
240
|
for (const p of taskWrites) {
|
|
@@ -283,7 +283,7 @@ function validateWritePartitions(wave) {
|
|
|
283
283
|
const claimedPaths = new Map(); // path -> task_id
|
|
284
284
|
|
|
285
285
|
for (const t of tasks) {
|
|
286
|
-
const writes = (t.write_set || []).map(normalizePath);
|
|
286
|
+
const writes = (t.write_set || []).map(p => normalizePath(p));
|
|
287
287
|
for (const p of writes) {
|
|
288
288
|
if (claimedPaths.has(p)) {
|
|
289
289
|
const otherTaskId = claimedPaths.get(p);
|
package/src/lib/visual-qa.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
const crypto = require('node:crypto');
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
|
-
const { normalizePath } = require('./hasher');
|
|
13
|
+
const { normalizePath, hashFile } = require('./hasher');
|
|
14
14
|
const { createFinding } = require('./findings');
|
|
15
|
+
const fssafe = require('./filesystem-safe');
|
|
15
16
|
|
|
16
17
|
const VQA_SCHEMA_VERSION = '1.0.0';
|
|
17
18
|
|
|
@@ -266,14 +267,42 @@ function validateBaselineIntegrity(scenario, targetDir) {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
/**
|
|
269
|
-
*
|
|
270
|
+
* Sanitizes and masks sensitive input fields and tokens before visual capture persistence.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} content - HTML or DOM text
|
|
273
|
+
* @returns {string} Sanitized content with sensitive data masked
|
|
274
|
+
*/
|
|
275
|
+
function maskSensitiveFieldsBeforeCapture(content) {
|
|
276
|
+
if (!content || typeof content !== 'string') return content;
|
|
277
|
+
let masked = content;
|
|
278
|
+
|
|
279
|
+
// 1. Password input values
|
|
280
|
+
masked = masked.replace(/(<input\b[^>]*\btype\s*=\s*["']?password["']?[^>]*\bvalue\s*=\s*["'])([^"']*)(["'])/gi, '$1[MASKED_PASSWORD]$3');
|
|
281
|
+
masked = masked.replace(/(<input\b[^>]*\bvalue\s*=\s*["'])([^"']*)(["'][^>]*\btype\s*=\s*["']?password["']?[^>]*>)/gi, '$1[MASKED_PASSWORD]$3');
|
|
282
|
+
|
|
283
|
+
// 2. Sensitive ids or names (password, token, secret, key, credit_card, card)
|
|
284
|
+
masked = masked.replace(/(<input\b[^>]*(?:\bid|\bname)\s*=\s*["']?[^"']*(?:password|token|secret|key|credit_card|card)[^"']*["']?[^>]*\bvalue\s*=\s*["'])([^"']*)(["'])/gi, '$1[MASKED_SENSITIVE]$3');
|
|
285
|
+
masked = masked.replace(/(<input\b[^>]*\bvalue\s*=\s*["'])([^"']*)(["'][^>]*(?:\bid|\bname)\s*=\s*["']?[^"']*(?:password|token|secret|key|credit_card|card)[^"']*["']?[^>]*>)/gi, '$1[MASKED_SENSITIVE]$3');
|
|
286
|
+
|
|
287
|
+
// 3. API Keys and Tokens in attributes or text
|
|
288
|
+
masked = masked.replace(/sk-(?:live_|proj-)?[a-zA-Z0-9_-]{10,}/g, '[MASKED_KEY]');
|
|
289
|
+
|
|
290
|
+
// 4. Credit card numbers (13-19 consecutive digits or grouped with spaces/dashes)
|
|
291
|
+
masked = masked.replace(/\b(?:\d{4}[ -]?){3,4}\d{1,4}\b/g, '[MASKED_CARD]');
|
|
292
|
+
|
|
293
|
+
return masked;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Compares live visual evidence against canonical baseline with honest disk recomputation.
|
|
270
298
|
*
|
|
271
299
|
* @param {object} scenario - Scenario definition
|
|
272
300
|
* @param {object} evidence - Submitted evidence object
|
|
273
301
|
* @param {string} targetDir - Repository target directory
|
|
302
|
+
* @param {object} [options={}] - Additional options (e.g. diffAdapter)
|
|
274
303
|
* @returns {{ status: string, passed: boolean, diff_percentage: number, findings: Array<object> }}
|
|
275
304
|
*/
|
|
276
|
-
function compareVisualEvidence(scenario, evidence, targetDir) {
|
|
305
|
+
function compareVisualEvidence(scenario, evidence, targetDir, options = {}) {
|
|
277
306
|
const findings = [];
|
|
278
307
|
const baseline = scenario.baseline;
|
|
279
308
|
|
|
@@ -299,8 +328,81 @@ function compareVisualEvidence(scenario, evidence, targetDir) {
|
|
|
299
328
|
return { status: 'BASELINE_MISSING', passed: false, diff_percentage: 1.0, findings };
|
|
300
329
|
}
|
|
301
330
|
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
let effectiveLiveHash = evidence.image_sha256;
|
|
332
|
+
let effectiveBaselineHash = baseline.image_sha256;
|
|
333
|
+
|
|
334
|
+
// Recompute hashes directly from disk files if targetDir and paths are available
|
|
335
|
+
if (targetDir) {
|
|
336
|
+
if (baseline.image_path) {
|
|
337
|
+
try {
|
|
338
|
+
const absBaselinePath = fssafe.resolveSafeStrict(targetDir, baseline.image_path);
|
|
339
|
+
if (fs.existsSync(absBaselinePath)) {
|
|
340
|
+
const diskBaselineHash = hashFile(absBaselinePath);
|
|
341
|
+
if (baseline.image_sha256 && diskBaselineHash !== baseline.image_sha256) {
|
|
342
|
+
findings.push(createFinding({
|
|
343
|
+
code: 'VQA_BASELINE_TAMPERED',
|
|
344
|
+
contractId: 'baseline-explicit-update-only',
|
|
345
|
+
phase: 'visual-qa',
|
|
346
|
+
location: baseline.image_path,
|
|
347
|
+
details: `Baseline image "${baseline.image_path}" was modified on disk. Recorded: ${baseline.image_sha256}, Actual: ${diskBaselineHash}.`
|
|
348
|
+
}));
|
|
349
|
+
return { status: 'BASELINE_TAMPERED', passed: false, diff_percentage: 1.0, findings };
|
|
350
|
+
}
|
|
351
|
+
effectiveBaselineHash = diskBaselineHash;
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
findings.push(createFinding({
|
|
355
|
+
code: 'VQA_BASELINE_TAMPERED',
|
|
356
|
+
contractId: 'baseline-explicit-update-only',
|
|
357
|
+
phase: 'visual-qa',
|
|
358
|
+
location: baseline.image_path,
|
|
359
|
+
details: `Error validating baseline path: ${err.message}`
|
|
360
|
+
}));
|
|
361
|
+
return { status: 'BASELINE_TAMPERED', passed: false, diff_percentage: 1.0, findings };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const livePathCandidate = evidence.live_screenshot_path || (evidence.image_path && fs.existsSync(fssafe.resolveSafe(targetDir, evidence.image_path)) ? evidence.image_path : null);
|
|
366
|
+
if (livePathCandidate) {
|
|
367
|
+
try {
|
|
368
|
+
const absLivePath = fssafe.resolveSafeStrict(targetDir, livePathCandidate);
|
|
369
|
+
if (!fs.existsSync(absLivePath)) {
|
|
370
|
+
findings.push(createFinding({
|
|
371
|
+
code: 'VQA_IMAGE_NOT_FOUND',
|
|
372
|
+
contractId: 'visual-evidence-subordinate',
|
|
373
|
+
phase: 'visual-qa',
|
|
374
|
+
location: scenario.scenario_id,
|
|
375
|
+
details: `Live screenshot file not found: ${livePathCandidate}`
|
|
376
|
+
}));
|
|
377
|
+
return { status: 'EVIDENCE_MISSING', passed: false, diff_percentage: 1.0, findings };
|
|
378
|
+
}
|
|
379
|
+
const diskLiveHash = hashFile(absLivePath);
|
|
380
|
+
if (evidence.image_sha256 && evidence.image_sha256 !== diskLiveHash) {
|
|
381
|
+
findings.push(createFinding({
|
|
382
|
+
code: 'VQA_EVIDENCE_HASH_MISMATCH',
|
|
383
|
+
contractId: 'visual-evidence-subordinate',
|
|
384
|
+
phase: 'visual-qa',
|
|
385
|
+
location: scenario.scenario_id,
|
|
386
|
+
details: `Submitted evidence image_sha256 (${evidence.image_sha256}) does not match disk file hash (${diskLiveHash}).`
|
|
387
|
+
}));
|
|
388
|
+
return { status: 'EVIDENCE_HASH_MISMATCH', passed: false, diff_percentage: 1.0, findings };
|
|
389
|
+
}
|
|
390
|
+
effectiveLiveHash = diskLiveHash;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
findings.push(createFinding({
|
|
393
|
+
code: 'VQA_IMAGE_NOT_FOUND',
|
|
394
|
+
contractId: 'visual-evidence-subordinate',
|
|
395
|
+
phase: 'visual-qa',
|
|
396
|
+
location: scenario.scenario_id,
|
|
397
|
+
details: `Error validating live screenshot path: ${err.message}`
|
|
398
|
+
}));
|
|
399
|
+
return { status: 'EVIDENCE_MISSING', passed: false, diff_percentage: 1.0, findings };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Fast-path: SHA-256 match from verified disk hashes
|
|
405
|
+
if (effectiveLiveHash && effectiveBaselineHash && effectiveLiveHash === effectiveBaselineHash) {
|
|
304
406
|
return {
|
|
305
407
|
status: 'PASS',
|
|
306
408
|
passed: true,
|
|
@@ -309,11 +411,52 @@ function compareVisualEvidence(scenario, evidence, targetDir) {
|
|
|
309
411
|
};
|
|
310
412
|
}
|
|
311
413
|
|
|
312
|
-
// Evaluate tolerances
|
|
414
|
+
// Evaluate tolerances and real diff adapter
|
|
313
415
|
const maxDiff = (scenario.tolerances && scenario.tolerances.max_diff_percentage !== undefined)
|
|
314
416
|
? scenario.tolerances.max_diff_percentage
|
|
315
417
|
: 0.00;
|
|
316
418
|
|
|
419
|
+
const diffAdapter = (options && options.diffAdapter) || (evidence && evidence.diffAdapter);
|
|
420
|
+
|
|
421
|
+
if (diffAdapter && typeof diffAdapter.computeDiff === 'function') {
|
|
422
|
+
let baselineBuf = null;
|
|
423
|
+
let liveBuf = null;
|
|
424
|
+
if (targetDir && baseline.image_path) {
|
|
425
|
+
try { baselineBuf = fs.readFileSync(fssafe.resolveSafeStrict(targetDir, baseline.image_path)); } catch {}
|
|
426
|
+
}
|
|
427
|
+
const liveRel = evidence.live_screenshot_path || evidence.image_path;
|
|
428
|
+
if (targetDir && liveRel) {
|
|
429
|
+
try { liveBuf = fs.readFileSync(fssafe.resolveSafeStrict(targetDir, liveRel)); } catch {}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const diffResult = diffAdapter.computeDiff(baselineBuf, liveBuf, scenario.tolerances);
|
|
433
|
+
const observedDiff = typeof diffResult.diff_percentage === 'number' ? diffResult.diff_percentage : 0.0;
|
|
434
|
+
|
|
435
|
+
if (observedDiff > maxDiff) {
|
|
436
|
+
findings.push(createFinding({
|
|
437
|
+
code: 'VQA_VISUAL_REGRESSION',
|
|
438
|
+
contractId: 'visual-evidence-subordinate',
|
|
439
|
+
phase: 'visual-qa',
|
|
440
|
+
location: scenario.scenario_id,
|
|
441
|
+
details: `Visual regression on "${scenario.scenario_id}": observed diff ${observedDiff} exceeds maximum allowed ${maxDiff}.`
|
|
442
|
+
}));
|
|
443
|
+
return {
|
|
444
|
+
status: 'VISUAL_REGRESSION',
|
|
445
|
+
passed: false,
|
|
446
|
+
diff_percentage: observedDiff,
|
|
447
|
+
findings
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
status: 'PASS',
|
|
453
|
+
passed: true,
|
|
454
|
+
diff_percentage: observedDiff,
|
|
455
|
+
findings: []
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// If no diff adapter is provided:
|
|
317
460
|
const observedDiff = typeof evidence.diff_percentage === 'number' ? evidence.diff_percentage : 0.05;
|
|
318
461
|
|
|
319
462
|
if (observedDiff > maxDiff) {
|
|
@@ -332,11 +475,20 @@ function compareVisualEvidence(scenario, evidence, targetDir) {
|
|
|
332
475
|
};
|
|
333
476
|
}
|
|
334
477
|
|
|
478
|
+
// When live screenshot differs from baseline and caller claims low diff without adapter: FAIL-CLOSED UNVERIFIED
|
|
479
|
+
findings.push(createFinding({
|
|
480
|
+
code: 'VQA_DIFF_ENGINE_UNAVAILABLE',
|
|
481
|
+
contractId: 'visual-evidence-subordinate',
|
|
482
|
+
phase: 'visual-qa',
|
|
483
|
+
location: scenario.scenario_id,
|
|
484
|
+
details: `Visual deviation detected on "${scenario.scenario_id}" but no diff engine adapter is available. Status UNVERIFIED.`
|
|
485
|
+
}));
|
|
486
|
+
|
|
335
487
|
return {
|
|
336
|
-
status: '
|
|
337
|
-
passed:
|
|
488
|
+
status: 'UNVERIFIED',
|
|
489
|
+
passed: false,
|
|
338
490
|
diff_percentage: observedDiff,
|
|
339
|
-
findings
|
|
491
|
+
findings
|
|
340
492
|
};
|
|
341
493
|
}
|
|
342
494
|
|
|
@@ -492,6 +644,7 @@ module.exports = {
|
|
|
492
644
|
validateViewport,
|
|
493
645
|
validateEnvironmentMetadata,
|
|
494
646
|
applySelectorMasks,
|
|
647
|
+
maskSensitiveFieldsBeforeCapture,
|
|
495
648
|
validateBaselineIntegrity,
|
|
496
649
|
compareVisualEvidence,
|
|
497
650
|
promoteVisualBaseline,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Gemstack Security Core (
|
|
1
|
+
# Gemstack Security Core (Architecture & Security Gates)
|
|
2
2
|
|
|
3
3
|
## Propósito
|
|
4
|
-
Esta es la Ley de "Seguridad por Diseño". Todo código escrito, planificado o revisado por la IA bajo el marco Gemstack DEBE adherirse a estos principios de blindaje, independientemente del stack tecnológico utilizado. El objetivo es
|
|
4
|
+
Esta es la Ley de "Seguridad por Diseño". Todo código escrito, planificado o revisado por la IA bajo el marco Gemstack DEBE adherirse a estos principios de blindaje, independientemente del stack tecnológico utilizado. El objetivo es establecer controles sistemáticos y verificables frente a vulnerabilidades comunes (OWASP Top 10) desde el momento de la concepción del código.
|
|
5
5
|
|
|
6
6
|
## 1. Cero Exposición de Credenciales (Zero Trust Secrets)
|
|
7
7
|
- **Regla Estricta:** JAMÁS hardcodees contraseñas, llaves de API, secrets de Webhooks o URIs de bases de datos en el código fuente.
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: Main CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
test:
|
|
10
|
-
name: Full CI
|
|
11
|
-
runs-on: ${{ matrix.os }}
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
15
|
-
node-version: [18.x, 20.x]
|
|
16
|
-
steps:
|
|
17
|
-
- name: Checkout repository
|
|
18
|
-
uses: actions/checkout@v4
|
|
19
|
-
- name: Setup Node.js
|
|
20
|
-
uses: actions/setup-node@v4
|
|
21
|
-
with:
|
|
22
|
-
node-version: ${{ matrix.node-version }}
|
|
23
|
-
- name: Install Dependencies
|
|
24
|
-
run: npm install
|
|
25
|
-
- name: Run Native Tests
|
|
26
|
-
run: npm test
|
|
27
|
-
- name: Run CI Suite
|
|
28
|
-
run: npm run ci:all
|
|
29
|
-
- name: Run Demo Smoke
|
|
30
|
-
run: npm run ci:demo
|
|
31
|
-
- name: Validate Package Build
|
|
32
|
-
run: npm run pack:dry
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
name: PR CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
test:
|
|
10
|
-
name: Quick CI
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
node-version: [18.x, 20.x]
|
|
15
|
-
steps:
|
|
16
|
-
- name: Checkout repository
|
|
17
|
-
uses: actions/checkout@v4
|
|
18
|
-
- name: Setup Node.js
|
|
19
|
-
uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: ${{ matrix.node-version }}
|
|
22
|
-
- name: Install Dependencies
|
|
23
|
-
run: npm install
|
|
24
|
-
- name: Run Native Tests
|
|
25
|
-
run: npm test
|
|
26
|
-
- name: Run CI Suite
|
|
27
|
-
run: npm run ci:all
|
|
28
|
-
- name: Run Demo Smoke
|
|
29
|
-
run: npm run ci:demo
|
|
30
|
-
- name: Validate Package Build
|
|
31
|
-
run: npm run pack:dry
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
name: Publish Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*'
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: write # Requerido para crear GitHub Releases
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
name: Publish to NPM and GitHub Releases
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
|
|
16
|
-
steps:
|
|
17
|
-
- name: Checkout code
|
|
18
|
-
uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Setup Node.js
|
|
21
|
-
uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: '20'
|
|
24
|
-
registry-url: 'https://registry.npmjs.org'
|
|
25
|
-
|
|
26
|
-
- name: Install Dependencies
|
|
27
|
-
run: npm ci || npm install
|
|
28
|
-
|
|
29
|
-
- name: Security Gate - Run CI All
|
|
30
|
-
run: npm run ci:all
|
|
31
|
-
|
|
32
|
-
- name: Pack Package
|
|
33
|
-
run: npm pack
|
|
34
|
-
id: pack
|
|
35
|
-
|
|
36
|
-
- name: Publish to NPM
|
|
37
|
-
run: npm publish --access public
|
|
38
|
-
env:
|
|
39
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
40
|
-
|
|
41
|
-
- name: Create GitHub Release
|
|
42
|
-
env:
|
|
43
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
44
|
-
run: |
|
|
45
|
-
# Obtener el nombre del archivo tarball generado por npm pack
|
|
46
|
-
TARBALL=$(ls gemstack-*.tgz)
|
|
47
|
-
|
|
48
|
-
# Crear el Release oficial de GitHub usando el CLI nativo 'gh'
|
|
49
|
-
gh release create ${{ github.ref_name }} \
|
|
50
|
-
--title "Release ${{ github.ref_name }}" \
|
|
51
|
-
--notes-file RELEASE_NOTES.md \
|
|
52
|
-
$TARBALL
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
name: Release Readiness
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
|
|
6
|
-
jobs:
|
|
7
|
-
validate-and-pack:
|
|
8
|
-
name: Validate and Pack
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
steps:
|
|
11
|
-
- name: Checkout repository
|
|
12
|
-
uses: actions/checkout@v4
|
|
13
|
-
- name: Setup Node.js
|
|
14
|
-
uses: actions/setup-node@v4
|
|
15
|
-
with:
|
|
16
|
-
node-version: 20.x
|
|
17
|
-
- name: Install Dependencies
|
|
18
|
-
run: npm install
|
|
19
|
-
- name: Run Native Tests
|
|
20
|
-
run: npm test
|
|
21
|
-
- name: Run CI Suite
|
|
22
|
-
run: npm run ci:all
|
|
23
|
-
- name: Run Demo Smoke
|
|
24
|
-
run: npm run ci:demo
|
|
25
|
-
- name: Pack NPM Tarball
|
|
26
|
-
run: npm pack
|
|
27
|
-
- name: Test Tarball in Dummy Project
|
|
28
|
-
run: |
|
|
29
|
-
mkdir ../gemstack-dummy
|
|
30
|
-
cd ../gemstack-dummy
|
|
31
|
-
npm init -y
|
|
32
|
-
npm install ../Gemstack/gemstack-*.tgz
|
|
33
|
-
npx gemstack --help
|
|
34
|
-
npx gemstack init --dry-run
|
|
35
|
-
npx gemstack init --yes
|
|
36
|
-
npx gemstack doctor
|
|
37
|
-
npx gemstack update --dry-run
|
|
38
|
-
- name: Upload NPM Tarball
|
|
39
|
-
uses: actions/upload-artifact@v4
|
|
40
|
-
with:
|
|
41
|
-
name: gemstack-npm-tarball
|
|
42
|
-
path: gemstack-*.tgz
|
|
43
|
-
if-no-files-found: error
|
package/gemstack-ai-1.4.0.tgz
DELETED
|
Binary file
|