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
4 changes: 3 additions & 1 deletion .github/workflows/doc-snippet-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ jobs:
# REPORT-ONLY (objectui#7864). Code a generator EMITS from a template
# literal under `packages/*/src/**` is compiled by nothing: `tsc` sees a
# string, `tsup` copies it through, and this gate's own scan surface stops
# at `content/docs`, the per-app docs trees and the package READMEs. This
# at the authored pages — `content/docs`, the per-app docs trees, the
# package READMEs, the root `README.md` (objectui#7115) and the top level
# of the root `docs/` tree (objectui#7856 card 1). This
# step censuses that class through the same `compileSnippets()` the doc
# blocks go through, against the closure the step above just built.
#
Expand Down
33 changes: 28 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,19 @@ Third-Party App

```tsx
import { AppShell } from '@object-ui/app-shell';
import type { AppShellProps } from '@object-ui/app-shell';
import { ObjectView } from '@object-ui/plugin-view';
import { DataSourceProvider, useDataSource } from '@object-ui/providers';
import type { DataSourceProviderProps } from '@object-ui/providers';

// The two things you bring. Both are typed from the surface these packages
// ship, so this snippet compiles on its own: `myAPI` is your backend adapter
// (the "Custom Data Source Interface" section below is the shape it needs at
// runtime — `DataSourceProvider` declares the prop `any`, so the annotation
// records where the value goes rather than checking it), and `MySidebar` is
// your own component, returning whatever `AppShell` accepts for `sidebar`.
declare const myAPI: DataSourceProviderProps['dataSource'];
declare function MySidebar(): AppShellProps['sidebar'];

function MyConsole() {
return (
Expand All @@ -183,24 +194,29 @@ function ContactList() {

### Example 2: Next.js Integration

`app/layout.tsx`:

```tsx
// app/layout.tsx
import { AppShell } from '@object-ui/app-shell';
import type { AppShellProps } from '@object-ui/app-shell';
import { ThemeProvider } from '@object-ui/providers';

