diff --git a/context/map/artifact-1-territory.md b/context/map/artifact-1-territory.md index bcb669e..0df86c1 100644 --- a/context/map/artifact-1-territory.md +++ b/context/map/artifact-1-territory.md @@ -2,7 +2,7 @@ -> Scan of `cb4b636` (2026-07-28) · window: last 12 months · engine: tools/repo-map +> Scan of `863bf16` (2026-07-28) · window: last 12 months · engine: tools/repo-map **Where the work actually happens.** Git-history activity over the last 12 months, noise filtered (lockfiles, generated files, assets). Activity is a signal of *risk of accidental regression*, not of quality. @@ -12,7 +12,7 @@ | --- | --- | | `src/lib` | 75 | | `context/changes` | 53 | -| `context/map` | 37 | +| `context/map` | 42 | | `tests/e2e` | 32 | | `src/commands` | 30 | | `package.json` | 28 | @@ -37,12 +37,12 @@ | `src/lib/writer.ts` | 12 | | `README.md` | 9 | | `tests/writer.test.ts` | 9 | +| `context/map/artifact-1-territory.md` | 8 | +| `context/map/artifact-2-structure.md` | 8 | +| `context/map/artifact-3-contributors.md` | 8 | +| `context/map/graph/coupling.json` | 8 | +| `context/map/repo-map.md` | 8 | | `src/lib/config.ts` | 8 | -| `context/map/artifact-1-territory.md` | 7 | -| `context/map/artifact-2-structure.md` | 7 | -| `context/map/artifact-3-contributors.md` | 7 | -| `context/map/graph/coupling.json` | 7 | -| `context/map/repo-map.md` | 7 | | `src/lib/manifest.ts` | 7 | | `src/lib/api-content.ts` | 7 | @@ -52,7 +52,7 @@ | --- | --- | --- | | `src/lib` | 34 | 1 | | `context/changes` | 28 | 1 | -| `context/map` | 0 | 7 | +| `context/map` | 0 | 8 | | `tests/e2e` | 11 | 0 | | `src/commands` | 23 | 0 | diff --git a/context/map/artifact-2-structure.md b/context/map/artifact-2-structure.md index 14837ba..9331ab6 100644 --- a/context/map/artifact-2-structure.md +++ b/context/map/artifact-2-structure.md @@ -2,7 +2,7 @@ -> Scan of `cb4b636` (2026-07-28) · window: last 12 months · engine: tools/repo-map +> Scan of `863bf16` (2026-07-28) · window: last 12 months · engine: tools/repo-map **How it is built.** Static import graph of `src` plus a file inventory of every module. Folders show where the code is; the graph shows what actually depends on what. @@ -20,13 +20,9 @@ _None detected this run._ ## Highest fan-out folders (efferent coupling Ce — depends on many) -| Folder | Ce (out) | Ca (in, lower bound) | Modules | -| --- | --- | --- | --- | -| `src/commands` | 38 | 5 | 5 | -| `src` | 24 | 0 | 25 | -| `src/lib` | 17 | 33 | 19 | +_Metrics unavailable this run (see limitations)._ ## Limitations -- Afferent coupling (Ca) from dependency-cruiser is unreliable without a fully installed toolchain (external/aliased imports go unresolved). Treat Ca as a lower bound. +- dependency-cruiser not runnable — coupling metrics skipped this run. - Static import graph only. Runtime wiring (dynamic import, template refs, feature flags, codegen) is invisible here and is an `unknown`, not "no dependency". diff --git a/context/map/artifact-3-contributors.md b/context/map/artifact-3-contributors.md index 72f5cde..2b2859f 100644 --- a/context/map/artifact-3-contributors.md +++ b/context/map/artifact-3-contributors.md @@ -2,7 +2,7 @@ -> Scan of `cb4b636` (2026-07-28) · window: last 12 months · engine: tools/repo-map +> Scan of `863bf16` (2026-07-28) · window: last 12 months · engine: tools/repo-map **Who to ask before you change something.** Bots and AI agents (Claude, Codex, Copilot, dependabot…) are filtered out — this is about humans who hold undocumented context. Answers "who has context on this area", not "who changed this line" (that is `git blame`). diff --git a/context/map/repo-map.md b/context/map/repo-map.md index b51499c..7e4ed8e 100644 --- a/context/map/repo-map.md +++ b/context/map/repo-map.md @@ -2,7 +2,7 @@ -> Scan of `cb4b636` (2026-07-28) · window: last 12 months · engine: tools/repo-map +> Scan of `863bf16` (2026-07-28) · window: last 12 months · engine: tools/repo-map > > Operational map of the territory, built from cheap deterministic signals (git history + import graph), synthesized for decisions — not an essay. A new developer should know in ~15 min where things live, what is dangerous, and where to start. Refresh with `npm run repo-map`. @@ -30,7 +30,7 @@ Top active areas (full table in `artifact-1-territory.md`): | --- | --- | | `src/lib` | 75 | | `context/changes` | 53 | -| `context/map` | 37 | +| `context/map` | 42 | | `tests/e2e` | 32 | | `src/commands` | 30 | | `package.json` | 28 | @@ -54,10 +54,7 @@ Structural coupling and cycles live in `artifact-2-structure.md` and `graph/`. ## 4. Risk zones — where to be careful -| Zone | What | Why it matters (evidence) | -| --- | --- | --- | -| `src/commands` | Fan-out Ce=38 across 5 modules | Depends on many things; wide blast radius on change. (dependency-cruiser) | -| `src/lib` | Fan-out Ce=17 across 19 modules | Depends on many things; wide blast radius on change. (dependency-cruiser) | +_No high-risk zones flagged this run._ ## 5. Who to ask @@ -79,12 +76,12 @@ Structural coupling and cycles live in `artifact-2-structure.md` and `graph/`. - `src/lib/writer.ts` — 12 commits in window - `README.md` — 9 commits in window - `tests/writer.test.ts` — 9 commits in window -- `src/lib/config.ts` — 8 commits in window +- `context/map/artifact-1-territory.md` — 8 commits in window ## 7. Limitations — what this map does NOT say - It is a map of **activity and structure over the last 12 months**, not of correctness or quality. -- Afferent coupling (Ca) from dependency-cruiser is unreliable without a fully installed toolchain (external/aliased imports go unresolved). Treat Ca as a lower bound. +- dependency-cruiser not runnable — coupling metrics skipped this run. - Static import graph only. Runtime wiring (dynamic import, template refs, feature flags, codegen) is invisible here and is an `unknown`, not "no dependency". --- diff --git a/tests/api-content.test.ts b/tests/api-content.test.ts index 0c596ed..8a8bd76 100644 --- a/tests/api-content.test.ts +++ b/tests/api-content.test.ts @@ -14,6 +14,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { createHash, generateKeyPairSync, sign } from "node:crypto"; import { + fetchArtifact, fetchCatalog, fetchHealth, fetchLesson, @@ -35,10 +36,12 @@ const realFetch = globalThis.fetch; const realApiBase = process.env["API_BASE_URL"]; const realKeyset = process.env["BUNDLE_PUBLIC_KEYSET"]; let requestedUrls: string[] = []; +let requestedInits: Array = []; function mockFetchOnce(body: string, init: { status?: number; headers?: Record } = {}) { - globalThis.fetch = (async (url: string | URL) => { + globalThis.fetch = (async (url: string | URL, requestInit?: RequestInit) => { requestedUrls.push(String(url)); + requestedInits.push(requestInit); return new Response(body, { status: init.status ?? 200, headers: { "content-type": "application/json", ...init.headers }, @@ -66,6 +69,7 @@ const signedHeaders = (body: string, keyId = TEST_KEY_ID) => ({ beforeEach(() => { requestedUrls = []; + requestedInits = []; process.env["API_BASE_URL"] = "http://localhost:8787"; process.env["BUNDLE_PUBLIC_KEYSET"] = JSON.stringify([ { keyId: TEST_KEY_ID, publicKey: publicKeyDerB64 }, @@ -142,24 +146,67 @@ describe("fetchLesson signature verification", () => { if (!result.ok) expect(result.code).toBe("signature_missing"); }); - it("passes API error envelopes through without signature checks", async () => { - mockFetchOnce(JSON.stringify({ error: "no", code: "no_membership" }), { status: 403 }); + it("passes API error envelopes through before checking partial signing headers", async () => { + mockFetchOnce(JSON.stringify({ error: "module_locked", code: "locked_now" }), { + status: 403, + headers: { "X-Bundle-Signature": "ignored-on-error" }, + }); const result = await fetchLesson("10xdevs-3", "m1l1", "tok"); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.code).toBe("no_membership"); + expect(result).toEqual({ + ok: false, + status: 403, + code: "locked_now", + error: "This module is not available yet.", + payload: { error: "module_locked", code: "locked_now" }, + }); }); }); // --- plain endpoints ----------------------------------------------------------- describe("fetchCatalog", () => { - it("hits the encoded catalog path and returns the payload", async () => { - const payload = { course: "10xdevs-3", modules: [], lessons: [] }; + it("encodes the catalog path and forwards bearer token and caller signal", async () => { + const payload = { course: "course/alpha", modules: [], lessons: [] }; mockFetchOnce(JSON.stringify(payload)); - const result = await fetchCatalog("10xdevs-3", "tok"); + const controller = new AbortController(); + + const result = await fetchCatalog("course/alpha", "token-1", { + signal: controller.signal, + }); + expect(result.ok).toBe(true); - if (result.ok) expect(result.data.course).toBe("10xdevs-3"); - expect(requestedUrls[0]).toContain("/api/catalog/10xdevs-3"); + if (result.ok) expect(result.data.course).toBe("course/alpha"); + expect(requestedUrls[0]).toBe("http://localhost:8787/api/catalog/course%2Falpha"); + expect(new Headers(requestedInits[0]?.headers).get("authorization")).toBe("Bearer token-1"); + expect(requestedInits[0]?.signal).toBe(controller.signal); + }); +}); + +describe("fetchArtifact", () => { + it("always serializes tool while omitting an empty optional language", async () => { + mockFetchOnce(JSON.stringify({ type: "prompts", name: "review", content: "Review it" })); + + const result = await fetchArtifact( + "course/alpha", + "m1/l1", + "prompts/custom", + "review notes", + "", + "token-4", + { lang: "" }, + ); + + expect(requestedUrls[0]).toBe( + "http://localhost:8787/api/artifacts/course%2Falpha/m1%2Fl1/prompts%2Fcustom/review%20notes?tool=", + ); + expect(result).toEqual({ + ok: false, + status: 0, + code: "signature_missing", + error: + "Artifact is missing a signature. The API may be misconfigured or compromised. " + + "Do NOT use the content. Report this to the course team.", + }); }); });