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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ the Lua language server in VS Code or Cursor completes `Open77.*` from the same
## Where the knowledge comes from

The index is built from the public documentation content of
[open77-app](https://github.com/Open2077/open77-app) (`content/api`, `content/docs`), which is
synced from the platform wiki, itself generated from the client and server source. Nothing here is
[open77-app](https://github.com/Open2077/open77-app): `content/api` and `content/docs`, which are
synced from the platform wiki, itself generated from the client and server source, plus the site's
own guides in `content/guides` (the RP job pages, the runtime and operator pages). Nothing here is
hand-written knowledge: cards, permissions, reasons, `since`, events and catalogues are extracted
from code; guides are chunked as published.
from code; guides are chunked as published. A wiki guide keeps its name over a site guide.

Index builds are published to `https://cdn.open2077.net/dev-index/<build>/` with a hashed manifest.
The package ships a snapshot of the latest index and refreshes from the CDN at most once a day,
Expand Down
2 changes: 1 addition & 1 deletion index/guides.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions index/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"formatVersion": 1,
"build": "2.31.21+op77.132",
"generatedAt": "2026-10-13T06:55:33.648Z",
"generatedAt": "2026-10-13T07:02:18.615Z",
"docsSyncedAt": "2026-09-24T12:33:14.568Z",
"source": {
"repository": "https://github.com/Open2077/open77-app",
Expand All @@ -10,8 +10,9 @@
"counts": {
"cards": 1739,
"releases": 81,
"guides": 105,
"sections": 1455,
"siteGuides": 29,
"guides": 134,
"sections": 1925,
"fivem": 100,
"catalogues": 9,
"schemas": 2,
Expand All @@ -35,8 +36,8 @@
"bytes": 6893
},
"guides.json": {
"sha256": "d9311d3f9c462df0ac8d698b4e3bef1ab1b1cef4ba4eed400e549be6ac685b1b",
"bytes": 2218037
"sha256": "9ed0f335b49e67a1328b6103b8d4932bb553c35e6cef0567883a73999e3d5dfa",
"bytes": 2879670
},
"fivem-map.json": {
"sha256": "f88c88ee9c0fa42b67a5ddfebb2226cd35564d34d797741f0f398964104ca3fa",
Expand Down
27 changes: 25 additions & 2 deletions src/index/builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Builds a Dev Index directory from the public documentation content of
* `Open2077/open77-app` (`content/api` + `content/docs`).
* `Open2077/open77-app` (`content/api` + `content/docs`, and the site's own
* guides in `content/guides`).
*
* Nothing here invents a fact: cards, events, permissions, releases,
* catalogues and schemas are copied verbatim from the content; the guides are
Expand Down Expand Up @@ -220,10 +221,32 @@ export async function buildIndex(options: BuildOptions): Promise<IndexManifest>
const markdown = await readFile(path.join(docs, name), "utf8");
guides.push(chunkGuide(name, markdown, cardIndex));
}
// The site's own guides (the RP job pages, build-a-job, the runtime and
// operator pages) are written for the website, not synced from the wiki, but
// they are public documentation too: an agent asked about the CHOOH2 job or
// the readiness gate found nothing before. A name the wiki already uses keeps
// the wiki's guide. Content older than the folder has none.
const siteGuides = path.join(contentDir, "guides");
const wikiFiles = new Set(guides.map((g) => g.file));
let siteNames: string[] = [];
try {
siteNames = (await readdir(siteGuides)).filter((f) => f.endsWith(".md")).sort();
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
counts.siteGuides = 0;
for (const name of siteNames) {
if (wikiFiles.has(name)) {
log(`site guide ${name} skipped: content/docs has a guide with that name`);
continue;
}
guides.push(chunkGuide(name, await readFile(path.join(siteGuides, name), "utf8"), cardIndex));
counts.siteGuides++;
}
counts.guides = guides.length;
counts.sections = guides.reduce((n, g) => n + g.sections.length, 0);
await write("guides.json", JSON.stringify(guides));
log(`guides: ${guides.length} (${counts.sections} sections)`);
log(`guides: ${guides.length}, ${counts.siteGuides} from the site (${counts.sections} sections)`);

const fivemGuide = guides.find((g) => g.file === "fivem-compatibility.md");
const fivem = fivemGuide ? parseFivemGuide(await readFile(path.join(docs, fivemGuide.file), "utf8")) : [];
Expand Down
35 changes: 33 additions & 2 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { copyFile, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { chunkGuide, opNumber, parseFivemGuide } from "../src/index/builder.js";
import { buildIndex, chunkGuide, opNumber, parseFivemGuide } from "../src/index/builder.js";
import { loadIndex } from "../src/index/loader.js";
import { IndexSearch } from "../src/index/search.js";
import type { ApiCard } from "../src/index/types.js";
Expand Down Expand Up @@ -32,6 +32,37 @@ test("opNumber reads the op77 build number", () => {
assert.equal(opNumber("nonsense"), -1);
});

test("buildIndex adds the site guides, and a wiki guide keeps its name", async () => {
// The RP job pages live in the app's content/guides, which the index did
// not read: an agent asked about CHOOH2 found nothing (2026-10-13).
const root = await mkdtemp(path.join(os.tmpdir(), "open77-build-"));
try {
const content = path.join(root, "content");
for (const dir of ["api/catalogues", "api/schemas", "docs", "guides"]) await mkdir(path.join(content, dir), { recursive: true });
await copyFile(path.join(INDEX_DIR, "cards.json"), path.join(content, "api", "api.json"));
for (const name of ["events.json", "permissions.json", "releases.json"]) await copyFile(path.join(INDEX_DIR, name), path.join(content, "api", name));
await copyFile(path.join(INDEX_DIR, "catalogues", "sfx.json"), path.join(content, "api", "catalogues", "sfx.json"));
await copyFile(path.join(INDEX_DIR, "schemas", "manifest.schema.json"), path.join(content, "api", "schemas", "manifest.schema.json"));
await writeFile(path.join(content, "docs", "_manifest.json"), JSON.stringify({ syncedAt: "2026-10-13T00:00:00.000Z" }));
await writeFile(path.join(content, "docs", "actions.md"), "# Actions\n\nFrom the wiki.\n");
await writeFile(path.join(content, "guides", "actions.md"), "# Actions, site copy\n\nShadowed.\n");
await writeFile(path.join(content, "guides", "rp-job-chooh2.md"), "# CHOOH2\n\nCorn to fuel.\n\n## Refinery\n\nOne timed operation.\n");

const manifest = await buildIndex({ contentDir: content, outDir: path.join(root, "out") });
assert.equal(manifest.counts["siteGuides"], 1);
const guides = JSON.parse(await readFile(path.join(root, "out", "guides.json"), "utf8")) as { slug: string; title: string; sections: { anchor: string }[] }[];
assert.deepEqual(guides.map((g) => g.slug), ["actions", "rp-job-chooh2"]);
assert.equal(guides[0]!.title, "Actions", "the wiki guide wins a name clash");
assert.deepEqual(guides[1]!.sections.map((s) => s.anchor), ["chooh2", "refinery"]);

await rm(path.join(content, "guides"), { recursive: true });
const older = await buildIndex({ contentDir: content, outDir: path.join(root, "out-older") });
assert.equal(older.counts["siteGuides"], 0, "content without a guides folder still builds");
} finally {
await rm(root, { recursive: true, force: true });
}
});

test("chunkGuide splits by heading, dedupes anchors and finds natives", () => {
const card = { qualified: "Open77.map.getWaypoint", route_id: "Open77.map.getWaypoint" } as ApiCard;
const guide = chunkGuide("x.md", [
Expand Down
Loading