export default function RootLayout({ children }) {
export default function RootLayout({ children }: { children: AppShellProps['children'] }) {
return (
<ThemeProvider>
<AppShell>{children}</AppShell>
</ThemeProvider>
);
}
```

// app/[object]/page.tsx
`app/[object]/page.tsx`:

```tsx
import { ObjectView } from '@object-ui/plugin-view';
import { useDataSource } from '@object-ui/providers';

export default function Page({ params }) {
export default function Page({ params }: { params: { object: string } }) {
const dataSource = useDataSource();
return <ObjectView schema={{ type: 'object-view', objectName: params.object }} dataSource={dataSource} />;
}
Expand All @@ -211,6 +227,11 @@ export default function Page({ params }) {
```tsx
import { ObjectView } from '@object-ui/plugin-view';
import { DataSourceProvider, useDataSource } from '@object-ui/providers';
import type { DataSourceProviderProps } from '@object-ui/providers';

// Your backend adapter, as in Example 1 — declared here because every block on
// this page compiles on its own.
declare const myAPI: DataSourceProviderProps['dataSource'];

function MyExistingApp() {
return (
Expand Down Expand Up @@ -252,7 +273,9 @@ Example implementation:

```tsx
const myDataSource = {
async find(objectName, params) {
// The parameter types are the ones the interface above declares; spelling
// them out is what lets this block be compiled rather than read.
async find(objectName: string, params?: any) {
return fetch(`/api/${objectName}`, {
method: 'POST',
body: JSON.stringify(params),
Expand Down
59 changes: 57 additions & 2 deletions scripts/__tests__/check-doc-fence-languages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import {
APP_DOCS as SNIPPET_APP_DOCS,
listDocuments as snippetDocuments,
ROOT_DOCS as SNIPPET_ROOT_DOCS,
rootDocsPages as snippetRootDocsPages,
ROOT_PAGES as SNIPPET_ROOT_PAGES,
TS_FENCE_LANGUAGES as GATE_TS_FENCES,
} from '../check-doc-snippet-types.mjs';
Expand Down Expand Up @@ -114,8 +116,61 @@ const MIN_HEADER_PROSE = 400;
* place for those numbers.
*/
describe('check-doc-fence-languages: the scan surface is check-doc-snippet-types’s', () => {
it('walks exactly the documents the snippet gate walks', () => {
expect(fenceDocuments(ROOT)).toEqual(snippetDocuments(ROOT));
/**
* objectui#7856 card 1 — the ONE place the two walks are allowed to differ,
* named rather than tolerated.
*
* That card brought the repository-root `docs/` tree, TOP LEVEL only, into
* `check-doc-snippet-types`' walk: an authored-documentation directory that no
* doc gate read, where three phantom-teaching sites (objectui#7838,
* objectui#7854) had already been found by hand. It moved THAT gate's
* population and no other, for a stated reason: the rest of the tree —
* `docs/adr/**`, a GOVERNED surface, and `docs/audits/**` — is card 2, whose
* pull request stops in draft for a human to merge, and `check:doc-fences`'
* own surface was not that card's to move.
*
* So the equality below subtracts exactly what the snippet gate exports as its
* leg, `rootDocsPages()`, rather than a hand-written list of today's two
* filenames: a page added to `docs/` tomorrow travels into BOTH sides of this
* comparison by itself, and a page added under `docs/adr/` travels into
* NEITHER. A hand-written list would have to be re-typed for the first case and
* would stay silently green for the second.
*
* ⛔ What this is not: a licence for the two walks to drift anywhere else. Any
* OTHER divergence still fails here, which is the whole point of keeping the
* comparison rather than deleting it.
*/
it('walks exactly the documents the snippet gate walks, minus that gate’s docs/*.md leg', () => {
const legOnly = new Set(snippetRootDocsPages(ROOT));
expect(fenceDocuments(ROOT)).toEqual(snippetDocuments(ROOT).filter((d: string) => !legOnly.has(d)));
});

it('…and that subtraction is non-empty, so it is not silently subtracting nothing', () => {
const leg = snippetRootDocsPages(ROOT);
expect(leg.length).toBeGreaterThan(0);
// Every subtracted document really is on the snippet gate's side only.
for (const doc of leg) {
expect(snippetDocuments(ROOT), `${doc} is not in the snippet gate's walk`).toContain(doc);
expect(fenceDocuments(ROOT), `${doc} reached the fence guard's walk`).not.toContain(doc);
}
});

/**
* The card-2 boundary, pinned on the leg itself. `recursive: false` is a claim
* about where this surface stops, and a claim about a walk is only worth what
* a test that reads the tree says about it.
*/
it('the docs/*.md leg stops at the top level — the governed subtrees stay out of both walks', () => {
expect(SNIPPET_ROOT_DOCS).toEqual({ dir: 'docs', recursive: false });
const nested = (docs: string[]) =>
docs.filter((d) => d.startsWith(`${SNIPPET_ROOT_DOCS.dir}/`) && d.slice(`${SNIPPET_ROOT_DOCS.dir}/`.length).includes('/'));
expect(nested(snippetDocuments(ROOT))).toEqual([]);
expect(nested(fenceDocuments(ROOT))).toEqual([]);
// Non-vacuous: the subdirectories this asserts are absent do hold pages.
expect(
fs.existsSync(path.join(ROOT, SNIPPET_ROOT_DOCS.dir, 'adr')),
'docs/adr no longer exists, so the exclusion above pins nothing',
).toBe(true);
});

it('…and that is a non-empty set, so the comparison is not vacuous', () => {
Expand Down
83 changes: 83 additions & 0 deletions scripts/__tests__/check-doc-snippet-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import {
moduleSpecifiersOf,
moduleSpecifiersOfBlock,
resolvesOnlyThroughRootManifest,
ROOT_DOCS,
rootDeclaredSpecifiers,
rootDocsPages,
scanFences,
scopedBuildNotice,
specifierRoot,
Expand Down Expand Up @@ -622,6 +624,87 @@ describe('objectui#7115 — the root README is in the scan set', () => {
});
});

/**
* objectui#7856 card 1 — the repository-root `docs/` tree was in NO doc gate's
* scan set, and `lint:root` ignores it by name (`--ignore-pattern 'docs/**'`).
* The card measured what that bought: three phantom-teaching sites found in it by
* hand (objectui#7838, objectui#7854) and 11 diagnostics under `docs/*.md` that
* nothing reported.
*
* The rule this file's sibling states — "Widening a scan surface is the change
* that can be GREEN ABOUT NOTHING… Anything added here later is owed the same
* proof" — is why membership is pinned by name below, and why the leg's
* BOUNDARY is pinned too. `recursive: false` is not a performance note: the
* subtree it excludes is `docs/adr/**`, a governed surface whose pull requests
* stop in draft for a human, plus `docs/audits/**`, and both are card 2. A leg
* that grew into them by accident would put a governed-surface failure in front
* of a pull request that cannot land it.
*/
describe('objectui#7856 — the root docs/*.md pages are in the scan set, and only those', () => {
it('listDocuments reaches them', () => {
const documents = listDocuments(repoRoot);
for (const doc of rootDocsPages(repoRoot)) expect(documents).toContain(doc);
// Non-vacuous: the leg reaches a real page, not an empty directory.
expect(rootDocsPages(repoRoot)).toContain('docs/ARCHITECTURE.md');
});

it('the widening judges something — the leg contributes blocks to the compiled tier', () => {
// Being IN the walk is one fact; being compiled is the other, and this card
// delivered both (no `UNGATED_DOCS` entry was needed — the blocks were
// repaired). A leg whose pages all sat on the ledger would be visible to the
// accounting and judged by nothing, which is a weaker claim than this test
// makes.
const state = analyze({});
const leg = new Set(rootDocsPages(repoRoot));
expect((state.covered as string[]).filter((d) => leg.has(d)).sort()).toEqual([...leg].sort());
expect((state.compiled as { doc: string }[]).some((b) => leg.has(b.doc))).toBe(true);
});

it('stops at the top level: a page in a subdirectory is NOT collected', () => {
const root = tempTree({
'docs/PAGE.md': '# top level\n',
'docs/adr/0001-decision.md': '# governed, card 2\n',
'docs/audits/2026-07-audit.md': '# card 2\n',
});
try {
expect(rootDocsPages(root)).toEqual(['docs/PAGE.md']);
expect(listDocuments(root)).toEqual(['docs/PAGE.md']);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

it('collects files only, and only page extensions', () => {
const root = tempTree({
'docs/b.mdx': '# b\n',
'docs/a.md': '# a\n',
'docs/notes.txt': 'not a page\n',
'docs/screenshots/shot.png': 'not a page\n',
});
try {
expect(rootDocsPages(root)).toEqual(['docs/a.md', 'docs/b.mdx']);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

it('an absent docs/ tree yields nothing here, and a verdict is refused in main', () => {
const root = tempTree({ 'README.md': '# root\n' });
try {
// A throwaway fixture tree stays listable…
expect(rootDocsPages(root)).toEqual([]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
// …while a REAL run refuses, the same way a dangling ROOT_PAGES name does.
// `main()` takes no `--root`, so this is pinned against the source for the
// same reason the ROOT_PAGES guard above is.
const source = fs.readFileSync(path.join(repoRoot, 'scripts/check-doc-snippet-types.mjs'), 'utf8');
expect(source).toMatch(/if \(!existsSync\(join\(repoRoot, ROOT_DOCS\.dir\)\)\) \{/);
expect(ROOT_DOCS).toEqual({ dir: 'docs', recursive: false });
});
});

describe('third-party resolution reaches exactly as far as the imported packages declare', () => {
/** A workspace package with its own `node_modules`, the way pnpm links one. */
function treeWithDependency(files: Record<string, string> = {}): string {
Expand Down
Loading
Loading