@ai-sdlc/orchestrator 0.10.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/backlog-adapter.d.ts +45 -0
  10. package/dist/backlog-adapter.js +145 -1
  11. package/dist/calibration.d.ts +81 -0
  12. package/dist/calibration.js +76 -0
  13. package/dist/cli/commands/init-features.d.ts +301 -2
  14. package/dist/cli/commands/init-features.js +634 -7
  15. package/dist/cli/commands/init-templates.d.ts +198 -1
  16. package/dist/cli/commands/init-templates.js +943 -1
  17. package/dist/cli/commands/init.d.ts +45 -0
  18. package/dist/cli/commands/init.js +147 -5
  19. package/dist/cli/commands/run.js +9 -1
  20. package/dist/cli/index.d.ts +11 -0
  21. package/dist/cli/index.js +42 -8
  22. package/dist/compliance/composer.d.ts +79 -0
  23. package/dist/compliance/composer.js +258 -0
  24. package/dist/compliance/errors.d.ts +64 -0
  25. package/dist/compliance/errors.js +85 -0
  26. package/dist/compliance/loader.d.ts +52 -0
  27. package/dist/compliance/loader.js +124 -0
  28. package/dist/compliance/types.d.ts +184 -0
  29. package/dist/compliance/types.js +41 -0
  30. package/dist/compliance-clearance.d.ts +269 -0
  31. package/dist/compliance-clearance.js +269 -0
  32. package/dist/config.js +17 -0
  33. package/dist/cost-tracker.d.ts +22 -0
  34. package/dist/cost-tracker.js +41 -0
  35. package/dist/database/adapters/external.js +5 -1
  36. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  37. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  38. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  39. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  40. package/dist/embedding/cross-provider.d.ts +78 -0
  41. package/dist/embedding/cross-provider.js +75 -0
  42. package/dist/embedding/deprecation.d.ts +151 -0
  43. package/dist/embedding/deprecation.js +229 -0
  44. package/dist/embedding/errors.d.ts +90 -0
  45. package/dist/embedding/errors.js +150 -0
  46. package/dist/embedding/index.d.ts +29 -0
  47. package/dist/embedding/index.js +24 -0
  48. package/dist/embedding/pipeline-load.d.ts +146 -0
  49. package/dist/embedding/pipeline-load.js +178 -0
  50. package/dist/embedding/registry.d.ts +45 -0
  51. package/dist/embedding/registry.js +61 -0
  52. package/dist/embedding/stale-vector.d.ts +110 -0
  53. package/dist/embedding/stale-vector.js +92 -0
  54. package/dist/embedding/storage/index.d.ts +51 -0
  55. package/dist/embedding/storage/index.js +43 -0
  56. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  57. package/dist/embedding/storage/jsonl-backend.js +332 -0
  58. package/dist/embedding/storage/types.d.ts +135 -0
  59. package/dist/embedding/storage/types.js +13 -0
  60. package/dist/embedding/types.d.ts +180 -0
  61. package/dist/embedding/types.js +10 -0
  62. package/dist/execute.d.ts +29 -2
  63. package/dist/execute.js +171 -30
  64. package/dist/index.d.ts +14 -2
  65. package/dist/index.js +19 -2
  66. package/dist/journey/inheritance-validator.d.ts +396 -0
  67. package/dist/journey/inheritance-validator.js +370 -0
  68. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  69. package/dist/journey/state-id-drift-rule.js +245 -0
  70. package/dist/journey-sa2-router.d.ts +395 -0
  71. package/dist/journey-sa2-router.js +308 -0
  72. package/dist/runners/review-agent.js +6 -2
  73. package/dist/runners/runner-registry.d.ts +36 -0
  74. package/dist/runners/runner-registry.js +90 -0
  75. package/dist/runtime/attestations.d.ts +173 -13
  76. package/dist/runtime/attestations.js +252 -40
  77. package/dist/runtime/index.d.ts +1 -1
  78. package/dist/runtime/index.js +1 -1
  79. package/dist/sa-scoring/layer3-llm.js +6 -1
  80. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  81. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  82. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  83. package/dist/sa-scoring/revision-proposal.js +417 -0
  84. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  85. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  86. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  87. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  88. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  89. package/dist/signal-ingestion/adapters/manual.js +112 -0
  90. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  91. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  92. package/dist/signal-ingestion/classifier.d.ts +205 -0
  93. package/dist/signal-ingestion/classifier.js +494 -0
  94. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  95. package/dist/signal-ingestion/clustering-types.js +14 -0
  96. package/dist/signal-ingestion/clustering.d.ts +200 -0
  97. package/dist/signal-ingestion/clustering.js +413 -0
  98. package/dist/signal-ingestion/config.d.ts +351 -0
  99. package/dist/signal-ingestion/config.js +587 -0
  100. package/dist/signal-ingestion/d1.d.ts +252 -0
  101. package/dist/signal-ingestion/d1.js +235 -0
  102. package/dist/signal-ingestion/errors.d.ts +73 -0
  103. package/dist/signal-ingestion/errors.js +108 -0
  104. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  105. package/dist/signal-ingestion/governance-events.js +189 -0
  106. package/dist/signal-ingestion/index.d.ts +35 -0
  107. package/dist/signal-ingestion/index.js +53 -0
  108. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  109. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  110. package/dist/signal-ingestion/registry.d.ts +40 -0
  111. package/dist/signal-ingestion/registry.js +137 -0
  112. package/dist/signal-ingestion/residency.d.ts +227 -0
  113. package/dist/signal-ingestion/residency.js +238 -0
  114. package/dist/signal-ingestion/significance.d.ts +554 -0
  115. package/dist/signal-ingestion/significance.js +555 -0
  116. package/dist/signal-ingestion/types.d.ts +191 -0
  117. package/dist/signal-ingestion/types.js +8 -0
  118. package/dist/substrate/drift-composition.d.ts +270 -0
  119. package/dist/substrate/drift-composition.js +306 -0
  120. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  121. package/dist/substrate/drift-tui-surface.js +102 -0
  122. package/dist/substrate/identity-class.d.ts +176 -0
  123. package/dist/substrate/identity-class.js +201 -0
  124. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  125. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  126. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  127. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  128. package/dist/tessellation/rule-registry.d.ts +269 -0
  129. package/dist/tessellation/rule-registry.js +92 -0
  130. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  131. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  132. package/dist/tessellation-admission.d.ts +162 -0
  133. package/dist/tessellation-admission.js +146 -0
  134. package/dist/tessellation-drift.d.ts +246 -0
  135. package/dist/tessellation-drift.js +250 -0
  136. package/dist/validate-config.js +13 -0
  137. package/dist/validate-issue.js +2 -2
  138. package/dist/variant/cardinality-activation.d.ts +126 -0
  139. package/dist/variant/cardinality-activation.js +101 -0
  140. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  141. package/dist/variant/deprecation-lifecycle.js +208 -0
  142. package/dist/variant/drift-extension.d.ts +136 -0
  143. package/dist/variant/drift-extension.js +164 -0
  144. package/dist/variant/engineering-review.d.ts +185 -0
  145. package/dist/variant/engineering-review.js +142 -0
  146. package/dist/variant/index.d.ts +32 -0
  147. package/dist/variant/index.js +32 -0
  148. package/dist/variant/inheritance-validator.d.ts +165 -0
  149. package/dist/variant/inheritance-validator.js +139 -0
  150. package/dist/variant/internal-adopter/index.d.ts +11 -0
  151. package/dist/variant/internal-adopter/index.js +10 -0
  152. package/dist/variant/internal-adopter/products.d.ts +156 -0
  153. package/dist/variant/internal-adopter/products.js +366 -0
  154. package/dist/variant-admission.d.ts +316 -0
  155. package/dist/variant-admission.js +247 -0
  156. package/package.json +10 -8
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Regime → DerivedGates composer (RFC-0022 §9 Phase 2).
3
+ *
4
+ * Reads the CompliancePosture's declared regime list, looks up each regime's
5
+ * DerivedGates values from spec/compliance/regime-mappings.yaml, composes
6
+ * them using "tightest-wins" semantics (RFC-0022 §6), applies adopter
7
+ * regimeOverrides per-regime (OQ-1), and finally applies operator-level
8
+ * derivedGates overrides last.
9
+ *
10
+ * Key design decisions:
11
+ * - YAML mapping file is read at call time (not module load) so tests can
12
+ * supply a custom mappings path without patching module state.
13
+ * - Tightest-wins is axis-specific: ordinal enums max, boolean OR, numeric max.
14
+ * - UnknownRegime is thrown when a declared regime ID has no entry in the
15
+ * mapping table (Phase 2 activation of the placeholder from Phase 1 errors.ts).
16
+ * - Adopter regimeOverrides (OQ-1) let per-regime control values be adjusted
17
+ * before the operator-override pass, enabling bespoke auditor interpretations
18
+ * without changing the canonical mapping table.
19
+ * - Operator derivedGates overrides (validated by the loader) always win last.
20
+ */
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { resolve } from 'node:path';
23
+ import { parse as parseYaml } from 'yaml';
24
+ import { BASELINE_DERIVED_GATES } from './types.js';
25
+ import { UnknownRegime } from './errors.js';
26
+ // ── Constants ─────────────────────────────────────────────────────────────
27
+ /**
28
+ * Path to the canonical regime-mappings.yaml, relative to the package root.
29
+ * The composer resolves this against the nearest ancestor directory containing
30
+ * the file (walking up from the orchestrator package).
31
+ *
32
+ * Tests may override via `ComposerOptions.mappingsPath`.
33
+ */
34
+ const DEFAULT_MAPPINGS_PATH = 'spec/compliance/regime-mappings.yaml';
35
+ // ── Ordinal helpers ───────────────────────────────────────────────────────
36
+ const SECRET_SCAN_ORDINAL = {
37
+ minimal: 0,
38
+ standard: 1,
39
+ strict: 2,
40
+ };
41
+ const REVIEWER_AUTHORITY_ORDINAL = {
42
+ open: 0,
43
+ allowlist: 1,
44
+ 'allowlist+role': 2,
45
+ };
46
+ // ── Mapping loader ────────────────────────────────────────────────────────
47
+ /**
48
+ * Load and parse the regime-mappings.yaml file.
49
+ */
50
+ function loadMappingsFile(mappingsPath) {
51
+ if (!existsSync(mappingsPath)) {
52
+ throw new Error(`Regime mappings file not found at '${mappingsPath}'. ` +
53
+ `Expected spec/compliance/regime-mappings.yaml in the repository root.`);
54
+ }
55
+ const content = readFileSync(mappingsPath, 'utf-8');
56
+ const parsed = parseYaml(content);
57
+ if (!parsed?.regimes || typeof parsed.regimes !== 'object') {
58
+ throw new Error(`Invalid regime-mappings.yaml: expected a top-level 'regimes' map at '${mappingsPath}'.`);
59
+ }
60
+ return parsed;
61
+ }
62
+ // ── Tightest-wins logic ───────────────────────────────────────────────────
63
+ /**
64
+ * Merge a single regime's DerivedGates into the accumulator using
65
+ * "tightest-wins" semantics per axis (RFC-0022 §6).
66
+ */
67
+ function mergeRegimeGates(accumulator, incoming) {
68
+ // databaseBranchPool: per-shard beats shared-with-rls
69
+ const databaseBranchPool = accumulator.databaseBranchPool === 'per-shard' || incoming.databaseBranchPool === 'per-shard'
70
+ ? 'per-shard'
71
+ : 'shared-with-rls';
72
+ // secretScanStrictness: ordinal max
73
+ const incomingOrdinal = SECRET_SCAN_ORDINAL[incoming.secretScanStrictness];
74
+ const accOrdinal = SECRET_SCAN_ORDINAL[accumulator.secretScanStrictness];
75
+ const secretScanStrictness = incomingOrdinal > accOrdinal ? incoming.secretScanStrictness : accumulator.secretScanStrictness;
76
+ // attestationRequired: boolean OR
77
+ const attestationRequired = accumulator.attestationRequired || incoming.attestationRequired;
78
+ // auditRetentionDays: numeric max
79
+ const auditRetentionDays = Math.max(accumulator.auditRetentionDays, incoming.auditRetentionDays);
80
+ // reviewerAuthorityModel: ordinal max
81
+ const incomingReviewerOrdinal = REVIEWER_AUTHORITY_ORDINAL[incoming.reviewerAuthorityModel];
82
+ const accReviewerOrdinal = REVIEWER_AUTHORITY_ORDINAL[accumulator.reviewerAuthorityModel];
83
+ const reviewerAuthorityModel = incomingReviewerOrdinal > accReviewerOrdinal
84
+ ? incoming.reviewerAuthorityModel
85
+ : accumulator.reviewerAuthorityModel;
86
+ return {
87
+ databaseBranchPool,
88
+ secretScanStrictness,
89
+ attestationRequired,
90
+ auditRetentionDays,
91
+ reviewerAuthorityModel,
92
+ };
93
+ }
94
+ // ── Adopter regimeOverrides application ──────────────────────────────────
95
+ /**
96
+ * Apply adopter-supplied per-regime overrides to a regime's base DerivedGates.
97
+ *
98
+ * Per OQ-1: adopters may supply `compliance.yaml regimeOverrides.<regimeId>`
99
+ * to adjust per-regime control values before the tightest-wins composition
100
+ * pass. This lets adopters document bespoke auditor interpretations
101
+ * (e.g., "our HIPAA auditor accepts shared-with-rls with X controls evidence").
102
+ *
103
+ * The override entry uses the same field names as DerivedGates; `_notes` is
104
+ * carried for audit traceability but not validated here (the loader handles
105
+ * that for operator-level overrides; adopter regimeOverrides are advisory).
106
+ */
107
+ function applyRegimeOverrides(base, override) {
108
+ return {
109
+ databaseBranchPool: override.databaseBranchPool ?? base.databaseBranchPool,
110
+ secretScanStrictness: override.secretScanStrictness ?? base.secretScanStrictness,
111
+ attestationRequired: override.attestationRequired ?? base.attestationRequired,
112
+ auditRetentionDays: override.auditRetentionDays ?? base.auditRetentionDays,
113
+ reviewerAuthorityModel: override.reviewerAuthorityModel ?? base.reviewerAuthorityModel,
114
+ };
115
+ }
116
+ // ── Operator override application ────────────────────────────────────────
117
+ /**
118
+ * Apply operator-level derivedGates overrides as the final pass.
119
+ *
120
+ * Per RFC-0022 §6: operator overrides always win. The loader already validated
121
+ * that each overridden field has a non-empty `_notes` entry; here we just
122
+ * apply the values.
123
+ */
124
+ function applyOperatorOverrides(composed, overrides) {
125
+ return {
126
+ databaseBranchPool: overrides.databaseBranchPool ?? composed.databaseBranchPool,
127
+ secretScanStrictness: overrides.secretScanStrictness ?? composed.secretScanStrictness,
128
+ attestationRequired: overrides.attestationRequired ?? composed.attestationRequired,
129
+ auditRetentionDays: overrides.auditRetentionDays ?? composed.auditRetentionDays,
130
+ reviewerAuthorityModel: overrides.reviewerAuthorityModel ?? composed.reviewerAuthorityModel,
131
+ };
132
+ }
133
+ /**
134
+ * Compose a loaded CompliancePosture into a concrete DerivedGates value.
135
+ *
136
+ * Algorithm (RFC-0022 §6):
137
+ * 1. Start from BASELINE_DERIVED_GATES.
138
+ * 2. For each declared regime, look up its entry in regime-mappings.yaml.
139
+ * Throw UnknownRegime if not found.
140
+ * 3. Apply adopter regimeOverrides (per-regime) before composition (OQ-1).
141
+ * 4. Merge each regime's gates into the accumulator via tightest-wins.
142
+ * 5. Apply operator derivedGates overrides last — they always win.
143
+ *
144
+ * @param posture - A single CompliancePosture (element from the loader's list).
145
+ * @param options - Optional override for the mappings file path.
146
+ * @returns CompositionResult with the resolved DerivedGates.
147
+ * @throws UnknownRegime if any declared regime ID is not in the mapping table.
148
+ */
149
+ export function composePostureDerivedGates(posture, options = {}) {
150
+ // Resolve mappings file path
151
+ let mappingsPath;
152
+ if (options.mappingsPath) {
153
+ mappingsPath = options.mappingsPath;
154
+ }
155
+ else {
156
+ const repoRoot = options.repoRoot ?? findRepoRoot();
157
+ mappingsPath = resolve(repoRoot, DEFAULT_MAPPINGS_PATH);
158
+ }
159
+ const mappingsFile = loadMappingsFile(mappingsPath);
160
+ const regimes = posture.spec.regimes;
161
+ // Baseline: no regimes declared → return baseline
162
+ if (regimes.length === 0) {
163
+ return {
164
+ derivedGates: { ...BASELINE_DERIVED_GATES },
165
+ composedRegimes: [],
166
+ regimesWithAdopterOverrides: [],
167
+ operatorOverriddenFields: [],
168
+ };
169
+ }
170
+ // Extract adopter regimeOverrides if present (OQ-1)
171
+ // The CompliancePosture spec does not currently have a `regimeOverrides` field
172
+ // at the TypeScript type level (it's an OQ-1 addition); we read it via type
173
+ // assertion since the loader passes through unknown fields from YAML.
174
+ const regimeOverrides = (posture.spec['regimeOverrides'] ??
175
+ {});
176
+ let accumulator = { ...BASELINE_DERIVED_GATES };
177
+ const composedRegimes = [];
178
+ const regimesWithAdopterOverrides = [];
179
+ for (const regime of regimes) {
180
+ const regimeId = regime.id;
181
+ const mappingEntry = mappingsFile.regimes[regimeId];
182
+ if (!mappingEntry) {
183
+ throw new UnknownRegime(regimeId);
184
+ }
185
+ // Apply adopter per-regime override before tightest-wins composition (OQ-1)
186
+ let effectiveEntry = mappingEntry;
187
+ if (regimeOverrides[regimeId]) {
188
+ effectiveEntry = applyRegimeOverrides(mappingEntry, regimeOverrides[regimeId]);
189
+ regimesWithAdopterOverrides.push(regimeId);
190
+ }
191
+ accumulator = mergeRegimeGates(accumulator, effectiveEntry);
192
+ composedRegimes.push(regimeId);
193
+ }
194
+ // Apply operator-level derivedGates overrides last (always win)
195
+ const operatorOverrides = posture.spec.derivedGates;
196
+ let finalGates = accumulator;
197
+ const operatorOverriddenFields = [];
198
+ if (operatorOverrides) {
199
+ const overrideKeys = [
200
+ 'databaseBranchPool',
201
+ 'secretScanStrictness',
202
+ 'attestationRequired',
203
+ 'auditRetentionDays',
204
+ 'reviewerAuthorityModel',
205
+ ];
206
+ for (const key of overrideKeys) {
207
+ if (key in operatorOverrides && operatorOverrides[key] !== undefined) {
208
+ operatorOverriddenFields.push(key);
209
+ }
210
+ }
211
+ finalGates = applyOperatorOverrides(accumulator, operatorOverrides);
212
+ }
213
+ return {
214
+ derivedGates: finalGates,
215
+ composedRegimes,
216
+ regimesWithAdopterOverrides,
217
+ operatorOverriddenFields,
218
+ };
219
+ }
220
+ // ── Repo root detection ───────────────────────────────────────────────────
221
+ /**
222
+ * Walk up from the current file's directory to find the repository root.
223
+ * Detected by the presence of spec/compliance/regime-mappings.yaml or package.json
224
+ * with a workspace marker.
225
+ *
226
+ * Falls back to `process.cwd()` if the walk exhausts without finding the root.
227
+ */
228
+ function findRepoRoot() {
229
+ // Walk up from the orchestrator package directory
230
+ try {
231
+ return findMappingsFileDir(new URL('.', import.meta.url).pathname);
232
+ }
233
+ catch {
234
+ // Fallback: walk up from cwd
235
+ return findMappingsFileDir(process.cwd());
236
+ }
237
+ }
238
+ /**
239
+ * Walk up from baseDir to find a directory containing
240
+ * `spec/compliance/regime-mappings.yaml`.
241
+ */
242
+ function findMappingsFileDir(baseDir) {
243
+ let dir = baseDir;
244
+ for (let i = 0; i < 10; i++) {
245
+ const candidate = resolve(dir, DEFAULT_MAPPINGS_PATH);
246
+ if (existsSync(candidate)) {
247
+ return dir;
248
+ }
249
+ const parent = resolve(dir, '..');
250
+ if (parent === dir) {
251
+ break;
252
+ }
253
+ dir = parent;
254
+ }
255
+ // Return current directory as last resort; loadMappingsFile will give a clear error
256
+ return process.cwd();
257
+ }
258
+ //# sourceMappingURL=composer.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Custom error classes for the CompliancePosture loader (RFC-0022 §9 Phase 1).
3
+ *
4
+ * These errors signal load-time validation failures that the loader surface
5
+ * propagates to callers so they can provide actionable operator messages.
6
+ */
7
+ /**
8
+ * Thrown when a declared regime is missing required attestation metadata.
9
+ *
10
+ * Per RFC-0022 §5: compliance regimes are legal claims; the framework MUST NOT
11
+ * let an operator silently declare HIPAA coverage without recording who said so.
12
+ * A CompliancePosture whose regimes lack `attestedBy` or `attestedAt` fails to
13
+ * load with this error.
14
+ */
15
+ export declare class MissingComplianceAttestation extends Error {
16
+ readonly regimeId: string;
17
+ readonly missingField: 'attestedBy' | 'attestedAt';
18
+ constructor(regimeId: string, missingField: 'attestedBy' | 'attestedAt');
19
+ }
20
+ /**
21
+ * Thrown when a `derivedGates` override is present without a corresponding
22
+ * `_notes` entry (or the notes entry is empty).
23
+ *
24
+ * Per RFC-0022 §6 (OQ-2): every operator override on a derived gate carries an
25
+ * `attestedNotes` entry; the framework refuses to load a posture with `derivedGates`
26
+ * overrides whose notes are missing or empty. Forcing explicit rationale makes
27
+ * overrides audit-traceable.
28
+ */
29
+ export declare class MissingDerivedGateOverrideNotes extends Error {
30
+ readonly gateField: string;
31
+ constructor(gateField: string);
32
+ }
33
+ /**
34
+ * Thrown when a regime `id` is declared but not present in the framework's
35
+ * canonical regime registry (spec/compliance/regime-mappings.yaml).
36
+ *
37
+ * Phase 1 note: the regime-mappings.yaml file ships in Phase 2 (AISDLC-323).
38
+ * In Phase 1, this error is reserved for future use when the composer validates
39
+ * declared regime IDs against the mapping table. The loader does NOT throw this
40
+ * error in Phase 1 — unknown regime IDs are loaded and passed through unchanged
41
+ * (the composer in Phase 2 will validate them against the mapping).
42
+ */
43
+ export declare class UnknownRegime extends Error {
44
+ readonly regimeId: string;
45
+ constructor(regimeId: string);
46
+ }
47
+ /**
48
+ * Thrown when .ai-sdlc/compliance.yaml exists but fails JSON Schema validation
49
+ * against spec/schemas/compliance-posture.v1.schema.json.
50
+ *
51
+ * Distinct from MissingComplianceAttestation and MissingDerivedGateOverrideNotes
52
+ * which are semantic validations layered on top of schema validation.
53
+ */
54
+ export declare class CompliancePostureValidationError extends Error {
55
+ readonly validationErrors: Array<{
56
+ path: string;
57
+ message: string;
58
+ }>;
59
+ constructor(validationErrors: Array<{
60
+ path: string;
61
+ message: string;
62
+ }>);
63
+ }
64
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Custom error classes for the CompliancePosture loader (RFC-0022 §9 Phase 1).
3
+ *
4
+ * These errors signal load-time validation failures that the loader surface
5
+ * propagates to callers so they can provide actionable operator messages.
6
+ */
7
+ /**
8
+ * Thrown when a declared regime is missing required attestation metadata.
9
+ *
10
+ * Per RFC-0022 §5: compliance regimes are legal claims; the framework MUST NOT
11
+ * let an operator silently declare HIPAA coverage without recording who said so.
12
+ * A CompliancePosture whose regimes lack `attestedBy` or `attestedAt` fails to
13
+ * load with this error.
14
+ */
15
+ export class MissingComplianceAttestation extends Error {
16
+ regimeId;
17
+ missingField;
18
+ constructor(regimeId, missingField) {
19
+ super(`CompliancePosture regime '${regimeId}' is missing required attestation field '${missingField}'. ` +
20
+ `Compliance regimes are legal claims — the framework requires explicit operator/legal sign-off. ` +
21
+ `Add '${missingField}' to the regime declaration in .ai-sdlc/compliance.yaml.`);
22
+ this.name = 'MissingComplianceAttestation';
23
+ this.regimeId = regimeId;
24
+ this.missingField = missingField;
25
+ }
26
+ }
27
+ /**
28
+ * Thrown when a `derivedGates` override is present without a corresponding
29
+ * `_notes` entry (or the notes entry is empty).
30
+ *
31
+ * Per RFC-0022 §6 (OQ-2): every operator override on a derived gate carries an
32
+ * `attestedNotes` entry; the framework refuses to load a posture with `derivedGates`
33
+ * overrides whose notes are missing or empty. Forcing explicit rationale makes
34
+ * overrides audit-traceable.
35
+ */
36
+ export class MissingDerivedGateOverrideNotes extends Error {
37
+ gateField;
38
+ constructor(gateField) {
39
+ super(`CompliancePosture 'derivedGates.${gateField}' is overridden but 'derivedGates._notes.${gateField}' ` +
40
+ `is missing or empty. All derived-gate overrides require a non-empty rationale in _notes ` +
41
+ `for audit traceability (RFC-0022 §6 OQ-2). ` +
42
+ `Add a non-empty notes entry: derivedGates._notes.${gateField}: "<rationale>".`);
43
+ this.name = 'MissingDerivedGateOverrideNotes';
44
+ this.gateField = gateField;
45
+ }
46
+ }
47
+ /**
48
+ * Thrown when a regime `id` is declared but not present in the framework's
49
+ * canonical regime registry (spec/compliance/regime-mappings.yaml).
50
+ *
51
+ * Phase 1 note: the regime-mappings.yaml file ships in Phase 2 (AISDLC-323).
52
+ * In Phase 1, this error is reserved for future use when the composer validates
53
+ * declared regime IDs against the mapping table. The loader does NOT throw this
54
+ * error in Phase 1 — unknown regime IDs are loaded and passed through unchanged
55
+ * (the composer in Phase 2 will validate them against the mapping).
56
+ */
57
+ export class UnknownRegime extends Error {
58
+ regimeId;
59
+ constructor(regimeId) {
60
+ super(`CompliancePosture declares regime '${regimeId}' which is not in the framework's canonical ` +
61
+ `regime registry. Known regimes are listed in spec/compliance/regime-mappings.yaml. ` +
62
+ `If this is a custom/adopter-specific regime, it will not derive framework gate defaults ` +
63
+ `automatically (the composer only handles known regimes).`);
64
+ this.name = 'UnknownRegime';
65
+ this.regimeId = regimeId;
66
+ }
67
+ }
68
+ /**
69
+ * Thrown when .ai-sdlc/compliance.yaml exists but fails JSON Schema validation
70
+ * against spec/schemas/compliance-posture.v1.schema.json.
71
+ *
72
+ * Distinct from MissingComplianceAttestation and MissingDerivedGateOverrideNotes
73
+ * which are semantic validations layered on top of schema validation.
74
+ */
75
+ export class CompliancePostureValidationError extends Error {
76
+ validationErrors;
77
+ constructor(validationErrors) {
78
+ const summary = validationErrors.map((e) => `${e.path}: ${e.message}`).join('; ');
79
+ super(`CompliancePosture at .ai-sdlc/compliance.yaml failed schema validation: ${summary}. ` +
80
+ `See spec/schemas/compliance-posture.v1.schema.json for the required shape.`);
81
+ this.name = 'CompliancePostureValidationError';
82
+ this.validationErrors = validationErrors;
83
+ }
84
+ }
85
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CompliancePosture loader (RFC-0022 §9 Phase 1).
3
+ *
4
+ * Reads `.ai-sdlc/compliance.yaml`, validates against
5
+ * `spec/schemas/compliance-posture.v1.schema.json`, enforces semantic rules
6
+ * (OQ-2 override-notes, OQ-6 list return type), and returns the parsed posture.
7
+ *
8
+ * Key design decisions:
9
+ * - Returns CompliancePosture[] (single-element list in v1) per OQ-6 so v2
10
+ * multi-tenant composition is additive — not a breaking API change.
11
+ * - Missing manifest → BASELINE_POSTURE returned (no error; AC #6).
12
+ * - Regime missing attestedBy / attestedAt → MissingComplianceAttestation thrown.
13
+ * - DerivedGates override without _notes entry → MissingDerivedGateOverrideNotes thrown.
14
+ * - Schema invalid → CompliancePostureValidationError thrown.
15
+ */
16
+ import type { CompliancePosture } from './types.js';
17
+ /**
18
+ * Default path within the project root for the compliance manifest.
19
+ * Overridable by passing `manifestPath` to `loadCompliancePosture`.
20
+ */
21
+ export declare const DEFAULT_COMPLIANCE_MANIFEST_PATH = ".ai-sdlc/compliance.yaml";
22
+ /**
23
+ * Options for `loadCompliancePosture`.
24
+ */
25
+ export interface LoadCompliancePostureOptions {
26
+ /**
27
+ * Absolute path to the directory containing `.ai-sdlc/compliance.yaml`.
28
+ * Defaults to `process.cwd()`.
29
+ */
30
+ projectRoot?: string;
31
+ /**
32
+ * Override the manifest file path (relative to `projectRoot`).
33
+ * Defaults to `.ai-sdlc/compliance.yaml`.
34
+ */
35
+ manifestPath?: string;
36
+ }
37
+ /**
38
+ * Load and validate the project's CompliancePosture from disk.
39
+ *
40
+ * Returns a single-element `CompliancePosture[]` per OQ-6 (v2 forward-compat:
41
+ * v2 multi-tenant will return multiple elements; the single-element v1 shape
42
+ * is already a list so callers compose against `posture[0].spec.derivedGates`
43
+ * today and multi-posture composition in v2 is purely additive).
44
+ *
45
+ * Semantics:
46
+ * - Missing manifest → `[BASELINE_POSTURE]` (AC #6: existing projects with
47
+ * no compliance.yaml get the "(none declared)" baseline; no gate changes).
48
+ * - Valid manifest → `[parsed_posture]`.
49
+ * - Invalid manifest → throws one of the compliance error classes.
50
+ */
51
+ export declare function loadCompliancePosture(options?: LoadCompliancePostureOptions): CompliancePosture[];
52
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * CompliancePosture loader (RFC-0022 §9 Phase 1).
3
+ *
4
+ * Reads `.ai-sdlc/compliance.yaml`, validates against
5
+ * `spec/schemas/compliance-posture.v1.schema.json`, enforces semantic rules
6
+ * (OQ-2 override-notes, OQ-6 list return type), and returns the parsed posture.
7
+ *
8
+ * Key design decisions:
9
+ * - Returns CompliancePosture[] (single-element list in v1) per OQ-6 so v2
10
+ * multi-tenant composition is additive — not a breaking API change.
11
+ * - Missing manifest → BASELINE_POSTURE returned (no error; AC #6).
12
+ * - Regime missing attestedBy / attestedAt → MissingComplianceAttestation thrown.
13
+ * - DerivedGates override without _notes entry → MissingDerivedGateOverrideNotes thrown.
14
+ * - Schema invalid → CompliancePostureValidationError thrown.
15
+ */
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { parse as parseYaml } from 'yaml';
19
+ import { validateResource } from '@ai-sdlc/reference';
20
+ import { BASELINE_POSTURE } from './types.js';
21
+ import { CompliancePostureValidationError, MissingComplianceAttestation, MissingDerivedGateOverrideNotes, } from './errors.js';
22
+ // ── Constants ────────────────────────────────────────────────────────────
23
+ /**
24
+ * Default path within the project root for the compliance manifest.
25
+ * Overridable by passing `manifestPath` to `loadCompliancePosture`.
26
+ */
27
+ export const DEFAULT_COMPLIANCE_MANIFEST_PATH = '.ai-sdlc/compliance.yaml';
28
+ /**
29
+ * Gate fields that are present in DerivedGates (excluding _notes itself).
30
+ * Used to enumerate override fields for OQ-2 validation.
31
+ */
32
+ const DERIVED_GATE_FIELDS = [
33
+ 'databaseBranchPool',
34
+ 'secretScanStrictness',
35
+ 'attestationRequired',
36
+ 'auditRetentionDays',
37
+ 'reviewerAuthorityModel',
38
+ ];
39
+ /**
40
+ * Load and validate the project's CompliancePosture from disk.
41
+ *
42
+ * Returns a single-element `CompliancePosture[]` per OQ-6 (v2 forward-compat:
43
+ * v2 multi-tenant will return multiple elements; the single-element v1 shape
44
+ * is already a list so callers compose against `posture[0].spec.derivedGates`
45
+ * today and multi-posture composition in v2 is purely additive).
46
+ *
47
+ * Semantics:
48
+ * - Missing manifest → `[BASELINE_POSTURE]` (AC #6: existing projects with
49
+ * no compliance.yaml get the "(none declared)" baseline; no gate changes).
50
+ * - Valid manifest → `[parsed_posture]`.
51
+ * - Invalid manifest → throws one of the compliance error classes.
52
+ */
53
+ export function loadCompliancePosture(options = {}) {
54
+ const projectRoot = options.projectRoot ?? process.cwd();
55
+ const relPath = options.manifestPath ?? DEFAULT_COMPLIANCE_MANIFEST_PATH;
56
+ const manifestPath = resolve(projectRoot, relPath);
57
+ // AC #6: missing manifest → baseline posture (no error)
58
+ if (!existsSync(manifestPath)) {
59
+ return [BASELINE_POSTURE];
60
+ }
61
+ // Parse YAML
62
+ let raw;
63
+ try {
64
+ const content = readFileSync(manifestPath, 'utf-8');
65
+ raw = parseYaml(content);
66
+ }
67
+ catch (err) {
68
+ throw new CompliancePostureValidationError([
69
+ { path: '/', message: `Failed to parse YAML: ${err.message}` },
70
+ ]);
71
+ }
72
+ // JSON Schema validation via reference package
73
+ // validateResource infers kind from the document and validates against the registered schema.
74
+ // CompliancePosture is in SCHEMA_FILES so it will NOT be skipped.
75
+ const result = validateResource(raw);
76
+ if (result.skipped) {
77
+ // This should not happen since CompliancePosture is a registered kind —
78
+ // guard defensively in case the manifest has an unexpected kind field.
79
+ throw new CompliancePostureValidationError([
80
+ {
81
+ path: '/kind',
82
+ message: `Expected kind 'CompliancePosture' but got kind '${raw?.kind}'`,
83
+ },
84
+ ]);
85
+ }
86
+ if (!result.valid) {
87
+ throw new CompliancePostureValidationError((result.errors ?? []).map((e) => ({ path: e.path, message: e.message })));
88
+ }
89
+ const posture = result.data;
90
+ // Semantic validation #1 — regime attestation (OQ-2 / AC #5)
91
+ for (const regime of posture.spec.regimes) {
92
+ if (!regime.attestedBy || regime.attestedBy.trim() === '') {
93
+ throw new MissingComplianceAttestation(regime.id, 'attestedBy');
94
+ }
95
+ if (!regime.attestedAt || regime.attestedAt.trim() === '') {
96
+ throw new MissingComplianceAttestation(regime.id, 'attestedAt');
97
+ }
98
+ }
99
+ // Semantic validation #2 — derivedGates override notes (OQ-2 / AC #5)
100
+ if (posture.spec.derivedGates) {
101
+ validateDerivedGatesOverrideNotes(posture.spec.derivedGates);
102
+ }
103
+ return [posture];
104
+ }
105
+ /**
106
+ * Validate that every overridden derived gate field has a corresponding
107
+ * non-empty `_notes` entry. Throws `MissingDerivedGateOverrideNotes` on first
108
+ * violation (fail-fast — single validation error at a time for clear messaging).
109
+ */
110
+ function validateDerivedGatesOverrideNotes(overrides) {
111
+ const notes = overrides._notes ?? {};
112
+ for (const field of DERIVED_GATE_FIELDS) {
113
+ // Field has been overridden if the key exists (even if value is undefined)
114
+ if (!(field in overrides)) {
115
+ continue;
116
+ }
117
+ // The field is present — check for a non-empty _notes entry
118
+ const note = notes[field];
119
+ if (note === undefined || note.trim() === '') {
120
+ throw new MissingDerivedGateOverrideNotes(field);
121
+ }
122
+ }
123
+ }
124
+ //# sourceMappingURL=loader.js.map