verify-archived-agent-notes.ts 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. /** Verify and append-seal the frozen Agent Note archive. */
  2. import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
  3. import { resolve } from 'node:path'
  4. import { AGENT_NOTE_CLASSES, agentNoteRoot } from './agent-note-tree.ts'
  5. import {
  6. extendArchiveManifest,
  7. parseArchiveManifest,
  8. renderArchiveManifest,
  9. validateArchiveArtifacts,
  10. type ArchiveManifest,
  11. } from './archived-agent-notes.ts'
  12. const args = process.argv.slice(2)
  13. const writeMode = args.length === 1 && args[0] === '--write'
  14. if (args.length > 0 && !writeMode) {
  15. console.error('verify-archived-agent-notes: usage: tsx scripts/verify-archived-agent-notes.ts [--write]')
  16. process.exit(1)
  17. }
  18. const archiveRoot = resolve(agentNoteRoot, 'archived')
  19. const manifestPath = resolve(archiveRoot, 'manifest.json')
  20. const errors: string[] = []
  21. const allowedRootFiles = new Set(['AGENTS.md', 'manifest.json'])
  22. const kinds = new Set<string>()
  23. if (!existsSync(resolve(archiveRoot, 'AGENTS.md'))) errors.push('archived/AGENTS.md is required')
  24. const artifacts = new Map<string, Buffer>()
  25. for (const entry of readdirSync(archiveRoot, { withFileTypes: true })) {
  26. if (entry.isFile()) {
  27. if (!allowedRootFiles.has(entry.name)) errors.push(`archived/${entry.name}: unexpected root file`)
  28. continue
  29. }
  30. if (!entry.isDirectory()) {
  31. errors.push(`archived/${entry.name}: only regular files and kind directories are allowed`)
  32. continue
  33. }
  34. if (!(AGENT_NOTE_CLASSES as readonly string[]).includes(entry.name)) {
  35. errors.push(`archived/${entry.name}/: unknown Agent Note kind`)
  36. continue
  37. }
  38. kinds.add(entry.name)
  39. for (const child of readdirSync(resolve(archiveRoot, entry.name), { withFileTypes: true })) {
  40. const rel = `${entry.name}/${child.name}`
  41. if (!child.isFile()) {
  42. errors.push(`${rel}: archived kind directories contain regular files only`)
  43. continue
  44. }
  45. artifacts.set(rel, readFileSync(resolve(archiveRoot, rel)))
  46. }
  47. }
  48. for (const kind of AGENT_NOTE_CLASSES) {
  49. if (!kinds.has(kind)) errors.push(`archived/${kind}/: required kind directory is missing`)
  50. }
  51. errors.push(...validateArchiveArtifacts(artifacts))
  52. let manifest: ArchiveManifest = { version: 1, files: {} }
  53. if (existsSync(manifestPath)) {
  54. try {
  55. manifest = parseArchiveManifest(readFileSync(manifestPath, 'utf8'))
  56. } catch (error: unknown) {
  57. errors.push(`archived/manifest.json: ${error instanceof Error ? error.message : String(error)}`)
  58. }
  59. } else if (!writeMode) {
  60. errors.push('archived/manifest.json is required; seal new artifacts with `pnpm run verify-archived-agent-notes --write`')
  61. }
  62. const extended = extendArchiveManifest(manifest, artifacts)
  63. errors.push(...extended.errors)
  64. if (!writeMode) {
  65. for (const path of extended.added) errors.push(`${path}: archived artifact is not sealed in manifest.json`)
  66. }
  67. if (errors.length > 0) {
  68. console.error('verify-archived-agent-notes: archive contract violated:')
  69. for (const error of errors) console.error(` ${error}`)
  70. process.exit(1)
  71. }
  72. if (writeMode) {
  73. const rendered = renderArchiveManifest(extended.files)
  74. if (!existsSync(manifestPath) || readFileSync(manifestPath, 'utf8') !== rendered) {
  75. writeFileSync(manifestPath, rendered)
  76. }
  77. console.log(`verify-archived-agent-notes: sealed ${extended.added.length} new artifact(s); existing seals unchanged.`)
  78. } else {
  79. console.log(`verify-archived-agent-notes: ${artifacts.size} frozen artifact(s) checked across ${kinds.size} kind(s).`)
  80. }