@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,40 @@
1
+ import type { AdapterRequiresCredentialMgmtRfcDecision, SignalFetchResult, SignalSourceAdapter, SignalSourceName } from './types.js';
2
+ export declare class SignalSourceRegistry {
3
+ private readonly adapters;
4
+ /**
5
+ * Register an adapter. Per RFC-0030 OQ-13.1 v0.3 re-walkthrough, adapters
6
+ * that declare `requiresOAuth = true` are REFUSED at registration with a
7
+ * structured `AdapterRequiresCredentialMgmtRfcDecision` returned to the
8
+ * caller. Phase 1 ships env-var-based adapters ONLY; OAuth lifecycle
9
+ * waits for the future credential-management RFC.
10
+ *
11
+ * Returns the `Decision` record when refused; `null` when registered
12
+ * successfully. Caller (typically the pipeline bootstrap) forwards the
13
+ * Decision into the catalog.
14
+ */
15
+ register(adapter: SignalSourceAdapter): AdapterRequiresCredentialMgmtRfcDecision | null;
16
+ get(name: string): SignalSourceAdapter;
17
+ has(name: string): boolean;
18
+ list(): SignalSourceName[];
19
+ }
20
+ export declare function getSignalSourceAdapter(registry: SignalSourceRegistry, name: string): Promise<SignalSourceAdapter>;
21
+ /**
22
+ * Fetch from every adapter, converting credential failures into non-blocking
23
+ * Decision records so remaining adapters can continue per RFC-0030 OQ-13.1.
24
+ *
25
+ * **OQ-13.1 v0.3 re-walkthrough — dual Decision routing**:
26
+ * - `AdapterCredentialNotConfigured` → `Decision: adapter-credential-not-configured`
27
+ * (env var missing; setup task downstream).
28
+ * - `AdapterCredentialRejected` → `Decision: adapter-credential-rejected`
29
+ * (env var present but auth failed; rotation task downstream).
30
+ * - Legacy `AdapterCredentialInvalid` → `Decision: adapter-credential-invalid`
31
+ * (preserved for backward-compat; adapters that don't yet probe env vars).
32
+ *
33
+ * **OQ-13.4 v0.3 re-walkthrough — manual rate-limit routing**:
34
+ * - `ManualSignalRateLimitExceeded` → `Decision: manual-signal-rate-limit-exceeded`
35
+ * (over the per-operator UTC-day cap; batch-review escalation downstream).
36
+ *
37
+ * Pipeline continues with remaining valid adapters in every case (AC #4).
38
+ */
39
+ export declare function fetchSignalsFromAvailableAdapters(adapters: readonly SignalSourceAdapter[], since: Date): Promise<SignalFetchResult>;
40
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { AdapterCredentialInvalid, AdapterCredentialNotConfigured, AdapterCredentialRejected, ManualSignalIncomplete, ManualSignalRateLimitExceeded, SignalSourceUnavailable, UnknownSignalSource, } from './errors.js';
2
+ export class SignalSourceRegistry {
3
+ adapters = new Map();
4
+ /**
5
+ * Register an adapter. Per RFC-0030 OQ-13.1 v0.3 re-walkthrough, adapters
6
+ * that declare `requiresOAuth = true` are REFUSED at registration with a
7
+ * structured `AdapterRequiresCredentialMgmtRfcDecision` returned to the
8
+ * caller. Phase 1 ships env-var-based adapters ONLY; OAuth lifecycle
9
+ * waits for the future credential-management RFC.
10
+ *
11
+ * Returns the `Decision` record when refused; `null` when registered
12
+ * successfully. Caller (typically the pipeline bootstrap) forwards the
13
+ * Decision into the catalog.
14
+ */
15
+ register(adapter) {
16
+ if (adapter.requiresOAuth === true) {
17
+ return {
18
+ type: 'Decision',
19
+ decision: 'adapter-requires-credential-mgmt-rfc',
20
+ adapter: adapter.name,
21
+ message: `Adapter ${adapter.name} declares requiresOAuth=true; OAuth credential ` +
22
+ `lifecycle (refresh tokens, scopes) is deferred to the future ` +
23
+ `credential-management RFC. Phase 1 ships env-var-based adapters only.`,
24
+ };
25
+ }
26
+ this.adapters.set(adapter.name, adapter);
27
+ return null;
28
+ }
29
+ get(name) {
30
+ const adapter = this.adapters.get(name);
31
+ if (!adapter)
32
+ throw new UnknownSignalSource(name);
33
+ return adapter;
34
+ }
35
+ has(name) {
36
+ return this.adapters.has(name);
37
+ }
38
+ list() {
39
+ return Array.from(this.adapters.keys());
40
+ }
41
+ }
42
+ export async function getSignalSourceAdapter(registry, name) {
43
+ const adapter = registry.get(name);
44
+ if (!(await adapter.isAvailable())) {
45
+ throw new SignalSourceUnavailable(adapter.name);
46
+ }
47
+ return adapter;
48
+ }
49
+ /**
50
+ * Fetch from every adapter, converting credential failures into non-blocking
51
+ * Decision records so remaining adapters can continue per RFC-0030 OQ-13.1.
52
+ *
53
+ * **OQ-13.1 v0.3 re-walkthrough — dual Decision routing**:
54
+ * - `AdapterCredentialNotConfigured` → `Decision: adapter-credential-not-configured`
55
+ * (env var missing; setup task downstream).
56
+ * - `AdapterCredentialRejected` → `Decision: adapter-credential-rejected`
57
+ * (env var present but auth failed; rotation task downstream).
58
+ * - Legacy `AdapterCredentialInvalid` → `Decision: adapter-credential-invalid`
59
+ * (preserved for backward-compat; adapters that don't yet probe env vars).
60
+ *
61
+ * **OQ-13.4 v0.3 re-walkthrough — manual rate-limit routing**:
62
+ * - `ManualSignalRateLimitExceeded` → `Decision: manual-signal-rate-limit-exceeded`
63
+ * (over the per-operator UTC-day cap; batch-review escalation downstream).
64
+ *
65
+ * Pipeline continues with remaining valid adapters in every case (AC #4).
66
+ */
67
+ export async function fetchSignalsFromAvailableAdapters(adapters, since) {
68
+ const signals = [];
69
+ const decisions = [];
70
+ for (const adapter of adapters) {
71
+ try {
72
+ if (!(await adapter.isAvailable()))
73
+ continue;
74
+ signals.push(...(await adapter.fetchSignals(since)));
75
+ }
76
+ catch (err) {
77
+ if (err instanceof AdapterCredentialNotConfigured) {
78
+ decisions.push({
79
+ type: 'Decision',
80
+ decision: 'adapter-credential-not-configured',
81
+ adapter: adapter.name,
82
+ envVarName: err.envVarName,
83
+ message: `Signal source adapter ${adapter.name} is not configured: env var ` +
84
+ `${err.envVarName} is missing or empty. Set the env var to enable this adapter; ` +
85
+ `pipeline continues with remaining valid adapters.`,
86
+ });
87
+ continue;
88
+ }
89
+ if (err instanceof AdapterCredentialRejected) {
90
+ decisions.push({
91
+ type: 'Decision',
92
+ decision: 'adapter-credential-rejected',
93
+ adapter: adapter.name,
94
+ message: `Signal source adapter ${adapter.name} credential rejected by upstream service. ` +
95
+ `Rotate / re-generate the credential; pipeline continues with remaining valid adapters.`,
96
+ });
97
+ continue;
98
+ }
99
+ if (err instanceof AdapterCredentialInvalid) {
100
+ decisions.push({
101
+ type: 'Decision',
102
+ decision: 'adapter-credential-invalid',
103
+ adapter: adapter.name,
104
+ message: `Signal source adapter credentials invalid: ${adapter.name}`,
105
+ });
106
+ continue;
107
+ }
108
+ if (err instanceof ManualSignalIncomplete) {
109
+ decisions.push({
110
+ type: 'Decision',
111
+ decision: 'manual-signal-incomplete',
112
+ adapter: 'signal-source-manual',
113
+ ...(err.sourceId ? { sourceId: err.sourceId } : {}),
114
+ message: 'Manual signal missing required attestation fields',
115
+ });
116
+ continue;
117
+ }
118
+ if (err instanceof ManualSignalRateLimitExceeded) {
119
+ decisions.push({
120
+ type: 'Decision',
121
+ decision: 'manual-signal-rate-limit-exceeded',
122
+ adapter: 'signal-source-manual',
123
+ attestedBy: err.attestedBy,
124
+ utcDate: err.utcDate,
125
+ dailyCap: err.dailyCap,
126
+ ...(err.sourceId ? { sourceId: err.sourceId } : {}),
127
+ message: `Manual signal rate limit exceeded for ${err.attestedBy} on ${err.utcDate} ` +
128
+ `(cap: ${err.dailyCap}/day). Operator may escalate via batch review.`,
129
+ });
130
+ continue;
131
+ }
132
+ throw err;
133
+ }
134
+ }
135
+ return { signals, decisions };
136
+ }
137
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,227 @@
1
+ /**
2
+ * RFC-0030 OQ-13.3 re-walkthrough refinement (v0.3) — per-stage residency
3
+ * enforcement points + multi-posture composition.
4
+ *
5
+ * This module consolidates the post-Phase-4 residency enforcement points that
6
+ * span the clustering, storage, and cost-report layers. The adapter-level
7
+ * `fetchSignals()` check is owned by `checkSignalResidency` /
8
+ * `filterSignalsByResidency` in `./significance.ts` (already shipped via
9
+ * AISDLC-343..348); this module adds the missing per-stage hooks specified by
10
+ * the 2026-05-26 re-walkthrough:
11
+ *
12
+ * 1. **Clustering** — `partitionSignalsByRegion()` segregates signals by
13
+ * `region` BEFORE similarity computation so cross-region merge is
14
+ * structurally impossible when the active regime requires it. The
15
+ * `clusterRequiresSegregation()` helper consults the
16
+ * `residencyEnforcement.clustering` config + the active regime list.
17
+ *
18
+ * 2. **Storage** — `StoredSignalRecord` is the persistence shape, with a
19
+ * mandatory `residencyRegion` field derived from the signal's
20
+ * `region` tag. `readSignalRecordWithAudit()` enforces the
21
+ * cross-region-read elevated audit-log entry per AC #4.
22
+ *
23
+ * 3. **Unified cost report** — `groupCostByRegion()` partitions cost
24
+ * attribution rows by `residencyRegion`, breaking out per-region totals
25
+ * so adopters can audit cross-region cost mingling.
26
+ *
27
+ * 4. **Multi-posture UNION** — when an adopter declares both HIPAA and
28
+ * GDPR (RFC-0022 OQ-7 forward-compat), `composePostures()` takes the
29
+ * UNION of constraints — strictest of each constraint applies. Output is
30
+ * a single `ResidencyRegimeDeclaration` consumable by the existing
31
+ * `checkSignalResidency` and by the new clustering / storage helpers.
32
+ *
33
+ * **G0 non-blocking pipeline contract** (RFC-0035): residency violations
34
+ * never halt the pipeline. Adapter-level violations surface as
35
+ * `Decision: signal-residency-violation` (already implemented). Storage /
36
+ * clustering / cost-report violations surface as elevated audit-log entries
37
+ * + per-region breakdowns — the pipeline continues to ingest signals from
38
+ * permitted regions in the same batch.
39
+ *
40
+ * @module signal-ingestion/residency
41
+ */
42
+ import type { RawSignal } from './types.js';
43
+ import type { ResidencyRegimeDeclaration } from './significance.js';
44
+ /**
45
+ * Per-regime constraint sub-shape consumed by `composePostures()`. Each input
46
+ * regime declaration carries (a) the regime ID, (b) the allowed regions for
47
+ * THAT regime (subset of ISO-3166 alpha-2 codes or compliance region tags).
48
+ *
49
+ * Concrete examples consumed by the composer:
50
+ * - `{ regime: 'gdpr', allowedRegions: ['eu', 'gb'] }`
51
+ * - `{ regime: 'hipaa', allowedRegions: ['us', 'us-east'] }`
52
+ * - `{ regime: 'ccpa', allowedRegions: ['us', 'us-west', 'ca'] }`
53
+ */
54
+ export interface PostureRegimeInput {
55
+ regime: string;
56
+ allowedRegions: string[];
57
+ }
58
+ /**
59
+ * Compose multiple posture regimes into a single `ResidencyRegimeDeclaration`
60
+ * with UNION-of-constraints semantics per the RFC-0030 v0.3 multi-posture
61
+ * forward-compat clause: when an adopter declares both HIPAA AND GDPR, every
62
+ * regime's allowed-region constraint MUST be satisfied (intersection of
63
+ * allowed regions across regimes, NOT union of allowed regions).
64
+ *
65
+ * Worked example: HIPAA allows `['us', 'us-east']`, GDPR allows `['eu', 'gb']`.
66
+ * The composed posture lists BOTH regimes as active, with `allowedRegions`
67
+ * tracked PER REGIME. The existing `checkSignalResidency` then refuses a
68
+ * signal IFF its region fails at least one active regime — which is the
69
+ * correct UNION-of-constraints / strictest-wins behaviour. (For the example
70
+ * above, NO region satisfies both regimes; the adopter must scope their
71
+ * adapter inputs to a single regime per source.)
72
+ *
73
+ * The composed declaration is normalised:
74
+ * - `regimes` is sorted alphabetically (deterministic event log + audit).
75
+ * - `allowedRegionsByRegime` lower-cases all region tags and dedupes.
76
+ * - Empty input → `{ regimes: [], allowedRegionsByRegime: {} }` (no
77
+ * constraints; matches single-regime empty case).
78
+ */
79
+ export declare function composePostures(postures: PostureRegimeInput[]): ResidencyRegimeDeclaration;
80
+ export declare function clusterRequiresSegregation(declaration: ResidencyRegimeDeclaration): boolean;
81
+ /**
82
+ * Partition a batch of signals by `region` so the clustering pass treats
83
+ * each partition as a separate input population. Signals with `region`
84
+ * undefined go into the special `__unspecified` partition — they're not
85
+ * cross-mingled with tagged regions (defensive default; adapters that don't
86
+ * surface region metadata cluster amongst themselves rather than getting
87
+ * silently merged into a region-tagged cluster they shouldn't be part of).
88
+ *
89
+ * Region keys are lower-cased for stable matching. The returned map iterates
90
+ * in deterministic order (Map preserves insertion order; partitioning loops
91
+ * over the input in order).
92
+ *
93
+ * **Usage**: pipeline callers invoke `clusterSignalsWithResidency()` rather
94
+ * than `clusterSignals()` directly when the active regime requires
95
+ * segregation. The wrapper runs `clusterSignals()` once per partition and
96
+ * concatenates the per-partition `ClusteringResult` arrays.
97
+ */
98
+ export declare function partitionSignalsByRegion<T extends {
99
+ signal: {
100
+ region?: string;
101
+ };
102
+ }>(signals: T[]): Map<string, T[]>;
103
+ /**
104
+ * The on-disk / on-database persistence shape for an ingested signal. The
105
+ * `residencyRegion` field is MANDATORY at storage time — adapters that
106
+ * couldn't derive a region tag persist `'unknown'` (visible-gap surface for
107
+ * the operator's compliance-config rollout, NOT a silent omission).
108
+ *
109
+ * `ingestedAt` is the pipeline's timestamp (independent of
110
+ * `sourceTimestamp`), useful for audit (when did THIS pipeline run see
111
+ * THIS signal?).
112
+ *
113
+ * `sourceTimestampIso` / `attestedAtIso` are ISO-8601 strings to keep the
114
+ * record JSON-serialisable without Date-deserialise hazards.
115
+ */
116
+ export interface StoredSignalRecord {
117
+ sourceId: string;
118
+ sourceTimestampIso: string;
119
+ ingestedAtIso: string;
120
+ customerId?: string;
121
+ customerTier?: string;
122
+ payload: string;
123
+ metadata?: Record<string, unknown>;
124
+ attestedBy?: string;
125
+ attestedAtIso?: string;
126
+ /**
127
+ * Residency region the signal was tagged with at fetchSignals time. Stored
128
+ * lower-cased for stable matching. `'unknown'` when the adapter didn't
129
+ * surface region metadata.
130
+ */
131
+ residencyRegion: string;
132
+ }
133
+ /**
134
+ * Build a `StoredSignalRecord` from a `RawSignal`. The `residencyRegion`
135
+ * field is derived from `signal.region` (lower-cased) or `'unknown'` when
136
+ * absent. `ingestedAt` defaults to `new Date()` — callers can pass an
137
+ * explicit clock for deterministic tests.
138
+ */
139
+ export declare function makeStoredSignalRecord(signal: RawSignal, options?: {
140
+ ingestedAt?: Date;
141
+ }): StoredSignalRecord;
142
+ /**
143
+ * Elevated audit-log entry emitted when a cross-region read occurs. The
144
+ * caller's `residencyRegion` (the region of the agent / surface reading the
145
+ * record) differs from the record's `residencyRegion`. Per RFC-0030 v0.3
146
+ * §11 storage enforcement clause, every such read MUST be logged for
147
+ * SOC2-style audit trails.
148
+ *
149
+ * `severity: 'elevated'` distinguishes this from regular pipeline events —
150
+ * downstream audit consumers (RFC-0022 Compliance Posture audit surface)
151
+ * filter on `severity` to surface only the audit-worthy entries.
152
+ */
153
+ export interface CrossRegionReadAuditEntry {
154
+ type: 'AuditEvent';
155
+ event: 'cross-region-signal-read';
156
+ severity: 'elevated';
157
+ sourceId: string;
158
+ recordResidencyRegion: string;
159
+ callerResidencyRegion: string;
160
+ reader: string;
161
+ readAtIso: string;
162
+ }
163
+ /**
164
+ * Read a stored signal record with cross-region audit enforcement. When the
165
+ * caller's region matches the record's region (or when either is `'unknown'`
166
+ * — a visible-gap state that doesn't trigger audit), returns the record
167
+ * with `auditEntry: null`. When regions differ AND both are known, returns
168
+ * the record with a populated `auditEntry` the caller MUST persist to the
169
+ * audit log.
170
+ *
171
+ * The pipeline does NOT block cross-region reads — the audit entry is the
172
+ * mitigation. RFC-0030 v0.3 explicitly says "cross-region read requires
173
+ * elevated audit log entry", not "is forbidden". Adopters who want to
174
+ * forbid cross-region reads do so via their own surface (the audit log is
175
+ * the input to that policy).
176
+ *
177
+ * **Why not just check at write time?** Storage is the canonical persistence
178
+ * layer — once a record is on disk it's findable. Restricting WRITES
179
+ * doesn't help: the audit obligation is on READ. This matches AWS S3
180
+ * cross-region replication audit semantics (log on read, not on write).
181
+ */
182
+ export declare function readSignalRecordWithAudit(record: StoredSignalRecord, options: {
183
+ callerRegion: string;
184
+ reader: string;
185
+ readAt?: Date;
186
+ }): {
187
+ record: StoredSignalRecord;
188
+ auditEntry: CrossRegionReadAuditEntry | null;
189
+ };
190
+ /**
191
+ * A single cost-attribution row consumed by `groupCostByRegion()`. Mirrors
192
+ * the cost-row shape the unified cost report would emit per RFC-0019 OQ-7
193
+ * (embedding cost) and downstream signal-pipeline cost extensions.
194
+ *
195
+ * `residencyRegion` is the region the cost was incurred for (e.g. clustering
196
+ * EU-tagged signals via an EU-region embedding provider). When the adapter
197
+ * didn't surface a region, the row carries `'unknown'`.
198
+ */
199
+ export interface CostAttributionRow {
200
+ consumerLabel: string;
201
+ costUsd: number;
202
+ residencyRegion: string;
203
+ metadata?: Record<string, unknown>;
204
+ }
205
+ /**
206
+ * Per-region cost breakdown — output of `groupCostByRegion()`. `__total` is
207
+ * the sum across all regions; per-region entries sum within each region.
208
+ * `__unknown` is the bucket for rows whose `residencyRegion` is `'unknown'`
209
+ * — operators see at a glance how much cost they're attributing to
210
+ * un-region-tagged sources.
211
+ */
212
+ export interface CostByRegionBreakdown {
213
+ totalUsd: number;
214
+ perRegion: Record<string, number>;
215
+ }
216
+ /**
217
+ * Group cost-attribution rows by `residencyRegion` so the unified cost report
218
+ * can break out per-region totals. Pipeline callers feed this with rows from
219
+ * the embedding adapter (`RFC-0019` OQ-7), the LLM classifier (per-signal
220
+ * cost), and any external-API cost (Zendesk / Salesforce). The output is a
221
+ * single normalised breakdown the cost-report surface renders.
222
+ *
223
+ * Rows are summed by region (case-insensitive on the region key). When the
224
+ * input is empty, `totalUsd: 0` and `perRegion: {}`.
225
+ */
226
+ export declare function groupCostByRegion(rows: CostAttributionRow[]): CostByRegionBreakdown;
227
+ //# sourceMappingURL=residency.d.ts.map
@@ -0,0 +1,238 @@
1
+ /**
2
+ * RFC-0030 OQ-13.3 re-walkthrough refinement (v0.3) — per-stage residency
3
+ * enforcement points + multi-posture composition.
4
+ *
5
+ * This module consolidates the post-Phase-4 residency enforcement points that
6
+ * span the clustering, storage, and cost-report layers. The adapter-level
7
+ * `fetchSignals()` check is owned by `checkSignalResidency` /
8
+ * `filterSignalsByResidency` in `./significance.ts` (already shipped via
9
+ * AISDLC-343..348); this module adds the missing per-stage hooks specified by
10
+ * the 2026-05-26 re-walkthrough:
11
+ *
12
+ * 1. **Clustering** — `partitionSignalsByRegion()` segregates signals by
13
+ * `region` BEFORE similarity computation so cross-region merge is
14
+ * structurally impossible when the active regime requires it. The
15
+ * `clusterRequiresSegregation()` helper consults the
16
+ * `residencyEnforcement.clustering` config + the active regime list.
17
+ *
18
+ * 2. **Storage** — `StoredSignalRecord` is the persistence shape, with a
19
+ * mandatory `residencyRegion` field derived from the signal's
20
+ * `region` tag. `readSignalRecordWithAudit()` enforces the
21
+ * cross-region-read elevated audit-log entry per AC #4.
22
+ *
23
+ * 3. **Unified cost report** — `groupCostByRegion()` partitions cost
24
+ * attribution rows by `residencyRegion`, breaking out per-region totals
25
+ * so adopters can audit cross-region cost mingling.
26
+ *
27
+ * 4. **Multi-posture UNION** — when an adopter declares both HIPAA and
28
+ * GDPR (RFC-0022 OQ-7 forward-compat), `composePostures()` takes the
29
+ * UNION of constraints — strictest of each constraint applies. Output is
30
+ * a single `ResidencyRegimeDeclaration` consumable by the existing
31
+ * `checkSignalResidency` and by the new clustering / storage helpers.
32
+ *
33
+ * **G0 non-blocking pipeline contract** (RFC-0035): residency violations
34
+ * never halt the pipeline. Adapter-level violations surface as
35
+ * `Decision: signal-residency-violation` (already implemented). Storage /
36
+ * clustering / cost-report violations surface as elevated audit-log entries
37
+ * + per-region breakdowns — the pipeline continues to ingest signals from
38
+ * permitted regions in the same batch.
39
+ *
40
+ * @module signal-ingestion/residency
41
+ */
42
+ /**
43
+ * Compose multiple posture regimes into a single `ResidencyRegimeDeclaration`
44
+ * with UNION-of-constraints semantics per the RFC-0030 v0.3 multi-posture
45
+ * forward-compat clause: when an adopter declares both HIPAA AND GDPR, every
46
+ * regime's allowed-region constraint MUST be satisfied (intersection of
47
+ * allowed regions across regimes, NOT union of allowed regions).
48
+ *
49
+ * Worked example: HIPAA allows `['us', 'us-east']`, GDPR allows `['eu', 'gb']`.
50
+ * The composed posture lists BOTH regimes as active, with `allowedRegions`
51
+ * tracked PER REGIME. The existing `checkSignalResidency` then refuses a
52
+ * signal IFF its region fails at least one active regime — which is the
53
+ * correct UNION-of-constraints / strictest-wins behaviour. (For the example
54
+ * above, NO region satisfies both regimes; the adopter must scope their
55
+ * adapter inputs to a single regime per source.)
56
+ *
57
+ * The composed declaration is normalised:
58
+ * - `regimes` is sorted alphabetically (deterministic event log + audit).
59
+ * - `allowedRegionsByRegime` lower-cases all region tags and dedupes.
60
+ * - Empty input → `{ regimes: [], allowedRegionsByRegime: {} }` (no
61
+ * constraints; matches single-regime empty case).
62
+ */
63
+ export function composePostures(postures) {
64
+ if (postures.length === 0) {
65
+ return { regimes: [], allowedRegionsByRegime: {} };
66
+ }
67
+ // Merge duplicate regime IDs by intersecting their allowedRegions (rare but
68
+ // possible when an adopter declares the same regime twice — strictest of
69
+ // the two wins, matching the UNION-of-constraints semantics).
70
+ const merged = new Map();
71
+ for (const p of postures) {
72
+ const regime = p.regime.toLowerCase();
73
+ const allowedLower = p.allowedRegions.map((r) => r.toLowerCase());
74
+ const existing = merged.get(regime);
75
+ if (existing === undefined) {
76
+ merged.set(regime, new Set(allowedLower));
77
+ }
78
+ else {
79
+ // Intersect with the prior declaration.
80
+ const intersection = new Set();
81
+ for (const r of existing)
82
+ if (allowedLower.includes(r))
83
+ intersection.add(r);
84
+ merged.set(regime, intersection);
85
+ }
86
+ }
87
+ const regimes = Array.from(merged.keys()).sort();
88
+ const allowedRegionsByRegime = {};
89
+ for (const regime of regimes) {
90
+ allowedRegionsByRegime[regime] = Array.from(merged.get(regime)).sort();
91
+ }
92
+ return { regimes, allowedRegionsByRegime };
93
+ }
94
+ // ── Clustering enforcement (AC #3) ──────────────────────────────────────────
95
+ /**
96
+ * Does the active regime declaration REQUIRE residency segregation at the
97
+ * clustering layer? Returns `true` when at least one active regime is in the
98
+ * known-segregation list (GDPR, HIPAA, PIPEDA — regimes whose data-handling
99
+ * rules forbid cross-region co-mingling of customer records). Returns `false`
100
+ * when no regimes are active OR when only non-segregation regimes are
101
+ * declared (e.g. CCPA which is about consumer rights, not data residency).
102
+ *
103
+ * The segregation list is intentionally conservative — adopters should
104
+ * explicitly enable clustering segregation via the config flag when their
105
+ * regime is not in the default list. Per RFC-0030 v0.3 §11
106
+ * `residencyEnforcement.enforcementPoints.clustering`, the config flag is
107
+ * the source of truth; this helper is the fallback when the flag isn't set.
108
+ */
109
+ const KNOWN_SEGREGATION_REGIMES = new Set(['gdpr', 'hipaa', 'pipeda']);
110
+ export function clusterRequiresSegregation(declaration) {
111
+ if (declaration.regimes.length === 0)
112
+ return false;
113
+ for (const regime of declaration.regimes) {
114
+ if (KNOWN_SEGREGATION_REGIMES.has(regime.toLowerCase()))
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Partition a batch of signals by `region` so the clustering pass treats
121
+ * each partition as a separate input population. Signals with `region`
122
+ * undefined go into the special `__unspecified` partition — they're not
123
+ * cross-mingled with tagged regions (defensive default; adapters that don't
124
+ * surface region metadata cluster amongst themselves rather than getting
125
+ * silently merged into a region-tagged cluster they shouldn't be part of).
126
+ *
127
+ * Region keys are lower-cased for stable matching. The returned map iterates
128
+ * in deterministic order (Map preserves insertion order; partitioning loops
129
+ * over the input in order).
130
+ *
131
+ * **Usage**: pipeline callers invoke `clusterSignalsWithResidency()` rather
132
+ * than `clusterSignals()` directly when the active regime requires
133
+ * segregation. The wrapper runs `clusterSignals()` once per partition and
134
+ * concatenates the per-partition `ClusteringResult` arrays.
135
+ */
136
+ export function partitionSignalsByRegion(signals) {
137
+ const partitions = new Map();
138
+ for (const s of signals) {
139
+ const region = (s.signal.region ?? '__unspecified').toLowerCase();
140
+ let bucket = partitions.get(region);
141
+ if (bucket === undefined) {
142
+ bucket = [];
143
+ partitions.set(region, bucket);
144
+ }
145
+ bucket.push(s);
146
+ }
147
+ return partitions;
148
+ }
149
+ /**
150
+ * Build a `StoredSignalRecord` from a `RawSignal`. The `residencyRegion`
151
+ * field is derived from `signal.region` (lower-cased) or `'unknown'` when
152
+ * absent. `ingestedAt` defaults to `new Date()` — callers can pass an
153
+ * explicit clock for deterministic tests.
154
+ */
155
+ export function makeStoredSignalRecord(signal, options = {}) {
156
+ const ingestedAt = options.ingestedAt ?? new Date();
157
+ return {
158
+ sourceId: signal.sourceId,
159
+ sourceTimestampIso: signal.sourceTimestamp.toISOString(),
160
+ ingestedAtIso: ingestedAt.toISOString(),
161
+ ...(signal.customerId !== undefined && { customerId: signal.customerId }),
162
+ ...(signal.customerTier !== undefined && { customerTier: signal.customerTier }),
163
+ payload: signal.payload,
164
+ ...(signal.metadata !== undefined && { metadata: signal.metadata }),
165
+ ...(signal.attestedBy !== undefined && { attestedBy: signal.attestedBy }),
166
+ ...(signal.attestedAt !== undefined && { attestedAtIso: signal.attestedAt.toISOString() }),
167
+ residencyRegion: (signal.region ?? 'unknown').toLowerCase(),
168
+ };
169
+ }
170
+ /**
171
+ * Read a stored signal record with cross-region audit enforcement. When the
172
+ * caller's region matches the record's region (or when either is `'unknown'`
173
+ * — a visible-gap state that doesn't trigger audit), returns the record
174
+ * with `auditEntry: null`. When regions differ AND both are known, returns
175
+ * the record with a populated `auditEntry` the caller MUST persist to the
176
+ * audit log.
177
+ *
178
+ * The pipeline does NOT block cross-region reads — the audit entry is the
179
+ * mitigation. RFC-0030 v0.3 explicitly says "cross-region read requires
180
+ * elevated audit log entry", not "is forbidden". Adopters who want to
181
+ * forbid cross-region reads do so via their own surface (the audit log is
182
+ * the input to that policy).
183
+ *
184
+ * **Why not just check at write time?** Storage is the canonical persistence
185
+ * layer — once a record is on disk it's findable. Restricting WRITES
186
+ * doesn't help: the audit obligation is on READ. This matches AWS S3
187
+ * cross-region replication audit semantics (log on read, not on write).
188
+ */
189
+ export function readSignalRecordWithAudit(record, options) {
190
+ const callerRegion = options.callerRegion.toLowerCase();
191
+ const recordRegion = record.residencyRegion;
192
+ const readAt = options.readAt ?? new Date();
193
+ // Visible-gap: when either side is unknown, no audit fires. Operators get
194
+ // visibility into the gap via the population-level region-breakdown metric
195
+ // (see groupCostByRegion's `__unknown` bucket).
196
+ if (callerRegion === 'unknown' || recordRegion === 'unknown') {
197
+ return { record, auditEntry: null };
198
+ }
199
+ if (callerRegion === recordRegion) {
200
+ return { record, auditEntry: null };
201
+ }
202
+ return {
203
+ record,
204
+ auditEntry: {
205
+ type: 'AuditEvent',
206
+ event: 'cross-region-signal-read',
207
+ severity: 'elevated',
208
+ sourceId: record.sourceId,
209
+ recordResidencyRegion: recordRegion,
210
+ callerResidencyRegion: callerRegion,
211
+ reader: options.reader,
212
+ readAtIso: readAt.toISOString(),
213
+ },
214
+ };
215
+ }
216
+ /**
217
+ * Group cost-attribution rows by `residencyRegion` so the unified cost report
218
+ * can break out per-region totals. Pipeline callers feed this with rows from
219
+ * the embedding adapter (`RFC-0019` OQ-7), the LLM classifier (per-signal
220
+ * cost), and any external-API cost (Zendesk / Salesforce). The output is a
221
+ * single normalised breakdown the cost-report surface renders.
222
+ *
223
+ * Rows are summed by region (case-insensitive on the region key). When the
224
+ * input is empty, `totalUsd: 0` and `perRegion: {}`.
225
+ */
226
+ export function groupCostByRegion(rows) {
227
+ const perRegion = {};
228
+ let totalUsd = 0;
229
+ for (const r of rows) {
230
+ if (!Number.isFinite(r.costUsd) || r.costUsd < 0)
231
+ continue; // skip malformed rows
232
+ const region = (r.residencyRegion || 'unknown').toLowerCase();
233
+ perRegion[region] = (perRegion[region] ?? 0) + r.costUsd;
234
+ totalUsd += r.costUsd;
235
+ }
236
+ return { totalUsd, perRegion };
237
+ }
238
+ //# sourceMappingURL=residency.js.map