@ai-sdlc/orchestrator 0.10.0 → 0.14.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 (168) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/analysis/file-walker.js +5 -0
  10. package/dist/backlog-adapter.d.ts +45 -0
  11. package/dist/backlog-adapter.js +145 -1
  12. package/dist/calibration.d.ts +81 -0
  13. package/dist/calibration.js +76 -0
  14. package/dist/cli/commands/git-remote.js +1 -1
  15. package/dist/cli/commands/init-features.d.ts +301 -2
  16. package/dist/cli/commands/init-features.js +647 -7
  17. package/dist/cli/commands/init-templates.d.ts +198 -1
  18. package/dist/cli/commands/init-templates.js +943 -1
  19. package/dist/cli/commands/init.d.ts +45 -0
  20. package/dist/cli/commands/init.js +147 -5
  21. package/dist/cli/commands/run.js +9 -1
  22. package/dist/cli/index.d.ts +11 -0
  23. package/dist/cli/index.js +42 -8
  24. package/dist/compliance/composer.d.ts +79 -0
  25. package/dist/compliance/composer.js +258 -0
  26. package/dist/compliance/errors.d.ts +64 -0
  27. package/dist/compliance/errors.js +85 -0
  28. package/dist/compliance/loader.d.ts +52 -0
  29. package/dist/compliance/loader.js +124 -0
  30. package/dist/compliance/types.d.ts +184 -0
  31. package/dist/compliance/types.js +41 -0
  32. package/dist/compliance-clearance.d.ts +269 -0
  33. package/dist/compliance-clearance.js +269 -0
  34. package/dist/config.js +17 -0
  35. package/dist/cost-tracker.d.ts +22 -0
  36. package/dist/cost-tracker.js +41 -0
  37. package/dist/cycle-utils.js +14 -1
  38. package/dist/database/adapters/external.js +5 -1
  39. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  40. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  41. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  42. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  43. package/dist/embedding/cross-provider.d.ts +78 -0
  44. package/dist/embedding/cross-provider.js +75 -0
  45. package/dist/embedding/deprecation.d.ts +151 -0
  46. package/dist/embedding/deprecation.js +229 -0
  47. package/dist/embedding/errors.d.ts +90 -0
  48. package/dist/embedding/errors.js +150 -0
  49. package/dist/embedding/index.d.ts +29 -0
  50. package/dist/embedding/index.js +24 -0
  51. package/dist/embedding/pipeline-load.d.ts +146 -0
  52. package/dist/embedding/pipeline-load.js +178 -0
  53. package/dist/embedding/registry.d.ts +45 -0
  54. package/dist/embedding/registry.js +61 -0
  55. package/dist/embedding/stale-vector.d.ts +110 -0
  56. package/dist/embedding/stale-vector.js +92 -0
  57. package/dist/embedding/storage/index.d.ts +51 -0
  58. package/dist/embedding/storage/index.js +43 -0
  59. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  60. package/dist/embedding/storage/jsonl-backend.js +332 -0
  61. package/dist/embedding/storage/types.d.ts +135 -0
  62. package/dist/embedding/storage/types.js +13 -0
  63. package/dist/embedding/types.d.ts +180 -0
  64. package/dist/embedding/types.js +10 -0
  65. package/dist/execute.d.ts +29 -2
  66. package/dist/execute.js +200 -38
  67. package/dist/fix-review.js +1 -1
  68. package/dist/index.d.ts +14 -2
  69. package/dist/index.js +19 -2
  70. package/dist/journey/inheritance-validator.d.ts +396 -0
  71. package/dist/journey/inheritance-validator.js +370 -0
  72. package/dist/journey/metric-snapshot.d.ts +403 -0
  73. package/dist/journey/metric-snapshot.js +370 -0
  74. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  75. package/dist/journey/state-id-drift-rule.js +245 -0
  76. package/dist/journey-sa2-router.d.ts +395 -0
  77. package/dist/journey-sa2-router.js +308 -0
  78. package/dist/otel-exporter.js +0 -3
  79. package/dist/runners/claude-code.js +3 -3
  80. package/dist/runners/review-agent.js +6 -2
  81. package/dist/runners/runner-registry.d.ts +36 -0
  82. package/dist/runners/runner-registry.js +90 -0
  83. package/dist/runtime/attestations.d.ts +173 -13
  84. package/dist/runtime/attestations.js +265 -43
  85. package/dist/runtime/index.d.ts +1 -1
  86. package/dist/runtime/index.js +1 -1
  87. package/dist/sa-scoring/exemplar-bank.js +3 -1
  88. package/dist/sa-scoring/layer3-llm.js +6 -1
  89. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  90. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  91. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  92. package/dist/sa-scoring/revision-proposal.js +417 -0
  93. package/dist/shared.d.ts +28 -0
  94. package/dist/shared.js +37 -0
  95. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  96. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  97. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  98. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  99. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  100. package/dist/signal-ingestion/adapters/manual.js +112 -0
  101. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  102. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  103. package/dist/signal-ingestion/classifier.d.ts +205 -0
  104. package/dist/signal-ingestion/classifier.js +494 -0
  105. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  106. package/dist/signal-ingestion/clustering-types.js +14 -0
  107. package/dist/signal-ingestion/clustering.d.ts +200 -0
  108. package/dist/signal-ingestion/clustering.js +413 -0
  109. package/dist/signal-ingestion/config.d.ts +351 -0
  110. package/dist/signal-ingestion/config.js +587 -0
  111. package/dist/signal-ingestion/d1.d.ts +252 -0
  112. package/dist/signal-ingestion/d1.js +235 -0
  113. package/dist/signal-ingestion/errors.d.ts +73 -0
  114. package/dist/signal-ingestion/errors.js +108 -0
  115. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  116. package/dist/signal-ingestion/governance-events.js +189 -0
  117. package/dist/signal-ingestion/index.d.ts +35 -0
  118. package/dist/signal-ingestion/index.js +53 -0
  119. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  120. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  121. package/dist/signal-ingestion/registry.d.ts +40 -0
  122. package/dist/signal-ingestion/registry.js +137 -0
  123. package/dist/signal-ingestion/residency.d.ts +227 -0
  124. package/dist/signal-ingestion/residency.js +238 -0
  125. package/dist/signal-ingestion/significance.d.ts +554 -0
  126. package/dist/signal-ingestion/significance.js +555 -0
  127. package/dist/signal-ingestion/types.d.ts +191 -0
  128. package/dist/signal-ingestion/types.js +8 -0
  129. package/dist/substrate/drift-composition.d.ts +270 -0
  130. package/dist/substrate/drift-composition.js +306 -0
  131. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  132. package/dist/substrate/drift-tui-surface.js +102 -0
  133. package/dist/substrate/identity-class.d.ts +176 -0
  134. package/dist/substrate/identity-class.js +201 -0
  135. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  136. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  137. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  138. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  139. package/dist/tessellation/rule-registry.d.ts +269 -0
  140. package/dist/tessellation/rule-registry.js +92 -0
  141. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  142. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  143. package/dist/tessellation-admission.d.ts +162 -0
  144. package/dist/tessellation-admission.js +146 -0
  145. package/dist/tessellation-drift.d.ts +246 -0
  146. package/dist/tessellation-drift.js +250 -0
  147. package/dist/validate-config.js +13 -0
  148. package/dist/validate-issue.js +2 -2
  149. package/dist/variant/cardinality-activation.d.ts +126 -0
  150. package/dist/variant/cardinality-activation.js +101 -0
  151. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  152. package/dist/variant/deprecation-lifecycle.js +208 -0
  153. package/dist/variant/drift-extension.d.ts +136 -0
  154. package/dist/variant/drift-extension.js +164 -0
  155. package/dist/variant/engineering-review.d.ts +185 -0
  156. package/dist/variant/engineering-review.js +142 -0
  157. package/dist/variant/index.d.ts +32 -0
  158. package/dist/variant/index.js +32 -0
  159. package/dist/variant/inheritance-validator.d.ts +165 -0
  160. package/dist/variant/inheritance-validator.js +139 -0
  161. package/dist/variant/internal-adopter/index.d.ts +11 -0
  162. package/dist/variant/internal-adopter/index.js +10 -0
  163. package/dist/variant/internal-adopter/products.d.ts +156 -0
  164. package/dist/variant/internal-adopter/products.js +366 -0
  165. package/dist/variant-admission.d.ts +316 -0
  166. package/dist/variant-admission.js +247 -0
  167. package/dist/webhook-manager.js +7 -1
  168. package/package.json +15 -12
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Signal ingestion configuration loader for RFC-0030 Phase 2.
3
+ *
4
+ * Reads `.ai-sdlc/signal-ingestion.yaml`, validates its shape, and returns a
5
+ * fully-resolved `SignalIngestionConfig` with all defaults applied.
6
+ *
7
+ * Design decisions:
8
+ * - Missing file → returns the default config (pipeline is disabled by default).
9
+ * - Invalid YAML or schema mismatch → throws `SignalIngestionConfigError`.
10
+ * - All numeric fields are validated to be non-negative finite numbers.
11
+ * - `acceptedLanguages` defaults to `['en']` per RFC-0030 OQ-13.2 resolution.
12
+ * - Tier multipliers and ICP resonance weights are read from the config; the
13
+ * defaults match RFC-0030 §11.
14
+ * - `flooding` block (RFC-0030 OQ-13.5 v0.3 re-walkthrough refinement,
15
+ * AISDLC-433) replaces the legacy `sourceBaselineDriftMultiplier` knob
16
+ * with a z-score detector on per-source rolling 7d baseline + quarantine.
17
+ * The legacy `flooding.detection.sourceBaselineDriftMultiplier` field at
18
+ * the LOADER level emits a `signal-ingestion-config-deprecated-field`
19
+ * Decision; the loader translates it to the closest z-score equivalent
20
+ * for one release window then hard-errors after the soak.
21
+ */
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { resolve } from 'node:path';
24
+ import { parse as parseYaml } from 'yaml';
25
+ // ── Default config path ─────────────────────────────────────────────────────
26
+ export const DEFAULT_SIGNAL_INGESTION_CONFIG_PATH = '.ai-sdlc/signal-ingestion.yaml';
27
+ // ── Defaults ────────────────────────────────────────────────────────────────
28
+ export const DEFAULT_SIGNAL_INGESTION_CONFIG = {
29
+ enabled: false,
30
+ tierMultipliers: {
31
+ enterprise: 3.0,
32
+ mid: 1.5,
33
+ smb: 1.0,
34
+ free: 0.5,
35
+ churned: 2.0,
36
+ },
37
+ icpResonanceWeights: {
38
+ strong: 1.5,
39
+ partial: 1.0,
40
+ weak: 0.5,
41
+ },
42
+ recencyHalfLifeDays: 30,
43
+ tier2SignificanceThreshold: {
44
+ minSignalCount: 5,
45
+ minUniqueSources: 3,
46
+ minTier1SignalCount: 1,
47
+ minClusterAgeDays: 7,
48
+ },
49
+ saResonanceThresholds: {
50
+ fullWeight: 0.7,
51
+ discounted: 0.4,
52
+ excluded: 0.0,
53
+ },
54
+ clustering: {
55
+ algorithm: 'bm25',
56
+ similarityThreshold: 0.6,
57
+ },
58
+ d1Composition: {
59
+ signalPipelineWeight: 0.5,
60
+ backlogItemWeight: 0.5,
61
+ },
62
+ // RFC-0030 OQ-13.1 v0.3 — env-var-based adapters only. OAuth-required
63
+ // adapters (full Salesforce / HubSpot integrations, Zendesk-with-OAuth)
64
+ // defer to the future credential-management RFC.
65
+ adapters: [
66
+ 'signal-source-support-ticket',
67
+ 'signal-source-community-thread',
68
+ 'signal-source-in-app-feedback',
69
+ ],
70
+ acceptedLanguages: ['en'],
71
+ languageDetection: {
72
+ library: 'franc',
73
+ minDetectionLength: 50,
74
+ onUndetermined: 'accept',
75
+ },
76
+ residencyEnforcement: {
77
+ sourceFromCompliancePosture: true,
78
+ enforcementPoints: {
79
+ fetchSignals: true,
80
+ clustering: true,
81
+ storage: true,
82
+ unifiedCostReport: true,
83
+ },
84
+ multiPostureBehavior: 'union',
85
+ },
86
+ manualEntry: {
87
+ dailyCapPerOperator: 10,
88
+ evidenceUrlOptional: true,
89
+ qualityMetric: {
90
+ enabled: true,
91
+ windowDays: 7,
92
+ shareWarningThreshold: 0.3,
93
+ },
94
+ },
95
+ flooding: {
96
+ detection: {
97
+ zScoreThreshold: 3.0,
98
+ windowMinutes: 60,
99
+ minUniqueSourcesForSuspicion: 3,
100
+ baselineDays: 7,
101
+ },
102
+ quarantine: {
103
+ enabled: true,
104
+ durationHours: 24,
105
+ },
106
+ },
107
+ };
108
+ // ── Error ───────────────────────────────────────────────────────────────────
109
+ export class SignalIngestionConfigError extends Error {
110
+ cause;
111
+ constructor(message, cause) {
112
+ super(message);
113
+ this.cause = cause;
114
+ this.name = 'SignalIngestionConfigError';
115
+ }
116
+ }
117
+ /**
118
+ * Load and resolve the signal ingestion configuration from
119
+ * `.ai-sdlc/signal-ingestion.yaml`.
120
+ *
121
+ * Returns `DEFAULT_SIGNAL_INGESTION_CONFIG` when the file is absent.
122
+ * Throws `SignalIngestionConfigError` on parse or validation failure.
123
+ */
124
+ export function loadSignalIngestionConfig(options = {}) {
125
+ const projectRoot = options.projectRoot ?? process.cwd();
126
+ const configPath = options.configPath ?? resolve(projectRoot, DEFAULT_SIGNAL_INGESTION_CONFIG_PATH);
127
+ if (!existsSync(configPath)) {
128
+ return { ...DEFAULT_SIGNAL_INGESTION_CONFIG };
129
+ }
130
+ let raw;
131
+ try {
132
+ raw = readFileSync(configPath, 'utf8');
133
+ }
134
+ catch (err) {
135
+ throw new SignalIngestionConfigError(`Failed to read signal ingestion config at ${configPath}`, err);
136
+ }
137
+ let parsed;
138
+ try {
139
+ parsed = parseYaml(raw);
140
+ }
141
+ catch (err) {
142
+ throw new SignalIngestionConfigError(`Failed to parse signal ingestion YAML at ${configPath}`, err);
143
+ }
144
+ return resolveConfig(parsed, configPath);
145
+ }
146
+ // ── Internal resolver ───────────────────────────────────────────────────────
147
+ function resolveConfig(raw, filePath) {
148
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
149
+ throw new SignalIngestionConfigError(`Signal ingestion config at ${filePath} must be a YAML object`);
150
+ }
151
+ const obj = raw;
152
+ // Validate top-level apiVersion / kind when present (advisory, not enforced)
153
+ const spec = obj['spec'] ?? obj;
154
+ return {
155
+ enabled: resolveBoolean(spec['enabled'], DEFAULT_SIGNAL_INGESTION_CONFIG.enabled),
156
+ tierMultipliers: resolveTierMultipliers(spec['tierMultipliers']),
157
+ icpResonanceWeights: resolveIcpResonanceWeights(spec['icpResonanceWeights']),
158
+ recencyHalfLifeDays: resolvePositiveNumber(spec['recencyHalfLifeDays'], DEFAULT_SIGNAL_INGESTION_CONFIG.recencyHalfLifeDays, 'recencyHalfLifeDays'),
159
+ tier2SignificanceThreshold: resolveTier2Threshold(spec['tier2SignificanceThreshold']),
160
+ saResonanceThresholds: resolveSaThresholds(spec['saResonanceThresholds']),
161
+ clustering: resolveClusteringConfig(spec['clustering']),
162
+ d1Composition: resolveD1Composition(spec['d1Composition']),
163
+ adapters: resolveStringArray(spec['adapters'], DEFAULT_SIGNAL_INGESTION_CONFIG.adapters),
164
+ acceptedLanguages: resolveLanguageList(spec['acceptedLanguages']),
165
+ languageDetection: resolveLanguageDetectionConfig(spec['languageDetection']),
166
+ residencyEnforcement: resolveResidencyEnforcement(spec['residencyEnforcement']),
167
+ manualEntry: resolveManualEntry(spec['manualEntry']),
168
+ flooding: resolveFloodingConfig(spec['flooding']),
169
+ };
170
+ }
171
+ function resolveFloodingConfig(value) {
172
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.flooding;
173
+ if (value === undefined || value === null) {
174
+ return { detection: { ...defaults.detection }, quarantine: { ...defaults.quarantine } };
175
+ }
176
+ if (typeof value !== 'object' || Array.isArray(value)) {
177
+ throw new SignalIngestionConfigError('flooding must be an object');
178
+ }
179
+ const obj = value;
180
+ return {
181
+ detection: resolveFloodingDetection(obj['detection']),
182
+ quarantine: resolveFloodingQuarantine(obj['quarantine']),
183
+ };
184
+ }
185
+ function resolveFloodingDetection(value) {
186
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.flooding.detection;
187
+ if (value === undefined || value === null)
188
+ return { ...defaults };
189
+ if (typeof value !== 'object' || Array.isArray(value)) {
190
+ throw new SignalIngestionConfigError('flooding.detection must be an object');
191
+ }
192
+ const obj = value;
193
+ // Reject the deprecated `algorithm` field iff it's not `z-score` — only the
194
+ // z-score algorithm is supported post-AISDLC-433. (Operators with the
195
+ // `algorithm: z-score` line from the v0.3 RFC YAML pass through silently.)
196
+ if (obj['algorithm'] !== undefined &&
197
+ obj['algorithm'] !== null &&
198
+ obj['algorithm'] !== 'z-score') {
199
+ throw new SignalIngestionConfigError(`flooding.detection.algorithm must be 'z-score' (multiplier-based detector removed in AISDLC-433), got ${JSON.stringify(obj['algorithm'])}`);
200
+ }
201
+ return {
202
+ zScoreThreshold: resolvePositiveNumber(obj['zScoreThreshold'], defaults.zScoreThreshold, 'flooding.detection.zScoreThreshold'),
203
+ windowMinutes: resolvePositiveNumber(obj['windowMinutes'], defaults.windowMinutes, 'flooding.detection.windowMinutes'),
204
+ minUniqueSourcesForSuspicion: resolvePositiveNumber(obj['minUniqueSourcesForSuspicion'], defaults.minUniqueSourcesForSuspicion, 'flooding.detection.minUniqueSourcesForSuspicion'),
205
+ baselineDays: resolvePositiveNumber(obj['baselineDays'], defaults.baselineDays, 'flooding.detection.baselineDays'),
206
+ };
207
+ }
208
+ function resolveFloodingQuarantine(value) {
209
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.flooding.quarantine;
210
+ if (value === undefined || value === null)
211
+ return { ...defaults };
212
+ if (typeof value !== 'object' || Array.isArray(value)) {
213
+ throw new SignalIngestionConfigError('flooding.quarantine must be an object');
214
+ }
215
+ const obj = value;
216
+ return {
217
+ enabled: resolveBoolean(obj['enabled'], defaults.enabled),
218
+ durationHours: resolvePositiveNumber(obj['durationHours'], defaults.durationHours, 'flooding.quarantine.durationHours'),
219
+ };
220
+ }
221
+ /**
222
+ * Env-var that flips legacy-field handling from "translate + emit Decision"
223
+ * to "hard-error". The intent: after the one-release-window soak, set this
224
+ * in CI so adopters who haven't migrated fail loudly. AISDLC-433 ships with
225
+ * the var OFF — translation + Decision routing is the cutover behaviour.
226
+ */
227
+ const LEGACY_HARD_ERROR_ENV_VAR = 'AI_SDLC_SIGNAL_INGESTION_LEGACY_HARD_ERROR';
228
+ /**
229
+ * Load + resolve config, AND surface every deprecated-field Decision the
230
+ * loader detected. Wraps `loadSignalIngestionConfig()` so callers that want
231
+ * the deprecation audit trail can opt in without changing the existing
232
+ * Pure-load callers (which keep using `loadSignalIngestionConfig`).
233
+ *
234
+ * Behaviour:
235
+ * - When the legacy `flooding.detection.sourceBaselineDriftMultiplier` key
236
+ * is present in the YAML AND `LEGACY_HARD_ERROR_ENV_VAR` is unset, the
237
+ * loader translates it to `zScoreThreshold = legacyMultiplier × 0.6` (an
238
+ * empirical mapping documented in the operator runbook — a 5× multiplier
239
+ * over a per-source baseline corresponds approximately to a 3σ spike on
240
+ * most production datasets) and emits one
241
+ * `signal-ingestion-config-deprecated-field` Decision.
242
+ * - When `LEGACY_HARD_ERROR_ENV_VAR=1`, the loader throws
243
+ * `SignalIngestionConfigError` with a migration message.
244
+ * - When neither legacy key nor v0.3 z-score key is present, the loader
245
+ * fills in defaults silently (no deprecation noise).
246
+ */
247
+ export function loadSignalIngestionConfigWithDeprecations(options = {}) {
248
+ const projectRoot = options.projectRoot ?? process.cwd();
249
+ const configPath = options.configPath ?? resolve(projectRoot, DEFAULT_SIGNAL_INGESTION_CONFIG_PATH);
250
+ // Read raw YAML to inspect for legacy keys BEFORE resolveConfig() strips them.
251
+ const deprecations = [];
252
+ if (existsSync(configPath)) {
253
+ let raw;
254
+ try {
255
+ raw = readFileSync(configPath, 'utf8');
256
+ }
257
+ catch (err) {
258
+ throw new SignalIngestionConfigError(`Failed to read signal ingestion config at ${configPath}`, err);
259
+ }
260
+ let parsed;
261
+ try {
262
+ parsed = parseYaml(raw);
263
+ }
264
+ catch (err) {
265
+ throw new SignalIngestionConfigError(`Failed to parse signal ingestion YAML at ${configPath}`, err);
266
+ }
267
+ const legacy = extractLegacyFloodingMultiplier(parsed);
268
+ if (legacy !== undefined) {
269
+ const hardError = isTruthyEnv(process.env[LEGACY_HARD_ERROR_ENV_VAR]);
270
+ if (hardError) {
271
+ throw new SignalIngestionConfigError(`flooding.detection.sourceBaselineDriftMultiplier is removed (AISDLC-433); ` +
272
+ `migrate to flooding.detection.zScoreThreshold. ` +
273
+ `See docs/operations/signal-ingestion.md §5 for the migration recipe.`);
274
+ }
275
+ // Translate legacy multiplier → z-score equivalent.
276
+ // Empirical mapping: a 5× multiplier ≈ 3σ on most production datasets
277
+ // (see operator runbook §5 for the derivation). Linear scaling around
278
+ // the default: `zScore = multiplier × 0.6` — preserves operator intent
279
+ // while letting the v0.3 algorithm take over.
280
+ const legacyMultiplier = Number(legacy);
281
+ const translatedZScore = Number.isFinite(legacyMultiplier)
282
+ ? legacyMultiplier * 0.6
283
+ : DEFAULT_SIGNAL_INGESTION_CONFIG.flooding.detection.zScoreThreshold;
284
+ deprecations.push({
285
+ type: 'Decision',
286
+ decision: 'signal-ingestion-config-deprecated-field',
287
+ field: 'flooding.detection.sourceBaselineDriftMultiplier',
288
+ replacement: 'flooding.detection.zScoreThreshold',
289
+ legacyValue: legacy,
290
+ translatedTo: translatedZScore,
291
+ message: `flooding.detection.sourceBaselineDriftMultiplier (legacy value ${JSON.stringify(legacy)}) ` +
292
+ `is deprecated and will be removed after the one-release-window soak. ` +
293
+ `Translated to flooding.detection.zScoreThreshold = ${translatedZScore} for this release. ` +
294
+ `Migrate by setting flooding.detection.zScoreThreshold explicitly in ` +
295
+ `.ai-sdlc/signal-ingestion.yaml (recommended default 3.0). ` +
296
+ `See docs/operations/signal-ingestion.md §5 for the full migration recipe.`,
297
+ });
298
+ // Mutate the parsed object so resolveConfig sees a translated value (the
299
+ // operator's intent is preserved through the legacy-to-z-score mapping).
300
+ injectTranslatedZScore(parsed, translatedZScore);
301
+ }
302
+ const config = resolveConfig(parsed, configPath);
303
+ return { config, deprecations };
304
+ }
305
+ // File absent → defaults; no deprecations.
306
+ return { config: { ...DEFAULT_SIGNAL_INGESTION_CONFIG }, deprecations };
307
+ }
308
+ function isTruthyEnv(value) {
309
+ if (value === undefined)
310
+ return false;
311
+ const v = value.trim().toLowerCase();
312
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
313
+ }
314
+ function extractLegacyFloodingMultiplier(parsed) {
315
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
316
+ return undefined;
317
+ const root = parsed;
318
+ const spec = root['spec'] ?? root;
319
+ const flooding = spec['flooding'];
320
+ if (typeof flooding !== 'object' || flooding === null)
321
+ return undefined;
322
+ const detection = flooding['detection'];
323
+ if (typeof detection !== 'object' || detection === null)
324
+ return undefined;
325
+ return detection['sourceBaselineDriftMultiplier'];
326
+ }
327
+ function injectTranslatedZScore(parsed, zScoreThreshold) {
328
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
329
+ return;
330
+ const root = parsed;
331
+ const spec = root['spec'] ?? root;
332
+ const flooding = spec['flooding'];
333
+ if (typeof flooding !== 'object' || flooding === null)
334
+ return;
335
+ const detection = flooding['detection'];
336
+ if (typeof detection !== 'object' || detection === null)
337
+ return;
338
+ const detectionObj = detection;
339
+ // Only inject when operator did NOT also supply the explicit v0.3 key;
340
+ // an explicit v0.3 value takes precedence over the translated legacy value
341
+ // (operator's stated intent wins over the inferred translation).
342
+ if (detectionObj['zScoreThreshold'] === undefined) {
343
+ detectionObj['zScoreThreshold'] = zScoreThreshold;
344
+ }
345
+ // Strip the legacy key so resolveFloodingDetection() doesn't see it via any
346
+ // future additionalProperties check.
347
+ delete detectionObj['sourceBaselineDriftMultiplier'];
348
+ }
349
+ function resolveBoolean(value, defaultValue) {
350
+ if (value === undefined || value === null)
351
+ return defaultValue;
352
+ if (typeof value === 'boolean')
353
+ return value;
354
+ throw new SignalIngestionConfigError(`Expected boolean, got ${JSON.stringify(value)}`);
355
+ }
356
+ function resolvePositiveNumber(value, defaultValue, field) {
357
+ if (value === undefined || value === null)
358
+ return defaultValue;
359
+ const n = Number(value);
360
+ if (!Number.isFinite(n) || n < 0) {
361
+ throw new SignalIngestionConfigError(`Field ${field} must be a non-negative finite number, got ${JSON.stringify(value)}`);
362
+ }
363
+ return n;
364
+ }
365
+ function resolveNonNegativeNumber(value, defaultValue, field) {
366
+ if (value === undefined || value === null)
367
+ return defaultValue;
368
+ const n = Number(value);
369
+ if (!Number.isFinite(n) || n < 0) {
370
+ throw new SignalIngestionConfigError(`Field ${field} must be a non-negative finite number, got ${JSON.stringify(value)}`);
371
+ }
372
+ return n;
373
+ }
374
+ function resolveTierMultipliers(value) {
375
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.tierMultipliers;
376
+ if (value === undefined || value === null)
377
+ return { ...defaults };
378
+ if (typeof value !== 'object' || Array.isArray(value)) {
379
+ throw new SignalIngestionConfigError('tierMultipliers must be an object');
380
+ }
381
+ const obj = value;
382
+ return {
383
+ enterprise: resolveNonNegativeNumber(obj['enterprise'], defaults.enterprise, 'enterprise'),
384
+ mid: resolveNonNegativeNumber(obj['mid'], defaults.mid, 'mid'),
385
+ smb: resolveNonNegativeNumber(obj['smb'], defaults.smb, 'smb'),
386
+ free: resolveNonNegativeNumber(obj['free'], defaults.free, 'free'),
387
+ churned: resolveNonNegativeNumber(obj['churned'], defaults.churned, 'churned'),
388
+ };
389
+ }
390
+ function resolveIcpResonanceWeights(value) {
391
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.icpResonanceWeights;
392
+ if (value === undefined || value === null)
393
+ return { ...defaults };
394
+ if (typeof value !== 'object' || Array.isArray(value)) {
395
+ throw new SignalIngestionConfigError('icpResonanceWeights must be an object');
396
+ }
397
+ const obj = value;
398
+ return {
399
+ strong: resolveNonNegativeNumber(obj['strong'], defaults.strong, 'strong'),
400
+ partial: resolveNonNegativeNumber(obj['partial'], defaults.partial, 'partial'),
401
+ weak: resolveNonNegativeNumber(obj['weak'], defaults.weak, 'weak'),
402
+ };
403
+ }
404
+ function resolveTier2Threshold(value) {
405
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.tier2SignificanceThreshold;
406
+ if (value === undefined || value === null)
407
+ return { ...defaults };
408
+ if (typeof value !== 'object' || Array.isArray(value)) {
409
+ throw new SignalIngestionConfigError('tier2SignificanceThreshold must be an object');
410
+ }
411
+ const obj = value;
412
+ return {
413
+ minSignalCount: resolvePositiveNumber(obj['minSignalCount'], defaults.minSignalCount, 'minSignalCount'),
414
+ minUniqueSources: resolvePositiveNumber(obj['minUniqueSources'], defaults.minUniqueSources, 'minUniqueSources'),
415
+ minTier1SignalCount: resolvePositiveNumber(obj['minTier1SignalCount'], defaults.minTier1SignalCount, 'minTier1SignalCount'),
416
+ minClusterAgeDays: resolvePositiveNumber(obj['minClusterAgeDays'], defaults.minClusterAgeDays, 'minClusterAgeDays'),
417
+ };
418
+ }
419
+ function resolveSaThresholds(value) {
420
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.saResonanceThresholds;
421
+ if (value === undefined || value === null)
422
+ return { ...defaults };
423
+ if (typeof value !== 'object' || Array.isArray(value)) {
424
+ throw new SignalIngestionConfigError('saResonanceThresholds must be an object');
425
+ }
426
+ const obj = value;
427
+ return {
428
+ fullWeight: resolveNonNegativeNumber(obj['fullWeight'], defaults.fullWeight, 'fullWeight'),
429
+ discounted: resolveNonNegativeNumber(obj['discounted'], defaults.discounted, 'discounted'),
430
+ excluded: resolveNonNegativeNumber(obj['excluded'], defaults.excluded, 'excluded'),
431
+ };
432
+ }
433
+ function resolveD1Composition(value) {
434
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.d1Composition;
435
+ if (value === undefined || value === null)
436
+ return { ...defaults };
437
+ if (typeof value !== 'object' || Array.isArray(value)) {
438
+ throw new SignalIngestionConfigError('d1Composition must be an object');
439
+ }
440
+ const obj = value;
441
+ return {
442
+ signalPipelineWeight: resolveNonNegativeNumber(obj['signalPipelineWeight'], defaults.signalPipelineWeight, 'signalPipelineWeight'),
443
+ backlogItemWeight: resolveNonNegativeNumber(obj['backlogItemWeight'], defaults.backlogItemWeight, 'backlogItemWeight'),
444
+ };
445
+ }
446
+ function resolveClusteringConfig(value) {
447
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.clustering;
448
+ if (value === undefined || value === null)
449
+ return { ...defaults };
450
+ if (typeof value !== 'object' || Array.isArray(value)) {
451
+ throw new SignalIngestionConfigError('clustering must be an object');
452
+ }
453
+ const obj = value;
454
+ const algorithm = obj['algorithm'];
455
+ if (algorithm !== undefined && algorithm !== 'bm25' && algorithm !== 'embedding') {
456
+ throw new SignalIngestionConfigError(`clustering.algorithm must be 'bm25' or 'embedding', got ${JSON.stringify(algorithm)}`);
457
+ }
458
+ return {
459
+ algorithm: algorithm ?? defaults.algorithm,
460
+ similarityThreshold: resolveNonNegativeNumber(obj['similarityThreshold'], defaults.similarityThreshold, 'similarityThreshold'),
461
+ };
462
+ }
463
+ function resolveStringArray(value, defaultValue) {
464
+ if (value === undefined || value === null)
465
+ return [...defaultValue];
466
+ if (!Array.isArray(value))
467
+ throw new SignalIngestionConfigError('adapters must be an array');
468
+ if (!value.every((v) => typeof v === 'string')) {
469
+ throw new SignalIngestionConfigError('adapters entries must be strings');
470
+ }
471
+ return value;
472
+ }
473
+ function resolveResidencyEnforcement(value) {
474
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.residencyEnforcement;
475
+ if (value === undefined || value === null) {
476
+ return {
477
+ sourceFromCompliancePosture: defaults.sourceFromCompliancePosture,
478
+ enforcementPoints: { ...defaults.enforcementPoints },
479
+ multiPostureBehavior: defaults.multiPostureBehavior,
480
+ };
481
+ }
482
+ if (typeof value !== 'object' || Array.isArray(value)) {
483
+ throw new SignalIngestionConfigError('residencyEnforcement must be an object');
484
+ }
485
+ const obj = value;
486
+ const points = obj['enforcementPoints'];
487
+ let enforcementPoints = { ...defaults.enforcementPoints };
488
+ if (points !== undefined && points !== null) {
489
+ if (typeof points !== 'object' || Array.isArray(points)) {
490
+ throw new SignalIngestionConfigError('residencyEnforcement.enforcementPoints must be an object');
491
+ }
492
+ const p = points;
493
+ enforcementPoints = {
494
+ fetchSignals: resolveBoolean(p['fetchSignals'], defaults.enforcementPoints.fetchSignals),
495
+ clustering: resolveBoolean(p['clustering'], defaults.enforcementPoints.clustering),
496
+ storage: resolveBoolean(p['storage'], defaults.enforcementPoints.storage),
497
+ unifiedCostReport: resolveBoolean(p['unifiedCostReport'], defaults.enforcementPoints.unifiedCostReport),
498
+ };
499
+ }
500
+ const multi = obj['multiPostureBehavior'];
501
+ if (multi !== undefined && multi !== null && multi !== 'union') {
502
+ throw new SignalIngestionConfigError(`residencyEnforcement.multiPostureBehavior must be 'union' (v1 only), got ${JSON.stringify(multi)}`);
503
+ }
504
+ return {
505
+ sourceFromCompliancePosture: resolveBoolean(obj['sourceFromCompliancePosture'], defaults.sourceFromCompliancePosture),
506
+ enforcementPoints,
507
+ multiPostureBehavior: 'union',
508
+ };
509
+ }
510
+ function resolveManualEntry(value) {
511
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.manualEntry;
512
+ if (value === undefined || value === null) {
513
+ return {
514
+ dailyCapPerOperator: defaults.dailyCapPerOperator,
515
+ evidenceUrlOptional: defaults.evidenceUrlOptional,
516
+ qualityMetric: { ...defaults.qualityMetric },
517
+ };
518
+ }
519
+ if (typeof value !== 'object' || Array.isArray(value)) {
520
+ throw new SignalIngestionConfigError('manualEntry must be an object');
521
+ }
522
+ const obj = value;
523
+ const qm = obj['qualityMetric'];
524
+ let qualityMetric;
525
+ if (qm === undefined || qm === null) {
526
+ qualityMetric = { ...defaults.qualityMetric };
527
+ }
528
+ else if (typeof qm !== 'object' || Array.isArray(qm)) {
529
+ throw new SignalIngestionConfigError('manualEntry.qualityMetric must be an object');
530
+ }
531
+ else {
532
+ const q = qm;
533
+ qualityMetric = {
534
+ enabled: resolveBoolean(q['enabled'], defaults.qualityMetric.enabled),
535
+ windowDays: resolvePositiveNumber(q['windowDays'], defaults.qualityMetric.windowDays, 'manualEntry.qualityMetric.windowDays'),
536
+ shareWarningThreshold: resolveShareThreshold(q['shareWarningThreshold'], defaults.qualityMetric.shareWarningThreshold),
537
+ };
538
+ }
539
+ return {
540
+ dailyCapPerOperator: resolveNonNegativeNumber(obj['dailyCapPerOperator'], defaults.dailyCapPerOperator, 'manualEntry.dailyCapPerOperator'),
541
+ evidenceUrlOptional: resolveBoolean(obj['evidenceUrlOptional'], defaults.evidenceUrlOptional),
542
+ qualityMetric,
543
+ };
544
+ }
545
+ function resolveShareThreshold(value, defaultValue) {
546
+ if (value === undefined || value === null)
547
+ return defaultValue;
548
+ const n = Number(value);
549
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
550
+ throw new SignalIngestionConfigError(`manualEntry.qualityMetric.shareWarningThreshold must be a number in [0, 1], got ${JSON.stringify(value)}`);
551
+ }
552
+ return n;
553
+ }
554
+ function resolveLanguageDetectionConfig(value) {
555
+ const defaults = DEFAULT_SIGNAL_INGESTION_CONFIG.languageDetection;
556
+ if (value === undefined || value === null)
557
+ return { ...defaults };
558
+ if (typeof value !== 'object' || Array.isArray(value)) {
559
+ throw new SignalIngestionConfigError('languageDetection must be an object');
560
+ }
561
+ const obj = value;
562
+ const library = obj['library'];
563
+ if (library !== undefined && library !== 'franc' && library !== 'none') {
564
+ throw new SignalIngestionConfigError(`languageDetection.library must be 'franc' or 'none', got ${JSON.stringify(library)}`);
565
+ }
566
+ const onUndetermined = obj['onUndetermined'];
567
+ if (onUndetermined !== undefined && onUndetermined !== 'accept' && onUndetermined !== 'drop') {
568
+ throw new SignalIngestionConfigError(`languageDetection.onUndetermined must be 'accept' or 'drop', got ${JSON.stringify(onUndetermined)}`);
569
+ }
570
+ return {
571
+ library: library ?? defaults.library,
572
+ minDetectionLength: resolvePositiveNumber(obj['minDetectionLength'], defaults.minDetectionLength, 'minDetectionLength'),
573
+ onUndetermined: onUndetermined ?? defaults.onUndetermined,
574
+ };
575
+ }
576
+ function resolveLanguageList(value) {
577
+ if (value === undefined || value === null)
578
+ return [...DEFAULT_SIGNAL_INGESTION_CONFIG.acceptedLanguages];
579
+ if (!Array.isArray(value))
580
+ throw new SignalIngestionConfigError('acceptedLanguages must be an array');
581
+ if (!value.every((v) => typeof v === 'string')) {
582
+ throw new SignalIngestionConfigError('acceptedLanguages entries must be strings');
583
+ }
584
+ // Normalize to lowercase BCP-47 language tags
585
+ return value.map((lang) => lang.toLowerCase());
586
+ }
587
+ //# sourceMappingURL=config.js.map