@devtrack-solution/codesdd 1.2.3 → 1.2.4

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 (213) hide show
  1. package/.sdd/skills/curated/devtrack-api/SKILL.md +98 -12
  2. package/.sdd/skills/curated/devtrack-api/agents/claude-code.yaml +10 -0
  3. package/.sdd/skills/curated/devtrack-api/agents/codex.yaml +10 -0
  4. package/.sdd/skills/curated/devtrack-api/agents/cursor.yaml +10 -0
  5. package/.sdd/skills/curated/devtrack-api/agents/gemini.yaml +10 -0
  6. package/.sdd/skills/curated/devtrack-api/agents/kimi.yaml +10 -0
  7. package/.sdd/skills/curated/devtrack-api/agents/openai.yaml +5 -3
  8. package/.sdd/skills/curated/devtrack-api/agents/opencode.yaml +12 -0
  9. package/.sdd/skills/curated/devtrack-api/references/application-presentation.md +61 -5
  10. package/.sdd/skills/curated/devtrack-api/references/consumer-sync-policy.md +15 -3
  11. package/.sdd/skills/curated/devtrack-api/references/contract-pack.yaml +1951 -0
  12. package/.sdd/skills/curated/devtrack-api/references/domain-modeling.md +16 -14
  13. package/.sdd/skills/curated/devtrack-api/references/field-validation-protocol.md +40 -0
  14. package/.sdd/skills/curated/devtrack-api/references/foundation-layout.md +19 -2
  15. package/.sdd/skills/curated/devtrack-api/references/generated-artifact-invalidation.md +97 -0
  16. package/.sdd/skills/curated/devtrack-api/references/implementation-checklist.md +30 -1
  17. package/.sdd/skills/curated/devtrack-api/references/portable-agent-contract.md +42 -0
  18. package/.sdd/skills/curated/devtrack-api/references/testing-validation.md +22 -1
  19. package/.sdd/skills/curated/devtrack-api/references/typeorm-infrastructure.md +9 -7
  20. package/README.md +280 -29
  21. package/dist/applications/sdd/index.d.ts +16 -0
  22. package/dist/applications/sdd/index.js +16 -0
  23. package/dist/cli/program.js +180 -11
  24. package/dist/commands/config.js +197 -10
  25. package/dist/commands/sdd/execution.js +408 -16
  26. package/dist/commands/sdd/plugin.js +5 -0
  27. package/dist/commands/sdd/shared.d.ts +1 -0
  28. package/dist/commands/sdd/shared.js +10 -0
  29. package/dist/commands/sdd.js +157 -7
  30. package/dist/core/cli/command-matrix.d.ts +18 -0
  31. package/dist/core/cli/command-matrix.js +157 -0
  32. package/dist/core/cli-command-quality.js +11 -0
  33. package/dist/core/completions/command-registry.js +45 -0
  34. package/dist/core/config-schema.d.ts +31 -1
  35. package/dist/core/config-schema.js +79 -5
  36. package/dist/core/config.d.ts +1 -0
  37. package/dist/core/config.js +11 -0
  38. package/dist/core/global-config.d.ts +29 -0
  39. package/dist/core/init.d.ts +2 -2
  40. package/dist/core/init.js +13 -14
  41. package/dist/core/sdd/agent-binding.d.ts +19 -19
  42. package/dist/core/sdd/agent-runtime-contract.d.ts +204 -0
  43. package/dist/core/sdd/agent-runtime-contract.js +200 -0
  44. package/dist/core/sdd/allocator-recovery.d.ts +14 -0
  45. package/dist/core/sdd/allocator-recovery.js +30 -0
  46. package/dist/core/sdd/allocator-security.d.ts +18 -0
  47. package/dist/core/sdd/allocator-security.js +36 -0
  48. package/dist/core/sdd/api-foundation-baseline.d.ts +111 -0
  49. package/dist/core/sdd/api-foundation-baseline.js +151 -0
  50. package/dist/core/sdd/api-foundation-parity.d.ts +114 -0
  51. package/dist/core/sdd/api-foundation-parity.js +131 -0
  52. package/dist/core/sdd/api-profile-catalog.d.ts +36 -0
  53. package/dist/core/sdd/api-profile-catalog.js +132 -0
  54. package/dist/core/sdd/api-profile-dry-run-projection.d.ts +93 -0
  55. package/dist/core/sdd/api-profile-dry-run-projection.js +370 -0
  56. package/dist/core/sdd/api-profile-recipes.d.ts +82 -0
  57. package/dist/core/sdd/api-profile-recipes.js +484 -0
  58. package/dist/core/sdd/artifact-id-allocator.d.ts +368 -0
  59. package/dist/core/sdd/artifact-id-allocator.js +510 -0
  60. package/dist/core/sdd/check.d.ts +52 -1
  61. package/dist/core/sdd/check.js +326 -11
  62. package/dist/core/sdd/coordination/coordination-adapters.d.ts +15 -8
  63. package/dist/core/sdd/coordination/coordination-adapters.js +43 -15
  64. package/dist/core/sdd/coordination/index.d.ts +1 -0
  65. package/dist/core/sdd/coordination/index.js +1 -0
  66. package/dist/core/sdd/coordination/redis-runtime.d.ts +131 -0
  67. package/dist/core/sdd/coordination/redis-runtime.js +698 -0
  68. package/dist/core/sdd/deepagent-contracts.d.ts +99 -5
  69. package/dist/core/sdd/deepagent-contracts.js +62 -0
  70. package/dist/core/sdd/deepagents/reversa-subagents.d.ts +3 -3
  71. package/dist/core/sdd/default-bootstrap-files.d.ts +2 -2
  72. package/dist/core/sdd/default-bootstrap-files.js +14 -10
  73. package/dist/core/sdd/default-skills.js +115 -9
  74. package/dist/core/sdd/devtrack-api-appliance.d.ts +42 -1
  75. package/dist/core/sdd/devtrack-api-appliance.js +159 -32
  76. package/dist/core/sdd/devtrack-api-architecture.d.ts +16 -0
  77. package/dist/core/sdd/devtrack-api-architecture.js +86 -0
  78. package/dist/core/sdd/docs-sync.js +24 -18
  79. package/dist/core/sdd/domain/capability-diff.d.ts +63 -0
  80. package/dist/core/sdd/domain/capability-diff.js +200 -0
  81. package/dist/core/sdd/domain/change-safety-guardrails.d.ts +74 -0
  82. package/dist/core/sdd/domain/change-safety-guardrails.js +333 -0
  83. package/dist/core/sdd/domain/semantic-intent-classifier.d.ts +29 -0
  84. package/dist/core/sdd/domain/semantic-intent-classifier.js +117 -0
  85. package/dist/core/sdd/enterprise-mutating-command-gate.d.ts +27 -0
  86. package/dist/core/sdd/enterprise-mutating-command-gate.js +104 -0
  87. package/dist/core/sdd/enterprise-provenance-gates.d.ts +20 -0
  88. package/dist/core/sdd/enterprise-provenance-gates.js +63 -0
  89. package/dist/core/sdd/enterprise-provisioning-policy.d.ts +26 -0
  90. package/dist/core/sdd/enterprise-provisioning-policy.js +104 -0
  91. package/dist/core/sdd/foundation-artifact-map-validator.d.ts +16 -0
  92. package/dist/core/sdd/foundation-artifact-map-validator.js +71 -0
  93. package/dist/core/sdd/foundation-layer-manifest.d.ts +24 -0
  94. package/dist/core/sdd/foundation-layer-manifest.js +117 -0
  95. package/dist/core/sdd/governance-schemas.d.ts +2 -2
  96. package/dist/core/sdd/governance-schemas.js +11 -2
  97. package/dist/core/sdd/intent-guard.d.ts +22 -0
  98. package/dist/core/sdd/intent-guard.js +67 -0
  99. package/dist/core/sdd/json-schema.js +13 -1
  100. package/dist/core/sdd/legacy-operations.js +169 -5
  101. package/dist/core/sdd/migrate-workspace.js +39 -0
  102. package/dist/core/sdd/package-security-gates.d.ts +21 -0
  103. package/dist/core/sdd/package-security-gates.js +121 -0
  104. package/dist/core/sdd/package-structure-gate.d.ts +85 -3
  105. package/dist/core/sdd/package-structure-gate.js +384 -11
  106. package/dist/core/sdd/parallel-feat-automation.d.ts +185 -7
  107. package/dist/core/sdd/parallel-feat-automation.js +212 -0
  108. package/dist/core/sdd/plugin-broker.d.ts +223 -4
  109. package/dist/core/sdd/plugin-broker.js +10 -0
  110. package/dist/core/sdd/plugin-cli.d.ts +30 -0
  111. package/dist/core/sdd/plugin-cli.js +70 -3
  112. package/dist/core/sdd/plugin-evidence.d.ts +73 -0
  113. package/dist/core/sdd/plugin-manifest.d.ts +69 -1
  114. package/dist/core/sdd/plugin-manifest.js +10 -0
  115. package/dist/core/sdd/plugin-policy-pack.d.ts +1 -1
  116. package/dist/core/sdd/plugin-policy.js +6 -1
  117. package/dist/core/sdd/plugin-registry.d.ts +138 -2
  118. package/dist/core/sdd/plugin-sdk-contract.d.ts +363 -0
  119. package/dist/core/sdd/plugin-sdk-contract.js +268 -0
  120. package/dist/core/sdd/plugin-skill-binding.d.ts +1 -1
  121. package/dist/core/sdd/quality-validation.d.ts +89 -16
  122. package/dist/core/sdd/release-readiness.d.ts +68 -0
  123. package/dist/core/sdd/release-readiness.js +767 -0
  124. package/dist/core/sdd/reversa-architecture-extractor.d.ts +13 -0
  125. package/dist/core/sdd/reversa-architecture-extractor.js +89 -0
  126. package/dist/core/sdd/reversa-artifact-writer.d.ts +18 -0
  127. package/dist/core/sdd/reversa-artifact-writer.js +40 -0
  128. package/dist/core/sdd/reversa-command-policy.d.ts +136 -0
  129. package/dist/core/sdd/reversa-command-policy.js +361 -0
  130. package/dist/core/sdd/reversa-data-extractor.d.ts +11 -0
  131. package/dist/core/sdd/reversa-data-extractor.js +73 -0
  132. package/dist/core/sdd/reversa-equivalence.d.ts +20 -0
  133. package/dist/core/sdd/reversa-equivalence.js +34 -0
  134. package/dist/core/sdd/reversa-evidence.d.ts +298 -0
  135. package/dist/core/sdd/reversa-evidence.js +118 -0
  136. package/dist/core/sdd/reversa-reconstruction.d.ts +29 -0
  137. package/dist/core/sdd/reversa-reconstruction.js +32 -0
  138. package/dist/core/sdd/reversa-rules-extractor.d.ts +12 -0
  139. package/dist/core/sdd/reversa-rules-extractor.js +86 -0
  140. package/dist/core/sdd/reversa-source-safety.d.ts +19 -0
  141. package/dist/core/sdd/reversa-source-safety.js +105 -0
  142. package/dist/core/sdd/reversa-surface-scout.d.ts +13 -0
  143. package/dist/core/sdd/reversa-surface-scout.js +85 -0
  144. package/dist/core/sdd/reversa-ux-mapper.d.ts +11 -0
  145. package/dist/core/sdd/reversa-ux-mapper.js +73 -0
  146. package/dist/core/sdd/runtime-boundary-contract.d.ts +45 -0
  147. package/dist/core/sdd/runtime-boundary-contract.js +90 -0
  148. package/dist/core/sdd/sdk-agent-plugin-quality-gates.d.ts +150 -0
  149. package/dist/core/sdd/sdk-agent-plugin-quality-gates.js +258 -0
  150. package/dist/core/sdd/services/agent-run.service.d.ts +38 -6
  151. package/dist/core/sdd/services/agent-run.service.js +73 -1
  152. package/dist/core/sdd/services/archive-quality-coherence.service.d.ts +17 -0
  153. package/dist/core/sdd/services/archive-quality-coherence.service.js +141 -0
  154. package/dist/core/sdd/services/capability-diff.service.d.ts +18 -0
  155. package/dist/core/sdd/services/capability-diff.service.js +26 -0
  156. package/dist/core/sdd/services/change-safety-preflight.service.d.ts +17 -0
  157. package/dist/core/sdd/services/change-safety-preflight.service.js +17 -0
  158. package/dist/core/sdd/services/context.service.d.ts +43 -340
  159. package/dist/core/sdd/services/context.service.js +323 -9
  160. package/dist/core/sdd/services/decide.service.js +1 -1
  161. package/dist/core/sdd/services/finalize.service.d.ts +27 -0
  162. package/dist/core/sdd/services/finalize.service.js +226 -18
  163. package/dist/core/sdd/services/frontend-impact.service.d.ts +1 -1
  164. package/dist/core/sdd/services/historical-quality-regression.service.d.ts +35 -0
  165. package/dist/core/sdd/services/historical-quality-regression.service.js +228 -0
  166. package/dist/core/sdd/services/ingest-deposito.service.js +1 -1
  167. package/dist/core/sdd/services/planning-execution-coherence.service.d.ts +45 -0
  168. package/dist/core/sdd/services/planning-execution-coherence.service.js +225 -0
  169. package/dist/core/sdd/services/semantic-intent-classifier.service.d.ts +6 -0
  170. package/dist/core/sdd/services/semantic-intent-classifier.service.js +7 -0
  171. package/dist/core/sdd/state.d.ts +1 -0
  172. package/dist/core/sdd/state.js +266 -34
  173. package/dist/core/sdd/store/sdd-stores.js +2 -2
  174. package/dist/core/sdd/structural-health.d.ts +13 -13
  175. package/dist/core/sdd/types.d.ts +30 -15
  176. package/dist/core/sdd/types.js +4 -0
  177. package/dist/core/sdd/views.js +17 -0
  178. package/dist/core/sdd/workspace-schemas.d.ts +428 -7
  179. package/dist/core/sdd/workspace-schemas.js +223 -70
  180. package/dist/core/shared/skill-generation.d.ts +2 -0
  181. package/dist/core/shared/skill-generation.js +19 -2
  182. package/dist/core/shared/tool-detection.d.ts +19 -0
  183. package/dist/core/shared/tool-detection.js +89 -0
  184. package/dist/domains/sdd/index.d.ts +6 -0
  185. package/dist/domains/sdd/index.js +6 -0
  186. package/dist/infrastructures/sdd/index.d.ts +7 -0
  187. package/dist/infrastructures/sdd/index.js +6 -0
  188. package/dist/presentations/cli/sdd/index.d.ts +3 -0
  189. package/dist/presentations/cli/sdd/index.js +3 -0
  190. package/dist/shared/sdd/index.d.ts +3 -0
  191. package/dist/shared/sdd/index.js +2 -0
  192. package/package.json +14 -10
  193. package/schemas/sdd/2-plan.schema.json +207 -2
  194. package/schemas/sdd/5-quality.schema.json +324 -25
  195. package/schemas/sdd/agent-runtime-command-plan.schema.json +212 -0
  196. package/schemas/sdd/agent-runtime-opencode-run-evidence.schema.json +270 -0
  197. package/schemas/sdd/codesdd-plugin.schema.json +171 -0
  198. package/schemas/sdd/deepagent-run-request.schema.json +316 -0
  199. package/schemas/sdd/parallel-feat-automation-plan.schema.json +89 -0
  200. package/schemas/sdd/parallel-feat-scheduler-request.schema.json +116 -0
  201. package/schemas/sdd/parallel-feat-scheduler-result.schema.json +404 -0
  202. package/schemas/sdd/plugin-artifact-manifest.schema.json +109 -0
  203. package/schemas/sdd/plugin-artifact-map.schema.json +223 -0
  204. package/schemas/sdd/plugin-evidence-manifest.schema.json +109 -0
  205. package/schemas/sdd/plugin-language-runtime.schema.json +103 -0
  206. package/schemas/sdd/plugin-package-governance.schema.json +74 -0
  207. package/schemas/sdd/plugin-registry.schema.json +171 -0
  208. package/schemas/sdd/plugin-runtime-invocation-plan.schema.json +109 -0
  209. package/schemas/sdd/quality-evidence-bundle.schema.json +109 -0
  210. package/schemas/sdd/reversa-evidence-bundle.schema.json +466 -0
  211. package/schemas/sdd/sdk-agent-plugin-quality-gate-input.schema.json +168 -0
  212. package/schemas/sdd/sdk-agent-plugin-quality-gate-report.schema.json +160 -0
  213. package/schemas/sdd/workspace-catalog.schema.json +5298 -1409
