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.
Files changed (78) hide show
  1. package/data/canonical-v1.json +39 -34
  2. package/data/rulesets/baseline.json +29 -0
  3. package/data/rulesets/clud-bug.json +39 -0
  4. package/data/rulesets/public-guard.json +29 -0
  5. package/data/rulesets/skdd.json +42 -0
  6. package/dist/cli/configure-github.d.ts +13 -4
  7. package/dist/cli/configure-github.d.ts.map +1 -1
  8. package/dist/cli/configure-github.js +73 -27
  9. package/dist/cli/configure-github.js.map +1 -1
  10. package/dist/cli/hooks.d.ts.map +1 -1
  11. package/dist/cli/hooks.js +5 -1
  12. package/dist/cli/hooks.js.map +1 -1
  13. package/dist/cli/main.d.ts.map +1 -1
  14. package/dist/cli/main.js +33 -10
  15. package/dist/cli/main.js.map +1 -1
  16. package/dist/cli/post-check-run.d.ts +2 -0
  17. package/dist/cli/post-check-run.d.ts.map +1 -1
  18. package/dist/cli/post-check-run.js +124 -5
  19. package/dist/cli/post-check-run.js.map +1 -1
  20. package/dist/cli/review-prompt.d.ts +11 -1
  21. package/dist/cli/review-prompt.d.ts.map +1 -1
  22. package/dist/cli/review-prompt.js +141 -37
  23. package/dist/cli/review-prompt.js.map +1 -1
  24. package/dist/core/check-verdict.d.ts +1 -1
  25. package/dist/core/check-verdict.d.ts.map +1 -1
  26. package/dist/core/check-verdict.js +14 -1
  27. package/dist/core/check-verdict.js.map +1 -1
  28. package/dist/core/configure-github.d.ts +184 -119
  29. package/dist/core/configure-github.d.ts.map +1 -1
  30. package/dist/core/configure-github.js +407 -209
  31. package/dist/core/configure-github.js.map +1 -1
  32. package/dist/core/index.d.ts +4 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/core/index.js +12 -1
  35. package/dist/core/index.js.map +1 -1
  36. package/dist/core/invariants.d.ts +46 -0
  37. package/dist/core/invariants.d.ts.map +1 -0
  38. package/dist/core/invariants.js +96 -0
  39. package/dist/core/invariants.js.map +1 -0
  40. package/dist/core/notary-bundle.d.ts +95 -0
  41. package/dist/core/notary-bundle.d.ts.map +1 -0
  42. package/dist/core/notary-bundle.js +129 -0
  43. package/dist/core/notary-bundle.js.map +1 -0
  44. package/dist/core/notary-validate.d.ts +86 -0
  45. package/dist/core/notary-validate.d.ts.map +1 -0
  46. package/dist/core/notary-validate.js +270 -0
  47. package/dist/core/notary-validate.js.map +1 -0
  48. package/dist/core/prompt-builder.js +2 -2
  49. package/dist/core/review-schema-zod.d.ts +48 -0
  50. package/dist/core/review-schema-zod.d.ts.map +1 -1
  51. package/dist/core/review-schema-zod.js +5 -0
  52. package/dist/core/review-schema-zod.js.map +1 -1
  53. package/dist/core/review-schema.d.ts +3 -0
  54. package/dist/core/review-schema.d.ts.map +1 -1
  55. package/dist/core/review-schema.js +9 -0
  56. package/dist/core/review-schema.js.map +1 -1
  57. package/dist/core/version.d.ts +1 -1
  58. package/dist/core/version.js +1 -1
  59. package/package.json +1 -1
  60. package/src/cli/configure-github.ts +108 -31
  61. package/src/cli/hooks.ts +5 -1
  62. package/src/cli/main.ts +32 -10
  63. package/src/cli/post-check-run.ts +149 -5
  64. package/src/cli/review-prompt.ts +162 -36
  65. package/src/core/check-verdict.ts +14 -2
  66. package/src/core/configure-github.ts +591 -349
  67. package/src/core/index.ts +46 -0
  68. package/src/core/invariants.ts +123 -0
  69. package/src/core/notary-bundle.ts +196 -0
  70. package/src/core/notary-validate.ts +343 -0
  71. package/src/core/prompt-builder.ts +2 -2
  72. package/src/core/review-schema-zod.ts +5 -0
  73. package/src/core/review-schema.ts +12 -0
  74. package/src/core/version.ts +1 -1
  75. package/templates/skills/design/designing-elite-ui.md +57 -0
  76. package/templates/workflow-py.yml.tmpl +1 -1
  77. package/templates/workflow-ts.yml.tmpl +1 -1
  78. 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
+ }