diff --git a/docs/native-crates.md b/docs/native-crates.md new file mode 100644 index 000000000..63988058c --- /dev/null +++ b/docs/native-crates.md @@ -0,0 +1,48 @@ +# Native Crates + +Contributor-facing map of the Rust crates under `crates/`. These crates back +`@oh-my-pi/pi-natives` and the embedded shell/PTY runtime. They are intentionally +internal: end users see `@oh-my-pi/pi-natives` exports, not these crate APIs. + +For the consumer-side runtime contract see +[`natives-architecture.md`](./natives-architecture.md). For inclusion policy +covering when a crate should be promoted to user-facing docs, see +[`user-facing-packages.md`](./user-facing-packages.md). + +## Crate map + +| Crate | Path | Role | +| --- | --- | --- | +| `pi-natives` | [`crates/pi-natives`](../crates/pi-natives) | Top-level N-API `cdylib`; aggregates the other crates and exposes the JS-visible API. | +| `pi-shell` | [`crates/pi-shell`](../crates/pi-shell) | Embedded shell / PTY / process management split out of `pi-natives` (wraps `brush-*`). | +| `pi-ast` | [`crates/pi-ast`](../crates/pi-ast) | tree-sitter-based code summarizer and AST utilities; 50+ language grammars. | +| `pi-iso` | [`crates/pi-iso`](../crates/pi-iso) | Task isolation backend resolver: APFS clones, btrfs/zfs reflinks, overlayfs, projfs, rcopy. | +| `pi-walker` | [`crates/pi-walker`](../crates/pi-walker) | Parallel filesystem walker (ignore + globset) shared by grep, glob, and fs-scan cache. | +| `pi_uu_grep` | [`crates/pi-uu-grep`](../crates/pi-uu-grep) | `grep` re-implemented on `grep-regex` / `grep-searcher`; runs in-process as a shell builtin. Entry: `pi_uu_grep::run`. | +| `pi-uutils-ctx` | [`crates/pi-uutils-ctx`](../crates/pi-uutils-ctx) | Thread-local stdio + cwd context shim for embedding vendored uutils as in-process shell builtins. | +| `brush-core` | [`crates/vendor/brush-core`](../crates/vendor/brush-core) | Vendored fork of [brush-shell](https://github.com/reubeno/brush) for embedded bash execution. | +| `brush-builtins` | [`crates/vendor/brush-builtins`](../crates/vendor/brush-builtins) | Vendored bash builtins (`cd`, `echo`, `test`, `printf`, `read`, `export`, ...). | + +## What lives where + +- Native API surface and loader (`@oh-my-pi/pi-natives`): + [`natives-architecture.md`](./natives-architecture.md), + [`natives-addon-loader-runtime.md`](./natives-addon-loader-runtime.md), + [`natives-binding-contract.md`](./natives-binding-contract.md), + [`natives-build-release-debugging.md`](./natives-build-release-debugging.md), + [`natives-media-system-utils.md`](./natives-media-system-utils.md), + [`natives-rust-task-cancellation.md`](./natives-rust-task-cancellation.md), + [`natives-shell-pty-process.md`](./natives-shell-pty-process.md), + [`natives-text-search-pipeline.md`](./natives-text-search-pipeline.md). +- Porting cross-references: + [`porting-from-pi-mono.md`](./porting-from-pi-mono.md), + [`porting-to-natives.md`](./porting-to-natives.md). +- Filesystem scan cache contract that consumes `pi-walker`: + [`fs-scan-cache-architecture.md`](./fs-scan-cache-architecture.md). + +## Policy + +These crates are implementation details. End-user docs live with the consuming +package (`@oh-my-pi/pi-natives`) and the architecture pages above. Promote a +crate to a dedicated user-facing doc only when it grows a standalone CLI or +public API consumed outside `packages/natives`. diff --git a/docs/natives-architecture.md b/docs/natives-architecture.md index eb4e851b6..bb124c0a7 100644 --- a/docs/natives-architecture.md +++ b/docs/natives-architecture.md @@ -150,7 +150,7 @@ N-API exports are generated from Rust `#[napi]` functions/classes/objects/enums. - user-facing policy and fallbacks that are not built into the native API - higher-level rendering, artifact, shell-session, and command behavior -For the root-docs inclusion policy that keeps internal Rust crates under native architecture docs unless promoted as user-facing, see [`user-facing-packages.md`](./user-facing-packages.md). +For the contributor-facing crate map covering `pi-natives`, `pi-shell`, `pi-ast`, `pi-iso`, `pi-walker`, `pi_uu_grep`, `pi-uutils-ctx`, and the vendored `brush-*` crates, see [`native-crates.md`](./native-crates.md). The root-docs inclusion policy that keeps internal Rust crates under native architecture docs unless promoted as user-facing also lives in [`user-facing-packages.md`](./user-facing-packages.md). ## Runtime flow (high level) diff --git a/docs/user-facing-packages.md b/docs/user-facing-packages.md index c6ed1d4e3..b76d85c13 100644 --- a/docs/user-facing-packages.md +++ b/docs/user-facing-packages.md @@ -7,7 +7,7 @@ This page indexes README-only user-facing package CLIs and features that need ro - **Include** root docs coverage for package-local CLIs, extension features, dashboards, and benchmark runners that users can run directly or through `omp`. - **Exclude explicitly** when a package/crate is internal implementation only; point to the architecture doc that owns it. - Package READMEs and manifests remain the source of truth for package-local setup and flags; root docs make the feature discoverable and link to exact source paths. -- Internal Rust crates remain covered by native architecture docs unless promoted as standalone user-facing commands or APIs. Today, `crates/pi-natives` is documented through [`natives-architecture.md`](./natives-architecture.md) and related native docs because it backs `@oh-my-pi/pi-natives` rather than exposing its own user CLI. +- Internal Rust crates remain covered by native architecture docs unless promoted as standalone user-facing commands or APIs. The contributor-facing map lives at [`native-crates.md`](./native-crates.md); today every `crates/*` entry is internal to `@oh-my-pi/pi-natives` and the embedded shell, so [`natives-architecture.md`](./natives-architecture.md) and the surrounding native docs own them. ## Package CLIs and features diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3af41787a..f49913eab 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed `omp://` documentation coverage for managed memory/skill tools, image generation, speech generation, README-only user-facing package CLIs, and docs-index freshness checks. ([#3934](https://github.com/can1357/oh-my-pi/issues/3934)) +- Fixed `omp://` documentation coverage for the managed memory/skill tools, image generation, speech generation, README-only user-facing package CLIs, and the internal Rust crate map, and added a docs-index freshness check and a built-in tool docs coverage test. ([#3934](https://github.com/can1357/oh-my-pi/issues/3934)) ## [16.2.10] - 2026-06-30 diff --git a/packages/coding-agent/scripts/generate-docs-index.ts b/packages/coding-agent/scripts/generate-docs-index.ts index 8c3a8b238..9be6f7418 100755 --- a/packages/coding-agent/scripts/generate-docs-index.ts +++ b/packages/coding-agent/scripts/generate-docs-index.ts @@ -40,10 +40,6 @@ function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every(item => typeof item === "string"); } -function fail(message: string): never { - throw new Error(message); -} - /** Build the exact two-line `omp://` docs embed from the source `docs/**\/*.md` corpus. */ export async function buildDocsIndexPayload(): Promise { const glob = new Glob("**/*.md"); @@ -68,52 +64,55 @@ export function decodeDocsIndexPayload(embed: string): DecodedDocsIndexPayload | if (newline === -1) return null; const filenames: unknown = JSON.parse(embed.slice(0, newline)); - if (!isStringArray(filenames)) fail("Embedded docs index filename line is not a JSON string array."); + if (!isStringArray(filenames)) { + throw new Error("Embedded docs index filename line is not a JSON string array."); + } const inflated = gunzipSync(Buffer.from(embed.slice(newline + 1), "base64")); const bodies: unknown = JSON.parse(inflated.toString("utf8")); - if (!isStringArray(bodies)) fail("Embedded docs index body blob is not a JSON string array."); + if (!isStringArray(bodies)) { + throw new Error("Embedded docs index body blob is not a JSON string array."); + } return { files: filenames, bodies }; } -/** Assert that an embed payload is fresh against the current source docs payload. */ +/** + * Assert that an embed payload is fresh against the current source docs payload. + * An empty placeholder is accepted by round-tripping the expected payload (the + * dev tree and post-build reset state both checked-in placeholders). + */ export function assertDocsIndexFresh(embed: string, expected: DecodedDocsIndexPayload): void { - const decoded = - embed.length === 0 ? decodeDocsIndexPayload(buildPayloadText(expected)) : decodeDocsIndexPayload(embed); - if (decoded === null) fail("Embedded docs index is malformed: missing newline separator."); + const source = + embed.length > 0 + ? embed + : `${JSON.stringify(expected.files)}\n${Buffer.from(gzipSync(Buffer.from(JSON.stringify(expected.bodies)), { level: 9 })).toString("base64")}`; + const decoded = decodeDocsIndexPayload(source); + if (decoded === null) { + throw new Error("Embedded docs index is malformed: missing newline separator."); + } if (decoded.files.length !== expected.files.length) { - fail(`Embedded docs index has ${decoded.files.length} docs; source corpus has ${expected.files.length}.`); + throw new Error( + `Embedded docs index has ${decoded.files.length} docs; source corpus has ${expected.files.length}.`, + ); } if (decoded.bodies.length !== expected.bodies.length) { - fail(`Embedded docs index has ${decoded.bodies.length} bodies; source corpus has ${expected.bodies.length}.`); + throw new Error( + `Embedded docs index has ${decoded.bodies.length} bodies; source corpus has ${expected.bodies.length}.`, + ); } for (let i = 0; i < expected.files.length; i++) { if (decoded.files[i] !== expected.files[i]) { - fail( + throw new Error( `Embedded docs index filename mismatch at ${i}: ${decoded.files[i] ?? ""} !== ${expected.files[i]}.`, ); } if (decoded.bodies[i] !== expected.bodies[i]) { - fail(`Embedded docs index body mismatch for ${expected.files[i]}. Run \`bun run gen:docs\`.`); + throw new Error(`Embedded docs index body mismatch for ${expected.files[i]}. Run \`bun run gen:docs\`.`); } } } -function buildPayloadText(payload: DecodedDocsIndexPayload): string { - const bodiesB64 = Buffer.from(gzipSync(Buffer.from(JSON.stringify(payload.bodies)), { level: 9 })).toString( - "base64", - ); - return `${JSON.stringify(payload.files)}\n${bodiesB64}`; -} - -async function checkDocsIndexFreshness(rel: string): Promise { - const current = await buildDocsIndexPayload(); - const embed = await Bun.file(outputPath).text(); - assertDocsIndexFresh(embed, current); - process.stdout.write(`Docs index fresh for ${current.files.length} docs (${rel})\n`); -} - async function main(): Promise { const rel = path.relative(process.cwd(), outputPath); @@ -124,7 +123,10 @@ async function main(): Promise { } if (process.argv.includes(CHECK_FLAG)) { - await checkDocsIndexFreshness(rel); + const current = await buildDocsIndexPayload(); + const embed = await Bun.file(outputPath).text(); + assertDocsIndexFresh(embed, current); + process.stdout.write(`Docs index fresh for ${current.files.length} docs (${rel})\n`); return; } diff --git a/packages/coding-agent/test/internal-urls/docs-tool-coverage.test.ts b/packages/coding-agent/test/internal-urls/docs-tool-coverage.test.ts new file mode 100644 index 000000000..457278a22 --- /dev/null +++ b/packages/coding-agent/test/internal-urls/docs-tool-coverage.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "bun:test"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { BUILTIN_TOOL_NAMES } from "@oh-my-pi/pi-coding-agent/tools/builtin-names"; + +// Every shipped built-in tool that is exposed to the model in normal sessions +// must have a docs/tools/.md root doc served by `omp://`. File names use +// underscores or hyphens; the test accepts either form so renaming the on-disk +// page does not require coordinating with the wire name. +const docsToolsDir = path.resolve(import.meta.dir, "../../../../docs/tools"); + +const expectedDocPaths = (name: string): string[] => [ + path.join(docsToolsDir, `${name}.md`), + path.join(docsToolsDir, `${name.replace(/_/g, "-")}.md`), +]; + +// Custom tools injected by the SDK (`packages/coding-agent/src/sdk.ts`) when +// their settings are enabled. Built-in tool factories live in BUILTIN_TOOLS but +// these custom tools are not present there, so the coverage list is explicit. +const CUSTOM_TOOL_NAMES = ["generate_image", "tts"] as const; + +describe("omp:// root docs coverage", () => { + it.each([...BUILTIN_TOOL_NAMES])("documents builtin tool %s", name => { + const candidates = expectedDocPaths(name); + const present = candidates.find(candidate => fs.existsSync(candidate)); + expect( + present, + `Missing docs/tools/.md for built-in tool "${name}". Tried: ${candidates.join(", ")}.`, + ).toBeDefined(); + }); + + it.each([...CUSTOM_TOOL_NAMES])("documents injected custom tool %s", name => { + const candidates = expectedDocPaths(name); + const present = candidates.find(candidate => fs.existsSync(candidate)); + expect(present, `Missing docs/tools/.md for injected custom tool "${name}".`).toBeDefined(); + }); +});