@ai-sdlc/orchestrator 0.10.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 (156) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/backlog-adapter.d.ts +45 -0
  10. package/dist/backlog-adapter.js +145 -1
  11. package/dist/calibration.d.ts +81 -0
  12. package/dist/calibration.js +76 -0
  13. package/dist/cli/commands/init-features.d.ts +301 -2
  14. package/dist/cli/commands/init-features.js +634 -7
  15. package/dist/cli/commands/init-templates.d.ts +198 -1
  16. package/dist/cli/commands/init-templates.js +943 -1
  17. package/dist/cli/commands/init.d.ts +45 -0
  18. package/dist/cli/commands/init.js +147 -5
  19. package/dist/cli/commands/run.js +9 -1
  20. package/dist/cli/index.d.ts +11 -0
  21. package/dist/cli/index.js +42 -8
  22. package/dist/compliance/composer.d.ts +79 -0
  23. package/dist/compliance/composer.js +258 -0
  24. package/dist/compliance/errors.d.ts +64 -0
  25. package/dist/compliance/errors.js +85 -0
  26. package/dist/compliance/loader.d.ts +52 -0
  27. package/dist/compliance/loader.js +124 -0
  28. package/dist/compliance/types.d.ts +184 -0
  29. package/dist/compliance/types.js +41 -0
  30. package/dist/compliance-clearance.d.ts +269 -0
  31. package/dist/compliance-clearance.js +269 -0
  32. package/dist/config.js +17 -0
  33. package/dist/cost-tracker.d.ts +22 -0
  34. package/dist/cost-tracker.js +41 -0
  35. package/dist/database/adapters/external.js +5 -1
  36. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  37. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  38. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  39. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  40. package/dist/embedding/cross-provider.d.ts +78 -0
  41. package/dist/embedding/cross-provider.js +75 -0
  42. package/dist/embedding/deprecation.d.ts +151 -0
  43. package/dist/embedding/deprecation.js +229 -0
  44. package/dist/embedding/errors.d.ts +90 -0
  45. package/dist/embedding/errors.js +150 -0
  46. package/dist/embedding/index.d.ts +29 -0
  47. package/dist/embedding/index.js +24 -0
  48. package/dist/embedding/pipeline-load.d.ts +146 -0
  49. package/dist/embedding/pipeline-load.js +178 -0
  50. package/dist/embedding/registry.d.ts +45 -0
  51. package/dist/embedding/registry.js +61 -0
  52. package/dist/embedding/stale-vector.d.ts +110 -0
  53. package/dist/embedding/stale-vector.js +92 -0
  54. package/dist/embedding/storage/index.d.ts +51 -0
  55. package/dist/embedding/storage/index.js +43 -0
  56. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  57. package/dist/embedding/storage/jsonl-backend.js +332 -0
  58. package/dist/embedding/storage/types.d.ts +135 -0
  59. package/dist/embedding/storage/types.js +13 -0
  60. package/dist/embedding/types.d.ts +180 -0
  61. package/dist/embedding/types.js +10 -0
  62. package/dist/execute.d.ts +29 -2
  63. package/dist/execute.js +171 -30
  64. package/dist/index.d.ts +14 -2
  65. package/dist/index.js +19 -2
  66. package/dist/journey/inheritance-validator.d.ts +396 -0
  67. package/dist/journey/inheritance-validator.js +370 -0
  68. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  69. package/dist/journey/state-id-drift-rule.js +245 -0
  70. package/dist/journey-sa2-router.d.ts +395 -0
  71. package/dist/journey-sa2-router.js +308 -0
  72. package/dist/runners/review-agent.js +6 -2
  73. package/dist/runners/runner-registry.d.ts +36 -0
  74. package/dist/runners/runner-registry.js +90 -0
  75. package/dist/runtime/attestations.d.ts +173 -13
  76. package/dist/runtime/attestations.js +252 -40
  77. package/dist/runtime/index.d.ts +1 -1
  78. package/dist/runtime/index.js +1 -1
  79. package/dist/sa-scoring/layer3-llm.js +6 -1
  80. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  81. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  82. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  83. package/dist/sa-scoring/revision-proposal.js +417 -0
  84. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  85. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  86. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  87. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  88. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  89. package/dist/signal-ingestion/adapters/manual.js +112 -0
  90. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  91. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  92. package/dist/signal-ingestion/classifier.d.ts +205 -0
  93. package/dist/signal-ingestion/classifier.js +494 -0
  94. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  95. package/dist/signal-ingestion/clustering-types.js +14 -0
  96. package/dist/signal-ingestion/clustering.d.ts +200 -0
  97. package/dist/signal-ingestion/clustering.js +413 -0
  98. package/dist/signal-ingestion/config.d.ts +351 -0
  99. package/dist/signal-ingestion/config.js +587 -0
  100. package/dist/signal-ingestion/d1.d.ts +252 -0
  101. package/dist/signal-ingestion/d1.js +235 -0
  102. package/dist/signal-ingestion/errors.d.ts +73 -0
  103. package/dist/signal-ingestion/errors.js +108 -0
  104. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  105. package/dist/signal-ingestion/governance-events.js +189 -0
  106. package/dist/signal-ingestion/index.d.ts +35 -0
  107. package/dist/signal-ingestion/index.js +53 -0
  108. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  109. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  110. package/dist/signal-ingestion/registry.d.ts +40 -0
  111. package/dist/signal-ingestion/registry.js +137 -0
  112. package/dist/signal-ingestion/residency.d.ts +227 -0
  113. package/dist/signal-ingestion/residency.js +238 -0
  114. package/dist/signal-ingestion/significance.d.ts +554 -0
  115. package/dist/signal-ingestion/significance.js +555 -0
  116. package/dist/signal-ingestion/types.d.ts +191 -0
  117. package/dist/signal-ingestion/types.js +8 -0
  118. package/dist/substrate/drift-composition.d.ts +270 -0
  119. package/dist/substrate/drift-composition.js +306 -0
  120. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  121. package/dist/substrate/drift-tui-surface.js +102 -0
  122. package/dist/substrate/identity-class.d.ts +176 -0
  123. package/dist/substrate/identity-class.js +201 -0
  124. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  125. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  126. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  127. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  128. package/dist/tessellation/rule-registry.d.ts +269 -0
  129. package/dist/tessellation/rule-registry.js +92 -0
  130. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  131. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  132. package/dist/tessellation-admission.d.ts +162 -0
  133. package/dist/tessellation-admission.js +146 -0
  134. package/dist/tessellation-drift.d.ts +246 -0
  135. package/dist/tessellation-drift.js +250 -0
  136. package/dist/validate-config.js +13 -0
  137. package/dist/validate-issue.js +2 -2
  138. package/dist/variant/cardinality-activation.d.ts +126 -0
  139. package/dist/variant/cardinality-activation.js +101 -0
  140. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  141. package/dist/variant/deprecation-lifecycle.js +208 -0
  142. package/dist/variant/drift-extension.d.ts +136 -0
  143. package/dist/variant/drift-extension.js +164 -0
  144. package/dist/variant/engineering-review.d.ts +185 -0
  145. package/dist/variant/engineering-review.js +142 -0
  146. package/dist/variant/index.d.ts +32 -0
  147. package/dist/variant/index.js +32 -0
  148. package/dist/variant/inheritance-validator.d.ts +165 -0
  149. package/dist/variant/inheritance-validator.js +139 -0
  150. package/dist/variant/internal-adopter/index.d.ts +11 -0
  151. package/dist/variant/internal-adopter/index.js +10 -0
  152. package/dist/variant/internal-adopter/products.d.ts +156 -0
  153. package/dist/variant/internal-adopter/products.js +366 -0
  154. package/dist/variant-admission.d.ts +316 -0
  155. package/dist/variant-admission.js +247 -0
  156. package/package.json +10 -8
