reverb-impact 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1027 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { analysisId, analysisSupersessionKey, ACTION_LABELS, contentHash, createSuppressionRule, decidePromotion, EDGE_LABELS, enumValue, evaluateCorpus, findingFingerprint, generationId, generationLeaseId, hashCanonical, IMPACT_LABELS, instant, overlayId, policyRevision, REVIEW_REASON_CODES, REVIEW_ROLES, reviewEventId, simulateFrozenPolicy, SUPPRESSION_SCOPES, } from '@yanib/reverb-domain';
4
+ import { AnalyzePullRequest, CreatePullRequestOverlay, IndexRepositoryGeneration, RecordReview, } from '@yanib/reverb-application';
5
+ import { materializeContractChanges } from '@yanib/reverb-adapter-sdk';
6
+ import { AlwaysCurrentCancellation, createSystemId, LocalArtifactObjectCache, LocalGitRepositoryReader, LocalWorkspaceConfig, NoopTelemetry, SystemClock, } from '@yanib/reverb-host-local';
7
+ import { SqliteStore } from '@yanib/reverb-storage-sqlite';
8
+ import { corpusManifestSchema, impactCaseSchema, reviewEventSchema, SchemaValidationError, suppressionRuleSchema, validateWithSchema, } from '@yanib/reverb-schema';
9
+ import { Command } from 'commander';
10
+ import { ensureContractObservation, extractContractsAtCommit, INITIAL_ADAPTERS, } from './contracts.js';
11
+ const INDEXER_BUNDLE_VERSION = 'foundation-1.0.0';
12
+ function leaseExpiry(now) {
13
+ return instant(new Date(new Date(now).valueOf() + 15 * 60_000).toISOString());
14
+ }
15
+ function repositoryByAlias(repositories, alias) {
16
+ const repository = repositories.find((candidate) => candidate.alias === alias);
17
+ if (!repository)
18
+ throw new Error(`Repository alias is not configured: ${alias}`);
19
+ return repository;
20
+ }
21
+ async function withStore(workspaceRoot, operation) {
22
+ const store = new SqliteStore(resolve(workspaceRoot, '.reverb/reverb.sqlite'));
23
+ try {
24
+ return await operation(store);
25
+ }
26
+ finally {
27
+ store.close();
28
+ }
29
+ }
30
+ async function indexRepository(input) {
31
+ const commit = await input.reader.resolveCommit(input.repository.repositoryId, input.ref);
32
+ if (!commit.ok)
33
+ throw new Error(commit.failure.safeMessage);
34
+ const previous = await input.store.selectGeneration({
35
+ workspaceId: input.workspace.snapshot.revision.workspaceId,
36
+ repositoryId: input.repository.repositoryId,
37
+ allowPartial: true,
38
+ });
39
+ const now = input.clock.now();
40
+ const indexer = new IndexRepositoryGeneration({
41
+ reader: input.reader,
42
+ store: input.store,
43
+ cache: input.cache,
44
+ clock: input.clock,
45
+ telemetry: input.telemetry,
46
+ cancellation: input.cancellation,
47
+ });
48
+ const result = await indexer.execute({
49
+ generationId: generationId(createSystemId('gen', now)),
50
+ leaseId: generationLeaseId(createSystemId('lea', now)),
51
+ leaseExpiresAt: leaseExpiry(now),
52
+ workspaceId: input.workspace.snapshot.revision.workspaceId,
53
+ registryRevision: input.workspace.snapshot.revision.revision,
54
+ repositoryId: input.repository.repositoryId,
55
+ commitSha: commit.value.sha,
56
+ configRevision: input.workspace.snapshot.revision.configRevision,
57
+ indexerBundleVersion: INDEXER_BUNDLE_VERSION,
58
+ ...(previous.ok && previous.value.state === 'selected'
59
+ ? { previousGenerationId: previous.value.generation.id }
60
+ : {}),
61
+ });
62
+ if (!result.ok)
63
+ throw new Error(result.failure.safeMessage);
64
+ const observation = await ensureContractObservation({
65
+ reader: input.reader,
66
+ generations: input.store,
67
+ evidence: input.store,
68
+ registry: input.workspace.snapshot,
69
+ workspaceId: input.workspace.snapshot.revision.workspaceId,
70
+ repositoryId: input.repository.repositoryId,
71
+ generationId: result.value.generationId,
72
+ commitSha: commit.value.sha,
73
+ observedAt: input.clock.now(),
74
+ });
75
+ return {
76
+ alias: input.repository.alias,
77
+ commitSha: commit.value.sha,
78
+ ...result.value,
79
+ contractCoverage: observation.coverageState,
80
+ definitionCount: observation.definitions.length,
81
+ referenceCount: observation.references.length,
82
+ observation,
83
+ };
84
+ }
85
+ function snakeKey(value) {
86
+ return value.replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`);
87
+ }
88
+ function canonicalProjection(value) {
89
+ if (Array.isArray(value))
90
+ return value.map(canonicalProjection);
91
+ if (value !== null && typeof value === 'object') {
92
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [snakeKey(key), canonicalProjection(nested)]));
93
+ }
94
+ return value;
95
+ }
96
+ function domainProjection(value) {
97
+ if (Array.isArray(value))
98
+ return value.map(domainProjection);
99
+ if (value !== null && typeof value === 'object') {
100
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
101
+ key.replace(/_([a-z])/g, (_, character) => character.toUpperCase()),
102
+ domainProjection(nested),
103
+ ]));
104
+ }
105
+ return value;
106
+ }
107
+ async function readJson(path) {
108
+ return JSON.parse(await readFile(resolve(path), 'utf8'));
109
+ }
110
+ function record(value, subject) {
111
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
112
+ throw new Error(`${subject} must be a JSON object.`);
113
+ }
114
+ return value;
115
+ }
116
+ async function loadCorpusBundle(path) {
117
+ const bundle = record(await readJson(path), 'Corpus bundle');
118
+ const manifestWire = bundle.manifest;
119
+ const casesWire = bundle.cases;
120
+ if (!Array.isArray(casesWire))
121
+ throw new Error('Corpus bundle cases must be an array.');
122
+ try {
123
+ validateWithSchema(corpusManifestSchema.$id, manifestWire);
124
+ casesWire.forEach((value) => validateWithSchema(impactCaseSchema.$id, value));
125
+ }
126
+ catch (error) {
127
+ if (error instanceof SchemaValidationError) {
128
+ throw new Error(`${error.message} ${error.validationErrors
129
+ .map((value) => `${value.instancePath || '/'} ${value.message ?? 'is invalid'}`)
130
+ .map((message, index) => {
131
+ const parameters = error.validationErrors[index]?.params;
132
+ return parameters === undefined ? message : `${message} ${JSON.stringify(parameters)}`;
133
+ })
134
+ .join('; ')}`);
135
+ }
136
+ throw error;
137
+ }
138
+ return {
139
+ manifest: domainProjection(manifestWire),
140
+ cases: domainProjection(casesWire),
141
+ };
142
+ }
143
+ function frozenPolicy(value) {
144
+ const projected = record(domainProjection(value), 'Frozen policy');
145
+ const strata = projected.allowedStrata;
146
+ const impacts = projected.allowedImpactClaims;
147
+ if (!Array.isArray(strata) || !strata.every((item) => typeof item === 'string')) {
148
+ throw new Error('Frozen policy allowed_strata must be a string array.');
149
+ }
150
+ if (!Array.isArray(impacts) ||
151
+ !impacts.every((item) => item === 'breaking' || item === 'behavior_risk')) {
152
+ throw new Error('Frozen policy allowed_impact_claims are invalid.');
153
+ }
154
+ if (typeof projected.respectFrozenSuppressions !== 'boolean' ||
155
+ typeof projected.maximumAlertsPerThousand !== 'number') {
156
+ throw new Error('Frozen policy suppression and alert-budget fields are required.');
157
+ }
158
+ return {
159
+ revision: policyRevision(String(projected.revision)),
160
+ allowedStrata: strata,
161
+ allowedImpactClaims: impacts,
162
+ respectFrozenSuppressions: projected.respectFrozenSuppressions,
163
+ maximumAlertsPerThousand: projected.maximumAlertsPerThousand,
164
+ };
165
+ }
166
+ function pageOptions(limitValue, cursorValue) {
167
+ const limit = Number(limitValue);
168
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
169
+ throw new Error('Finding limit must be an integer from 1 through 100.');
170
+ }
171
+ if (cursorValue === undefined)
172
+ return { limit, offset: 0 };
173
+ const match = /^offset:([0-9]+)$/.exec(cursorValue);
174
+ if (match === null)
175
+ throw new Error('Finding cursor is malformed.');
176
+ return { limit, offset: Number(match[1]) };
177
+ }
178
+ export async function createCli() {
179
+ const program = new Command()
180
+ .name('reverb')
181
+ .description('Evidence-first cross-repository pull-request impact analysis')
182
+ .version('0.1.0');
183
+ program
184
+ .command('init')
185
+ .argument('[path]', 'workspace root', '.')
186
+ .option('--name <name>', 'workspace display name')
187
+ .action(async (path, options) => {
188
+ const workspace = await LocalWorkspaceConfig.initialize(path, {
189
+ ...(options.name ? { name: options.name } : {}),
190
+ });
191
+ await withStore(workspace.root, async (store) => {
192
+ const result = await store.putRevision(workspace.snapshot);
193
+ if (!result.ok)
194
+ throw new Error(result.failure.safeMessage);
195
+ });
196
+ process.stdout.write(`${JSON.stringify({ workspace_id: workspace.snapshot.revision.workspaceId, root: workspace.root })}\n`);
197
+ });
198
+ const workspace = program
199
+ .command('workspace')
200
+ .description('manage explicit repository membership');
201
+ workspace
202
+ .command('add')
203
+ .argument('<repo-path>')
204
+ .requiredOption('--alias <alias>')
205
+ .action(async (path, options) => {
206
+ const current = await LocalWorkspaceConfig.load(process.cwd());
207
+ const updated = await LocalWorkspaceConfig.addRepository(current, path, options.alias);
208
+ await withStore(updated.root, async (store) => {
209
+ const result = await store.putRevision(updated.snapshot);
210
+ if (!result.ok)
211
+ throw new Error(result.failure.safeMessage);
212
+ });
213
+ process.stdout.write(`${updated.snapshot.revision.revision}\n`);
214
+ });
215
+ workspace
216
+ .command('remove')
217
+ .argument('<alias>')
218
+ .action(async (alias) => {
219
+ const current = await LocalWorkspaceConfig.load(process.cwd());
220
+ const updated = await LocalWorkspaceConfig.removeRepository(current, alias);
221
+ await withStore(updated.root, async (store) => {
222
+ const result = await store.putRevision(updated.snapshot);
223
+ if (!result.ok)
224
+ throw new Error(result.failure.safeMessage);
225
+ });
226
+ process.stdout.write(`${updated.snapshot.revision.revision}\n`);
227
+ });
228
+ const registry = program.command('registry').description('manage the service registry');
229
+ registry.command('validate').action(async () => {
230
+ const current = await LocalWorkspaceConfig.load(process.cwd());
231
+ process.stdout.write(`${JSON.stringify({ revision: current.snapshot.revision.revision, repositories: current.snapshot.repositories.length, services: current.snapshot.services.length, aliases: current.snapshot.aliases.length })}\n`);
232
+ });
233
+ registry
234
+ .command('service-add')
235
+ .requiredOption('--id <id>', 'stable service ID')
236
+ .requiredOption('--repo <alias>', 'repository alias')
237
+ .requiredOption('--root <path>', 'repository-relative service root')
238
+ .requiredOption('--environment <environment>')
239
+ .requiredOption('--owner <owner>')
240
+ .action(async (options) => {
241
+ const current = await LocalWorkspaceConfig.load(process.cwd());
242
+ const updated = await LocalWorkspaceConfig.addService(current, {
243
+ id: options.id,
244
+ repositoryAlias: options.repo,
245
+ rootPath: options.root,
246
+ environment: options.environment,
247
+ owner: options.owner,
248
+ });
249
+ await withStore(updated.root, async (store) => {
250
+ const result = await store.putRevision(updated.snapshot);
251
+ if (!result.ok)
252
+ throw new Error(result.failure.safeMessage);
253
+ });
254
+ process.stdout.write(`${updated.snapshot.revision.revision}\n`);
255
+ });
256
+ registry
257
+ .command('alias-add')
258
+ .requiredOption('--service <id>', 'stable service ID')
259
+ .requiredOption('--kind <kind>', 'alias kind')
260
+ .requiredOption('--value <value>', 'alias value')
261
+ .requiredOption('--environment <environment>')
262
+ .requiredOption('--owner <owner>')
263
+ .option('--path-prefix <path>', 'explicit gateway prefix to strip')
264
+ .action(async (options) => {
265
+ const current = await LocalWorkspaceConfig.load(process.cwd());
266
+ const updated = await LocalWorkspaceConfig.addServiceAlias(current, {
267
+ serviceId: options.service,
268
+ kind: options.kind,
269
+ value: options.value,
270
+ environment: options.environment,
271
+ owner: options.owner,
272
+ ...(options.pathPrefix === undefined ? {} : { pathPrefix: options.pathPrefix }),
273
+ });
274
+ await withStore(updated.root, async (store) => {
275
+ const result = await store.putRevision(updated.snapshot);
276
+ if (!result.ok)
277
+ throw new Error(result.failure.safeMessage);
278
+ });
279
+ process.stdout.write(`${updated.snapshot.revision.revision}\n`);
280
+ });
281
+ program
282
+ .command('index')
283
+ .option('--repo <alias>', 'one repository alias')
284
+ .option('--ref <ref>', 'Git ref, resolved to an exact commit')
285
+ .option('--json', 'emit canonical machine output')
286
+ .action(async (options) => {
287
+ const current = await LocalWorkspaceConfig.load(process.cwd());
288
+ const repositories = options.repo
289
+ ? [repositoryByAlias(current.snapshot.repositories, options.repo)]
290
+ : current.snapshot.repositories.filter((repository) => repository.selected);
291
+ const reader = new LocalGitRepositoryReader(LocalWorkspaceConfig.repositoryBindings(current));
292
+ const cache = new LocalArtifactObjectCache(resolve(current.root, '.reverb/objects'));
293
+ const clock = new SystemClock();
294
+ const telemetry = new NoopTelemetry();
295
+ const cancellation = new AlwaysCurrentCancellation();
296
+ const results = await withStore(current.root, async (store) => {
297
+ const registryWrite = await store.putRevision(current.snapshot);
298
+ if (!registryWrite.ok)
299
+ throw new Error(registryWrite.failure.safeMessage);
300
+ const indexed = [];
301
+ for (const repository of repositories) {
302
+ const result = await indexRepository({
303
+ workspace: current,
304
+ repository,
305
+ ref: options.ref ?? repository.defaultBranch,
306
+ reader,
307
+ store,
308
+ cache,
309
+ clock,
310
+ telemetry,
311
+ cancellation,
312
+ });
313
+ indexed.push({
314
+ alias: result.alias,
315
+ commit_sha: result.commitSha,
316
+ generationId: result.generationId,
317
+ state: result.state,
318
+ artifactCount: result.artifactCount,
319
+ reusedArtifactCount: result.reusedArtifactCount,
320
+ coverage: result.coverage,
321
+ diagnostics: result.diagnostics,
322
+ artifactResultHash: result.artifactResultHash,
323
+ contract_coverage: result.contractCoverage,
324
+ definition_count: result.definitionCount,
325
+ reference_count: result.referenceCount,
326
+ });
327
+ }
328
+ return indexed;
329
+ });
330
+ if (options.json)
331
+ process.stdout.write(`${JSON.stringify(results)}\n`);
332
+ else {
333
+ for (const result of results) {
334
+ process.stdout.write(`${result.alias}: ${result.state} ${result.artifactCount} artifacts, ${result.definition_count} definitions, ${result.reference_count} references at ${result.commit_sha} (${result.contract_coverage} contract coverage)\n`);
335
+ }
336
+ }
337
+ });
338
+ program
339
+ .command('analyze')
340
+ .description('preview exact base-to-head cross-repository impact')
341
+ .requiredOption('--repo <alias>', 'producer repository alias')
342
+ .requiredOption('--base <ref>', 'exact base SHA or resolvable Git ref')
343
+ .requiredOption('--head <ref>', 'exact head SHA or resolvable Git ref')
344
+ .option('--pr-number <number>', 'stable local pull request number')
345
+ .option('--limit <count>', 'maximum findings returned', '50')
346
+ .option('--cursor <cursor>', 'pagination cursor')
347
+ .option('--json', 'emit canonical machine output')
348
+ .action(async (options) => {
349
+ const current = await LocalWorkspaceConfig.load(process.cwd());
350
+ const repository = repositoryByAlias(current.snapshot.repositories, options.repo);
351
+ const reader = new LocalGitRepositoryReader(LocalWorkspaceConfig.repositoryBindings(current));
352
+ const cache = new LocalArtifactObjectCache(resolve(current.root, '.reverb/objects'));
353
+ const clock = new SystemClock();
354
+ const telemetry = new NoopTelemetry();
355
+ const cancellation = new AlwaysCurrentCancellation();
356
+ const baseCommit = await reader.resolveCommit(repository.repositoryId, options.base);
357
+ if (!baseCommit.ok)
358
+ throw new Error(baseCommit.failure.safeMessage);
359
+ const headCommit = await reader.resolveCommit(repository.repositoryId, options.head);
360
+ if (!headCommit.ok)
361
+ throw new Error(headCommit.failure.safeMessage);
362
+ if (baseCommit.value.sha === headCommit.value.sha) {
363
+ throw new Error('Analysis base and head must be different exact commits.');
364
+ }
365
+ const pullRequestNumber = options.prNumber === undefined ? undefined : Number(options.prNumber);
366
+ if (pullRequestNumber !== undefined &&
367
+ (!Number.isSafeInteger(pullRequestNumber) || pullRequestNumber < 1)) {
368
+ throw new Error('Pull request number must be a positive integer.');
369
+ }
370
+ const paging = pageOptions(options.limit, options.cursor);
371
+ const result = await withStore(current.root, async (store) => {
372
+ const registryWrite = await store.putRevision(current.snapshot);
373
+ if (!registryWrite.ok)
374
+ throw new Error(registryWrite.failure.safeMessage);
375
+ const common = {
376
+ workspace: current,
377
+ repository,
378
+ reader,
379
+ store,
380
+ cache,
381
+ clock,
382
+ telemetry,
383
+ cancellation,
384
+ };
385
+ const base = await indexRepository({ ...common, ref: baseCommit.value.sha });
386
+ const head = await indexRepository({ ...common, ref: headCommit.value.sha });
387
+ const baseExtraction = await extractContractsAtCommit({
388
+ reader,
389
+ generations: store,
390
+ registry: current.snapshot,
391
+ repositoryId: repository.repositoryId,
392
+ generationId: base.generationId,
393
+ commitSha: baseCommit.value.sha,
394
+ observedAt: clock.now(),
395
+ });
396
+ const headExtraction = await extractContractsAtCommit({
397
+ reader,
398
+ generations: store,
399
+ registry: current.snapshot,
400
+ repositoryId: repository.repositoryId,
401
+ generationId: head.generationId,
402
+ commitSha: headCommit.value.sha,
403
+ observedAt: clock.now(),
404
+ });
405
+ const diffs = await Promise.all(INITIAL_ADAPTERS.map((adapter, index) => adapter.diff({
406
+ base: baseExtraction.extractions[index],
407
+ head: headExtraction.extractions[index],
408
+ configRevision: current.snapshot.revision.configRevision,
409
+ context: {},
410
+ })));
411
+ const changes = materializeContractChanges({
412
+ workspaceId: current.snapshot.revision.workspaceId,
413
+ producerRepositoryId: repository.repositoryId,
414
+ baseGenerationId: base.generationId,
415
+ headGenerationId: head.generationId,
416
+ baseSha: baseCommit.value.sha,
417
+ headSha: headCommit.value.sha,
418
+ diffs,
419
+ });
420
+ const policyMajor = 1;
421
+ const policy = policyRevision(`pol_${hashCanonical({ policyMajor, mode: 'local_preview' })}`);
422
+ const runKey = analysisSupersessionKey({
423
+ workspaceId: current.snapshot.revision.workspaceId,
424
+ producerRepositoryId: repository.repositoryId,
425
+ provider: 'local',
426
+ ...(pullRequestNumber === undefined ? {} : { pullRequestNumber }),
427
+ policyMajor,
428
+ });
429
+ const overlay = overlayId(createSystemId('ovl', clock.now()));
430
+ const overlayBuilder = new CreatePullRequestOverlay({
431
+ reader,
432
+ store,
433
+ clock,
434
+ telemetry,
435
+ cancellation,
436
+ });
437
+ const overlayResult = await overlayBuilder.execute({
438
+ overlayId: overlay,
439
+ leaseId: generationLeaseId(createSystemId('lea', clock.now())),
440
+ leaseExpiresAt: leaseExpiry(clock.now()),
441
+ workspaceId: current.snapshot.revision.workspaceId,
442
+ registryRevision: current.snapshot.revision.revision,
443
+ repositoryId: repository.repositoryId,
444
+ baseGenerationId: base.generationId,
445
+ baseSha: baseCommit.value.sha,
446
+ headSha: headCommit.value.sha,
447
+ configRevision: current.snapshot.revision.configRevision,
448
+ indexerBundleVersion: INDEXER_BUNDLE_VERSION,
449
+ supersessionKey: runKey,
450
+ });
451
+ if (!overlayResult.ok)
452
+ throw new Error(overlayResult.failure.safeMessage);
453
+ const analyzer = new AnalyzePullRequest({
454
+ generations: store,
455
+ evidence: store,
456
+ reviews: store,
457
+ registry: store,
458
+ clock,
459
+ cancellation,
460
+ });
461
+ const analyzed = await analyzer.execute({
462
+ analysisId: analysisId(createSystemId('ana', clock.now())),
463
+ workspaceId: current.snapshot.revision.workspaceId,
464
+ registryRevision: current.snapshot.revision.revision,
465
+ policyRevision: policy,
466
+ policyMajor,
467
+ producerRepositoryId: repository.repositoryId,
468
+ baseGenerationId: base.generationId,
469
+ overlayId: overlay,
470
+ pullRequest: {
471
+ provider: 'local',
472
+ ...(pullRequestNumber === undefined ? {} : { number: pullRequestNumber }),
473
+ baseSha: baseCommit.value.sha,
474
+ headSha: headCommit.value.sha,
475
+ },
476
+ changes,
477
+ producerDefinitions: base.observation.definitions,
478
+ });
479
+ if (!analyzed.ok)
480
+ throw new Error(analyzed.failure.safeMessage);
481
+ return analyzed.value;
482
+ });
483
+ const findings = result.findings.slice(paging.offset, paging.offset + paging.limit);
484
+ const nextOffset = paging.offset + findings.length;
485
+ const nextCursor = nextOffset < result.findings.length ? `offset:${nextOffset}` : null;
486
+ if (options.json) {
487
+ process.stdout.write(`${JSON.stringify({
488
+ schema: 'reverb.analysis-page',
489
+ schema_version: '1.0',
490
+ total_findings: result.findings.length,
491
+ returned_findings: findings.length,
492
+ next_cursor: nextCursor,
493
+ result: canonicalProjection({ ...result, findings }),
494
+ })}\n`);
495
+ }
496
+ else {
497
+ process.stdout.write(`analysis ${result.state}: ${result.pullRequest.baseSha} -> ${result.pullRequest.headSha}; ${result.findings.length} findings, ${result.abstentions.length} abstentions\n`);
498
+ for (const finding of findings) {
499
+ process.stdout.write(`${finding.fingerprint} ${finding.change.compatibility} ${finding.edge.consumerRepositoryId} ${finding.change.canonicalKey}\n remedy: ${finding.remedy.text}\n`);
500
+ }
501
+ for (const abstention of result.abstentions) {
502
+ process.stdout.write(`abstained ${abstention.consumerRepositoryId}: ${abstention.reason}\n`);
503
+ }
504
+ if (nextCursor !== null)
505
+ process.stdout.write(`next cursor: ${nextCursor}\n`);
506
+ process.stdout.write('preview only: evidence strata are not calibrated for delivery\n');
507
+ }
508
+ });
509
+ const finding = program.command('finding').description('inspect persisted finding evidence');
510
+ finding
511
+ .command('show')
512
+ .argument('<fingerprint>')
513
+ .option('--json', 'emit canonical machine output')
514
+ .action(async (fingerprintValue, options) => {
515
+ const current = await LocalWorkspaceConfig.load(process.cwd());
516
+ const value = findingFingerprint(fingerprintValue);
517
+ const found = await withStore(current.root, async (store) => store.findFinding(current.snapshot.revision.workspaceId, value));
518
+ if (!found.ok)
519
+ throw new Error(found.failure.safeMessage);
520
+ if (options.json) {
521
+ process.stdout.write(`${JSON.stringify(canonicalProjection(found.value))}\n`);
522
+ }
523
+ else {
524
+ process.stdout.write(`${found.value.finding.fingerprint} ${found.value.finding.state}\nproducer ${found.value.analysis.producerRepositoryId} ${found.value.analysis.pullRequest.baseSha} -> ${found.value.analysis.pullRequest.headSha}\nconsumer ${found.value.finding.edge.consumerRepositoryId} ${found.value.finding.consumer.commitSha ?? 'generation unavailable'}\ncontract ${found.value.finding.change.contractKind} ${found.value.finding.change.canonicalKey}\nremedy ${found.value.finding.remedy.text}\n`);
525
+ }
526
+ });
527
+ const review = program
528
+ .command('review')
529
+ .description('append human labels and inspect immutable review history');
530
+ review
531
+ .command('add')
532
+ .argument('<fingerprint>')
533
+ .requiredOption('--edge <label>', EDGE_LABELS.join('|'))
534
+ .requiredOption('--impact <label>', IMPACT_LABELS.join('|'))
535
+ .requiredOption('--action <label>', ACTION_LABELS.join('|'))
536
+ .requiredOption('--reason <code>', REVIEW_REASON_CODES.join('|'))
537
+ .requiredOption('--actor <id>', 'stable reviewer identity')
538
+ .option('--role <role>', REVIEW_ROLES.join('|'), 'reviewer')
539
+ .requiredOption('--capability <description>', 'reviewer domain capability')
540
+ .option('--note <text>', 'bounded reviewer note', 'No additional reviewer note.')
541
+ .option('--detector-author-conflict', 'record a detector-author conflict', false)
542
+ .option('--suppress-scope <scope>', SUPPRESSION_SCOPES.join('|'))
543
+ .option('--suppression-justification <text>')
544
+ .option('--suppression-review-at <instant>')
545
+ .option('--suppression-expires-at <instant>')
546
+ .option('--rule-id <id>', 'adapter/workspace rule identifier')
547
+ .option('--json', 'emit canonical machine output')
548
+ .action(async (fingerprintValue, options) => {
549
+ const current = await LocalWorkspaceConfig.load(process.cwd());
550
+ const fingerprintValueType = findingFingerprint(fingerprintValue);
551
+ const edgeLabel = enumValue(EDGE_LABELS, options.edge, 'edge label');
552
+ const impactLabel = enumValue(IMPACT_LABELS, options.impact, 'impact label');
553
+ const actionLabel = enumValue(ACTION_LABELS, options.action, 'action label');
554
+ const reason = enumValue(REVIEW_REASON_CODES, options.reason, 'review reason');
555
+ const role = enumValue(REVIEW_ROLES, options.role, 'review role');
556
+ const now = new SystemClock().now();
557
+ const result = await withStore(current.root, async (store) => {
558
+ const found = await store.findFinding(current.snapshot.revision.workspaceId, fingerprintValueType);
559
+ if (!found.ok)
560
+ throw new Error(found.failure.safeMessage);
561
+ const findingValue = found.value.finding;
562
+ const history = await store.listReviews(current.snapshot.revision.workspaceId, fingerprintValueType);
563
+ if (!history.ok)
564
+ throw new Error(history.failure.safeMessage);
565
+ const previous = [...history.value]
566
+ .filter((value) => value.findingOccurrenceId === findingValue.id)
567
+ .sort((left, right) => left.occurredAt.localeCompare(right.occurredAt))
568
+ .at(-1);
569
+ let matcher;
570
+ if (options.suppressScope !== undefined) {
571
+ const scope = enumValue(SUPPRESSION_SCOPES, options.suppressScope, 'suppression scope');
572
+ if (options.suppressionJustification === undefined ||
573
+ options.suppressionReviewAt === undefined ||
574
+ options.suppressionExpiresAt === undefined) {
575
+ throw new Error('Suppression scope requires justification, review-at, and expires-at.');
576
+ }
577
+ matcher =
578
+ scope === 'occurrence'
579
+ ? { scope, occurrenceId: findingValue.id }
580
+ : scope === 'stable_finding'
581
+ ? { scope, fingerprint: findingValue.fingerprint }
582
+ : scope === 'contract_consumer'
583
+ ? {
584
+ scope,
585
+ contractKind: findingValue.change.contractKind,
586
+ canonicalContractKey: findingValue.change.canonicalKey,
587
+ consumerRepositoryId: findingValue.edge.consumerRepositoryId,
588
+ }
589
+ : scope === 'repository_pair_kind'
590
+ ? {
591
+ scope,
592
+ producerRepositoryId: found.value.analysis.producerRepositoryId,
593
+ consumerRepositoryId: findingValue.edge.consumerRepositoryId,
594
+ contractKind: findingValue.change.contractKind,
595
+ }
596
+ : scope === 'adapter_rule'
597
+ ? {
598
+ scope,
599
+ adapterId: findingValue.change.adapterId,
600
+ ruleId: options.ruleId ?? findingValue.change.changeKind,
601
+ }
602
+ : {
603
+ scope,
604
+ ruleId: options.ruleId ?? findingValue.change.changeKind,
605
+ };
606
+ }
607
+ const suppression = matcher === undefined
608
+ ? undefined
609
+ : createSuppressionRule({
610
+ workspaceId: current.snapshot.revision.workspaceId,
611
+ matcher,
612
+ owner: {
613
+ actorId: options.actor,
614
+ role,
615
+ authorizationRevision: current.snapshot.revision.revision,
616
+ },
617
+ justification: options.suppressionJustification,
618
+ createdAt: now,
619
+ reviewAt: instant(options.suppressionReviewAt),
620
+ expiresAt: instant(options.suppressionExpiresAt),
621
+ invalidationPredicates: [
622
+ {
623
+ kind: 'producer_code',
624
+ repositoryId: found.value.analysis.producerRepositoryId,
625
+ generationId: findingValue.edge.producerGenerationId,
626
+ },
627
+ {
628
+ kind: 'consumer_code',
629
+ repositoryId: findingValue.edge.consumerRepositoryId,
630
+ generationId: findingValue.edge.consumerGenerationId,
631
+ },
632
+ {
633
+ kind: 'consumer_reference',
634
+ stableReferenceId: findingValue.edge.stableReferenceId,
635
+ contentHash: findingValue.edge.reference.contentHash,
636
+ },
637
+ {
638
+ kind: 'contract_shape',
639
+ contractKind: findingValue.change.contractKind,
640
+ canonicalContractKey: findingValue.change.canonicalKey,
641
+ shapeHash: findingValue.edge.definition.shapeHash,
642
+ },
643
+ {
644
+ kind: 'identity_version',
645
+ adapterId: findingValue.change.adapterId,
646
+ identityVersion: findingValue.change.identityVersion,
647
+ },
648
+ {
649
+ kind: 'adapter_version',
650
+ adapterId: findingValue.change.adapterId,
651
+ adapterVersion: findingValue.change.adapterVersion,
652
+ },
653
+ { kind: 'evidence_stratum', stratumKey: findingValue.edge.stratumKey },
654
+ { kind: 'policy_revision', revision: found.value.analysis.policyRevision },
655
+ { kind: 'registry_revision', revision: found.value.analysis.registryRevision },
656
+ ],
657
+ });
658
+ const adapters = new Map([findingValue.edge.definition, findingValue.edge.reference].map((value) => [
659
+ value.adapterId,
660
+ {
661
+ id: value.adapterId,
662
+ version: value.adapterVersion,
663
+ identityVersion: value.identityVersion,
664
+ },
665
+ ]));
666
+ const recorded = await new RecordReview(store, store).execute({
667
+ review: {
668
+ id: reviewEventId(createSystemId('rev', now)),
669
+ workspaceId: current.snapshot.revision.workspaceId,
670
+ findingOccurrenceId: findingValue.id,
671
+ findingFingerprint: findingValue.fingerprint,
672
+ actor: {
673
+ id: options.actor,
674
+ role,
675
+ domainCapability: options.capability,
676
+ detectorAuthorConflict: options.detectorAuthorConflict,
677
+ },
678
+ authorization: {
679
+ revision: current.snapshot.revision.revision,
680
+ authorizedAt: now,
681
+ permission: 'finding.review',
682
+ },
683
+ occurredAt: now,
684
+ versions: {
685
+ producerGenerationId: findingValue.edge.producerGenerationId,
686
+ consumerGenerationId: findingValue.edge.consumerGenerationId,
687
+ adapters: [...adapters.values()],
688
+ evidenceStratum: findingValue.edge.stratumKey,
689
+ policyRevision: found.value.analysis.policyRevision,
690
+ registryRevision: found.value.analysis.registryRevision,
691
+ },
692
+ labels: { edge: edgeLabel, impact: impactLabel, action: actionLabel },
693
+ reason,
694
+ noteHash: contentHash(hashCanonical(options.note)),
695
+ ...(previous === undefined ? {} : { supersedes: previous.id }),
696
+ ...(suppression === undefined ? {} : { suppressionRuleId: suppression.id }),
697
+ },
698
+ ...(suppression === undefined ? {} : { suppression }),
699
+ });
700
+ if (!recorded.ok)
701
+ throw new Error(recorded.failure.safeMessage);
702
+ return { event: recorded.value, suppression };
703
+ });
704
+ if (options.json)
705
+ process.stdout.write(`${JSON.stringify(canonicalProjection(result))}\n`);
706
+ else {
707
+ process.stdout.write(`recorded ${result.event.id} for ${fingerprintValueType}\n`);
708
+ if (result.suppression !== undefined) {
709
+ process.stdout.write(`suppression ${result.suppression.id} (${result.suppression.matcher.scope})\n`);
710
+ }
711
+ }
712
+ });
713
+ review
714
+ .command('list')
715
+ .argument('<fingerprint>')
716
+ .option('--json', 'emit canonical machine output')
717
+ .action(async (fingerprintValue, options) => {
718
+ const current = await LocalWorkspaceConfig.load(process.cwd());
719
+ const events = await withStore(current.root, (store) => store.listReviews(current.snapshot.revision.workspaceId, findingFingerprint(fingerprintValue)));
720
+ if (!events.ok)
721
+ throw new Error(events.failure.safeMessage);
722
+ if (options.json)
723
+ process.stdout.write(`${JSON.stringify(canonicalProjection(events.value))}\n`);
724
+ else {
725
+ events.value.forEach((event) => process.stdout.write(`${event.id} ${event.labels.edge}/${event.labels.impact}/${event.labels.action} ${event.reason}\n`));
726
+ }
727
+ });
728
+ review
729
+ .command('import')
730
+ .argument('<jsonl>')
731
+ .description('import canonical review-event records, optionally bundled with suppressions')
732
+ .action(async (path) => {
733
+ const current = await LocalWorkspaceConfig.load(process.cwd());
734
+ const lines = (await readFile(resolve(path), 'utf8'))
735
+ .split(/\r?\n/)
736
+ .map((value) => value.trim())
737
+ .filter((value) => value.length > 0);
738
+ const imported = await withStore(current.root, async (store) => {
739
+ let count = 0;
740
+ for (const line of lines) {
741
+ const wrapper = record(JSON.parse(line), 'Review import line');
742
+ const eventWire = wrapper.event ?? wrapper;
743
+ const suppressionWire = wrapper.suppression;
744
+ validateWithSchema(reviewEventSchema.$id, eventWire);
745
+ if (suppressionWire !== undefined) {
746
+ validateWithSchema(suppressionRuleSchema.$id, suppressionWire);
747
+ }
748
+ const event = domainProjection(eventWire);
749
+ const suppression = suppressionWire === undefined
750
+ ? undefined
751
+ : domainProjection(suppressionWire);
752
+ const outputHash = event.outputHash;
753
+ const draft = { ...event };
754
+ Reflect.deleteProperty(draft, 'schema');
755
+ Reflect.deleteProperty(draft, 'schemaVersion');
756
+ Reflect.deleteProperty(draft, 'outputHash');
757
+ const result = await new RecordReview(store, store).execute({
758
+ review: draft,
759
+ ...(suppression === undefined ? {} : { suppression }),
760
+ });
761
+ if (!result.ok)
762
+ throw new Error(result.failure.safeMessage);
763
+ if (result.value.outputHash !== outputHash) {
764
+ throw new Error('Imported review output_hash does not match canonical content.');
765
+ }
766
+ count += 1;
767
+ }
768
+ return count;
769
+ });
770
+ process.stdout.write(`${JSON.stringify({ imported_reviews: imported })}\n`);
771
+ });
772
+ const corpus = program.command('corpus').description('manage frozen evaluation corpora');
773
+ corpus
774
+ .command('import')
775
+ .argument('<manifest>', 'JSON bundle containing canonical manifest and cases')
776
+ .action(async (path) => {
777
+ const current = await LocalWorkspaceConfig.load(process.cwd());
778
+ const bundle = await loadCorpusBundle(path);
779
+ await withStore(current.root, async (store) => {
780
+ const result = await store.putCorpus(bundle.manifest, bundle.cases);
781
+ if (!result.ok)
782
+ throw new Error(result.failure.safeMessage);
783
+ });
784
+ process.stdout.write(`${JSON.stringify({ corpus_revision: bundle.manifest.revision, cases: bundle.cases.length })}\n`);
785
+ });
786
+ program
787
+ .command('eval')
788
+ .description('evaluate a frozen corpus without rerunning adapters or models')
789
+ .requiredOption('--corpus <revision>')
790
+ .option('--policy <file>', 'also replay a frozen candidate policy')
791
+ .option('--json', 'emit canonical machine output')
792
+ .action(async (options) => {
793
+ const current = await LocalWorkspaceConfig.load(process.cwd());
794
+ const generatedAt = new SystemClock().now();
795
+ const result = await withStore(current.root, async (store) => {
796
+ const corpusResult = await store.getCorpus(contentHash(options.corpus));
797
+ if (!corpusResult.ok)
798
+ throw new Error(corpusResult.failure.safeMessage);
799
+ const evaluation = evaluateCorpus({
800
+ corpusRevision: corpusResult.value.manifest.revision,
801
+ generatedAt,
802
+ cases: corpusResult.value.cases,
803
+ });
804
+ const write = await store.putEvaluationReport(evaluation);
805
+ if (!write.ok)
806
+ throw new Error(write.failure.safeMessage);
807
+ if (options.policy === undefined)
808
+ return { evaluation };
809
+ const candidate = frozenPolicy(await readJson(options.policy));
810
+ const baseline = {
811
+ revision: policyRevision(`pol_${hashCanonical({
812
+ corpusRevision: corpusResult.value.manifest.revision,
813
+ mode: 'no_delivery_baseline',
814
+ })}`),
815
+ allowedStrata: [],
816
+ allowedImpactClaims: ['breaking', 'behavior_risk'],
817
+ respectFrozenSuppressions: true,
818
+ maximumAlertsPerThousand: 0,
819
+ };
820
+ return {
821
+ evaluation,
822
+ policySimulation: simulateFrozenPolicy({
823
+ corpusRevision: corpusResult.value.manifest.revision,
824
+ cases: corpusResult.value.cases,
825
+ baseline,
826
+ candidate,
827
+ }),
828
+ };
829
+ });
830
+ if (options.json)
831
+ process.stdout.write(`${JSON.stringify(canonicalProjection(result))}\n`);
832
+ else {
833
+ process.stdout.write(`evaluation ${result.evaluation.outputHash}: ${result.evaluation.realWorld.independentlyLabeledCases} independently labelled real-world cases\n`);
834
+ if ('policySimulation' in result) {
835
+ process.stdout.write(`policy simulation ${result.policySimulation.resultHash}\n`);
836
+ }
837
+ }
838
+ });
839
+ const policyCommands = program.command('policy').description('replay frozen delivery policies');
840
+ policyCommands
841
+ .command('simulate')
842
+ .argument('<file>', 'candidate policy JSON')
843
+ .requiredOption('--corpus <revision>')
844
+ .option('--baseline <file>', 'baseline policy JSON')
845
+ .option('--json', 'emit canonical machine output')
846
+ .action(async (path, options) => {
847
+ const current = await LocalWorkspaceConfig.load(process.cwd());
848
+ const candidate = frozenPolicy(await readJson(path));
849
+ const result = await withStore(current.root, async (store) => {
850
+ const corpusResult = await store.getCorpus(contentHash(options.corpus));
851
+ if (!corpusResult.ok)
852
+ throw new Error(corpusResult.failure.safeMessage);
853
+ const baseline = options.baseline === undefined
854
+ ? {
855
+ revision: policyRevision(`pol_${hashCanonical({
856
+ corpusRevision: corpusResult.value.manifest.revision,
857
+ mode: 'no_delivery_baseline',
858
+ })}`),
859
+ allowedStrata: [],
860
+ allowedImpactClaims: ['breaking', 'behavior_risk'],
861
+ respectFrozenSuppressions: true,
862
+ maximumAlertsPerThousand: 0,
863
+ }
864
+ : frozenPolicy(await readJson(options.baseline));
865
+ return simulateFrozenPolicy({
866
+ corpusRevision: corpusResult.value.manifest.revision,
867
+ cases: corpusResult.value.cases,
868
+ baseline,
869
+ candidate,
870
+ });
871
+ });
872
+ if (options.json)
873
+ process.stdout.write(`${JSON.stringify(canonicalProjection(result))}\n`);
874
+ else {
875
+ process.stdout.write(`${result.resultHash}: baseline ${result.baseline.deliveries}, candidate ${result.candidate.deliveries} deliveries\n`);
876
+ }
877
+ });
878
+ const promotion = program.command('promotion').description('append promotion audit decisions');
879
+ promotion
880
+ .command('decide')
881
+ .argument('<evidence>', 'frozen promotion evidence JSON')
882
+ .requiredOption('--actor <id>')
883
+ .option('--json', 'emit canonical machine output')
884
+ .action(async (path, options) => {
885
+ const current = await LocalWorkspaceConfig.load(process.cwd());
886
+ const evidence = domainProjection(await readJson(path));
887
+ const decidedAt = new SystemClock().now();
888
+ const decision = await withStore(current.root, async (store) => {
889
+ const history = await store.listPromotions(evidence.stratumKey);
890
+ if (!history.ok)
891
+ throw new Error(history.failure.safeMessage);
892
+ const previous = [...history.value]
893
+ .sort((left, right) => left.decidedAt.localeCompare(right.decidedAt))
894
+ .at(-1);
895
+ const recordValue = decidePromotion({
896
+ ...(previous === undefined ? {} : { previous }),
897
+ evidence,
898
+ decidedAt,
899
+ decidedBy: options.actor,
900
+ });
901
+ const stored = await store.appendPromotion(recordValue);
902
+ if (!stored.ok)
903
+ throw new Error(stored.failure.safeMessage);
904
+ return recordValue;
905
+ });
906
+ if (options.json)
907
+ process.stdout.write(`${JSON.stringify(canonicalProjection(decision))}\n`);
908
+ else {
909
+ process.stdout.write(`${decision.id} ${decision.stratumKey}: ${decision.state} (${decision.reasons.join(', ') || 'gate passed'})\n`);
910
+ }
911
+ });
912
+ program
913
+ .command('status')
914
+ .option('--json', 'emit JSON')
915
+ .action(async (options) => {
916
+ const current = await LocalWorkspaceConfig.load(process.cwd());
917
+ const statuses = await withStore(current.root, async (store) => {
918
+ const output = [];
919
+ for (const repository of current.snapshot.repositories) {
920
+ const selected = await store.selectGeneration({
921
+ workspaceId: current.snapshot.revision.workspaceId,
922
+ repositoryId: repository.repositoryId,
923
+ allowPartial: true,
924
+ });
925
+ const observation = selected.ok && selected.value.state === 'selected'
926
+ ? await store.getContractObservation(selected.value.generation.id)
927
+ : null;
928
+ output.push({
929
+ alias: repository.alias,
930
+ repository_id: repository.repositoryId,
931
+ selected: selected.ok && selected.value.state === 'selected'
932
+ ? {
933
+ generation_id: selected.value.generation.id,
934
+ commit_sha: selected.value.generation.commitSha,
935
+ state: selected.value.generation.state,
936
+ selected_at: selected.value.generation.completedAt,
937
+ contracts: observation?.ok && observation.value !== null
938
+ ? {
939
+ coverage: observation.value.coverageState,
940
+ definitions: observation.value.definitions.length,
941
+ references: observation.value.references.length,
942
+ observed_at: observation.value.observedAt,
943
+ }
944
+ : null,
945
+ }
946
+ : null,
947
+ });
948
+ }
949
+ return output;
950
+ });
951
+ if (options.json)
952
+ process.stdout.write(`${JSON.stringify(statuses)}\n`);
953
+ else {
954
+ for (const status of statuses) {
955
+ process.stdout.write(`${status.alias}: ${status.selected ? `${status.selected.state} ${status.selected.commit_sha}; contracts ${status.selected.contracts?.coverage ?? 'not indexed'} (${status.selected.contracts?.definitions ?? 0} definitions, ${status.selected.contracts?.references ?? 0} references)` : 'not indexed'}\n`);
956
+ }
957
+ }
958
+ });
959
+ program
960
+ .command('doctor')
961
+ .option('--json', 'emit JSON')
962
+ .action(async (options) => {
963
+ const checks = [];
964
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
965
+ checks.push({
966
+ name: 'node',
967
+ state: nodeMajor >= 24 ? 'pass' : 'fail',
968
+ detail: `Node ${process.versions.node}`,
969
+ });
970
+ try {
971
+ const current = await LocalWorkspaceConfig.load(process.cwd());
972
+ checks.push({
973
+ name: 'workspace',
974
+ state: 'pass',
975
+ detail: current.snapshot.revision.revision,
976
+ });
977
+ const reader = new LocalGitRepositoryReader(LocalWorkspaceConfig.repositoryBindings(current));
978
+ for (const repository of current.snapshot.repositories) {
979
+ const resolved = await reader.resolveRepository(repository.repositoryId);
980
+ checks.push({
981
+ name: `repository:${repository.alias}`,
982
+ state: resolved.ok ? 'pass' : 'fail',
983
+ detail: resolved.ok ? 'Git root is readable' : resolved.failure.code,
984
+ });
985
+ }
986
+ await withStore(current.root, async (store) => {
987
+ checks.push({
988
+ name: 'sqlite',
989
+ state: store.migrationVersions().includes(1) ? 'pass' : 'fail',
990
+ detail: `migrations=${store.migrationVersions().join(',')}`,
991
+ });
992
+ });
993
+ for (const adapter of INITIAL_ADAPTERS) {
994
+ checks.push({
995
+ name: `adapter:${adapter.manifest.id}`,
996
+ state: 'pass',
997
+ detail: `${adapter.manifest.version}; identity v${adapter.manifest.identityVersion}; preview UNMEASURED; ${adapter.manifest.limitations.join('; ')}`,
998
+ });
999
+ }
1000
+ }
1001
+ catch (error) {
1002
+ checks.push({
1003
+ name: 'workspace',
1004
+ state: 'fail',
1005
+ detail: error instanceof Error ? error.message : 'Workspace check failed',
1006
+ });
1007
+ }
1008
+ if (options.json)
1009
+ process.stdout.write(`${JSON.stringify({ checks })}\n`);
1010
+ else
1011
+ checks.forEach((check) => process.stdout.write(`${check.state}: ${check.name} — ${check.detail}\n`));
1012
+ if (checks.some((check) => check.state === 'fail'))
1013
+ process.exitCode = 3;
1014
+ });
1015
+ return program;
1016
+ }
1017
+ export async function main(argv = process.argv) {
1018
+ const cli = await createCli();
1019
+ try {
1020
+ await cli.parseAsync(argv);
1021
+ }
1022
+ catch (error) {
1023
+ process.stderr.write(`${error instanceof Error ? error.message : 'Reverb command failed.'}\n`);
1024
+ process.exitCode = 5;
1025
+ }
1026
+ }
1027
+ //# sourceMappingURL=cli.js.map