@prodcycle/prodcycle 0.4.2 → 0.6.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/README.md CHANGED
@@ -31,18 +31,27 @@ npm install @prodcycle/prodcycle
31
31
  ### CLI
32
32
 
33
33
  ```bash
34
- # Scan current directory against SOC 2 and HIPAA
34
+ # Scan current directory against all 3 frameworks (default: soc2, hipaa, nist-csf).
35
+ # Auto-flips to SARIF in known CI environments so output drops into
36
+ # code-scanning dashboards without extra wiring.
37
+ prodcycle scan .
38
+
39
+ # Pin a specific framework or subset
35
40
  prodcycle scan . --framework soc2,hipaa
41
+ prodcycle scan . --framework hipaa --severity-threshold high
36
42
 
37
- # Output as SARIF for GitHub Code Scanning
38
- prodcycle scan . --framework soc2 --format sarif --output results.sarif
43
+ # Explicit SARIF (overrides the CI auto-flip)
44
+ prodcycle scan . --format sarif --output results.sarif
39
45
 
40
- # Set severity threshold (only report HIGH and above)
41
- prodcycle scan . --framework hipaa --severity-threshold high
46
+ # CI: scan only files changed in the PR
47
+ prodcycle scan . --pr origin/main..HEAD
42
48
 
43
49
  # Auto-configure compliance hooks/instructions for your coding agents
44
50
  # (Claude Code, Cursor, Codex, OpenCode, GitHub Copilot, Gemini CLI)
45
51
  prodcycle init --agent all
52
+
53
+ # Scaffold a CI workflow that delegates to prodcycle/actions/compliance
54
+ prodcycle init --ci github # also: gitlab | circleci
46
55
  ```
47
56
 
48
57
  Subcommands: `scan` (full repo scan), `gate` (JSON payload from stdin), `hook` (coding-agent post-edit hook), `init` (agent setup).
@@ -16,11 +16,164 @@ export interface GateOptions {
16
16
  apiUrl?: string;
17
17
  config?: Record<string, unknown>;
18
18
  }
