@qijenchen/governance 0.1.0-beta.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +89 -0
  2. package/bin/attest.mjs +4 -0
  3. package/bin/check.mjs +4 -0
  4. package/bin/doctor.mjs +4 -0
  5. package/bin/generate.mjs +4 -0
  6. package/bin/governance.mjs +4 -0
  7. package/bin/hook.mjs +4 -0
  8. package/bin/upgrade.mjs +4 -0
  9. package/canonical/gates.json +42 -0
  10. package/canonical/manifest.json +476 -0
  11. package/canonical/plugin-aliases.json +25 -0
  12. package/canonical/provider-lifecycle.json +48 -0
  13. package/canonical/providers.json +455 -0
  14. package/canonical/roles.json +12 -0
  15. package/canonical/rules.json +81 -0
  16. package/canonical/schemas/attestation.schema.json +61 -0
  17. package/canonical/schemas/diagnostic.schema.json +37 -0
  18. package/canonical/schemas/gates.schema.json +27 -0
  19. package/canonical/schemas/lock.schema.json +189 -0
  20. package/canonical/schemas/manifest.schema.json +103 -0
  21. package/canonical/schemas/plugin-aliases.schema.json +59 -0
  22. package/canonical/schemas/provider-hook-coverage.schema.json +173 -0
  23. package/canonical/schemas/provider-lifecycle.schema.json +126 -0
  24. package/canonical/schemas/providers.schema.json +917 -0
  25. package/canonical/schemas/roles.schema.json +27 -0
  26. package/canonical/schemas/rules.schema.json +46 -0
  27. package/canonical/schemas/upgrade-plan.schema.json +31 -0
  28. package/package.json +42 -0
  29. package/src/authority-decision-evidence.mjs +413 -0
  30. package/src/canonical-order.mjs +8 -0
  31. package/src/carrier-projection.mjs +407 -0
  32. package/src/cli.mjs +114 -0
  33. package/src/closed-tool-execution.mjs +1001 -0
  34. package/src/common.mjs +278 -0
  35. package/src/contract.mjs +760 -0
  36. package/src/hook-api.mjs +107 -0
  37. package/src/index.mjs +14 -0
  38. package/src/provider-hook-normalization.mjs +1646 -0
  39. package/src/provider-review-binding.mjs +2377 -0
  40. package/src/snapshot.mjs +520 -0
