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
package/src/commands/verify.js
CHANGED
|
@@ -87,6 +87,16 @@ module.exports = async (flags) => {
|
|
|
87
87
|
} else {
|
|
88
88
|
logger.ok('Sección inmutable "4. Intentos fallidos" preservada.');
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
// Cross-Audit con Git Log (Gemstack 2.0 Sprint D)
|
|
92
|
+
const { crossAuditMemoryWithGit } = require('../lib/memory-audit');
|
|
93
|
+
const memAudit = crossAuditMemoryWithGit(targetDir);
|
|
94
|
+
if (!memAudit.valid && memAudit.unrecorded_commits.length > 0) {
|
|
95
|
+
logger.warn(`Detectados commits recientes no registrados en handoff.md: ${memAudit.unrecorded_commits.map(c => c.hash).join(', ')}`);
|
|
96
|
+
totalWarnings++;
|
|
97
|
+
} else {
|
|
98
|
+
logger.ok('Memoria cruzada (handoff.md <-> git log) verificada.');
|
|
99
|
+
}
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
// 3. Consistencia de Estado Local (.gemstack/state.json)
|
|
@@ -430,6 +440,44 @@ module.exports = async (flags) => {
|
|
|
430
440
|
logger.ok('Sin spec activa configurada para verificación de context capsule.');
|
|
431
441
|
}
|
|
432
442
|
|
|
443
|
+
// 5.3 Verificación de Swarm Manifest (Upgrade E - Read-Only)
|
|
444
|
+
logger.info('--- 5.3 Verificación de Swarm Manifest & Partition Safety (Read-Only) ---');
|
|
445
|
+
if (loadedState && loadedState.active_spec) {
|
|
446
|
+
const { validateSwarmManifest } = require('../lib/swarm');
|
|
447
|
+
const swarmResult = validateSwarmManifest(targetDir, loadedState.active_spec);
|
|
448
|
+
if (swarmResult.valid) {
|
|
449
|
+
logger.ok(`Swarm manifest verificado y sin colisiones (${loadedState.active_spec}/swarm.json).`);
|
|
450
|
+
} else if (swarmResult.state === 'MISSING') {
|
|
451
|
+
logger.info(`[LEGACY] No se detectó swarm.json en "${loadedState.active_spec}" (Modo Legacy Swarm-Free).`);
|
|
452
|
+
} else {
|
|
453
|
+
for (const f of swarmResult.findings) {
|
|
454
|
+
logger.error(`[${f.code}] ${f.details || f.message}`);
|
|
455
|
+
totalErrors++;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
logger.ok('Sin spec activa configurada para verificación de swarm manifest.');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 5.4 Verificación de Visual QA Manifest & Baselines (Upgrade E - Read-Only)
|
|
463
|
+
logger.info('--- 5.4 Verificación de Visual QA Manifest & Baselines (Read-Only) ---');
|
|
464
|
+
if (loadedState && loadedState.active_spec) {
|
|
465
|
+
const { validateVisualManifest } = require('../lib/visual-qa');
|
|
466
|
+
const vqaResult = validateVisualManifest(targetDir, loadedState.active_spec);
|
|
467
|
+
if (vqaResult.valid) {
|
|
468
|
+
logger.ok(`Visual QA manifest y baselines íntegros (${loadedState.active_spec}/visual-qa.json).`);
|
|
469
|
+
} else if (vqaResult.state === 'MISSING') {
|
|
470
|
+
logger.info(`[LEGACY] No se detectó visual-qa.json en "${loadedState.active_spec}" (Modo Legacy Visual-Free).`);
|
|
471
|
+
} else {
|
|
472
|
+
for (const f of vqaResult.findings) {
|
|
473
|
+
logger.error(`[${f.code}] ${f.details || f.message}`);
|
|
474
|
+
totalErrors++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
logger.ok('Sin spec activa configurada para verificación de visual QA.');
|
|
479
|
+
}
|
|
480
|
+
|
|
433
481
|
// 6. Seguridad Local y Anti-Silent Failures en Tests
|
|
434
482
|
logger.info('--- 6/6 Verificación de Seguridad y Test Runners ---');
|
|
435
483
|
const envPath = fssafe.resolveSafe(targetDir, '.env');
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemstack Visual QA Command Handler (Upgrade E)
|
|
3
|
+
*
|
|
4
|
+
* Implements "gemstack vqa validate" and "gemstack vqa promote" subcommands.
|
|
5
|
+
* Read-only validation and explicit baseline promotion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const logger = require('../lib/logger');
|
|
11
|
+
const fssafe = require('../lib/filesystem-safe');
|
|
12
|
+
const { readState } = require('../lib/state');
|
|
13
|
+
const { validateVisualManifest, promoteVisualBaseline } = require('../lib/visual-qa');
|
|
14
|
+
|
|
15
|
+
async function visualCommand(args = [], flags = {}) {
|
|
16
|
+
const subcommand = args[0] || 'validate';
|
|
17
|
+
const targetDir = flags.target ? path.resolve(flags.target) : process.cwd();
|
|
18
|
+
|
|
19
|
+
let state;
|
|
20
|
+
try {
|
|
21
|
+
state = readState(targetDir);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
logger.error(`Error loading .gemstack/state.json: ${e.message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const activeSpec = state.active_spec;
|
|
28
|
+
if (!activeSpec) {
|
|
29
|
+
if (flags.json) {
|
|
30
|
+
console.log(JSON.stringify({ status: 'NO_ACTIVE_SPEC', message: 'No active feature spec set.' }));
|
|
31
|
+
} else {
|
|
32
|
+
logger.info('Sin spec activa pendiente. Opera en modo legacy.');
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (subcommand === 'validate') {
|
|
38
|
+
const res = validateVisualManifest(targetDir, activeSpec);
|
|
39
|
+
if (flags.json) {
|
|
40
|
+
console.log(JSON.stringify(res, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (res.state === 'MISSING') {
|
|
45
|
+
logger.info(`[LEGACY] No se detectó visual-qa.json en "${activeSpec}".`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (res.valid) {
|
|
50
|
+
logger.ok(`Visual QA manifest validado con éxito: ${activeSpec}/visual-qa.json.`);
|
|
51
|
+
} else {
|
|
52
|
+
for (const f of res.findings) {
|
|
53
|
+
logger.error(`[${f.code}] ${f.details}`);
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
} else if (subcommand === 'promote') {
|
|
58
|
+
const scenarioId = args[1];
|
|
59
|
+
const liveImagePath = args[2];
|
|
60
|
+
|
|
61
|
+
if (!scenarioId || !liveImagePath) {
|
|
62
|
+
logger.error('Usage: gemstack vqa promote <scenario-id> <live-image-path>');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const res = promoteVisualBaseline(targetDir, activeSpec, scenarioId, liveImagePath);
|
|
68
|
+
logger.ok(`Baseline promoted for scenario "${scenarioId}". Updated ${activeSpec}/visual-qa.json.`);
|
|
69
|
+
if (flags.json) {
|
|
70
|
+
console.log(JSON.stringify(res, null, 2));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error(`Failed to promote baseline: ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
logger.error(`Unknown visual subcommand: "${subcommand}". Use "validate" or "promote".`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = visualCommand;
|
package/src/lib/backup.js
CHANGED
|
@@ -5,12 +5,12 @@ const logger = require('./logger');
|
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
backupFile: (targetDir, relativeFilePath, dryRun, sessionTimestamp) => {
|
|
8
|
-
const fullPath = fssafe.
|
|
8
|
+
const fullPath = fssafe.resolveSafeStrict(targetDir, relativeFilePath);
|
|
9
9
|
if (!fs.existsSync(fullPath)) return;
|
|
10
10
|
|
|
11
11
|
const timestamp = sessionTimestamp || new Date().toISOString().replace(/[:.]/g, '-');
|
|
12
|
-
const backupDir = fssafe.
|
|
13
|
-
const backupDest = fssafe.
|
|
12
|
+
const backupDir = fssafe.resolveSafeStrict(targetDir, `.gemstack/backups/${timestamp}`);
|
|
13
|
+
const backupDest = fssafe.resolveSafeStrict(backupDir, relativeFilePath);
|
|
14
14
|
|
|
15
15
|
logger.info(`Backup: ${relativeFilePath}`);
|
|
16
16
|
if (!dryRun) {
|
|
@@ -220,7 +220,7 @@ function parseTaskMetadata(tasksContent) {
|
|
|
220
220
|
const lines = normalized.split('\n');
|
|
221
221
|
const tasks = [];
|
|
222
222
|
|
|
223
|
-
const taskHeaderRegex = /^[*-]\s+\[[ xX]\]\s+\*\*(
|
|
223
|
+
const taskHeaderRegex = /^[*-]\s+\[[ xX]\]\s+\*\*([A-Za-z0-9_-]+):\s*(.*?)\*\*/;
|
|
224
224
|
let currentTask = null;
|
|
225
225
|
|
|
226
226
|
for (const line of lines) {
|
|
@@ -343,6 +343,14 @@ function resolveRelevantFiles(rootPath, featureDir, planBindings, taskList, plan
|
|
|
343
343
|
if (fs.existsSync(path.join(rootPath, featureCapsule))) filesSet.add(featureCapsule);
|
|
344
344
|
if (fs.existsSync(path.join(rootPath, '.gemstack/context-capsule.json'))) filesSet.add('.gemstack/context-capsule.json');
|
|
345
345
|
|
|
346
|
+
const featureSwarm = (relFeature + '/swarm.json').replace(/^\.\//, '');
|
|
347
|
+
if (fs.existsSync(path.join(rootPath, featureSwarm))) filesSet.add(featureSwarm);
|
|
348
|
+
if (fs.existsSync(path.join(rootPath, '.gemstack/swarm.json'))) filesSet.add('.gemstack/swarm.json');
|
|
349
|
+
|
|
350
|
+
const featureVqa = (relFeature + '/visual-qa.json').replace(/^\.\//, '');
|
|
351
|
+
if (fs.existsSync(path.join(rootPath, featureVqa))) filesSet.add(featureVqa);
|
|
352
|
+
if (fs.existsSync(path.join(rootPath, '.gemstack/visual-qa.json'))) filesSet.add('.gemstack/visual-qa.json');
|
|
353
|
+
|
|
346
354
|
for (const b of (planBindings || [])) {
|
|
347
355
|
if (b.file && fs.existsSync(path.join(rootPath, b.file))) {
|
|
348
356
|
filesSet.add(b.file);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context Fatigue & Noise Pruning Engine (Gemstack 2.0 Sprint D)
|
|
5
|
+
* Monitors accumulated token load, detects redundancy, and prunes ephemeral noise.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TOKEN_THRESHOLD = 16000;
|
|
9
|
+
const DEFAULT_REDUNDANCY_THRESHOLD = 0.4;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fast conservative token estimation (~4 characters per token).
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function estimateTokens(text) {
|
|
17
|
+
if (!text || typeof text !== 'string') return 0;
|
|
18
|
+
return Math.ceil(text.length / 4);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calculates redundancy ratio based on repeated content blocks and duplicate messages.
|
|
23
|
+
* @param {Array<string|object>} messages
|
|
24
|
+
* @returns {number} Float between 0.0 and 1.0
|
|
25
|
+
*/
|
|
26
|
+
function computeRedundancyRatio(messages) {
|
|
27
|
+
if (!Array.isArray(messages) || messages.length <= 1) return 0;
|
|
28
|
+
|
|
29
|
+
const texts = messages.map(m => (typeof m === 'string' ? m : (m.content || m.text || JSON.stringify(m))));
|
|
30
|
+
const totalLength = texts.reduce((acc, t) => acc + t.length, 0);
|
|
31
|
+
if (totalLength === 0) return 0;
|
|
32
|
+
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
let duplicateLength = 0;
|
|
35
|
+
|
|
36
|
+
for (const t of texts) {
|
|
37
|
+
const trimmed = t.trim();
|
|
38
|
+
if (seen.has(trimmed)) {
|
|
39
|
+
duplicateLength += trimmed.length;
|
|
40
|
+
} else {
|
|
41
|
+
seen.add(trimmed);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Number((duplicateLength / totalLength).toFixed(4));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detects whether the current context window suffers from token fatigue or high redundancy.
|
|
50
|
+
* @param {Array<string|object>} messages
|
|
51
|
+
* @param {object} options - { tokenThreshold?: number, redundancyThreshold?: number }
|
|
52
|
+
* @returns {{ fatigue: boolean, total_tokens: number, token_limit: number, redundancy_ratio: number, reason?: string }}
|
|
53
|
+
*/
|
|
54
|
+
function detectContextFatigue(messages, options = {}) {
|
|
55
|
+
const tokenThreshold = options.tokenThreshold || DEFAULT_TOKEN_THRESHOLD;
|
|
56
|
+
const redundancyThreshold = options.redundancyThreshold || DEFAULT_REDUNDANCY_THRESHOLD;
|
|
57
|
+
|
|
58
|
+
if (!Array.isArray(messages)) {
|
|
59
|
+
return {
|
|
60
|
+
fatigue: false,
|
|
61
|
+
total_tokens: 0,
|
|
62
|
+
token_limit: tokenThreshold,
|
|
63
|
+
redundancy_ratio: 0
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const texts = messages.map(m => (typeof m === 'string' ? m : (m.content || m.text || JSON.stringify(m))));
|
|
68
|
+
const totalTokens = texts.reduce((acc, t) => acc + estimateTokens(t), 0);
|
|
69
|
+
const redundancyRatio = computeRedundancyRatio(messages);
|
|
70
|
+
|
|
71
|
+
let fatigue = false;
|
|
72
|
+
const reasons = [];
|
|
73
|
+
|
|
74
|
+
if (totalTokens > tokenThreshold) {
|
|
75
|
+
fatigue = true;
|
|
76
|
+
reasons.push(`Token count (${totalTokens}) exceeds threshold (${tokenThreshold})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (redundancyRatio > redundancyThreshold) {
|
|
80
|
+
fatigue = true;
|
|
81
|
+
reasons.push(`Redundancy ratio (${(redundancyRatio * 100).toFixed(1)}%) exceeds limit (${(redundancyThreshold * 100).toFixed(1)}%)`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
fatigue,
|
|
86
|
+
total_tokens: totalTokens,
|
|
87
|
+
token_limit: tokenThreshold,
|
|
88
|
+
redundancy_ratio: redundancyRatio,
|
|
89
|
+
reason: reasons.length > 0 ? reasons.join('; ') : undefined
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Deterministically prunes ephemeral noise, duplicated tool outputs, and redundant dialogue.
|
|
95
|
+
* Guarantees preservation of architectural contracts, state definitions, and critical decisions.
|
|
96
|
+
* @param {Array<string|object>} messages
|
|
97
|
+
* @param {object} options - { retainTail?: number }
|
|
98
|
+
* @returns {Array<string|object>} Pruned message array
|
|
99
|
+
*/
|
|
100
|
+
function pruneContextNoise(messages, options = {}) {
|
|
101
|
+
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
102
|
+
const retainTail = options.retainTail || 3;
|
|
103
|
+
|
|
104
|
+
const isContractOrState = (text) => {
|
|
105
|
+
return (
|
|
106
|
+
text.includes('gemstack-contracts') ||
|
|
107
|
+
text.includes('gemstack-inherited-contracts') ||
|
|
108
|
+
text.includes('gemstack-test-matrix') ||
|
|
109
|
+
text.includes('phase_hashes') ||
|
|
110
|
+
text.includes('handoff.md') ||
|
|
111
|
+
text.includes('### Intentos fallidos') ||
|
|
112
|
+
text.includes('4. Intentos fallidos')
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isEphemeralNoise = (text) => {
|
|
117
|
+
const trimmed = text.trim();
|
|
118
|
+
if (trimmed.length < 5) return true;
|
|
119
|
+
if (/^(ok|done|entendido|continuando|esperando|running)(?:\.{1,3})?$/i.test(trimmed)) return true;
|
|
120
|
+
return false;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const seenHashes = new Set();
|
|
124
|
+
const pruned = [];
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < messages.length; i++) {
|
|
127
|
+
const m = messages[i];
|
|
128
|
+
const text = typeof m === 'string' ? m : (m.content || m.text || JSON.stringify(m));
|
|
129
|
+
|
|
130
|
+
// Always preserve contracts, invariants and state
|
|
131
|
+
if (isContractOrState(text)) {
|
|
132
|
+
pruned.push(m);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Always preserve recent tail messages
|
|
137
|
+
if (i >= messages.length - retainTail) {
|
|
138
|
+
pruned.push(m);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Skip pure ephemeral chit-chat
|
|
143
|
+
if (isEphemeralNoise(text)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// De-duplicate identical intermediate messages
|
|
148
|
+
const trimmed = text.trim();
|
|
149
|
+
if (seenHashes.has(trimmed)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
seenHashes.add(trimmed);
|
|
153
|
+
|
|
154
|
+
pruned.push(m);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return pruned;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
estimateTokens,
|
|
162
|
+
computeRedundancyRatio,
|
|
163
|
+
detectContextFatigue,
|
|
164
|
+
pruneContextNoise
|
|
165
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formal Contract Amendment Engine (Gemstack 2.0 Sprint C)
|
|
5
|
+
* Replaces silent contract mutations with signed, auditable amendment records.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
const REQUIRED_AMENDMENT_FIELDS = ['amendment_id', 'contract_id', 'version', 'reason', 'approved_by', 'signature'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Computes deterministic signature for an amendment.
|
|
14
|
+
* @param {object} amendment - { amendment_id, contract_id, version, reason, approved_by }
|
|
15
|
+
* @param {string|null} secret - Optional HMAC secret
|
|
16
|
+
* @returns {string} Hex-encoded SHA-256 or HMAC-SHA256 digest
|
|
17
|
+
*/
|
|
18
|
+
function computeAmendmentSignature(amendment, secret = null) {
|
|
19
|
+
if (!amendment || typeof amendment !== 'object') {
|
|
20
|
+
throw new Error('Amendment must be an object');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const payload = [
|
|
24
|
+
String(amendment.amendment_id || ''),
|
|
25
|
+
String(amendment.contract_id || ''),
|
|
26
|
+
String(amendment.version || ''),
|
|
27
|
+
String(amendment.reason || '').trim(),
|
|
28
|
+
String(amendment.approved_by || '').trim()
|
|
29
|
+
].join('|');
|
|
30
|
+
|
|
31
|
+
if (secret && typeof secret === 'string' && secret.length > 0) {
|
|
32
|
+
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validates that any changes from upstreamContracts to currentContracts are justified by signed amendments.
|
|
40
|
+
* @param {Array<object>} upstreamContracts
|
|
41
|
+
* @param {Array<object>} currentContracts
|
|
42
|
+
* @param {Array<object>} amendments
|
|
43
|
+
* @param {object} options - { secret?: string }
|
|
44
|
+
* @returns {{ valid: boolean, code?: string, error?: string, verified_amendments?: number }}
|
|
45
|
+
*/
|
|
46
|
+
function validateContractAmendments(upstreamContracts = [], currentContracts = [], amendments = [], options = {}) {
|
|
47
|
+
const upstreamMap = new Map((upstreamContracts || []).map(c => [c.id, c]));
|
|
48
|
+
const currentMap = new Map((currentContracts || []).map(c => [c.id, c]));
|
|
49
|
+
const amendmentList = Array.isArray(amendments) ? amendments : [];
|
|
50
|
+
const amendmentMap = new Map(amendmentList.map(a => [a.contract_id, a]));
|
|
51
|
+
|
|
52
|
+
// 1. Detect modified or removed contracts
|
|
53
|
+
const changedContractIds = [];
|
|
54
|
+
|
|
55
|
+
for (const [id, upstream] of upstreamMap.entries()) {
|
|
56
|
+
if (!currentMap.has(id)) {
|
|
57
|
+
changedContractIds.push({ id, type: 'REMOVED', upstream });
|
|
58
|
+
} else {
|
|
59
|
+
const current = currentMap.get(id);
|
|
60
|
+
if (JSON.stringify(upstream) !== JSON.stringify(current)) {
|
|
61
|
+
changedContractIds.push({ id, type: 'MODIFIED', upstream, current });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Ensure each changed contract has a valid, signed amendment
|
|
67
|
+
for (const changed of changedContractIds) {
|
|
68
|
+
if (!amendmentMap.has(changed.id)) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
code: 'UNAUTHORIZED_CONTRACT_MUTATION',
|
|
72
|
+
error: `El contrato congelado "${changed.id}" fue ${changed.type === 'REMOVED' ? 'eliminado' : 'modificado'} sin un registro formal de enmienda.`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Verify each declared amendment
|
|
78
|
+
for (const a of amendmentList) {
|
|
79
|
+
for (const field of REQUIRED_AMENDMENT_FIELDS) {
|
|
80
|
+
if (!a[field] && a[field] !== 0) {
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
code: 'AMENDMENT_MALFORMED',
|
|
84
|
+
error: `Enmienda "${a.amendment_id || 'UNKNOWN'}" carece del campo obligatorio "${field}".`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const expectedSig = computeAmendmentSignature(a, options.secret);
|
|
90
|
+
if (a.signature !== expectedSig) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
code: 'AMENDMENT_SIGNATURE_INVALID',
|
|
94
|
+
error: `Firma criptográfica inválida para la enmienda "${a.amendment_id}" del contrato "${a.contract_id}".`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
valid: true,
|
|
101
|
+
verified_amendments: amendmentList.length
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
REQUIRED_AMENDMENT_FIELDS,
|
|
107
|
+
computeAmendmentSignature,
|
|
108
|
+
validateContractAmendments
|
|
109
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Offline Dependency Auditor (Gemstack 2.0 Sprint D)
|
|
5
|
+
* Detects orphan dependencies, undeclared imports, and circular local import cycles offline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fssafe = require('./filesystem-safe');
|
|
11
|
+
|
|
12
|
+
const NODE_BUILTINS = new Set([
|
|
13
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
|
|
14
|
+
'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
|
|
15
|
+
'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
|
|
16
|
+
'module', 'net', 'os', 'path', 'path/posix', 'path/win32', 'perf_hooks',
|
|
17
|
+
'process', 'punycode', 'querystring', 'readline', 'repl', 'stream',
|
|
18
|
+
'stream/promises', 'stream/consumers', 'stream/web', 'string_decoder',
|
|
19
|
+
'test', 'timers', 'timers/promises', 'tls', 'trace_events', 'tty',
|
|
20
|
+
'url', 'util', 'util/types', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isNodeBuiltin(moduleName) {
|
|
24
|
+
if (moduleName.startsWith('node:')) return true;
|
|
25
|
+
return NODE_BUILTINS.has(moduleName);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extracts import/require targets from file content.
|
|
30
|
+
* @param {string} content
|
|
31
|
+
* @returns {Array<string>} List of required/imported module specifiers
|
|
32
|
+
*/
|
|
33
|
+
function extractImportsFromContent(content) {
|
|
34
|
+
const imports = [];
|
|
35
|
+
// Match require('...')
|
|
36
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
39
|
+
imports.push(match[1]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Match import ... from '...' or import('...')
|
|
43
|
+
const importRegex = /(?:import\s+(?:[\s\S]*?from\s+)?|import\s*\()\s*['"]([^'"]+)['"]/g;
|
|
44
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
45
|
+
imports.push(match[1]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return imports;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves package name from import specifier.
|
|
53
|
+
* E.g., 'express' -> 'express', '@scope/pkg/sub' -> '@scope/pkg'
|
|
54
|
+
*/
|
|
55
|
+
function getPackageName(specifier) {
|
|
56
|
+
if (specifier.startsWith('@')) {
|
|
57
|
+
const parts = specifier.split('/');
|
|
58
|
+
return parts.slice(0, 2).join('/');
|
|
59
|
+
}
|
|
60
|
+
return specifier.split('/')[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scans directory recursively for JavaScript/TypeScript files.
|
|
65
|
+
*/
|
|
66
|
+
function collectSourceFiles(dir, files = []) {
|
|
67
|
+
if (!fs.existsSync(dir)) return files;
|
|
68
|
+
|
|
69
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = path.join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
74
|
+
collectSourceFiles(fullPath, files);
|
|
75
|
+
}
|
|
76
|
+
} else if (entry.isFile() && /\.(js|mjs|cjs|ts)$/.test(entry.name)) {
|
|
77
|
+
files.push(fullPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detects circular dependency cycles using depth-first search.
|
|
85
|
+
*/
|
|
86
|
+
function findCircularCycles(dependencyGraph) {
|
|
87
|
+
const cycles = [];
|
|
88
|
+
const visited = new Set();
|
|
89
|
+
const recursionStack = [];
|
|
90
|
+
|
|
91
|
+
function dfs(node) {
|
|
92
|
+
visited.add(node);
|
|
93
|
+
recursionStack.push(node);
|
|
94
|
+
|
|
95
|
+
const neighbors = dependencyGraph.get(node) || [];
|
|
96
|
+
for (const neighbor of neighbors) {
|
|
97
|
+
if (!visited.has(neighbor)) {
|
|
98
|
+
dfs(neighbor);
|
|
99
|
+
} else {
|
|
100
|
+
const cycleStartIndex = recursionStack.indexOf(neighbor);
|
|
101
|
+
if (cycleStartIndex !== -1) {
|
|
102
|
+
const cyclePath = [...recursionStack.slice(cycleStartIndex), neighbor];
|
|
103
|
+
cycles.push(cyclePath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
recursionStack.pop();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const node of dependencyGraph.keys()) {
|
|
112
|
+
if (!visited.has(node)) {
|
|
113
|
+
dfs(node);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return cycles;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Audits project dependencies and local module cycles completely offline.
|
|
122
|
+
* @param {string} targetDir - Directory containing package.json and src/
|
|
123
|
+
* @returns {{ orphans: string[], undeclared: string[], circularCycles: string[][], is_clean: boolean }}
|
|
124
|
+
*/
|
|
125
|
+
function auditDependencies(targetDir) {
|
|
126
|
+
const pkgPath = fssafe.resolveSafe(targetDir, 'package.json');
|
|
127
|
+
let dependencies = {};
|
|
128
|
+
let devDependencies = {};
|
|
129
|
+
|
|
130
|
+
if (fs.existsSync(pkgPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
133
|
+
dependencies = pkg.dependencies || {};
|
|
134
|
+
devDependencies = pkg.devDependencies || {};
|
|
135
|
+
} catch (_) {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const srcDir = fssafe.resolveSafe(targetDir, 'src');
|
|
139
|
+
const sourceFiles = collectSourceFiles(srcDir);
|
|
140
|
+
|
|
141
|
+
const usedPackages = new Set();
|
|
142
|
+
const dependencyGraph = new Map();
|
|
143
|
+
|
|
144
|
+
for (const filePath of sourceFiles) {
|
|
145
|
+
const normFile = filePath.replace(/\\/g, '/');
|
|
146
|
+
dependencyGraph.set(normFile, []);
|
|
147
|
+
|
|
148
|
+
let content = '';
|
|
149
|
+
try {
|
|
150
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
151
|
+
} catch (_) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const imports = extractImportsFromContent(content);
|
|
156
|
+
for (const imp of imports) {
|
|
157
|
+
if (imp.startsWith('.')) {
|
|
158
|
+
// Local relative import
|
|
159
|
+
const resolvedPath = path.resolve(path.dirname(filePath), imp);
|
|
160
|
+
const candidates = [
|
|
161
|
+
resolvedPath,
|
|
162
|
+
resolvedPath + '.js',
|
|
163
|
+
resolvedPath + '.mjs',
|
|
164
|
+
path.join(resolvedPath, 'index.js')
|
|
165
|
+
];
|
|
166
|
+
const match = candidates.find(c => fs.existsSync(c) && fs.statSync(c).isFile());
|
|
167
|
+
if (match) {
|
|
168
|
+
const normNeighbor = match.replace(/\\/g, '/');
|
|
169
|
+
dependencyGraph.get(normFile).push(normNeighbor);
|
|
170
|
+
}
|
|
171
|
+
} else if (!isNodeBuiltin(imp)) {
|
|
172
|
+
// External package
|
|
173
|
+
const pkgName = getPackageName(imp);
|
|
174
|
+
usedPackages.add(pkgName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 1. Detect orphans (in dependencies but not used in src)
|
|
180
|
+
const orphans = Object.keys(dependencies).filter(dep => !usedPackages.has(dep));
|
|
181
|
+
|
|
182
|
+
// 2. Detect undeclared (used in src but missing from dependencies & devDependencies)
|
|
183
|
+
const allDeclared = new Set([...Object.keys(dependencies), ...Object.keys(devDependencies)]);
|
|
184
|
+
const undeclared = Array.from(usedPackages).filter(dep => !allDeclared.has(dep));
|
|
185
|
+
|
|
186
|
+
// 3. Detect circular cycles
|
|
187
|
+
const circularCycles = findCircularCycles(dependencyGraph);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
orphans,
|
|
191
|
+
undeclared,
|
|
192
|
+
circularCycles,
|
|
193
|
+
is_clean: orphans.length === 0 && undeclared.length === 0 && circularCycles.length === 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
isNodeBuiltin,
|
|
199
|
+
extractImportsFromContent,
|
|
200
|
+
auditDependencies,
|
|
201
|
+
findCircularCycles
|
|
202
|
+
};
|