resave-hosted-empty-frontmatter.mjs
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d
docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge
Human
11 hours ago
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Phase D remediation: re-POST notes whose stored frontmatter parses to {} so the gateway |
| 4 | * merges provenance (knowtation_edited_at, etc.) and optional title from the path. |
| 5 | * |
| 6 | * Use only after verify-hosted-hub-api investigation shows write_path_ok_legacy_data_likely. |
| 7 | * |
| 8 | * KNOWTATION_HUB_TOKEN='...' node scripts/resave-hosted-empty-frontmatter.mjs --dry-run |
| 9 | * KNOWTATION_HUB_TOKEN='...' node scripts/resave-hosted-empty-frontmatter.mjs --execute |
| 10 | * |
| 11 | * Same env as verify: KNOWTATION_HUB_API, KNOWTATION_HUB_VAULT_ID, KNOWTATION_HUB_TOKEN_FILE |
| 12 | */ |
| 13 | |
| 14 | import fs from 'fs'; |
| 15 | import path from 'path'; |
| 16 | import { fileURLToPath } from 'url'; |
| 17 | import dotenv from 'dotenv'; |
| 18 | import { materializeListFrontmatter } from '../hub/gateway/note-facets.mjs'; |
| 19 | |
| 20 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 21 | const repoRoot = path.resolve(__dirname, '..'); |
| 22 | dotenv.config({ path: path.join(repoRoot, '.env') }); |
| 23 | |
| 24 | function resolveToken() { |
| 25 | let t = process.env.KNOWTATION_HUB_TOKEN || process.env.HUB_JWT || ''; |
| 26 | const fp = (process.env.KNOWTATION_HUB_TOKEN_FILE || '').trim(); |
| 27 | if (!t && fp) { |
| 28 | const expanded = fp.startsWith('~') ? path.join(process.env.HOME || '', fp.slice(1)) : fp; |
| 29 | t = fs.readFileSync(expanded, 'utf8').trim(); |
| 30 | } |
| 31 | return t; |
| 32 | } |
| 33 | |
| 34 | const token = resolveToken(); |
| 35 | const apiBase = (process.env.KNOWTATION_HUB_API || 'https://knowtation-gateway.netlify.app').replace(/\/$/, ''); |
| 36 | const vaultId = process.env.KNOWTATION_HUB_VAULT_ID || 'default'; |
| 37 | const execute = process.argv.includes('--execute'); |
| 38 | const dryRun = process.argv.includes('--dry-run') || !execute; |
| 39 | |
| 40 | function headers() { |
| 41 | return { |
| 42 | Accept: 'application/json', |
| 43 | 'Content-Type': 'application/json', |
| 44 | 'X-Vault-Id': vaultId, |
| 45 | Authorization: 'Bearer ' + token, |
| 46 | }; |
| 47 | } |
| 48 | |
| 49 | function titleFromPath(p) { |
| 50 | const base = path.posix.basename(String(p || ''), '.md'); |
| 51 | return base || 'note'; |
| 52 | } |
| 53 | |
| 54 | async function main() { |
| 55 | if (!token) { |
| 56 | console.error('Set KNOWTATION_HUB_TOKEN (or HUB_JWT / KNOWTATION_HUB_TOKEN_FILE).'); |
| 57 | process.exit(1); |
| 58 | } |
| 59 | if (!execute && !process.argv.includes('--dry-run')) { |
| 60 | console.error('Pass --dry-run (default) or --execute.'); |
| 61 | process.exit(1); |
| 62 | } |
| 63 | |
| 64 | const listRes = await fetch(`${apiBase}/api/v1/notes?limit=500&offset=0`, { headers: headers() }); |
| 65 | const listText = await listRes.text(); |
| 66 | if (!listRes.ok) { |
| 67 | console.error('List failed', listRes.status, listText.slice(0, 400)); |
| 68 | process.exit(1); |
| 69 | } |
| 70 | const data = JSON.parse(listText); |
| 71 | const notes = Array.isArray(data.notes) ? data.notes : []; |
| 72 | const targets = []; |
| 73 | for (const n of notes) { |
| 74 | const fm = materializeListFrontmatter(n.frontmatter); |
| 75 | if (Object.keys(fm).length === 0 && n.path) targets.push(n.path); |
| 76 | } |
| 77 | console.log('empty_frontmatter_paths', targets.length); |
| 78 | if (dryRun) { |
| 79 | targets.forEach((p) => console.log('would_resave', p)); |
| 80 | console.log('Dry run only. Re-run with --execute to POST each note (same body, new minimal frontmatter).'); |
| 81 | return; |
| 82 | } |
| 83 | |
| 84 | for (const p of targets) { |
| 85 | const enc = encodeURIComponent(p); |
| 86 | const getRes = await fetch(`${apiBase}/api/v1/notes/${enc}`, { headers: headers() }); |
| 87 | const getText = await getRes.text(); |
| 88 | if (!getRes.ok) { |
| 89 | console.error('GET failed', p, getRes.status, getText.slice(0, 120)); |
| 90 | continue; |
| 91 | } |
| 92 | const note = JSON.parse(getText); |
| 93 | const body = typeof note.body === 'string' ? note.body : ''; |
| 94 | const fmStr = JSON.stringify({ title: titleFromPath(p) }); |
| 95 | const postBody = JSON.stringify({ path: p, body, frontmatter: fmStr }); |
| 96 | const postRes = await fetch(`${apiBase}/api/v1/notes`, { |
| 97 | method: 'POST', |
| 98 | headers: headers(), |
| 99 | body: postBody, |
| 100 | }); |
| 101 | const postText = await postRes.text(); |
| 102 | console.log('POST', p, postRes.status, postRes.ok ? 'ok' : postText.slice(0, 80)); |
| 103 | await new Promise((r) => setTimeout(r, 150)); |
| 104 | } |
| 105 | console.log('Done. Re-run npm run verify:hosted-api to confirm empty_frontmatter_count dropped.'); |
| 106 | } |
| 107 | |
| 108 | main().catch((e) => { |
| 109 | console.error(e); |
| 110 | process.exit(1); |
| 111 | }); |
File History
1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d
docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge
Human
11 hours ago