@ai-sdlc/orchestrator 0.9.0 → 0.13.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/__test-helpers/git-env.d.ts +40 -0
- package/dist/__test-helpers/git-env.js +73 -0
- package/dist/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +134 -1
- package/dist/admission-composite.js +170 -6
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +71 -10
- package/dist/admission-hc.d.ts +20 -1
- package/dist/admission-hc.js +12 -1
- package/dist/admission-score.d.ts +44 -0
- package/dist/admission-score.js +11 -2
- 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.d.ts +25 -0
- package/dist/cli/commands/git-remote.js +80 -1
- package/dist/cli/commands/init-features.d.ts +507 -0
- package/dist/cli/commands/init-features.js +1100 -0
- package/dist/cli/commands/init-templates.d.ts +301 -0
- package/dist/cli/commands/init-templates.js +1341 -0
- package/dist/cli/commands/init.d.ts +67 -0
- package/dist/cli/commands/init.js +301 -2
- 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/database/adapters/external.js +5 -1
- package/dist/design-authority.d.ts +13 -0
- package/dist/design-authority.js +13 -0
- 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 +179 -35
- 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/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/models/classifier.d.ts +3 -1
- package/dist/models/classifier.js +61 -5
- package/dist/pillar-breakdown.d.ts +23 -1
- package/dist/pillar-breakdown.js +9 -0
- 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 +663 -11
- package/dist/runtime/attestations.js +971 -24
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/parallelism-flag.d.ts +21 -3
- package/dist/runtime/parallelism-flag.js +27 -6
- package/dist/runtime/port-allocator.d.ts +11 -0
- package/dist/runtime/port-allocator.js +30 -0
- 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/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/package.json +11 -8
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DIDRevisionProposal mechanism (RFC-0031).
|
|
3
|
+
*
|
|
4
|
+
* Implements the calibration-driven proposal mechanism that fires when the
|
|
5
|
+
* PPA flywheel accumulates sufficient evidence that a DID field has drifted
|
|
6
|
+
* from observed reality.
|
|
7
|
+
*
|
|
8
|
+
* What ships here:
|
|
9
|
+
* - DIDRevisionProposal event shape + classification logic (§3, §5)
|
|
10
|
+
* - SoulHealthDiagnostic event for unhealthy-drift path (§7)
|
|
11
|
+
* - Approval routing by identityClass (§8)
|
|
12
|
+
* - 14-day expiry + DIDRevisionProposalExpired event (§9)
|
|
13
|
+
* - lockNoProposal opt-out per field from calibration config (OQ-12.3)
|
|
14
|
+
* - Rejection learnings flowing back into the calibration log (OQ-12.5)
|
|
15
|
+
* - Multi-field bundling explicitly deferred to v2 (OQ-12.2)
|
|
16
|
+
* - Confidence derived from sample size + classification clarity + identityClass (OQ-12.1)
|
|
17
|
+
* - PPA generates proposal regardless of pillar; pillar lead approves (OQ-12.4)
|
|
18
|
+
*
|
|
19
|
+
* Trigger entry point: `evaluateRevisionProposal()` — called after drift
|
|
20
|
+
* is detected (i.e., a `SoulDriftDetected` event fires). One call per
|
|
21
|
+
* field; bundling is v2.
|
|
22
|
+
*
|
|
23
|
+
* Scope: Shard-DID only. Platform-level DID changes are human-initiated.
|
|
24
|
+
*/
|
|
25
|
+
import { randomUUID } from 'crypto';
|
|
26
|
+
import { DEFAULT_CONFIDENCE_HIGH_SAMPLE_SIZE, DEFAULT_CONFIDENCE_LOW_SAMPLE_SIZE, DEFAULT_REJECTION_WEIGHT_HIGH, DEFAULT_REJECTION_WEIGHT_MEDIUM, DEFAULT_REJECTION_WEIGHT_LOW, DEFAULT_CONFIDENCE_PENALTY_FLOOR, } from './revision-proposal-config.js';
|
|
27
|
+
export { DEFAULT_CONFIDENCE_HIGH_SAMPLE_SIZE, DEFAULT_CONFIDENCE_LOW_SAMPLE_SIZE, DEFAULT_REJECTION_WEIGHT_HIGH, DEFAULT_REJECTION_WEIGHT_MEDIUM, DEFAULT_REJECTION_WEIGHT_LOW, DEFAULT_CONFIDENCE_PENALTY_FLOOR, DEFAULT_RESOLVED_CALIBRATION_CONFIG, validateRevisionProposalCalibrationConfig, resolveRevisionProposalCalibrationConfig, parseRevisionProposalCalibrationYaml, } from './revision-proposal-config.js';
|
|
28
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
29
|
+
/** Default trigger thresholds (§6). Configurable per deployment. */
|
|
30
|
+
export const DEFAULT_DISMISS_THRESHOLD = 10;
|
|
31
|
+
export const DEFAULT_DEMAND_MISALIGNMENT_THRESHOLD = 0.3;
|
|
32
|
+
export const DEFAULT_DRIFT_EVENTS_THRESHOLD = 3;
|
|
33
|
+
export const DEFAULT_TRIGGER_WINDOW_DAYS = 60;
|
|
34
|
+
export const DEFAULT_PROPOSAL_EXPIRY_DAYS = 14;
|
|
35
|
+
/** Classification thresholds per §7. */
|
|
36
|
+
export const HEALTHY_ICP_MATCH_MIN = 0.6;
|
|
37
|
+
export const UNHEALTHY_ICP_MATCH_MAX = 0.3;
|
|
38
|
+
// ── Classification logic (§7) ────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Classify drift as healthy / unhealthy / ambiguous per RFC-0031 §7.
|
|
41
|
+
*
|
|
42
|
+
* ```
|
|
43
|
+
* healthy: icpMatchRate > 0.6 AND NOT coreDIDFieldsAffected
|
|
44
|
+
* unhealthy: icpMatchRate < 0.3 OR (coreDIDFieldsAffected AND dismissToEscalateRatio < 1.0)
|
|
45
|
+
* ambiguous: everything else
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function classifyDrift(evidence) {
|
|
49
|
+
const { demandClusterICPMatchRate, coreDIDFieldsAffected, dismissToEscalateRatio } = evidence;
|
|
50
|
+
if (demandClusterICPMatchRate > HEALTHY_ICP_MATCH_MIN && !coreDIDFieldsAffected) {
|
|
51
|
+
return 'healthy';
|
|
52
|
+
}
|
|
53
|
+
if (demandClusterICPMatchRate < UNHEALTHY_ICP_MATCH_MAX ||
|
|
54
|
+
(coreDIDFieldsAffected && dismissToEscalateRatio < 1.0)) {
|
|
55
|
+
return 'unhealthy';
|
|
56
|
+
}
|
|
57
|
+
return 'ambiguous';
|
|
58
|
+
}
|
|
59
|
+
// ── Approval routing (§8) ────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Derive approval path from identityClass per RFC-0031 §8.
|
|
62
|
+
*
|
|
63
|
+
* - `core` → `triad` (all three pillar leads required)
|
|
64
|
+
* - `evolving` → `pillarLead` (owning lead + one other)
|
|
65
|
+
* - `undefined` → `triad` (default-tighten when class is undeclared)
|
|
66
|
+
* - ambiguous classification always → `triad` (§7 override)
|
|
67
|
+
*/
|
|
68
|
+
export function deriveApprovalPath(identityClass, classification) {
|
|
69
|
+
// AISDLC-271 PR #476 round-2: reject non-enum identityClass to prevent
|
|
70
|
+
// a future caller from passing untrusted input that downgrades the
|
|
71
|
+
// approval requirement (e.g. attacker passes 'evolving' for a core field
|
|
72
|
+
// → triad → pillarLead, weaker authorization).
|
|
73
|
+
assertValidIdentityClass(identityClass, 'deriveApprovalPath');
|
|
74
|
+
// Ambiguous drift always requires triad review regardless of identityClass (§7).
|
|
75
|
+
if (classification === 'ambiguous')
|
|
76
|
+
return 'triad';
|
|
77
|
+
if (identityClass === 'evolving')
|
|
78
|
+
return 'pillarLead';
|
|
79
|
+
// 'core' or unset → triad (safer default per §8)
|
|
80
|
+
return 'triad';
|
|
81
|
+
}
|
|
82
|
+
// ── Confidence calculation (OQ-12.1) ────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Compute proposal confidence from trigger evidence + classification clarity
|
|
85
|
+
* + identityClass (OQ-12.1 resolved position: confidence = f(sample size,
|
|
86
|
+
* classification clarity, identityClass)).
|
|
87
|
+
*
|
|
88
|
+
* Thresholds are per-org configurable via `.ai-sdlc/calibration.yaml`
|
|
89
|
+
* (Refit AISDLC-310). Defaults match the shipped AISDLC-271 values:
|
|
90
|
+
*
|
|
91
|
+
* - high: sampleSize ≥ highSampleSize (default 20) AND classification != 'ambiguous' AND identityClass = 'evolving'
|
|
92
|
+
* - low: sampleSize < lowSampleSize (default 5) OR classification = 'ambiguous' OR identityClass = 'core'
|
|
93
|
+
* - medium: everything else
|
|
94
|
+
*
|
|
95
|
+
* @param calibrationConfig Optional resolved config from `parseRevisionProposalCalibrationYaml()`.
|
|
96
|
+
* When omitted, shipped defaults (20 / 5) are used.
|
|
97
|
+
*/
|
|
98
|
+
export function computeConfidence(triggerEvidence, classification, identityClass, calibrationConfig) {
|
|
99
|
+
// AISDLC-271 PR #476 round-2 security follow-up: also guard the exported
|
|
100
|
+
// computeConfidence entry point. Without this an HTTP caller bypassing
|
|
101
|
+
// evaluateRevisionProposal could pass an invalid identityClass and
|
|
102
|
+
// receive an incorrect confidence weight (not a privilege escalation
|
|
103
|
+
// since deriveApprovalPath is separately guarded, but inconsistent
|
|
104
|
+
// defense-in-depth posture).
|
|
105
|
+
assertValidIdentityClass(identityClass, 'computeConfidence');
|
|
106
|
+
const sampleSize = triggerEvidence.dismissSignals + triggerEvidence.escalateSignals + triggerEvidence.driftEvents;
|
|
107
|
+
const highSampleSize = calibrationConfig?.confidenceThresholds.highSampleSize ?? DEFAULT_CONFIDENCE_HIGH_SAMPLE_SIZE;
|
|
108
|
+
const lowSampleSize = calibrationConfig?.confidenceThresholds.lowSampleSize ?? DEFAULT_CONFIDENCE_LOW_SAMPLE_SIZE;
|
|
109
|
+
if (sampleSize >= highSampleSize &&
|
|
110
|
+
classification !== 'ambiguous' &&
|
|
111
|
+
identityClass === 'evolving') {
|
|
112
|
+
return 'high';
|
|
113
|
+
}
|
|
114
|
+
if (sampleSize < lowSampleSize || classification === 'ambiguous' || identityClass === 'core') {
|
|
115
|
+
return 'low';
|
|
116
|
+
}
|
|
117
|
+
return 'medium';
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns true if ANY trigger condition fires per RFC-0031 §6.
|
|
121
|
+
*/
|
|
122
|
+
export function triggerConditionMet(conditions, config = {}) {
|
|
123
|
+
const dismissThreshold = config.dismissThreshold ?? DEFAULT_DISMISS_THRESHOLD;
|
|
124
|
+
const demandThreshold = config.demandMisalignmentThreshold ?? DEFAULT_DEMAND_MISALIGNMENT_THRESHOLD;
|
|
125
|
+
const driftThreshold = config.driftEventsThreshold ?? DEFAULT_DRIFT_EVENTS_THRESHOLD;
|
|
126
|
+
return (conditions.dismissSignals >= dismissThreshold ||
|
|
127
|
+
conditions.demandMisalignment > demandThreshold ||
|
|
128
|
+
conditions.driftEvents >= driftThreshold);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Returns true if the given field is locked from auto-proposals per OQ-12.3.
|
|
132
|
+
*
|
|
133
|
+
* Proposal generation SKIPS locked fields. Operators remove entries from
|
|
134
|
+
* `lockNoProposal` to opt back in.
|
|
135
|
+
*/
|
|
136
|
+
export function isFieldLocked(field, lockConfig) {
|
|
137
|
+
return lockConfig.lockNoProposal?.includes(field) ?? false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Evaluate whether a DIDRevisionProposal should be generated for a given
|
|
141
|
+
* DID field. Entry point called once per field (one-field-per-proposal in
|
|
142
|
+
* v1 per OQ-12.2).
|
|
143
|
+
*
|
|
144
|
+
* Returns a discriminated union describing what was produced:
|
|
145
|
+
* - `proposal` — healthy drift; one DIDRevisionProposalEvent
|
|
146
|
+
* - `diagnostic` — unhealthy drift; one SoulHealthDiagnosticEvent
|
|
147
|
+
* - `ambiguous` — both events produced, both flagged for triad review
|
|
148
|
+
* - `skipped` — field is locked or trigger conditions not met
|
|
149
|
+
*/
|
|
150
|
+
export function evaluateRevisionProposal(input) {
|
|
151
|
+
// AISDLC-271 PR #476 round-2: validate identityClass at the entry point
|
|
152
|
+
// so any future caller passing untrusted input fails fast. Callers MUST
|
|
153
|
+
// derive identityClass from the compiled DID document (did-compiler.ts
|
|
154
|
+
// output), never from external request payloads or mutable records.
|
|
155
|
+
assertValidIdentityClass(input.identityClass, 'evaluateRevisionProposal');
|
|
156
|
+
// Opt-out check (OQ-12.3)
|
|
157
|
+
// AISDLC-310 review fix: honor both the legacy lockConfig AND the
|
|
158
|
+
// calibrationConfig.lockNoProposal field (populated from calibration.yaml).
|
|
159
|
+
// Either signal locks the field — they're additive opt-outs.
|
|
160
|
+
if (input.lockConfig && isFieldLocked(input.field, input.lockConfig)) {
|
|
161
|
+
return { kind: 'skipped', reason: 'locked' };
|
|
162
|
+
}
|
|
163
|
+
if (input.calibrationConfig?.lockNoProposal?.includes(input.field)) {
|
|
164
|
+
return { kind: 'skipped', reason: 'locked' };
|
|
165
|
+
}
|
|
166
|
+
// Trigger condition check (§6)
|
|
167
|
+
if (!triggerConditionMet(input.triggerConditions, input.config)) {
|
|
168
|
+
return { kind: 'skipped', reason: 'no-trigger' };
|
|
169
|
+
}
|
|
170
|
+
const nowMs = (input.now ?? (() => Date.now()))();
|
|
171
|
+
const expiryDays = input.config?.expiryDays ?? DEFAULT_PROPOSAL_EXPIRY_DAYS;
|
|
172
|
+
const triggerWindowDays = input.config?.triggerWindowDays ?? DEFAULT_TRIGGER_WINDOW_DAYS;
|
|
173
|
+
const createdAt = new Date(nowMs).toISOString();
|
|
174
|
+
const expiresAt = new Date(nowMs + expiryDays * 24 * 60 * 60 * 1000).toISOString();
|
|
175
|
+
const triggerEvidence = {
|
|
176
|
+
dismissSignals: input.triggerConditions.dismissSignals,
|
|
177
|
+
escalateSignals: input.triggerConditions.escalateSignals,
|
|
178
|
+
demandMisalignment: input.triggerConditions.demandMisalignment,
|
|
179
|
+
driftEvents: input.triggerConditions.driftEvents,
|
|
180
|
+
triggerWindow: `P${triggerWindowDays}D`,
|
|
181
|
+
};
|
|
182
|
+
const classification = classifyDrift(input.classificationEvidence);
|
|
183
|
+
const approvalPath = deriveApprovalPath(input.identityClass, classification);
|
|
184
|
+
const confidence = computeConfidence(triggerEvidence, classification, input.identityClass, input.calibrationConfig);
|
|
185
|
+
if (classification === 'healthy') {
|
|
186
|
+
const event = {
|
|
187
|
+
type: 'DIDRevisionProposal',
|
|
188
|
+
proposalId: randomUUID(),
|
|
189
|
+
scope: 'shard',
|
|
190
|
+
shardId: input.shardId,
|
|
191
|
+
field: input.field,
|
|
192
|
+
currentValue: input.currentValue,
|
|
193
|
+
proposedValue: input.proposedValue,
|
|
194
|
+
identityClass: input.identityClass,
|
|
195
|
+
classification,
|
|
196
|
+
classificationEvidence: input.classificationEvidence,
|
|
197
|
+
triggerEvidence,
|
|
198
|
+
confidence,
|
|
199
|
+
approvalPath,
|
|
200
|
+
expiresAt,
|
|
201
|
+
createdAt,
|
|
202
|
+
status: 'pending',
|
|
203
|
+
};
|
|
204
|
+
return { kind: 'proposal', event };
|
|
205
|
+
}
|
|
206
|
+
if (classification === 'unhealthy') {
|
|
207
|
+
const diagnostic = buildDiagnosticEvent(input.shardId, input.field, input.classificationEvidence, triggerEvidence, createdAt);
|
|
208
|
+
return { kind: 'diagnostic', event: diagnostic };
|
|
209
|
+
}
|
|
210
|
+
// Ambiguous — emit both (§7): proposal flagged for triad review + diagnostic
|
|
211
|
+
const proposal = {
|
|
212
|
+
type: 'DIDRevisionProposal',
|
|
213
|
+
proposalId: randomUUID(),
|
|
214
|
+
scope: 'shard',
|
|
215
|
+
shardId: input.shardId,
|
|
216
|
+
field: input.field,
|
|
217
|
+
currentValue: input.currentValue,
|
|
218
|
+
proposedValue: input.proposedValue,
|
|
219
|
+
identityClass: input.identityClass,
|
|
220
|
+
classification: 'ambiguous',
|
|
221
|
+
classificationEvidence: input.classificationEvidence,
|
|
222
|
+
triggerEvidence,
|
|
223
|
+
confidence,
|
|
224
|
+
approvalPath: 'triad', // Always triad for ambiguous (§7 + §8)
|
|
225
|
+
expiresAt,
|
|
226
|
+
createdAt,
|
|
227
|
+
status: 'pending',
|
|
228
|
+
};
|
|
229
|
+
const diagnostic = buildDiagnosticEvent(input.shardId, input.field, input.classificationEvidence, triggerEvidence, createdAt);
|
|
230
|
+
return { kind: 'ambiguous', proposal, diagnostic };
|
|
231
|
+
}
|
|
232
|
+
function buildDiagnosticEvent(shardId, field, classificationEvidence, triggerEvidence, createdAt) {
|
|
233
|
+
const recommendation = classificationEvidence.demandClusterICPMatchRate < UNHEALTHY_ICP_MATCH_MAX
|
|
234
|
+
? 'review-demand-source'
|
|
235
|
+
: 'tighten-admission-threshold';
|
|
236
|
+
return {
|
|
237
|
+
type: 'SoulHealthDiagnostic',
|
|
238
|
+
proposalId: randomUUID(),
|
|
239
|
+
shardId,
|
|
240
|
+
field,
|
|
241
|
+
classification: 'unhealthy',
|
|
242
|
+
classificationEvidence,
|
|
243
|
+
triggerEvidence,
|
|
244
|
+
recommendation,
|
|
245
|
+
createdAt,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// ── Proposal expiry (§9) ─────────────────────────────────────────────
|
|
249
|
+
/**
|
|
250
|
+
* AISDLC-271 PR #476 round-2 hardening: defensive validators for the
|
|
251
|
+
* caller-supplied surface that becomes attack surface once persistence /
|
|
252
|
+
* HTTP layers ship (RFC §9 explicitly defers those layers to a future
|
|
253
|
+
* task; until then these functions are pure-library and not reachable
|
|
254
|
+
* from external input — but the validators are still correct on
|
|
255
|
+
* trusted input AND lock down the contract for the future caller).
|
|
256
|
+
*
|
|
257
|
+
* IDENTITY_CLASS_VALUES enforces the schema boundary: callers MUST
|
|
258
|
+
* derive identityClass from the compiled DID
|
|
259
|
+
* document (did-compiler.ts output), not from a request payload. This
|
|
260
|
+
* runtime check ensures no future caller accidentally trusts a
|
|
261
|
+
* webhook / issue-body / mutable-record value.
|
|
262
|
+
*
|
|
263
|
+
* `assertValidExpiresAt` ensures the persisted proposal record's
|
|
264
|
+
* expiresAt is a parseable ISO-8601 timestamp. Re-armed-past-expiry
|
|
265
|
+
* via mutation requires the persistence layer to enforce immutability
|
|
266
|
+
* AT WRITE TIME — see RFC-0031 §9 follow-up.
|
|
267
|
+
*/
|
|
268
|
+
const IDENTITY_CLASS_VALUES = ['core', 'evolving'];
|
|
269
|
+
function assertValidIdentityClass(identityClass, where) {
|
|
270
|
+
if (identityClass === undefined)
|
|
271
|
+
return; // undefined → triad default per §8 — safe
|
|
272
|
+
if (!IDENTITY_CLASS_VALUES.includes(identityClass)) {
|
|
273
|
+
throw new Error(`[revision-proposal] ${where}: invalid identityClass ${JSON.stringify(identityClass)}. ` +
|
|
274
|
+
`Must be one of ${JSON.stringify(IDENTITY_CLASS_VALUES)}. ` +
|
|
275
|
+
`Callers MUST derive identityClass from the compiled DID document, never from external input.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function assertValidExpiresAt(expiresAt, where) {
|
|
279
|
+
if (typeof expiresAt !== 'string' || isNaN(Date.parse(expiresAt))) {
|
|
280
|
+
throw new Error(`[revision-proposal] ${where}: invalid expiresAt ${JSON.stringify(expiresAt)}. ` +
|
|
281
|
+
`Must be ISO-8601 string. ` +
|
|
282
|
+
`Persistence layer MUST treat expiresAt as immutable to prevent re-arming past expiry.`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function assertValidActor(actor, where) {
|
|
286
|
+
if (typeof actor !== 'string' || actor.trim().length < 3) {
|
|
287
|
+
throw new Error(`[revision-proposal] ${where}: invalid actor identity ${JSON.stringify(actor)}. ` +
|
|
288
|
+
`Must be a non-empty string of length ≥ 3 after trimming whitespace. ` +
|
|
289
|
+
`Persistence/HTTP callers MUST validate actor matches an authenticated session identity.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Check whether a proposal has passed its `expiresAt` timestamp.
|
|
294
|
+
*
|
|
295
|
+
* AISDLC-271 PR #476 round-2: validates expiresAt is parseable. Pre-fix
|
|
296
|
+
* a malformed (or attacker-mutated) expiresAt would silently NEVER
|
|
297
|
+
* expire because Date.parse returns NaN and `NaN <= now` is always false.
|
|
298
|
+
*/
|
|
299
|
+
export function isProposalExpired(proposal, nowMs) {
|
|
300
|
+
assertValidExpiresAt(proposal.expiresAt, 'isProposalExpired');
|
|
301
|
+
const now = nowMs ?? Date.now();
|
|
302
|
+
return Date.parse(proposal.expiresAt) <= now;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Archive expired proposals and emit `DIDRevisionProposalExpired` events.
|
|
306
|
+
* Returns the list of expiry events for caller to process (persist / alert).
|
|
307
|
+
*
|
|
308
|
+
* Per §9: expiry without resolution emits the event — operator alert, not
|
|
309
|
+
* silent dismissal. Auto-archives by setting status to 'expired'.
|
|
310
|
+
*/
|
|
311
|
+
export function archiveExpiredProposals(proposals, nowMs) {
|
|
312
|
+
const now = nowMs ?? Date.now();
|
|
313
|
+
const expired = [];
|
|
314
|
+
const remaining = [];
|
|
315
|
+
for (const p of proposals) {
|
|
316
|
+
if (p.status !== 'pending') {
|
|
317
|
+
remaining.push(p);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (isProposalExpired(p, now)) {
|
|
321
|
+
expired.push({
|
|
322
|
+
type: 'DIDRevisionProposalExpired',
|
|
323
|
+
proposalId: p.proposalId,
|
|
324
|
+
shardId: p.shardId,
|
|
325
|
+
field: p.field,
|
|
326
|
+
classification: p.classification,
|
|
327
|
+
createdAt: p.createdAt,
|
|
328
|
+
expiredAt: new Date(now).toISOString(),
|
|
329
|
+
});
|
|
330
|
+
remaining.push({ ...p, status: 'expired' });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
remaining.push(p);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { expired, remaining };
|
|
337
|
+
}
|
|
338
|
+
// ── Rejection learnings (OQ-12.5) ────────────────────────────────────
|
|
339
|
+
/**
|
|
340
|
+
* Record a proposal rejection and compute the `rejectionPrecedentWeight`
|
|
341
|
+
* to feed back into future trigger evaluations (OQ-12.5 resolved position:
|
|
342
|
+
* rejection rationale captured in calibration log; future trigger evaluations
|
|
343
|
+
* weight rejection-precedent into confidence).
|
|
344
|
+
*
|
|
345
|
+
* Weight formula (defaults from AISDLC-271, per-org configurable via
|
|
346
|
+
* `calibration.yaml` — Refit AISDLC-310 / RFC-0031 §12.6):
|
|
347
|
+
* - high-confidence proposals that were rejected → 0.8 (default)
|
|
348
|
+
* - medium-confidence → 0.5 (default)
|
|
349
|
+
* - low-confidence → 0.2 default (expected noise level, lower weight)
|
|
350
|
+
*
|
|
351
|
+
* Callers persist this record to their calibration log; the next trigger
|
|
352
|
+
* evaluation should factor it in by reducing effective confidence.
|
|
353
|
+
*
|
|
354
|
+
* @param calibrationConfig Optional resolved config from `parseRevisionProposalCalibrationYaml()`.
|
|
355
|
+
* When omitted, shipped AISDLC-271 weights (0.8 / 0.5 / 0.2) are used.
|
|
356
|
+
*/
|
|
357
|
+
export function recordRejection(proposal, rejectedBy, rationale, now, calibrationConfig) {
|
|
358
|
+
// AISDLC-271 PR #476 round-2: defensive validation of rejectedBy. The
|
|
359
|
+
// persistence/HTTP layer (when added) MUST validate this against the
|
|
360
|
+
// authenticated session identity — without it, an attacker who can call
|
|
361
|
+
// recordRejection forges identity strings and floods the rejection log
|
|
362
|
+
// to permanently suppress proposals via computeRejectionPrecedentFactor.
|
|
363
|
+
// This length check is the in-library minimum; the calling layer is
|
|
364
|
+
// responsible for the authoritative authorization check.
|
|
365
|
+
assertValidActor(rejectedBy, 'recordRejection');
|
|
366
|
+
if (typeof rationale !== 'string' || rationale.trim().length < 10) {
|
|
367
|
+
throw new Error(`[revision-proposal] recordRejection: rationale must be ≥10 chars after trimming ` +
|
|
368
|
+
`(got ${typeof rationale === 'string' ? rationale.trim().length : 'non-string'}). ` +
|
|
369
|
+
`Persistence layer captures this in the calibration log for future trigger evaluations.`);
|
|
370
|
+
}
|
|
371
|
+
const nowMs = (now ?? (() => Date.now()))();
|
|
372
|
+
const w = calibrationConfig?.rejectionPrecedent.weights;
|
|
373
|
+
const weightByConfidence = {
|
|
374
|
+
high: w?.highConfidenceRejection ?? DEFAULT_REJECTION_WEIGHT_HIGH,
|
|
375
|
+
medium: w?.mediumConfidenceRejection ?? DEFAULT_REJECTION_WEIGHT_MEDIUM,
|
|
376
|
+
low: w?.lowConfidenceRejection ?? DEFAULT_REJECTION_WEIGHT_LOW,
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
proposalId: proposal.proposalId,
|
|
380
|
+
shardId: proposal.shardId,
|
|
381
|
+
field: proposal.field,
|
|
382
|
+
rejectedBy,
|
|
383
|
+
rationale,
|
|
384
|
+
rejectedAt: new Date(nowMs).toISOString(),
|
|
385
|
+
classification: proposal.classification,
|
|
386
|
+
rejectionPrecedentWeight: weightByConfidence[proposal.confidence],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Apply rejection precedent to reduce effective trigger confidence.
|
|
391
|
+
* When a field has prior rejection records, the aggregated precedent
|
|
392
|
+
* weight penalises future proposals for the same field.
|
|
393
|
+
*
|
|
394
|
+
* Formula (RFC-0031 §12.6): `factor = max(floor, 1.0 - avgWeight × 0.5)`
|
|
395
|
+
* where `floor` is `confidencePenaltyFloor` from calibration config
|
|
396
|
+
* (default 0.2 — at most 80% suppression).
|
|
397
|
+
*
|
|
398
|
+
* Returns a factor in `[floor, 1.0]`. Callers decide how to interpret.
|
|
399
|
+
*
|
|
400
|
+
* @param calibrationConfig Optional resolved config from `parseRevisionProposalCalibrationYaml()`.
|
|
401
|
+
* When omitted, shipped AISDLC-271 floor (0.2) is used.
|
|
402
|
+
*/
|
|
403
|
+
export function computeRejectionPrecedentFactor(field, rejections, calibrationConfig) {
|
|
404
|
+
const fieldRejections = rejections.filter((r) => r.field === field);
|
|
405
|
+
if (fieldRejections.length === 0)
|
|
406
|
+
return 1.0;
|
|
407
|
+
// Average rejection precedent weight across all prior rejections for this field.
|
|
408
|
+
const avgWeight = fieldRejections.reduce((acc, r) => acc + r.rejectionPrecedentWeight, 0) /
|
|
409
|
+
fieldRejections.length;
|
|
410
|
+
// The precedent factor reduces future proposal confidence:
|
|
411
|
+
// higher avg rejection weight → lower factor (penalty)
|
|
412
|
+
const rawFactor = 1.0 - avgWeight * 0.5;
|
|
413
|
+
const floor = calibrationConfig?.rejectionPrecedent.confidencePenaltyFloor ??
|
|
414
|
+
DEFAULT_CONFIDENCE_PENALTY_FLOOR;
|
|
415
|
+
return Math.max(floor, rawFactor);
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=revision-proposal.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RawSignal, SignalSourceAdapter } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default env var holding either a Discord bot token or a Slack bot token
|
|
4
|
+
* per RFC-0030 OQ-13.1 v0.3 re-walkthrough. Both platforms support bot-token
|
|
5
|
+
* based access without OAuth; the env var name follows the
|
|
6
|
+
* `SIGNAL_<PLATFORM>_BOT_TOKEN` convention.
|
|
7
|
+
*
|
|
8
|
+
* Adapters wired to a single platform set the env var name explicitly via
|
|
9
|
+
* `envVarName`; the default `SIGNAL_COMMUNITY_BOT_TOKEN` is the generic
|
|
10
|
+
* fallback for adopters who alias their secret manager output.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_COMMUNITY_THREAD_ENV_VAR = "SIGNAL_COMMUNITY_BOT_TOKEN";
|
|
13
|
+
export interface CommunityThreadAdapterOptions {
|
|
14
|
+
signals?: RawSignal[];
|
|
15
|
+
available?: boolean;
|
|
16
|
+
/** Legacy test override (pre-OQ-13.1 v0.3); emits collapsed Decision. */
|
|
17
|
+
credentialInvalid?: boolean;
|
|
18
|
+
/** RFC-0030 OQ-13.1 v0.3: env-var missing test override. */
|
|
19
|
+
credentialNotConfigured?: boolean;
|
|
20
|
+
/** RFC-0030 OQ-13.1 v0.3: env-var present but upstream rejected. */
|
|
21
|
+
credentialRejected?: boolean;
|
|
22
|
+
/** Override the env var name (e.g. `SIGNAL_DISCORD_BOT_TOKEN`). */
|
|
23
|
+
envVarName?: string;
|
|
24
|
+
/** Optional explicit env accessor (test seam). Defaults to `process.env`. */
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
26
|
+
/**
|
|
27
|
+
* When `true`, the adapter probes its env var. See `SupportTicketAdapter`
|
|
28
|
+
* for backward-compat rationale (`false` default).
|
|
29
|
+
*/
|
|
30
|
+
probeEnvVar?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class CommunityThreadSignalSourceAdapter implements SignalSourceAdapter {
|
|
33
|
+
private readonly options;
|
|
34
|
+
readonly name = "signal-source-community-thread";
|
|
35
|
+
readonly defaultTier = 2;
|
|
36
|
+
/** OAuth NOT required for v1 Discord-bot-token / Slack-bot-token model. */
|
|
37
|
+
readonly requiresOAuth = false;
|
|
38
|
+
constructor(options?: CommunityThreadAdapterOptions);
|
|
39
|
+
get envVarName(): string;
|
|
40
|
+
isAvailable(): Promise<boolean>;
|
|
41
|
+
fetchSignals(since: Date): Promise<RawSignal[]>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=community-thread.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AdapterCredentialInvalid, AdapterCredentialNotConfigured, AdapterCredentialRejected, } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default env var holding either a Discord bot token or a Slack bot token
|
|
4
|
+
* per RFC-0030 OQ-13.1 v0.3 re-walkthrough. Both platforms support bot-token
|
|
5
|
+
* based access without OAuth; the env var name follows the
|
|
6
|
+
* `SIGNAL_<PLATFORM>_BOT_TOKEN` convention.
|
|
7
|
+
*
|
|
8
|
+
* Adapters wired to a single platform set the env var name explicitly via
|
|
9
|
+
* `envVarName`; the default `SIGNAL_COMMUNITY_BOT_TOKEN` is the generic
|
|
10
|
+
* fallback for adopters who alias their secret manager output.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_COMMUNITY_THREAD_ENV_VAR = 'SIGNAL_COMMUNITY_BOT_TOKEN';
|
|
13
|
+
export class CommunityThreadSignalSourceAdapter {
|
|
14
|
+
options;
|
|
15
|
+
name = 'signal-source-community-thread';
|
|
16
|
+
defaultTier = 2;
|
|
17
|
+
/** OAuth NOT required for v1 Discord-bot-token / Slack-bot-token model. */
|
|
18
|
+
requiresOAuth = false;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
get envVarName() {
|
|
23
|
+
return this.options.envVarName ?? DEFAULT_COMMUNITY_THREAD_ENV_VAR;
|
|
24
|
+
}
|
|
25
|
+
async isAvailable() {
|
|
26
|
+
if (this.options.available !== undefined)
|
|
27
|
+
return this.options.available;
|
|
28
|
+
if (this.options.probeEnvVar) {
|
|
29
|
+
const env = this.options.env ?? process.env;
|
|
30
|
+
const v = env[this.envVarName];
|
|
31
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
async fetchSignals(since) {
|
|
36
|
+
if (this.options.credentialNotConfigured) {
|
|
37
|
+
throw new AdapterCredentialNotConfigured(this.name, this.envVarName);
|
|
38
|
+
}
|
|
39
|
+
if (this.options.credentialRejected) {
|
|
40
|
+
throw new AdapterCredentialRejected(this.name);
|
|
41
|
+
}
|
|
42
|
+
if (this.options.credentialInvalid) {
|
|
43
|
+
throw new AdapterCredentialInvalid(this.name);
|
|
44
|
+
}
|
|
45
|
+
if (this.options.probeEnvVar && this.options.available === undefined) {
|
|
46
|
+
const env = this.options.env ?? process.env;
|
|
47
|
+
const v = env[this.envVarName];
|
|
48
|
+
if (!(typeof v === 'string' && v.trim().length > 0)) {
|
|
49
|
+
throw new AdapterCredentialNotConfigured(this.name, this.envVarName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return (this.options.signals ?? []).filter((signal) => signal.sourceTimestamp >= since);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=community-thread.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RawSignal, SignalSourceAdapter } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* RFC-0030 OQ-13.1 v0.3 re-walkthrough: env-var-based in-app-feedback
|
|
4
|
+
* adapter (e.g. Productboard, Pendo, in-house widget). Reads its API key from
|
|
5
|
+
* `SIGNAL_IN_APP_FEEDBACK_API_KEY`. Phase 1 ships env-var-based adapters
|
|
6
|
+
* ONLY; OAuth-required services defer to the future credential-management RFC.
|
|
7
|
+
*
|
|
8
|
+
* Two distinct failure modes per OQ-13.1 v0.3:
|
|
9
|
+
* - **Not configured** — env var missing or empty → emit setup task.
|
|
10
|
+
* - **Rejected** — env var present but upstream auth call failed → emit
|
|
11
|
+
* rotation task.
|
|
12
|
+
*
|
|
13
|
+
* The Phase 1 adapter is in-memory (test fixture mode) — production wiring
|
|
14
|
+
* to a real upstream service is per-deployment. The env-var probe + dual
|
|
15
|
+
* failure-mode split are the contract this adapter ships.
|
|
16
|
+
*/
|
|
17
|
+
export interface InAppFeedbackAdapterOptions {
|
|
18
|
+
signals?: RawSignal[];
|
|
19
|
+
/**
|
|
20
|
+
* Test override: when set, bypasses the env-var probe and `isAvailable()`
|
|
21
|
+
* returns this value directly. Useful for fixtures that need to simulate
|
|
22
|
+
* "configured but upstream rejected" without touching `process.env`.
|
|
23
|
+
*/
|
|
24
|
+
available?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Test override: when `true`, `fetchSignals()` raises
|
|
27
|
+
* `AdapterCredentialNotConfigured` regardless of env probe (test seam for
|
|
28
|
+
* forcing the "not configured" Decision path).
|
|
29
|
+
*/
|
|
30
|
+
credentialNotConfigured?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Test override: when `true`, `fetchSignals()` raises
|
|
33
|
+
* `AdapterCredentialRejected` (env var was present but upstream said 401/403).
|
|
34
|
+
*/
|
|
35
|
+
credentialRejected?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Override the env var name (defaults to `SIGNAL_IN_APP_FEEDBACK_API_KEY`).
|
|
38
|
+
* Allows adopters to wire to their existing secret-naming convention.
|
|
39
|
+
*/
|
|
40
|
+
envVarName?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional explicit env accessor (test seam). Defaults to `process.env`.
|
|
43
|
+
* Allows hermetic tests without mutating the global process env.
|
|
44
|
+
*/
|
|
45
|
+
env?: NodeJS.ProcessEnv;
|
|
46
|
+
}
|
|
47
|
+
export declare const DEFAULT_IN_APP_FEEDBACK_ENV_VAR = "SIGNAL_IN_APP_FEEDBACK_API_KEY";
|
|
48
|
+
export declare class InAppFeedbackSignalSourceAdapter implements SignalSourceAdapter {
|
|
49
|
+
private readonly options;
|
|
50
|
+
readonly name = "signal-source-in-app-feedback";
|
|
51
|
+
readonly defaultTier = 1;
|
|
52
|
+
/** OAuth NOT required — env-var-based API key only. */
|
|
53
|
+
readonly requiresOAuth = false;
|
|
54
|
+
constructor(options?: InAppFeedbackAdapterOptions);
|
|
55
|
+
/** Resolve the env var name for this adapter. */
|
|
56
|
+
get envVarName(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Adapter availability: env-var-probe-based. Returns `true` when the
|
|
59
|
+
* configured env var is present and non-empty, OR when the `available`
|
|
60
|
+
* test override is set. Returns `false` otherwise — the registry will
|
|
61
|
+
* surface `Decision: adapter-credential-not-configured` rather than
|
|
62
|
+
* blocking the pipeline.
|
|
63
|
+
*/
|
|
64
|
+
isAvailable(): Promise<boolean>;
|
|
65
|
+
fetchSignals(since: Date): Promise<RawSignal[]>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=in-app-feedback.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AdapterCredentialNotConfigured, AdapterCredentialRejected } from '../errors.js';
|
|
2
|
+
export const DEFAULT_IN_APP_FEEDBACK_ENV_VAR = 'SIGNAL_IN_APP_FEEDBACK_API_KEY';
|
|
3
|
+
export class InAppFeedbackSignalSourceAdapter {
|
|
4
|
+
options;
|
|
5
|
+
name = 'signal-source-in-app-feedback';
|
|
6
|
+
defaultTier = 1;
|
|
7
|
+
/** OAuth NOT required — env-var-based API key only. */
|
|
8
|
+
requiresOAuth = false;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
/** Resolve the env var name for this adapter. */
|
|
13
|
+
get envVarName() {
|
|
14
|
+
return this.options.envVarName ?? DEFAULT_IN_APP_FEEDBACK_ENV_VAR;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Adapter availability: env-var-probe-based. Returns `true` when the
|
|
18
|
+
* configured env var is present and non-empty, OR when the `available`
|
|
19
|
+
* test override is set. Returns `false` otherwise — the registry will
|
|
20
|
+
* surface `Decision: adapter-credential-not-configured` rather than
|
|
21
|
+
* blocking the pipeline.
|
|
22
|
+
*/
|
|
23
|
+
async isAvailable() {
|
|
24
|
+
if (this.options.available !== undefined)
|
|
25
|
+
return this.options.available;
|
|
26
|
+
const env = this.options.env ?? process.env;
|
|
27
|
+
const v = env[this.envVarName];
|
|
28
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
async fetchSignals(since) {
|
|
31
|
+
// Test overrides take precedence so fixtures can exercise both failure
|
|
32
|
+
// paths regardless of env state.
|
|
33
|
+
if (this.options.credentialNotConfigured) {
|
|
34
|
+
throw new AdapterCredentialNotConfigured(this.name, this.envVarName);
|
|
35
|
+
}
|
|
36
|
+
if (this.options.credentialRejected) {
|
|
37
|
+
throw new AdapterCredentialRejected(this.name);
|
|
38
|
+
}
|
|
39
|
+
// Production path: re-probe env var (unless `available: true` was set as
|
|
40
|
+
// an explicit bypass for hermetic tests that don't care about env probing).
|
|
41
|
+
if (this.options.available === undefined) {
|
|
42
|
+
const env = this.options.env ?? process.env;
|
|
43
|
+
const v = env[this.envVarName];
|
|
44
|
+
if (!(typeof v === 'string' && v.trim().length > 0)) {
|
|
45
|
+
throw new AdapterCredentialNotConfigured(this.name, this.envVarName);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (this.options.signals ?? []).filter((signal) => signal.sourceTimestamp >= since);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=in-app-feedback.js.map
|