@ai-sdlc/orchestrator 0.9.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__test-helpers/git-env.d.ts +40 -0
- package/dist/__test-helpers/git-env.js +73 -0
- package/dist/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +134 -1
- package/dist/admission-composite.js +170 -6
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +71 -10
- package/dist/admission-hc.d.ts +20 -1
- package/dist/admission-hc.js +12 -1
- package/dist/admission-score.d.ts +44 -0
- package/dist/admission-score.js +11 -2
- package/dist/backlog-adapter.d.ts +45 -0
- package/dist/backlog-adapter.js +145 -1
- package/dist/calibration.d.ts +81 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/git-remote.d.ts +25 -0
- package/dist/cli/commands/git-remote.js +80 -1
- package/dist/cli/commands/init-features.d.ts +507 -0
- package/dist/cli/commands/init-features.js +1100 -0
- package/dist/cli/commands/init-templates.d.ts +301 -0
- package/dist/cli/commands/init-templates.js +1341 -0
- package/dist/cli/commands/init.d.ts +67 -0
- package/dist/cli/commands/init.js +301 -2
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +42 -8
- package/dist/compliance/composer.d.ts +79 -0
- package/dist/compliance/composer.js +258 -0
- package/dist/compliance/errors.d.ts +64 -0
- package/dist/compliance/errors.js +85 -0
- package/dist/compliance/loader.d.ts +52 -0
- package/dist/compliance/loader.js +124 -0
- package/dist/compliance/types.d.ts +184 -0
- package/dist/compliance/types.js +41 -0
- package/dist/compliance-clearance.d.ts +269 -0
- package/dist/compliance-clearance.js +269 -0
- package/dist/config.js +17 -0
- package/dist/cost-tracker.d.ts +22 -0
- package/dist/cost-tracker.js +41 -0
- package/dist/database/adapters/external.js +5 -1
- package/dist/design-authority.d.ts +13 -0
- package/dist/design-authority.js +13 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
- package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
- package/dist/embedding/consumers/tessellation-drift.js +76 -0
- package/dist/embedding/cross-provider.d.ts +78 -0
- package/dist/embedding/cross-provider.js +75 -0
- package/dist/embedding/deprecation.d.ts +151 -0
- package/dist/embedding/deprecation.js +229 -0
- package/dist/embedding/errors.d.ts +90 -0
- package/dist/embedding/errors.js +150 -0
- package/dist/embedding/index.d.ts +29 -0
- package/dist/embedding/index.js +24 -0
- package/dist/embedding/pipeline-load.d.ts +146 -0
- package/dist/embedding/pipeline-load.js +178 -0
- package/dist/embedding/registry.d.ts +45 -0
- package/dist/embedding/registry.js +61 -0
- package/dist/embedding/stale-vector.d.ts +110 -0
- package/dist/embedding/stale-vector.js +92 -0
- package/dist/embedding/storage/index.d.ts +51 -0
- package/dist/embedding/storage/index.js +43 -0
- package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
- package/dist/embedding/storage/jsonl-backend.js +332 -0
- package/dist/embedding/storage/types.d.ts +135 -0
- package/dist/embedding/storage/types.js +13 -0
- package/dist/embedding/types.d.ts +180 -0
- package/dist/embedding/types.js +10 -0
- package/dist/execute.d.ts +29 -2
- package/dist/execute.js +179 -35
- package/dist/index.d.ts +14 -2
- package/dist/index.js +19 -2
- package/dist/journey/inheritance-validator.d.ts +396 -0
- package/dist/journey/inheritance-validator.js +370 -0
- package/dist/journey/state-id-drift-rule.d.ts +137 -0
- package/dist/journey/state-id-drift-rule.js +245 -0
- package/dist/journey-sa2-router.d.ts +395 -0
- package/dist/journey-sa2-router.js +308 -0
- package/dist/models/classifier.d.ts +3 -1
- package/dist/models/classifier.js +61 -5
- package/dist/pillar-breakdown.d.ts +23 -1
- package/dist/pillar-breakdown.js +9 -0
- package/dist/runners/review-agent.js +6 -2
- package/dist/runners/runner-registry.d.ts +36 -0
- package/dist/runners/runner-registry.js +90 -0
- package/dist/runtime/attestations.d.ts +663 -11
- package/dist/runtime/attestations.js +971 -24
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/parallelism-flag.d.ts +21 -3
- package/dist/runtime/parallelism-flag.js +27 -6
- package/dist/runtime/port-allocator.d.ts +11 -0
- package/dist/runtime/port-allocator.js +30 -0
- package/dist/sa-scoring/layer3-llm.js +6 -1
- package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
- package/dist/sa-scoring/revision-proposal-config.js +198 -0
- package/dist/sa-scoring/revision-proposal.d.ts +285 -0
- package/dist/sa-scoring/revision-proposal.js +417 -0
- package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
- package/dist/signal-ingestion/adapters/community-thread.js +55 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
- package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
- package/dist/signal-ingestion/adapters/manual.js +112 -0
- package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
- package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
- package/dist/signal-ingestion/classifier.d.ts +205 -0
- package/dist/signal-ingestion/classifier.js +494 -0
- package/dist/signal-ingestion/clustering-types.d.ts +36 -0
- package/dist/signal-ingestion/clustering-types.js +14 -0
- package/dist/signal-ingestion/clustering.d.ts +200 -0
- package/dist/signal-ingestion/clustering.js +413 -0
- package/dist/signal-ingestion/config.d.ts +351 -0
- package/dist/signal-ingestion/config.js +587 -0
- package/dist/signal-ingestion/d1.d.ts +252 -0
- package/dist/signal-ingestion/d1.js +235 -0
- package/dist/signal-ingestion/errors.d.ts +73 -0
- package/dist/signal-ingestion/errors.js +108 -0
- package/dist/signal-ingestion/governance-events.d.ts +181 -0
- package/dist/signal-ingestion/governance-events.js +189 -0
- package/dist/signal-ingestion/index.d.ts +35 -0
- package/dist/signal-ingestion/index.js +53 -0
- package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
- package/dist/signal-ingestion/manual-share-metric.js +106 -0
- package/dist/signal-ingestion/registry.d.ts +40 -0
- package/dist/signal-ingestion/registry.js +137 -0
- package/dist/signal-ingestion/residency.d.ts +227 -0
- package/dist/signal-ingestion/residency.js +238 -0
- package/dist/signal-ingestion/significance.d.ts +554 -0
- package/dist/signal-ingestion/significance.js +555 -0
- package/dist/signal-ingestion/types.d.ts +191 -0
- package/dist/signal-ingestion/types.js +8 -0
- package/dist/substrate/drift-composition.d.ts +270 -0
- package/dist/substrate/drift-composition.js +306 -0
- package/dist/substrate/drift-tui-surface.d.ts +61 -0
- package/dist/substrate/drift-tui-surface.js +102 -0
- package/dist/substrate/identity-class.d.ts +176 -0
- package/dist/substrate/identity-class.js +201 -0
- package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
- package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
- package/dist/tessellation/rule-registry.d.ts +269 -0
- package/dist/tessellation/rule-registry.js +92 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
- package/dist/tessellation-admission.d.ts +162 -0
- package/dist/tessellation-admission.js +146 -0
- package/dist/tessellation-drift.d.ts +246 -0
- package/dist/tessellation-drift.js +250 -0
- package/dist/validate-config.js +13 -0
- package/dist/validate-issue.js +2 -2
- package/dist/variant/cardinality-activation.d.ts +126 -0
- package/dist/variant/cardinality-activation.js +101 -0
- package/dist/variant/deprecation-lifecycle.d.ts +184 -0
- package/dist/variant/deprecation-lifecycle.js +208 -0
- package/dist/variant/drift-extension.d.ts +136 -0
- package/dist/variant/drift-extension.js +164 -0
- package/dist/variant/engineering-review.d.ts +185 -0
- package/dist/variant/engineering-review.js +142 -0
- package/dist/variant/index.d.ts +32 -0
- package/dist/variant/index.js +32 -0
- package/dist/variant/inheritance-validator.d.ts +165 -0
- package/dist/variant/inheritance-validator.js +139 -0
- package/dist/variant/internal-adopter/index.d.ts +11 -0
- package/dist/variant/internal-adopter/index.js +10 -0
- package/dist/variant/internal-adopter/products.d.ts +156 -0
- package/dist/variant/internal-adopter/products.js +366 -0
- package/dist/variant-admission.d.ts +316 -0
- package/dist/variant-admission.js +247 -0
- package/package.json +11 -8
|
@@ -0,0 +1,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
|