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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
.pnpm-store/
# test-fixture packages are TEST DATA, committed on purpose
!tests/diagnostics/npm-imports/node_modules/
!tests/fixtures/commander-calc/node_modules/
Expand Down Expand Up @@ -40,3 +41,4 @@ tmp-*/
.vercel/
.env.local
.env*
.env*.local
153 changes: 145 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Agent Guide

Guidance for agents (and humans) working on this repository. These conventions apply repo-wide; the docs site under `docs/` additionally has its own conventions in `docs/AGENTS.md`.
This is the only agent guide for the repository. Its rules apply everywhere, including `docs/` and `internal/compatibility/`.

## Markdown style

Write prose paragraphs and list items as single source lines and let editors and renderers wrap them naturally. Do not hard-wrap Markdown at a fixed column; preserve separate lines only where Markdown syntax or intentional formatting requires them, such as headings, lists, tables, blockquotes, and code blocks.

## Build and test

Expand Down Expand Up @@ -41,9 +45,7 @@ SCRIPTC_TEST_WORKERS=4 pnpm test # plain lane
SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
```

`SCRIPTC_TEST_WORKERS` caps the vitest worker pool so concurrent agents don't
contend for cores; full local suites also queue behind an advisory lock per
lane.
`SCRIPTC_TEST_WORKERS` caps the vitest worker pool so concurrent agents don't contend for cores; full local suites also queue behind an advisory lock per lane.

Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.

Expand All @@ -59,12 +61,147 @@ new tests should follow this convention.

## Where things live

- `packages/compiler` — the frontend (tsc API to IR), the typed IR with validator and serializer, and the LLVM and C backends.
- `packages/compiler` — the frontend (tsc API to IR), typed IR, validator, serializer, and LLVM and C backends.
- `packages/runtime` — the C runtime compiled into every scriptc binary.
- `packages/cli` — `scriptc build | run | coverage`.
- `tests/` — the differential corpus, diagnostics snapshots, and the harness.
- `docs/` — the documentation site (standalone pnpm workspace); see `docs/AGENTS.md`.
- `scripts/` — repo tooling, including the release version stamp.
- `internal/compatibility` — generated Node.js parity inventory, implementation-owned compatibility manifests, and engineering backlog.
- `tests/` — the differential corpus, diagnostics snapshots, and harness.
- `docs/` — the standalone Next.js App Router + MDX documentation workspace.
- `scripts/` — repository tooling, including the release version stamp.

## Node.js compatibility

The Node.js compatibility system is an implementation inventory and planning tool. Keep its claims narrower than the evidence. Do not change a status merely to make the public matrix look correct.

### Sources of truth

- `internal/compatibility/node-v24.json` pins the Node release, commit, official API inputs, and documentation URLs. The pinned Node API is the denominator.
- Node's official documentation defines the public API hierarchy and stability metadata. Consult Node source when the docs do not fully specify behavior.
- Running the pinned Node release is the behavior oracle for differential tests.
- Compiler source and `packages/compiler/surface-manifest.json`, augmented by `internal/compatibility/static-support.json`, define static-native support.
- `internal/compatibility/dynamic-support.json` defines dynamic-island modules, exports, members, explicit stubs, globals, and policies. It also generates the runtime builtin registry.
- Test evidence determines positive behavioral claims. Source manifests alone do not prove Node-compatible behavior.

Treat static-native and dynamic-island compatibility independently. Support in one tier says nothing about the other, and `--dynamic` does not turn an unsupported typed call into a supported static call.

### Status contract

- `supported` — broadly implemented. This requires test evidence.
- `partial` — a useful implemented subset. This requires test evidence, but evidence may cover an API family rather than every documented overload.
- `refused` — a named compiler diagnostic or explicit dynamic throwing or rejecting stub exists. Never infer this status from absence.
- `not-implemented` — no implementation claim. Its verification basis decides whether it is ready to implement or must first be investigated.
- `by-design` — intentionally outside a scriptc execution model. This requires an explicit implementation-owned architectural policy.
- `unreviewed` — no compatibility subsystem owns a reliable classification. Do not treat this as unsupported or as an implementation ticket.
- `not-applicable` — documentation, configuration, executable behavior, or an embedding model that does not map to the execution tier.

Chapter rows are generated summaries of their descendants, never independent support claims.

### Verification basis and backlog actions

Read a row's verification basis before acting:

- `test-backed` — mapped to implementation and relevant tests. Audit the exact overload or behavior before broadening a `partial` claim.
- `explicit-refusal` — source-verified refusal boundary.
- `verified-absence` — a runtime presence test confirms absence.
- `declared-gap` — an implementation-owned manifest explicitly declares the gap; it is ready for implementation.
- `registry-gap` — no implementation registry entry matched. Verify the gap before implementing: it may be an incomplete manifest or symbol match.
- `architectural-policy` — explicit intentional exclusion.
- `unreviewed`, `not-applicable`, and `derived` retain the meanings above.

`pnpm node-compat:backlog` emits tier-specific work. Its actions are:

- `verify-gap` — investigate a registry gap first; this is not yet an implementation ticket.
- `implement` — implement a declared or verified gap.
- `replace-refusal` — add support where an explicit refusal currently exists.
- `audit-partial` — identify and implement missing behavior in a tested subset, or tighten an overbroad mapping.
- `classify` — establish ownership and evidence for an unreviewed row.

Backlog priority is a heuristic based on Node stability: stable APIs are raised, release-candidate or unrated APIs remain normal, and experimental, deprecated, and legacy APIs are lowered. It is not a product roadmap.

Rows are not necessarily independent work items. One implementation can close a class, methods, overloads, aliases, and nested rows. Plan work by coherent API family and inspect the regenerated diff instead of creating one ticket per row.

Useful queue filters compose:

```bash
pnpm node-compat:backlog --action=implement --priority=high
pnpm node-compat:backlog --action=verify-gap --chapter=assert
pnpm node-compat:backlog --tier=dynamic --format=tsv
```

### Generated files

Never hand-edit these files:

- `packages/compiler/surface-manifest.json`
- `internal/compatibility/generated/node-v24-internal.json`
- `internal/compatibility/generated/node-v24-backlog.json`
- `docs/src/generated/node-v24-compatibility.json`
- `docs/src/generated/node-v24-compatibility-meta.json`
- `packages/runtime/src/scr_island_manifest.h`

Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, or dynamic registry changes.

The public docs artifact must not expose repository test paths, internal source identifiers, engineering evidence bookkeeping, documentation/metadata headings, command-line or configuration-only entries, or rows that are N/A in both tiers. Those remain in the complete internal census under `internal/compatibility/generated/`.

### Landing compatibility work

For each API family:

1. Start from the backlog action and verification basis. Verify every `registry-gap` before assuming code is missing.
2. Read the pinned Node documentation, stability metadata, relevant Node source when needed, and existing scriptc implementation paths.
3. Decide static-native and dynamic-island scope separately.
4. Implement the smallest coherent behavior family. Preserve explicit refusals for unimplemented forms rather than silently diverging.
5. Add differential evidence that executes under Node and as a compiled binary. Cover stdout, stderr, exit status, success behavior, and important error shapes. Dynamic-island work needs island-executed differential fixtures; a load-only export check does not prove behavior.
6. Update the implementation-owned source or manifest. Positive `supported` and `partial` mappings must name existing test evidence.
7. Run `pnpm manifest` if compiler surface tables changed, then run `pnpm node-compat`. Never patch generated JSON or headers.
8. Inspect the internal ledger, public artifact, and backlog diff. Confirm the intended API family changed and unrelated rows did not.
9. Run focused tests, `pnpm node-compat:check`, and the full validation gate required for the change. If docs output changed, also run the docs gate.

Compatibility commands:

```bash
pnpm node-compat
pnpm node-compat:check
pnpm --filter @internal/compatibility check:upstream # networked pinned-input check
```

## Documentation site

The docs site is a standalone pnpm workspace under `docs/`. It uses Next.js App Router + MDX, with one topic per `docs/src/app/<topic>/page.mdx`.

### Naming and content

- The project is **scriptc**, lowercase, everywhere: page titles, prose, and code samples.
- Name-bearing strings such as the site name, tagline, GitHub URL, and canonical origin `https://scriptc.dev` live in `docs/src/lib/site.ts`.
- Coverage numbers on a page may only be output from `scriptc coverage` for a specific program shown in the same block; never publish invented aggregate coverage statistics.
- Every shell command shown in a fence must have run successfully against the current build. Output blocks contain real output; trimming is acceptable, invention is not.
- Document limitations plainly on the limitations page rather than scattering them as fine print.

