ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs
Kjell Tore Guttormsen a5a13b010a docs(migration): record that step 5 never reached the published repos
org-ops reported that 30-fix-references.mjs:3-5 promises a disclosure-link
rewrite that 10 published plugin repos do not have. Confirmed by measurement,
at each plugin's catalog-pinned tag rather than in the sibling working trees:
10 of 12 still carry ../../README.md#ai-generated-code-disclosure on README.md:7.
Only repo-mailbox and repo-standard — both created after the migration — are clean.

The rewriter is not broken. Its DISCLOSURE_LINK regex was re-verified against the
live text at okr@v1.8.2:README.md:7 and matches, so "fix the script" would have
been a no-op. What never happened is the application: the migration's push half
was operator-gated and ran 0 pushes (review.md:79), so this step's output never
reached the content that was published. The header was true about the code and
false about the outcome — which is exactly how it got read as a receipt.

The header now says so, with the measurement date and method. Remediation is not
the catalog's to make: each plugin repo owns its README, and all 10 were notified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bmkz7r2zycwwG2kcCCf9H
2026-08-03 21:12:50 +02:00

116 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
// ⚠ NOT APPLIED TO THE PUBLISHED REPOS. Read item 1 below as a description of what this code DOES,
// never as a record of what the standalone repos CONTAIN. Measured 2026-08-03 at each plugin's
// catalog-pinned tag: 10 of 12 still carry the `../../README.md#ai-generated-code-disclosure` link
// on README.md:7 (only repo-mailbox and repo-standard, both created after the migration, are clean).
// The rewriter itself is correct — its DISCLOSURE_LINK regex was re-verified against the live text
// at okr@v1.8.2:README.md:7 and matches. What never happened is the application: the migration's
// push half was operator-gated (review.md:79 — "full local dry-run 11/11 targets, 0 pushes"), so
// this step's output never reached the content that was published. Remediation is NOT the catalog's
// to make — each plugin repo owns its own README; all 10 were notified 2026-08-03.
//
// Step 5 — Reference-rot rewriter. Operates on an extracted repo in $WORK/<key>.
// 1. Replaces the "[Full disclosure →](../../README.md#ai-generated-code-disclosure)" footnote with
// INLINE disclosure text (M13 — self-contained, no dangling cross-repo anchor; the monorepo root
// README has no such section, so the link was dead even in-repo).
// 2. Rewrites any remaining ../../README.md and ../../.claude-plugin/marketplace.json reference
// (e.g. graceful-handoff README footer, linkedin-studio remediation docs) to the absolute catalog URL.
// 3. Sets plugin.json `repository` (and package.json homepage/repository/bugs, when present) to the
// standalone open/<plugin> HTTPS URL read from plugin-map.json, and DROPS any monorepo-relative
// `repository.directory` sub-path (e.g. llm-security's "plugins/llm-security") — it points nowhere
// once the content lives at the standalone repo root.
// ai-psychosis already points at open/ai-psychosis (verify-only no-op, M14). Idempotent: a second run
// rewrites zero files. Reports every file it touched. NULL push (D8) — operates only inside $WORK/<key>.
//
// Usage: node 30-fix-references.mjs <target-key>
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const here = path.dirname(fileURLToPath(import.meta.url));
const MAP = path.join(here, 'plugin-map.json');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
const CATALOG = 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main';
const DISCLOSURE_LINK = /\[Full disclosure →\]\(\.\.\/\.\.\/README\.md#ai-generated-code-disclosure\)/g;
const DISCLOSURE_INLINE = 'Every change is human-directed, reviewed, and validated before commit.';
const readJson = async (p) => JSON.parse(await fs.readFile(p, 'utf8'));
const exists = async (p) => { try { await fs.access(p); return true; } catch { return false; } };
async function walkMd(dir, out = []) {
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
if (e.name === '.git') continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) await walkMd(full, out);
else if (e.isFile() && e.name.endsWith('.md')) out.push(full);
}
return out;
}
async function main() {
const key = process.argv[2];
if (!key) { console.error('usage: 30-fix-references.mjs <target-key>'); process.exit(2); }
const map = await readJson(MAP);
const t = map.targets[key];
if (!t) { console.error(`unknown target ${key}`); process.exit(2); }
const dest = path.join(WORK, key);
if (!(await exists(path.join(dest, '.git')))) {
const r = spawnSync('bash', [path.join(here, '10-extract.sh'), key], { stdio: 'inherit', env: { ...process.env, WORK } });
if (r.status !== 0) { console.error('extract failed'); process.exit(1); }
}
const base = t.repo_url.replace(/\.git$/, ''); // https://git.fromaitochitta.com/open/<key>
const changed = [];
// 1) + 2) markdown rewrites
for (const file of await walkMd(dest)) {
const orig = await fs.readFile(file, 'utf8');
let next = orig.replace(DISCLOSURE_LINK, DISCLOSURE_INLINE);
next = next.split('../../README.md').join(`${CATALOG}/README.md`);
next = next.split('../../.claude-plugin/marketplace.json').join(`${CATALOG}/.claude-plugin/marketplace.json`);
if (next !== orig) { await fs.writeFile(file, next, 'utf8'); changed.push(path.relative(dest, file)); }
}
// 3a) plugin.json repository → standalone URL (+ drop any stale monorepo-relative repository.directory)
const pjPath = path.join(dest, '.claude-plugin', 'plugin.json');
if (await exists(pjPath)) {
const pj = await readJson(pjPath);
let touched = false;
if (pj.repository && typeof pj.repository === 'object') {
if (pj.repository.url !== base) { pj.repository.url = base; touched = true; }
// A monorepo-relative repository.directory points nowhere in the standalone repo — drop it.
if ('directory' in pj.repository) { delete pj.repository.directory; touched = true; }
} else if (pj.repository !== base) {
pj.repository = base; touched = true;
}
if (touched) {
await fs.writeFile(pjPath, JSON.stringify(pj, null, 2) + '\n', 'utf8');
changed.push('.claude-plugin/plugin.json');
}
}
// 3b) package.json homepage/repository/bugs (when present)
const pkgPath = path.join(dest, 'package.json');
if (await exists(pkgPath)) {
const pkg = await readJson(pkgPath);
let touched = false;
if (pkg.homepage !== base) { pkg.homepage = base; touched = true; }
if (pkg.repository && typeof pkg.repository === 'object') {
if (pkg.repository.url !== base) { pkg.repository.url = base; touched = true; }
// Drop any stale monorepo-relative repository.directory (points nowhere in the standalone repo).
if ('directory' in pkg.repository) { delete pkg.repository.directory; touched = true; }
} else if (pkg.repository && pkg.repository !== base) { pkg.repository = base; touched = true; }
if (pkg.bugs && typeof pkg.bugs === 'object' && pkg.bugs.url !== `${base}/issues`) {
pkg.bugs.url = `${base}/issues`; touched = true;
}
if (touched) { await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); changed.push('package.json'); }
}
for (const f of changed) console.log(` rewrote ${f}`);
console.log(`FIX-REFERENCES OK ${key} → rewrote ${changed.length} file(s)`);
}
main().catch((e) => { console.error(`Error: ${e.message}`); process.exit(1); });