@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.
- package/dist/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +112 -1
- package/dist/admission-composite.js +85 -4
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +64 -13
- package/dist/admission-score.d.ts +30 -0
- package/dist/admission-score.js +4 -1
- 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/init-features.d.ts +301 -2
- package/dist/cli/commands/init-features.js +634 -7
- package/dist/cli/commands/init-templates.d.ts +198 -1
- package/dist/cli/commands/init-templates.js +943 -1
- package/dist/cli/commands/init.d.ts +45 -0
- package/dist/cli/commands/init.js +147 -5
- 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/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 +171 -30
- 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/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 +173 -13
- package/dist/runtime/attestations.js +252 -40
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- 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 +10 -8
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 3. The branch-protection helper (applyBranchProtection) including the
|
|
12
12
|
* `--dry-run` JSON-print path required by AC #6.
|
|
13
13
|
* 4. The "next steps" summary printed at the end of init (AC #5).
|
|
14
|
+
* 5. The compliance-posture wizard step (RFC-0022 §7 / AISDLC-324).
|
|
14
15
|
*
|
|
15
16
|
* Test surface: every public function takes a small options bag with
|
|
16
17
|
* injectable side-effect adapters (prompter, writeFile, runCommand) so the
|
|
@@ -19,23 +20,538 @@
|
|
|
19
20
|
* the real adapters.
|
|
20
21
|
*/
|
|
21
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
-
import { join, dirname } from 'node:path';
|
|
23
|
-
import { execFileSync } from 'node:child_process';
|
|
24
|
-
import {
|
|
23
|
+
import { join, dirname, basename } from 'node:path';
|
|
24
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
25
|
+
import { BASELINE_DERIVED_GATES } from '../../compliance/types.js';
|
|
26
|
+
import { ATTESTATION_TEMPLATES, BASELINE_WORKFLOW_TEMPLATES, CLASSIFIER_TEMPLATES, DOR_TEMPLATES, HUSKY_PREPUSH_SIGN_SNIPPET, SIGNAL_INGESTION_TEMPLATES, WORKFLOWS_TEMPLATES, } from './init-templates.js';
|
|
27
|
+
/**
|
|
28
|
+
* Canonical list of regulatory regimes presented in the compliance wizard.
|
|
29
|
+
* Mirrors the entries in spec/compliance/regime-mappings.yaml.
|
|
30
|
+
*/
|
|
31
|
+
export const COMPLIANCE_REGIME_CHOICES = [
|
|
32
|
+
{ value: 'SOC2-T2', label: 'SOC2 Type 2 (Service Organization Control)' },
|
|
33
|
+
{ value: 'HIPAA', label: 'HIPAA (Health Insurance Portability and Accountability Act)' },
|
|
34
|
+
{ value: 'PCI-DSS-L1', label: 'PCI-DSS Level 1 (Payment Card Industry Data Security Standard)' },
|
|
35
|
+
{ value: 'GDPR', label: 'GDPR (General Data Protection Regulation; EU)' },
|
|
36
|
+
{ value: 'FedRAMP-Moderate', label: 'FedRAMP Moderate (US federal)' },
|
|
37
|
+
{ value: 'ISO-27001:2022', label: 'ISO 27001:2022' },
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Inline regime → DerivedGates mapping for the init wizard.
|
|
41
|
+
* Mirrors spec/compliance/regime-mappings.yaml so the wizard works in
|
|
42
|
+
* installed-package contexts (where the YAML file is not bundled).
|
|
43
|
+
*
|
|
44
|
+
* NOTE: This data is intentionally duplicated from regime-mappings.yaml to
|
|
45
|
+
* avoid a runtime file-read dependency in the init wizard. When regime-mappings.yaml
|
|
46
|
+
* is updated, this table MUST be updated in sync. The RFC-0022 §13 Q7 PR-template
|
|
47
|
+
* compliance-impact checkbox enforces this via reviewer check.
|
|
48
|
+
*/
|
|
49
|
+
const INIT_WIZARD_REGIME_GATES = {
|
|
50
|
+
'SOC2-T2': {
|
|
51
|
+
databaseBranchPool: 'per-shard',
|
|
52
|
+
secretScanStrictness: 'strict',
|
|
53
|
+
attestationRequired: true,
|
|
54
|
+
auditRetentionDays: 2555,
|
|
55
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
56
|
+
},
|
|
57
|
+
HIPAA: {
|
|
58
|
+
databaseBranchPool: 'per-shard',
|
|
59
|
+
secretScanStrictness: 'strict',
|
|
60
|
+
attestationRequired: true,
|
|
61
|
+
auditRetentionDays: 2190,
|
|
62
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
63
|
+
},
|
|
64
|
+
'PCI-DSS-L1': {
|
|
65
|
+
databaseBranchPool: 'per-shard',
|
|
66
|
+
secretScanStrictness: 'strict',
|
|
67
|
+
attestationRequired: true,
|
|
68
|
+
auditRetentionDays: 365,
|
|
69
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
70
|
+
},
|
|
71
|
+
GDPR: {
|
|
72
|
+
databaseBranchPool: 'per-shard',
|
|
73
|
+
secretScanStrictness: 'standard',
|
|
74
|
+
attestationRequired: true,
|
|
75
|
+
auditRetentionDays: 365,
|
|
76
|
+
reviewerAuthorityModel: 'allowlist',
|
|
77
|
+
},
|
|
78
|
+
'FedRAMP-Moderate': {
|
|
79
|
+
databaseBranchPool: 'per-shard',
|
|
80
|
+
secretScanStrictness: 'strict',
|
|
81
|
+
attestationRequired: true,
|
|
82
|
+
auditRetentionDays: 1095,
|
|
83
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
84
|
+
},
|
|
85
|
+
'ISO-27001:2022': {
|
|
86
|
+
databaseBranchPool: 'per-shard',
|
|
87
|
+
secretScanStrictness: 'strict',
|
|
88
|
+
attestationRequired: true,
|
|
89
|
+
auditRetentionDays: 365,
|
|
90
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Ordinal scales for tightest-wins composition.
|
|
95
|
+
*/
|
|
96
|
+
const SECRET_SCAN_ORDINAL = {
|
|
97
|
+
minimal: 0,
|
|
98
|
+
standard: 1,
|
|
99
|
+
strict: 2,
|
|
100
|
+
};
|
|
101
|
+
const REVIEWER_AUTHORITY_ORDINAL = {
|
|
102
|
+
open: 0,
|
|
103
|
+
allowlist: 1,
|
|
104
|
+
'allowlist+role': 2,
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Compute DerivedGates from a list of regime IDs using tightest-wins semantics.
|
|
108
|
+
* Unknown regime IDs are skipped (the wizard's informational display degrades
|
|
109
|
+
* gracefully rather than throwing).
|
|
110
|
+
*
|
|
111
|
+
* This is a lightweight inline computation used exclusively by the init wizard.
|
|
112
|
+
* The canonical composer (orchestrator/src/compliance/composer.ts) is used for
|
|
113
|
+
* all post-init runtime gate resolution.
|
|
114
|
+
*/
|
|
115
|
+
export function computeInitWizardDerivedGates(regimes) {
|
|
116
|
+
if (regimes.length === 0)
|
|
117
|
+
return { ...BASELINE_DERIVED_GATES };
|
|
118
|
+
let accumulated = { ...BASELINE_DERIVED_GATES };
|
|
119
|
+
for (const regimeId of regimes) {
|
|
120
|
+
const entry = INIT_WIZARD_REGIME_GATES[regimeId];
|
|
121
|
+
if (!entry)
|
|
122
|
+
continue; // Unknown regime — skip gracefully
|
|
123
|
+
// databaseBranchPool: per-shard beats shared-with-rls
|
|
124
|
+
if (entry.databaseBranchPool === 'per-shard') {
|
|
125
|
+
accumulated = { ...accumulated, databaseBranchPool: 'per-shard' };
|
|
126
|
+
}
|
|
127
|
+
// secretScanStrictness: ordinal max
|
|
128
|
+
if (SECRET_SCAN_ORDINAL[entry.secretScanStrictness] >
|
|
129
|
+
SECRET_SCAN_ORDINAL[accumulated.secretScanStrictness]) {
|
|
130
|
+
accumulated = { ...accumulated, secretScanStrictness: entry.secretScanStrictness };
|
|
131
|
+
}
|
|
132
|
+
// attestationRequired: boolean OR
|
|
133
|
+
if (entry.attestationRequired) {
|
|
134
|
+
accumulated = { ...accumulated, attestationRequired: true };
|
|
135
|
+
}
|
|
136
|
+
// auditRetentionDays: max
|
|
137
|
+
if (entry.auditRetentionDays > accumulated.auditRetentionDays) {
|
|
138
|
+
accumulated = { ...accumulated, auditRetentionDays: entry.auditRetentionDays };
|
|
139
|
+
}
|
|
140
|
+
// reviewerAuthorityModel: ordinal max
|
|
141
|
+
if (REVIEWER_AUTHORITY_ORDINAL[entry.reviewerAuthorityModel] >
|
|
142
|
+
REVIEWER_AUTHORITY_ORDINAL[accumulated.reviewerAuthorityModel]) {
|
|
143
|
+
accumulated = { ...accumulated, reviewerAuthorityModel: entry.reviewerAuthorityModel };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return accumulated;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Apply the RFC-0009 §8.7 / OQ-11 trigger checklist on top of regime-derived
|
|
150
|
+
* `DerivedGates`. Upgrades `databaseBranchPool` from `shared-with-rls` to
|
|
151
|
+
* `per-shard` when ANY of the three triggers fires:
|
|
152
|
+
*
|
|
153
|
+
* - Regulatory: already reflected in `inputGates.databaseBranchPool === 'per-shard'`
|
|
154
|
+
* (set by `computeInitWizardDerivedGates` when a per-shard-forcing regime was
|
|
155
|
+
* declared). When this is the case, 'regulatory' is recorded in `triggers`.
|
|
156
|
+
* - Customer contract: `answers.customerContract === true`.
|
|
157
|
+
* - Operator security review: `answers.operatorSecurityReview === true`.
|
|
158
|
+
*
|
|
159
|
+
* The function is monotonic — once `databaseBranchPool` is `per-shard`, it stays
|
|
160
|
+
* `per-shard`. Other DerivedGates fields are passed through unchanged.
|
|
161
|
+
*
|
|
162
|
+
* Pure function — no I/O, no side-effects. The wizard wires it into
|
|
163
|
+
* `runComplianceStep`; tests pin behavior directly.
|
|
164
|
+
*/
|
|
165
|
+
export function applyOq11TriggerChecklistUpgrade(inputGates, answers) {
|
|
166
|
+
const triggers = [];
|
|
167
|
+
// Trigger 1 (regulatory) — surfaces only when the gate was already
|
|
168
|
+
// upgraded upstream by a per-shard-forcing regime declaration.
|
|
169
|
+
if (inputGates.databaseBranchPool === 'per-shard') {
|
|
170
|
+
triggers.push('regulatory');
|
|
171
|
+
}
|
|
172
|
+
if (answers.customerContract) {
|
|
173
|
+
triggers.push('customer-contract');
|
|
174
|
+
}
|
|
175
|
+
if (answers.operatorSecurityReview) {
|
|
176
|
+
triggers.push('operator-security-review');
|
|
177
|
+
}
|
|
178
|
+
const upgraded = triggers.length > 0;
|
|
179
|
+
return {
|
|
180
|
+
derivedGates: upgraded ? { ...inputGates, databaseBranchPool: 'per-shard' } : { ...inputGates },
|
|
181
|
+
triggers,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Human-readable line for each trigger kind, shown in the wizard summary so
|
|
186
|
+
* the operator can see WHY the framework selected `per-shard`.
|
|
187
|
+
*/
|
|
188
|
+
export function describeOq11Trigger(kind) {
|
|
189
|
+
switch (kind) {
|
|
190
|
+
case 'regulatory':
|
|
191
|
+
return 'regulatory regime declared in .ai-sdlc/compliance.yaml (RFC-0022 derivedGates)';
|
|
192
|
+
case 'customer-contract':
|
|
193
|
+
return 'customer contract requires tenant physical isolation (operator-declared)';
|
|
194
|
+
case 'operator-security-review':
|
|
195
|
+
return 'operator security review identified a risk RLS cannot mitigate (operator-declared)';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build the .ai-sdlc/compliance.yaml content for a given compliance declaration.
|
|
200
|
+
*
|
|
201
|
+
* The written file contains the declared `spec.regimes` with attestation metadata.
|
|
202
|
+
* The computed `derivedGates` are added as YAML comments (read-only reference)
|
|
203
|
+
* so the loader continues to compute them from regimes (not from spec.derivedGates,
|
|
204
|
+
* which is the operator-override field requiring _notes for each entry).
|
|
205
|
+
*
|
|
206
|
+
* Pure function — no filesystem side-effects, fully testable.
|
|
207
|
+
*/
|
|
208
|
+
export function buildComplianceYaml(opts) {
|
|
209
|
+
const { projectName, regimes, attestedBy, attestedAt, attestedNotes, derivedGates } = opts;
|
|
210
|
+
// AISDLC-324 review fix: quote attestedBy/id/attestedAt so an operator
|
|
211
|
+
// git config user.email containing ": " or other YAML-significant chars
|
|
212
|
+
// can't break the YAML structure. attestedNotes already quoted+escaped.
|
|
213
|
+
const quotedAttestedBy = `"${attestedBy.replace(/"/g, '\\"')}"`;
|
|
214
|
+
const regimeItems = regimes
|
|
215
|
+
.map((id) => {
|
|
216
|
+
// id is from hardcoded COMPLIANCE_REGIME_CHOICES (validated upstream)
|
|
217
|
+
// so it's safe to emit unquoted; YAML-significant chars never reach here.
|
|
218
|
+
const lines = [
|
|
219
|
+
` - id: ${id}`,
|
|
220
|
+
` attestedBy: ${quotedAttestedBy}`,
|
|
221
|
+
` attestedAt: "${attestedAt}"`,
|
|
222
|
+
];
|
|
223
|
+
if (attestedNotes) {
|
|
224
|
+
lines.push(` attestedNotes: "${attestedNotes.replace(/"/g, '\\"')}"`);
|
|
225
|
+
}
|
|
226
|
+
return lines.join('\n');
|
|
227
|
+
})
|
|
228
|
+
.join('\n');
|
|
229
|
+
const derivedGatesComment = [
|
|
230
|
+
'# --- Derived gates (computed from declared regimes; read-only) ---',
|
|
231
|
+
`# databaseBranchPool: ${derivedGates.databaseBranchPool}`,
|
|
232
|
+
`# secretScanStrictness: ${derivedGates.secretScanStrictness}`,
|
|
233
|
+
`# attestationRequired: ${derivedGates.attestationRequired}`,
|
|
234
|
+
`# auditRetentionDays: ${derivedGates.auditRetentionDays}`,
|
|
235
|
+
`# reviewerAuthorityModel: ${derivedGates.reviewerAuthorityModel}`,
|
|
236
|
+
'#',
|
|
237
|
+
'# To override a gate, add spec.derivedGates.<field> with a sibling _notes entry.',
|
|
238
|
+
'# See docs/operations/compliance-posture.md for override patterns.',
|
|
239
|
+
].join('\n');
|
|
240
|
+
// AISDLC-324 review fix: avoid duplicate `regimes:` key when empty.
|
|
241
|
+
// Inline `[]` for empty case; emit block under `regimes:` for non-empty.
|
|
242
|
+
const regimesSection = regimes.length === 0 ? ` regimes: []` : ` regimes:\n${regimeItems}`;
|
|
243
|
+
return [
|
|
244
|
+
`apiVersion: ai-sdlc.io/v1alpha1`,
|
|
245
|
+
`kind: CompliancePosture`,
|
|
246
|
+
`metadata:`,
|
|
247
|
+
` name: "${projectName.replace(/"/g, '\\"')}"`,
|
|
248
|
+
`spec:`,
|
|
249
|
+
regimesSection,
|
|
250
|
+
` auditExports: []`,
|
|
251
|
+
'',
|
|
252
|
+
derivedGatesComment,
|
|
253
|
+
'',
|
|
254
|
+
].join('\n');
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Format the derived gates for console display (the "✓ Wrote ... with derived gates:" block).
|
|
258
|
+
*/
|
|
259
|
+
export function formatDerivedGatesDisplay(derivedGates) {
|
|
260
|
+
return [
|
|
261
|
+
` databaseBranchPool: ${derivedGates.databaseBranchPool}`,
|
|
262
|
+
` secretScanStrictness: ${derivedGates.secretScanStrictness}`,
|
|
263
|
+
` attestationRequired: ${derivedGates.attestationRequired}`,
|
|
264
|
+
` auditRetentionDays: ${derivedGates.auditRetentionDays}`,
|
|
265
|
+
` reviewerAuthorityModel: ${derivedGates.reviewerAuthorityModel}`,
|
|
266
|
+
].join('\n');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* DB-pool rationale displayed when a compliance regime forces `per-shard`.
|
|
270
|
+
* Returns null if the DB-pool is `shared-with-rls` (no rationale needed).
|
|
271
|
+
*/
|
|
272
|
+
export function getDbPoolRationale(regimes, derivedGates) {
|
|
273
|
+
if (derivedGates.databaseBranchPool !== 'per-shard')
|
|
274
|
+
return null;
|
|
275
|
+
if (regimes.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const forcingRegimes = regimes.filter((id) => INIT_WIZARD_REGIME_GATES[id]?.databaseBranchPool === 'per-shard');
|
|
278
|
+
if (forcingRegimes.length === 0)
|
|
279
|
+
return null;
|
|
280
|
+
return ` (${forcingRegimes.join(', ')} declared at .ai-sdlc/compliance.yaml requires per-shard isolation)`;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Run the compliance posture wizard step (RFC-0022 §7 / AISDLC-324).
|
|
284
|
+
*
|
|
285
|
+
* Inserted into the init wizard BEFORE the gate-config feature prompts.
|
|
286
|
+
* Always runs — even for unregulated projects (declaring "(none)" is the
|
|
287
|
+
* explicit choice; the resulting compliance.yaml carries that decision).
|
|
288
|
+
*
|
|
289
|
+
* Flow:
|
|
290
|
+
* 1. Auto-detect git config user.email for attestedBy default.
|
|
291
|
+
* 2. If --yes or non-TTY: use baseline (no regimes), auto-fill attestedBy.
|
|
292
|
+
* 3. Otherwise: multi-select regimes, text-input attestedBy + notes.
|
|
293
|
+
* 4. Compute derivedGates via inline tightest-wins composition.
|
|
294
|
+
* 5. Write .ai-sdlc/compliance.yaml (skips if file already exists and not
|
|
295
|
+
* in --add compliance mode).
|
|
296
|
+
* 6. Log derived gates + DB-pool rationale.
|
|
297
|
+
*/
|
|
298
|
+
export async function runComplianceStep(projectDir, flags, adapters) {
|
|
299
|
+
const compliancePath = join(projectDir, '.ai-sdlc', 'compliance.yaml');
|
|
300
|
+
// ── Auto-detect git email for attestedBy default ──────────────────────
|
|
301
|
+
let gitEmail = '';
|
|
302
|
+
try {
|
|
303
|
+
const result = adapters.runCommand('git', ['config', 'user.email']);
|
|
304
|
+
if (result.exitCode === 0) {
|
|
305
|
+
gitEmail = result.stdout.trim();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Ignore — git config may not be set; attestedBy will be empty default
|
|
310
|
+
}
|
|
311
|
+
// Resolve project name from git remote or directory basename.
|
|
312
|
+
const projectName = (() => {
|
|
313
|
+
try {
|
|
314
|
+
const r = adapters.runCommand('git', ['remote', 'get-url', 'origin']);
|
|
315
|
+
if (r.exitCode === 0) {
|
|
316
|
+
// Extract repo name from remote URL (ssh: git@github.com:org/repo.git, https: .../org/repo.git)
|
|
317
|
+
const m = r.stdout.trim().match(/\/([^/]+?)(\.git)?$/);
|
|
318
|
+
if (m)
|
|
319
|
+
return m[1];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Ignore
|
|
324
|
+
}
|
|
325
|
+
return basename(projectDir);
|
|
326
|
+
})();
|
|
327
|
+
// ── --yes / non-TTY path: baseline (no regimes, no triggers) ──────────
|
|
328
|
+
if (flags.yes || !process.stdin.isTTY) {
|
|
329
|
+
const derivedGates = computeInitWizardDerivedGates([]);
|
|
330
|
+
const attestedAt = new Date().toISOString();
|
|
331
|
+
const written = writeComplianceYaml({
|
|
332
|
+
projectDir,
|
|
333
|
+
compliancePath,
|
|
334
|
+
projectName,
|
|
335
|
+
regimes: [],
|
|
336
|
+
attestedBy: gitEmail,
|
|
337
|
+
attestedAt,
|
|
338
|
+
attestedNotes: undefined,
|
|
339
|
+
derivedGates,
|
|
340
|
+
flags,
|
|
341
|
+
adapters,
|
|
342
|
+
});
|
|
343
|
+
adapters.log('');
|
|
344
|
+
adapters.log(` Compliance posture: no regimes declared (unregulated baseline)`);
|
|
345
|
+
adapters.log(` derivedGates: ${JSON.stringify(derivedGates)}`);
|
|
346
|
+
return {
|
|
347
|
+
regimes: [],
|
|
348
|
+
attestedBy: gitEmail,
|
|
349
|
+
attestedAt,
|
|
350
|
+
attestedNotes: undefined,
|
|
351
|
+
derivedGates,
|
|
352
|
+
oq11Triggers: [],
|
|
353
|
+
yamlPath: compliancePath,
|
|
354
|
+
written,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// ── Interactive path ──────────────────────────────────────────────────
|
|
358
|
+
adapters.log('> Compliance posture');
|
|
359
|
+
adapters.log('');
|
|
360
|
+
const selectedRegimes = await adapters.multiSelect('Which regulatory regimes apply to this project? (space to toggle, enter to confirm)', COMPLIANCE_REGIME_CHOICES);
|
|
361
|
+
const attestedBy = await adapters.textInput('Who is attesting these regimes apply?', gitEmail || undefined);
|
|
362
|
+
const attestedNotesRaw = await adapters.textInput('Notes on the attestation (optional, audit-visible):', undefined);
|
|
363
|
+
const attestedNotes = attestedNotesRaw.trim() || undefined;
|
|
364
|
+
const attestedAt = new Date().toISOString();
|
|
365
|
+
const regimeDerivedGates = computeInitWizardDerivedGates(selectedRegimes);
|
|
366
|
+
// ── RFC-0009 §8.7 / OQ-11 trigger checklist (AISDLC-319) ──────────────
|
|
367
|
+
// Trigger 1 (regulatory) is already reflected in `regimeDerivedGates`;
|
|
368
|
+
// collect triggers 2 + 3 from the operator. The framework cannot auto-detect
|
|
369
|
+
// these (per RFC-0009 §8.7) — operator declares.
|
|
370
|
+
adapters.log('');
|
|
371
|
+
adapters.log('> DatabaseBranchPool trigger checklist (RFC-0009 §8.7)');
|
|
372
|
+
adapters.log(' Default is shared pool with row-level-security isolation. Answer Yes only');
|
|
373
|
+
adapters.log(' if the trigger actually applies — per-soul pools add operational complexity.');
|
|
374
|
+
const customerContract = await adapters.prompt('Does any customer contract require tenant physical isolation (independent of regulatory baseline)?', false);
|
|
375
|
+
const operatorSecurityReview = await adapters.prompt('Has operator security review identified a risk that RLS cannot mitigate?', false);
|
|
376
|
+
const triggerResult = applyOq11TriggerChecklistUpgrade(regimeDerivedGates, {
|
|
377
|
+
customerContract,
|
|
378
|
+
operatorSecurityReview,
|
|
379
|
+
});
|
|
380
|
+
const derivedGates = triggerResult.derivedGates;
|
|
381
|
+
const oq11Triggers = triggerResult.triggers;
|
|
382
|
+
const written = writeComplianceYaml({
|
|
383
|
+
projectDir,
|
|
384
|
+
compliancePath,
|
|
385
|
+
projectName,
|
|
386
|
+
regimes: selectedRegimes,
|
|
387
|
+
attestedBy,
|
|
388
|
+
attestedAt,
|
|
389
|
+
attestedNotes,
|
|
390
|
+
derivedGates,
|
|
391
|
+
flags,
|
|
392
|
+
adapters,
|
|
393
|
+
});
|
|
394
|
+
adapters.log('');
|
|
395
|
+
if (written) {
|
|
396
|
+
adapters.log(`✓ Wrote .ai-sdlc/compliance.yaml with derived gates:`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
adapters.log(` .ai-sdlc/compliance.yaml already exists — skipped (run --add compliance to update)`);
|
|
400
|
+
adapters.log(` Derived gates (from existing compliance.yaml, if unchanged):`);
|
|
401
|
+
}
|
|
402
|
+
adapters.log(formatDerivedGatesDisplay(derivedGates));
|
|
403
|
+
// DB-pool rationale: surface BOTH the regime-derived rationale (AC #6) and
|
|
404
|
+
// the OQ-11 trigger checklist rationale (AISDLC-319 AC #2/#4).
|
|
405
|
+
const dbRationale = getDbPoolRationale(selectedRegimes, derivedGates);
|
|
406
|
+
if (dbRationale) {
|
|
407
|
+
adapters.log('');
|
|
408
|
+
adapters.log(` DatabaseBranchPool pre-selection: per-shard`);
|
|
409
|
+
adapters.log(dbRationale);
|
|
410
|
+
}
|
|
411
|
+
else if (oq11Triggers.length > 0) {
|
|
412
|
+
// Triggers 2 / 3 fired but no regulatory trigger — explain the upgrade.
|
|
413
|
+
adapters.log('');
|
|
414
|
+
adapters.log(` DatabaseBranchPool pre-selection: per-shard`);
|
|
415
|
+
adapters.log(` (RFC-0009 §8.7 trigger fired:`);
|
|
416
|
+
for (const t of oq11Triggers) {
|
|
417
|
+
if (t === 'regulatory')
|
|
418
|
+
continue; // already covered by dbRationale path
|
|
419
|
+
adapters.log(` - ${describeOq11Trigger(t)}`);
|
|
420
|
+
}
|
|
421
|
+
adapters.log(` )`);
|
|
422
|
+
}
|
|
423
|
+
adapters.log('');
|
|
424
|
+
adapters.log(` Review with: cat .ai-sdlc/compliance.yaml`);
|
|
425
|
+
adapters.log(` Override any field with the attestedNotes pattern (see docs/operations/compliance-posture.md).`);
|
|
426
|
+
return {
|
|
427
|
+
regimes: selectedRegimes,
|
|
428
|
+
attestedBy,
|
|
429
|
+
attestedAt,
|
|
430
|
+
attestedNotes,
|
|
431
|
+
derivedGates,
|
|
432
|
+
oq11Triggers,
|
|
433
|
+
yamlPath: compliancePath,
|
|
434
|
+
written,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Helper: write .ai-sdlc/compliance.yaml (or skip if exists). Returns true if written. */
|
|
438
|
+
function writeComplianceYaml(opts) {
|
|
439
|
+
const { compliancePath, flags, adapters } = opts;
|
|
440
|
+
if (flags.dryRun) {
|
|
441
|
+
adapters.log(` would write .ai-sdlc/compliance.yaml`);
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (adapters.exists(compliancePath)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const yamlContent = buildComplianceYaml({
|
|
448
|
+
projectName: opts.projectName,
|
|
449
|
+
regimes: opts.regimes,
|
|
450
|
+
attestedBy: opts.attestedBy,
|
|
451
|
+
attestedAt: opts.attestedAt,
|
|
452
|
+
attestedNotes: opts.attestedNotes,
|
|
453
|
+
derivedGates: opts.derivedGates,
|
|
454
|
+
});
|
|
455
|
+
adapters.mkdirp(join(opts.projectDir, '.ai-sdlc'));
|
|
456
|
+
adapters.writeFile(compliancePath, yamlContent);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
25
459
|
/** All feature flags off — used as the initial state before flags + prompts. */
|
|
26
460
|
export const NO_FEATURES = {
|
|
27
461
|
dor: false,
|
|
28
462
|
attestation: false,
|
|
29
463
|
classifier: false,
|
|
30
464
|
branchProtection: false,
|
|
465
|
+
workflows: false,
|
|
466
|
+
signalIngestion: false,
|
|
31
467
|
};
|
|
32
|
-
/**
|
|
468
|
+
/**
|
|
469
|
+
* All features on — the answer used by `--yes` (accept all defaults).
|
|
470
|
+
*
|
|
471
|
+
* Note: `signalIngestion` is deliberately FALSE in this set even though
|
|
472
|
+
* `--yes` accepts all *defaults*. RFC-0030's pipeline is gated by the
|
|
473
|
+
* `AI_SDLC_SIGNAL_INGESTION` env flag during its soak window, and the
|
|
474
|
+
* shipped default for the flag is OFF. Scaffolding the config stub on a
|
|
475
|
+
* fresh adopter who hasn't opted in would be noise; the file is only
|
|
476
|
+
* meaningful when the operator has explicit interest. Adopters opt in
|
|
477
|
+
* via `--with-signal-ingestion` or `--add signal-ingestion`.
|
|
478
|
+
*/
|
|
33
479
|
export const ALL_FEATURES = {
|
|
34
480
|
dor: true,
|
|
35
481
|
attestation: true,
|
|
36
482
|
classifier: true,
|
|
37
483
|
branchProtection: true,
|
|
484
|
+
workflows: true,
|
|
485
|
+
signalIngestion: false,
|
|
38
486
|
};
|
|
487
|
+
/**
|
|
488
|
+
* Resolve the AI-SDLC install target directory for the current invocation
|
|
489
|
+
* (AISDLC-262).
|
|
490
|
+
*
|
|
491
|
+
* ## Default behavior (no `--workspace` flag)
|
|
492
|
+
*
|
|
493
|
+
* 1. Shell out to `git rev-parse --show-toplevel` to find the repo root.
|
|
494
|
+
* - If the cwd is not inside a git repo, install at cwd (plain-dir
|
|
495
|
+
* fallback — same as the pre-AISDLC-262 behavior).
|
|
496
|
+
* 2. If `<git-root>/.ai-sdlc/` **already exists**, refuse with a clear
|
|
497
|
+
* "already installed at <root>; pass --workspace <name>" message so
|
|
498
|
+
* the operator knows exactly what to do next.
|
|
499
|
+
* 3. Otherwise install at the git root.
|
|
500
|
+
*
|
|
501
|
+
* ## `--workspace <name>` mode
|
|
502
|
+
*
|
|
503
|
+
* The operator explicitly wants a per-workspace install at
|
|
504
|
+
* `packages/<name>/.ai-sdlc/` (or `<name>/.ai-sdlc/` if `packages/` does
|
|
505
|
+
* not exist under the git root). No nesting check is performed — the
|
|
506
|
+
* operator has opted in.
|
|
507
|
+
*
|
|
508
|
+
* ## Dry-run output
|
|
509
|
+
*
|
|
510
|
+
* The resolved `installDir` is logged by the caller on the FIRST output
|
|
511
|
+
* line so adopters can sanity-check the target before any files are written.
|
|
512
|
+
*/
|
|
513
|
+
export function resolveInstallTarget(opts = {}) {
|
|
514
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
515
|
+
const configDirName = opts.configDirName ?? '.ai-sdlc';
|
|
516
|
+
const exists = opts.exists ?? existsSync;
|
|
517
|
+
// Resolve git root (or fall back to cwd when not in a git repo).
|
|
518
|
+
let gitRoot;
|
|
519
|
+
try {
|
|
520
|
+
const getToplevel = opts.gitShowToplevel ??
|
|
521
|
+
((dir) => execSync(`git rev-parse --show-toplevel`, {
|
|
522
|
+
cwd: dir,
|
|
523
|
+
encoding: 'utf-8',
|
|
524
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
525
|
+
}).trim());
|
|
526
|
+
gitRoot = getToplevel(cwd);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
// Not inside a git repo — install at cwd (plain-dir fallback).
|
|
530
|
+
gitRoot = cwd;
|
|
531
|
+
}
|
|
532
|
+
if (opts.workspace) {
|
|
533
|
+
// Per-workspace install: `<git-root>/packages/<name>` if packages/ exists,
|
|
534
|
+
// otherwise `<git-root>/<name>`.
|
|
535
|
+
const packagesDir = join(gitRoot, 'packages');
|
|
536
|
+
const workspaceBase = exists(packagesDir) ? packagesDir : gitRoot;
|
|
537
|
+
const installDir = join(workspaceBase, opts.workspace);
|
|
538
|
+
return { installDir, resolved: installDir !== cwd };
|
|
539
|
+
}
|
|
540
|
+
// Default: install at git root. If `.ai-sdlc/` already exists there,
|
|
541
|
+
// refuse with a helpful message — UNLESS `skipExistingCheck` is set
|
|
542
|
+
// (used by `--add <feature>`, which intentionally extends an existing
|
|
543
|
+
// install and thus expects the directory to already be present).
|
|
544
|
+
const rootConfigDir = join(gitRoot, configDirName);
|
|
545
|
+
if (exists(rootConfigDir) && !opts.skipExistingCheck) {
|
|
546
|
+
return {
|
|
547
|
+
installDir: gitRoot,
|
|
548
|
+
resolved: gitRoot !== cwd,
|
|
549
|
+
error: `AI-SDLC is already installed at ${gitRoot}; ` +
|
|
550
|
+
`pass --workspace <name> to add a child install at packages/<name>/.ai-sdlc/`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { installDir: gitRoot, resolved: gitRoot !== cwd };
|
|
554
|
+
}
|
|
39
555
|
// ── Production adapter factory ───────────────────────────────────────────
|
|
40
556
|
/**
|
|
41
557
|
* Build the production adapter bag. Pulled into a factory so tests can
|
|
@@ -55,6 +571,17 @@ export function buildProductionAdapters() {
|
|
|
55
571
|
const { confirm } = await import('@inquirer/prompts');
|
|
56
572
|
return confirm({ message: question, default: defaultYes });
|
|
57
573
|
},
|
|
574
|
+
multiSelect: async (question, choices) => {
|
|
575
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
576
|
+
return checkbox({
|
|
577
|
+
message: question,
|
|
578
|
+
choices: choices.map((c) => ({ name: c.label, value: c.value })),
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
textInput: async (question, defaultValue) => {
|
|
582
|
+
const { input } = await import('@inquirer/prompts');
|
|
583
|
+
return input({ message: question, default: defaultValue });
|
|
584
|
+
},
|
|
58
585
|
writeFile: (path, contents) => writeFileSync(path, contents, 'utf-8'),
|
|
59
586
|
appendOnce: (path, contents, sentinel) => {
|
|
60
587
|
// Make sure the parent dir exists (appendFileSync errors with ENOENT
|
|
@@ -129,6 +656,12 @@ export async function resolveFeatureSelection(flags, adapters) {
|
|
|
129
656
|
case 'branch-protection':
|
|
130
657
|
sel.branchProtection = true;
|
|
131
658
|
break;
|
|
659
|
+
case 'workflows':
|
|
660
|
+
sel.workflows = true;
|
|
661
|
+
break;
|
|
662
|
+
case 'signal-ingestion':
|
|
663
|
+
sel.signalIngestion = true;
|
|
664
|
+
break;
|
|
132
665
|
}
|
|
133
666
|
return sel;
|
|
134
667
|
}
|
|
@@ -136,6 +669,18 @@ export async function resolveFeatureSelection(flags, adapters) {
|
|
|
136
669
|
if (flags.yes) {
|
|
137
670
|
return { ...ALL_FEATURES };
|
|
138
671
|
}
|
|
672
|
+
// ── Path 2b: non-TTY auto-fall-through ────────────────────────────────
|
|
673
|
+
// When stdin is not a TTY (CI runners, agent bash, piped input, Docker
|
|
674
|
+
// containers without `-it`), interactive prompts hang then throw an
|
|
675
|
+
// unhandled ExitPromptError. Auto-fall-through to ALL_FEATURES (same
|
|
676
|
+
// as --yes) so adopters can run `ai-sdlc init` in CI without any
|
|
677
|
+
// additional flags. This matches the documented behavior of --yes, which
|
|
678
|
+
// is defined as "accept all defaults non-interactively".
|
|
679
|
+
if (!process.stdin.isTTY) {
|
|
680
|
+
adapters.log('Non-TTY stdin detected — auto-accepting all feature defaults (equivalent to --yes).');
|
|
681
|
+
adapters.log('Pass --yes explicitly to suppress this message.');
|
|
682
|
+
return { ...ALL_FEATURES };
|
|
683
|
+
}
|
|
139
684
|
// ── Path 3+4: `--with-X` overrides + prompts for the rest ─────────────
|
|
140
685
|
const sel = { ...NO_FEATURES };
|
|
141
686
|
// DoR
|
|
@@ -166,6 +711,24 @@ export async function resolveFeatureSelection(flags, adapters) {
|
|
|
166
711
|
else {
|
|
167
712
|
sel.branchProtection = await adapters.prompt('Apply recommended branch protection? (required: ai-sdlc/pr-ready + codecov/patch)', true);
|
|
168
713
|
}
|
|
714
|
+
// GitHub Actions workflows bundle (AISDLC-261)
|
|
715
|
+
if (flags.withWorkflows) {
|
|
716
|
+
sel.workflows = true;
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
sel.workflows = await adapters.prompt('Scaffold GitHub Actions workflows (gate, review, attestation, auto-merge)?', true);
|
|
720
|
+
}
|
|
721
|
+
// RFC-0030 Signal Ingestion Pipeline (AISDLC-348)
|
|
722
|
+
// Default to FALSE in the interactive prompt: the pipeline is in a soak
|
|
723
|
+
// window gated by AI_SDLC_SIGNAL_INGESTION; scaffolding the config on a
|
|
724
|
+
// fresh adopter who hasn't opted in is noise. Opt-in path: --with-signal-
|
|
725
|
+
// ingestion or --add signal-ingestion.
|
|
726
|
+
if (flags.withSignalIngestion) {
|
|
727
|
+
sel.signalIngestion = true;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
sel.signalIngestion = await adapters.prompt('Scaffold RFC-0030 signal-ingestion config (default OFF; opt in via AI_SDLC_SIGNAL_INGESTION soak)?', false);
|
|
731
|
+
}
|
|
169
732
|
return sel;
|
|
170
733
|
}
|
|
171
734
|
/**
|
|
@@ -176,12 +739,18 @@ export async function resolveFeatureSelection(flags, adapters) {
|
|
|
176
739
|
* Idempotent: any file that already exists at the target path is skipped
|
|
177
740
|
* with a "skip" log line. This is what makes `--add <feature>` safe to
|
|
178
741
|
* run on an already-initialized repo (AC #7).
|
|
742
|
+
*
|
|
743
|
+
* The `workflows` feature (AISDLC-261) supports `--force` to overwrite
|
|
744
|
+
* existing workflow files with the current template versions. Use this
|
|
745
|
+
* to upgrade a pre-261 repo to the full workflow bundle.
|
|
179
746
|
*/
|
|
180
747
|
export async function applyFeatureSelection(projectDir, selection, flags, adapters) {
|
|
181
748
|
const result = { created: [], skipped: [], wouldCreate: [] };
|
|
182
749
|
// Build the union of templates to write.
|
|
183
750
|
const templateSets = [];
|
|
184
751
|
// `--add` mode: skip the baseline (we're EXTENDING an existing init).
|
|
752
|
+
// Exception: `--add workflows` must write the workflows bundle even in
|
|
753
|
+
// --add mode (the bundle IS the feature; it's not an extension of baseline).
|
|
185
754
|
if (!flags.add) {
|
|
186
755
|
templateSets.push(BASELINE_WORKFLOW_TEMPLATES);
|
|
187
756
|
}
|
|
@@ -191,15 +760,36 @@ export async function applyFeatureSelection(projectDir, selection, flags, adapte
|
|
|
191
760
|
templateSets.push(ATTESTATION_TEMPLATES);
|
|
192
761
|
if (selection.classifier)
|
|
193
762
|
templateSets.push(CLASSIFIER_TEMPLATES);
|
|
763
|
+
if (selection.workflows)
|
|
764
|
+
templateSets.push(WORKFLOWS_TEMPLATES);
|
|
765
|
+
if (selection.signalIngestion)
|
|
766
|
+
templateSets.push(SIGNAL_INGESTION_TEMPLATES);
|
|
767
|
+
// AISDLC-261 PR #480 review fix: dedupe across template sets so e.g.
|
|
768
|
+
// `ai-sdlc-gate.yml` (in BOTH BASELINE_WORKFLOW_TEMPLATES and
|
|
769
|
+
// WORKFLOWS_TEMPLATES) doesn't get written twice on
|
|
770
|
+
// `--with-workflows --force` (which would log "overwrite ${relPath}
|
|
771
|
+
// (--force)" against the duplicate, doubling result.created entries).
|
|
772
|
+
// First-set wins (BASELINE comes first, then feature bundles).
|
|
773
|
+
const seenRelPaths = new Set();
|
|
194
774
|
for (const set of templateSets) {
|
|
195
775
|
for (const [relPath, contents] of Object.entries(set.files)) {
|
|
776
|
+
if (seenRelPaths.has(relPath))
|
|
777
|
+
continue;
|
|
778
|
+
seenRelPaths.add(relPath);
|
|
196
779
|
const absPath = join(projectDir, relPath);
|
|
197
780
|
if (flags.dryRun) {
|
|
781
|
+
const isWorkflowFileDry = relPath.startsWith('.github/workflows/');
|
|
782
|
+
const wouldForce = isWorkflowFileDry && flags.force && adapters.exists(absPath);
|
|
198
783
|
result.wouldCreate.push(relPath);
|
|
199
|
-
adapters.log(` would create ${relPath}`);
|
|
784
|
+
adapters.log(wouldForce ? ` would overwrite ${relPath} (--force)` : ` would create ${relPath}`);
|
|
200
785
|
continue;
|
|
201
786
|
}
|
|
202
|
-
|
|
787
|
+
// For the workflows feature: `--force` overwrites existing files.
|
|
788
|
+
// For all other features: existing files are always skipped (idempotent).
|
|
789
|
+
const isWorkflowFile = relPath.startsWith('.github/workflows/');
|
|
790
|
+
const shouldForce = isWorkflowFile && flags.force;
|
|
791
|
+
const alreadyExists = adapters.exists(absPath);
|
|
792
|
+
if (alreadyExists && !shouldForce) {
|
|
203
793
|
result.skipped.push(relPath);
|
|
204
794
|
adapters.log(` skip ${relPath} (already exists)`);
|
|
205
795
|
continue;
|
|
@@ -208,7 +798,12 @@ export async function applyFeatureSelection(projectDir, selection, flags, adapte
|
|
|
208
798
|
adapters.mkdirp(dirname(absPath));
|
|
209
799
|
adapters.writeFile(absPath, contents);
|
|
210
800
|
result.created.push(relPath);
|
|
211
|
-
|
|
801
|
+
if (alreadyExists && shouldForce) {
|
|
802
|
+
adapters.log(` overwrite ${relPath} (--force)`);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
adapters.log(` created ${relPath}`);
|
|
806
|
+
}
|
|
212
807
|
}
|
|
213
808
|
}
|
|
214
809
|
// Husky pre-push sign hook is a separate concern from the
|
|
@@ -402,6 +997,38 @@ export function renderNextSteps(selection, result, adapters) {
|
|
|
402
997
|
lines.push('');
|
|
403
998
|
stepN++;
|
|
404
999
|
}
|
|
1000
|
+
if (selection.workflows) {
|
|
1001
|
+
lines.push(`${stepN}. GitHub Actions workflows were scaffolded into .github/workflows/.`);
|
|
1002
|
+
lines.push(' Four files are now in place:');
|
|
1003
|
+
lines.push(' - ai-sdlc-gate.yml — ai-sdlc/pr-ready rollup check');
|
|
1004
|
+
lines.push(' - verify-attestation.yml — DSSE attestation verifier (audit-only)');
|
|
1005
|
+
lines.push(' - ai-sdlc-review.yml — PR review status (stub: wire your reviewers)');
|
|
1006
|
+
lines.push(' - auto-enable-auto-merge.yml — arms auto-merge on same-repo PRs');
|
|
1007
|
+
lines.push(' To activate auto-merge:');
|
|
1008
|
+
lines.push(' a) Enable "Allow auto-merge" in GitHub Settings → General.');
|
|
1009
|
+
lines.push(' b) Set the AI_SDLC_PAT secret with write access to the repo.');
|
|
1010
|
+
lines.push(' Re-run with --force to overwrite workflows on a pre-261 repo:');
|
|
1011
|
+
lines.push(' ai-sdlc init --add workflows --force');
|
|
1012
|
+
lines.push('');
|
|
1013
|
+
stepN++;
|
|
1014
|
+
}
|
|
1015
|
+
if (selection.signalIngestion) {
|
|
1016
|
+
lines.push(`${stepN}. RFC-0030 signal-ingestion config was scaffolded (DISABLED by default).`);
|
|
1017
|
+
lines.push(' .ai-sdlc/signal-ingestion.yaml ships every block commented-out under');
|
|
1018
|
+
lines.push(' enabled: false. The pipeline is gated by BOTH the YAML toggle AND the');
|
|
1019
|
+
lines.push(' AI_SDLC_SIGNAL_INGESTION env flag during the soak window.');
|
|
1020
|
+
lines.push(' To opt in:');
|
|
1021
|
+
lines.push(' a) Read docs/operations/signal-ingestion.md (adapter setup +');
|
|
1022
|
+
lines.push(' tier-multiplier + SA-threshold tuning + manual entry workflow).');
|
|
1023
|
+
lines.push(' b) Set AI_SDLC_SIGNAL_INGESTION=1 in your shell / CI env.');
|
|
1024
|
+
lines.push(' c) Edit .ai-sdlc/signal-ingestion.yaml, set spec.enabled: true,');
|
|
1025
|
+
lines.push(' uncomment the adapters block, and confirm tier multipliers.');
|
|
1026
|
+
lines.push(' Config edits emit SignalIngestionConfigChanged governance events to');
|
|
1027
|
+
lines.push(' <ARTIFACTS_DIR>/_orchestrator/events-YYYY-MM-DD.jsonl.');
|
|
1028
|
+
lines.push(' Promotion runbook: docs/operations/signal-ingestion-promotion.md');
|
|
1029
|
+
lines.push('');
|
|
1030
|
+
stepN++;
|
|
1031
|
+
}
|
|
405
1032
|
if (selection.branchProtection) {
|
|
406
1033
|
if (result.branchProtection?.applied) {
|
|
407
1034
|
lines.push(`${stepN}. Branch protection on \`main\` was updated.`);
|