sinapse-ai 1.6.1 → 1.7.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/.claude/rules/documentation-first.md +1 -1
- package/.sinapse-ai/core/config/merge-utils.js +8 -0
- package/.sinapse-ai/core/errors/constants.js +147 -0
- package/.sinapse-ai/core/errors/error-registry.js +176 -0
- package/.sinapse-ai/core/errors/index.js +50 -0
- package/.sinapse-ai/core/errors/serializer.js +147 -0
- package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
- package/.sinapse-ai/core/errors/utils.js +187 -0
- package/.sinapse-ai/core/execution/build-orchestrator.js +43 -48
- package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
- package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +86 -43
- package/.sinapse-ai/core/ideation/ideation-engine.js +63 -7
- package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
- package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +45 -3
- package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
- package/.sinapse-ai/core/registry/registry-loader.js +71 -5
- package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
- package/.sinapse-ai/core/synapse/context/index.js +19 -0
- package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
- package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
- package/.sinapse-ai/core/synapse/engine.js +43 -3
- package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
- package/.sinapse-ai/core-config.yaml +19 -0
- package/.sinapse-ai/data/entity-registry.yaml +190 -72
- package/.sinapse-ai/data/registry-update-log.jsonl +57 -0
- package/.sinapse-ai/development/scripts/squad/squad-downloader.js +115 -3
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +1 -1
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +2 -2
- package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
- package/.sinapse-ai/install-manifest.yaml +76 -40
- package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
- package/docs/framework/collaboration-autonomy-plan.md +18 -18
- package/docs/guides/parallel-workflow.md +6 -6
- package/package.json +10 -3
- package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
- package/scripts/regenerate-orqx-stubs.ps1 +6 -5
- package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
- package/squads/squad-brand/templates/client-delivery-template.md +1 -1
- package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
- package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
- package/docs/chrome-brain-upgrade-plan.md +0 -624
- package/docs/constitution-compliance.md +0 -87
- package/docs/mega-upgrade-orchestration-plan.md +0 -71
- package/docs/research-synthesis-for-upgrade.md +0 -511
- package/docs/security-audit-report.md +0 -306
|
@@ -103,7 +103,7 @@ complexity_score ≥ 16 (COMPLEX class) → run Spec Pipeline FIRST
|
|
|
103
103
|
|
|
104
104
|
| User says | Classification | What the framework does |
|
|
105
105
|
|---|---|---|
|
|
106
|
-
| "criar um site pra meu cliente
|
|
106
|
+
| "criar um site pra meu cliente Acme" | `site` | Invokes `greenfield-ui.yaml` (5-agent Phase 1) |
|
|
107
107
|
| "monta uma plataforma SaaS de gestão" | `saas` (COMPLEX) | Spec Pipeline → then `greenfield-fullstack.yaml` |
|
|
108
108
|
| "API de cobrança Asaas" | `api` | Invokes `greenfield-service.yaml` |
|
|
109
109
|
| "corrige o botão verde da home" | `fix` | SDC YOLO direct |
|
|
@@ -50,6 +50,14 @@ function deepMerge(target, source) {
|
|
|
50
50
|
const result = { ...target };
|
|
51
51
|
|
|
52
52
|
for (const [key, value] of Object.entries(source)) {
|
|
53
|
+
// Defense-in-depth: block prototype-pollution keys (CWE-1321).
|
|
54
|
+
// The {...target} spread already neutralizes this today, but an explicit
|
|
55
|
+
// deny-list prevents a future refactor from reintroducing the vulnerability
|
|
56
|
+
// via untrusted config (YAML/JSON).
|
|
57
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
// Handle +append modifier for arrays
|
|
54
62
|
if (key.endsWith('+append')) {
|
|
55
63
|
const baseKey = key.slice(0, -7); // Remove '+append'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/errors/constants.js — SINAPSE typed error constants.
|
|
3
|
+
*
|
|
4
|
+
* SNPS_* typed error codes with SINAPSE branding.
|
|
5
|
+
*
|
|
6
|
+
* ErrorCategory / ErrorSeverity are the closed enums consumed across the
|
|
7
|
+
* framework. DEFAULT_ERROR_CODE is the fallback code used when a thrown value
|
|
8
|
+
* carries no recognizable code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ErrorCategory = Object.freeze({
|
|
12
|
+
CONFIGURATION: 'configuration',
|
|
13
|
+
VALIDATION: 'validation',
|
|
14
|
+
FILESYSTEM: 'filesystem',
|
|
15
|
+
NETWORK: 'network',
|
|
16
|
+
REGISTRY: 'registry',
|
|
17
|
+
ORCHESTRATION: 'orchestration',
|
|
18
|
+
SYNAPSE: 'synapse',
|
|
19
|
+
EXECUTION: 'execution',
|
|
20
|
+
PERMISSION: 'permission',
|
|
21
|
+
EXTERNAL_EXECUTOR: 'external_executor',
|
|
22
|
+
UNKNOWN: 'unknown',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const ErrorSeverity = Object.freeze({
|
|
26
|
+
CRITICAL: 'critical',
|
|
27
|
+
ERROR: 'error',
|
|
28
|
+
WARNING: 'warning',
|
|
29
|
+
INFO: 'info',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const DEFAULT_ERROR_CODE = 'SNPS_UNKNOWN_ERROR';
|
|
33
|
+
|
|
34
|
+
const CORE_ERROR_DEFINITIONS = Object.freeze([
|
|
35
|
+
{
|
|
36
|
+
code: DEFAULT_ERROR_CODE,
|
|
37
|
+
category: ErrorCategory.UNKNOWN,
|
|
38
|
+
severity: ErrorSeverity.ERROR,
|
|
39
|
+
retryable: false,
|
|
40
|
+
userMessage: 'An unexpected SINAPSE core error occurred.',
|
|
41
|
+
recovery: ['Review the error metadata and retry if the operation is safe to repeat.'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
code: 'SNPS_CONFIGURATION_INVALID',
|
|
45
|
+
category: ErrorCategory.CONFIGURATION,
|
|
46
|
+
severity: ErrorSeverity.ERROR,
|
|
47
|
+
retryable: false,
|
|
48
|
+
userMessage: 'SINAPSE configuration is invalid.',
|
|
49
|
+
recovery: ['Validate the active SINAPSE configuration and retry.'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: 'SNPS_VALIDATION_FAILED',
|
|
53
|
+
category: ErrorCategory.VALIDATION,
|
|
54
|
+
severity: ErrorSeverity.ERROR,
|
|
55
|
+
retryable: false,
|
|
56
|
+
userMessage: 'SINAPSE validation failed.',
|
|
57
|
+
recovery: ['Review validation errors and correct the invalid input.'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: 'SNPS_FILESYSTEM_ERROR',
|
|
61
|
+
category: ErrorCategory.FILESYSTEM,
|
|
62
|
+
severity: ErrorSeverity.ERROR,
|
|
63
|
+
retryable: true,
|
|
64
|
+
userMessage: 'A filesystem operation failed.',
|
|
65
|
+
recovery: ['Check path existence, permissions, and disk availability.'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
code: 'SNPS_PERMISSION_DENIED',
|
|
69
|
+
category: ErrorCategory.PERMISSION,
|
|
70
|
+
severity: ErrorSeverity.ERROR,
|
|
71
|
+
retryable: false,
|
|
72
|
+
exitCode: 13,
|
|
73
|
+
userMessage: 'The operation does not have the required permissions.',
|
|
74
|
+
recovery: ['Grant the required permission or run the command in an authorized context.'],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
code: 'SNPS_NETWORK_ERROR',
|
|
78
|
+
category: ErrorCategory.NETWORK,
|
|
79
|
+
severity: ErrorSeverity.ERROR,
|
|
80
|
+
retryable: true,
|
|
81
|
+
userMessage: 'A network operation failed.',
|
|
82
|
+
recovery: ['Check connectivity and retry the operation.'],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
code: 'SNPS_REGISTRY_LOAD_FAILED',
|
|
86
|
+
category: ErrorCategory.REGISTRY,
|
|
87
|
+
severity: ErrorSeverity.ERROR,
|
|
88
|
+
retryable: false,
|
|
89
|
+
userMessage: 'SINAPSE could not load a registry.',
|
|
90
|
+
recovery: ['Validate registry file syntax and path configuration.'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
code: 'SNPS_REGISTRY_WRITE_FAILED',
|
|
94
|
+
category: ErrorCategory.REGISTRY,
|
|
95
|
+
severity: ErrorSeverity.ERROR,
|
|
96
|
+
retryable: true,
|
|
97
|
+
userMessage: 'SINAPSE could not write a registry.',
|
|
98
|
+
recovery: ['Check registry path permissions and retry.'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: 'SNPS_ORCHESTRATION_FAILED',
|
|
102
|
+
category: ErrorCategory.ORCHESTRATION,
|
|
103
|
+
severity: ErrorSeverity.ERROR,
|
|
104
|
+
retryable: false,
|
|
105
|
+
userMessage: 'SINAPSE orchestration failed.',
|
|
106
|
+
recovery: ['Review orchestration metadata and the active workflow state.'],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
code: 'SNPS_SYNAPSE_LAYER_FAILED',
|
|
110
|
+
category: ErrorCategory.SYNAPSE,
|
|
111
|
+
severity: ErrorSeverity.WARNING,
|
|
112
|
+
retryable: true,
|
|
113
|
+
userMessage: 'A Synapse layer failed while processing context.',
|
|
114
|
+
recovery: ['Review layer metadata and continue with graceful degradation when possible.'],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
code: 'SNPS_EXECUTION_FAILED',
|
|
118
|
+
category: ErrorCategory.EXECUTION,
|
|
119
|
+
severity: ErrorSeverity.ERROR,
|
|
120
|
+
retryable: false,
|
|
121
|
+
userMessage: 'SINAPSE execution failed.',
|
|
122
|
+
recovery: ['Review execution logs and retry after correcting the failure cause.'],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
code: 'SNPS_EXTERNAL_EXECUTOR_FAILED',
|
|
126
|
+
category: ErrorCategory.EXTERNAL_EXECUTOR,
|
|
127
|
+
severity: ErrorSeverity.ERROR,
|
|
128
|
+
retryable: true,
|
|
129
|
+
userMessage: 'An external executor failed.',
|
|
130
|
+
recovery: ['Review external executor logs and retry if the command is idempotent.'],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
code: 'SNPS_PERSISTENCE_DEGRADED',
|
|
134
|
+
category: ErrorCategory.FILESYSTEM,
|
|
135
|
+
severity: ErrorSeverity.WARNING,
|
|
136
|
+
retryable: true,
|
|
137
|
+
userMessage: 'SINAPSE persistence degraded and continued in memory.',
|
|
138
|
+
recovery: ['Check persistence path permissions and available disk space.'],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
ErrorCategory,
|
|
144
|
+
ErrorSeverity,
|
|
145
|
+
DEFAULT_ERROR_CODE,
|
|
146
|
+
CORE_ERROR_DEFINITIONS,
|
|
147
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/errors/error-registry.js — SINAPSE error definition registry.
|
|
3
|
+
*
|
|
4
|
+
* SINAPSE-branded error registry; code regex is [A-Z0-9_]+, so SNPS_* codes
|
|
5
|
+
* validate. The Pro-tier registry (pro-error-registry.js) is intentionally
|
|
6
|
+
* not included.
|
|
7
|
+
*
|
|
8
|
+
* The registry maps an error code → frozen definition (category, severity,
|
|
9
|
+
* retryable, userMessage, recovery, optional exitCode). Lookups never throw:
|
|
10
|
+
* unknown codes fall back to the UNKNOWN definition flagged unregistered.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
ErrorCategory,
|
|
15
|
+
ErrorSeverity,
|
|
16
|
+
DEFAULT_ERROR_CODE,
|
|
17
|
+
CORE_ERROR_DEFINITIONS,
|
|
18
|
+
} = require('./constants');
|
|
19
|
+
const {
|
|
20
|
+
cloneMetadataValue,
|
|
21
|
+
deepMerge,
|
|
22
|
+
hasOwn,
|
|
23
|
+
isPlainObject,
|
|
24
|
+
normalizeErrorCode,
|
|
25
|
+
normalizeRecovery,
|
|
26
|
+
} = require('./utils');
|
|
27
|
+
|
|
28
|
+
const VALID_CATEGORIES = new Set(Object.values(ErrorCategory));
|
|
29
|
+
const VALID_SEVERITIES = new Set(Object.values(ErrorSeverity));
|
|
30
|
+
|
|
31
|
+
function freezeDefinition(definition) {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
...definition,
|
|
34
|
+
metadata: Object.freeze(cloneMetadataValue(definition.metadata || {})),
|
|
35
|
+
recovery: Object.freeze([...(definition.recovery || [])]),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createUnknownDefinition(code = DEFAULT_ERROR_CODE) {
|
|
40
|
+
return freezeDefinition({
|
|
41
|
+
code,
|
|
42
|
+
category: ErrorCategory.UNKNOWN,
|
|
43
|
+
severity: ErrorSeverity.ERROR,
|
|
44
|
+
retryable: false,
|
|
45
|
+
userMessage: 'An unexpected SINAPSE core error occurred.',
|
|
46
|
+
recovery: ['Review the error metadata and retry if the operation is safe to repeat.'],
|
|
47
|
+
metadata: code === DEFAULT_ERROR_CODE ? {} : { registry: { registered: false } },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class ErrorRegistry {
|
|
52
|
+
constructor(definitions = CORE_ERROR_DEFINITIONS) {
|
|
53
|
+
this._entries = new Map();
|
|
54
|
+
this.registerMany(definitions);
|
|
55
|
+
|
|
56
|
+
if (!this.has(DEFAULT_ERROR_CODE)) {
|
|
57
|
+
this.register(createUnknownDefinition());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
registerMany(definitions) {
|
|
62
|
+
if (Array.isArray(definitions)) {
|
|
63
|
+
definitions.forEach((definition) => this.register(definition));
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isPlainObject(definitions)) {
|
|
68
|
+
Object.values(definitions).forEach((definition) => this.register(definition));
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new TypeError('ErrorRegistry definitions must be an array or object');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
register(definition) {
|
|
76
|
+
const normalized = this._normalizeDefinition(definition);
|
|
77
|
+
|
|
78
|
+
if (this._entries.has(normalized.code)) {
|
|
79
|
+
throw new Error(`Duplicate SINAPSE error code: ${normalized.code}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this._entries.set(normalized.code, freezeDefinition(normalized));
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lookup(code) {
|
|
87
|
+
const normalizedCode = normalizeErrorCode(code) || DEFAULT_ERROR_CODE;
|
|
88
|
+
const found = this._entries.get(normalizedCode);
|
|
89
|
+
|
|
90
|
+
if (found) {
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const fallback = this._entries.get(DEFAULT_ERROR_CODE) || createUnknownDefinition();
|
|
95
|
+
return freezeDefinition({
|
|
96
|
+
...fallback,
|
|
97
|
+
code: normalizedCode,
|
|
98
|
+
metadata: deepMerge(fallback.metadata, { registry: { registered: false } }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
has(code) {
|
|
103
|
+
const normalizedCode = normalizeErrorCode(code);
|
|
104
|
+
return Boolean(normalizedCode && this._entries.has(normalizedCode));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
list() {
|
|
108
|
+
return Array.from(this._entries.values()).sort((left, right) => left.code.localeCompare(right.code));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get size() {
|
|
112
|
+
return this._entries.size;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
assertUnique() {
|
|
116
|
+
const codes = this.list().map((definition) => definition.code);
|
|
117
|
+
const unique = new Set(codes);
|
|
118
|
+
|
|
119
|
+
if (codes.length !== unique.size) {
|
|
120
|
+
throw new Error('ErrorRegistry contains duplicate error codes');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_normalizeDefinition(definition) {
|
|
127
|
+
if (!isPlainObject(definition)) {
|
|
128
|
+
throw new TypeError('Error definition must be a plain object');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const code = normalizeErrorCode(definition.code);
|
|
132
|
+
if (!code) {
|
|
133
|
+
throw new Error('Error definition requires a non-empty code');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!/^[A-Z0-9_]+$/.test(code)) {
|
|
137
|
+
throw new Error(`Invalid SINAPSE error code: ${code}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const category = definition.category || ErrorCategory.UNKNOWN;
|
|
141
|
+
if (!VALID_CATEGORIES.has(category)) {
|
|
142
|
+
throw new Error(`Invalid error category for ${code}: ${category}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const severity = definition.severity || ErrorSeverity.ERROR;
|
|
146
|
+
if (!VALID_SEVERITIES.has(severity)) {
|
|
147
|
+
throw new Error(`Invalid error severity for ${code}: ${severity}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalized = {
|
|
151
|
+
code,
|
|
152
|
+
category,
|
|
153
|
+
severity,
|
|
154
|
+
retryable: Boolean(definition.retryable),
|
|
155
|
+
userMessage: definition.userMessage || definition.message || code,
|
|
156
|
+
recovery: normalizeRecovery(definition.recovery),
|
|
157
|
+
metadata: isPlainObject(definition.metadata) ? cloneMetadataValue(definition.metadata) : {},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (hasOwn(definition, 'exitCode') && definition.exitCode !== undefined) {
|
|
161
|
+
if (!Number.isInteger(definition.exitCode) || definition.exitCode < 0) {
|
|
162
|
+
throw new Error(`Invalid exitCode for ${code}: ${definition.exitCode}`);
|
|
163
|
+
}
|
|
164
|
+
normalized.exitCode = definition.exitCode;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const defaultErrorRegistry = new ErrorRegistry(CORE_ERROR_DEFINITIONS);
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
ErrorRegistry,
|
|
175
|
+
defaultErrorRegistry,
|
|
176
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/errors/index.js — barrel for the SINAPSE typed-error module.
|
|
3
|
+
*
|
|
4
|
+
* SINAPSE-branded barrel. This is the FOUNDATION module: downstream modules consume it via
|
|
5
|
+
* const { SinapseError, normalizeError, serializeError } = require('../errors');
|
|
6
|
+
*
|
|
7
|
+
* Public API exported here:
|
|
8
|
+
* - SinapseError error class (code/category/severity/retryable/recovery/exitCode)
|
|
9
|
+
* - ErrorRegistry registry class for custom error definition sets
|
|
10
|
+
* - ErrorCategory frozen category enum
|
|
11
|
+
* - ErrorSeverity frozen severity enum
|
|
12
|
+
* - DEFAULT_ERROR_CODE 'SNPS_UNKNOWN_ERROR'
|
|
13
|
+
* - CORE_ERROR_DEFINITIONS frozen array of built-in SNPS_* definitions
|
|
14
|
+
* - defaultErrorRegistry singleton registry (core definitions loaded)
|
|
15
|
+
* - isSinapseError(value) duck-typed instanceof check
|
|
16
|
+
* - normalizeError(err, opts) envelope any thrown value into a SinapseError
|
|
17
|
+
* - serializeError(err, opts) JSON-safe envelope (stack redacted in prod)
|
|
18
|
+
* - sanitizeValue(value) cycle-safe value sanitizer
|
|
19
|
+
* - shouldExposeErrorStack stack-exposure policy predicate
|
|
20
|
+
* - deepMerge / isPlainObject / normalizeErrorCode shared helpers
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
ErrorCategory,
|
|
25
|
+
ErrorSeverity,
|
|
26
|
+
DEFAULT_ERROR_CODE,
|
|
27
|
+
CORE_ERROR_DEFINITIONS,
|
|
28
|
+
} = require('./constants');
|
|
29
|
+
const { ErrorRegistry, defaultErrorRegistry } = require('./error-registry');
|
|
30
|
+
const { SinapseError, isSinapseError, normalizeError } = require('./sinapse-error');
|
|
31
|
+
const { shouldExposeErrorStack, sanitizeValue, serializeError } = require('./serializer');
|
|
32
|
+
const { deepMerge, isPlainObject, normalizeErrorCode } = require('./utils');
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
SinapseError,
|
|
36
|
+
ErrorRegistry,
|
|
37
|
+
ErrorCategory,
|
|
38
|
+
ErrorSeverity,
|
|
39
|
+
DEFAULT_ERROR_CODE,
|
|
40
|
+
CORE_ERROR_DEFINITIONS,
|
|
41
|
+
defaultErrorRegistry,
|
|
42
|
+
isSinapseError,
|
|
43
|
+
normalizeError,
|
|
44
|
+
serializeError,
|
|
45
|
+
sanitizeValue,
|
|
46
|
+
shouldExposeErrorStack,
|
|
47
|
+
deepMerge,
|
|
48
|
+
isPlainObject,
|
|
49
|
+
normalizeErrorCode,
|
|
50
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/errors/serializer.js — JSON-safe error serialization for SINAPSE.
|
|
3
|
+
*
|
|
4
|
+
* SINAPSE-branded serializer with one ADDITIVE, defensive hardening on stack exposure:
|
|
5
|
+
* stacks are NEVER exposed when NODE_ENV === 'production' unless the caller
|
|
6
|
+
* explicitly passes `includeStack: true`. This prevents leaking internal stack
|
|
7
|
+
* traces to users in production (security: do not leak implementation detail).
|
|
8
|
+
*
|
|
9
|
+
* `sanitizeValue` lives in ./utils (cycle-safe via WeakSet). Here we inject the
|
|
10
|
+
* local `serializeError` into it so nested Error values get the full typed
|
|
11
|
+
* envelope. We re-export `sanitizeValue` so the module's public surface is
|
|
12
|
+
* unchanged from the original (index barrel still exports it from here too).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { DEFAULT_ERROR_CODE } = require('./constants');
|
|
16
|
+
const { hasOwn, sanitizeValue: sanitizeValueBase } = require('./utils');
|
|
17
|
+
|
|
18
|
+
const STACK_TRUTHY_FLAGS = ['1', 'true', 'yes', 'on'];
|
|
19
|
+
|
|
20
|
+
function isProductionEnv() {
|
|
21
|
+
return String(process.env.NODE_ENV || '').toLowerCase() === 'production';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decide whether an error stack may be included in serialized output.
|
|
26
|
+
*
|
|
27
|
+
* Precedence:
|
|
28
|
+
* 1. options.includeStack === true → always expose (explicit caller intent)
|
|
29
|
+
* 2. options.includeStack === false → never expose
|
|
30
|
+
* 3. NODE_ENV === 'production' → NEVER expose (additive prod guard)
|
|
31
|
+
* 4. otherwise → expose only if a debug flag is set
|
|
32
|
+
* (SINAPSE_DEBUG | DEBUG_ERROR_STACKS | DEBUG_STACKS)
|
|
33
|
+
*/
|
|
34
|
+
function shouldExposeErrorStack(options = {}) {
|
|
35
|
+
if (options.includeStack === true) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.includeStack === false) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Additive defensive guard: in production, never auto-expose stacks.
|
|
44
|
+
if (isProductionEnv()) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stackFlag =
|
|
49
|
+
process.env.SINAPSE_DEBUG ||
|
|
50
|
+
process.env.DEBUG_ERROR_STACKS ||
|
|
51
|
+
process.env.DEBUG_STACKS ||
|
|
52
|
+
'';
|
|
53
|
+
|
|
54
|
+
return STACK_TRUTHY_FLAGS.includes(String(stackFlag).toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sanitizeValue(value, seen = new WeakSet(), options = {}) {
|
|
58
|
+
// Inject serializeError so nested Error values produce the full envelope.
|
|
59
|
+
return sanitizeValueBase(value, seen, options, serializeError);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function serializeError(error, options = {}, seen = new WeakSet()) {
|
|
63
|
+
if (!(error instanceof Error)) {
|
|
64
|
+
return sanitizeValue(error, seen, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (seen.has(error)) {
|
|
68
|
+
return '[Circular]';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
seen.add(error);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const serialized = {
|
|
75
|
+
name: error.name || 'Error',
|
|
76
|
+
message: error.message || '',
|
|
77
|
+
stack: shouldExposeErrorStack(options) ? error.stack : '[redacted]',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (error.code) {
|
|
81
|
+
serialized.code = error.code;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (error.isSinapseError || error.name === 'SinapseError') {
|
|
85
|
+
serialized.code = error.code || DEFAULT_ERROR_CODE;
|
|
86
|
+
serialized.category = error.category;
|
|
87
|
+
serialized.severity = error.severity;
|
|
88
|
+
serialized.retryable = Boolean(error.retryable);
|
|
89
|
+
|
|
90
|
+
if (hasOwn(error, 'exitCode')) {
|
|
91
|
+
serialized.exitCode = error.exitCode;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (error.userMessage) {
|
|
95
|
+
serialized.userMessage = error.userMessage;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(error.recovery)) {
|
|
99
|
+
serialized.recovery = [...error.recovery];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
serialized.metadata = sanitizeValue(error.metadata || {}, seen, options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (error.cause !== undefined) {
|
|
106
|
+
serialized.cause = error.cause instanceof Error
|
|
107
|
+
? serializeError(error.cause, options, seen)
|
|
108
|
+
: sanitizeValue(error.cause, seen, options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
112
|
+
if ([
|
|
113
|
+
'name',
|
|
114
|
+
'message',
|
|
115
|
+
'stack',
|
|
116
|
+
'code',
|
|
117
|
+
'category',
|
|
118
|
+
'severity',
|
|
119
|
+
'retryable',
|
|
120
|
+
'exitCode',
|
|
121
|
+
'metadata',
|
|
122
|
+
'cause',
|
|
123
|
+
'userMessage',
|
|
124
|
+
'recovery',
|
|
125
|
+
'isSinapseError',
|
|
126
|
+
].includes(key)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
serialized[key] = sanitizeValue(error[key], seen, options);
|
|
132
|
+
} catch (serializationError) {
|
|
133
|
+
serialized[key] = `[Unserializable: ${serializationError.message}]`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return serialized;
|
|
138
|
+
} finally {
|
|
139
|
+
seen.delete(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
shouldExposeErrorStack,
|
|
145
|
+
sanitizeValue,
|
|
146
|
+
serializeError,
|
|
147
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/errors/sinapse-error.js — the SinapseError class + normalizeError.
|
|
3
|
+
*
|
|
4
|
+
* SinapseError class + normalizeError with SINAPSE branding.
|
|
5
|
+
*
|
|
6
|
+
* SinapseError extends Error and carries the typed fields the framework relies
|
|
7
|
+
* on: code, category, severity, retryable, recovery, exitCode (+ userMessage,
|
|
8
|
+
* metadata, cause). normalizeError envelopes any thrown value (raw Error,
|
|
9
|
+
* existing SinapseError, or non-Error) into a SinapseError without losing
|
|
10
|
+
* context. Serialization is delegated to ./serializer (no duplicated logic).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { DEFAULT_ERROR_CODE } = require('./constants');
|
|
14
|
+
const { defaultErrorRegistry } = require('./error-registry');
|
|
15
|
+
const { serializeError } = require('./serializer');
|
|
16
|
+
const { deepMerge, hasOwn, isPlainObject, normalizeErrorCode } = require('./utils');
|
|
17
|
+
|
|
18
|
+
class SinapseError extends Error {
|
|
19
|
+
constructor(message, options = {}) {
|
|
20
|
+
const code = normalizeErrorCode(options.code) || DEFAULT_ERROR_CODE;
|
|
21
|
+
const registry = options.registry || defaultErrorRegistry;
|
|
22
|
+
const definition = registry.lookup(code);
|
|
23
|
+
const finalMessage = message || options.message || definition.userMessage || code;
|
|
24
|
+
|
|
25
|
+
if (hasOwn(options, 'cause')) {
|
|
26
|
+
super(finalMessage, { cause: options.cause });
|
|
27
|
+
} else {
|
|
28
|
+
super(finalMessage);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.name = 'SinapseError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.category = options.category || definition.category;
|
|
34
|
+
this.severity = options.severity || definition.severity;
|
|
35
|
+
this.retryable = hasOwn(options, 'retryable') ? Boolean(options.retryable) : Boolean(definition.retryable);
|
|
36
|
+
this.userMessage = options.userMessage || definition.userMessage;
|
|
37
|
+
this.recovery = Array.isArray(options.recovery) ? [...options.recovery] : [...(definition.recovery || [])];
|
|
38
|
+
this.metadata = deepMerge(definition.metadata || {}, options.metadata || {});
|
|
39
|
+
this.isSinapseError = true;
|
|
40
|
+
|
|
41
|
+
if (hasOwn(options, 'exitCode')) {
|
|
42
|
+
this.exitCode = options.exitCode;
|
|
43
|
+
} else if (hasOwn(definition, 'exitCode')) {
|
|
44
|
+
this.exitCode = definition.exitCode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (hasOwn(options, 'cause')) {
|
|
48
|
+
this.cause = options.cause;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Error.captureStackTrace) {
|
|
52
|
+
Error.captureStackTrace(this, SinapseError);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
toJSON(options = {}) {
|
|
57
|
+
return serializeError(this, options);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSinapseError(value) {
|
|
62
|
+
return value instanceof SinapseError || Boolean(value && value.isSinapseError === true);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectErrorOwnProperties(error) {
|
|
66
|
+
if (!(error instanceof Error)) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Object.getOwnPropertyNames(error).reduce((properties, key) => {
|
|
71
|
+
if (['name', 'message', 'stack', 'cause'].includes(key)) {
|
|
72
|
+
return properties;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
properties[key] = error[key];
|
|
76
|
+
return properties;
|
|
77
|
+
}, {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeError(error, overrides = {}) {
|
|
81
|
+
if (isSinapseError(error)) {
|
|
82
|
+
if (!overrides || Object.keys(overrides).length === 0) {
|
|
83
|
+
return error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new SinapseError(overrides.message || error.message, {
|
|
87
|
+
code: overrides.code || error.code,
|
|
88
|
+
category: overrides.category || error.category,
|
|
89
|
+
severity: overrides.severity || error.severity,
|
|
90
|
+
retryable: hasOwn(overrides, 'retryable') ? overrides.retryable : error.retryable,
|
|
91
|
+
exitCode: hasOwn(overrides, 'exitCode') ? overrides.exitCode : error.exitCode,
|
|
92
|
+
userMessage: overrides.userMessage || error.userMessage,
|
|
93
|
+
recovery: overrides.recovery || error.recovery,
|
|
94
|
+
metadata: deepMerge(error.metadata || {}, overrides.metadata || {}),
|
|
95
|
+
cause: hasOwn(overrides, 'cause') ? overrides.cause : error.cause,
|
|
96
|
+
registry: overrides.registry,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
const ownProperties = collectErrorOwnProperties(error);
|
|
102
|
+
const metadata = deepMerge(
|
|
103
|
+
{
|
|
104
|
+
originalError: {
|
|
105
|
+
name: error.name || 'Error',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
Object.keys(ownProperties).length > 0 ? { originalError: { properties: ownProperties } } : {},
|
|
109
|
+
isPlainObject(overrides.metadata) ? overrides.metadata : {},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return new SinapseError(overrides.message || error.message, {
|
|
113
|
+
code: overrides.code || error.code || DEFAULT_ERROR_CODE,
|
|
114
|
+
category: overrides.category,
|
|
115
|
+
severity: overrides.severity,
|
|
116
|
+
retryable: overrides.retryable,
|
|
117
|
+
exitCode: overrides.exitCode,
|
|
118
|
+
userMessage: overrides.userMessage,
|
|
119
|
+
recovery: overrides.recovery,
|
|
120
|
+
metadata,
|
|
121
|
+
cause: hasOwn(overrides, 'cause') ? overrides.cause : error,
|
|
122
|
+
registry: overrides.registry,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return new SinapseError(overrides.message || String(error), {
|
|
127
|
+
code: overrides.code || DEFAULT_ERROR_CODE,
|
|
128
|
+
category: overrides.category,
|
|
129
|
+
severity: overrides.severity,
|
|
130
|
+
retryable: overrides.retryable,
|
|
131
|
+
exitCode: overrides.exitCode,
|
|
132
|
+
userMessage: overrides.userMessage,
|
|
133
|
+
recovery: overrides.recovery,
|
|
134
|
+
metadata: deepMerge({ originalValue: { type: typeof error } }, overrides.metadata || {}),
|
|
135
|
+
cause: overrides.cause,
|
|
136
|
+
registry: overrides.registry,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
SinapseError,
|
|
142
|
+
isSinapseError,
|
|
143
|
+
normalizeError,
|
|
144
|
+
};
|