@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.
Files changed (172) hide show
  1. package/dist/__test-helpers/git-env.d.ts +40 -0
  2. package/dist/__test-helpers/git-env.js +73 -0
  3. package/dist/adapters.d.ts +42 -3
  4. package/dist/adapters.js +133 -3
  5. package/dist/admission-composite.d.ts +134 -1
  6. package/dist/admission-composite.js +170 -6
  7. package/dist/admission-enrichment.d.ts +12 -3
  8. package/dist/admission-enrichment.js +71 -10
  9. package/dist/admission-hc.d.ts +20 -1
  10. package/dist/admission-hc.js +12 -1
  11. package/dist/admission-score.d.ts +44 -0
  12. package/dist/admission-score.js +11 -2
  13. package/dist/backlog-adapter.d.ts +45 -0
  14. package/dist/backlog-adapter.js +145 -1
  15. package/dist/calibration.d.ts +81 -0
  16. package/dist/calibration.js +76 -0
  17. package/dist/cli/commands/git-remote.d.ts +25 -0
  18. package/dist/cli/commands/git-remote.js +80 -1
  19. package/dist/cli/commands/init-features.d.ts +507 -0
  20. package/dist/cli/commands/init-features.js +1100 -0
  21. package/dist/cli/commands/init-templates.d.ts +301 -0
  22. package/dist/cli/commands/init-templates.js +1341 -0
  23. package/dist/cli/commands/init.d.ts +67 -0
  24. package/dist/cli/commands/init.js +301 -2
  25. package/dist/cli/commands/run.js +9 -1
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +42 -8
  28. package/dist/compliance/composer.d.ts +79 -0
  29. package/dist/compliance/composer.js +258 -0
  30. package/dist/compliance/errors.d.ts +64 -0
  31. package/dist/compliance/errors.js +85 -0
  32. package/dist/compliance/loader.d.ts +52 -0
  33. package/dist/compliance/loader.js +124 -0
  34. package/dist/compliance/types.d.ts +184 -0
  35. package/dist/compliance/types.js +41 -0
  36. package/dist/compliance-clearance.d.ts +269 -0
  37. package/dist/compliance-clearance.js +269 -0
  38. package/dist/config.js +17 -0
  39. package/dist/cost-tracker.d.ts +22 -0
  40. package/dist/cost-tracker.js +41 -0
  41. package/dist/database/adapters/external.js +5 -1
  42. package/dist/design-authority.d.ts +13 -0
  43. package/dist/design-authority.js +13 -0
  44. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  45. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  46. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  47. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  48. package/dist/embedding/cross-provider.d.ts +78 -0
  49. package/dist/embedding/cross-provider.js +75 -0
  50. package/dist/embedding/deprecation.d.ts +151 -0
  51. package/dist/embedding/deprecation.js +229 -0
  52. package/dist/embedding/errors.d.ts +90 -0
  53. package/dist/embedding/errors.js +150 -0
  54. package/dist/embedding/index.d.ts +29 -0
  55. package/dist/embedding/index.js +24 -0
  56. package/dist/embedding/pipeline-load.d.ts +146 -0
  57. package/dist/embedding/pipeline-load.js +178 -0
  58. package/dist/embedding/registry.d.ts +45 -0
  59. package/dist/embedding/registry.js +61 -0
  60. package/dist/embedding/stale-vector.d.ts +110 -0
  61. package/dist/embedding/stale-vector.js +92 -0
  62. package/dist/embedding/storage/index.d.ts +51 -0
  63. package/dist/embedding/storage/index.js +43 -0
  64. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  65. package/dist/embedding/storage/jsonl-backend.js +332 -0
  66. package/dist/embedding/storage/types.d.ts +135 -0
  67. package/dist/embedding/storage/types.js +13 -0
  68. package/dist/embedding/types.d.ts +180 -0
  69. package/dist/embedding/types.js +10 -0
  70. package/dist/execute.d.ts +29 -2
  71. package/dist/execute.js +179 -35
  72. package/dist/index.d.ts +14 -2
  73. package/dist/index.js +19 -2
  74. package/dist/journey/inheritance-validator.d.ts +396 -0
  75. package/dist/journey/inheritance-validator.js +370 -0
  76. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  77. package/dist/journey/state-id-drift-rule.js +245 -0
  78. package/dist/journey-sa2-router.d.ts +395 -0
  79. package/dist/journey-sa2-router.js +308 -0
  80. package/dist/models/classifier.d.ts +3 -1
  81. package/dist/models/classifier.js +61 -5
  82. package/dist/pillar-breakdown.d.ts +23 -1
  83. package/dist/pillar-breakdown.js +9 -0
  84. package/dist/runners/review-agent.js +6 -2
  85. package/dist/runners/runner-registry.d.ts +36 -0
  86. package/dist/runners/runner-registry.js +90 -0
  87. package/dist/runtime/attestations.d.ts +663 -11
  88. package/dist/runtime/attestations.js +971 -24
  89. package/dist/runtime/index.d.ts +1 -1
  90. package/dist/runtime/index.js +1 -1
  91. package/dist/runtime/parallelism-flag.d.ts +21 -3
  92. package/dist/runtime/parallelism-flag.js +27 -6
  93. package/dist/runtime/port-allocator.d.ts +11 -0
  94. package/dist/runtime/port-allocator.js +30 -0
  95. package/dist/sa-scoring/layer3-llm.js +6 -1
  96. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  97. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  98. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  99. package/dist/sa-scoring/revision-proposal.js +417 -0
  100. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  101. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  102. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  103. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  104. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  105. package/dist/signal-ingestion/adapters/manual.js +112 -0
  106. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  107. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  108. package/dist/signal-ingestion/classifier.d.ts +205 -0
  109. package/dist/signal-ingestion/classifier.js +494 -0
  110. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  111. package/dist/signal-ingestion/clustering-types.js +14 -0
  112. package/dist/signal-ingestion/clustering.d.ts +200 -0
  113. package/dist/signal-ingestion/clustering.js +413 -0
  114. package/dist/signal-ingestion/config.d.ts +351 -0
  115. package/dist/signal-ingestion/config.js +587 -0
  116. package/dist/signal-ingestion/d1.d.ts +252 -0
  117. package/dist/signal-ingestion/d1.js +235 -0
  118. package/dist/signal-ingestion/errors.d.ts +73 -0
  119. package/dist/signal-ingestion/errors.js +108 -0
  120. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  121. package/dist/signal-ingestion/governance-events.js +189 -0
  122. package/dist/signal-ingestion/index.d.ts +35 -0
  123. package/dist/signal-ingestion/index.js +53 -0
  124. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  125. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  126. package/dist/signal-ingestion/registry.d.ts +40 -0
  127. package/dist/signal-ingestion/registry.js +137 -0
  128. package/dist/signal-ingestion/residency.d.ts +227 -0
  129. package/dist/signal-ingestion/residency.js +238 -0
  130. package/dist/signal-ingestion/significance.d.ts +554 -0
  131. package/dist/signal-ingestion/significance.js +555 -0
  132. package/dist/signal-ingestion/types.d.ts +191 -0
  133. package/dist/signal-ingestion/types.js +8 -0
  134. package/dist/substrate/drift-composition.d.ts +270 -0
  135. package/dist/substrate/drift-composition.js +306 -0
  136. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  137. package/dist/substrate/drift-tui-surface.js +102 -0
  138. package/dist/substrate/identity-class.d.ts +176 -0
  139. package/dist/substrate/identity-class.js +201 -0
  140. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  141. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  142. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  143. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  144. package/dist/tessellation/rule-registry.d.ts +269 -0
  145. package/dist/tessellation/rule-registry.js +92 -0
  146. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  147. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  148. package/dist/tessellation-admission.d.ts +162 -0
  149. package/dist/tessellation-admission.js +146 -0
  150. package/dist/tessellation-drift.d.ts +246 -0
  151. package/dist/tessellation-drift.js +250 -0
  152. package/dist/validate-config.js +13 -0
  153. package/dist/validate-issue.js +2 -2
  154. package/dist/variant/cardinality-activation.d.ts +126 -0
  155. package/dist/variant/cardinality-activation.js +101 -0
  156. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  157. package/dist/variant/deprecation-lifecycle.js +208 -0
  158. package/dist/variant/drift-extension.d.ts +136 -0
  159. package/dist/variant/drift-extension.js +164 -0
  160. package/dist/variant/engineering-review.d.ts +185 -0
  161. package/dist/variant/engineering-review.js +142 -0
  162. package/dist/variant/index.d.ts +32 -0
  163. package/dist/variant/index.js +32 -0
  164. package/dist/variant/inheritance-validator.d.ts +165 -0
  165. package/dist/variant/inheritance-validator.js +139 -0
  166. package/dist/variant/internal-adopter/index.d.ts +11 -0
  167. package/dist/variant/internal-adopter/index.js +10 -0
  168. package/dist/variant/internal-adopter/products.d.ts +156 -0
  169. package/dist/variant/internal-adopter/products.js +366 -0
  170. package/dist/variant-admission.d.ts +316 -0
  171. package/dist/variant-admission.js +247 -0
  172. 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