From 363d390f3f0b766934e6fc5a36369df3e09e54d6 Mon Sep 17 00:00:00 2001 From: Jeremy Levartovsky Date: Sun, 30 Aug 2026 09:22:26 +1000 Subject: [PATCH 1/3] fix(context): only skip repo-meta filenames at the scan root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `IGNORED_FILES` is matched by basename at every depth, so any documentation page that happens to be named `security.md`, `license.md`, `changelog.md`, `contributing.md` or `history.md` is dropped as if it were repo housekeeping. Measured on codeberg.org/forgejo/docs with @neuledge/context 1.2.3: 138 markdown files under `docs/`, 135 reach the builder. The two lost files are `docs/admin/actions/security.md` and `docs/user/actions/security.md` — real documentation about securing Forgejo Actions, and the only source in the repo for `container.valid_volumes`. `context add` prints "Found 135 markdown files" and exits 0, so nothing signals the loss; a later query for `valid_volumes` simply returns nothing. Other repos hit by the same rule: docker/docs loses `content/manuals/extensions/extensions-sdk/architecture/security.md`, two `history.md` pages and two `changelog.md` API references; excalidraw/excalidraw loses `dev-docs/docs/introduction/contributing.mdx`. Restricting the check to `basePath === ""` keeps the original intent — those names mean repo housekeeping at the top of a tree — while leaving nested pages alone. Two tests cover both halves. --- packages/context/src/git.test.ts | 45 +++++++++++++++++++++++++++++++- packages/context/src/git.ts | 6 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/context/src/git.test.ts b/packages/context/src/git.test.ts index 925b4fd..2f567c6 100644 --- a/packages/context/src/git.test.ts +++ b/packages/context/src/git.test.ts @@ -1,9 +1,13 @@ -import { describe, expect, it } from "vitest"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { findLatestStableVersion, isMissingRefError, isTransientGitError, parseMonorepoTag, + readLocalDocsFiles, } from "./git.js"; describe("isTransientGitError", () => { @@ -189,3 +193,42 @@ describe("findLatestStableVersion", () => { }); }); }); + +describe("readLocalDocsFiles — repo-meta filenames", () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "ctx-localdocs-")); + }); + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + const write = (rel: string, body: string): void => { + const full = join(dir, rel); + mkdirSync(join(full, ".."), { recursive: true }); + writeFileSync(full, body); + }; + + it("skips repo-meta files at the scan root", () => { + write("security.md", "# Security policy\n\nReport issues to us.\n"); + write("license.md", "# License\n\nMIT.\n"); + write("guide.md", "# Guide\n\nContent.\n"); + + const paths = readLocalDocsFiles(dir).map((f) => f.path); + + expect(paths).toEqual(["guide.md"]); + }); + + it("keeps a documentation page that merely shares a repo-meta filename", () => { + write( + "admin/actions/security.md", + "# Securing Actions\n\nThe default value of valid_volumes is an empty array.\n", + ); + + const paths = readLocalDocsFiles(dir).map((f) => f.path); + + expect(paths).toEqual(["admin/actions/security.md"]); + }); +}); diff --git a/packages/context/src/git.ts b/packages/context/src/git.ts index fa764d1..bc58be9 100755 --- a/packages/context/src/git.ts +++ b/packages/context/src/git.ts @@ -474,7 +474,11 @@ function findMarkdownFiles( const matchingExt = DOCUMENTATION_EXTENSIONS.find((ext) => lowerName.endsWith(ext), ); - if (matchingExt) { + // Only at the scan root: these names mean repo housekeeping there, but + // deeper in a docs tree they are ordinary pages — forgejo's + // docs/admin/actions/security.md documents Actions security, and was + // being dropped as if it were a SECURITY.md policy file. + if (matchingExt && basePath === "") { const baseName = lowerName.slice(0, -matchingExt.length); if (IGNORED_FILES.has(baseName)) continue; } From a24d887ba53ad3b73f5f278a026da781c96db87d Mon Sep 17 00:00:00 2001 From: Jeremy Levartovsky Date: Sun, 30 Aug 2026 12:59:00 +1000 Subject: [PATCH 2/3] Add a changeset for the IGNORED_FILES fix --- .changeset/lucky-pugs-repeat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-pugs-repeat.md diff --git a/.changeset/lucky-pugs-repeat.md b/.changeset/lucky-pugs-repeat.md new file mode 100644 index 0000000..7a1071b --- /dev/null +++ b/.changeset/lucky-pugs-repeat.md @@ -0,0 +1,5 @@ +--- +"@neuledge/context": patch +--- + +Only skip repo-meta filenames (`security`, `license`, `changelog`, `contributing`, …) at the scan root, not at every depth. A documentation page that happens to share one of those names was being dropped silently: `context add` on the forgejo docs lost `docs/admin/actions/security.md`, the only source in the repo for `container.valid_volumes`, and reported success. `docker/docs` and `excalidraw/excalidraw` lose pages to the same rule. From 263c096c49b1dc9dffc137d2c8864cb1957888e1 Mon Sep 17 00:00:00 2001 From: WayOfThKeyboard <140487034+WayOfThKeyboard@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:40:20 +1000 Subject: [PATCH 3/3] fix(context): gate repo-meta skipping on the repo root, not on depth `IGNORED_FILES` is housekeeping for a repository root, so the skip should apply only when the walk starts there. The previous guard tested `basePath === ""`, which is also true at the top of a `docs_path` folder, because `readLocalDocsFiles` seeds the walk from `searchPath` with an empty base. A genuine page at `docs/security.md` was therefore still dropped whenever a definition set `docs_path`, which 135 of the 140 definitions do. `readLocalDocsFiles` now passes `atRepoRoot: !docsPath` into `findMarkdownFiles`, and the skip requires both that flag and the empty base path. Same file is kept or dropped on what it is, not on how the scan happened to be rooted. Third test added: with `path: "docs"`, a root `SECURITY.md` is still skipped and `docs/security.md` is kept. It fails without the flag, and the existing root test fails if the flag is forced false, so both directions are covered. 224/224 green, biome clean. --- .changeset/lucky-pugs-repeat.md | 2 +- packages/context/src/git.test.ts | 10 ++++++++++ packages/context/src/git.ts | 16 +++++++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.changeset/lucky-pugs-repeat.md b/.changeset/lucky-pugs-repeat.md index 7a1071b..5d124b8 100644 --- a/.changeset/lucky-pugs-repeat.md +++ b/.changeset/lucky-pugs-repeat.md @@ -2,4 +2,4 @@ "@neuledge/context": patch --- -Only skip repo-meta filenames (`security`, `license`, `changelog`, `contributing`, …) at the scan root, not at every depth. A documentation page that happens to share one of those names was being dropped silently: `context add` on the forgejo docs lost `docs/admin/actions/security.md`, the only source in the repo for `container.valid_volumes`, and reported success. `docker/docs` and `excalidraw/excalidraw` lose pages to the same rule. +Only skip repo-meta filenames (`security`, `license`, `changelog`, `contributing`, and the rest) when the scan starts at the repository root. They were matched by basename at every depth, and also at the top of a `docs_path` folder, so an ordinary documentation page that happens to share one of those names was dropped without a word. `context add` on the forgejo docs lost `docs/admin/actions/security.md`, the only source in that repo for `container.valid_volumes`, and still reported success. `docker/docs` and `excalidraw/excalidraw` lose pages to the same rule. diff --git a/packages/context/src/git.test.ts b/packages/context/src/git.test.ts index 2f567c6..e21d61d 100644 --- a/packages/context/src/git.test.ts +++ b/packages/context/src/git.test.ts @@ -231,4 +231,14 @@ describe("readLocalDocsFiles — repo-meta filenames", () => { expect(paths).toEqual(["admin/actions/security.md"]); }); + + it("keeps a repo-meta filename at a docs root that is not the repo root", () => { + write("SECURITY.md", "# Security policy\n\nReport issues to us.\n"); + write("docs/security.md", "# Securing deployments\n\nSet valid_volumes.\n"); + write("docs/guide.md", "# Guide\n\nContent.\n"); + + const paths = readLocalDocsFiles(dir, { path: "docs" }).map((f) => f.path); + + expect(paths.sort()).toEqual(["docs/guide.md", "docs/security.md"]); + }); }); diff --git a/packages/context/src/git.ts b/packages/context/src/git.ts index bc58be9..79b75a8 100755 --- a/packages/context/src/git.ts +++ b/packages/context/src/git.ts @@ -407,6 +407,8 @@ function loadGitignore(basePath: string): Ignore { export interface FindMarkdownOptions { /** Language filter: "all" includes everything, specific code (e.g., "en") includes only that locale */ lang?: string; + /** True when the scan starts at the repo root rather than inside a docs folder */ + atRepoRoot?: boolean; } /** @@ -474,11 +476,12 @@ function findMarkdownFiles( const matchingExt = DOCUMENTATION_EXTENSIONS.find((ext) => lowerName.endsWith(ext), ); - // Only at the scan root: these names mean repo housekeeping there, but - // deeper in a docs tree they are ordinary pages — forgejo's + // Only at the repo root: these names mean repo housekeeping there, + // but anywhere in a docs tree they are ordinary pages — forgejo's // docs/admin/actions/security.md documents Actions security, and was - // being dropped as if it were a SECURITY.md policy file. - if (matchingExt && basePath === "") { + // being dropped as if it were a SECURITY.md policy file. A docs + // folder is not the repo root even though the walk starts there. + if (matchingExt && basePath === "" && options.atRepoRoot) { const baseName = lowerName.slice(0, -matchingExt.length); if (IGNORED_FILES.has(baseName)) continue; } @@ -531,7 +534,10 @@ export function readLocalDocsFiles( // Load gitignore from repo root const ig = loadGitignore(basePath); - const markdownFiles = findMarkdownFiles(searchPath, ig, "", { lang }); + const markdownFiles = findMarkdownFiles(searchPath, ig, "", { + lang, + atRepoRoot: !docsPath, + }); const files: Array<{ path: string; content: string }> = []; const seenHashes = new Set();