clud-bug 0.7.0-rc.21 → 0.7.0-rc.23
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/data/canonical-v1.json +39 -34
- package/data/rulesets/baseline.json +29 -0
- package/data/rulesets/clud-bug.json +39 -0
- package/data/rulesets/public-guard.json +29 -0
- package/data/rulesets/skdd.json +42 -0
- package/dist/cli/configure-github.d.ts +13 -4
- package/dist/cli/configure-github.d.ts.map +1 -1
- package/dist/cli/configure-github.js +73 -27
- package/dist/cli/configure-github.js.map +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +5 -1
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +33 -10
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/post-check-run.d.ts +2 -0
- package/dist/cli/post-check-run.d.ts.map +1 -1
- package/dist/cli/post-check-run.js +124 -5
- package/dist/cli/post-check-run.js.map +1 -1
- package/dist/cli/review-prompt.d.ts +11 -1
- package/dist/cli/review-prompt.d.ts.map +1 -1
- package/dist/cli/review-prompt.js +141 -37
- package/dist/cli/review-prompt.js.map +1 -1
- package/dist/core/check-verdict.d.ts +1 -1
- package/dist/core/check-verdict.d.ts.map +1 -1
- package/dist/core/check-verdict.js +14 -1
- package/dist/core/check-verdict.js.map +1 -1
- package/dist/core/configure-github.d.ts +184 -119
- package/dist/core/configure-github.d.ts.map +1 -1
- package/dist/core/configure-github.js +407 -209
- package/dist/core/configure-github.js.map +1 -1
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +12 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/invariants.d.ts +46 -0
- package/dist/core/invariants.d.ts.map +1 -0
- package/dist/core/invariants.js +96 -0
- package/dist/core/invariants.js.map +1 -0
- package/dist/core/notary-bundle.d.ts +95 -0
- package/dist/core/notary-bundle.d.ts.map +1 -0
- package/dist/core/notary-bundle.js +129 -0
- package/dist/core/notary-bundle.js.map +1 -0
- package/dist/core/notary-validate.d.ts +86 -0
- package/dist/core/notary-validate.d.ts.map +1 -0
- package/dist/core/notary-validate.js +270 -0
- package/dist/core/notary-validate.js.map +1 -0
- package/dist/core/prompt-builder.js +2 -2
- package/dist/core/review-schema-zod.d.ts +48 -0
- package/dist/core/review-schema-zod.d.ts.map +1 -1
- package/dist/core/review-schema-zod.js +5 -0
- package/dist/core/review-schema-zod.js.map +1 -1
- package/dist/core/review-schema.d.ts +3 -0
- package/dist/core/review-schema.d.ts.map +1 -1
- package/dist/core/review-schema.js +9 -0
- package/dist/core/review-schema.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
- package/src/cli/configure-github.ts +108 -31
- package/src/cli/hooks.ts +5 -1
- package/src/cli/main.ts +32 -10
- package/src/cli/post-check-run.ts +149 -5
- package/src/cli/review-prompt.ts +162 -36
- package/src/core/check-verdict.ts +14 -2
- package/src/core/configure-github.ts +591 -349
- package/src/core/index.ts +46 -0
- package/src/core/invariants.ts +123 -0
- package/src/core/notary-bundle.ts +196 -0
- package/src/core/notary-validate.ts +343 -0
- package/src/core/prompt-builder.ts +2 -2
- package/src/core/review-schema-zod.ts +5 -0
- package/src/core/review-schema.ts +12 -0
- package/src/core/version.ts +1 -1
- package/templates/skills/design/designing-elite-ui.md +57 -0
- package/templates/workflow-py.yml.tmpl +1 -1
- package/templates/workflow-ts.yml.tmpl +1 -1
- package/templates/workflow.yml.tmpl +1 -1
package/src/core/index.ts
CHANGED
|
@@ -214,7 +214,14 @@ export {
|
|
|
214
214
|
export {
|
|
215
215
|
applyCanonicalRuleset,
|
|
216
216
|
loadCanonicalV1,
|
|
217
|
+
loadPreset,
|
|
218
|
+
isPresetName,
|
|
219
|
+
PRESET_NAMES,
|
|
220
|
+
DEFAULT_PRESET,
|
|
221
|
+
CANONICAL_REPO_CONVENIENCES,
|
|
217
222
|
type CanonicalRuleset,
|
|
223
|
+
type PresetName,
|
|
224
|
+
type RepoConveniences,
|
|
218
225
|
type OctokitLike,
|
|
219
226
|
type ApplyCanonicalRulesetParams,
|
|
220
227
|
type ApplyResult,
|
|
@@ -291,6 +298,17 @@ export {
|
|
|
291
298
|
type DesignConfig,
|
|
292
299
|
type DesignGate,
|
|
293
300
|
} from './design.js';
|
|
301
|
+
// Phase R (clud-bug-app #87) — executable-probe invariants: config + in-scope gate.
|
|
302
|
+
// A probe is a repo-declared command that goes RED when a behavioral property is
|
|
303
|
+
// violated; RED output grounds a finding equal to a quoted diff line, catching the
|
|
304
|
+
// emergent / combinatorial / cross-cutting bugs the "quote-the-line" gate misses.
|
|
305
|
+
export {
|
|
306
|
+
readInvariantsConfig,
|
|
307
|
+
shouldRunProbes,
|
|
308
|
+
BUILTIN_INVARIANTS_CONFIG,
|
|
309
|
+
type Invariant,
|
|
310
|
+
type InvariantsConfig,
|
|
311
|
+
} from './invariants.js';
|
|
294
312
|
export {
|
|
295
313
|
readReviewContext,
|
|
296
314
|
extractPrContext,
|
|
@@ -309,6 +327,34 @@ export {
|
|
|
309
327
|
type DerivedCheck,
|
|
310
328
|
type DeriveCheckInput,
|
|
311
329
|
} from './check-verdict.js';
|
|
330
|
+
// Phase Z3 — the NOTARY. `notary-bundle` owns the attestation-bundle shape (the
|
|
331
|
+
// contract Z4's `/notarize` consumes) + a tolerant parser; `notary-validate` owns
|
|
332
|
+
// the pure deterministic ③④⑤ checks (coverage / grounding / consistency) both the
|
|
333
|
+
// local CLI and the server re-run. SPEC §10.3.3.
|
|
334
|
+
export {
|
|
335
|
+
buildBundle,
|
|
336
|
+
parseBundle,
|
|
337
|
+
notaryResponseIsRejection,
|
|
338
|
+
NOTARY_BUNDLE_VERSION,
|
|
339
|
+
NOTARY_PROTOCOL_VERSION,
|
|
340
|
+
type NotaryBundle,
|
|
341
|
+
type NotaryFinding,
|
|
342
|
+
type NotarySeverity,
|
|
343
|
+
type GroundingKind,
|
|
344
|
+
} from './notary-bundle.js';
|
|
345
|
+
export {
|
|
346
|
+
validateBundle,
|
|
347
|
+
validateCoverage,
|
|
348
|
+
validateGrounding,
|
|
349
|
+
validateConsistency,
|
|
350
|
+
spanAppearsInDiff,
|
|
351
|
+
splitUnifiedDiff,
|
|
352
|
+
type BundleValidation,
|
|
353
|
+
type CoverageResult,
|
|
354
|
+
type GroundingResult,
|
|
355
|
+
type GroundingViolation,
|
|
356
|
+
type ConsistencyResult,
|
|
357
|
+
} from './notary-validate.js';
|
|
312
358
|
export {
|
|
313
359
|
API_BASE,
|
|
314
360
|
MAX_SKILLS,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Executable-probe invariants: config + run-gate (Phase R / clud-bug-app #87).
|
|
2
|
+
//
|
|
3
|
+
// An INVARIANT is a repo-declared behavioral property paired with an executable
|
|
4
|
+
// PROBE: a shell command that exits non-zero (RED) when the property is violated.
|
|
5
|
+
// Unlike a prose `reviewContext` instruction — which is checked *statically*
|
|
6
|
+
// against the diff and so can never fire on a bug that lives in no single changed
|
|
7
|
+
// line — a probe is *run*, so it can ground the emergent / combinatorial /
|
|
8
|
+
// cross-cutting bugs the "quote-the-line" gate structurally misses. A finding
|
|
9
|
+
// fires only when a probe runs RED; RED output is trusted machine evidence, equal
|
|
10
|
+
// in standing to a quoted diff line.
|
|
11
|
+
//
|
|
12
|
+
// This module is the shared, pure brain (like `design.ts`): every surface resolves
|
|
13
|
+
// config + the in-scope gate here so policy can't fork. Whether a probe can
|
|
14
|
+
// actually be *executed* differs per surface (local/max: full shell; hosted
|
|
15
|
+
// serverless: static-degrade, no checkout; Action: a separate CI job outside the
|
|
16
|
+
// allowlist-sandboxed reviewer) — that is a consumer concern. This module only
|
|
17
|
+
// decides *whether* probes are in scope for a given diff + trigger.
|
|
18
|
+
|
|
19
|
+
import type { ReviewTrigger } from './plan-review.js';
|
|
20
|
+
|
|
21
|
+
/** A repo-declared behavioral property with an executable probe. */
|
|
22
|
+
export interface Invariant {
|
|
23
|
+
/** Human name, shown in the probe-results block + any resulting finding. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Glob(s) over changed paths; the probe is in scope only when the diff hits one. */
|
|
26
|
+
appliesTo: string[];
|
|
27
|
+
/** Shell command; a non-zero exit is RED (the property is violated). */
|
|
28
|
+
probe: string;
|
|
29
|
+
/** Optional expected-output / golden reference, surfaced in the prompt + transcript. */
|
|
30
|
+
expect?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolved `.clud-bug.json` `invariants` config (defaults applied). */
|
|
34
|
+
export interface InvariantsConfig {
|
|
35
|
+
/** Master switch. Default OFF — probes never run unless this is true. */
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
/** The validated invariant set (malformed entries dropped). */
|
|
38
|
+
invariants: Invariant[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Off-by-default builtin — the cost-control floor (probes build+run, so they cost). */
|
|
42
|
+
export const BUILTIN_INVARIANTS_CONFIG: InvariantsConfig = {
|
|
43
|
+
enabled: false,
|
|
44
|
+
invariants: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Parse + validate one raw invariant entry. Tolerant; returns null if unusable. */
|
|
48
|
+
function parseInvariant(raw: unknown): Invariant | null {
|
|
49
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
50
|
+
const o = raw as Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
const name = typeof o['name'] === 'string' && o['name'].trim() ? o['name'] : null;
|
|
53
|
+
const probe = typeof o['probe'] === 'string' && o['probe'].trim() ? o['probe'] : null;
|
|
54
|
+
if (!name || !probe) return null;
|
|
55
|
+
|
|
56
|
+
// appliesTo: accept a single glob string or an array of them; drop empties.
|
|
57
|
+
let appliesTo: string[] = [];
|
|
58
|
+
if (typeof o['appliesTo'] === 'string' && o['appliesTo'].trim()) {
|
|
59
|
+
appliesTo = [o['appliesTo']];
|
|
60
|
+
} else if (Array.isArray(o['appliesTo'])) {
|
|
61
|
+
appliesTo = o['appliesTo'].filter((g): g is string => typeof g === 'string' && g.trim().length > 0);
|
|
62
|
+
}
|
|
63
|
+
if (appliesTo.length === 0) return null; // no globs → cannot cost-gate → unusable
|
|
64
|
+
|
|
65
|
+
const invariant: Invariant = { name, appliesTo, probe };
|
|
66
|
+
if (typeof o['expect'] === 'string') invariant.expect = o['expect'];
|
|
67
|
+
return invariant;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read + normalize the `invariants` config from a parsed `.clud-bug.json` manifest.
|
|
72
|
+
*
|
|
73
|
+
* Accepts two authoring forms (tolerant, mirroring `readReviewPassesConfig`):
|
|
74
|
+
* - a bare array: `"invariants": [ { name, appliesTo, probe, expect? }, … ]`
|
|
75
|
+
* - a wrapper object: `"invariants": { "enabled": false, "list": [ … ] }` (a
|
|
76
|
+
* kill-switch that retains the config while turning probes off)
|
|
77
|
+
*
|
|
78
|
+
* A missing/malformed block, or one with zero *valid* invariants, resolves to the
|
|
79
|
+
* off-by-default builtin — a typo can never silently enable the cost-bearing probe
|
|
80
|
+
* run. Declaring at least one valid invariant is the explicit opt-in.
|
|
81
|
+
*/
|
|
82
|
+
export function readInvariantsConfig(manifest: unknown): InvariantsConfig {
|
|
83
|
+
const raw = (manifest as { invariants?: unknown } | null | undefined)?.invariants;
|
|
84
|
+
|
|
85
|
+
let list: unknown;
|
|
86
|
+
let enabledOverride: boolean | undefined;
|
|
87
|
+
if (Array.isArray(raw)) {
|
|
88
|
+
list = raw;
|
|
89
|
+
} else if (raw && typeof raw === 'object') {
|
|
90
|
+
const o = raw as Record<string, unknown>;
|
|
91
|
+
list = o['list'] ?? o['invariants'];
|
|
92
|
+
if (typeof o['enabled'] === 'boolean') enabledOverride = o['enabled'];
|
|
93
|
+
} else {
|
|
94
|
+
return { enabled: false, invariants: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const invariants = Array.isArray(list)
|
|
98
|
+
? list.map(parseInvariant).filter((i): i is Invariant => i !== null)
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
// Default ON when invariants are present; an explicit `enabled: false` disables
|
|
102
|
+
// while retaining them (kill-switch). Never enabled with zero valid invariants.
|
|
103
|
+
const enabled = (enabledOverride ?? true) && invariants.length > 0;
|
|
104
|
+
return { enabled, invariants };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Consumer-agnostic in-scope gate for the probe run. Pure.
|
|
109
|
+
*
|
|
110
|
+
* True only when the repo opted in (`enabled`), at least one invariant's
|
|
111
|
+
* `appliesTo` matched the changed paths (`applicableCount`, computed by the caller
|
|
112
|
+
* with the existing glob matcher), and this is a PR-level review — build+run is
|
|
113
|
+
* too expensive for the per-commit / per-push triggers. Consumers layer their own
|
|
114
|
+
* runtime preconditions on top (local: a shell + toolchain present; hosted:
|
|
115
|
+
* static-degrade; Action: a dedicated CI job).
|
|
116
|
+
*/
|
|
117
|
+
export function shouldRunProbes(
|
|
118
|
+
config: InvariantsConfig,
|
|
119
|
+
applicableCount: number,
|
|
120
|
+
trigger: ReviewTrigger,
|
|
121
|
+
): boolean {
|
|
122
|
+
return config.enabled && applicableCount > 0 && trigger === 'pr';
|
|
123
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Phase Z3 — the notary ATTESTATION BUNDLE.
|
|
2
|
+
//
|
|
3
|
+
// clud-bug is a NOTARY: it VALIDATES + CERTIFIES that a review actually ran
|
|
4
|
+
// before a green `clud-bug-review` check gates a merge (protocol SPEC §10.3.3).
|
|
5
|
+
// The bundle is the wire artifact the local CLI assembles from a completed
|
|
6
|
+
// review and submits to the notary (`POST /notarize`, built in Z4). The server
|
|
7
|
+
// re-validates it against GitHub's ground-truth diff, then — as the SOLE issuer
|
|
8
|
+
// — posts the pinned check. This module owns the bundle SHAPE (the contract Z4
|
|
9
|
+
// consumes) + a tolerant parser; the pure ①–⑤ checks live in `./notary-validate`.
|
|
10
|
+
//
|
|
11
|
+
// Design note (grounding): `grounding` is REQUIRED-for-critical HERE (enforced
|
|
12
|
+
// by `validateGrounding`), not in the shared review-output schema
|
|
13
|
+
// (`./review-schema`). A schema-`required` field can be satisfied with junk; the
|
|
14
|
+
// load-bearing guarantee is the notary re-checking the quoted span against the
|
|
15
|
+
// diff. Keeping the review-output field optional avoids a discriminated-union
|
|
16
|
+
// ripple through the App's finding aggregator and any coupling to the live
|
|
17
|
+
// hosted Zod pipeline. See docs/decisions-branches/feat-notary-z3.md.
|
|
18
|
+
|
|
19
|
+
import type { ReviewVerdict } from './check-verdict.js';
|
|
20
|
+
|
|
21
|
+
/** Bundle wire-format version. Bump on a breaking shape change. */
|
|
22
|
+
export const NOTARY_BUNDLE_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The protocol contract the bundle attests to — tracks SPEC §10.3.3
|
|
26
|
+
* ("Attestation integrity"). Bump in lockstep with a §10.3.3 contract change
|
|
27
|
+
* (coordinate the SPEC edit with logmind — the SPEC is the shared SkDD contract).
|
|
28
|
+
*/
|
|
29
|
+
export const NOTARY_PROTOCOL_VERSION = '1.2.0';
|
|
30
|
+
|
|
31
|
+
export type NotarySeverity = 'critical' | 'minor' | 'preexisting';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How a finding is grounded (mirrors the Phase R grounding forms: quote /
|
|
35
|
+
* reproduction / invariant). The notary deterministically verifies `quote`
|
|
36
|
+
* (the span must appear in the ground-truth diff); `reproduction`/`invariant`
|
|
37
|
+
* carry no diff-checkable artifact and are deferred to the clean-case audit.
|
|
38
|
+
*/
|
|
39
|
+
export type GroundingKind = 'quote' | 'reproduction' | 'invariant';
|
|
40
|
+
|
|
41
|
+
/** A single finding as carried in the attestation bundle. */
|
|
42
|
+
export interface NotaryFinding {
|
|
43
|
+
severity: NotarySeverity;
|
|
44
|
+
/** File path (repo-root-relative). Absent for a genuinely cross-cutting finding. */
|
|
45
|
+
file?: string;
|
|
46
|
+
/** 1-indexed line in `file`. */
|
|
47
|
+
line?: number;
|
|
48
|
+
summary: string;
|
|
49
|
+
/**
|
|
50
|
+
* Evidence anchoring the finding. REQUIRED for `critical` (the notary rejects
|
|
51
|
+
* a bare critical): a verbatim span from a changed line (`quote`), a
|
|
52
|
+
* reproduction's command+output (`reproduction`), or the violated-invariant
|
|
53
|
+
* statement (`invariant`).
|
|
54
|
+
*/
|
|
55
|
+
grounding?: string;
|
|
56
|
+
/** Which grounding form `grounding` uses (defaults to `quote` when absent). */
|
|
57
|
+
grounding_kind?: GroundingKind;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The attestation bundle — assembled locally from a completed review, submitted
|
|
62
|
+
* to the notary. Z4's `/notarize` consumes exactly this shape, re-validates ①–⑤
|
|
63
|
+
* against GitHub, then posts the pinned check.
|
|
64
|
+
*/
|
|
65
|
+
export interface NotaryBundle {
|
|
66
|
+
bundle_version: number;
|
|
67
|
+
/** `owner/repo`. */
|
|
68
|
+
repo: string;
|
|
69
|
+
/** PR number. Absent for a commit-trigger local pre-notarization (no PR yet). */
|
|
70
|
+
pr?: number;
|
|
71
|
+
head_sha: string;
|
|
72
|
+
verdict: ReviewVerdict;
|
|
73
|
+
findings: NotaryFinding[];
|
|
74
|
+
/**
|
|
75
|
+
* The changed-file paths the review covered. The notary set-diffs this against
|
|
76
|
+
* GitHub's ground-truth changed files (③ coverage) so a partial / stale-checkout
|
|
77
|
+
* review can't be certified as complete.
|
|
78
|
+
*/
|
|
79
|
+
coverage: string[];
|
|
80
|
+
/** The review recipe / CLI version that produced this bundle (provenance). */
|
|
81
|
+
recipe_version: string;
|
|
82
|
+
/** The SPEC §10.3.3 contract version this bundle conforms to. */
|
|
83
|
+
protocol_version: string;
|
|
84
|
+
/**
|
|
85
|
+
* Single-use nonce bound to (repo, PR, head_sha), minted by the server
|
|
86
|
+
* (`POST /challenge`, Z4). Absent on a locally pre-built bundle; the server
|
|
87
|
+
* requires it before certifying (① replay-closure).
|
|
88
|
+
*/
|
|
89
|
+
nonce?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Assemble a bundle from a completed review, stamping the wire + protocol versions. */
|
|
93
|
+
export function buildBundle(input: {
|
|
94
|
+
repo: string;
|
|
95
|
+
pr?: number;
|
|
96
|
+
headSha: string;
|
|
97
|
+
verdict: ReviewVerdict;
|
|
98
|
+
findings: NotaryFinding[];
|
|
99
|
+
coverage: string[];
|
|
100
|
+
recipeVersion: string;
|
|
101
|
+
nonce?: string;
|
|
102
|
+
}): NotaryBundle {
|
|
103
|
+
return {
|
|
104
|
+
bundle_version: NOTARY_BUNDLE_VERSION,
|
|
105
|
+
repo: input.repo,
|
|
106
|
+
...(input.pr !== undefined ? { pr: input.pr } : {}),
|
|
107
|
+
head_sha: input.headSha,
|
|
108
|
+
verdict: input.verdict,
|
|
109
|
+
findings: input.findings,
|
|
110
|
+
coverage: input.coverage,
|
|
111
|
+
recipe_version: input.recipeVersion,
|
|
112
|
+
protocol_version: NOTARY_PROTOCOL_VERSION,
|
|
113
|
+
...(input.nonce !== undefined ? { nonce: input.nonce } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const SEVERITIES: ReadonlySet<string> = new Set(['critical', 'minor', 'preexisting']);
|
|
118
|
+
const GROUNDING_KINDS: ReadonlySet<string> = new Set(['quote', 'reproduction', 'invariant']);
|
|
119
|
+
const VERDICTS: ReadonlySet<string> = new Set(['clean', 'critical', 'failed', 'unverified']);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Classify a notary `/notarize` HTTP response for the submit path. A definitive
|
|
123
|
+
* client error (4xx) is the server AUTHORITATIVELY refusing to certify this
|
|
124
|
+
* bundle → terminal (post NO check). A 5xx or a network error is the endpoint
|
|
125
|
+
* being DOWN, not a verdict → the caller may fall back to the self-attested
|
|
126
|
+
* check. Conflating the two would let a server "no" be overridden by a local green.
|
|
127
|
+
*/
|
|
128
|
+
export function notaryResponseIsRejection(status: number): boolean {
|
|
129
|
+
return status >= 400 && status < 500;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseFinding(raw: unknown): NotaryFinding | null {
|
|
133
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
134
|
+
const r = raw as Record<string, unknown>;
|
|
135
|
+
if (typeof r['severity'] !== 'string' || !SEVERITIES.has(r['severity'])) return null;
|
|
136
|
+
if (typeof r['summary'] !== 'string' || !r['summary']) return null;
|
|
137
|
+
const finding: NotaryFinding = {
|
|
138
|
+
severity: r['severity'] as NotarySeverity,
|
|
139
|
+
summary: r['summary'],
|
|
140
|
+
};
|
|
141
|
+
if (typeof r['file'] === 'string' && r['file']) finding.file = r['file'];
|
|
142
|
+
if (typeof r['line'] === 'number' && Number.isInteger(r['line']) && r['line'] >= 1) {
|
|
143
|
+
finding.line = r['line'];
|
|
144
|
+
}
|
|
145
|
+
if (typeof r['grounding'] === 'string' && r['grounding']) finding.grounding = r['grounding'];
|
|
146
|
+
if (typeof r['grounding_kind'] === 'string' && GROUNDING_KINDS.has(r['grounding_kind'])) {
|
|
147
|
+
finding.grounding_kind = r['grounding_kind'] as GroundingKind;
|
|
148
|
+
}
|
|
149
|
+
return finding;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Tolerant parser for a bundle read from disk / the wire. Returns null on a
|
|
154
|
+
* missing or malformed required field (never throws) — the CLI treats a null as
|
|
155
|
+
* "no submittable bundle" and falls back to the self-attested path. A malformed
|
|
156
|
+
* finding is DROPPED, not tolerated silently into a green: if any finding is
|
|
157
|
+
* unparseable the whole bundle is rejected (null), so a truncated payload can't
|
|
158
|
+
* be certified as if it were complete.
|
|
159
|
+
*/
|
|
160
|
+
export function parseBundle(raw: unknown): NotaryBundle | null {
|
|
161
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
162
|
+
const r = raw as Record<string, unknown>;
|
|
163
|
+
if (typeof r['repo'] !== 'string' || !r['repo'].includes('/')) return null;
|
|
164
|
+
if (typeof r['head_sha'] !== 'string' || !r['head_sha']) return null;
|
|
165
|
+
// `verdict` is enum-typed — whitelist it like every other enum field, so a
|
|
166
|
+
// garbage verdict is a malformed bundle (null), never blind-cast through the
|
|
167
|
+
// trust boundary into validateConsistency / a derived check.
|
|
168
|
+
if (typeof r['verdict'] !== 'string' || !VERDICTS.has(r['verdict'])) return null;
|
|
169
|
+
if (!Array.isArray(r['findings'])) return null;
|
|
170
|
+
if (!Array.isArray(r['coverage'])) return null;
|
|
171
|
+
|
|
172
|
+
const findings: NotaryFinding[] = [];
|
|
173
|
+
for (const f of r['findings']) {
|
|
174
|
+
const parsed = parseFinding(f);
|
|
175
|
+
if (!parsed) return null; // reject the whole bundle rather than silently drop a finding
|
|
176
|
+
findings.push(parsed);
|
|
177
|
+
}
|
|
178
|
+
const coverage = r['coverage'].filter((c): c is string => typeof c === 'string');
|
|
179
|
+
if (coverage.length !== r['coverage'].length) return null;
|
|
180
|
+
|
|
181
|
+
const bundle: NotaryBundle = {
|
|
182
|
+
bundle_version:
|
|
183
|
+
typeof r['bundle_version'] === 'number' ? r['bundle_version'] : NOTARY_BUNDLE_VERSION,
|
|
184
|
+
repo: r['repo'],
|
|
185
|
+
head_sha: r['head_sha'],
|
|
186
|
+
verdict: r['verdict'] as ReviewVerdict,
|
|
187
|
+
findings,
|
|
188
|
+
coverage,
|
|
189
|
+
recipe_version: typeof r['recipe_version'] === 'string' ? r['recipe_version'] : '',
|
|
190
|
+
protocol_version:
|
|
191
|
+
typeof r['protocol_version'] === 'string' ? r['protocol_version'] : NOTARY_PROTOCOL_VERSION,
|
|
192
|
+
};
|
|
193
|
+
if (typeof r['pr'] === 'number' && Number.isInteger(r['pr'])) bundle.pr = r['pr'];
|
|
194
|
+
if (typeof r['nonce'] === 'string' && r['nonce']) bundle.nonce = r['nonce'];
|
|
195
|
+
return bundle;
|
|
196
|
+
}
|