mirror of
https://github.com/vectorize-io/hindsight.git
synced 2026-09-14 19:31:49 +08:00
6f2799569f
* docs: link every integration changelog from its doc page The release script has written a changelog page per integration since 0.4.20 (src/pages/changelog/integrations/<slug>.md), but nothing reliably linked them: 16 of 61 doc pages carried a "View Changelog" link, and the only index was a hand-written table at the bottom of /changelog listing 6 of the 48 pages, stale since the release that introduced them. coding-agents was one of the 31 pages with a changelog nobody could reach except by guessing the URL. Adds the link to the 30 pages missing one, plus a third invariant in check-integrations.mjs so the next one can't ship unlinked: every changelog page whose integration has a gallery entry must be linked from its doc page. coding-agents.md is generated, so its link goes in the README as an absolute hindsight.vectorize.io URL — the sync script rewrites our own absolute URLs to site-relative, so it resolves on npm, GitHub and the docs site. It sits outside the skill:begin regions, so SKILL.md is unchanged. Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN * docs(integrations): compact grouped gallery with Docs/Changelog buttons The Integrations Hub listed 60 three-line cards with a repeated "Hindsight Team" byline and an OFFICIAL badge on 52 of them, running 6092px tall — scanning it meant scrolling past what you came for. Cards are now a compact row (icon, name, two-line description) grouped by category, with the featured three using the same row tinted rather than a second card design: 4352px, and the shape of the catalogue is visible at a glance. Each card carries Docs and Changelog buttons. Which integrations have a changelog comes from a new build-time plugin that indexes src/pages/changelog/integrations/*.md and joins integrations.json on the link slug (vercel-ai-sdk -> /sdks/integrations/ai-sdk -> ai-sdk.md), so an integration grows the button on its first release with nothing to update by hand. The card is a <div> now — nested anchors are invalid — so the name and the pills are the links, not the whole card. /changelog's first line points here instead of carrying its own listing. Also drops the 14 CSS classes the old card owned, and shares CATEGORY_LABELS / groupByCategory from src/lib/integrations.ts so the taxonomy has one definition. Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN
148 lines
6.3 KiB
JavaScript
148 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Integrations single-source-of-truth guardrails.
|
|
*
|
|
* src/data/integrations.json is the single source: it drives the /integrations
|
|
* gallery and (via the swizzled DocPage/Layout/Sidebar component) the
|
|
* Integrations sidebar category on every docs version. This script enforces the
|
|
* two invariants that keep it honest:
|
|
*
|
|
* 1. Forward — every entry with an internal `/sdks/integrations/<slug>` link
|
|
* has a doc page at docs-integrations/<slug>.{md,mdx}. (The sidebar is
|
|
* injected at render time, so it isn't covered by Docusaurus' build-time
|
|
* broken-link check — this is what catches a missing page.)
|
|
*
|
|
* 3. Discoverable — every integration with a changelog page links to it from
|
|
* its doc page. The changelog pages are written by the release script and
|
|
* were otherwise reachable only by guessing the URL.
|
|
*
|
|
* 2. Reverse — every *released* integration (a published git tag
|
|
* `integrations/<name>/vX.Y.Z`) appears in integrations.json, so a release
|
|
* can't silently skip the gallery/sidebar. Degrades to a skip when tags
|
|
* aren't available (shallow checkout); CI fetches tags (fetch-depth: 0).
|
|
*
|
|
* Run: node scripts/check-integrations.mjs
|
|
*/
|
|
|
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const docsDir = join(__dirname, '..');
|
|
const integrationsJson = join(docsDir, 'src', 'data', 'integrations.json');
|
|
const integrationsDocsDir = join(docsDir, 'docs-integrations');
|
|
const changelogDir = join(docsDir, 'src', 'pages', 'changelog', 'integrations');
|
|
|
|
// Released integrations that intentionally have no gallery/doc page.
|
|
// cloudflare-oauth-proxy is internal infrastructure (an OAuth proxy Worker,
|
|
// `"private": true`), not a user-facing framework integration.
|
|
// coding-agents is released but deliberately unlisted for now: the package ships
|
|
// so it can be installed and tested end to end, while its page stays out of the
|
|
// gallery and sidebar until it is announced. Remove from this set (and restore
|
|
// its integrations.json entry + drop `unlisted` from the doc page) to publish it.
|
|
const EXCLUDED = new Set(['cloudflare-oauth-proxy']);
|
|
|
|
const { integrations } = JSON.parse(readFileSync(integrationsJson, 'utf8'));
|
|
const internal = integrations.filter((entry) => entry.link.startsWith('/sdks/integrations/'));
|
|
|
|
let failed = false;
|
|
|
|
// ─── 1. Forward: every internal entry has a doc page ──────────────────────────
|
|
const missingPages = [];
|
|
for (const entry of internal) {
|
|
const slug = entry.link.replace('/sdks/integrations/', '');
|
|
const hasDoc = ['md', 'mdx'].some((ext) => existsSync(join(integrationsDocsDir, `${slug}.${ext}`)));
|
|
if (!hasDoc) {
|
|
missingPages.push({ id: entry.id, slug });
|
|
}
|
|
}
|
|
if (missingPages.length > 0) {
|
|
failed = true;
|
|
console.error('[integrations] ❌ integrations.json entries with no doc page:\n');
|
|
for (const { id, slug } of missingPages) {
|
|
console.error(` ${id} — expected docs-integrations/${slug}.{md,mdx}`);
|
|
}
|
|
console.error('\nAdd the doc page, or remove or externalize the entry in integrations.json.\n');
|
|
} else {
|
|
console.log(`[integrations] ✅ All ${internal.length} integration entries have a doc page.`);
|
|
}
|
|
|
|
// ─── 2. Reverse: every released integration is in integrations.json ───────────
|
|
const documented = new Set(internal.map((entry) => entry.link.replace('/sdks/integrations/', '')));
|
|
|
|
function releasedIntegrations() {
|
|
let raw;
|
|
try {
|
|
raw = execFileSync('git', ['tag', '-l', 'integrations/*'], { encoding: 'utf8' });
|
|
} catch {
|
|
return null; // git unavailable
|
|
}
|
|
const names = new Set();
|
|
for (const tag of raw.split('\n')) {
|
|
const m = tag.match(/^integrations\/(.+)\/v\d/);
|
|
if (m) names.add(m[1]);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
const released = releasedIntegrations();
|
|
if (released === null || released.size === 0) {
|
|
console.warn(
|
|
'[integrations] ⚠️ No integration tags found (shallow checkout?). ' +
|
|
'Skipping reverse check — fetch tags (fetch-depth: 0) to enforce in CI.',
|
|
);
|
|
} else {
|
|
const missingEntries = [...released]
|
|
.filter((name) => !EXCLUDED.has(name) && !documented.has(name))
|
|
.sort();
|
|
if (missingEntries.length > 0) {
|
|
failed = true;
|
|
console.error('[integrations] ❌ Released integrations missing from integrations.json:\n');
|
|
for (const name of missingEntries) {
|
|
console.error(` ${name} — released as integrations/${name}/vX.Y.Z but no entry in integrations.json`);
|
|
}
|
|
console.error(
|
|
'\nAdd each to integrations.json (with an internal `link`), or — if not user-facing — ' +
|
|
'add it to the EXCLUDED set in this script.\n',
|
|
);
|
|
} else {
|
|
console.log(`[integrations] ✅ All ${released.size - EXCLUDED.size} released integrations are present in integrations.json.`);
|
|
}
|
|
}
|
|
|
|
// ─── 3. Discoverable: a changelog page is linked from its doc page ────────────
|
|
const changelogSlugs = readdirSync(changelogDir)
|
|
.filter((f) => f.endsWith('.md') && f !== 'index.md')
|
|
.map((f) => f.slice(0, -'.md'.length));
|
|
|
|
const unlinked = [];
|
|
let checkedChangelogs = 0;
|
|
for (const slug of changelogSlugs) {
|
|
if (!documented.has(slug)) continue; // no gallery entry ⇒ not user-facing (see EXCLUDED)
|
|
checkedChangelogs++;
|
|
const docPath = ['md', 'mdx']
|
|
.map((ext) => join(integrationsDocsDir, `${slug}.${ext}`))
|
|
.find((p) => existsSync(p));
|
|
if (!docPath) continue; // already reported by check 1
|
|
if (!readFileSync(docPath, 'utf8').includes(`/changelog/integrations/${slug}`)) {
|
|
unlinked.push(slug);
|
|
}
|
|
}
|
|
if (unlinked.length > 0) {
|
|
failed = true;
|
|
console.error('[integrations] ❌ Changelog pages not linked from their doc page:\n');
|
|
for (const slug of unlinked) {
|
|
console.error(` ${slug} — add [View Changelog →](/changelog/integrations/${slug})`);
|
|
}
|
|
console.error(
|
|
'\nThe /changelog listing finds these pages automatically, but the doc page is where ' +
|
|
'someone using the integration looks first.\n',
|
|
);
|
|
} else {
|
|
console.log(`[integrations] ✅ All ${checkedChangelogs} changelog pages are linked from their doc page.`);
|
|
}
|
|
|
|
process.exit(failed ? 1 : 0);
|