@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,520 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { realpathSync } from 'node:fs'
3
+ import { cp, mkdir, readFile, rename, rm } from 'node:fs/promises'
4
+ import { basename, dirname, resolve } from 'node:path'
5
+ import { mkdtemp } from 'node:fs/promises'
6
+ import { tmpdir } from 'node:os'
7
+ import {
8
+ PACKAGE_VERSION,
9
+ assertNoSymlinkWithin,
10
+ canonicalize,
11
+ compareUtf8Bytes,
12
+ compareInventories,
13
+ diagnostic,
14
+ digest,
15
+ exists,
16
+ inventoryFile,
17
+ inventoryTree,
18
+ resolveWithin,
19
+ result,
20
+ stableJson,
21
+ treeDigest,
22
+ writeFileWithMode,
23
+ writeJsonAtomic,
24
+ } from './common.mjs'
25
+ import {
26
+ assertArtifactSchema,
27
+ inspectContract,
28
+ inspectSkillParity,
29
+ sourceRecords,
30
+ } from './contract.mjs'
31
+ import {
32
+ assertClosedGitLocalConfiguration,
33
+ runClosedGit,
34
+ } from './closed-tool-execution.mjs'
35
+
36
+ function provenance(contract) {
37
+ return {
38
+ generated: true,
39
+ generator: '@qijenchen/governance',
40
+ generatorVersion: PACKAGE_VERSION,
41
+ manifestId: contract.manifest.id,
42
+ contractDigest: contract.contractDigest,
43
+ role: contract.roleId,
44
+ }
45
+ }
46
+
47
+ function hasFailure(diagnostics) {
48
+ return diagnostics.some((item) => ['FAIL', 'BLOCK', 'ERROR'].includes(item.outcome))
49
+ }
50
+
51
+ function applicableRules(contract) {
52
+ return contract.rules
53
+ .filter((rule) => rule.appliesToRoles?.includes('*') || rule.appliesToRoles?.includes(contract.roleId))
54
+ .map((rule) => ({
55
+ ruleId: rule.ruleId,
56
+ severity: rule.severity,
57
+ source: rule.source,
58
+ blocking: rule.blocking,
59
+ gateIds: rule.gateIds,
60
+ providerCoverage: rule.providerCoverage,
61
+ }))
62
+ .sort((a, b) => compareUtf8Bytes(a.ruleId, b.ruleId))
63
+ }
64
+
65
+ function providerAdapter(contract, provider) {
66
+ return `#!/usr/bin/env node
67
+ // @generated by @qijenchen/governance ${PACKAGE_VERSION}; role=${contract.roleId}; contract=${contract.contractDigest}; DO NOT EDIT.
68
+ import { runHookCli } from '@qijenchen/governance/hook'
69
+
70
+ await runHookCli({ provider: ${JSON.stringify(provider.id)}, role: ${JSON.stringify(contract.roleId)} })
71
+ `
72
+ }
73
+
74
+ function provenanceMarkdown(contract, sources, skillParity) {
75
+ const sourceRows = sources.map((source) => `| ${source.id} | \`${source.path}\` | \`${source.hash}\` |`).join('\n')
76
+ const parityRows = skillParity.map((item) => `| ${item.id} | ${item.sourceProvider} \`${item.sourceDirectory}\` | ${item.targetProvider} \`${item.targetDirectory}\` | ${item.sourceSkills.length} shared + ${item.requiredTargetExtras.length} target-only |`).join('\n')
77
+ return `<!-- @generated by @qijenchen/governance ${PACKAGE_VERSION}; role=${contract.roleId}; contract=${contract.contractDigest}; DO NOT EDIT. -->
78
+ # Governance snapshot provenance
79
+
80
+ - Manifest: \`${contract.manifest.id}\`
81
+ - Role: \`${contract.roleId}\`
82
+ - Contract digest: \`${contract.contractDigest}\`
83
+ - Generator: \`@qijenchen/governance@${PACKAGE_VERSION}\`
84
+
85
+ No rule prose is copied into this snapshot. Paths and digests point to their canonical owners.
86
+
87
+ ## Canonical inputs
88
+
89
+ | ID | Owner path | Digest |
90
+ |---|---|---|
91
+ ${sourceRows}
92
+
93
+ ## Provider skill parity
94
+
95
+ | Contract | Source | Target | Expected surface |
96
+ |---|---|---|---|
97
+ ${parityRows || '| — | — | — | — |'}
98
+ `
99
+ }
100
+
101
+ export async function buildExpectedSnapshot(contract, temporaryRepoRoot) {
102
+ const output = resolveWithin(temporaryRepoRoot, contract.outputDirectory, 'outputDirectory')
103
+ const lock = resolveWithin(temporaryRepoRoot, contract.lockFile, 'lockFile')
104
+ await rm(output, { recursive: true, force: true })
105
+ await rm(lock, { force: true })
106
+ await mkdir(output, { recursive: true })
107
+
108
+ const sources = await sourceRecords(contract)
109
+ const parity = await inspectSkillParity(contract)
110
+ const roleProviders = (contract.role?.requiredProviders || [])
111
+ .map((id) => contract.providers.find((provider) => provider.id === id))
112
+ .filter(Boolean)
113
+ const roleGates = (contract.role?.requiredGateIds || [])
114
+ .map((id) => contract.gates.find((gate) => gate.id === id))
115
+ .filter(Boolean)
116
+ const generatedProvenance = provenance(contract)
117
+
118
+ const controlPlane = {
119
+ _provenance: generatedProvenance,
120
+ schemaVersion: 1,
121
+ role: contract.roleId,
122
+ canonicalSources: sources,
123
+ rules: applicableRules(contract),
124
+ providers: roleProviders.map((provider) => provider.id),
125
+ gates: roleGates.map((gate) => gate.id),
126
+ skillParity: parity.records,
127
+ registryDigests: contract.registryDigests,
128
+ schemaDigests: contract.schemaDigests,
129
+ }
130
+ await writeFileWithMode(resolve(output, 'control-plane.json'), stableJson(controlPlane), 0o644)
131
+
132
+ for (const provider of roleProviders) {
133
+ await writeFileWithMode(resolve(output, 'providers', `${provider.id}.json`), stableJson({
134
+ _provenance: generatedProvenance,
135
+ schemaVersion: 1,
136
+ provider,
137
+ applicableRules: applicableRules(contract).map((rule) => ({ ruleId: rule.ruleId, coverage: rule.providerCoverage[provider.id] })),
138
+ }), 0o644)
139
+ await writeFileWithMode(resolve(output, 'adapters', `${provider.id}-hook.mjs`), providerAdapter(contract, provider), 0o755)
140
+ }
141
+
142
+ await writeFileWithMode(resolve(output, 'PROVENANCE.md'), provenanceMarkdown(contract, sources, parity.records), 0o644)
143
+ const files = await inventoryTree(output)
144
+ const inputDigest = digest({
145
+ contractDigest: contract.contractDigest,
146
+ registryDigests: contract.registryDigests,
147
+ sources,
148
+ skillParity: parity.records,
149
+ })
150
+ const lockBody = {
151
+ _provenance: generatedProvenance,
152
+ schemaVersion: 2,
153
+ role: contract.roleId,
154
+ outputDirectory: contract.outputDirectory,
155
+ contractDigest: contract.contractDigest,
156
+ inputDigest,
157
+ snapshotDigest: treeDigest(files),
158
+ sources,
159
+ skillParity: parity.records,
160
+ files,
161
+ }
162
+ await assertArtifactSchema(contract, 'lock', lockBody)
163
+ await writeFileWithMode(lock, stableJson(lockBody), 0o644)
164
+ return { output, lock, files, lockBody, parityDiagnostics: parity.diagnostics }
165
+ }
166
+
167
+ async function withExpected(contract, callback) {
168
+ const temporary = await mkdtemp(resolve(tmpdir(), 'qijenchen-governance-'))
169
+ try {
170
+ const expected = await buildExpectedSnapshot(contract, temporary)
171
+ return await callback(expected, temporary)
172
+ } finally {
173
+ await rm(temporary, { recursive: true, force: true })
174
+ }
175
+ }
176
+
177
+ async function checkWithContract(contract) {
178
+ return withExpected(contract, async (expected) => {
179
+ const actualOutput = resolveWithin(contract.repoRoot, contract.outputDirectory, 'outputDirectory')
180
+ const actualLock = resolveWithin(contract.repoRoot, contract.lockFile, 'lockFile')
181
+ const expectedInventory = await inventoryTree(expected.output)
182
+ const actualInventory = await inventoryTree(actualOutput)
183
+ const diagnostics = compareInventories(expectedInventory, actualInventory, { prefix: contract.outputDirectory })
184
+ const expectedLock = await inventoryFile(expected.lock, contract.lockFile)
185
+ const actualLockInventory = await inventoryFile(actualLock, contract.lockFile)
186
+ diagnostics.push(...compareInventories(expectedLock, actualLockInventory))
187
+
188
+ if (actualLockInventory.length) {
189
+ try {
190
+ const body = JSON.parse(await readFile(actualLock, 'utf8'))
191
+ const lockedFiles = Array.isArray(body.files) ? body.files : []
192
+ diagnostics.push(...compareInventories(lockedFiles, actualInventory, { prefix: contract.outputDirectory }))
193
+ } catch (error) {
194
+ diagnostics.push(diagnostic({ ruleId: 'GOV-SNAPSHOT-001', severity: 'critical', kind: 'lock-parse-error', path: contract.lockFile, message: `Governance lock is invalid JSON: ${error.message}` }))
195
+ }
196
+ }
197
+ return { diagnostics, expectedDigest: await snapshotStateDigest(expected.output, expected.lock), actualDigest: await snapshotStateDigest(actualOutput, actualLock) }
198
+ })
199
+ }
200
+
201
+ export async function checkRepository(options = {}) {
202
+ const inspected = await inspectContract(options)
203
+ if (!inspected.contract || hasFailure(inspected.diagnostics)) return result('check', inspected.diagnostics)
204
+ const parity = await inspectSkillParity(inspected.contract)
205
+ const checked = await checkWithContract(inspected.contract)
206
+ const diagnostics = [...inspected.diagnostics, ...parity.diagnostics, ...checked.diagnostics]
207
+ const hooksOff = Boolean(options.hooksOff)
208
+ if (!diagnostics.length) diagnostics.push(diagnostic({ ruleId: 'GOV-SNAPSHOT-001', severity: 'info', kind: 'snapshot-exact', path: inspected.contract.outputDirectory, outcome: 'PASS', expected: checked.expectedDigest, actual: checked.actualDigest, message: `Snapshot is exact${hooksOff ? ' with native hooks disabled' : ''}.` }))
209
+ return result('check', diagnostics, { role: inspected.contract.roleId, contractDigest: inspected.contract.contractDigest, snapshotDigest: checked.actualDigest, hooksOff })
210
+ }
211
+
212
+ export async function doctorRepository(options = {}) {
213
+ const checked = await checkRepository(options)
214
+ const diagnostics = [...checked.diagnostics]
215
+ if (!diagnostics.some((item) => ['FAIL', 'BLOCK', 'ERROR'].includes(item.outcome))) {
216
+ diagnostics.push(diagnostic({ ruleId: 'GOV-CONTRACT-001', severity: 'info', kind: 'contract-valid', path: options.manifestPath || 'canonical/manifest.json', outcome: 'PASS', message: 'Canonical registries, source pointers, providers, gates, skills, snapshot, and lock are coherent.' }))
217
+ }
218
+ return result('doctor', diagnostics, checked.data)
219
+ }
220
+
221
+ async function copyExpectedToStage(expectedOutput, stageOutput) {
222
+ await cp(expectedOutput, stageOutput, { recursive: true, force: true, preserveTimestamps: false })
223
+ const entries = await inventoryTree(expectedOutput)
224
+ for (const entry of entries) {
225
+ if (entry.type === 'file') {
226
+ const mode = Number.parseInt(entry.mode, 8)
227
+ const { chmod } = await import('node:fs/promises')
228
+ await chmod(resolve(stageOutput, entry.path), mode)
229
+ }
230
+ }
231
+ }
232
+
233
+ // Exported from this module (but not the package entrypoint) so rollback can be
234
+ // fault-injected and proven without weakening the production CLI contract.
235
+ export async function installExpected(contract, expected, { transitionProbe } = {}) {
236
+ const targetOutput = resolveWithin(contract.repoRoot, contract.outputDirectory, 'outputDirectory')
237
+ const targetLock = resolveWithin(contract.repoRoot, contract.lockFile, 'lockFile')
238
+ await assertNoSymlinkWithin(contract.repoRoot, targetOutput, 'outputDirectory')
239
+ await assertNoSymlinkWithin(contract.repoRoot, targetLock, 'lockFile')
240
+ const token = `${process.pid}-${randomUUID()}`
241
+ const stageOutput = resolve(dirname(targetOutput), `.${basename(targetOutput)}.stage-${token}`)
242
+ const backupOutput = resolve(dirname(targetOutput), `.${basename(targetOutput)}.backup-${token}`)
243
+ const stageLock = resolve(dirname(targetLock), `.${basename(targetLock)}.stage-${token}`)
244
+ const backupLock = resolve(dirname(targetLock), `.${basename(targetLock)}.backup-${token}`)
245
+ let outputBackedUp = false
246
+ let lockBackedUp = false
247
+ let outputInstalled = false
248
+ let lockInstalled = false
249
+ try {
250
+ await mkdir(dirname(targetOutput), { recursive: true })
251
+ await mkdir(dirname(targetLock), { recursive: true })
252
+ await copyExpectedToStage(expected.output, stageOutput)
253
+ await cp(expected.lock, stageLock, { force: true })
254
+ if (await exists(targetOutput)) { await rename(targetOutput, backupOutput); outputBackedUp = true }
255
+ if (await exists(targetLock)) { await rename(targetLock, backupLock); lockBackedUp = true }
256
+ await rename(stageOutput, targetOutput); outputInstalled = true
257
+ if (transitionProbe) await transitionProbe('output-installed')
258
+ await rename(stageLock, targetLock); lockInstalled = true
259
+ } catch (error) {
260
+ const rollbackErrors = []
261
+ await rm(stageOutput, { recursive: true, force: true }).catch(() => {})
262
+ await rm(stageLock, { force: true }).catch(() => {})
263
+ try {
264
+ if (outputBackedUp) {
265
+ if (outputInstalled) await rm(targetOutput, { recursive: true, force: true })
266
+ await rename(backupOutput, targetOutput)
267
+ outputBackedUp = false
268
+ } else if (outputInstalled) {
269
+ await rm(targetOutput, { recursive: true, force: true })
270
+ }
271
+ } catch (rollbackError) {
272
+ rollbackErrors.push(rollbackError)
273
+ }
274
+ try {
275
+ if (lockBackedUp) {
276
+ if (lockInstalled) await rm(targetLock, { recursive: true, force: true })
277
+ await rename(backupLock, targetLock)
278
+ lockBackedUp = false
279
+ } else if (lockInstalled) {
280
+ await rm(targetLock, { recursive: true, force: true })
281
+ }
282
+ } catch (rollbackError) {
283
+ rollbackErrors.push(rollbackError)
284
+ }
285
+ if (rollbackErrors.length) throw new AggregateError([error, ...rollbackErrors], 'Governance install failed and rollback was incomplete')
286
+ throw error
287
+ } finally {
288
+ if (!outputBackedUp) await rm(backupOutput, { recursive: true, force: true }).catch(() => {})
289
+ if (!lockBackedUp) await rm(backupLock, { recursive: true, force: true }).catch(() => {})
290
+ }
291
+ await rm(backupOutput, { recursive: true, force: true }).catch(() => {})
292
+ await rm(backupLock, { recursive: true, force: true }).catch(() => {})
293
+ }
294
+
295
+ export async function generateRepository(options = {}) {
296
+ const inspected = await inspectContract(options)
297
+ if (!inspected.contract || hasFailure(inspected.diagnostics)) return result('generate', inspected.diagnostics)
298
+ const parity = await inspectSkillParity(inspected.contract)
299
+ const diagnostics = [...inspected.diagnostics, ...parity.diagnostics]
300
+ if (diagnostics.some((item) => ['FAIL', 'BLOCK', 'ERROR'].includes(item.outcome))) return result('generate', diagnostics)
301
+ return withExpected(inspected.contract, async (expected) => {
302
+ await installExpected(inspected.contract, expected)
303
+ const state = await snapshotState(inspected.contract)
304
+ diagnostics.push(diagnostic({ ruleId: 'GOV-SNAPSHOT-001', severity: 'info', kind: 'snapshot-generated', path: inspected.contract.outputDirectory, outcome: 'PASS', actual: state.digest, message: 'Canonical snapshot and committed lock were generated atomically.' }))
305
+ return result('generate', diagnostics, { role: inspected.contract.roleId, contractDigest: inspected.contract.contractDigest, snapshotDigest: state.digest })
306
+ })
307
+ }
308
+
309
+ async function snapshotStateDigest(output, lock) {
310
+ const files = await inventoryTree(output)
311
+ const lockEntries = await inventoryFile(lock, '__lock__')
312
+ return treeDigest([...files, ...lockEntries])
313
+ }
314
+
315
+ export async function snapshotState(contract) {
316
+ const output = resolveWithin(contract.repoRoot, contract.outputDirectory, 'outputDirectory')
317
+ const lock = resolveWithin(contract.repoRoot, contract.lockFile, 'lockFile')
318
+ const files = await inventoryTree(output)
319
+ const lockEntries = await inventoryFile(lock, '__lock__')
320
+ return { files, lockEntries, digest: treeDigest([...files, ...lockEntries]) }
321
+ }
322
+
323
+ function actionFromDiagnostic(item) {
324
+ const map = {
325
+ 'missing-file': 'add',
326
+ 'extra-file': 'remove',
327
+ 'content-hash-mismatch': 'replace-content',
328
+ 'mode-mismatch': 'replace-mode',
329
+ 'type-mismatch': 'replace-type',
330
+ }
331
+ const action = map[item.evidence?.kind]
332
+ return action ? { action, path: item.evidence.path } : null
333
+ }
334
+
335
+ export async function planUpgrade(options = {}) {
336
+ const inspected = await inspectContract(options)
337
+ if (!inspected.contract || hasFailure(inspected.diagnostics)) return result('upgrade.plan', inspected.diagnostics, undefined, 'FAIL')
338
+ const parity = await inspectSkillParity(inspected.contract)
339
+ const diagnostics = [...inspected.diagnostics, ...parity.diagnostics]
340
+ if (diagnostics.some((item) => ['FAIL', 'BLOCK', 'ERROR'].includes(item.outcome))) return result('upgrade.plan', diagnostics, undefined, 'FAIL')
341
+ const checked = await checkWithContract(inspected.contract)
342
+ const actions = checked.diagnostics.map(actionFromDiagnostic).filter(Boolean).sort((a, b) => compareUtf8Bytes(a.path, b.path) || compareUtf8Bytes(a.action, b.action))
343
+ const plan = canonicalize({
344
+ schemaVersion: 1,
345
+ command: 'upgrade.plan',
346
+ role: inspected.contract.roleId,
347
+ outputDirectory: inspected.contract.outputDirectory,
348
+ lockFile: inspected.contract.lockFile,
349
+ contractDigest: inspected.contract.contractDigest,
350
+ currentDigest: checked.actualDigest,
351
+ expectedDigest: checked.expectedDigest,
352
+ actions,
353
+ })
354
+ await assertArtifactSchema(inspected.contract, 'upgradePlan', plan)
355
+ diagnostics.push(diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'info', kind: 'upgrade-plan', path: inspected.contract.outputDirectory, outcome: 'PLAN', expected: checked.expectedDigest, actual: checked.actualDigest, message: `Upgrade plan contains ${actions.length} action(s).` }))
356
+ if (options.planFile) {
357
+ const planFile = resolveWithin(inspected.contract.repoRoot, options.planFile, 'planFile')
358
+ await assertNoSymlinkWithin(inspected.contract.repoRoot, planFile, 'planFile')
359
+ await writeJsonAtomic(planFile, plan)
360
+ }
361
+ return result('upgrade.plan', diagnostics, { plan }, 'PLAN')
362
+ }
363
+
364
+ export async function applyUpgrade(options = {}) {
365
+ const inspected = await inspectContract(options)
366
+ if (!inspected.contract || hasFailure(inspected.diagnostics)) return result('upgrade.apply', inspected.diagnostics)
367
+ if (!options.planFile) return result('upgrade.apply', [diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'critical', kind: 'plan-required', path: '', message: 'upgrade apply requires --plan-file.' })])
368
+ let plan
369
+ try {
370
+ const planFile = resolveWithin(inspected.contract.repoRoot, options.planFile, 'planFile')
371
+ await assertNoSymlinkWithin(inspected.contract.repoRoot, planFile, 'planFile', { allowMissing: false })
372
+ plan = JSON.parse(await readFile(planFile, 'utf8'))
373
+ await assertArtifactSchema(inspected.contract, 'upgradePlan', plan)
374
+ } catch (error) {
375
+ return result('upgrade.apply', [diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'critical', kind: 'plan-read-error', path: options.planFile, message: `Cannot read upgrade plan: ${error.message}` })])
376
+ }
377
+ const current = await snapshotState(inspected.contract)
378
+ const diagnostics = [...inspected.diagnostics]
379
+ if (plan.command !== 'upgrade.plan') diagnostics.push(diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'critical', kind: 'plan-schema', path: options.planFile, expected: 'upgrade.plan', actual: plan.command, message: 'File is not a governance upgrade plan.' }))
380
+ if (plan.contractDigest !== inspected.contract.contractDigest) diagnostics.push(diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'critical', kind: 'contract-changed', path: options.planFile, expected: plan.contractDigest, actual: inspected.contract.contractDigest, message: 'Canonical contract changed after the plan was created.' }))
381
+ if (plan.currentDigest !== current.digest) diagnostics.push(diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'critical', kind: 'stale-plan', path: inspected.contract.outputDirectory, expected: plan.currentDigest, actual: current.digest, message: 'Generated state changed after the plan was created.' }))
382
+ if (diagnostics.some((item) => ['FAIL', 'BLOCK', 'ERROR'].includes(item.outcome))) return result('upgrade.apply', diagnostics)
383
+
384
+ return withExpected(inspected.contract, async (expected) => {
385
+ const expectedDigest = await snapshotStateDigest(expected.output, expected.lock)
386
+ if (expectedDigest !== plan.expectedDigest) {
387
+ diagnostics.push(diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'critical', kind: 'expected-state-changed', path: options.planFile, expected: plan.expectedDigest, actual: expectedDigest, message: 'Expected generated state changed after the plan was created.' }))
388
+ return result('upgrade.apply', diagnostics)
389
+ }
390
+ await installExpected(inspected.contract, expected)
391
+ diagnostics.push(diagnostic({ ruleId: 'GOV-UPGRADE-001', severity: 'info', kind: 'upgrade-applied', path: inspected.contract.outputDirectory, outcome: 'APPLIED', actual: expectedDigest, message: 'Validated upgrade plan was applied.' }))
392
+ return result('upgrade.apply', diagnostics, { snapshotDigest: expectedDigest }, 'APPLIED')
393
+ })
394
+ }
395
+
396
+ export async function attestRepository(options = {}) {
397
+ const doctor = await doctorRepository(options)
398
+ if (doctor.outcome !== 'PASS') return result('attest', doctor.diagnostics)
399
+ const inspected = await inspectContract(options)
400
+ const contract = inspected.contract
401
+ const state = await snapshotState(contract)
402
+ let gitHead = null
403
+ let gitTree = null
404
+ let inGitRepository = false
405
+ try {
406
+ const probe = runClosedGit(['rev-parse', '--is-inside-work-tree'], {
407
+ cwd: contract.repoRoot,
408
+ timeoutMs: 5000,
409
+ })
410
+ inGitRepository = probe.status === 0 && probe.stdout.trim() === 'true'
411
+ } catch (error) {
412
+ return result('attest', [diagnostic({
413
+ ruleId: 'GOV-ATTEST-001',
414
+ severity: 'critical',
415
+ kind: 'closed-git-unavailable',
416
+ path: '',
417
+ message: `Cannot establish the closed Git runtime required for attestation: ${error.message}`,
418
+ })])
419
+ }
420
+ if (inGitRepository) {
421
+ try {
422
+ assertClosedGitLocalConfiguration(contract.repoRoot)
423
+ } catch (error) {
424
+ return result('attest', [diagnostic({
425
+ ruleId: 'GOV-ATTEST-001',
426
+ severity: 'critical',
427
+ kind: 'unsafe-git-configuration',
428
+ path: '',
429
+ message: error.message,
430
+ })])
431
+ }
432
+ const identity = runClosedGit(['rev-parse', '--show-toplevel', 'HEAD', 'HEAD^{tree}'], {
433
+ cwd: contract.repoRoot,
434
+ timeoutMs: 5000,
435
+ })
436
+ const identityLines = identity.status === 0 ? identity.stdout.trim().split(/\r?\n/) : []
437
+ let exactRepositoryRoot = false
438
+ try {
439
+ exactRepositoryRoot = identityLines.length > 0
440
+ && realpathSync(identityLines[0]) === realpathSync(contract.repoRoot)
441
+ } catch {}
442
+ if (
443
+ identityLines.length !== 3
444
+ || !exactRepositoryRoot
445
+ || !identityLines.slice(1).every(value => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value))
446
+ ) {
447
+ return result('attest', [diagnostic({
448
+ ruleId: 'GOV-ATTEST-001',
449
+ severity: 'critical',
450
+ kind: 'git-identity-unavailable',
451
+ path: '',
452
+ message: 'Cannot resolve the exact Git commit and tree through the closed Git runtime.',
453
+ })])
454
+ }
455
+ ;[, gitHead, gitTree] = identityLines
456
+ const status = runClosedGit([
457
+ 'status',
458
+ '--porcelain=v1',
459
+ '-z',
460
+ '--untracked-files=all',
461
+ '--ignore-submodules=none',
462
+ '--no-renames',
463
+ ], {
464
+ cwd: contract.repoRoot,
465
+ maxOutputBytes: 32 * 1024 * 1024,
466
+ output: 'buffer',
467
+ timeoutMs: 10_000,
468
+ })
469
+ if (status.status !== 0 || status.error || status.signal !== null) {
470
+ return result('attest', [diagnostic({
471
+ ruleId: 'GOV-ATTEST-001',
472
+ severity: 'critical',
473
+ kind: 'git-status-unavailable',
474
+ path: '',
475
+ message: 'Cannot resolve the complete worktree state through the closed Git runtime.',
476
+ })])
477
+ }
478
+ const dirty = status.stdout.length > 0
479
+ if (dirty) return result('attest', [diagnostic({ ruleId: 'GOV-ATTEST-001', severity: 'critical', kind: 'dirty-worktree', path: '', expected: 'clean Git snapshot', actual: 'non-empty porcelain-v1-z status', message: 'Refusing evidence for a dirty worktree; commit the exact checked snapshot first.' })])
480
+ if (options.gitHead != null && options.gitHead !== gitHead) return result('attest', [diagnostic({ ruleId: 'GOV-ATTEST-001', severity: 'critical', kind: 'git-head-mismatch', path: '', expected: gitHead, actual: options.gitHead, message: '--git-head does not match the checked repository HEAD.' })])
481
+ } else {
482
+ // Deterministic fixtures may supply a synthetic subject. Real release evidence is always issued
483
+ // from a Git repository and signed externally by GitHub OIDC artifact attestation.
484
+ gitHead = options.gitHead ?? null
485
+ }
486
+ const issuedAt = options.issuedAt || new Date().toISOString()
487
+ if (Number.isNaN(Date.parse(issuedAt))) return result('attest', [diagnostic({ ruleId: 'GOV-ATTEST-001', severity: 'critical', kind: 'invalid-issued-at', path: '', actual: issuedAt, message: '--at must be an ISO-8601 timestamp.' })])
488
+ const claims = ['snapshot-integrity', 'contract-doctor', 'provider-skill-parity']
489
+ if (options.hooksOff) claims.push('hook-independent-check')
490
+ claims.sort()
491
+ const evidenceDigest = digest({
492
+ role: contract.roleId,
493
+ contractDigest: contract.contractDigest,
494
+ snapshotDigest: state.digest,
495
+ gitHead,
496
+ gitTree,
497
+ claims,
498
+ })
499
+ const attestation = canonicalize({
500
+ _provenance: provenance(contract),
501
+ schemaVersion: 1,
502
+ issuedAt: new Date(issuedAt).toISOString(),
503
+ subject: `${contract.manifest.id}:${contract.roleId}`,
504
+ role: contract.roleId,
505
+ contractDigest: contract.contractDigest,
506
+ snapshotDigest: state.digest,
507
+ gitHead,
508
+ gitTree,
509
+ evidenceDigest,
510
+ claims,
511
+ })
512
+ await assertArtifactSchema(contract, 'attestation', attestation)
513
+ if (options.attestationFile) {
514
+ const attestationFile = resolveWithin(contract.repoRoot, options.attestationFile, 'attestationFile')
515
+ await assertNoSymlinkWithin(contract.repoRoot, attestationFile, 'attestationFile')
516
+ await writeJsonAtomic(attestationFile, attestation)
517
+ }
518
+ const diagnostics = [diagnostic({ ruleId: 'GOV-ATTEST-001', severity: 'info', kind: 'evidence-manifest-issued', path: options.attestationFile || '<stdout>', outcome: 'PASS', actual: evidenceDigest, message: 'Digest-bound evidence manifest issued for the exact claims actually checked; external signing is separate.' })]
519
+ return result('attest', diagnostics, { attestation })
520
+ }