@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.
- package/dist/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +112 -1
- package/dist/admission-composite.js +85 -4
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +64 -13
- package/dist/admission-score.d.ts +30 -0
- package/dist/admission-score.js +4 -1
- package/dist/analysis/file-walker.js +5 -0
- package/dist/backlog-adapter.d.ts +45 -0
- package/dist/backlog-adapter.js +145 -1
- package/dist/calibration.d.ts +81 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/git-remote.js +1 -1
- package/dist/cli/commands/init-features.d.ts +301 -2
- package/dist/cli/commands/init-features.js +647 -7
- package/dist/cli/commands/init-templates.d.ts +198 -1
- package/dist/cli/commands/init-templates.js +943 -1
- package/dist/cli/commands/init.d.ts +45 -0
- package/dist/cli/commands/init.js +147 -5
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +42 -8
- package/dist/compliance/composer.d.ts +79 -0
- package/dist/compliance/composer.js +258 -0
- package/dist/compliance/errors.d.ts +64 -0
- package/dist/compliance/errors.js +85 -0
- package/dist/compliance/loader.d.ts +52 -0
- package/dist/compliance/loader.js +124 -0
- package/dist/compliance/types.d.ts +184 -0
- package/dist/compliance/types.js +41 -0
- package/dist/compliance-clearance.d.ts +269 -0
- package/dist/compliance-clearance.js +269 -0
- package/dist/config.js +17 -0
- package/dist/cost-tracker.d.ts +22 -0
- package/dist/cost-tracker.js +41 -0
- package/dist/cycle-utils.js +14 -1
- package/dist/database/adapters/external.js +5 -1
- package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
- package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
- package/dist/embedding/consumers/tessellation-drift.js +76 -0
- package/dist/embedding/cross-provider.d.ts +78 -0
- package/dist/embedding/cross-provider.js +75 -0
- package/dist/embedding/deprecation.d.ts +151 -0
- package/dist/embedding/deprecation.js +229 -0
- package/dist/embedding/errors.d.ts +90 -0
- package/dist/embedding/errors.js +150 -0
- package/dist/embedding/index.d.ts +29 -0
- package/dist/embedding/index.js +24 -0
- package/dist/embedding/pipeline-load.d.ts +146 -0
- package/dist/embedding/pipeline-load.js +178 -0
- package/dist/embedding/registry.d.ts +45 -0
- package/dist/embedding/registry.js +61 -0
- package/dist/embedding/stale-vector.d.ts +110 -0
- package/dist/embedding/stale-vector.js +92 -0
- package/dist/embedding/storage/index.d.ts +51 -0
- package/dist/embedding/storage/index.js +43 -0
- package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
- package/dist/embedding/storage/jsonl-backend.js +332 -0
- package/dist/embedding/storage/types.d.ts +135 -0
- package/dist/embedding/storage/types.js +13 -0
- package/dist/embedding/types.d.ts +180 -0
- package/dist/embedding/types.js +10 -0
- package/dist/execute.d.ts +29 -2
- package/dist/execute.js +200 -38
- package/dist/fix-review.js +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.js +19 -2
- package/dist/journey/inheritance-validator.d.ts +396 -0
- package/dist/journey/inheritance-validator.js +370 -0
- package/dist/journey/metric-snapshot.d.ts +403 -0
- package/dist/journey/metric-snapshot.js +370 -0
- package/dist/journey/state-id-drift-rule.d.ts +137 -0
- package/dist/journey/state-id-drift-rule.js +245 -0
- package/dist/journey-sa2-router.d.ts +395 -0
- package/dist/journey-sa2-router.js +308 -0
- package/dist/otel-exporter.js +0 -3
- package/dist/runners/claude-code.js +3 -3
- package/dist/runners/review-agent.js +6 -2
- package/dist/runners/runner-registry.d.ts +36 -0
- package/dist/runners/runner-registry.js +90 -0
- package/dist/runtime/attestations.d.ts +173 -13
- package/dist/runtime/attestations.js +265 -43
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/sa-scoring/exemplar-bank.js +3 -1
- package/dist/sa-scoring/layer3-llm.js +6 -1
- package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
- package/dist/sa-scoring/revision-proposal-config.js +198 -0
- package/dist/sa-scoring/revision-proposal.d.ts +285 -0
- package/dist/sa-scoring/revision-proposal.js +417 -0
- package/dist/shared.d.ts +28 -0
- package/dist/shared.js +37 -0
- package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
- package/dist/signal-ingestion/adapters/community-thread.js +55 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
- package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
- package/dist/signal-ingestion/adapters/manual.js +112 -0
- package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
- package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
- package/dist/signal-ingestion/classifier.d.ts +205 -0
- package/dist/signal-ingestion/classifier.js +494 -0
- package/dist/signal-ingestion/clustering-types.d.ts +36 -0
- package/dist/signal-ingestion/clustering-types.js +14 -0
- package/dist/signal-ingestion/clustering.d.ts +200 -0
- package/dist/signal-ingestion/clustering.js +413 -0
- package/dist/signal-ingestion/config.d.ts +351 -0
- package/dist/signal-ingestion/config.js +587 -0
- package/dist/signal-ingestion/d1.d.ts +252 -0
- package/dist/signal-ingestion/d1.js +235 -0
- package/dist/signal-ingestion/errors.d.ts +73 -0
- package/dist/signal-ingestion/errors.js +108 -0
- package/dist/signal-ingestion/governance-events.d.ts +181 -0
- package/dist/signal-ingestion/governance-events.js +189 -0
- package/dist/signal-ingestion/index.d.ts +35 -0
- package/dist/signal-ingestion/index.js +53 -0
- package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
- package/dist/signal-ingestion/manual-share-metric.js +106 -0
- package/dist/signal-ingestion/registry.d.ts +40 -0
- package/dist/signal-ingestion/registry.js +137 -0
- package/dist/signal-ingestion/residency.d.ts +227 -0
- package/dist/signal-ingestion/residency.js +238 -0
- package/dist/signal-ingestion/significance.d.ts +554 -0
- package/dist/signal-ingestion/significance.js +555 -0
- package/dist/signal-ingestion/types.d.ts +191 -0
- package/dist/signal-ingestion/types.js +8 -0
- package/dist/substrate/drift-composition.d.ts +270 -0
- package/dist/substrate/drift-composition.js +306 -0
- package/dist/substrate/drift-tui-surface.d.ts +61 -0
- package/dist/substrate/drift-tui-surface.js +102 -0
- package/dist/substrate/identity-class.d.ts +176 -0
- package/dist/substrate/identity-class.js +201 -0
- package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
- package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
- package/dist/tessellation/rule-registry.d.ts +269 -0
- package/dist/tessellation/rule-registry.js +92 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
- package/dist/tessellation-admission.d.ts +162 -0
- package/dist/tessellation-admission.js +146 -0
- package/dist/tessellation-drift.d.ts +246 -0
- package/dist/tessellation-drift.js +250 -0
- package/dist/validate-config.js +13 -0
- package/dist/validate-issue.js +2 -2
- package/dist/variant/cardinality-activation.d.ts +126 -0
- package/dist/variant/cardinality-activation.js +101 -0
- package/dist/variant/deprecation-lifecycle.d.ts +184 -0
- package/dist/variant/deprecation-lifecycle.js +208 -0
- package/dist/variant/drift-extension.d.ts +136 -0
- package/dist/variant/drift-extension.js +164 -0
- package/dist/variant/engineering-review.d.ts +185 -0
- package/dist/variant/engineering-review.js +142 -0
- package/dist/variant/index.d.ts +32 -0
- package/dist/variant/index.js +32 -0
- package/dist/variant/inheritance-validator.d.ts +165 -0
- package/dist/variant/inheritance-validator.js +139 -0
- package/dist/variant/internal-adopter/index.d.ts +11 -0
- package/dist/variant/internal-adopter/index.js +10 -0
- package/dist/variant/internal-adopter/products.d.ts +156 -0
- package/dist/variant/internal-adopter/products.js +366 -0
- package/dist/variant-admission.d.ts +316 -0
- package/dist/variant-admission.js +247 -0
- package/dist/webhook-manager.js +7 -1
- 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
|