### MDX conventions

- Use literal HTML `<table>` markup in MDX, never Markdown pipe tables.
- Fence info strings contain the language and optionally a filename after a colon, for example `ts:src/main.ts`; no other fence metadata survives.
- Use `console` fences for shell sessions (`$ command` followed by output).
- Use `diff` fences for additions and removals.
- Use HTML `<dl>/<dt>/<dd>` for flag and subcommand references.

### Adding a docs page

1. Add `docs/src/app/<topic>/page.mdx`.
2. Add `docs/src/app/<topic>/layout.tsx` exporting `pageMetadata("<topic>")`.
3. Add the slug to `PAGE_TITLES` in `docs/src/lib/page-titles.ts`.
4. Add navigation in `docs/src/lib/docs-navigation.ts`; it also drives mobile navigation and the sitemap.

### Docs development and verification

Do not start duplicate docs dev servers. Reuse the existing server and its hot reload process when one is running. CI-style builds must use a separate dist directory so they do not corrupt the dev server's `.next` state:

```bash
cd docs
NEXT_DIST_DIR=.next-check pnpm check
```

`pnpm check` runs the compatibility drift check, TypeScript check, and production build. It is required before landing docs or compatibility output changes.

## Releases

Expand Down
52 changes: 0 additions & 52 deletions docs/AGENTS.md

This file was deleted.

