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.
Files changed (47) hide show
  1. package/.claude/rules/documentation-first.md +1 -1
  2. package/.sinapse-ai/core/config/merge-utils.js +8 -0
  3. package/.sinapse-ai/core/errors/constants.js +147 -0
  4. package/.sinapse-ai/core/errors/error-registry.js +176 -0
  5. package/.sinapse-ai/core/errors/index.js +50 -0
  6. package/.sinapse-ai/core/errors/serializer.js +147 -0
  7. package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
  8. package/.sinapse-ai/core/errors/utils.js +187 -0
  9. package/.sinapse-ai/core/execution/build-orchestrator.js +43 -48
  10. package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
  11. package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
  12. package/.sinapse-ai/core/execution/subagent-dispatcher.js +86 -43
  13. package/.sinapse-ai/core/ideation/ideation-engine.js +63 -7
  14. package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
  15. package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
  16. package/.sinapse-ai/core/orchestration/master-orchestrator.js +45 -3
  17. package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
  18. package/.sinapse-ai/core/registry/registry-loader.js +71 -5
  19. package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
  20. package/.sinapse-ai/core/synapse/context/index.js +19 -0
  21. package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
  22. package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
  23. package/.sinapse-ai/core/synapse/engine.js +43 -3
  24. package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
  25. package/.sinapse-ai/core-config.yaml +19 -0
  26. package/.sinapse-ai/data/entity-registry.yaml +190 -72
  27. package/.sinapse-ai/data/registry-update-log.jsonl +57 -0
  28. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +115 -3
  29. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +1 -1
  30. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +2 -2
  31. package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
  32. package/.sinapse-ai/install-manifest.yaml +76 -40
  33. package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
  34. package/docs/framework/collaboration-autonomy-plan.md +18 -18
  35. package/docs/guides/parallel-workflow.md +6 -6
  36. package/package.json +10 -3
  37. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
  38. package/scripts/regenerate-orqx-stubs.ps1 +6 -5
  39. package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
  40. package/squads/squad-brand/templates/client-delivery-template.md +1 -1
  41. package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
  42. package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
  43. package/docs/chrome-brain-upgrade-plan.md +0 -624
  44. package/docs/constitution-compliance.md +0 -87
  45. package/docs/mega-upgrade-orchestration-plan.md +0 -71
  46. package/docs/research-synthesis-for-upgrade.md +0 -511
  47. 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 Vascularte" | `site` | Invokes `greenfield-ui.yaml` (5-agent Phase 1) |
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
+ };