Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`.
- `scripts/artifacts.js` — Safe tarball extraction + tree copy, shared by both fetch paths. Validates archive members (no traversal, no absolute paths, no symlinks) before anything is written, and replaces the old `cp -r`/`tar` shell-outs so the build runs on Windows
- `scripts/hoist-inline-scripts.js` — Moves inline `<script>` bodies in sub-app HTML into files before the Astro build, so the deployment can serve `script-src 'self'`. Needed because bundles published before the action stopped emitting an inline mermaid bootstrap still contain one. A sub-app's dark-mode bootstrap is deleted here rather than hoisted — light only, and hoisting would put it beyond the reach of `transform.js`
- `actions/publish-single-page-docs/` — Reusable GitHub Action that turns a repo's markdown into a single-page bundle
- `skills/kb-docs-add/` — Agent skill (Claude Code, GitHub Copilot, `npx skills add`) that walks an agent through onboarding a docs repo: classify, write only the contract-required files, verify, troubleshoot. Guidance only — no scripts; `examples/` are the contract's own code blocks and `tests/skill.spec.js` fails if they drift. Eval fixtures live in `tests/fixtures/kb-docs-add/`
- `skills/kb-docs-add/` — Agent skill (Claude Code, GitHub Copilot, `npx skills add`) that walks an agent through onboarding a docs repo (classify, write only the contract-required files, verify, troubleshoot) and through auditing one already onboarded (`references/audit.md`: run `actions/lib/check-cli.js` from a scratch clone, fix findings by rule ID at the source, report what the checker cannot see). Guidance only — no scripts; `examples/` are the contract's own code blocks and `tests/skill.spec.js` fails if they drift. Eval fixtures live in `tests/fixtures/kb-docs-add/`

### Onboarding Types

Expand Down Expand Up @@ -126,7 +126,7 @@ Known gap: inline `on*` handlers in sub-app HTML are not stripped (#67). They ar

`transform.js` parses the document with **parse5** and rewrites every URL-bearing attribute to an absolute `/{prefix}/{slug}/…` path: `href`/`src`/`action`/`formaction`/`poster`, `object[data]`, `srcset`/`imagesrcset`, `url()` in inline `style=` and `<style>` blocks, and URL-bearing `<meta>` content. `<base>` tags are removed. Because it walks a parsed tree, markup quoted inside prose or comments is left alone.

Root-relative `url()` inside a sub-app's **copied CSS files** is a separate rewrite, in `copyAssets()` (`scripts/build-vite.js`), targeting the same absolute path.
`url()` and `@import` inside a sub-app's **copied CSS files** are a separate rewrite, in `copyAssets()` (`scripts/build-vite.js`, via `rewriteCssUrls()`): root-relative ones target the same absolute path, relative ones are resolved against the stylesheet's own URL and made absolute too. That is not cosmetic — a pierced fragment's linked sheets are copied by reframed into constructed stylesheets, which resolve URLs against the host document, so a relative `url()` 404s there. `embedded-transitions.js` drops those copies at the first swap (reframed never does on the `moveBefore()` path, so the first app's CSS would otherwise follow the visitor everywhere).

## Contract for Doc Apps

Expand All @@ -135,6 +135,7 @@ Apps registered in `apps.json` must comply with:
- `contract/kb-docs.schema.json` — JSON Schema for `kb-docs.json`
- `contract/DEPLOYMENT.md` — What a private deployment repo owns, and the reusable workflow it calls
- `contract/HEADLESS_RULES.md` — Structural requirements (headless HTML, relative paths, `data-kb-headless` attribute)
- `contract/RULES.md` — Every checked rule by ID (`KB-<area>-<nnn>`), severity and fix. `actions/lib/rules.js` is the machine side; `actions/lib/check.selftest.js` fails if the two differ. The checker is `actions/lib/check.js` (run by `publish-docs`, or standalone via `actions/lib/check-cli.js`). New rules start as `warning`; promoting one to `error` is a major-version change of the actions
- `contract/STYLE_GUIDE.md` — Design tokens and typography (light only — the knowledge base has no dark mode)
- `contract/SINGLE_PAGE.md` — The copy-paste onboarding workflow for single-page docs

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ Apps must comply with the knowledge base contract before they can be registered:
| [`contract/kb-docs.schema.json`](contract/kb-docs.schema.json) | JSON Schema for `kb-docs.json` |
| [`contract/DEPLOYMENT.md`](contract/DEPLOYMENT.md) | Deployment repo layout, credentials, triggers, rollback |
| [`contract/HEADLESS_RULES.md`](contract/HEADLESS_RULES.md) | Headless HTML, relative paths, `data-kb-headless` |
| [`contract/RULES.md`](contract/RULES.md) | Every checked rule by ID (`KB-HTML-003` …), its severity and fix; how to run the checker |
| [`contract/STYLE_GUIDE.md`](contract/STYLE_GUIDE.md) | Design tokens (`--color-kb-*`) and typography — light only; the knowledge base has no dark mode |
| [`contract/SINGLE_PAGE.md`](contract/SINGLE_PAGE.md) | Zero-config markdown onboarding |

Expand Down
79 changes: 79 additions & 0 deletions actions/lib/check-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node
/**
* check-cli.js — runs the contract checks outside a release.
*
* The publish-docs action runs the same checks at release time; this is for
* everywhere else: a docs repo's pull-request CI, a local build, an agent
* fixing a repo. It reads nothing but the manifest and the built output, and
* never packs or uploads.
*
* node actions/lib/check-cli.js [--manifest kb-docs.json] [--dist dist] [--json] [--strict]
*
* Exit status: 1 when there is an error finding (or any finding under
* --strict), 0 otherwise. --json prints the findings as a JSON array, for a
* caller that wants to act on rule IDs rather than read prose.
*/

import { existsSync } from 'node:fs';
import { resolve } from 'node:path';

import { appDirResolver, checkApps } from './check.js';
import { PublishError, readManifestFile } from './manifest.js';
import { RULES_DOC, finding, formatFinding } from './rules.js';

function parseArgs(argv) {
const args = { manifest: 'kb-docs.json', dist: 'dist', json: false, strict: false };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--json') args.json = true;
else if (arg === '--strict') args.strict = true;
else if (arg === '--manifest' || arg === '--dist') args[arg.slice(2)] = argv[++i];
else if (arg === '--help' || arg === '-h') args.help = true;
else throw new Error(`Unknown argument "${arg}". Try --help.`);
}
return args;
}

function collect({ manifest: manifestPath, dist }) {
let manifest;
try {
manifest = readManifestFile(resolve(manifestPath));
} catch (err) {
if (!(err instanceof PublishError)) throw err;
return [finding('KB-MAN-001', manifestPath, err.message.replace(/^KB-MAN-001 /, ''))];
}
const distDir = resolve(dist);
if (!existsSync(distDir)) {
return [finding('KB-ART-001', dist, `the built output directory does not exist. Build the site first, or pass --dist.`)];
}
const appDirFor = appDirResolver(manifest, distDir);
const missing = manifest.apps.filter((app) => !existsSync(appDirFor(app.slug)));
if (missing.length > 0) {
return missing.map((app) => finding('KB-ART-001', app.slug,
`no built output at ${appDirFor(app.slug)} — with several apps, --dist holds one subdirectory per slug.`));
}
return checkApps(manifest, appDirFor);
}

function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log('Usage: check-cli.js [--manifest kb-docs.json] [--dist dist] [--json] [--strict]');
return 0;
}

const findings = collect(args);
const errors = findings.filter((f) => f.severity === 'error').length;

if (args.json) {
console.log(JSON.stringify(findings, null, 2));
} else if (findings.length === 0) {
console.log('No findings — the built output satisfies the knowledge base contract.');
} else {
for (const f of findings) console.log(`${f.severity.padEnd(7)} ${formatFinding(f)}`);
console.log(`\n${errors} error(s), ${findings.length - errors} warning(s). Each rule is explained in ${RULES_DOC}.`);
}
return errors > 0 || (args.strict && findings.length > 0) ? 1 : 0;
}

process.exitCode = main();
Loading
Loading