4 changes: 0 additions & 4 deletions docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ const nextConfig = {
// CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next
// with a running dev server (a shared dist dir corrupts the dev cache).
distDir: process.env.NEXT_DIST_DIR || ".next",
// Keep check builds from retriggering a running dev server's watcher.
watchOptions: {
ignored: ["**/.next-check/**"],
},
};

export default withMDX(nextConfig);
3 changes: 2 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"check": "pnpm typecheck && pnpm build"
"compat:check": "pnpm --dir .. node-compat:check",
"check": "pnpm compat:check && pnpm typecheck && pnpm build"
},
"dependencies": {
"@mdx-js/loader": "^3",
Expand Down
14 changes: 14 additions & 0 deletions docs/src/app/compatibility/data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import snapshot from "@/generated/node-v24-compatibility.json";

export const dynamic = "force-static";
export const revalidate = false;

export function GET() {
return Response.json(snapshot, {
headers: {
// The client adds a generated content hash to the URL. Keep dev
// uncached; production can cache each immutable artifact URL.
"Cache-Control": process.env.NODE_ENV === "production" ? "public, max-age=31536000, immutable" : "no-store",
},
});
}
7 changes: 7 additions & 0 deletions docs/src/app/compatibility/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";

export const metadata = pageMetadata("compatibility");

export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
5 changes: 5 additions & 0 deletions docs/src/app/compatibility/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NodeCompatibilityMatrix } from "@/components/node-compatibility-matrix";

export default function CompatibilityPage() {
return <NodeCompatibilityMatrix />;
}
23 changes: 23 additions & 0 deletions docs/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,29 @@ article td {
color: var(--ds-gray-900);
}

.compatibility-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}

.compatibility-table th {
border-bottom: 1px solid var(--ds-gray-alpha-400);
padding: 0.5rem 0.75rem;
text-align: left;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ds-gray-900);
}

.compatibility-table td {
border-bottom: 1px solid var(--ds-gray-alpha-200);
padding: 0.5rem 0.75rem;
color: var(--ds-gray-900);
}

/* Definition lists (flag/subcommand references on the CLI page and
friends). Literal HTML in MDX bypasses the mdx-components mapping, so
these live next to the article table rules above. */
Expand Down
3 changes: 3 additions & 0 deletions docs/src/app/node-compatibility/data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function GET(request: Request) {
return Response.redirect(new URL("/compatibility/data", request.url), 308);
}
5 changes: 5 additions & 0 deletions docs/src/app/node-compatibility/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { permanentRedirect } from "next/navigation";

export default function LegacyNodeCompatibilityPage() {
permanentRedirect("/compatibility");
}
6 changes: 3 additions & 3 deletions docs/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import { statSync } from "node:fs";
import path from "node:path";

export default function sitemap(): MetadataRoute.Sitemap {
// The homepage lives in the site header, not the docs nav, so list it explicitly.
const hrefs = ["/", ...allDocsPages.map((page) => page.href)];
// Top-level site pages live in the header, not the docs nav.
const hrefs = ["/", "/compatibility", ...allDocsPages.map((page) => page.href)];
return hrefs.map((href) => ({
url: `${siteUrl}${href}`,
lastModified: lastModifiedFor(href),
}));
}

function lastModifiedFor(href: string): Date {
const relative = href === "/" ? "page.tsx" : path.join(href.slice(1), "page.mdx");
const relative = href === "/" || href === "/compatibility" ? path.join(href.slice(1), "page.tsx") : path.join(href.slice(1), "page.mdx");
try {
return statSync(path.join(process.cwd(), "src", "app", relative)).mtime;
} catch {
Expand Down
4 changes: 2 additions & 2 deletions docs/src/components/docs-mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export function DocsMobileNav() {
return allDocsPages.find((page) => page.href === pathname) ?? allDocsPages[0];
}, [pathname]);

// The homepage is a full-width landing page without the docs chrome.
if (pathname === "/") {
// Top-level site pages are outside the documentation chrome.
if (pathname === "/" || pathname === "/compatibility") {
return null;
}

Expand Down
5 changes: 4 additions & 1 deletion docs/src/components/docs-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ function Sidebar() {
export function DocsNav({ children }: { children: React.ReactNode }) {
const pathname = usePathname();

// The homepage is a full-width landing page without the docs sidebar.
// Top-level site pages bypass the documentation sidebar and article shell.
if (pathname === "/") {
return <main>{children}</main>;
}
if (pathname === "/compatibility") {
return <main className="h-[calc(100dvh-4rem)] overflow-hidden">{children}</main>;
}

return (
<div className="mx-auto max-w-5xl px-6 py-8 lg:py-12 xl:max-w-7xl">
Expand Down
5 changes: 3 additions & 2 deletions docs/src/components/header-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/s
const links = [
{ name: "Home", href: "/" },
{ name: "Docs", href: "/introduction" },
{ name: "Compatibility", href: "/compatibility" },
];

function isCurrent(href: string, pathname: string): boolean {
if (href === "/") return pathname === "/";
// Docs owns every other page.
return pathname !== "/";
if (href === "/compatibility") return pathname === "/compatibility";
return pathname !== "/" && pathname !== "/compatibility";
}

export function HeaderNav() {
Expand Down
Loading
Loading