@@ -0,0 +1,269 @@
1
+ /**
2
+ * RFC-0009 Phase 4.1 — Eρ₅ Compliance Clearance scoring.
3
+ *
4
+ * Implements the categorical 0/1 gating dimension from RFC-0009 §7.1:
5
+ *
6
+ * Eρ₅ = soul.triad.engineering.complianceRegimes[].clearance(work_item)
7
+ * = 0 if any named regime is violated
8
+ * = 1 otherwise
9
+ *
10
+ * **OQ-5 scope** (v3.3 resolution, 2026-05-03): HARD regulatory frameworks
11
+ * ONLY — anything with formal external-audit consequences. Internal best
12
+ * practices, code style, architectural preferences, and team conventions are
13
+ * OUT OF SCOPE and rejected at declaration time per the boundary test:
14
+ * "would an external regulator or auditor have grounds to act on a violation?"
15
+ *
16
+ * In scope:
17
+ * - GDPR (EU data protection)
18
+ * - HIPAA (US healthcare)
19
+ * - SOC2 (audit trust framework — variants like SOC2-T2 accepted)
20
+ * - PCI-DSS (payment card data — variants like PCI-DSS-L1 accepted)
21
+ * - FedRAMP (US federal cloud — variants like FedRAMP-Moderate accepted)
22
+ * - Regional data-residency (Schrems II, EU data-localization, China PIPL,
23
+ * Canadian PIPEDA cross-border)
24
+ * - Regulated-industry rules (financial KYC/AML, healthcare device
25
+ * certification, telecom regulations, ISO-27001 audit framework, etc.)
26
+ *
27
+ * Out of scope (rejected at declaration time):
28
+ * - Internal best-practices ("clean-code", "team-style", "house-style")
29
+ * - Code-quality rules ("eslint-recommended", "prettier-standard")
30
+ * - Architectural preferences ("hexagonal", "ddd-strict")
31
+ * - Team conventions ("our-conventions", "internal-coding-standard")
32
+ *
33
+ * **Adopter opt-in gate** (RFC-0009 §10 Phase 4): Eρ₅ is gated behind
34
+ * `ComplianceClearanceContext.enabled === true` initially. Promotion to
35
+ * default behavior is subject to ecosystem feedback. When disabled (default),
36
+ * the admission composite does not apply the Eρ₅ multiplier and behaves
37
+ * exactly as in Phase 2/3 — full backward compatibility.
38
+ *
39
+ * **RFC-0022 consumption surface**: when an adopter wires the loaded
40
+ * `CompliancePosture[]` into `ComplianceClearanceContext.posture`, the
41
+ * declared regimes from that resource compose with the soul's own
42
+ * `complianceRegimes` field. Both sources contribute to the regime set
43
+ * the work item is checked against; clearance is gating across the union.
44
+ *
45
+ * @see spec/rfcs/RFC-0009-tessellated-design-intent-documents.md §7.1 + §10
46
+ * @see spec/rfcs/RFC-0022-compliance-posture-audit-surface.md (consumption surface)
47
+ */
48
+ // ── Hard-regulatory whitelist (OQ-5 scope) ─────────────────────────────
49
+ /**
50
+ * Canonical hard-regulatory framework prefixes per RFC-0009 §7.1 OQ-5 resolution.
51
+ *
52
+ * Matching is **prefix-based** to accept variants like `SOC2-T2`, `PCI-DSS-L1`,
53
+ * `FedRAMP-Moderate`, `FedRAMP-High`, `ISO-27001:2022`. The exact framework name
54
+ * (without tier/version suffix) is the prefix; anything starting with one of
55
+ * these prefixes is considered a hard-regulatory regime.
56
+ *
57
+ * Regional data-residency frameworks (Schrems II / PIPL / PIPEDA) are listed
58
+ * explicitly. Generic `DATA-RESIDENCY-*` prefix is also accepted for adopters
59
+ * who declare per-region constraints under a unified naming scheme.
60
+ */
61
+ export const HARD_REGULATORY_REGIME_PREFIXES = Object.freeze([
62
+ // ── Core US/EU/global frameworks ────────────────────────────────────
63
+ 'GDPR', // EU General Data Protection Regulation
64
+ 'HIPAA', // US Health Insurance Portability and Accountability Act
65
+ 'SOC2', // Service Organization Control 2 (T1 / T2 variants)
66
+ 'PCI-DSS', // Payment Card Industry Data Security Standard (L1-L4 variants)
67
+ 'FedRAMP', // US Federal Risk and Authorization Management Program
68
+ 'ISO-27001', // ISO/IEC 27001 Information Security Management Systems
69
+ // ── Regional data-residency frameworks ──────────────────────────────
70
+ 'PIPL', // China Personal Information Protection Law
71
+ 'PIPEDA', // Canadian Personal Information Protection and Electronic Documents Act
72
+ 'SCHREMS-II', // EU Schrems II ruling (cross-border data transfers)
73
+ 'DATA-RESIDENCY', // Generic regional residency declaration
74
+ // ── Regulated-industry rules ────────────────────────────────────────
75
+ 'KYC', // Know-Your-Customer (financial services)
76
+ 'AML', // Anti-Money Laundering (financial services)
77
+ 'GLBA', // Gramm-Leach-Bliley Act (US financial privacy)
78
+ 'SOX', // Sarbanes-Oxley Act (US public-company audit)
79
+ 'NERC-CIP', // North American Electric Reliability Corp. Critical Infrastructure Protection
80
+ 'FDA-21CFR11', // FDA 21 CFR Part 11 (regulated healthcare/pharma device)
81
+ 'CCPA', // California Consumer Privacy Act (state-level, regulator-enforced)
82
+ 'CPRA', // California Privacy Rights Act (CCPA successor)
83
+ ]);
84
+ /**
85
+ * Predicate: returns true iff the regime ID matches a hard-regulatory prefix
86
+ * per the OQ-5 scope whitelist.
87
+ *
88
+ * Comparison is case-insensitive on the regime ID (`HIPAA` and `hipaa` both
89
+ * match) but the canonical prefixes are uppercase by convention.
90
+ *
91
+ * **Boundary guard (AISDLC-316 round-2 code-review fix)**: a match requires the
92
+ * prefix to be either the WHOLE regime id OR followed by a separator
93
+ * (`-`, `_`, `:`, `.`) or a digit (tier-variant pattern). This prevents
94
+ * collision-prone soft regimes like `SOXophone`, `AMLET`, `KYCS-internal`,
95
+ * `HIPAAphobia` from being mis-classified as hard-regulatory by a naive
96
+ * `startsWith` check. Tier-variants like `SOC2-T2`, `PCI-DSS-L1`,
97
+ * `FedRAMP-Moderate`, `ISO-27001:2022`, `SOC2T2` (digit boundary) still match.
98
+ *
99
+ * @example
100
+ * isHardRegulatoryRegime('HIPAA') // true
101
+ * isHardRegulatoryRegime('SOC2-T2') // true (SOC2 prefix + '-' boundary)
102
+ * isHardRegulatoryRegime('PCI-DSS-L1') // true (PCI-DSS prefix + '-' boundary)
103
+ * isHardRegulatoryRegime('FedRAMP-Moderate')// true (FedRAMP prefix + '-' boundary)
104
+ * isHardRegulatoryRegime('ISO-27001:2022') // true (ISO-27001 prefix + ':' boundary)
105
+ * isHardRegulatoryRegime('clean-code') // false (soft / out of scope)
106
+ * isHardRegulatoryRegime('team-style') // false (soft / out of scope)
107
+ * isHardRegulatoryRegime('SOXophone') // false (SOX prefix but letter boundary)
108
+ * isHardRegulatoryRegime('AMLET') // false (AML prefix but letter boundary)
109
+ * isHardRegulatoryRegime('HIPAAphobia') // false (HIPAA prefix but letter boundary)
110
+ */
111
+ export function isHardRegulatoryRegime(regimeId) {
112
+ if (typeof regimeId !== 'string' || regimeId.length === 0)
113
+ return false;
114
+ const upper = regimeId.toUpperCase();
115
+ return HARD_REGULATORY_REGIME_PREFIXES.some((prefix) => {
116
+ const upperPrefix = prefix.toUpperCase();
117
+ if (!upper.startsWith(upperPrefix))
118
+ return false;
119
+ // Whole-id match → accept.
120
+ if (upper.length === upperPrefix.length)
121
+ return true;
122
+ // Boundary char must be a separator or a digit (tier/version variant).
123
+ // Letters following the prefix would be a soft-regime collision (e.g.
124
+ // SOXophone, AMLET, HIPAAphobia) and MUST NOT match.
125
+ const next = upper.charCodeAt(upperPrefix.length);
126
+ // '-' = 45, '_' = 95, ':' = 58, '.' = 46, '0'-'9' = 48-57
127
+ return (next === 45 || // '-'
128
+ next === 95 || // '_'
129
+ next === 58 || // ':'
130
+ next === 46 || // '.'
131
+ (next >= 48 && next <= 57) // digit
132
+ );
133
+ });
134
+ }
135
+ /**
136
+ * Validate a list of declared compliance regimes against the OQ-5 hard-regulatory
137
+ * scope. Used at declaration time (DID load, RFC-0022 posture parse) to reject
138
+ * soft regimes before they reach the Eρ₅ scoring path.
139
+ *
140
+ * Per RFC-0009 OQ-5 sub-decision: soft/advisory regimes are NOT in scope and
141
+ * MUST be filtered at declaration time. Internal best-practices, code style,
142
+ * and team conventions belong to other mechanisms (code review, lint).
143
+ *
144
+ * @param regimes - Declared regime IDs from a soul's `complianceRegimes`
145
+ * field or an RFC-0022 `CompliancePosture.spec.regimes[].id`.
146
+ * @returns Validation result with split accepted/rejected lists.
147
+ */
148
+ export function validateComplianceRegimes(regimes) {
149
+ if (!regimes || regimes.length === 0) {
150
+ return { valid: true, accepted: [], rejected: [] };
151
+ }
152
+ const accepted = [];
153
+ const rejected = [];
154
+ for (const r of regimes) {
155
+ if (isHardRegulatoryRegime(r))
156
+ accepted.push(r);
157
+ else
158
+ rejected.push(r);
159
+ }
160
+ return { valid: rejected.length === 0, accepted, rejected };
161
+ }
162
+ // ── Core scoring function ──────────────────────────────────────────────
163
+ /**
164
+ * Compute Eρ₅ Compliance Clearance for a work item.
165
+ *
166
+ * Algorithm (RFC-0009 §7.1 + §10 Phase 4):
167
+ * 1. If `ctx.enabled !== true` → return `er5 = 1` with `routingPath = 'disabled'`.
168
+ * 2. Build the regime set from:
169
+ * - Each soul in `affectedSoulIds`'s `perSoulRegimes` entry, OR
170
+ * the `__platform` sentinel entry when `affectedSoulIds` is empty.
171
+ * - All RFC-0022 posture regimes (if `ctx.posture` present).
172
+ * Filter through `isHardRegulatoryRegime` (defense-in-depth on the OQ-5 boundary).
173
+ * 3. If regime set is empty → `er5 = 1`, `routingPath = 'no-regimes'`.
174
+ * 4. Look up violations for `workItemId` in `ctx.violations` (case-insensitive).
175
+ * Filter to violations whose `regimeId` is in the regime set.
176
+ * - If any remain → `er5 = 0`, `routingPath = 'clearance-violated'`.
177
+ * - Else → `er5 = 1`, `routingPath = 'clearance-holds'`.
178
+ *
179
+ * @param workItemId - Canonical work item ID (e.g. `AISDLC-316`).
180
+ * @param affectedSoulIds - Soul IDs the work item targets (from
181
+ * `resolveAffectedSouls()`). Empty array =
182
+ * non-tessellated or substrate-only; uses
183
+ * `__platform` sentinel.
184
+ * @param ctx - Compliance clearance context (or undefined =
185
+ * treated as disabled).
186
+ * @returns Eρ₅ clearance result with routing audit trail.
187
+ */
188
+ export function computeComplianceClearance(workItemId, affectedSoulIds, ctx) {
189
+ // ── Opt-in gate ────────────────────────────────────────────────
190
+ if (!ctx || ctx.enabled !== true) {
191
+ return {
192
+ er5: 1,
193
+ routingPath: 'disabled',
194
+ checkedRegimes: [],
195
+ violations: [],
196
+ };
197
+ }
198
+ // ── Build the regime set ───────────────────────────────────────
199
+ // Set keys are stored as UPPER-CASE canonical form so that case-collisions
200
+ // between declaration and violation reporting do not silently drop a
201
+ // gating violation. AISDLC-316 round-2 code-review MAJOR #1 fix: previously
202
+ // a soul declaring `['hipaa']` and an enforcement plugin asserting
203
+ // `regimeId: 'HIPAA'` failed the `regimeSet.has(v.regimeId)` lookup
204
+ // (case-sensitive `Set.has`) → violation silently dropped → composite NOT
205
+ // gated. Normalising both sides to uppercase is the defense.
206
+ // `displayCheckedRegimes` preserves the original (first-seen) casing so
207
+ // the audit trail shown in `checkedRegimes` stays human-friendly.
208
+ const regimeSet = new Set();
209
+ const displayCheckedRegimes = [];
210
+ function addRegime(rawId) {
211
+ if (!isHardRegulatoryRegime(rawId))
212
+ return;
213
+ const upper = rawId.toUpperCase();
214
+ if (regimeSet.has(upper))
215
+ return;
216
+ regimeSet.add(upper);
217
+ displayCheckedRegimes.push(rawId);
218
+ }
219
+ // Soul-declared regimes — use affectedSoulIds when present, else __platform.
220
+ const soulIdsToCheck = affectedSoulIds.length === 0 ? ['__platform'] : affectedSoulIds;
221
+ for (const soulId of soulIdsToCheck) {
222
+ const entry = ctx.perSoulRegimes.find((e) => e.soulId === soulId);
223
+ if (!entry)
224
+ continue;
225
+ for (const r of entry.regimes) {
226
+ addRegime(r);
227
+ }
228
+ }
229
+ // RFC-0022 posture regimes — added across the union.
230
+ if (ctx.posture && ctx.posture.length > 0) {
231
+ for (const posture of ctx.posture) {
232
+ for (const regime of posture.spec.regimes ?? []) {
233
+ addRegime(regime.id);
234
+ }
235
+ }
236
+ }
237
+ const checkedRegimes = displayCheckedRegimes;
238
+ // ── No regimes apply ───────────────────────────────────────────
239
+ if (checkedRegimes.length === 0) {
240
+ return {
241
+ er5: 1,
242
+ routingPath: 'no-regimes',
243
+ checkedRegimes: [],
244
+ violations: [],
245
+ };
246
+ }
247
+ // ── Look up violations for this work item ──────────────────────
248
+ // Violation regimeId is normalised to UPPER-CASE before lookup so that a
249
+ // soul declaring `['hipaa']` matches an enforcement plugin asserting
250
+ // `regimeId: 'HIPAA'` (and vice versa). AISDLC-316 round-2 fix.
251
+ const normalizedId = workItemId.toLowerCase();
252
+ const entry = ctx.violations?.find((e) => e.id.toLowerCase() === normalizedId);
253
+ const applicableViolations = entry?.violations.filter((v) => typeof v.regimeId === 'string' && regimeSet.has(v.regimeId.toUpperCase())) ?? [];
254
+ if (applicableViolations.length > 0) {
255
+ return {
256
+ er5: 0,
257
+ routingPath: 'clearance-violated',
258
+ checkedRegimes,
259
+ violations: applicableViolations,
260
+ };
261
+ }
262
+ return {
263
+ er5: 1,
264
+ routingPath: 'clearance-holds',
265
+ checkedRegimes,
266
+ violations: [],
267
+ };
268
+ }
269
+ //# sourceMappingURL=compliance-clearance.js.map
package/dist/config.js CHANGED
@@ -46,6 +46,23 @@ export function loadConfig(configDir) {
46
46
  continue;
47
47
  }
