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/LICENSE +202 -0
- package/README.md +26 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1027 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts.d.ts +29 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +115 -0
- package/dist/contracts.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
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
|