Skip to content

Commit 1f8f6ac

Browse files
ci(docs): guard skills/docs against bare metadata literals (#2035) (#2127)
Closes the drift root cause found while updating the docs (#2124): TypeScript code blocks in Markdown/MDX are not type-checked or ESLinted, so skills/ and content/docs/ silently drifted back to teaching the bare `: Page = {}` literal while the example apps — which the ESLint guard polices — stayed clean. Skills are the corpus AI authors from, so a stale sample there is the worst place for it. `scripts/check-doc-authoring.mjs` (dependency-free) scans every ```ts/typescript/ tsx fenced block under skills/ and content/ for an exported metadata literal annotated with one of the 16 factory domains (or its Input alias) instead of the `defineX(...)` factory, and fails with file:line + guidance. Generated files (references/, the frontmatter-generated skills.mdx) are skipped. Wired into lint.yml's ESLint job via `pnpm check:doc-authoring` (no build, no new dep). Verified: 160 files clean on current main (#2124 migration holds); teeth confirmed — flags a bare `: Page` block, ignores `definePage({...})`. CI-only — no package code, no changeset. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
1 parent 918e7e1 commit 1f8f6ac

3 files changed

Lines changed: 80 additions & 1 deletion

File tree

.github/workflows/lint.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ jobs:
5050
- name: ESLint
5151
run: pnpm lint
5252

53+
# Docs/skills authoring guard (#2035 / ADR-0059): TS code blocks in
54+
# Markdown/MDX are not type-checked or ESLinted, so skills/ and
55+
# content/docs/ can drift back to teaching the bare `: Page = {}` literal
56+
# while the examples (which ARE linted) stay clean. This fails on any bare
57+
# metadata literal for the 16 factory domains in a doc code block.
58+
- name: Doc/skill authoring guard
59+
run: pnpm check:doc-authoring
60+
5361
typecheck:
5462
name: TypeScript Type Check
5563
runs-on: ubuntu-latest

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"objectui:bump": "bash scripts/bump-objectui.sh",
2525
"objectui:refresh": "bash scripts/bump-objectui.sh && bash scripts/build-console.sh",
2626
"objectui:clean": "rm -rf packages/console/dist .cache/objectui-*",
27-
"lint": "eslint . --no-inline-config"
27+
"lint": "eslint . --no-inline-config",
28+
"check:doc-authoring": "node scripts/check-doc-authoring.mjs"
2829
},
2930
"keywords": [
3031
"objectstack",

scripts/check-doc-authoring.mjs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env node
2+
// check-doc-authoring.mjs — guard the docs/skills corpus against the bare
3+
// metadata-literal anti-pattern (#2035 / ADR-0059).
4+
//
5+
// The example apps are kept on the `defineX` factories by an ESLint rule, but
6+
// TypeScript code blocks inside Markdown/MDX are not type-checked or linted by
7+
// anything — which is exactly how skills/ and content/docs/ drifted back to
8+
// teaching `: Page = {}` while the examples stayed clean. Skills are the corpus
9+
// AI authors from, so a bad sample there is worse than one in app code.
10+
//
11+
// This scans ```ts|typescript|tsx fenced blocks for an exported metadata literal
12+
// annotated with one of the 16 factory domains (or its `Input` alias) instead of
13+
// being wrapped in the `defineX(...)` factory, and fails if it finds one.
14+
//
15+
// node scripts/check-doc-authoring.mjs
16+
import { readdirSync, readFileSync, statSync } from 'node:fs';
17+
import { join } from 'node:path';
18+
19+
const ROOTS = ['skills', 'content'];
20+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'references']);
21+
// Generated from spec/frontmatter — not hand-authored, don't police.
22+
const SKIP_FILES = new Set(['content/docs/guides/skills.mdx']);
23+
24+
const DOMAINS = [
25+
'Datasource', 'Connector', 'Policy', 'SharingRule', 'Role', 'PermissionSet',
26+
'EmailTemplateDefinition', 'Report', 'Webhook', 'ObjectExtension', 'Cube',
27+
'Mapping', 'Theme', 'TranslationBundle', 'Page', 'Action',
28+
].join('|');
29+
const NS = '(?:UI\\.|Data\\.|System\\.|Security\\.|Identity\\.|Automation\\.|Integration\\.)?';
30+
const BARE = new RegExp(`^export const \\w+:\\s*${NS}(?:${DOMAINS})(?:Input)?\\s*=\\s*\\{`);
31+
const FENCE_OPEN = /^```(?:ts|typescript|tsx)\s*$/;
32+
const FENCE_CLOSE = /^```\s*$/;
33+
34+
function walk(dir, out) {
35+
for (const e of readdirSync(dir)) {
36+
if (SKIP_DIRS.has(e)) continue;
37+
const p = join(dir, e);
38+
const s = statSync(p);
39+
if (s.isDirectory()) walk(p, out);
40+
else if (/\.mdx?$/.test(e) && !SKIP_FILES.has(p)) out.push(p);
41+
}
42+
}
43+
44+
const files = [];
45+
for (const r of ROOTS) { try { walk(r, files); } catch {} }
46+
47+
const violations = [];
48+
for (const file of files) {
49+
const lines = readFileSync(file, 'utf8').split('\n');
50+
let inBlock = false;
51+
for (let i = 0; i < lines.length; i++) {
52+
const ln = lines[i];
53+
if (!inBlock) { if (FENCE_OPEN.test(ln)) inBlock = true; continue; }
54+
if (FENCE_CLOSE.test(ln)) { inBlock = false; continue; }
55+
if (BARE.test(ln)) violations.push({ file, line: i + 1, text: ln.trim() });
56+
}
57+
}
58+
59+
if (violations.length === 0) {
60+
console.log(`✓ doc authoring guard: ${files.length} files clean — no bare metadata literals.`);
61+
process.exit(0);
62+
}
63+
64+
console.error(`\n✗ Bare metadata-literal authoring found in docs/skills (#2035). Use the defineX factory instead:\n`);
65+
for (const v of violations) {
66+
console.error(` ${v.file}:${v.line}`);
67+
console.error(` ${v.text}`);
68+
}
69+
console.error(`\n${violations.length} violation(s). Author via e.g. \`definePage({ ... })\` — a value import that fails loudly, validates at parse time, and is the one pattern AI should learn. See ADR-0059.\n`);
70+
process.exit(1);

0 commit comments

Comments
 (0)