@delegance/claude-autopilot 5.5.2 → 7.2.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/CHANGELOG.md +1776 -6
- package/README.md +65 -1
- package/bin/_launcher.js +38 -23
- package/dist/src/adapters/council/openai.js +12 -6
- package/dist/src/adapters/deploy/_http.d.ts +43 -0
- package/dist/src/adapters/deploy/_http.js +99 -0
- package/dist/src/adapters/deploy/fly.d.ts +206 -0
- package/dist/src/adapters/deploy/fly.js +696 -0
- package/dist/src/adapters/deploy/index.d.ts +2 -0
- package/dist/src/adapters/deploy/index.js +33 -0
- package/dist/src/adapters/deploy/render.d.ts +181 -0
- package/dist/src/adapters/deploy/render.js +550 -0
- package/dist/src/adapters/deploy/types.d.ts +67 -3
- package/dist/src/adapters/deploy/vercel.d.ts +17 -1
- package/dist/src/adapters/deploy/vercel.js +29 -49
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/codex.js +10 -7
- package/dist/src/cli/autopilot.d.ts +75 -0
- package/dist/src/cli/autopilot.js +750 -0
- package/dist/src/cli/brainstorm.d.ts +23 -0
- package/dist/src/cli/brainstorm.js +131 -0
- package/dist/src/cli/costs.d.ts +15 -1
- package/dist/src/cli/costs.js +99 -10
- package/dist/src/cli/dashboard/index.d.ts +5 -0
- package/dist/src/cli/dashboard/index.js +49 -0
- package/dist/src/cli/dashboard/login.d.ts +22 -0
- package/dist/src/cli/dashboard/login.js +260 -0
- package/dist/src/cli/dashboard/logout.d.ts +12 -0
- package/dist/src/cli/dashboard/logout.js +45 -0
- package/dist/src/cli/dashboard/status.d.ts +30 -0
- package/dist/src/cli/dashboard/status.js +65 -0
- package/dist/src/cli/dashboard/upload.d.ts +16 -0
- package/dist/src/cli/dashboard/upload.js +48 -0
- package/dist/src/cli/deploy.d.ts +3 -3
- package/dist/src/cli/deploy.js +34 -9
- package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
- package/dist/src/cli/engine-flag-deprecation.js +20 -0
- package/dist/src/cli/fix.d.ts +18 -0
- package/dist/src/cli/fix.js +105 -11
- package/dist/src/cli/help-text.d.ts +52 -0
- package/dist/src/cli/help-text.js +416 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.d.ts +2 -1
- package/dist/src/cli/index.js +774 -245
- package/dist/src/cli/json-envelope.d.ts +187 -0
- package/dist/src/cli/json-envelope.js +270 -0
- package/dist/src/cli/json-mode.d.ts +33 -0
- package/dist/src/cli/json-mode.js +201 -0
- package/dist/src/cli/migrate.d.ts +111 -0
- package/dist/src/cli/migrate.js +305 -0
- package/dist/src/cli/plan.d.ts +81 -0
- package/dist/src/cli/plan.js +149 -0
- package/dist/src/cli/pr.d.ts +106 -0
- package/dist/src/cli/pr.js +191 -19
- package/dist/src/cli/preflight.js +26 -0
- package/dist/src/cli/review.d.ts +27 -0
- package/dist/src/cli/review.js +126 -0
- package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
- package/dist/src/cli/runs-watch-renderer.js +275 -0
- package/dist/src/cli/runs-watch.d.ts +41 -0
- package/dist/src/cli/runs-watch.js +395 -0
- package/dist/src/cli/runs.d.ts +122 -0
- package/dist/src/cli/runs.js +902 -0
- package/dist/src/cli/scaffold.d.ts +39 -0
- package/dist/src/cli/scaffold.js +287 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/setup.d.ts +30 -0
- package/dist/src/cli/setup.js +137 -0
- package/dist/src/cli/spec.d.ts +66 -0
- package/dist/src/cli/spec.js +132 -0
- package/dist/src/cli/validate.d.ts +29 -0
- package/dist/src/cli/validate.js +131 -0
- package/dist/src/core/config/schema.d.ts +9 -0
- package/dist/src/core/config/schema.js +7 -0
- package/dist/src/core/config/types.d.ts +11 -0
- package/dist/src/core/council/runner.d.ts +10 -1
- package/dist/src/core/council/runner.js +25 -3
- package/dist/src/core/council/types.d.ts +7 -0
- package/dist/src/core/errors.d.ts +1 -1
- package/dist/src/core/errors.js +11 -0
- package/dist/src/core/logging/redaction.d.ts +13 -0
- package/dist/src/core/logging/redaction.js +20 -0
- package/dist/src/core/migrate/schema-validator.js +15 -1
- package/dist/src/core/phases/static-rules.d.ts +5 -1
- package/dist/src/core/phases/static-rules.js +2 -5
- package/dist/src/core/run-state/budget.d.ts +88 -0
- package/dist/src/core/run-state/budget.js +141 -0
- package/dist/src/core/run-state/cli-internal.d.ts +21 -0
- package/dist/src/core/run-state/cli-internal.js +174 -0
- package/dist/src/core/run-state/events.d.ts +59 -0
- package/dist/src/core/run-state/events.js +512 -0
- package/dist/src/core/run-state/lock.d.ts +61 -0
- package/dist/src/core/run-state/lock.js +206 -0
- package/dist/src/core/run-state/phase-context.d.ts +60 -0
- package/dist/src/core/run-state/phase-context.js +108 -0
- package/dist/src/core/run-state/phase-registry.d.ts +137 -0
- package/dist/src/core/run-state/phase-registry.js +162 -0
- package/dist/src/core/run-state/phase-runner.d.ts +80 -0
- package/dist/src/core/run-state/phase-runner.js +447 -0
- package/dist/src/core/run-state/provider-readback.d.ts +130 -0
- package/dist/src/core/run-state/provider-readback.js +426 -0
- package/dist/src/core/run-state/replay-decision.d.ts +69 -0
- package/dist/src/core/run-state/replay-decision.js +144 -0
- package/dist/src/core/run-state/resolve-engine.d.ts +45 -0
- package/dist/src/core/run-state/resolve-engine.js +74 -0
- package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
- package/dist/src/core/run-state/resume-preflight.js +116 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +69 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +193 -0
- package/dist/src/core/run-state/runs.d.ts +57 -0
- package/dist/src/core/run-state/runs.js +288 -0
- package/dist/src/core/run-state/snapshot.d.ts +14 -0
- package/dist/src/core/run-state/snapshot.js +114 -0
- package/dist/src/core/run-state/state.d.ts +40 -0
- package/dist/src/core/run-state/state.js +164 -0
- package/dist/src/core/run-state/types.d.ts +284 -0
- package/dist/src/core/run-state/types.js +19 -0
- package/dist/src/core/run-state/ulid.d.ts +11 -0
- package/dist/src/core/run-state/ulid.js +95 -0
- package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
- package/dist/src/core/schema-alignment/extractor/index.js +2 -2
- package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
- package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
- package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
- package/dist/src/core/schema-alignment/git-history.js +53 -0
- package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
- package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
- package/dist/src/dashboard/auto-upload.d.ts +26 -0
- package/dist/src/dashboard/auto-upload.js +107 -0
- package/dist/src/dashboard/config.d.ts +22 -0
- package/dist/src/dashboard/config.js +109 -0
- package/dist/src/dashboard/upload/canonical.d.ts +3 -0
- package/dist/src/dashboard/upload/canonical.js +16 -0
- package/dist/src/dashboard/upload/chain.d.ts +9 -0
- package/dist/src/dashboard/upload/chain.js +27 -0
- package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
- package/dist/src/dashboard/upload/snapshot.js +66 -0
- package/dist/src/dashboard/upload/uploader.d.ts +54 -0
- package/dist/src/dashboard/upload/uploader.js +330 -0
- package/package.json +19 -3
- package/scripts/autoregress.ts +1 -1
- package/scripts/test-runner.mjs +4 -0
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- package/skills/simplify-ui/SKILL.md +103 -0
- package/skills/ui/SKILL.md +117 -0
- package/skills/ui-ux-pro-max/SKILL.md +90 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/** Schema version for everything written by this engine. Bump on breaking
|
|
2
|
+
* changes to RunState / RunEvent / PhaseSnapshot shape.
|
|
3
|
+
*
|
|
4
|
+
* v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
|
|
5
|
+
* state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
|
|
6
|
+
* because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
|
|
7
|
+
* cannot read v7-written runs; the corrupted_state error includes a
|
|
8
|
+
* "downgrade resume is not supported" hint so operators know why. */
|
|
9
|
+
export declare const RUN_STATE_SCHEMA_VERSION: 2;
|
|
10
|
+
export type SchemaVersion = typeof RUN_STATE_SCHEMA_VERSION;
|
|
11
|
+
/** Identifies a single OS-level writer. PID + a hash of the hostname (we
|
|
12
|
+
* don't persist the raw hostname to the lock metadata so co-tenant signal
|
|
13
|
+
* doesn't leak between users sharing a directory). */
|
|
14
|
+
export interface WriterId {
|
|
15
|
+
pid: number;
|
|
16
|
+
hostHash: string;
|
|
17
|
+
}
|
|
18
|
+
/** Top-level run status, mirroring the lifecycle diagram in the spec. */
|
|
19
|
+
export type RunStatus = 'pending' | 'running' | 'paused' | 'success' | 'failed' | 'aborted';
|
|
20
|
+
/** Per-phase status within a run. */
|
|
21
|
+
export type PhaseStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped' | 'aborted';
|
|
22
|
+
/** External operation reference — the persisted breadcrumb that makes replay
|
|
23
|
+
* decisions deterministic. Used heavily in Phase 6 (idempotency contracts);
|
|
24
|
+
* typed now so events can carry it without later schema churn.
|
|
25
|
+
*
|
|
26
|
+
* v6.2.1 — `migration-batch` joins the union as the side-effect contract's
|
|
27
|
+
* PRE-effect breadcrumb for the `migrate` phase. Semantics: a deterministic
|
|
28
|
+
* id covers a planned migration batch and is emitted BEFORE the dispatcher
|
|
29
|
+
* is invoked, so a partial crash leaves a resume target visible to the
|
|
30
|
+
* orchestrator's preflight readback. The post-effect `migration-version`
|
|
31
|
+
* refs (one per actually-applied migration) remain authoritative for
|
|
32
|
+
* reconciliation; `migration-batch` exists purely so resume can tell
|
|
33
|
+
* "we started this batch but didn't finish" apart from "we never started." */
|
|
34
|
+
export type ExternalRefKind = 'github-pr' | 'github-comment' | 'git-remote-push' | 'deploy' | 'migration-batch' | 'migration-version' | 'rollback-target' | 'spec-file' | 'plan-file' | 'sarif-artifact' | 'review-comments';
|
|
35
|
+
export interface ExternalRef {
|
|
36
|
+
kind: ExternalRefKind;
|
|
37
|
+
/** Provider-specific identifier (PR number, commit SHA, deploy ID, …). */
|
|
38
|
+
id: string;
|
|
39
|
+
provider?: string;
|
|
40
|
+
/** Human-readable artifact link if the provider exposes one. */
|
|
41
|
+
url?: string;
|
|
42
|
+
/** ISO timestamp of the platform's confirmation. */
|
|
43
|
+
observedAt: string;
|
|
44
|
+
}
|
|
45
|
+
/** Per-phase artifact pointer recorded inside the snapshot. */
|
|
46
|
+
export interface PhaseArtifactRef {
|
|
47
|
+
/** Logical name (e.g. "spec", "plan", "impl-diff"). */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Path inside `artifacts/` (relative to run dir). */
|
|
50
|
+
path: string;
|
|
51
|
+
sha256?: string;
|
|
52
|
+
size?: number;
|
|
53
|
+
copiedAt?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Snapshot of a single phase, persisted under `phases/<name>.json` and
|
|
56
|
+
* reflected inside state.json's `phases[]`. */
|
|
57
|
+
export interface PhaseSnapshot {
|
|
58
|
+
schema_version: SchemaVersion;
|
|
59
|
+
name: string;
|
|
60
|
+
/** Order within the run (0-indexed). */
|
|
61
|
+
index: number;
|
|
62
|
+
status: PhaseStatus;
|
|
63
|
+
/** True iff `RunPhase.idempotent` was declared true at registration. */
|
|
64
|
+
idempotent: boolean;
|
|
65
|
+
/** True iff `RunPhase.hasSideEffects` was declared true at registration. */
|
|
66
|
+
hasSideEffects: boolean;
|
|
67
|
+
startedAt?: string;
|
|
68
|
+
endedAt?: string;
|
|
69
|
+
durationMs?: number;
|
|
70
|
+
/** Sum of `phase.cost` events for this phase. */
|
|
71
|
+
costUSD: number;
|
|
72
|
+
attempts: number;
|
|
73
|
+
/** Last failure message (string) if status === 'failed'. */
|
|
74
|
+
lastError?: string;
|
|
75
|
+
artifacts: PhaseArtifactRef[];
|
|
76
|
+
externalRefs: ExternalRef[];
|
|
77
|
+
/** Phase-specific metadata. Free-form; engine doesn't introspect. */
|
|
78
|
+
meta?: Record<string, unknown>;
|
|
79
|
+
/** Phase 6 — last successful output, persisted so a future
|
|
80
|
+
* `skip-already-applied` decision can return it without re-execution.
|
|
81
|
+
* The engine writes this on every `phase.success`; absent on failed
|
|
82
|
+
* / pre-Phase-6 snapshots. JSON-serializable values only. */
|
|
83
|
+
result?: unknown;
|
|
84
|
+
}
|
|
85
|
+
/** The state.json checkpoint. Authoritative answer is always
|
|
86
|
+
* events.ndjson; this is a derived snapshot for O(1) status queries. */
|
|
87
|
+
export interface RunState {
|
|
88
|
+
schema_version: SchemaVersion;
|
|
89
|
+
runId: string;
|
|
90
|
+
/** ULID generation time, ISO. */
|
|
91
|
+
startedAt: string;
|
|
92
|
+
endedAt?: string;
|
|
93
|
+
status: RunStatus;
|
|
94
|
+
/** Phase order is fixed at run creation. */
|
|
95
|
+
phases: PhaseSnapshot[];
|
|
96
|
+
/** Index into `phases[]` of the currently-running or last-attempted phase. */
|
|
97
|
+
currentPhaseIdx: number;
|
|
98
|
+
/** Sum of phase.cost events across the whole run. */
|
|
99
|
+
totalCostUSD: number;
|
|
100
|
+
/** Last seq written to events.ndjson at the time of the snapshot. */
|
|
101
|
+
lastEventSeq: number;
|
|
102
|
+
/** The writer that wrote this snapshot. */
|
|
103
|
+
writerId: WriterId;
|
|
104
|
+
/** Working directory the run was started in (absolute path). */
|
|
105
|
+
cwd: string;
|
|
106
|
+
/** Snapshot of the run config at creation (subset of guardrail.config.yaml).
|
|
107
|
+
* Free-form; engine doesn't introspect here, later phases do. */
|
|
108
|
+
config?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
/** Universal envelope on every event line. */
|
|
111
|
+
export interface RunEventBase {
|
|
112
|
+
schema_version: SchemaVersion;
|
|
113
|
+
/** ISO timestamp. */
|
|
114
|
+
ts: string;
|
|
115
|
+
runId: string;
|
|
116
|
+
/** Monotonic per-run sequence. Receivers MUST detect gaps. */
|
|
117
|
+
seq: number;
|
|
118
|
+
/** The writer that appended this event. */
|
|
119
|
+
writerId: WriterId;
|
|
120
|
+
}
|
|
121
|
+
export interface RunStartEvent extends RunEventBase {
|
|
122
|
+
event: 'run.start';
|
|
123
|
+
phases: string[];
|
|
124
|
+
config?: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
export interface RunCompleteEvent extends RunEventBase {
|
|
127
|
+
event: 'run.complete';
|
|
128
|
+
status: 'success' | 'failed' | 'aborted';
|
|
129
|
+
totalCostUSD: number;
|
|
130
|
+
durationMs: number;
|
|
131
|
+
}
|
|
132
|
+
export interface RunWarningEvent extends RunEventBase {
|
|
133
|
+
event: 'run.warning';
|
|
134
|
+
message: string;
|
|
135
|
+
details?: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
export interface RunRecoveryEvent extends RunEventBase {
|
|
138
|
+
event: 'run.recovery';
|
|
139
|
+
reason: 'recovered-from-partial-write' | 'recovered-from-corrupt-snapshot' | 'recovered-from-missing-snapshot';
|
|
140
|
+
details?: Record<string, unknown>;
|
|
141
|
+
}
|
|
142
|
+
export interface PhaseStartEvent extends RunEventBase {
|
|
143
|
+
event: 'phase.start';
|
|
144
|
+
phase: string;
|
|
145
|
+
phaseIdx: number;
|
|
146
|
+
idempotent: boolean;
|
|
147
|
+
hasSideEffects: boolean;
|
|
148
|
+
/** Attempt counter, 1-based. >1 implies a resume / retry. */
|
|
149
|
+
attempt: number;
|
|
150
|
+
}
|
|
151
|
+
export interface PhaseSuccessEvent extends RunEventBase {
|
|
152
|
+
event: 'phase.success';
|
|
153
|
+
phase: string;
|
|
154
|
+
phaseIdx: number;
|
|
155
|
+
durationMs: number;
|
|
156
|
+
artifacts: PhaseArtifactRef[];
|
|
157
|
+
}
|
|
158
|
+
export interface PhaseFailedEvent extends RunEventBase {
|
|
159
|
+
event: 'phase.failed';
|
|
160
|
+
phase: string;
|
|
161
|
+
phaseIdx: number;
|
|
162
|
+
durationMs: number;
|
|
163
|
+
/** Stringified error message. Stack traces stay out of the durable log. */
|
|
164
|
+
error: string;
|
|
165
|
+
/** Optional structured error code (matches GuardrailError.code if thrown). */
|
|
166
|
+
errorCode?: string;
|
|
167
|
+
}
|
|
168
|
+
export interface PhaseAbortedEvent extends RunEventBase {
|
|
169
|
+
event: 'phase.aborted';
|
|
170
|
+
phase: string;
|
|
171
|
+
phaseIdx: number;
|
|
172
|
+
reason: 'user-interrupt' | 'budget-exceeded' | 'lock-takeover' | 'crash';
|
|
173
|
+
}
|
|
174
|
+
export interface PhaseCostEvent extends RunEventBase {
|
|
175
|
+
event: 'phase.cost';
|
|
176
|
+
phase: string;
|
|
177
|
+
phaseIdx: number;
|
|
178
|
+
provider: string;
|
|
179
|
+
inputTokens: number;
|
|
180
|
+
outputTokens: number;
|
|
181
|
+
costUSD: number;
|
|
182
|
+
}
|
|
183
|
+
export interface PhaseExternalRefEvent extends RunEventBase {
|
|
184
|
+
event: 'phase.externalRef';
|
|
185
|
+
phase: string;
|
|
186
|
+
phaseIdx: number;
|
|
187
|
+
ref: ExternalRef;
|
|
188
|
+
}
|
|
189
|
+
export interface PhaseNeedsHumanEvent extends RunEventBase {
|
|
190
|
+
event: 'phase.needs-human';
|
|
191
|
+
phase: string;
|
|
192
|
+
phaseIdx: number;
|
|
193
|
+
reason: string;
|
|
194
|
+
/** Hint surfaced to the user / CI consumer. */
|
|
195
|
+
nextActions?: string[];
|
|
196
|
+
}
|
|
197
|
+
export interface LockTakeoverEvent extends RunEventBase {
|
|
198
|
+
event: 'lock.takeover';
|
|
199
|
+
/** Identity of the writer who previously held the lock (best-effort —
|
|
200
|
+
* may be null if metadata was missing). */
|
|
201
|
+
previousWriter: WriterId | null;
|
|
202
|
+
reason: string;
|
|
203
|
+
}
|
|
204
|
+
export interface IndexRebuiltEvent extends RunEventBase {
|
|
205
|
+
event: 'index.rebuilt';
|
|
206
|
+
/** Why the rebuild was needed: "missing", "corrupt", or "force". */
|
|
207
|
+
cause: 'missing' | 'corrupt' | 'force';
|
|
208
|
+
}
|
|
209
|
+
/** Phase 4 — budget enforcement preflight. Emitted by `runPhase` BEFORE
|
|
210
|
+
* `phase.start` for every phase whose parent run carries a `BudgetConfig`.
|
|
211
|
+
* Carries the full `BudgetCheck` payload from `checkPhaseBudget` so
|
|
212
|
+
* consumers (cost dashboards, CI) can attribute spend and decisions
|
|
213
|
+
* without re-running the policy. Per the v6 spec "Budget enforcement"
|
|
214
|
+
* section + Codex CRITICAL #3 (two-layer guard, layer 2 always runs). */
|
|
215
|
+
export interface BudgetCheckEvent extends RunEventBase {
|
|
216
|
+
event: 'budget.check';
|
|
217
|
+
phase: string;
|
|
218
|
+
phaseIdx: number;
|
|
219
|
+
decision: 'proceed' | 'pause' | 'hard-fail';
|
|
220
|
+
/** `estimate.high` from `RunPhase.estimateCost` if it returned a value;
|
|
221
|
+
* null when the phase doesn't implement estimateCost. Layer 2 (the
|
|
222
|
+
* mandatory floor) ALWAYS runs regardless. */
|
|
223
|
+
estimatedHigh: number | null;
|
|
224
|
+
/** Sum of every prior `phase.cost` event in this run, in USD. */
|
|
225
|
+
actualSoFar: number;
|
|
226
|
+
/** The reserve the runner deducted against `perRunUSD` for this phase
|
|
227
|
+
* (Layer 2 floor, expressed in USD). */
|
|
228
|
+
reserveApplied: number;
|
|
229
|
+
/** USD remaining under `perRunUSD` after `actualSoFar` + the larger of
|
|
230
|
+
* `estimatedHigh` and `reserveApplied`. May be negative on hard-fail. */
|
|
231
|
+
capRemaining: number;
|
|
232
|
+
reason: string;
|
|
233
|
+
/** v6.2.0 — which scope produced the decision. `'phase'` (legacy
|
|
234
|
+
* default) is the single-phase wrapper path; `'run'` is the
|
|
235
|
+
* orchestrator's cross-phase mode. Optional only for back-compat
|
|
236
|
+
* with older events.ndjson files; events emitted on v6.2.0+ always
|
|
237
|
+
* carry a value. */
|
|
238
|
+
scope?: 'phase' | 'run';
|
|
239
|
+
}
|
|
240
|
+
/** Phase 6 — emitted when a `forceReplay` override flips a needs-human (or
|
|
241
|
+
* any other refusal) into a retry. Carries the phase and the refs that
|
|
242
|
+
* WERE consulted so the durable log shows exactly what was overridden.
|
|
243
|
+
* Spec: "a `--force-replay` override writes an explicit `replay.override`
|
|
244
|
+
* event with user-supplied reason." */
|
|
245
|
+
export interface ReplayOverrideEvent extends RunEventBase {
|
|
246
|
+
event: 'replay.override';
|
|
247
|
+
phase: string;
|
|
248
|
+
phaseIdx: number;
|
|
249
|
+
/** Free-form user / CI reason for the override. */
|
|
250
|
+
reason: string;
|
|
251
|
+
/** Refs the underlying refusal cited (echoed for triage). */
|
|
252
|
+
refsConsulted: ExternalRef[];
|
|
253
|
+
}
|
|
254
|
+
/** Discriminated union of every event variant. Add new variants here and
|
|
255
|
+
* the code that switches over `event` will type-error at compile time. */
|
|
256
|
+
export type RunEvent = RunStartEvent | RunCompleteEvent | RunWarningEvent | RunRecoveryEvent | PhaseStartEvent | PhaseSuccessEvent | PhaseFailedEvent | PhaseAbortedEvent | PhaseCostEvent | PhaseExternalRefEvent | PhaseNeedsHumanEvent | LockTakeoverEvent | IndexRebuiltEvent | BudgetCheckEvent | ReplayOverrideEvent;
|
|
257
|
+
/** Distributive Omit so the discriminated-union shape is preserved when we
|
|
258
|
+
* strip the fields the appender fills in. Plain `Omit<RunEvent, ...>`
|
|
259
|
+
* collapses the union into a single intersection and loses variant-specific
|
|
260
|
+
* fields — so a literal `{ event: 'phase.cost', costUSD: 1, ... }` would
|
|
261
|
+
* fail typecheck. */
|
|
262
|
+
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
263
|
+
/** When appending we don't know `seq`, `ts`, `runId`, `schema_version`,
|
|
264
|
+
* or `writerId` yet — the appender supplies them. */
|
|
265
|
+
export type RunEventInput = DistributiveOmit<RunEvent, 'seq' | 'ts' | 'runId' | 'schema_version' | 'writerId'>;
|
|
266
|
+
export interface RunIndexEntry {
|
|
267
|
+
runId: string;
|
|
268
|
+
status: RunStatus;
|
|
269
|
+
startedAt: string;
|
|
270
|
+
endedAt?: string;
|
|
271
|
+
totalCostUSD: number;
|
|
272
|
+
/** Last completed phase name (for `runs list` summary). */
|
|
273
|
+
lastPhase?: string;
|
|
274
|
+
/** True if state.json was synthesized via replay because it was missing
|
|
275
|
+
* or corrupt at last open. Surfaces as a warning in the UI. */
|
|
276
|
+
recovered?: boolean;
|
|
277
|
+
}
|
|
278
|
+
export interface RunIndex {
|
|
279
|
+
schema_version: SchemaVersion;
|
|
280
|
+
/** Newest first. */
|
|
281
|
+
runs: RunIndexEntry[];
|
|
282
|
+
}
|
|
283
|
+
export {};
|
|
284
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/core/run-state/types.ts
|
|
2
|
+
//
|
|
3
|
+
// v6 Run State Engine — pure data layer types. Phase 1 (persistence) only.
|
|
4
|
+
// Behavior — phase wrapping, CLI verbs, budget enforcement, etc. — lands in
|
|
5
|
+
// later phases. The shapes here are versioned via `schema_version: 1` so a
|
|
6
|
+
// future migration can detect and migrate older runs.
|
|
7
|
+
//
|
|
8
|
+
// Spec: docs/specs/v6-run-state-engine.md ("State on disk", "Run lifecycle",
|
|
9
|
+
// "Idempotency rules + external operation ledger", "Persistence protocol").
|
|
10
|
+
/** Schema version for everything written by this engine. Bump on breaking
|
|
11
|
+
* changes to RunState / RunEvent / PhaseSnapshot shape.
|
|
12
|
+
*
|
|
13
|
+
* v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
|
|
14
|
+
* state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
|
|
15
|
+
* because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
|
|
16
|
+
* cannot read v7-written runs; the corrupted_state error includes a
|
|
17
|
+
* "downgrade resume is not supported" hint so operators know why. */
|
|
18
|
+
export const RUN_STATE_SCHEMA_VERSION = 2;
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Validate that a string matches the ULID shape (length + alphabet). */
|
|
2
|
+
export declare function isValidULID(s: string): boolean;
|
|
3
|
+
/** Generate a new ULID. Optionally pass a fixed `now` (ms epoch) for tests. */
|
|
4
|
+
export declare function ulid(now?: number): string;
|
|
5
|
+
/** Decode the timestamp-portion of a ULID back to ms epoch. Throws on
|
|
6
|
+
* malformed input. Useful for `runs list` ordering and for tests. */
|
|
7
|
+
export declare function decodeTime(id: string): number;
|
|
8
|
+
/** Exposed for tests that want to verify alphabet membership. */
|
|
9
|
+
export declare const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
10
|
+
export declare const ULID_LENGTH: number;
|
|
11
|
+
//# sourceMappingURL=ulid.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/core/run-state/ulid.ts
|
|
2
|
+
//
|
|
3
|
+
// Tiny pure-TS ULID generator. We deliberately avoid pulling in the `ulid`
|
|
4
|
+
// npm package — the algorithm is short and the runtime dep budget for the
|
|
5
|
+
// engine is tight. Conforms to https://github.com/ulid/spec :
|
|
6
|
+
//
|
|
7
|
+
// - 26 characters, Crockford's Base32 (no I, L, O, U).
|
|
8
|
+
// - First 10 chars = 48-bit Unix-millisecond timestamp (ms since epoch).
|
|
9
|
+
// - Last 16 chars = 80 bits of randomness (crypto.randomBytes).
|
|
10
|
+
// - Lexicographic sort == chronological sort (within ms; tie-break is
|
|
11
|
+
// random within the same ms — Phase 1 does not implement the optional
|
|
12
|
+
// monotonic-overflow within-ms behavior, since runIDs are issued by a
|
|
13
|
+
// single writer and one-per-millisecond is unreachable in practice).
|
|
14
|
+
// - URL-safe (Crockford Base32 only emits [0-9A-Z]).
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford Base32 alphabet (32 chars).
|
|
17
|
+
const ENCODING_LEN = ENCODING.length; // 32.
|
|
18
|
+
const TIME_LEN = 10;
|
|
19
|
+
const RANDOM_LEN = 16;
|
|
20
|
+
const ULID_LEN = TIME_LEN + RANDOM_LEN; // 26.
|
|
21
|
+
/** Max representable timestamp = 2^48 - 1 ms. Sanity-check, not a bug
|
|
22
|
+
* most callers will hit (it's the year 10889). */
|
|
23
|
+
const TIME_MAX = 281474976710655;
|
|
24
|
+
/** Validate that a string matches the ULID shape (length + alphabet). */
|
|
25
|
+
export function isValidULID(s) {
|
|
26
|
+
if (typeof s !== 'string')
|
|
27
|
+
return false;
|
|
28
|
+
if (s.length !== ULID_LEN)
|
|
29
|
+
return false;
|
|
30
|
+
for (let i = 0; i < ULID_LEN; i++) {
|
|
31
|
+
if (ENCODING.indexOf(s[i]) < 0)
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function encodeTime(now) {
|
|
37
|
+
if (!Number.isFinite(now) || now < 0 || now > TIME_MAX) {
|
|
38
|
+
throw new RangeError(`ulid: timestamp out of range (got ${now})`);
|
|
39
|
+
}
|
|
40
|
+
let out = '';
|
|
41
|
+
let t = now;
|
|
42
|
+
for (let i = TIME_LEN - 1; i >= 0; i--) {
|
|
43
|
+
const mod = t % ENCODING_LEN;
|
|
44
|
+
out = ENCODING[mod] + out;
|
|
45
|
+
t = (t - mod) / ENCODING_LEN;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function encodeRandom() {
|
|
50
|
+
// 16 chars * 5 bits each = 80 bits. We draw 10 bytes (80 bits) of
|
|
51
|
+
// crypto-grade randomness and encode 5 bits at a time. Any extra fractional
|
|
52
|
+
// bits are discarded — this is the standard ULID approach.
|
|
53
|
+
const bytes = randomBytes(10);
|
|
54
|
+
// Pack the 10 bytes into a 80-bit unsigned integer view, 5-bit chunks.
|
|
55
|
+
// We do it manually rather than using BigInt for portability — the array
|
|
56
|
+
// is short enough that the explicit math is just as fast and avoids any
|
|
57
|
+
// dependency on BigInt-typed regression in older runtimes.
|
|
58
|
+
const bits = new Array(80);
|
|
59
|
+
for (let i = 0; i < 10; i++) {
|
|
60
|
+
const b = bytes[i];
|
|
61
|
+
for (let j = 0; j < 8; j++) {
|
|
62
|
+
bits[i * 8 + j] = (b >> (7 - j)) & 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let out = '';
|
|
66
|
+
for (let i = 0; i < RANDOM_LEN; i++) {
|
|
67
|
+
let v = 0;
|
|
68
|
+
for (let j = 0; j < 5; j++) {
|
|
69
|
+
v = (v << 1) | bits[i * 5 + j];
|
|
70
|
+
}
|
|
71
|
+
out += ENCODING[v];
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Generate a new ULID. Optionally pass a fixed `now` (ms epoch) for tests. */
|
|
76
|
+
export function ulid(now = Date.now()) {
|
|
77
|
+
return encodeTime(now) + encodeRandom();
|
|
78
|
+
}
|
|
79
|
+
/** Decode the timestamp-portion of a ULID back to ms epoch. Throws on
|
|
80
|
+
* malformed input. Useful for `runs list` ordering and for tests. */
|
|
81
|
+
export function decodeTime(id) {
|
|
82
|
+
if (!isValidULID(id)) {
|
|
83
|
+
throw new Error(`ulid: not a valid ULID: ${String(id)}`);
|
|
84
|
+
}
|
|
85
|
+
let t = 0;
|
|
86
|
+
for (let i = 0; i < TIME_LEN; i++) {
|
|
87
|
+
const idx = ENCODING.indexOf(id[i]);
|
|
88
|
+
t = t * ENCODING_LEN + idx;
|
|
89
|
+
}
|
|
90
|
+
return t;
|
|
91
|
+
}
|
|
92
|
+
/** Exposed for tests that want to verify alphabet membership. */
|
|
93
|
+
export const ULID_ALPHABET = ENCODING;
|
|
94
|
+
export const ULID_LENGTH = ULID_LEN;
|
|
95
|
+
//# sourceMappingURL=ulid.js.map
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { extractFromSql } from "./sql.js";
|
|
5
5
|
import { extractFromPrisma } from "./prisma.js";
|
|
6
|
-
export function extract(filePath) {
|
|
6
|
+
export function extract(filePath, previousContent) {
|
|
7
7
|
let content;
|
|
8
8
|
try {
|
|
9
9
|
content = fs.readFileSync(filePath, 'utf8');
|
|
@@ -16,7 +16,7 @@ export function extract(filePath) {
|
|
|
16
16
|
if (ext === '.sql')
|
|
17
17
|
return extractFromSql(content);
|
|
18
18
|
if (base === 'schema.prisma' || ext === '.prisma')
|
|
19
|
-
return extractFromPrisma(content);
|
|
19
|
+
return extractFromPrisma(content, previousContent);
|
|
20
20
|
process.stderr.write(`[schema-alignment] no extractor for ${ext} files — skipping ${path.basename(filePath)}\n`);
|
|
21
21
|
return [];
|
|
22
22
|
}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import type { SchemaEntity } from '../types.ts';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Extract schema entities from a Prisma schema file.
|
|
4
|
+
*
|
|
5
|
+
* When `previousContent` is provided, only the diff (added/removed fields,
|
|
6
|
+
* new/dropped tables) is emitted — this avoids over-reporting when a user
|
|
7
|
+
* touches schema.prisma for any reason (adding one field, editing a comment)
|
|
8
|
+
* and every long-existing field gets re-checked against type/API/UI layers.
|
|
9
|
+
*
|
|
10
|
+
* When `previousContent` is null/undefined, every model and field is emitted
|
|
11
|
+
* — the original "all entities are new" behavior used as a fallback when git
|
|
12
|
+
* history isn't available.
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractFromPrisma(content: string, previousContent?: string | null): SchemaEntity[];
|
|
3
15
|
//# sourceMappingURL=prisma.d.ts.map
|
|
@@ -1,18 +1,73 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
for (const modelMatch of content.matchAll(
|
|
1
|
+
const MODEL_RE = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
|
|
2
|
+
const FIELD_RE = /^\s+(\w+)\s+\S/gm;
|
|
3
|
+
function parseModels(content) {
|
|
4
|
+
const models = new Map();
|
|
5
|
+
for (const modelMatch of content.matchAll(MODEL_RE)) {
|
|
6
6
|
const table = modelMatch[1];
|
|
7
|
-
|
|
7
|
+
const fields = new Set();
|
|
8
8
|
const body = modelMatch[2];
|
|
9
|
-
|
|
10
|
-
const fieldRe = /^\s+(\w+)\s+\S/gm;
|
|
11
|
-
for (const fieldMatch of body.matchAll(fieldRe)) {
|
|
9
|
+
for (const fieldMatch of body.matchAll(FIELD_RE)) {
|
|
12
10
|
const column = fieldMatch[1];
|
|
13
11
|
if (column.startsWith('@') || column === 'id')
|
|
14
12
|
continue;
|
|
15
|
-
|
|
13
|
+
fields.add(column);
|
|
14
|
+
}
|
|
15
|
+
models.set(table, fields);
|
|
16
|
+
}
|
|
17
|
+
return models;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract schema entities from a Prisma schema file.
|
|
21
|
+
*
|
|
22
|
+
* When `previousContent` is provided, only the diff (added/removed fields,
|
|
23
|
+
* new/dropped tables) is emitted — this avoids over-reporting when a user
|
|
24
|
+
* touches schema.prisma for any reason (adding one field, editing a comment)
|
|
25
|
+
* and every long-existing field gets re-checked against type/API/UI layers.
|
|
26
|
+
*
|
|
27
|
+
* When `previousContent` is null/undefined, every model and field is emitted
|
|
28
|
+
* — the original "all entities are new" behavior used as a fallback when git
|
|
29
|
+
* history isn't available.
|
|
30
|
+
*/
|
|
31
|
+
export function extractFromPrisma(content, previousContent) {
|
|
32
|
+
const current = parseModels(content);
|
|
33
|
+
if (previousContent === undefined || previousContent === null) {
|
|
34
|
+
const entities = [];
|
|
35
|
+
for (const [table, fields] of current) {
|
|
36
|
+
entities.push({ table, operation: 'create_table' });
|
|
37
|
+
for (const column of fields)
|
|
38
|
+
entities.push({ table, column, operation: 'add_column' });
|
|
39
|
+
}
|
|
40
|
+
return entities;
|
|
41
|
+
}
|
|
42
|
+
const previous = parseModels(previousContent);
|
|
43
|
+
const entities = [];
|
|
44
|
+
for (const [table, currentFields] of current) {
|
|
45
|
+
const previousFields = previous.get(table);
|
|
46
|
+
if (!previousFields) {
|
|
47
|
+
// New table — emit create_table + add_column for every field
|
|
48
|
+
entities.push({ table, operation: 'create_table' });
|
|
49
|
+
for (const column of currentFields)
|
|
50
|
+
entities.push({ table, column, operation: 'add_column' });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const column of currentFields) {
|
|
54
|
+
if (!previousFields.has(column))
|
|
55
|
+
entities.push({ table, column, operation: 'add_column' });
|
|
56
|
+
}
|
|
57
|
+
for (const column of previousFields) {
|
|
58
|
+
if (!currentFields.has(column))
|
|
59
|
+
entities.push({ table, column, operation: 'drop_column' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Second pass — entirely dropped models. The first loop only iterates
|
|
63
|
+
// `current`, so a model present in `previous` but removed from `current`
|
|
64
|
+
// would never emit any entity, leaving stale references in type/API/UI
|
|
65
|
+
// layers undetected. Caught by Cursor Bugbot on PR #44 (MEDIUM).
|
|
66
|
+
for (const [table, previousFields] of previous) {
|
|
67
|
+
if (current.has(table))
|
|
68
|
+
continue;
|
|
69
|
+
for (const column of previousFields) {
|
|
70
|
+
entities.push({ table, column, operation: 'drop_column' });
|
|
16
71
|
}
|
|
17
72
|
}
|
|
18
73
|
return entities;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the previous version of a file from git, comparing against `base`.
|
|
3
|
+
* Returns null if the file has no git history at that ref (untracked,
|
|
4
|
+
* brand-new, not in a repo, or the ref doesn't resolve) — callers should
|
|
5
|
+
* fall back to whole-file extraction in that case.
|
|
6
|
+
*
|
|
7
|
+
* When `base` is omitted, the default is CI-aware (see `resolveDefaultBase`):
|
|
8
|
+
* - GitHub Actions PR build → `origin/<GITHUB_BASE_REF>`
|
|
9
|
+
* - GitLab MR build → `origin/<CI_MERGE_REQUEST_TARGET_BRANCH_NAME>`
|
|
10
|
+
* - everything else → `HEAD~1`
|
|
11
|
+
*
|
|
12
|
+
* Reading from `HEAD` directly is wrong in CI (any post-commit context):
|
|
13
|
+
* `HEAD` IS the current commit, so the diff against the working-tree file
|
|
14
|
+
* is always empty and no schema entities are emitted. Caught by Cursor
|
|
15
|
+
* Bugbot on PR #44 (HIGH); the multi-commit-PR variant of the same bug
|
|
16
|
+
* caught as a MEDIUM follow-up on the rebased commit.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getPreviousFileContent(filePath: string, cwd?: string, base?: string): string | null;
|
|
19
|
+
//# sourceMappingURL=git-history.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the default base ref for git diffs in a CI-aware way.
|
|
5
|
+
*
|
|
6
|
+
* Priority: `GITHUB_BASE_REF` (GitHub Actions PR builds), then
|
|
7
|
+
* `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` (GitLab MR builds), then `HEAD~1`
|
|
8
|
+
* for local / single-commit contexts. Branch-name env vars are prefixed
|
|
9
|
+
* with `origin/` so `git show <ref>:<file>` resolves to the
|
|
10
|
+
* remote-tracking-branch tip (the actual merge base).
|
|
11
|
+
*
|
|
12
|
+
* Caught + extended by Cursor Bugbot follow-up on PR #44 (MEDIUM): a static
|
|
13
|
+
* default of `HEAD~1` is wrong for multi-commit PRs — it points at the
|
|
14
|
+
* previous commit on the branch, not the merge base.
|
|
15
|
+
*/
|
|
16
|
+
function resolveDefaultBase() {
|
|
17
|
+
const ghBase = process.env.GITHUB_BASE_REF;
|
|
18
|
+
if (ghBase && ghBase.length > 0)
|
|
19
|
+
return `origin/${ghBase}`;
|
|
20
|
+
const glBase = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
21
|
+
if (glBase && glBase.length > 0)
|
|
22
|
+
return `origin/${glBase}`;
|
|
23
|
+
return 'HEAD~1';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the previous version of a file from git, comparing against `base`.
|
|
27
|
+
* Returns null if the file has no git history at that ref (untracked,
|
|
28
|
+
* brand-new, not in a repo, or the ref doesn't resolve) — callers should
|
|
29
|
+
* fall back to whole-file extraction in that case.
|
|
30
|
+
*
|
|
31
|
+
* When `base` is omitted, the default is CI-aware (see `resolveDefaultBase`):
|
|
32
|
+
* - GitHub Actions PR build → `origin/<GITHUB_BASE_REF>`
|
|
33
|
+
* - GitLab MR build → `origin/<CI_MERGE_REQUEST_TARGET_BRANCH_NAME>`
|
|
34
|
+
* - everything else → `HEAD~1`
|
|
35
|
+
*
|
|
36
|
+
* Reading from `HEAD` directly is wrong in CI (any post-commit context):
|
|
37
|
+
* `HEAD` IS the current commit, so the diff against the working-tree file
|
|
38
|
+
* is always empty and no schema entities are emitted. Caught by Cursor
|
|
39
|
+
* Bugbot on PR #44 (HIGH); the multi-commit-PR variant of the same bug
|
|
40
|
+
* caught as a MEDIUM follow-up on the rebased commit.
|
|
41
|
+
*/
|
|
42
|
+
export function getPreviousFileContent(filePath, cwd = process.cwd(), base = resolveDefaultBase()) {
|
|
43
|
+
const relPath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
|
|
44
|
+
const result = spawnSync('git', ['show', `${base}:${relPath}`], {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
timeout: 5000,
|
|
48
|
+
});
|
|
49
|
+
if (result.status !== 0)
|
|
50
|
+
return null;
|
|
51
|
+
return result.stdout;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=git-history.js.map
|
|
@@ -36,8 +36,8 @@ function buildPalette(brandCfg, cwd) {
|
|
|
36
36
|
export const brandTokensRule = {
|
|
37
37
|
name: 'brand-tokens',
|
|
38
38
|
severity: 'warning',
|
|
39
|
-
async check(touchedFiles,
|
|
40
|
-
const brandCfg = config
|
|
39
|
+
async check(touchedFiles, ctx = {}) {
|
|
40
|
+
const brandCfg = ctx.config?.brand;
|
|
41
41
|
if (!brandCfg)
|
|
42
42
|
return [];
|
|
43
43
|
const cwd = process.cwd();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { detect } from "../../schema-alignment/detector.js";
|
|
2
2
|
import { extract } from "../../schema-alignment/extractor/index.js";
|
|
3
|
+
import { getPreviousFileContent } from "../../schema-alignment/git-history.js";
|
|
3
4
|
import { scanLayers } from "../../schema-alignment/scanner.js";
|
|
4
5
|
import { runLlmCheck } from "../../schema-alignment/llm-check.js";
|
|
5
6
|
function isDestructive(entity) {
|
|
@@ -50,15 +51,24 @@ function structuralFinding(result, layer, defaultSev, sourceFile) {
|
|
|
50
51
|
export const schemaAlignmentRule = {
|
|
51
52
|
name: 'schema-alignment',
|
|
52
53
|
severity: 'warning',
|
|
53
|
-
async check(touchedFiles,
|
|
54
|
-
const saConfig = config['schema-alignment'];
|
|
54
|
+
async check(touchedFiles, ctx = {}) {
|
|
55
|
+
const saConfig = ctx.config?.['schema-alignment'];
|
|
55
56
|
if (saConfig?.enabled === false)
|
|
56
57
|
return [];
|
|
57
58
|
const cwd = process.cwd();
|
|
58
59
|
const migrationFiles = detect(touchedFiles, saConfig);
|
|
59
60
|
if (migrationFiles.length === 0)
|
|
60
61
|
return [];
|
|
61
|
-
|
|
62
|
+
// For Prisma schema files, fetch the previous version from git so we only
|
|
63
|
+
// emit entities for what actually changed in this diff. SQL migrations
|
|
64
|
+
// are inherently a diff already; the SQL extractor ignores
|
|
65
|
+
// `previousContent`, so skipping the `git show` spawn there avoids pure
|
|
66
|
+
// waste (Bugbot LOW on PR #44).
|
|
67
|
+
const allEntities = migrationFiles.flatMap(f => {
|
|
68
|
+
const isPrisma = f.endsWith('.prisma');
|
|
69
|
+
const previousContent = isPrisma ? getPreviousFileContent(f, cwd) : null;
|
|
70
|
+
return extract(f, previousContent).map(entity => ({ entity, sourceFile: f }));
|
|
71
|
+
});
|
|
62
72
|
if (allEntities.length === 0)
|
|
63
73
|
return [];
|
|
64
74
|
const scanResults = scanLayers(allEntities.map(e => e.entity), cwd, saConfig);
|
|
@@ -75,7 +85,7 @@ export const schemaAlignmentRule = {
|
|
|
75
85
|
return [];
|
|
76
86
|
const defaultSev = saConfig?.severity ?? 'warning';
|
|
77
87
|
const llmEnabled = saConfig?.llmCheck !== false;
|
|
78
|
-
const engine =
|
|
88
|
+
const engine = ctx.engine;
|
|
79
89
|
// Structural mode — always compute these so we can fall back if LLM path yields nothing
|
|
80
90
|
const structural = [];
|
|
81
91
|
for (const { result: r, sourceFile } of gapResults) {
|