48
48
  const result = validateResource(doc);
49
+ if (result.skipped) {
50
+ // AISDLC-265 PR #474 review fix: skipped kinds get an INFO-level warning
51
+ // entry so typo'd canonical kinds (e.g. `kind: AutonomyPolcy` instead of
52
+ // `AutonomyPolicy`) don't silently disappear. The legitimate loader-private
53
+ // kinds (MaintainersList, SoulTrackMap) appear in this list too, but
54
+ // operators can recognise them and ignore — the dangerous case before the
55
+ // fix was a silent drop with NO surface, which let security-relevant
56
+ // misconfigurations (typo'd AutonomyPolicy → no policy loaded → permissive
57
+ // defaults) ship undetected. See docs/operations/schema-extensions.md.
58
+ const docKind = doc.kind;
59
+ const kindStr = typeof docKind === 'string' ? docKind : String(docKind);
60
+ warnings.push({
61
+ file,
62
+ error: `unknown kind '${kindStr}' — skipped (loader-private convention or typo of canonical kind?)`,
63
+ });
64
+ continue;
65
+ }
49
66
  if (!result.valid) {
50
67
  const msgs = (result.errors ?? []).map((e) => `${e.path}: ${e.message}`).join('; ');
51
68
  // Forward-looking schemas are common during incremental adoption
@@ -3,9 +3,15 @@
3
3
  * and tracks budget status.
4
4
  *
5
5
  * RFC reference: Lines 618-720 (cost tracking).
6
+ *
7
+ * RFC-0019 §10 / AISDLC-337: embeddingTokens line item added via
8
+ * recordEmbeddingCost(). Embedding costs are recorded with
9
+ * pipelineType='embeddingTokens' and do NOT decrement SubscriptionLedger
10
+ * window quota when adapter billingModel='pay-per-token' (OQ-7 re-walkthrough).
6
11
  */
7
12
  import type { StateStore } from './state/store.js';
8
13
  import type { CostLedgerEntry } from './state/types.js';
14
+ import type { EmbeddingCostRecord } from './embedding/types.js';
9
15
  export interface CostSummary {
10
16
  totalCostUsd: number;
11
17
  totalTokens: number;
@@ -71,5 +77,21 @@ export declare class CostTracker {
71
77
  * Get cost time series at a given granularity.
72
78
  */
73
79
  getCostTimeSeries(granularity?: 'day' | 'week' | 'month', since?: string): CostTimeSeriesPoint[];
80
+ /**
81
+ * Record an embedding cost event per RFC-0019 §10 / AISDLC-337.
82
+ *
83
+ * Records a 'embeddingTokens' line item in the cost ledger with full
84
+ * (provider, modelVersion, accountId, consumerLabel) attribution dimensions
85
+ * per OQ-6 re-walkthrough.
86
+ *
87
+ * When adapter.billingModel === 'pay-per-token', the cost is recorded
88
+ * but does NOT consume SubscriptionLedger window quota (OQ-7 re-walkthrough).
89
+ * When billingModel === 'subscription-quota', callers must separately update
90
+ * the SubscriptionLedger via the inputTokens/outputTokens mechanism.
91
+ *
92
+ * @param record - Embedding cost attribution data from the adapter.
93
+ * @param runId - Pipeline run ID for traceability (optional; defaults to 'embedding').
94
+ */
95
+ recordEmbeddingCost(record: EmbeddingCostRecord, runId?: string): number;
74
96
  }
75
97
  //# sourceMappingURL=cost-tracker.d.ts.map
@@ -3,6 +3,11 @@
3
3
  * and tracks budget status.
4
4
  *
5
5
  * RFC reference: Lines 618-720 (cost tracking).
6
+ *
7
+ * RFC-0019 §10 / AISDLC-337: embeddingTokens line item added via
8
+ * recordEmbeddingCost(). Embedding costs are recorded with
9
+ * pipelineType='embeddingTokens' and do NOT decrement SubscriptionLedger
10
+ * window quota when adapter billingModel='pay-per-token' (OQ-7 re-walkthrough).
6
11
  */
7
12
  import { DEFAULT_MODEL_COSTS, DEFAULT_COST_BUDGET_USD } from './defaults.js';
8
13
  export class CostTracker {
@@ -173,5 +178,41 @@ export class CostTracker {
173
178
  }
174
179
  return [...buckets.values()].sort((a, b) => a.date.localeCompare(b.date));
175
180
  }
181
+ /**
182
+ * Record an embedding cost event per RFC-0019 §10 / AISDLC-337.
183
+ *
184
+ * Records a 'embeddingTokens' line item in the cost ledger with full
185
+ * (provider, modelVersion, accountId, consumerLabel) attribution dimensions
186
+ * per OQ-6 re-walkthrough.
187
+ *
188
+ * When adapter.billingModel === 'pay-per-token', the cost is recorded
189
+ * but does NOT consume SubscriptionLedger window quota (OQ-7 re-walkthrough).
190
+ * When billingModel === 'subscription-quota', callers must separately update
191
+ * the SubscriptionLedger via the inputTokens/outputTokens mechanism.
192
+ *
193
+ * @param record - Embedding cost attribution data from the adapter.
194
+ * @param runId - Pipeline run ID for traceability (optional; defaults to 'embedding').
195
+ */
196
+ recordEmbeddingCost(record, runId = 'embedding') {
197
+ // Encoding convention for embeddingTokens line items:
198
+ // pipelineType = 'embeddingTokens' ← discriminates from LLM entries
199
+ // agentName = consumerLabel ← per-consumer attribution (OQ-6)
200
+ // model = provider@modelVersion ← identifies exact model snapshot
201
+ // inputTokens = tokens ← total embedding tokens consumed
202
+ // costUsd = pre-computed by adapter ← $0.02/1M for OpenAI small
203
+ // stageName = accountId (or 'self-hosted') ← per-credential attribution
204
+ const entry = {
205
+ runId,
206
+ agentName: record.consumerLabel,
207
+ pipelineType: 'embeddingTokens',
208
+ model: `${record.provider}@${record.modelVersion}`,
209
+ inputTokens: record.tokens,
210
+ outputTokens: 0,
211
+ totalTokens: record.tokens,
212
+ costUsd: record.costUsd,
213
+ stageName: record.accountId ?? 'self-hosted',
214
+ };
215
+ return this.store.saveCostEntry(entry);
216
+ }
176
217
  }
177
218
  //# sourceMappingURL=cost-tracker.js.map
@@ -43,7 +43,11 @@ export class ExternalAdapter {
43
43
  const exec = this.deps.exec ?? ((c, a) => execFileAsync(c, a));
44
44
  let stdout;
45
45
  try {
46
- const result = await exec('sh', ['-c', `${cmd} ${branchKey}`]);
46
+ // Pass branchKey as positional $1 (not string-interpolated) so the shell
47
+ // quotes it safely — `cmd` is the operator's trusted hook, but branchKey
48
+ // is pipeline-derived and must not be able to inject (CodeQL
49
+ // js/shell-command-constructed-from-input).
50
+ const result = await exec('sh', ['-c', `${cmd} "$1"`, 'sh', branchKey]);
47
51
  stdout = result.stdout;
48
52
  }
49
53
  catch (err) {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Default embedding adapter: openai-text-embedding-3-small per RFC-0019 §7.
3
+ *
4
+ * Model: text-embedding-3-small (OpenAI, snapshot 2024-01-25)
5
+ * Dimensions: 1536
6
+ * Max input: 8191 tokens
7
+ * Batch: up to 2048 inputs per call
8
+ * Billing: pay-per-token at ~$0.02 / 1M tokens
9
+ *
10
+ * Why text-embedding-3-small over -large:
11
+ * At $0.02/1M tokens, a 10K-token corpus re-embed costs ~$0.0002. The -large
12
+ * variant is 6.5x more expensive for marginal quality improvement on the
13
+ * short-text drift detection use case (RFC-0009 OQ-6). Adopters with
14
+ * quality-sensitive use cases MAY register the -large variant.
15
+ *
16
+ * Why snapshot 2024-01-25:
17
+ * Most recent stable snapshot as of RFC-0019 authoring. Pinning the snapshot
18
+ * date makes adapter upgrades a code change (visible in PR review) rather
19
+ * than a silent provider-side rollout. OpenAI has silently changed
20
+ * text-embedding-ada-002 behavior in the past — explicit pinning prevents that.
21
+ */
22
+ import type { EmbeddingAdapter, EmbeddingAvailability, EmbeddingCapabilities, EmbeddingRequires } from '../types.js';
23
+ import type { EmbeddingCostRecord } from '../types.js';
24
+ /**
25
+ * Callback invoked by the adapter after each embed() / embedBatch() call
26
+ * with cost attribution data. The orchestrator wires this to CostTracker.
27
+ * Decoupled from CostTracker directly so the adapter can be unit-tested
28
+ * without a StateStore dependency.
29
+ */
30
+ export type EmbeddingCostCallback = (record: EmbeddingCostRecord) => void;
31
+ export declare class OpenAITextEmbedding3Small implements EmbeddingAdapter {
32
+ readonly name = "openai-text-embedding-3-small";
33
+ readonly modelId = "text-embedding-3-small";
34
+ readonly modelVersion = "2024-01-25";
35
+ readonly dimensions = 1536;
36
+ readonly capabilities: EmbeddingCapabilities;
37
+ readonly requires: EmbeddingRequires;
38
+ /** Optional cost-tracking callback. Set by the orchestrator after adapter instantiation. */
39
+ private costCallback?;
40
+ constructor(costCallback?: EmbeddingCostCallback);
41
+ /**
42
+ * Wire a cost-tracking callback after construction.
43
+ * Called by the orchestrator when it has a CostTracker available.
44
+ */
45
+ setCostCallback(callback: EmbeddingCostCallback): void;
46
+ isAvailable(): Promise<EmbeddingAvailability>;
47
+ getAccountId(): Promise<string | null>;
48
+ /**
49
+ * Embed a single text string.
50
+ *
51
+ * @param text - Source text. MUST be non-empty.
52
+ * @param consumerLabel - Cost attribution label (default: 'unspecified').
53
+ * Examples: 'rfc-0009-tessellation-drift', 'rfc-0008-ppa-similarity'.
54
+ */
55
+ embed(text: string, consumerLabel?: string): Promise<number[]>;
56
+ /**
57
+ * Embed a batch of texts.
58
+ * OpenAI accepts up to 2048 inputs per call; this method chunks above that.
59
+ * Input order is preserved in the returned array.
60
+ *
61
+ * @param texts - Array of source texts. Each MUST be non-empty.
62
+ * @param consumerLabel - Cost attribution label (applies to all texts in batch).
63
+ */
64
+ embedBatch(texts: string[], consumerLabel?: string): Promise<number[][]>;
65
+ /**
66
+ * Record a cost event via the cost callback.
67
+ * No-op when no callback is wired (e.g., in unit tests without a CostTracker).
68
+ */
69
+ private _recordCost;
70
+ }
71
+ //# sourceMappingURL=openai-text-embedding-3-small.d.ts.map
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Default embedding adapter: openai-text-embedding-3-small per RFC-0019 §7.
3
+ *
4
+ * Model: text-embedding-3-small (OpenAI, snapshot 2024-01-25)
5
+ * Dimensions: 1536
6
+ * Max input: 8191 tokens
7
+ * Batch: up to 2048 inputs per call
8
+ * Billing: pay-per-token at ~$0.02 / 1M tokens
9
+ *
10
+ * Why text-embedding-3-small over -large:
11
+ * At $0.02/1M tokens, a 10K-token corpus re-embed costs ~$0.0002. The -large
12
+ * variant is 6.5x more expensive for marginal quality improvement on the
13
+ * short-text drift detection use case (RFC-0009 OQ-6). Adopters with
14
+ * quality-sensitive use cases MAY register the -large variant.
15
+ *
16
+ * Why snapshot 2024-01-25:
17
+ * Most recent stable snapshot as of RFC-0019 authoring. Pinning the snapshot
18
+ * date makes adapter upgrades a code change (visible in PR review) rather
19
+ * than a silent provider-side rollout. OpenAI has silently changed
20
+ * text-embedding-ada-002 behavior in the past — explicit pinning prevents that.
21
+ */
22
+ import { createHash } from 'node:crypto';
23
+ import { EmbeddingProviderError, EmbeddingDimensionMismatch } from '../errors.js';
24
+ /** Cost rate for text-embedding-3-small as of 2024-01-25. */
25
+ const COST_PER_TOKEN_USD = 0.02 / 1_000_000; // $0.02 per 1M tokens
26
+ /** Maximum inputs per batch call (OpenAI API limit). */
27
+ const MAX_BATCH_SIZE = 2048;
28
+ export class OpenAITextEmbedding3Small {
29
+ name = 'openai-text-embedding-3-small';
30
+ modelId = 'text-embedding-3-small';
31
+ modelVersion = '2024-01-25';
32
+ dimensions = 1536;
33
+ capabilities = {
34
+ dimensions: 1536,
35
+ maxInputTokens: 8191,
36
+ supportsBatching: true,
37
+ selfHosted: false,
38
+ billingModel: 'pay-per-token',
39
+ approxCostPer1MTokens: 0.02,
40
+ };
41
+ requires = {
42
+ envVar: 'OPENAI_API_KEY',
43
+ };
44
+ /** Optional cost-tracking callback. Set by the orchestrator after adapter instantiation. */
45
+ costCallback;
46
+ constructor(costCallback) {
47
+ this.costCallback = costCallback;
48
+ }
49
+ /**
50
+ * Wire a cost-tracking callback after construction.
51
+ * Called by the orchestrator when it has a CostTracker available.
52
+ */
53
+ setCostCallback(callback) {
54
+ this.costCallback = callback;
55
+ }
56
+ async isAvailable() {
57
+ if (!process.env.OPENAI_API_KEY) {
58
+ return {
59
+ available: false,
60
+ reason: 'env-var-missing',
61
+ detail: 'OPENAI_API_KEY not set; openai-text-embedding-3-small requires it.',
62
+ };
63
+ }
64
+ return { available: true };
65
+ }
66
+ async getAccountId() {
67
+ const key = process.env.OPENAI_API_KEY;
68
+ if (!key)
69
+ return null;
70
+ // One-way derivation: SHA-256 of '<adapter-name>:<api-key>'.
71
+ // MUST NOT leak the credential.
72
+ return createHash('sha256').update(`${this.name}:${key}`).digest('hex');
73
+ }
74
+ /**
75
+ * Embed a single text string.
76
+ *
77
+ * @param text - Source text. MUST be non-empty.
78
+ * @param consumerLabel - Cost attribution label (default: 'unspecified').
79
+ * Examples: 'rfc-0009-tessellation-drift', 'rfc-0008-ppa-similarity'.
80
+ */
81
+ async embed(text, consumerLabel = 'unspecified') {
82
+ if (!text || text.trim().length === 0) {
83
+ throw new EmbeddingProviderError(this.name, 'embed(): empty input rejected — pass non-empty text.');
84
+ }
85
+ const apiKey = process.env.OPENAI_API_KEY;
86
+ if (!apiKey) {
87
+ throw new EmbeddingProviderError(this.name, 'OPENAI_API_KEY is not set. Call isAvailable() before embed().');
88
+ }
89
+ const response = await fetch('https://api.openai.com/v1/embeddings', {
90
+ method: 'POST',
91
+ headers: {
92
+ Authorization: `Bearer ${apiKey}`,
93
+ 'Content-Type': 'application/json',
94
+ },
95
+ body: JSON.stringify({
96
+ model: this.modelId,
97
+ input: text,
98
+ encoding_format: 'float',
99
+ }),
100
+ });
101
+ if (!response.ok) {
102
+ const body = await response.text().catch(() => '(unreadable)');
103
+ throw new EmbeddingProviderError(this.name, `OpenAI /v1/embeddings returned HTTP ${response.status}: ${body}`);
104
+ }
105
+ const data = (await response.json());
106
+ const vector = data.data[0]?.embedding;
107
+ if (!vector) {
108
+ throw new EmbeddingProviderError(this.name, 'OpenAI /v1/embeddings response contained no embedding in data[0].');
109
+ }
110
+ if (vector.length !== this.dimensions) {
111
+ throw new EmbeddingDimensionMismatch(this.name, this.dimensions, vector.length);
112
+ }
113
+ await this._recordCost(data.usage.total_tokens, consumerLabel);
114
+ return vector;
115
+ }
116
+ /**
117
+ * Embed a batch of texts.
118
+ * OpenAI accepts up to 2048 inputs per call; this method chunks above that.
119
+ * Input order is preserved in the returned array.
120
+ *
121
+ * @param texts - Array of source texts. Each MUST be non-empty.
122
+ * @param consumerLabel - Cost attribution label (applies to all texts in batch).
123
+ */
124
+ async embedBatch(texts, consumerLabel = 'unspecified') {
125
+ if (texts.length === 0) {
126
+ return [];
127
+ }
128
+ for (const t of texts) {
129
+ if (!t || t.trim().length === 0) {
130
+ throw new EmbeddingProviderError(this.name, 'embedBatch(): empty string in input array rejected — all texts must be non-empty.');
131
+ }
132
+ }
133
+ const apiKey = process.env.OPENAI_API_KEY;
134
+ if (!apiKey) {
135
+ throw new EmbeddingProviderError(this.name, 'OPENAI_API_KEY is not set. Call isAvailable() before embedBatch().');
136
+ }
137
+ const results = [];
138
+ // Chunk into MAX_BATCH_SIZE slices to respect the OpenAI API limit.
139
+ for (let i = 0; i < texts.length; i += MAX_BATCH_SIZE) {
140
+ const chunk = texts.slice(i, i + MAX_BATCH_SIZE);
141
+ const response = await fetch('https://api.openai.com/v1/embeddings', {
142
+ method: 'POST',
143
+ headers: {
144
+ Authorization: `Bearer ${apiKey}`,
145
+ 'Content-Type': 'application/json',
146
+ },
147
+ body: JSON.stringify({
148
+ model: this.modelId,
149
+ input: chunk,
150
+ encoding_format: 'float',
151
+ }),
152
+ });
153
+ if (!response.ok) {
154
+ const body = await response.text().catch(() => '(unreadable)');
155
+ throw new EmbeddingProviderError(this.name, `OpenAI /v1/embeddings returned HTTP ${response.status} on batch chunk [${i}, ${i + chunk.length}): ${body}`);
156
+ }
157
+ const data = (await response.json());
158
+ // OpenAI returns data sorted by index — preserve input order.
159
+ const sorted = [...data.data].sort((a, b) => a.index - b.index);
160
+ for (const item of sorted) {
161
+ if (item.embedding.length !== this.dimensions) {
162
+ throw new EmbeddingDimensionMismatch(this.name, this.dimensions, item.embedding.length);
163
+ }
164
+ results.push(item.embedding);
165
+ }
166
+ await this._recordCost(data.usage.total_tokens, consumerLabel);
167
+ }
168
+ return results;
169
+ }
170
+ /**
171
+ * Record a cost event via the cost callback.
172
+ * No-op when no callback is wired (e.g., in unit tests without a CostTracker).
173
+ */
174
+ async _recordCost(tokens, consumerLabel) {
175
+ if (!this.costCallback)
176
+ return;
177
+ const accountId = await this.getAccountId();
178
+ const costRecord = {
179
+ provider: this.name,
180
+ modelVersion: this.modelVersion,
181
+ accountId,
182
+ consumerLabel,
183
+ tokens,
184
+ costUsd: tokens * COST_PER_TOKEN_USD,
185
+ billingModel: 'pay-per-token',
186
+ };
187
+ this.costCallback(costRecord);
188
+ }
189
+ }
190
+ //# sourceMappingURL=openai-text-embedding-3-small.js.map