19
+ /**
20
+ * Set when `validateChunked`'s post-`/complete` enrichment GET failed and the
21
+ * structured `findings` could not be recovered. Distinguishable from the
22
+ * server-side `scannerError`: this signals "we know there are N findings (per
23
+ * the summary) but we couldn't fetch them — retry with `prodcycle scans
24
+ * <id>`," not "we couldn't certify the scan." Surfaced as a named field
25
+ * (rather than via the `[key: string]: unknown` index signature) so
26
+ * TypeScript callers get a typed contract instead of `unknown`.
27
+ *
28
+ * `code` distinguishes:
29
+ * - `BACKFILL_GET_FAILED`: GET threw / non-2xx — backfill couldn't run
30
+ * - `BACKFILL_GET_RETURNED_EMPTY`: GET succeeded but findings were still
31
+ * empty despite `summary.total > 0`. Usually means eventual consistency
32
+ * between the `/complete` writer and the scan-record reader; retrying
33
+ * after a short delay typically populates the findings. Surfaced as a
34
+ * separate code so SARIF/dashboard consumers can decide whether to
35
+ * auto-retry vs. surface as a hard error.
36
+ */
37
+ export interface BackfillError {
38
+ code: 'BACKFILL_GET_FAILED' | 'BACKFILL_GET_RETURNED_EMPTY';
39
+ message: string;
40
+ scanId: string;
41
+ summaryTotal: number;
42
+ }
43
+ export interface ScanResult {
44
+ scanId?: string;
45
+ passed: boolean;
46
+ findingsCount?: number;
47
+ findings?: unknown[];
48
+ summary?: unknown;
49
+ prompt?: string;
50
+ status?: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
51
+ backfillError?: BackfillError;
52
+ [key: string]: unknown;
53
+ }
54
+ interface ApiErrorBody {
55
+ status: 'error';
56
+ statusCode: number;
57
+ error?: {
58
+ type?: string;
59
+ message?: string;
60
+ suggestion?: string;
61
+ details?: {
62
+ maxBytes?: number;
63
+ maxFiles?: number;
64
+ chunkSizeBytes?: number;
65
+ receivedBytes?: number;
66
+ suggestedEndpoint?: string;
67
+ [key: string]: unknown;
68
+ };
69
+ };
70
+ }
71
+ /**
72
+ * Error thrown for any non-2xx response. Carries the parsed body + status so
73
+ * callers can branch on `details.suggestedEndpoint` (413 → chunked-session
74
+ * fallback) or `Retry-After` (429 / 503 → backoff + retry).
75
+ */
76
+ export declare class ApiError extends Error {
77
+ readonly statusCode: number;
78
+ readonly body: ApiErrorBody | null;
79
+ readonly retryAfterSeconds: number | null;
80
+ constructor(statusCode: number, body: ApiErrorBody | null, retryAfterSeconds: number | null, message: string);
81
+ }
19
82
  export declare class ComplianceApiClient {
20
83
  private apiUrl;
21
84
  private apiKey;
22
85
  constructor(apiUrl?: string, apiKey?: string);
23
- validate(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<any>;
24
- hook(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<any>;
25
- private post;
86
+ /**
87
+ * Synchronous validate. On a 413 with `details.suggestedEndpoint ===
88
+ * '/v1/compliance/scans'`, silently falls back to the chunked-session
89
+ * flow so large-repo CI jobs don't have to know the difference.
90
+ */
91
+ validate(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<ScanResult>;
92
+ /**
93
+ * Hook endpoint — small per-write call from coding agents. No
94
+ * suggestedEndpoint fallback because /hook keeps the historical 50 MB
95
+ * ceiling; if a single hook write exceeds that, the caller's batching
96
+ * is the bug to fix.
97
+ */
98
+ hook(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<ScanResult>;
99
+ /**
100
+ * Open a chunked scan session. Returns a `scanId` that subsequent
101
+ * `appendChunk` / `completeSession` calls reference. Server-side TTL is
102
+ * 30 minutes by default — abandoned sessions self-clean via the
103
+ * stale-session reaper.
104
+ */
105
+ openSession(frameworks: string[], options?: ScanOptions): Promise<{
106
+ scanId: string;
107
+ chunkSizeBytes: number;
108
+ maxFilesPerChunk: number;
109
+ expiresAt: string;
110
+ }>;
111
+ /**
112
+ * Append a chunk of files to an open session. Each call has its own
113
+ * /hook-style cap (50 MB / 2000 files). The server caches per-content
114
+ * findings, so re-scans of unchanged files are O(1).
115
+ */
116
+ appendChunk(scanId: string, files: Record<string, string>): Promise<{
117
+ filesScanned: number;
118
+ cachedFiles: number;
119
+ findingsAdded: number;
120
+ }>;
121
+ /**
122
+ * Finalize a chunked session: flips status to COMPLETED, computes
123
+ * summary + passed, returns final findings.
124
+ */
125
+ completeSession(scanId: string): Promise<ScanResult>;
126
+ /**
127
+ * High-level helper: open → append (in chunks) → complete. Returns the
128
+ * same shape as `validate()` so callers that auto-fallback don't have
129
+ * to special-case the result.
130
+ *
131
+ * Caller can pre-set `chunkMaxBytes` / `chunkMaxFiles` on `options.config`
132
+ * to override the conservative defaults.
133
+ */
134
+ validateChunked(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<ScanResult>;
135
+ /**
136
+ * Some server versions of `POST /scans/:id/complete` return only the summary,
137
+ * leaving `findings` empty even when `summary.total > 0`. The findings are
138
+ * persisted on the scan record and recoverable via `GET /scans/:id`. Call
139
+ * this after `completeSession` (and any other path where the response shape
140
+ * may be summary-only) so SARIF/JSON consumers always see structured findings,
141
+ * not just a count. No-op when findings are already present or the scan is
142
+ * genuinely clean.
143
+ *
144
+ * Timeout: the follow-up GET goes through `this.request`, which wraps every
145
+ * fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` (120 s default,
146
+ * tunable via `PC_REQUEST_TIMEOUT_MS`). A stalled server can't hang
147
+ * `validateChunked` indefinitely; if the abort fires, the catch below
148
+ * falls through with the original summary-only result.
149
+ */
150
+ private backfillFindingsIfMissing;
151
+ /**
152
+ * Async-validate: returns a `scanId` immediately; caller polls
153
+ * `getScan(scanId)` until status is COMPLETED or FAILED. Useful for CI
154
+ * runners that don't want to hold a connection for a 60 s scan.
155
+ */
156
+ validateAsync(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<{
157
+ scanId: string;
158
+ }>;
159
+ /**
160
+ * Fetch the current state of any scan (sync, async, or chunked-session).
161
+ */
162
+ getScan(scanId: string): Promise<ScanResult>;
163
+ /**
164
+ * High-level helper: kicks off an async-validate, polls until terminal,
165
+ * returns the same shape as `validate()`.
166
+ */
167
+ validateAndPoll(files: Record<string, string>, frameworks: string[], options?: ScanOptions): Promise<ScanResult>;
168
+ private buildOptions;
169
+ /**
170
+ * Single HTTP request with:
171
+ * - auth header
172
+ * - 429/503 + Retry-After honoring (up to MAX_RETRY_ATTEMPTS)
173
+ * - structured error with parsed body so callers can introspect
174
+ * `details.suggestedEndpoint` etc.
175
+ */
176
+ private request;
26
177
  }
178
+ export declare function chunkFiles(files: Record<string, string>, maxBytes: number, maxFiles: number): Record<string, string>[];
179
+ export {};