@@ -0,0 +1,407 @@
1
+ import { stableJson } from './common.mjs'
2
+
3
+ export const EXTERNAL_ACTIVATION_CARRIER_PATH = 'infra/governance/external-activation-requirements.json'
4
+ export const PROVIDER_CERTIFICATION_CARRIER_PATH = 'infra/governance/providers/certifications.json'
5
+ export const REVIEW_CAPABILITY_CERTIFICATION_CARRIER_PATH = 'infra/governance/providers/review-capability-certifications.json'
6
+
7
+ export const GOVERNANCE_CARRIER_PROJECTION_IDS = Object.freeze([
8
+ 'external-activation-state-v1',
9
+ 'provider-certification-state-v1',
10
+ 'review-capability-certification-state-v1',
11
+ ])
12
+
13
+ export const GOVERNANCE_CARRIER_MAX_BYTES = 8 * 1024 * 1024
14
+ const UTF8 = new TextDecoder('utf-8', { fatal: true })
15
+ const ACTIVATION_STATE_KEYS = Object.freeze(['status', 'evidence', 'observedAt', 'expiresAt'])
16
+ const CERTIFICATION_RECORD_KEYS = new Set([
17
+ 'id',
18
+ 'provider',
19
+ 'runtimeKind',
20
+ 'providerVersion',
21
+ 'adapterVersion',
22
+ 'surface',
23
+ 'repositoryRole',
24
+ 'status',
25
+ 'platformMatrix',
26
+ 'certifiedAt',
27
+ 'expiresAt',
28
+ 'runtimeEvidence',
29
+ 'checks',
30
+ 'limitations',
31
+ ])
32
+ const CERTIFICATION_TARGET_KEYS = new Set([
33
+ 'id',
34
+ 'operatingSystem',
35
+ 'platform',
36
+ 'arch',
37
+ 'executionEnvironment',
38
+ 'distributionVersion',
39
+ 'pathClass',
40
+ 'status',
41
+ 'limitations',
42
+ 'certifiedAt',
43
+ 'expiresAt',
44
+ 'runtimeEvidence',
45
+ ])
46
+ const REVIEW_CAPABILITY_CERTIFICATION_KEYS = new Set([
47
+ 'id',
48
+ 'reviewProfileId',
49
+ 'reviewProfileDigest',
50
+ 'providerId',
51
+ 'modelReleaseId',
52
+ 'entitlementId',
53
+ 'status',
54
+ 'assuranceTier',
55
+ 'reasoningTier',
56
+ 'computeTier',
57
+ 'certifiedAt',
58
+ 'expiresAt',
59
+ 'evidence',
60
+ ])
61
+ const TARGET_AXES = Object.freeze([
62
+ 'id',
63
+ 'operatingSystem',
64
+ 'platform',
65
+ 'arch',
66
+ 'executionEnvironment',
67
+ 'distributionVersion',
68
+ 'pathClass',
69
+ ])
70
+
71
+ function invariant(condition, message) {
72
+ if (!condition) throw new Error(`governance carrier projection blocked:${message}`)
73
+ }
74
+
75
+ function plainObject(value, label) {
76
+ invariant(value && typeof value === 'object' && !Array.isArray(value), `${label} must be an object`)
77
+ invariant([Object.prototype, null].includes(Object.getPrototypeOf(value)), `${label} must be a plain object`)
78
+ return value
79
+ }
80
+
81
+ function exactKeys(value, expected, label) {
82
+ plainObject(value, label)
83
+ const actual = Object.keys(value).sort()
84
+ const wanted = [...expected].sort()
85
+ invariant(actual.length === wanted.length && actual.every((key, index) => key === wanted[index]), `${label} has an invalid or open shape`)
86
+ }
87
+
88
+ function allowedKeys(value, allowed, required, label) {
89
+ plainObject(value, label)
90
+ const keys = Object.keys(value)
91
+ invariant(required.every(key => keys.includes(key)) && keys.every(key => allowed.has(key)), `${label} has an invalid or open shape`)
92
+ }
93
+
94
+ function assertNoDuplicateJsonObjectKeys(text, label) {
95
+ const stack = []
96
+ let rootConsumed = false
97
+ const expectsValue = () => {
98
+ if (stack.length === 0) return !rootConsumed
99
+ const frame = stack.at(-1)
100
+ return frame.kind === 'array' ? frame.state === 'value-or-end' : frame.state === 'value'
101
+ }
102
+ const consumeValue = () => {
103
+ invariant(expectsValue(), `${label} has an invalid JSON token position`)
104
+ if (stack.length === 0) rootConsumed = true
105
+ else stack.at(-1).state = 'comma-or-end'
106
+ }
107
+ const stringEnd = start => {
108
+ let escaped = false
109
+ for (let index = start + 1; index < text.length; index += 1) {
110
+ const character = text[index]
111
+ if (escaped) {
112
+ escaped = false
113
+ continue
114
+ }
115
+ if (character === '\\') {
116
+ escaped = true
117
+ continue
118
+ }
119
+ if (character === '"') return index
120
+ }
121
+ return -1
122
+ }
123
+
124
+ for (let index = 0; index < text.length;) {
125
+ const character = text[index]
126
+ if (/\s/u.test(character)) {
127
+ index += 1
128
+ continue
129
+ }
130
+ const frame = stack.at(-1)
131
+ if (character === '{' || character === '[') {
132
+ consumeValue()
133
+ stack.push(character === '{'
134
+ ? { kind: 'object', state: 'key-or-end', keys: new Set() }
135
+ : { kind: 'array', state: 'value-or-end' })
136
+ index += 1
137
+ continue
138
+ }
139
+ if (character === '}' || character === ']') {
140
+ invariant(frame && ((character === '}') === (frame.kind === 'object')), `${label} has mismatched JSON delimiters`)
141
+ invariant(
142
+ frame.kind === 'object'
143
+ ? ['key-or-end', 'comma-or-end'].includes(frame.state)
144
+ : ['value-or-end', 'comma-or-end'].includes(frame.state),
145
+ `${label} has an incomplete JSON container`,
146
+ )
147
+ stack.pop()
148
+ index += 1
149
+ continue
150
+ }
151
+ if (character === ',') {
152
+ invariant(frame?.state === 'comma-or-end', `${label} has an invalid JSON comma`)
153
+ frame.state = frame.kind === 'object' ? 'key-or-end' : 'value-or-end'
154
+ index += 1
155
+ continue
156
+ }
157
+ if (character === ':') {
158
+ invariant(frame?.kind === 'object' && frame.state === 'colon', `${label} has an invalid JSON colon`)
159
+ frame.state = 'value'
160
+ index += 1
161
+ continue
162
+ }
163
+ if (character === '"') {
164
+ const end = stringEnd(index)
165
+ invariant(end > index, `${label} has an unterminated JSON string`)
166
+ const token = text.slice(index, end + 1)
167
+ if (frame?.kind === 'object' && frame.state === 'key-or-end') {
168
+ const key = JSON.parse(token)
169
+ invariant(!frame.keys.has(key), `${label} contains duplicate object key:${key}`)
170
+ frame.keys.add(key)
171
+ frame.state = 'colon'
172
+ } else {
173
+ consumeValue()
174
+ }
175
+ index = end + 1
176
+ continue
177
+ }
178
+ let end = index + 1
179
+ while (end < text.length && !/[\s,\]}]/u.test(text[end])) end += 1
180
+ consumeValue()
181
+ index = end
182
+ }
183
+ invariant(rootConsumed && stack.length === 0, `${label} has an incomplete JSON document`)
184
+ }
185
+
186
+ function parseCarrierJson(bytes, label) {
187
+ invariant(Buffer.isBuffer(bytes), `${label} bytes must be a Buffer`)
188
+ invariant(bytes.length > 0 && bytes.length <= GOVERNANCE_CARRIER_MAX_BYTES, `${label} byte length is outside the closed bound`)
189
+ invariant(!(bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf), `${label} must not contain a UTF-8 BOM`)
190
+ let text
191
+ try {
192
+ text = UTF8.decode(bytes)
193
+ } catch (error) {
194
+ throw new Error(`governance carrier projection blocked:${label} is not canonical UTF-8`, { cause: error })
195
+ }
196
+ assertNoDuplicateJsonObjectKeys(text, label)
197
+ try {
198
+ return JSON.parse(text)
199
+ } catch (error) {
200
+ throw new Error(`governance carrier projection blocked:${label} is invalid JSON:${error.message}`, { cause: error })
201
+ }
202
+ }
203
+
204
+ function externalActivationProjection(bytes) {
205
+ const ledger = parseCarrierJson(bytes, 'external activation ledger')
206
+ exactKeys(ledger, ['$schema', 'schemaVersion', 'policy', 'requirements'], 'external activation ledger')
207
+ invariant(ledger.$schema === 'schemas/external-activation-requirements.schema.json' && ledger.schemaVersion === 4, 'external activation ledger identity/version is invalid')
208
+ plainObject(ledger.policy, 'external activation policy binding')
209
+ invariant(Array.isArray(ledger.requirements) && ledger.requirements.length > 0, 'external activation requirements must be a non-empty array')
210
+ const ids = new Set()
211
+ const projected = structuredClone(ledger)
212
+ for (const [index, requirement] of projected.requirements.entries()) {
213
+ plainObject(requirement, `external activation requirement[${index}]`)
214
+ invariant(typeof requirement.id === 'string' && requirement.id.length > 0 && !ids.has(requirement.id), `external activation requirement id is invalid or duplicated:${String(requirement.id)}`)
215
+ invariant(ACTIVATION_STATE_KEYS.every(key => Object.hasOwn(requirement, key)), `external activation requirement state is incomplete:${requirement.id}`)
216
+ for (const key of ACTIVATION_STATE_KEYS) delete requirement[key]
217
+ ids.add(requirement.id)
218
+ }
219
+ return Buffer.from(stableJson(projected))
220
+ }
221
+
222
+ function projectedCertificationTarget(target, recordId, targetIds) {
223
+ allowedKeys(
224
+ target,
225
+ CERTIFICATION_TARGET_KEYS,
226
+ [...TARGET_AXES, 'status', 'limitations'],
227
+ `certification ${recordId} target`,
228
+ )
229
+ invariant(typeof target.id === 'string' && target.id.length > 0 && !targetIds.has(target.id), `certification ${recordId} target id is invalid or duplicated:${String(target.id)}`)
230
+ invariant(['certified', 'not-certified', 'unsupported'].includes(target.status), `certification ${recordId}/${target.id} status is invalid`)
231
+ invariant(Array.isArray(target.limitations), `certification ${recordId}/${target.id} limitations must be an array`)
232
+ const projection = Object.fromEntries(TARGET_AXES.map(axis => {
233
+ invariant(typeof target[axis] === 'string' && target[axis].length > 0, `certification ${recordId}/${target.id} ${axis} is invalid`)
234
+ return [axis, target[axis]]
235
+ }))
236
+ if (target.status === 'unsupported') {
237
+ invariant(target.limitations.length > 0 && target.limitations.every(item => typeof item === 'string' && item.length > 0), `certification ${recordId}/${target.id} unsupported limitations are invalid`)
238
+ projection.certificationClass = 'unsupported'
239
+ projection.limitations = structuredClone(target.limitations)
240
+ } else {
241
+ projection.certificationClass = 'certifiable'
242
+ }
243
+ targetIds.add(target.id)
244
+ return projection
245
+ }
246
+
247
+ function providerCertificationProjection(bytes) {
248
+ const ledger = parseCarrierJson(bytes, 'provider certification ledger')
249
+ exactKeys(ledger, ['$schema', 'schemaVersion', 'certifications'], 'provider certification ledger')
250
+ invariant(ledger.$schema === '../schemas/provider-surface-certification.schema.json' && ledger.schemaVersion === 2, 'provider certification ledger identity/version is invalid')
251
+ invariant(Array.isArray(ledger.certifications), 'provider certifications must be an array')
252
+ const ids = new Set()
253
+ const projected = {
254
+ $schema: ledger.$schema,
255
+ schemaVersion: ledger.schemaVersion,
256
+ certifications: ledger.certifications.map((certification, index) => {
257
+ allowedKeys(
258
+ certification,
259
+ CERTIFICATION_RECORD_KEYS,
260
+ [
261
+ 'id',
262
+ 'provider',
263
+ 'runtimeKind',
264
+ 'providerVersion',
265
+ 'adapterVersion',
266
+ 'surface',
267
+ 'repositoryRole',
268
+ 'status',
269
+ 'platformMatrix',
270
+ 'checks',
271
+ 'limitations',
272
+ ],
273
+ `certification[${index}]`,
274
+ )
275
+ invariant(typeof certification.id === 'string' && certification.id.length > 0 && !ids.has(certification.id), `certification id is invalid or duplicated:${String(certification.id)}`)
276
+ for (const key of ['provider', 'runtimeKind', 'providerVersion', 'adapterVersion', 'surface', 'repositoryRole']) {
277
+ invariant(typeof certification[key] === 'string' && certification[key].length > 0, `certification ${certification.id} ${key} is invalid`)
278
+ }
279
+ invariant(Array.isArray(certification.platformMatrix) && certification.platformMatrix.length > 0, `certification ${certification.id} platformMatrix is empty`)
280
+ const record = {
281
+ id: certification.id,
282
+ provider: certification.provider,
283
+ runtimeKind: certification.runtimeKind,
284
+ providerVersion: certification.providerVersion,
285
+ adapterVersion: certification.adapterVersion,
286
+ surface: certification.surface,
287
+ repositoryRole: certification.repositoryRole,
288
+ platformMatrix: [],
289
+ }
290
+ if (Object.hasOwn(certification, 'certifiedAt')) record.certifiedAt = certification.certifiedAt
291
+ if (Object.hasOwn(certification, 'expiresAt')) record.expiresAt = certification.expiresAt
292
+ const targetIds = new Set()
293
+ record.platformMatrix = certification.platformMatrix.map(target => projectedCertificationTarget(target, certification.id, targetIds))
294
+ ids.add(certification.id)
295
+ return record
296
+ }),
297
+ }
298
+ return Buffer.from(stableJson(projected))
299
+ }
300
+
301
+ function reviewCapabilityCertificationProjection(bytes) {
302
+ const ledger = parseCarrierJson(bytes, 'review capability certification ledger')
303
+ exactKeys(
304
+ ledger,
305
+ ['$schema', 'schemaVersion', 'kind', 'certifications'],
306
+ 'review capability certification ledger',
307
+ )
308
+ invariant(
309
+ ledger.$schema === '../schemas/review-capability-certifications.schema.json'
310
+ && ledger.schemaVersion === 1
311
+ && ledger.kind === 'review-capability-certification-ledger',
312
+ 'review capability certification ledger identity/version is invalid',
313
+ )
314
+ invariant(Array.isArray(ledger.certifications),
315
+ 'review capability certifications must be an array')
316
+ const ids = new Set()
317
+ for (const [index, certification] of ledger.certifications.entries()) {
318
+ allowedKeys(
319
+ certification,
320
+ REVIEW_CAPABILITY_CERTIFICATION_KEYS,
321
+ [...REVIEW_CAPABILITY_CERTIFICATION_KEYS],
322
+ `review capability certification[${index}]`,
323
+ )
324
+ invariant(
325
+ typeof certification.id === 'string'
326
+ && certification.id.length > 0
327
+ && !ids.has(certification.id),
328
+ `review capability certification id is invalid or duplicated:${String(certification.id)}`,
329
+ )
330
+ ids.add(certification.id)
331
+ }
332
+ return Buffer.from(stableJson({
333
+ $schema: ledger.$schema,
334
+ schemaVersion: ledger.schemaVersion,
335
+ kind: ledger.kind,
336
+ certifications: [],
337
+ }))
338
+ }
339
+
340
+ const PROJECTIONS = Object.freeze({
341
+ 'external-activation-state-v1': Object.freeze({
342
+ path: EXTERNAL_ACTIVATION_CARRIER_PATH,
343
+ project: externalActivationProjection,
344
+ }),
345
+ 'provider-certification-state-v1': Object.freeze({
346
+ path: PROVIDER_CERTIFICATION_CARRIER_PATH,
347
+ project: providerCertificationProjection,
348
+ }),
349
+ 'review-capability-certification-state-v1': Object.freeze({
350
+ path: REVIEW_CAPABILITY_CERTIFICATION_CARRIER_PATH,
351
+ project: reviewCapabilityCertificationProjection,
352
+ }),
353
+ })
354
+
355
+ export function projectGovernanceCarrierBytes({ path, projectionId, bytes } = {}) {
356
+ const definition = PROJECTIONS[projectionId]
357
+ invariant(definition, `unknown projection id:${String(projectionId)}`)
358
+ invariant(path === definition.path, `projection ${projectionId} is not registered for path:${String(path)}`)
359
+ return definition.project(bytes)
360
+ }
361
+
362
+ export function governanceCarrierProjectionMap(sources) {
363
+ invariant(Array.isArray(sources), 'manifest sources must be an array')
364
+ const bindings = new Map()
365
+ for (const source of sources) {
366
+ if (source?.carrierProjection === undefined) continue
367
+ const definition = PROJECTIONS[source.carrierProjection]
368
+ invariant(definition, `manifest source ${String(source?.id)} uses an unknown carrier projection:${String(source?.carrierProjection)}`)
369
+ invariant(source.path === definition.path, `manifest source ${String(source?.id)} projection/path pair is not registered`)
370
+ invariant(!Array.isArray(source.excludes) || source.excludes.length === 0, `projected manifest source ${String(source?.id)} cannot declare directory exclusions`)
371
+ invariant(!bindings.has(source.path), `manifest carrier projection path is duplicated:${source.path}`)
372
+ bindings.set(source.path, Object.freeze({ path: source.path, projection: source.carrierProjection }))
373
+ }
374
+ return bindings
375
+ }
376
+
377
+ export function assertCanonicalGovernanceCarrierProjectionMap(bindings) {
378
+ invariant(bindings instanceof Map, 'carrier projection bindings must be a Map')
379
+ const expected = Object.values(PROJECTIONS)
380
+ .map(definition => definition.path)
381
+ .sort()
382
+ const actual = [...bindings.keys()].sort()
383
+ invariant(actual.length === expected.length && actual.every((path, index) => path === expected[index]), 'canonical carrier projection closure is incomplete or contains extra paths')
384
+ return true
385
+ }
386
+
387
+ function excludedBySource(sourcePath, excludes, path) {
388
+ for (const raw of excludes ?? []) {
389
+ const exclusion = raw.replace(/\/+$/, '')
390
+ invariant(exclusion.startsWith(`${sourcePath}/`), `source exclusion escapes projected source:${raw}`)
391
+ if (path === exclusion || path.startsWith(`${exclusion}/`)) return true
392
+ }
393
+ return false
394
+ }
395
+
396
+ export function governanceCarrierProjectionsForSource(source, bindings) {
397
+ invariant(bindings instanceof Map, 'carrier projection bindings must be a Map')
398
+ const sourcePath = String(source?.path ?? '').replace(/\/+$/, '')
399
+ invariant(sourcePath.length > 0, 'source path is empty')
400
+ return [...bindings.values()]
401
+ .filter(binding => (
402
+ (binding.path === sourcePath || binding.path.startsWith(`${sourcePath}/`))
403
+ && !excludedBySource(sourcePath, source?.excludes, binding.path)
404
+ ))
405
+ .sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)))
406
+ .map(binding => ({ path: binding.path, projection: binding.projection }))
407
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,114 @@
1
+ import { resolve } from 'node:path'
2
+ import {
3
+ diagnostic,
4
+ humanResult,
5
+ readStdin,
6
+ result,
7
+ stableJson,
8
+ } from './common.mjs'
9
+ import { evaluateHook } from './hook-api.mjs'
10
+ import {
11
+ applyUpgrade,
12
+ attestRepository,
13
+ checkRepository,
14
+ doctorRepository,
15
+ generateRepository,
16
+ planUpgrade,
17
+ } from './snapshot.mjs'
18
+ import { assertArtifactSchema, inspectContract } from './contract.mjs'
19
+
20
+ const commands = new Set(['generate', 'check', 'doctor', 'hook', 'attest', 'upgrade'])
21
+
22
+ function usage(message = 'A command is required.') {
23
+ return result('usage', [diagnostic({
24
+ ruleId: 'GOV-CONTRACT-001',
25
+ severity: 'critical',
26
+ kind: 'usage',
27
+ outcome: 'ERROR',
28
+ message: `${message} Commands: generate, check, doctor, hook, attest, upgrade plan, upgrade apply.`,
29
+ })])
30
+ }
31
+
32
+ function parse(argv, fixedCommand) {
33
+ const args = [...argv]
34
+ const command = fixedCommand || args.shift()
35
+ if (!commands.has(command)) return { error: usage(command ? `Unknown command: ${command}.` : undefined) }
36
+ let upgradeAction
37
+ if (command === 'upgrade') upgradeAction = args.shift()
38
+ const options = {}
39
+ const booleanOptions = new Set(['json', 'hooks-off', 'help'])
40
+ const keyMap = new Map([
41
+ ['repo', 'repoRoot'],
42
+ ['manifest', 'manifestPath'],
43
+ ['role', 'role'],
44
+ ['output', 'outputDirectory'],
45
+ ['lock', 'lockFile'],
46
+ ['provider', 'provider'],
47
+ ['plan-file', 'planFile'],
48
+ ['attestation-file', 'attestationFile'],
49
+ ['at', 'issuedAt'],
50
+ ['git-head', 'gitHead'],
51
+ ])
52
+ while (args.length) {
53
+ const token = args.shift()
54
+ if (!token.startsWith('--')) return { error: usage(`Unexpected argument: ${token}.`) }
55
+ const rawKey = token.slice(2)
56
+ if (booleanOptions.has(rawKey)) {
57
+ options[rawKey === 'hooks-off' ? 'hooksOff' : rawKey] = true
58
+ continue
59
+ }
60
+ const key = keyMap.get(rawKey)
61
+ if (!key) return { error: usage(`Unknown option: ${token}.`) }
62
+ const value = args.shift()
63
+ if (value == null || value.startsWith('--')) return { error: usage(`${token} requires a value.`) }
64
+ options[key] = value
65
+ }
66
+ if (options.repoRoot) options.repoRoot = resolve(options.repoRoot)
67
+ if (options.manifestPath) options.manifestPath = resolve(options.manifestPath)
68
+ return { command, upgradeAction, options }
69
+ }
70
+
71
+ async function hookCommand(options) {
72
+ let input
73
+ try {
74
+ const raw = await readStdin()
75
+ input = raw.trim() ? JSON.parse(raw) : {}
76
+ } catch {
77
+ input = null
78
+ }
79
+ const evaluated = await evaluateHook({ ...options, input })
80
+ return { value: evaluated.result, exitCode: evaluated.exitCode }
81
+ }
82
+
83
+ async function execute(parsed) {
84
+ const { command, upgradeAction, options } = parsed
85
+ if (options.help) return { value: usage(), exitCode: 1 }
86
+ if (command === 'generate') return { value: await generateRepository(options) }
87
+ if (command === 'check') return { value: await checkRepository(options) }
88
+ if (command === 'doctor') return { value: await doctorRepository(options) }
89
+ if (command === 'hook') {
90
+ if (!options.provider) return { value: usage('hook requires --provider.'), exitCode: 2 }
91
+ return hookCommand(options)
92
+ }
93
+ if (command === 'attest') return { value: await attestRepository(options) }
94
+ if (command === 'upgrade') {
95
+ if (upgradeAction === 'plan') return { value: await planUpgrade(options) }
96
+ if (upgradeAction === 'apply') return { value: await applyUpgrade(options) }
97
+ return { value: usage('upgrade requires plan or apply.') }
98
+ }
99
+ return { value: usage() }
100
+ }
101
+
102
+ export async function runCli({ argv = process.argv.slice(2), fixedCommand } = {}) {
103
+ const parsed = parse(argv, fixedCommand)
104
+ const executed = parsed.error ? { value: parsed.error } : await execute(parsed)
105
+ const value = executed.value
106
+ if (!parsed.error) {
107
+ const inspected = await inspectContract(parsed.options)
108
+ if (inspected.contract) await assertArtifactSchema(inspected.contract, 'diagnostic', value)
109
+ }
110
+ process.stdout.write(parsed.options?.json ? stableJson(value) : `${humanResult(value)}\n`)
111
+ const successful = ['PASS', 'PLAN', 'APPLIED'].includes(value.outcome)
112
+ process.exitCode = executed.exitCode ?? (successful ? 0 : 1)
113
+ return value
114
+ }