@@ -0,0 +1,698 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createClient } from 'redis';
3
+ import { StateLockTimeoutError } from '../state-lock.js';
4
+ const DEFAULT_CONNECT_TIMEOUT_MS = 500;
5
+ const DEFAULT_COMMAND_TIMEOUT_MS = 1_000;
6
+ const DEFAULT_MAX_RETRIES = 2;
7
+ const DEFAULT_CACHE_TTL_MS = 300_000;
8
+ const DEFAULT_LOCK_TTL_MS = 30_000;
9
+ const DEFAULT_STREAM_MAX_LEN = 10_000;
10
+ const DEFAULT_NAMESPACE = 'codesdd';
11
+ const REDIS_SCHEMA_VERSION = 1;
12
+ function parseBoolean(value) {
13
+ if (value === undefined)
14
+ return undefined;
15
+ const normalized = value.trim().toLowerCase();
16
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes')
17
+ return true;
18
+ if (normalized === 'false' || normalized === '0' || normalized === 'no')
19
+ return false;
20
+ return undefined;
21
+ }
22
+ function parsePositiveInteger(value) {
23
+ if (value === undefined || value.trim() === '')
24
+ return undefined;
25
+ const parsed = Number.parseInt(value, 10);
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
27
+ }
28
+ function parseNonNegativeInteger(value) {
29
+ if (value === undefined || value.trim() === '')
30
+ return undefined;
31
+ const parsed = Number.parseInt(value, 10);
32
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
33
+ }
34
+ function envString(env, key) {
35
+ const value = env[key]?.trim();
36
+ return value ? value : undefined;
37
+ }
38
+ function normalizeFallback(value) {
39
+ if (value === 'filesystem' || value === 'none')
40
+ return value;
41
+ return undefined;
42
+ }
43
+ function normalizeNamespace(value) {
44
+ const namespace = value?.trim() || DEFAULT_NAMESPACE;
45
+ return namespace;
46
+ }
47
+ function validateNamespace(namespace) {
48
+ if (!/^[A-Za-z0-9:_-]{1,80}$/.test(namespace)) {
49
+ return 'Redis namespace must use only letters, numbers, colon, underscore, or hyphen and be at most 80 characters.';
50
+ }
51
+ return undefined;
52
+ }
53
+ export function redactRedisUrl(url) {
54
+ if (!url)
55
+ return undefined;
56
+ try {
57
+ const parsed = new URL(url);
58
+ const auth = parsed.username || parsed.password ? '[REDACTED]@' : '';
59
+ return `${parsed.protocol}//${auth}${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`;
60
+ }
61
+ catch {
62
+ return '[REDACTED_REDIS_URL]';
63
+ }
64
+ }
65
+ export function sanitizeRedisError(error, config) {
66
+ const raw = error instanceof Error ? error.message : String(error);
67
+ const redactedUrl = redactRedisUrl(config?.url);
68
+ let sanitized = raw;
69
+ if (config?.url) {
70
+ sanitized = sanitized.split(config.url).join(redactedUrl ?? '[REDACTED_REDIS_URL]');
71
+ }
72
+ sanitized = sanitized.replace(/redis(s)?:\/\/[^\s]+/giu, '[REDACTED_REDIS_URL]');
73
+ return sanitized;
74
+ }
75
+ export function hashRedisKey(input) {
76
+ return createHash('sha256').update(input).digest('hex');
77
+ }
78
+ export function buildRedisKey(config, projectFingerprint, domain, key) {
79
+ return [
80
+ config.namespace,
81
+ `v${REDIS_SCHEMA_VERSION}`,
82
+ projectFingerprint,
83
+ domain,
84
+ hashRedisKey(key),
85
+ ].join(':');
86
+ }
87
+ export function resolveRedisRuntimeConfig(env = process.env, globalConfig = {}) {
88
+ const redisConfig = globalConfig.redis ?? {};
89
+ const warnings = [];
90
+ const validationErrors = [];
91
+ const primaryEnvUrl = envString(env, 'CODESDD_REDIS_URL');
92
+ const fallbackEnvUrl = envString(env, 'REDIS_URL');
93
+ if (primaryEnvUrl && fallbackEnvUrl && primaryEnvUrl !== fallbackEnvUrl) {
94
+ validationErrors.push('CODESDD_REDIS_URL and REDIS_URL are both set with different values.');
95
+ }
96
+ const urlFromConfiguredEnv = redisConfig.url_env ? envString(env, redisConfig.url_env) : undefined;
97
+ const url = primaryEnvUrl || fallbackEnvUrl || urlFromConfiguredEnv || redisConfig.url?.trim() || undefined;
98
+ const enabledFromEnv = parseBoolean(env.CODESDD_REDIS_ENABLED);
99
+ const enabled = enabledFromEnv ?? redisConfig.enabled ?? Boolean(url);
100
+ const namespace = normalizeNamespace(envString(env, 'CODESDD_REDIS_NAMESPACE') || redisConfig.namespace);
101
+ const namespaceError = validateNamespace(namespace);
102
+ if (namespaceError)
103
+ validationErrors.push(namespaceError);
104
+ const tlsFromEnv = parseBoolean(env.CODESDD_REDIS_TLS);
105
+ const tls = tlsFromEnv ?? redisConfig.tls ?? Boolean(url?.startsWith('rediss://'));
106
+ const connectTimeoutMs = parsePositiveInteger(env.CODESDD_REDIS_CONNECT_TIMEOUT_MS) ??
107
+ redisConfig.connect_timeout_ms ??
108
+ DEFAULT_CONNECT_TIMEOUT_MS;
109
+ const commandTimeoutMs = parsePositiveInteger(env.CODESDD_REDIS_COMMAND_TIMEOUT_MS) ??
110
+ redisConfig.command_timeout_ms ??
111
+ DEFAULT_COMMAND_TIMEOUT_MS;
112
+ const maxRetries = parseNonNegativeInteger(env.CODESDD_REDIS_MAX_RETRIES) ??
113
+ redisConfig.max_retries ??
114
+ DEFAULT_MAX_RETRIES;
115
+ const fallback = normalizeFallback(env.CODESDD_REDIS_FALLBACK) ??
116
+ normalizeFallback(redisConfig.fallback) ??
117
+ 'filesystem';
118
+ const cacheDefaultTtlMs = parsePositiveInteger(env.CODESDD_REDIS_CACHE_TTL_MS) ??
119
+ redisConfig.cache_default_ttl_ms ??
120
+ DEFAULT_CACHE_TTL_MS;
121
+ const lockTtlMs = parsePositiveInteger(env.CODESDD_REDIS_LOCK_TTL_MS) ??
122
+ redisConfig.lock_ttl_ms ??
123
+ DEFAULT_LOCK_TTL_MS;
124
+ const streamMaxLen = parsePositiveInteger(env.CODESDD_REDIS_STREAM_MAX_LEN) ??
125
+ redisConfig.stream_max_len ??
126
+ DEFAULT_STREAM_MAX_LEN;
127
+ if (url) {
128
+ try {
129
+ const parsed = new URL(url);
130
+ if (parsed.protocol !== 'redis:' && parsed.protocol !== 'rediss:') {
131
+ validationErrors.push('Redis URL scheme must be redis:// or rediss://.');
132
+ }
133
+ }
134
+ catch {
135
+ validationErrors.push('Redis URL is invalid.');
136
+ }
137
+ }
138
+ if (enabled && !url) {
139
+ warnings.push('Redis is enabled but no Redis URL is configured.');
140
+ }
141
+ return {
142
+ requested: Boolean(enabled || url),
143
+ enabled,
144
+ namespace,
145
+ url,
146
+ redactedUrl: redactRedisUrl(url),
147
+ tls,
148
+ connectTimeoutMs,
149
+ commandTimeoutMs,
150
+ maxRetries,
151
+ fallback,
152
+ cacheDefaultTtlMs,
153
+ lockTtlMs,
154
+ streamMaxLen,
155
+ validationErrors,
156
+ warnings,
157
+ };
158
+ }
159
+ export function resolveRedisBoundaryConfig(env = process.env, globalConfig = {}) {
160
+ return resolveRedisRuntimeConfig(env, globalConfig);
161
+ }
162
+ function defaultRedisClientCreator(config) {
163
+ if (!config.url) {
164
+ throw new Error('Redis URL is required to create a client.');
165
+ }
166
+ const socket = config.tls
167
+ ? {
168
+ tls: true,
169
+ connectTimeout: config.connectTimeoutMs,
170
+ socketTimeout: config.commandTimeoutMs,
171
+ reconnectStrategy: (retries) => retries >= config.maxRetries ? false : Math.min(50 * (retries + 1), 250),
172
+ }
173
+ : {
174
+ tls: false,
175
+ connectTimeout: config.connectTimeoutMs,
176
+ socketTimeout: config.commandTimeoutMs,
177
+ reconnectStrategy: (retries) => retries >= config.maxRetries ? false : Math.min(50 * (retries + 1), 250),
178
+ };
179
+ const client = createClient({
180
+ url: config.url,
181
+ socket,
182
+ disableOfflineQueue: true,
183
+ });
184
+ return client;
185
+ }
186
+ async function withTimeout(promise, timeoutMs, label) {
187
+ let timer;
188
+ try {
189
+ return await Promise.race([
190
+ promise,
191
+ new Promise((_, reject) => {
192
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
193
+ }),
194
+ ]);
195
+ }
196
+ finally {
197
+ if (timer)
198
+ clearTimeout(timer);
199
+ }
200
+ }
201
+ export class RedisClientFactory {
202
+ config;
203
+ createClientImpl;
204
+ client;
205
+ lastError;
206
+ constructor(config, createClientImpl = defaultRedisClientCreator) {
207
+ this.config = config;
208
+ this.createClientImpl = createClientImpl;
209
+ }
210
+ async getClient() {
211
+ if (!this.config.url) {
212
+ throw new Error('Redis URL is not configured.');
213
+ }
214
+ if (this.client?.isReady) {
215
+ return this.client;
216
+ }
217
+ if (!this.client) {
218
+ const client = this.createClientImpl(this.config);
219
+ client.on?.('error', (error) => {
220
+ this.lastError = sanitizeRedisError(error, this.config);
221
+ });
222
+ this.client = client;
223
+ }
224
+ if (!this.client.isOpen && !this.client.isReady) {
225
+ await withTimeout(this.client.connect(), this.config.connectTimeoutMs, 'Redis connect');
226
+ }
227
+ return this.client;
228
+ }
229
+ async ping() {
230
+ const started = performance.now();
231
+ try {
232
+ const client = await this.getClient();
233
+ const response = await withTimeout(client.ping(), this.config.commandTimeoutMs, 'Redis ping');
234
+ return {
235
+ ok: response === 'PONG' || response.length > 0,
236
+ latencyMs: Math.round(performance.now() - started),
237
+ message: response,
238
+ };
239
+ }
240
+ catch (error) {
241
+ return {
242
+ ok: false,
243
+ latencyMs: Math.round(performance.now() - started),
244
+ message: sanitizeRedisError(error, this.config),
245
+ };
246
+ }
247
+ }
248
+ async command(args) {
249
+ const client = await this.getClient();
250
+ return withTimeout(client.sendCommand(args), this.config.commandTimeoutMs, `Redis ${args[0] ?? 'command'}`);
251
+ }
252
+ async close() {
253
+ if (!this.client)
254
+ return;
255
+ const client = this.client;
256
+ this.client = undefined;
257
+ try {
258
+ if (client.close && (client.isOpen || client.isReady)) {
259
+ await client.close();
260
+ return;
261
+ }
262
+ client.destroy?.();
263
+ }
264
+ catch {
265
+ // Shutdown is best effort; connection errors are reported by the operation itself.
266
+ }
267
+ }
268
+ getLastError() {
269
+ return this.lastError;
270
+ }
271
+ }
272
+ export async function buildRedisOperationalReport(options = {}) {
273
+ const config = resolveRedisRuntimeConfig(options.env, options.globalConfig);
274
+ if (!config.requested) {
275
+ return {
276
+ requested: false,
277
+ namespace: config.namespace,
278
+ status: 'disabled',
279
+ fallback: config.fallback,
280
+ reason: 'Redis is optional and disabled; filesystem-first defaults are authoritative.',
281
+ validation_errors: [],
282
+ warnings: config.warnings,
283
+ };
284
+ }
285
+ if (config.validationErrors.length > 0) {
286
+ return {
287
+ requested: true,
288
+ namespace: config.namespace,
289
+ status: 'blocked',
290
+ fallback: config.fallback,
291
+ redacted_url: config.redactedUrl,
292
+ reason: 'Redis configuration is invalid and was blocked fail-closed.',
293
+ validation_errors: config.validationErrors,
294
+ warnings: config.warnings,
295
+ };
296
+ }
297
+ if (!config.url) {
298
+ return {
299
+ requested: true,
300
+ namespace: config.namespace,
301
+ status: config.fallback === 'none' ? 'blocked' : 'requested-unavailable',
302
+ fallback: config.fallback,
303
+ reason: config.fallback === 'none'
304
+ ? 'Redis is required but no Redis URL is configured.'
305
+ : 'Redis is requested but no Redis URL is configured; filesystem fallback remains active.',
306
+ validation_errors: [],
307
+ warnings: config.warnings,
308
+ };
309
+ }
310
+ const factory = new RedisClientFactory(config, options.createClient);
311
+ try {
312
+ const ping = await factory.ping();
313
+ if (!ping.ok) {
314
+ return {
315
+ requested: true,
316
+ namespace: config.namespace,
317
+ status: config.fallback === 'none' ? 'blocked' : 'requested-unavailable',
318
+ fallback: config.fallback,
319
+ redacted_url: config.redactedUrl,
320
+ latency_ms: ping.latencyMs,
321
+ reason: ping.message,
322
+ validation_errors: [],
323
+ warnings: config.warnings,
324
+ };
325
+ }
326
+ return {
327
+ requested: true,
328
+ namespace: config.namespace,
329
+ status: 'ready',
330
+ fallback: config.fallback,
331
+ redacted_url: config.redactedUrl,
332
+ latency_ms: ping.latencyMs,
333
+ reason: 'Redis ping succeeded.',
334
+ validation_errors: [],
335
+ warnings: config.warnings,
336
+ };
337
+ }
338
+ finally {
339
+ await factory.close();
340
+ }
341
+ }
342
+ function ttlSeconds(ttlMs) {
343
+ return String(Math.max(1, Math.ceil(ttlMs / 1000)));
344
+ }
345
+ function remainingTtlMs(expiresAt) {
346
+ if (!expiresAt)
347
+ return -1;
348
+ const parsed = Date.parse(expiresAt);
349
+ if (Number.isNaN(parsed))
350
+ return 0;
351
+ return Math.max(0, parsed - Date.now());
352
+ }
353
+ export class RedisCoordinationCache {
354
+ redis;
355
+ fallback;
356
+ projectFingerprint;
357
+ domain;
358
+ constructor(redis, fallback, projectFingerprint, domain = 'cache') {
359
+ this.redis = redis;
360
+ this.fallback = fallback;
361
+ this.projectFingerprint = projectFingerprint;
362
+ this.domain = domain;
363
+ }
364
+ async get(key) {
365
+ const fallbackValue = await this.fallback.get(key);
366
+ if (typeof fallbackValue !== 'undefined')
367
+ return fallbackValue;
368
+ try {
369
+ const redisKey = buildRedisKey(this.redis.config, this.projectFingerprint, this.domain, key);
370
+ const raw = await this.redis.command(['GET', redisKey]);
371
+ if (!raw)
372
+ return undefined;
373
+ const entry = JSON.parse(raw);
374
+ if (entry.schema_version !== REDIS_SCHEMA_VERSION)
375
+ return undefined;
376
+ const ttlMs = remainingTtlMs(entry.expires_at);
377
+ if (ttlMs === 0) {
378
+ await this.redis.command(['DEL', redisKey]);
379
+ return undefined;
380
+ }
381
+ await this.fallback.set(key, entry.value, ttlMs > 0 ? { ttlMs } : {});
382
+ return entry.value;
383
+ }
384
+ catch {
385
+ return this.fallback.get(key);
386
+ }
387
+ }
388
+ async set(key, value, options = {}) {
389
+ await this.fallback.set(key, value, options);
390
+ const ttlMs = options.ttlMs ?? this.redis.config.cacheDefaultTtlMs;
391
+ const entry = {
392
+ schema_version: REDIS_SCHEMA_VERSION,
393
+ value,
394
+ created_at: new Date().toISOString(),
395
+ expires_at: new Date(Date.now() + ttlMs).toISOString(),
396
+ };
397
+ try {
398
+ await this.redis.command(['SET', buildRedisKey(this.redis.config, this.projectFingerprint, this.domain, key), JSON.stringify(entry), 'EX', ttlSeconds(ttlMs)]);
399
+ }
400
+ catch {
401
+ // Redis is an acceleration tier; filesystem fallback already persisted the value.
402
+ }
403
+ }
404
+ async delete(key) {
405
+ await this.fallback.delete(key);
406
+ try {
407
+ await this.redis.command(['DEL', buildRedisKey(this.redis.config, this.projectFingerprint, this.domain, key)]);
408
+ }
409
+ catch {
410
+ // best effort
411
+ }
412
+ }
413
+ async clear() {
414
+ await this.fallback.clear();
415
+ await deleteKeysByPrefix(this.redis, `${this.redis.config.namespace}:v${REDIS_SCHEMA_VERSION}:${this.projectFingerprint}:${this.domain}:`);
416
+ }
417
+ }
418
+ export class RedisLockAdapter {
419
+ redis;
420
+ fallback;
421
+ projectFingerprint;
422
+ constructor(redis, fallback, projectFingerprint) {
423
+ this.redis = redis;
424
+ this.fallback = fallback;
425
+ this.projectFingerprint = projectFingerprint;
426
+ }
427
+ async withLock(scope, fn, options = {}) {
428
+ if (!this.redis.config.url || this.redis.config.validationErrors.length > 0) {
429
+ return this.fallback.withLock(scope, fn, options);
430
+ }
431
+ const token = randomUUID();
432
+ const ttlMs = options.staleMs ?? this.redis.config.lockTtlMs;
433
+ const timeoutMs = options.timeoutMs ?? 30_000;
434
+ const deadline = Date.now() + timeoutMs;
435
+ const key = buildRedisKey(this.redis.config, this.projectFingerprint, 'lock', scope);
436
+ const fencingKey = buildRedisKey(this.redis.config, this.projectFingerprint, 'fence', scope);
437
+ while (true) {
438
+ if (options.signal?.aborted) {
439
+ throw new StateLockTimeoutError(key);
440
+ }
441
+ try {
442
+ const response = await this.redis.command(['SET', key, token, 'PX', String(ttlMs), 'NX']);
443
+ if (response === 'OK') {
444
+ await this.redis.command(['INCR', fencingKey]);
445
+ try {
446
+ return await fn();
447
+ }
448
+ finally {
449
+ await this.release(key, token);
450
+ }
451
+ }
452
+ }
453
+ catch {
454
+ if (this.redis.config.fallback === 'filesystem') {
455
+ return this.fallback.withLock(scope, fn, options);
456
+ }
457
+ throw new StateLockTimeoutError(key);
458
+ }
459
+ if (Date.now() >= deadline) {
460
+ throw new StateLockTimeoutError(key);
461
+ }
462
+ await new Promise((resolve) => setTimeout(resolve, 100));
463
+ }
464
+ }
465
+ async extend(scope, ownerToken, ttlMs = this.redis.config.lockTtlMs) {
466
+ const key = buildRedisKey(this.redis.config, this.projectFingerprint, 'lock', scope);
467
+ const result = await this.redis.command([
468
+ 'EVAL',
469
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end",
470
+ '1',
471
+ key,
472
+ ownerToken,
473
+ String(ttlMs),
474
+ ]);
475
+ return Number(result) === 1;
476
+ }
477
+ async release(key, token) {
478
+ try {
479
+ await this.redis.command([
480
+ 'EVAL',
481
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
482
+ '1',
483
+ key,
484
+ token,
485
+ ]);
486
+ }
487
+ catch {
488
+ // Lock release is best effort after TTL; never expose Redis details here.
489
+ }
490
+ }
491
+ }
492
+ export class RedisQueueAdapter {
493
+ redis;
494
+ fallback;
495
+ projectFingerprint;
496
+ constructor(redis, fallback, projectFingerprint) {
497
+ this.redis = redis;
498
+ this.fallback = fallback;
499
+ this.projectFingerprint = projectFingerprint;
500
+ }
501
+ async enqueue(topic, item) {
502
+ try {
503
+ const key = buildRedisKey(this.redis.config, this.projectFingerprint, 'queue', topic);
504
+ await this.redis.command([
505
+ 'XADD',
506
+ key,
507
+ 'MAXLEN',
508
+ '~',
509
+ String(this.redis.config.streamMaxLen),
510
+ '*',
511
+ 'payload',
512
+ JSON.stringify(item),
513
+ ]);
514
+ }
515
+ catch {
516
+ await this.fallback.enqueue(topic, item);
517
+ }
518
+ }
519
+ async dequeue(topic) {
520
+ try {
521
+ const key = buildRedisKey(this.redis.config, this.projectFingerprint, 'queue', topic);
522
+ const reply = await this.redis.command(['XREAD', 'COUNT', '1', 'STREAMS', key, '0']);
523
+ const parsed = parseFirstStreamPayload(reply);
524
+ if (!parsed)
525
+ return undefined;
526
+ await this.redis.command(['XDEL', key, parsed.id]);
527
+ return parsed.payload;
528
+ }
529
+ catch {
530
+ return this.fallback.dequeue(topic);
531
+ }
532
+ }
533
+ async size(topic) {
534
+ try {
535
+ const key = buildRedisKey(this.redis.config, this.projectFingerprint, 'queue', topic);
536
+ const size = await this.redis.command(['XLEN', key]);
537
+ return Number(size);
538
+ }
539
+ catch {
540
+ return this.fallback.size(topic);
541
+ }
542
+ }
543
+ }
544
+ export class RedisEventBusAdapter {
545
+ redis;
546
+ fallback;
547
+ projectFingerprint;
548
+ constructor(redis, fallback, projectFingerprint) {
549
+ this.redis = redis;
550
+ this.fallback = fallback;
551
+ this.projectFingerprint = projectFingerprint;
552
+ }
553
+ async publish(event) {
554
+ try {
555
+ const streamKey = buildRedisKey(this.redis.config, this.projectFingerprint, 'events', event.type);
556
+ const channelKey = `${this.redis.config.namespace}:events:${hashRedisKey(event.type)}`;
557
+ const payload = JSON.stringify(event);
558
+ await this.redis.command([
559
+ 'XADD',
560
+ streamKey,
561
+ 'MAXLEN',
562
+ '~',
563
+ String(this.redis.config.streamMaxLen),
564
+ '*',
565
+ 'payload',
566
+ payload,
567
+ ]);
568
+ await this.redis.command(['PUBLISH', channelKey, payload]);
569
+ }
570
+ catch {
571
+ await this.fallback.publish(event);
572
+ }
573
+ }
574
+ async drain() {
575
+ try {
576
+ const prefix = `${this.redis.config.namespace}:v${REDIS_SCHEMA_VERSION}:${this.projectFingerprint}:events:`;
577
+ const keys = await scanKeysByPrefix(this.redis, prefix);
578
+ const events = [];
579
+ for (const key of keys) {
580
+ const reply = await this.redis.command(['XREAD', 'COUNT', '100', 'STREAMS', key, '0']);
581
+ const parsed = parseStreamPayloads(reply);
582
+ events.push(...parsed.map((entry) => entry.payload));
583
+ if (parsed.length > 0) {
584
+ await this.redis.command(['XDEL', key, ...parsed.map((entry) => entry.id)]);
585
+ }
586
+ }
587
+ return events;
588
+ }
589
+ catch {
590
+ return this.fallback.drain();
591
+ }
592
+ }
593
+ }
594
+ export class RedisWorkDeduper {
595
+ redis;
596
+ projectFingerprint;
597
+ constructor(redis, projectFingerprint) {
598
+ this.redis = redis;
599
+ this.projectFingerprint = projectFingerprint;
600
+ }
601
+ async claim(key, ttlMs = this.redis.config.cacheDefaultTtlMs) {
602
+ const redisKey = buildRedisKey(this.redis.config, this.projectFingerprint, 'dedupe', key);
603
+ const result = await this.redis.command(['SET', redisKey, '1', 'PX', String(ttlMs), 'NX']);
604
+ return result === 'OK';
605
+ }
606
+ }
607
+ function parseFirstStreamPayload(reply) {
608
+ return parseStreamPayloads(reply)[0];
609
+ }
610
+ function parseStreamPayloads(reply) {
611
+ const entries = [];
612
+ const streamEntries = Array.isArray(reply)
613
+ ? reply.map((streamEntry) => (Array.isArray(streamEntry) ? streamEntry[1] : undefined))
614
+ : typeof reply === 'object' && reply !== null
615
+ ? Object.values(reply)
616
+ : [];
617
+ for (const stream of streamEntries) {
618
+ if (!Array.isArray(stream))
619
+ continue;
620
+ for (const entry of stream) {
621
+ const parsed = parseStreamEntryPayload(entry);
622
+ if (parsed)
623
+ entries.push(parsed);
624
+ }
625
+ }
626
+ return entries;
627
+ }
628
+ function parseStreamEntryPayload(entry) {
629
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !Array.isArray(entry[1]))
630
+ return undefined;
631
+ const fields = entry[1];
632
+ const payloadIndex = fields.findIndex((field) => field === 'payload');
633
+ if (payloadIndex < 0 || typeof fields[payloadIndex + 1] !== 'string')
634
+ return undefined;
635
+ return { id: entry[0], payload: JSON.parse(fields[payloadIndex + 1]) };
636
+ }
637
+ async function scanKeysByPrefix(redis, prefix) {
638
+ const keys = [];
639
+ let cursor = '0';
640
+ do {
641
+ const reply = await redis.command(['SCAN', cursor, 'MATCH', `${prefix}*`, 'COUNT', '100']);
642
+ cursor = String(reply[0] ?? '0');
643
+ if (Array.isArray(reply[1])) {
644
+ keys.push(...reply[1].filter((key) => typeof key === 'string'));
645
+ }
646
+ } while (cursor !== '0');
647
+ return keys;
648
+ }
649
+ export async function deleteKeysByPrefix(redis, prefix) {
650
+ const keys = await scanKeysByPrefix(redis, prefix);
651
+ if (keys.length === 0)
652
+ return 0;
653
+ const result = await redis.command(['DEL', ...keys]);
654
+ return Number(result);
655
+ }
656
+ export async function runRedisBenchmark(options) {
657
+ const config = resolveRedisRuntimeConfig(options.env, options.globalConfig);
658
+ const iterations = Math.max(1, Math.min(options.iterations ?? 10, 100));
659
+ if (!config.requested || config.validationErrors.length > 0 || !config.url) {
660
+ const report = await buildRedisOperationalReport(options);
661
+ return {
662
+ status: report.status,
663
+ namespace: report.namespace,
664
+ iterations,
665
+ reason: report.reason,
666
+ };
667
+ }
668
+ const factory = new RedisClientFactory(config, options.createClient);
669
+ const samples = [];
670
+ try {
671
+ for (let i = 0; i < iterations; i++) {
672
+ const key = buildRedisKey(config, 'benchmark', 'bench', String(i));
673
+ const started = performance.now();
674
+ await factory.command(['SET', key, '1', 'PX', '1000']);
675
+ await factory.command(['GET', key]);
676
+ samples.push(performance.now() - started);
677
+ }
678
+ samples.sort((a, b) => a - b);
679
+ return {
680
+ status: 'ready',
681
+ namespace: config.namespace,
682
+ iterations,
683
+ p50_ms: Math.round(percentile(samples, 0.5)),
684
+ p95_ms: Math.round(percentile(samples, 0.95)),
685
+ reason: 'Redis benchmark completed.',
686
+ };
687
+ }
688
+ finally {
689
+ await factory.close();
690
+ }
691
+ }
692
+ function percentile(values, quantile) {
693
+ if (values.length === 0)
694
+ return 0;
695
+ const index = Math.min(values.length - 1, Math.ceil(values.length * quantile) - 1);
696
+ return values[index];
697
+ }
698
+ //# sourceMappingURL=redis-runtime.js.map