@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,308 @@
1
+ /**
2
+ * RFC-0018 Phase 2 — In-Soul Journey Pattern admission scorer composition.
3
+ *
4
+ * Implements the journey-scope routing algorithm described in RFC-0018 §5.4
5
+ * and §10.1 (OQ-4 resolution):
6
+ *
7
+ * resolveTargetedJourneys(w) = set of (soulId, [variantId,] journeyId) triples
8
+ * declared on the work item via `targetedJourneys[]` (URI shapes
9
+ * `<soul-id>/<journey-id>` or `<soul-id>/<variant-id>/<journey-id>`).
10
+ *
11
+ * If no `targetedJourneys` declared:
12
+ * Scoring proceeds at soul/variant scope (backward-compatible — unchanged
13
+ * from RFC-0009 + RFC-0017 baseline).
14
+ *
15
+ * Else if |targeted| == 1 (single-journey):
16
+ * Sα₂(w) = scoreSα₂(journey.designImperatives UNION variant's UNION soul's;
17
+ * most-specific wins: journey > variant > soul)
18
+ * Cκ(w) = boosted when journey.successMetrics.completion-rate < alertBelow
19
+ * Eρ₅(w) = elevated when journey.accessibility.wcagLevel > soul/variant default
20
+ *
21
+ * Else (multi-journey):
22
+ * Per-journey scores aggregated via `crossJourneyAggregation` config
23
+ * (default `min` per RFC-0018 §5.4 + RFC-0017 OQ-4 cross-variant pattern).
24
+ *
25
+ * ### Completion-criteria closed enum (OQ-4 resolution)
26
+ *
27
+ * v1 allows only: `terminal-success-state` | `all-states-reached`
28
+ * `custom-predicate` is rejected at schema validation time.
29
+ * Future activation via `Decision: journey-custom-predicate-activation-request`
30
+ * (Stage A counter, auto-promote at ≥2 distinct adopter requests).
31
+ * Future language: CEL (Google Common Expression Language) per OQ-4 resolution.
32
+ *
33
+ * ### Backward compatibility
34
+ *
35
+ * Work items without `targetedJourneys` score against soul / variant (existing
36
+ * RFC-0017 behavior preserved). Soul DIDs without `journeys[]` behave identically.
37
+ *
38
+ * @see spec/rfcs/RFC-0018-in-soul-journey-pattern.md §5.4 + §10 + §10.1
39
+ * @see orchestrator/src/variant-admission.ts — sibling RFC-0017 Phase 2 router
40
+ * @see orchestrator/src/compliance-clearance.ts — Eρ₅ base scorer
41
+ */
42
+ /**
43
+ * The set of valid v1 completion-criteria kind values.
44
+ * Used by `validateCompletionCriteriaKind` for closed-enum rejection.
45
+ */
46
+ export const COMPLETION_CRITERIA_V1_KINDS = new Set([
47
+ 'terminal-success-state',
48
+ 'all-states-reached',
49
+ ]);
50
+ /**
51
+ * Decision kind for the `custom-predicate` activation request counter.
52
+ * Stage A counter per RFC-0035 G0 non-blocking contract.
53
+ * Auto-promotes at ≥2 distinct adopter requests (RFC-0018 OQ-4 resolution).
54
+ */
55
+ export const JOURNEY_CUSTOM_PREDICATE_DECISION_KIND = 'journey-custom-predicate-activation-request';
56
+ /**
57
+ * Validate a completion-criteria `kind` value against the v1 closed enum.
58
+ *
59
+ * Returns `{ valid: true }` for `terminal-success-state` and `all-states-reached`.
60
+ * Returns `{ valid: false, rejectedKind, decisionKind }` for `custom-predicate`
61
+ * (or any other unrecognized value). The caller is responsible for emitting the
62
+ * Decision event to the RFC-0035 catalog.
63
+ */
64
+ export function validateCompletionCriteriaKind(kind) {
65
+ if (COMPLETION_CRITERIA_V1_KINDS.has(kind)) {
66
+ return { valid: true };
67
+ }
68
+ const result = {
69
+ valid: false,
70
+ rejectedKind: kind,
71
+ };
72
+ // Wire the Decision counter for custom-predicate specifically (OQ-4 auto-promote mechanism)
73
+ if (kind === 'custom-predicate') {
74
+ result.decisionKind = JOURNEY_CUSTOM_PREDICATE_DECISION_KIND;
75
+ }
76
+ return result;
77
+ }
78
+ /**
79
+ * Increment the Stage A counter for a `journey-custom-predicate-activation-request`.
80
+ *
81
+ * @param counter - The counter to increment (mutated in-place).
82
+ * @param adopterId - Adopter ID submitting the request (for deduplication).
83
+ * Use `'__unknown__'` when adopter identity is unavailable.
84
+ */
85
+ export function incrementDecisionCounter(counter, adopterId) {
86
+ counter.totalRequests++;
87
+ counter.distinctAdopterIds.add(adopterId);
88
+ counter.shouldAutoPromote = counter.distinctAdopterIds.size >= 2;
89
+ }
90
+ /**
91
+ * Create a fresh Stage A counter for `journey-custom-predicate-activation-request`.
92
+ */
93
+ export function createCustomPredicateDecisionCounter() {
94
+ return {
95
+ decisionKind: JOURNEY_CUSTOM_PREDICATE_DECISION_KIND,
96
+ distinctAdopterIds: new Set(),
97
+ totalRequests: 0,
98
+ shouldAutoPromote: false,
99
+ };
100
+ }
101
+ /**
102
+ * Ordinal index for WCAG levels — higher = stricter.
103
+ * `A` < `AA` < `AAA`.
104
+ */
105
+ const WCAG_LEVEL_ORDER = {
106
+ A: 0,
107
+ AA: 1,
108
+ AAA: 2,
109
+ };
110
+ /**
111
+ * Returns true when `journeyLevel` is strictly ABOVE `soulDefault`.
112
+ * Journeys MAY raise the WCAG level above the parent; they MAY NOT lower it.
113
+ */
114
+ export function isWcagElevated(journeyLevel, soulDefault) {
115
+ return WCAG_LEVEL_ORDER[journeyLevel] > WCAG_LEVEL_ORDER[soulDefault];
116
+ }
117
+ // ── URI parsing ──────────────────────────────────────────────────────────
118
+ const SLUG_RE = /^[a-z][a-z0-9-]*$/;
119
+ /**
120
+ * Parse a targeted-journey reference.
121
+ *
122
+ * Accepts two URI forms per RFC-0018 §6.1:
123
+ *
124
+ * 1. `<soul-id>/<journey-id>` — soul-scoped journey
125
+ * 2. `<soul-id>/<variant-id>/<journey-id>` — variant-scoped journey
126
+ *
127
+ * All slug segments must match `^[a-z][a-z0-9-]*$`.
128
+ * Returns `undefined` for malformed input (silent skip; schema-side validator
129
+ * is Phase 1's concern per RFC-0018 §6.1 pattern).
130
+ */
131
+ export function parseTargetedJourneyRef(raw) {
132
+ const parts = raw.split('/');
133
+ if (parts.length === 2) {
134
+ const [soulId, journeyId] = parts;
135
+ if (SLUG_RE.test(soulId) && SLUG_RE.test(journeyId)) {
136
+ return { soulId, journeyId, raw };
137
+ }
138
+ }
139
+ else if (parts.length === 3) {
140
+ const [soulId, variantId, journeyId] = parts;
141
+ if (SLUG_RE.test(soulId) && SLUG_RE.test(variantId) && SLUG_RE.test(journeyId)) {
142
+ return { soulId, variantId, journeyId, raw };
143
+ }
144
+ }
145
+ return undefined;
146
+ }
147
+ // ── resolveTargetedJourneys ──────────────────────────────────────────────
148
+ /**
149
+ * Resolve the set of targeted (soulId, [variantId,] journeyId) triples for a work item.
150
+ *
151
+ * Algorithm:
152
+ * 1. Find the work item by case-insensitive ID match in `workItemTargeting`.
153
+ * 2. Parse each entry of `targetedJourneys[]` via `parseTargetedJourneyRef`.
154
+ * 3. Filter parsed refs against `journeysBySoul` — a (soulId, journeyId) pair
155
+ * only survives if the soul exists AND the journeyId is declared on it.
156
+ * 4. Return the validated intersection (empty = backward-compat soul/variant scope).
157
+ */
158
+ export function resolveTargetedJourneys(workItemId, journeyCtx) {
159
+ if (!journeyCtx || !journeyCtx.workItemTargeting || journeyCtx.workItemTargeting.length === 0) {
160
+ return [];
161
+ }
162
+ const normalizedId = workItemId.toLowerCase();
163
+ const entry = journeyCtx.workItemTargeting.find((e) => e.id.toLowerCase() === normalizedId);
164
+ if (!entry || !entry.targetedJourneys || entry.targetedJourneys.length === 0) {
165
+ return [];
166
+ }
167
+ const out = [];
168
+ for (const raw of entry.targetedJourneys) {
169
+ const parsed = parseTargetedJourneyRef(raw);
170
+ if (!parsed)
171
+ continue;
172
+ const journeys = journeyCtx.journeysBySoul[parsed.soulId];
173
+ if (!journeys)
174
+ continue;
175
+ if (!journeys.some((j) => j.id === parsed.journeyId))
176
+ continue;
177
+ out.push(parsed);
178
+ }
179
+ return out;
180
+ }
181
+ // ── Cross-journey aggregation ────────────────────────────────────────────
182
+ /**
183
+ * Apply a per-Soul `crossJourneyAggregation` rule over per-journey scores.
184
+ * Mirrors `applyCrossVariantRule` in `variant-admission.ts` but for journey scope.
185
+ *
186
+ * @param values - Per-journey score samples (one per targeted journey).
187
+ * @param rule - The aggregation rule (defaults to `min` per RFC-0018 §5.4 + OQ-4).
188
+ * @param fallback - Returned when `values` is empty.
189
+ */
190
+ export function applyCrossJourneyRule(values, rule, fallback = 0.5) {
191
+ if (values.length === 0)
192
+ return fallback;
193
+ switch (rule ?? 'min') {
194
+ case 'min':
195
+ return Math.min(...values);
196
+ case 'max':
197
+ return Math.max(...values);
198
+ case 'mean':
199
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
200
+ }
201
+ }
202
+ /**
203
+ * Compute the journey-scope-refined Sα₂ + Cκ + Eρ₅ for a work item.
204
+ *
205
+ * This runs AFTER variant-scope resolution: the caller has already routed the
206
+ * work item to its target Soul(s)/Variant(s) and obtained the soul/variant
207
+ * Sα₂ (the `fallbackSa2` argument) and Cκ (the `fallbackCk` argument).
208
+ * Journey routing refines those values when the work item declares
209
+ * `targetedJourneys` of one of the affected Souls (RFC-0018 §5.4).
210
+ *
211
+ * **Sα₂ Vibe Coherence** — journey's `designImperatives` UNION variant's UNION
212
+ * soul's; conflict resolution: most-specific wins (journey > variant > soul).
213
+ * The pre-computed `JourneyScores.sa2` in the context encodes this resolution.
214
+ *
215
+ * **Cκ Capability Coverage** — boosted when `completion-rate < alertBelow`.
216
+ * The pre-computed `JourneyScores.ck` encodes the boost; callers supply live
217
+ * metric snapshots to the loader that builds `journeyScores`.
218
+ *
219
+ * **Eρ₅ Compliance Clearance** — elevated when journey WCAG > soul-default.
220
+ * The pre-computed `JourneyScores.er5Elevated` flag captures this; the caller
221
+ * applies Eρ₅ gating at the journey's elevated level when `er5Elevated = true`.
222
+ *
223
+ * **Cross-journey aggregation** (work touches multiple journeys) — same `min`
224
+ * as RFC-0018 §5.4 / RFC-0009 §7.2 / RFC-0017 OQ-4 by default. Per-Soul
225
+ * override via `journeyConfig.crossJourneyAggregation`.
226
+ *
227
+ * @param workItemId - The canonical work item ID.
228
+ * @param fallbackSa2 - Soul/variant Sα₂ to use when no journey routing applies.
229
+ * @param fallbackCk - Soul/variant Cκ to use when no journey routing applies.
230
+ * @param fallbackEr5Elevated - Soul/variant Eρ₅ elevation flag (usually false).
231
+ * @param journeyCtx - Journey-scope context; undefined → backward-compat passthrough.
232
+ */
233
+ export function computeJourneyScopedScores(workItemId, fallbackSa2, fallbackCk, fallbackEr5Elevated, journeyCtx) {
234
+ const targetedJourneys = resolveTargetedJourneys(workItemId, journeyCtx);
235
+ // ── Backward-compat: no targeted journeys → soul/variant scope passthrough ──
236
+ if (targetedJourneys.length === 0 || !journeyCtx) {
237
+ return {
238
+ sa2: fallbackSa2,
239
+ ck: fallbackCk,
240
+ er5Elevated: fallbackEr5Elevated,
241
+ routingPath: 'no-journey-routing',
242
+ targetedJourneys: [],
243
+ };
244
+ }
245
+ // ── Single-journey fast path ──
246
+ if (targetedJourneys.length === 1) {
247
+ const ref = targetedJourneys[0];
248
+ const scores = journeyCtx.journeyScores[ref.soulId]?.[ref.journeyId];
249
+ return {
250
+ sa2: scores?.sa2 ?? fallbackSa2,
251
+ ck: scores?.ck ?? fallbackCk,
252
+ er5Elevated: scores?.er5Elevated ?? fallbackEr5Elevated,
253
+ routingPath: 'single-journey',
254
+ targetedJourneys,
255
+ };
256
+ }
257
+ // ── Multi-journey: aggregate per-Soul, then aggregate cross-Soul ──
258
+ // Per-Soul aggregation uses the Soul's `crossJourneyAggregation` config
259
+ // (default `min`). When journeys span multiple souls (rare), the cross-Soul
260
+ // layer also uses `min` as the safest default (RFC-0009 §7.2).
261
+ const bySoul = new Map();
262
+ for (const ref of targetedJourneys) {
263
+ const bucket = bySoul.get(ref.soulId);
264
+ if (bucket)
265
+ bucket.push(ref);
266
+ else
267
+ bySoul.set(ref.soulId, [ref]);
268
+ }
269
+ const perSoulSa2 = [];
270
+ const perSoulCk = [];
271
+ const perSoulEr5Elevated = [];
272
+ let firstAggregationRule;
273
+ for (const [soulId, refs] of bySoul) {
274
+ const cfg = journeyCtx.configBySoul?.[soulId];
275
+ const rule = cfg?.crossJourneyAggregation ?? 'min';
276
+ if (firstAggregationRule === undefined)
277
+ firstAggregationRule = rule;
278
+ const sa2Samples = [];
279
+ const ckSamples = [];
280
+ const er5Samples = [];
281
+ for (const ref of refs) {
282
+ const scores = journeyCtx.journeyScores[soulId]?.[ref.journeyId];
283
+ sa2Samples.push(scores?.sa2 ?? fallbackSa2);
284
+ ckSamples.push(scores?.ck ?? fallbackCk);
285
+ er5Samples.push(scores?.er5Elevated ?? fallbackEr5Elevated);
286
+ }
287
+ perSoulSa2.push(applyCrossJourneyRule(sa2Samples, rule, fallbackSa2));
288
+ perSoulCk.push(applyCrossJourneyRule(ckSamples, rule, fallbackCk));
289
+ // Eρ₅ elevation: `true` wins if ANY journey requires elevation
290
+ // (conservative — if at least one journey has elevated WCAG, the
291
+ // work item is evaluated at that stricter level).
292
+ perSoulEr5Elevated.push(er5Samples.some(Boolean));
293
+ }
294
+ // Cross-Soul layering: `min` between Souls for numeric scores (RFC-0009 §7.2).
295
+ // Eρ₅ elevation: `true` if any Soul has elevated WCAG (conservative/safe).
296
+ const sa2 = perSoulSa2.length === 1 ? perSoulSa2[0] : Math.min(...perSoulSa2);
297
+ const ck = perSoulCk.length === 1 ? perSoulCk[0] : Math.min(...perSoulCk);
298
+ const er5Elevated = perSoulEr5Elevated.some(Boolean);
299
+ return {
300
+ sa2,
301
+ ck,
302
+ er5Elevated,
303
+ routingPath: 'multi-journey',
304
+ targetedJourneys,
305
+ aggregationRule: firstAggregationRule ?? 'min',
306
+ };
307
+ }
308
+ //# sourceMappingURL=journey-sa2-router.js.map
@@ -51,7 +51,9 @@ export interface DiffSummary {
51
51
  * Apply the default classifier ruleset from RFC §12.3 to a diff summary. Used as the
52
52
  * fallback when no LLM classifier is configured, and as the seed prompt for LLM-based
53
53
  * classifiers. Always returns confident: true with confidence: 1.0 because these are
54
- * deterministic rules.
54
+ * deterministic rules. AISDLC-145 added the docs denylist + widened
55
+ * auth/lockfile/CI predicates to close downgrade vectors flagged by the
56
+ * AISDLC-141 security reviewer.
55
57
  */
56
58
  export declare function defaultRulesetDecision(diff: DiffSummary): ClassifierOutput;
57
59
  export interface CalibrationLogEntry {
@@ -146,11 +146,52 @@ export function validateClassifierOutput(value) {
146
146
  }
147
147
  return { ok: true, value: obj };
148
148
  }
149
+ // ── AISDLC-145 path-classification helpers ─────────────────────────────────────────────
150
+ //
151
+ // These predicates are duplicated verbatim in the pipeline-cli copy
152
+ // (`pipeline-cli/src/classifier/classifier.ts`). Keep them in sync — drift
153
+ // here silently shifts which reviewers fire between callers. A future
154
+ // consolidation task should extract a single `@ai-sdlc/classifier-ruleset`
155
+ // package both sides import; tracked as a follow-up to AISDLC-145.
156
+ /** Renderable-docs / image extensions allowed in the docs-only branch. */
157
+ const DOCS_EXTENSIONS_RE = /\.(md|rst|txt|png|jpe?g|svg|gif|ico|pdf)$/i;
158
+ /**
159
+ * Filenames that look secret-y or executable-y and must NEVER be classified
160
+ * as docs even if they sit under `docs/`. Hits include `.env`, `.env.local`,
161
+ * `private-key.pem`, `signing.key`, `install.sh`, `Dockerfile`, `Dockerfile.prod`,
162
+ * `package-lock.json`, etc. Anchored on the basename so path-prefix doesn't
163
+ * matter.
164
+ */
165
+ const DOCS_DENYLIST_RE = /(?:^|\/)(\.env(?:\..+)?|.+\.pem|.+\.key|.+\.sh|Dockerfile.*|.+\.lock)$/i;
166
+ /** True iff the path is safe to treat as documentation-only (no security review). */
167
+ function isDocsLikePath(p) {
168
+ if (DOCS_DENYLIST_RE.test(p))
169
+ return false;
170
+ return DOCS_EXTENSIONS_RE.test(p);
171
+ }
172
+ /** Auth-tier secret files (env vars, private keys) — treated as auth-touching. */
173
+ function isSecretFilePath(p) {
174
+ return /(?:^|\/)(\.env(?:\..+)?|.+\.pem|.+\.key)$/i.test(p);
175
+ }
176
+ /** Supply-chain lockfile detection (widened in AISDLC-145). */
177
+ function isLockfilePath(p) {
178
+ return /(?:^|\/)(package(-lock)?\.json|requirements\.txt|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|Gemfile\.lock|composer\.lock|go\.sum|bun\.lockb)$/i.test(p);
179
+ }
180
+ /** CI-config detection (widened beyond GitHub Actions in AISDLC-145). */
181
+ function isCiPath(p) {
182
+ if (p.startsWith('.github/workflows/'))
183
+ return true;
184
+ if (p.startsWith('.circleci/'))
185
+ return true;
186
+ return /(?:^|\/)(\.gitlab-ci\.yml|Jenkinsfile|azure-pipelines\.yml)$/i.test(p);
187
+ }
149
188
  /**
150
189
  * Apply the default classifier ruleset from RFC §12.3 to a diff summary. Used as the
151
190
  * fallback when no LLM classifier is configured, and as the seed prompt for LLM-based
152
191
  * classifiers. Always returns confident: true with confidence: 1.0 because these are
153
- * deterministic rules.
192
+ * deterministic rules. AISDLC-145 added the docs denylist + widened
193
+ * auth/lockfile/CI predicates to close downgrade vectors flagged by the
194
+ * AISDLC-141 security reviewer.
154
195
  */
155
196
  export function defaultRulesetDecision(diff) {
156
197
  if (diff.filesChanged === 0) {
@@ -161,7 +202,15 @@ export function defaultRulesetDecision(diff) {
161
202
  confidence: 1,
162
203
  };
163
204
  }
164
- const allDocs = diff.paths.every((p) => /\.(md|rst|txt)$/i.test(p) || p.startsWith('docs/'));
205
+ // AISDLC-145 hardening: the docs branch is a security DOWNGRADE — it skips
206
+ // both `testing` and `security` reviewers. So the predicate must be
207
+ // conservative: a docs-like file is one whose extension is in the safe set
208
+ // (renderable docs / images) AND is NOT on the unconditional denylist of
209
+ // executable-or-secret-looking filenames. Pre-145 the rule was just
210
+ // `p.startsWith('docs/')`, which let `docs/install.sh`, `docs/.env`,
211
+ // `docs/private-key.pem`, `docs/Dockerfile`, etc. silently bypass the
212
+ // security reviewer. See the AISDLC-141 reviewer findings.
213
+ const allDocs = diff.paths.every((p) => isDocsLikePath(p));
165
214
  if (allDocs) {
166
215
  return {
167
216
  reviewers: ['critic'],
@@ -170,9 +219,16 @@ export function defaultRulesetDecision(diff) {
170
219
  confidence: 0.95,
171
220
  };
172
221
  }
173
- const touchesAuth = diff.paths.some((p) => /(?:^|\/)(auth|crypto|secrets?)\b/i.test(p));
174
- const touchesLockfiles = diff.paths.some((p) => /(?:^|\/)(package(-lock)?\.json|requirements\.txt|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock)$/i.test(p));
175
- const touchesCi = diff.paths.some((p) => p.startsWith('.github/workflows/'));
222
+ // AISDLC-145: widen auth/secret detection. The pre-145 regex
223
+ // `(auth|crypto|secrets?)` missed common identity/authn paths
224
+ // (`oauth/`, `iam/`, `jwt/`, `session/`, `login.ts`, `rbac/`, `tokens.ts`,
225
+ // `credentials.ts`, `password.ts`, `signin/`, `signup/`). Those still ran 3
226
+ // reviewers via the default branch but never got the opus model bump.
227
+ // Also: `.env*`, `*.pem`, `*.key` files are treated as auth-tier — they
228
+ // contain or directly grant credentials.
229
+ const touchesAuth = diff.paths.some((p) => /(?:^|\/)(auth|oauth|crypto|secrets?|iam|jwt|session|login|rbac|tokens?|credentials?|password|signin|signup)\b/i.test(p) || isSecretFilePath(p));
230
+ const touchesLockfiles = diff.paths.some((p) => isLockfilePath(p));
231
+ const touchesCi = diff.paths.some((p) => isCiPath(p));
176
232
  if (touchesAuth) {
177
233
  return {
178
234
  reviewers: ['testing', 'critic', 'security'],
@@ -36,9 +36,31 @@ export interface SharedDimensions {
36
36
  * populates this once the SA-1/SA-2/SA-3 decomposition lands.
37
37
  */
38
38
  saAlpha3?: number;
39
- /** Per-channel HC breakdown with the tanh composite for reference. */
39
+ /**
40
+ * Per-channel HC breakdown with the tanh composite for reference.
41
+ *
42
+ * `designAuthorityConfigured` (AISDLC-171) — diagnostic flag set when
43
+ * the resolved DSB declares any `stewardship.designAuthority.principals`
44
+ * entries, regardless of whether one of them participated in the issue.
45
+ * Lets operators distinguish three pillarBreakdown states for the design
46
+ * channel:
47
+ * - `design = 0` and `configured === undefined` → no DSB resolved
48
+ * (preDesignSystem). HC_design intentionally inert.
49
+ * - `design = 0` and `configured === true` → DSB declares design
50
+ * authority but no principal participated as author/commenter.
51
+ * HC_design intentionally 0 per RFC-0008 §14.2 (only principals
52
+ * emit full-weight HC_design signals; non-principal opinions route
53
+ * through HC_consensus, not HC_design).
54
+ * - `design ≠ 0` and `configured === true` → a principal
55
+ * participated; signal weight reflects label-derived signalType.
56
+ *
57
+ * The `false` case (DSB exists but `principals` is empty) is also
58
+ * surfaced for completeness — a DSB without designAuthority principals
59
+ * cannot ever fire HC_design and operators should know.
60
+ */
40
61
  hcComposite: HcChannelBreakdown & {
41
62
  value: number;
63
+ designAuthorityConfigured?: boolean;
42
64
  };
43
65
  }
44
66
  export interface TensionFlag {
@@ -70,6 +70,15 @@ export function computePillarBreakdown(composite) {
70
70
  decision: b.humanCurve.hcDecision,
71
71
  design: b.humanCurve.hcDesign,
72
72
  value: b.humanCurve.hcComposite,
73
+ // AISDLC-171: only include the flag when the underlying signal
74
+ // populated it (i.e., a DSB was resolved). Leaving it `undefined`
75
+ // when no DSB was supplied keeps the preDesignSystem state
76
+ // distinct from the "configured but inactive" state at the API
77
+ // surface — operators inspecting `pillarBreakdown.shared` can
78
+ // tell the three states apart without reading the DSB themselves.
79
+ ...(b.humanCurve.designAuthorityConfigured !== undefined
80
+ ? { designAuthorityConfigured: b.humanCurve.designAuthorityConfigured }
81
+ : {}),
73
82
  },
74
83
  };
75
84
  const tensions = detectTensions({ product, design, engineering, shared, tensions: [] });
@@ -291,8 +291,12 @@ export class ReviewAgentRunner {
291
291
  }
292
292
  }
293
293
  parseVerdict(text) {
294
- // Strip markdown fences if the model wraps the JSON
295
- const cleaned = text.replace(/^```(?:json)?\s*/m, '').replace(/\s*```$/m, '');
294
+ // Strip markdown fences if the model wraps the JSON. Horizontal-whitespace
295
+ // classes ([ \t\r]) instead of \s so the trailing-whitespace group can't
296
+ // overlap the \n the anchors already consume — avoids polynomial
297
+ // backtracking on adversarial model output (CodeQL js/polynomial-redos).
298
+ // JSON.parse tolerates the leading/trailing newline left behind.
299
+ const cleaned = text.replace(/^```(?:json)?[ \t\r]*/m, '').replace(/[ \t\r]*```$/m, '');
296
300
  try {
297
301
  const parsed = JSON.parse(cleaned);
298
302
  const rawFindings = Array.isArray(parsed.findings)
@@ -38,6 +38,17 @@ export declare class RunnerRegistry {
38
38
  * Check if a runner is registered and available.
39
39
  */
40
40
  has(name: string): boolean;
41
+ /**
42
+ * Load and register a runner from an external plugin module.
43
+ *
44
+ * The module must export a default export or a named `runner` export that
45
+ * satisfies the `AgentRunner` interface (i.e. has a `run(ctx)` method).
46
+ *
47
+ * @param pluginPath - Absolute or resolvable path to the plugin module (e.g. `/path/to/runner.mjs`).
48
+ * @param name - Registry name for the loaded runner (defaults to the basename of the path).
49
+ * @throws Error when the module cannot be imported or does not export a valid AgentRunner.
50
+ */
51
+ loadFromPlugin(pluginPath: string, name?: string): Promise<string>;
41
52
  /**
42
53
  * Auto-discover runners from environment variables and register them.
43
54
  */
@@ -47,4 +58,29 @@ export declare class RunnerRegistry {
47
58
  * Create a runner registry with auto-discovery.
48
59
  */
49
60
  export declare function createRunnerRegistry(env?: Record<string, string | undefined>): RunnerRegistry;
61
+ /**
62
+ * Resolve the agent runner to use, applying the full precedence chain:
63
+ *
64
+ * 1. `injectedRunner` — programmatic override (options.runner from caller / tests)
65
+ * 2. `runnerName` — explicit `--runner <name>` flag (must already be registered after discoverFromEnv)
66
+ * 3. `AI_SDLC_RUNNER_PLUGIN` env — path to a dynamic plugin module (loaded + registered)
67
+ * 4. ClaudeCodeRunner (hard-coded default)
68
+ *
69
+ * IMPORTANT — env-discovered runners do NOT auto-win (AISDLC-529 code review). They are
70
+ * registered by `discoverFromEnv()` so they are *selectable by name* via `--runner <name>`,
71
+ * but the mere PRESENCE of an ambient env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GH_TOKEN,
72
+ * etc. — commonly set for unrelated tools) must NOT silently switch the runner. Before this
73
+ * seam existed the orchestrator always used ClaudeCodeRunner; preserving that as the default
74
+ * (absent an explicit selector) avoids a breaking, surprising change for existing adopters.
75
+ *
76
+ * This function is async because step 3 may dynamically import a module.
77
+ *
78
+ * @throws Error when `runnerName` is provided but not registered in the registry.
79
+ * @throws Error when `AI_SDLC_RUNNER_PLUGIN` points to an invalid module.
80
+ */
81
+ export declare function resolveRunner(registry: RunnerRegistry, opts?: {
82
+ injectedRunner?: AgentRunner;
83
+ runnerName?: string;
84
+ env?: Record<string, string | undefined>;
85
+ }): Promise<AgentRunner>;
50
86
  //# sourceMappingURL=runner-registry.d.ts.map
@@ -58,6 +58,45 @@ export class RunnerRegistry {
58
58
  const entry = this.runners.get(name);
59
59
  return entry?.available ?? false;
60
60
  }
61
+ /**
62
+ * Load and register a runner from an external plugin module.
63
+ *
64
+ * The module must export a default export or a named `runner` export that
65
+ * satisfies the `AgentRunner` interface (i.e. has a `run(ctx)` method).
66
+ *
67
+ * @param pluginPath - Absolute or resolvable path to the plugin module (e.g. `/path/to/runner.mjs`).
68
+ * @param name - Registry name for the loaded runner (defaults to the basename of the path).
69
+ * @throws Error when the module cannot be imported or does not export a valid AgentRunner.
70
+ */
71
+ async loadFromPlugin(pluginPath, name) {
72
+ let mod;
73
+ try {
74
+ mod = await import(pluginPath);
75
+ }
76
+ catch (err) {
77
+ throw new Error(`AI_SDLC_RUNNER_PLUGIN: failed to import plugin module "${pluginPath}": ${err instanceof Error ? err.message : String(err)}.\n` +
78
+ `Ensure the path is correct and the module is a valid ESM/CJS module.`);
79
+ }
80
+ // Accept default export or named 'runner' export
81
+ const exported = mod.default ?? mod.runner;
82
+ if (!exported || typeof exported.run !== 'function') {
83
+ throw new Error(`AI_SDLC_RUNNER_PLUGIN: plugin module "${pluginPath}" does not export a valid AgentRunner.\n` +
84
+ `Expected a default export (or named 'runner' export) with a \`run(ctx: AgentContext): Promise<AgentResult>\` method.\n` +
85
+ `Got: ${JSON.stringify(Object.keys(mod))}`);
86
+ }
87
+ const runnerName = name ??
88
+ pluginPath
89
+ .split('/')
90
+ .pop()
91
+ .replace(/\.(m|c)?[jt]s$/, '');
92
+ this.runners.set(runnerName, {
93
+ name: runnerName,
94
+ runner: exported,
95
+ available: true,
96
+ source: 'manual',
97
+ });
98
+ return runnerName;
99
+ }
61
100
  /**
62
101
  * Auto-discover runners from environment variables and register them.
63
102
  */
@@ -163,4 +202,55 @@ export function createRunnerRegistry(env) {
163
202
  registry.discoverFromEnv(env);
164
203
  return registry;
165
204
  }
205
+ /**
206
+ * Resolve the agent runner to use, applying the full precedence chain:
207
+ *
208
+ * 1. `injectedRunner` — programmatic override (options.runner from caller / tests)
209
+ * 2. `runnerName` — explicit `--runner <name>` flag (must already be registered after discoverFromEnv)
210
+ * 3. `AI_SDLC_RUNNER_PLUGIN` env — path to a dynamic plugin module (loaded + registered)
211
+ * 4. ClaudeCodeRunner (hard-coded default)
212
+ *
213
+ * IMPORTANT — env-discovered runners do NOT auto-win (AISDLC-529 code review). They are
214
+ * registered by `discoverFromEnv()` so they are *selectable by name* via `--runner <name>`,
215
+ * but the mere PRESENCE of an ambient env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GH_TOKEN,
216
+ * etc. — commonly set for unrelated tools) must NOT silently switch the runner. Before this
217
+ * seam existed the orchestrator always used ClaudeCodeRunner; preserving that as the default
218
+ * (absent an explicit selector) avoids a breaking, surprising change for existing adopters.
219
+ *
220
+ * This function is async because step 3 may dynamically import a module.
221
+ *
222
+ * @throws Error when `runnerName` is provided but not registered in the registry.
223
+ * @throws Error when `AI_SDLC_RUNNER_PLUGIN` points to an invalid module.
224
+ */
225
+ export async function resolveRunner(registry, opts = {}) {
226
+ const env = opts.env ?? process.env;
227
+ // 1. Programmatic injection (options.runner / test override) — always wins
228
+ if (opts.injectedRunner) {
229
+ return opts.injectedRunner;
230
+ }
231
+ // 2. Explicit --runner <name> flag
232
+ if (opts.runnerName) {
233
+ const named = registry.get(opts.runnerName);
234
+ if (!named) {
235
+ const available = registry.listAvailable().map((r) => r.name);
236
+ throw new Error(`--runner "${opts.runnerName}" is not registered. ` +
237
+ `Available runners: ${available.length > 0 ? available.join(', ') : '(none)'}.\n` +
238
+ `Tip: set AI_SDLC_RUNNER_PLUGIN=/path/to/runner.mjs to load a custom runner first.`);
239
+ }
240
+ return named;
241
+ }
242
+ // 3. AI_SDLC_RUNNER_PLUGIN env — dynamically load + register, then return
243
+ const pluginPath = env.AI_SDLC_RUNNER_PLUGIN;
244
+ if (pluginPath) {
245
+ const registeredName = await registry.loadFromPlugin(pluginPath);
246
+ const pluginRunner = registry.get(registeredName);
247
+ // loadFromPlugin throws on invalid module, so pluginRunner is guaranteed to exist here
248
+ return pluginRunner;
249
+ }
250
+ // 4. ClaudeCodeRunner default (always in registry after discoverFromEnv).
251
+ // Env-discovered runners are intentionally NOT auto-selected here — the mere presence
252
+ // of an ambient API-key env var must not silently override the default (AISDLC-529 code
253
+ // review). An adopter selects an env-discovered runner explicitly via `--runner <name>`.
254
+ return registry.get('claude-code') ?? new (await import('./claude-code.js')).ClaudeCodeRunner();
255
+ }
166
256
  //# sourceMappingURL=runner-registry.js.map