From d332541bc539d42cadb1805b64239887d98499a6 Mon Sep 17 00:00:00 2001 From: zhao Date: Wed, 29 Jul 2026 05:51:56 +0800 Subject: [PATCH 1/8] docs: add production readiness sign-off for npm 0.5.0 --- CHANGELOG.md | 2 +- README.md | 2 +- docs/README.md | 1 + docs/RELEASING.md | 4 ++++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed787dc..82ef916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ### Removed -- **Breaking (library):** Trimmed internal-only re-exports from the package root and `/alliance` entry to shrink the public surface and blast radius: `trimOptional`, `createUnconfiguredAllianceContext` (core), and the concrete URL generators `generatorMailing`, `generatorSlackCpplang` (alliance). These were internal helpers, not part of the documented API; register built-ins via `registerBuiltinUrlGenerators` and build contexts via `createServer` / `createIsolatedContext`. A snapshot test now guards the runtime export surface so internal symbols cannot leak back in. See [MIGRATION.md](docs/MIGRATION.md#internal-only-re-exports-removed-203). (#203) +- **Breaking (library):** Trimmed internal-only re-exports from the package root and `/alliance` entry to shrink the public surface and blast radius: `trimOptional`, `createUnconfiguredAllianceContext` (core), and the concrete URL generators `generatorMailing`, `generatorSlackCpplang` (alliance). These were internal helpers, not part of the documented API; register built-ins via `registerBuiltinUrlGenerators` and build contexts via `createServer` / `createIsolatedContext`. A snapshot test now guards the runtime export surface so internal symbols cannot leak back in. See [MIGRATION.md](docs/MIGRATION.md#050-internal-only-re-exports-removed-203). (#203) ### Fixed diff --git a/README.md b/README.md index 5184070..0d103f1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Current version: 0.5.0 (npm `latest` after publish). Pin `@0.5.0` in install and ## Upgrading to 0.5.0 -Version **0.5.0** includes breaking MCP and library changes: `list_sources` returns `sources: { name, description? }[]`, `PineconeClient.listNamespacesWithMetadata()` returns `{ namespaces, warnings }`, internal-only package re-exports were removed (#203), the MCP SDK floor is **^1.29.0**, and hybrid leg failure with zero surviving hits is signaled under `experimental.degraded` / `experimental.hybrid_leg_failed` (#228). Legacy module facades remain deprecated but are **not** removed in this release. See [docs/MIGRATION.md § list_sources](docs/MIGRATION.md#050-list_sources-response-shape), [§ listNamespacesWithMetadata](docs/MIGRATION.md#050-pineconeclientlistnamespaceswithmetadata-return-shape), [§ internal re-exports (#203)](docs/MIGRATION.md#internal-only-re-exports-removed-203), and the [CHANGELOG](CHANGELOG.md#050---2026-07-25). +Version **0.5.0** includes breaking MCP and library changes: `list_sources` returns `sources: { name, description? }[]`, `PineconeClient.listNamespacesWithMetadata()` returns `{ namespaces, warnings }`, internal-only package re-exports were removed (#203), the MCP SDK floor is **^1.29.0**, and hybrid leg failure with zero surviving hits is signaled under `experimental.degraded` / `experimental.hybrid_leg_failed` (#228). Legacy module facades remain deprecated but are **not** removed in this release. See [docs/MIGRATION.md § list_sources](docs/MIGRATION.md#050-list_sources-response-shape), [§ listNamespacesWithMetadata](docs/MIGRATION.md#050-pineconeclientlistnamespaceswithmetadata-return-shape), [§ internal re-exports (#203)](docs/MIGRATION.md#050-internal-only-re-exports-removed-203), and the [CHANGELOG](CHANGELOG.md#050---2026-07-25). ## Upgrading to 0.4.0 diff --git a/docs/README.md b/docs/README.md index f587083..3e4061d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,5 +14,6 @@ Guides for operators, MCP client authors, and library embedders. | [deprecation-policy.md](./deprecation-policy.md) | Deprecation window, semver, CHANGELOG conventions | | [templates/breaking-change-release-notes.md](./templates/breaking-change-release-notes.md) | GitHub Release body template for breaking versions | | [RELEASING.md](./RELEASING.md) | npm publish via GitHub Releases | +| [release-verification/](./release-verification/) | Post-publish verification and production-readiness sign-offs | The main [README.md](../README.md) remains the high-level overview; deep reference lives here. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index c893998..3f59d1a 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -16,3 +16,7 @@ Packages are published to npm as **`@will-cppa/pinecone-read-only-mcp`**. ## Version source `SERVER_VERSION` is read from `package.json` at runtime so MCP `serverInfo` matches the published package. + +## Post-publish verification + +After a release is on npm, record artifact and CI alignment under [release-verification/](./release-verification/) (see [0.5.0.md](./release-verification/0.5.0.md) for the `v0.5.0` sign-off). From 4f25c943a6193a4b1e635f2cee1b493873b5c393 Mon Sep 17 00:00:00 2001 From: zhao Date: Wed, 29 Jul 2026 06:00:36 +0800 Subject: [PATCH 2/8] resolved link errors --- docs/README.md | 2 +- docs/RELEASING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3e4061d..abe4226 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,6 @@ Guides for operators, MCP client authors, and library embedders. | [deprecation-policy.md](./deprecation-policy.md) | Deprecation window, semver, CHANGELOG conventions | | [templates/breaking-change-release-notes.md](./templates/breaking-change-release-notes.md) | GitHub Release body template for breaking versions | | [RELEASING.md](./RELEASING.md) | npm publish via GitHub Releases | -| [release-verification/](./release-verification/) | Post-publish verification and production-readiness sign-offs | +| [release-verification/README.md](./release-verification/README.md) | Post-publish verification and production-readiness sign-offs | The main [README.md](../README.md) remains the high-level overview; deep reference lives here. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 3f59d1a..fa327c3 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -19,4 +19,4 @@ Packages are published to npm as **`@will-cppa/pinecone-read-only-mcp`**. ## Post-publish verification -After a release is on npm, record artifact and CI alignment under [release-verification/](./release-verification/) (see [0.5.0.md](./release-verification/0.5.0.md) for the `v0.5.0` sign-off). +After a release is on npm, record artifact and CI alignment under [release-verification](./release-verification/README.md) (see [0.5.0 sign-off](./release-verification/0.5.0.md) for `v0.5.0`). From 0a1c7e9f03db0ae4710edbbec4157f37ce51b700 Mon Sep 17 00:00:00 2001 From: zhao Date: Wed, 29 Jul 2026 06:05:05 +0800 Subject: [PATCH 3/8] add release documentations --- docs/README.md | 2 +- docs/RELEASING.md | 2 +- docs/release-verification-0.5.0.md | 98 ++++++++++++++++++++++++++++++ docs/release-verification.md | 9 +++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 docs/release-verification-0.5.0.md create mode 100644 docs/release-verification.md diff --git a/docs/README.md b/docs/README.md index abe4226..91b8ffb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,6 @@ Guides for operators, MCP client authors, and library embedders. | [deprecation-policy.md](./deprecation-policy.md) | Deprecation window, semver, CHANGELOG conventions | | [templates/breaking-change-release-notes.md](./templates/breaking-change-release-notes.md) | GitHub Release body template for breaking versions | | [RELEASING.md](./RELEASING.md) | npm publish via GitHub Releases | -| [release-verification/README.md](./release-verification/README.md) | Post-publish verification and production-readiness sign-offs | +| [release-verification.md](./release-verification.md) | Post-publish verification and production-readiness sign-offs | The main [README.md](../README.md) remains the high-level overview; deep reference lives here. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index fa327c3..0afae2c 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -19,4 +19,4 @@ Packages are published to npm as **`@will-cppa/pinecone-read-only-mcp`**. ## Post-publish verification -After a release is on npm, record artifact and CI alignment under [release-verification](./release-verification/README.md) (see [0.5.0 sign-off](./release-verification/0.5.0.md) for `v0.5.0`). +After a release is on npm, record artifact and CI alignment in [release-verification.md](./release-verification.md) (see [0.5.0 sign-off](./release-verification-0.5.0.md) for `v0.5.0`). diff --git a/docs/release-verification-0.5.0.md b/docs/release-verification-0.5.0.md new file mode 100644 index 0000000..52e57e7 --- /dev/null +++ b/docs/release-verification-0.5.0.md @@ -0,0 +1,98 @@ +# Production readiness sign-off: `@will-cppa/pinecone-read-only-mcp@0.5.0` + +- **Package:** `@will-cppa/pinecone-read-only-mcp` +- **Version:** `0.5.0` +- **Git tag:** `v0.5.0` → `c28e346efafadbfef98891e523d566eebe957dad` (“Updated documents for new release (#236)”) +- **Recorded:** 2026-07-29 (UTC+8 local) +- **Verifier:** Jonathan (@jonathanMLDev), assignee for week 5 verification task +- **Scope:** Verification and sign-off only. No product code changes. `ServerContext` decomposition and legacy facade removal remain deferred past July 2026. + +## Acceptance criteria evidence + +### AC 1 — npm registry version and publish time + +| Field | Value | +| ----- | ----- | +| `npm view … version` | `0.5.0` | +| `dist-tags.latest` | `0.5.0` | +| `time['0.5.0']` (UTC) | `2026-07-24T18:41:39.687Z` | +| CHANGELOG release date | `2026-07-25` ([CHANGELOG.md](../CHANGELOG.md#050---2026-07-25); calendar date vs UTC publish timestamp) | + +Commands used: `npm view @will-cppa/pinecone-read-only-mcp@0.5.0 version gitHead dist-tags time --json` + +### AC 2 — `package.json` in tarball vs tagged tree + +Repository state for verification: **detached HEAD at `v0.5.0`** (`c28e346`). + +Compared packed `package/package.json` from: + +1. Local `npm pack` after `npm ci` and `npm run build` on `v0.5.0` +2. Registry `npm pack @will-cppa/pinecone-read-only-mcp@0.5.0` + +These fields are **identical** in both tarballs: + +- `version`: `0.5.0` +- `files`: `dist`, `README.md`, `LICENSE`, `CHANGELOG.md` +- `exports`: `.`, `./alliance`, `./package.json` (paths under `dist/` as in tag `package.json`) +- `bin.pinecone-read-only-mcp`: `dist/index.js` + +`npm pack --dry-run` on the tag reported **208** packaged files; registry pack lists the same paths (no `src/`). + +### AC 3 — `dist/` contents and hygiene + +- Fresh build on tag: `npm run build` (runs `clean` then `tsc -p tsconfig.build.json`) before pack. +- **No** `*.test.*` (or other test sources) under packed `dist/`. +- **File path set** under `package/` (local vs registry): identical. +- **`dist/**` content:** SHA-256 compared for every file under `dist/` in both tarballs → **0 mismatches** (functional parity of compiled output). +- **Tarball bytes:** full `.tgz` SHA-256 differs between local pack and registry (expected: npm metadata / root doc packaging); registry tarball matches npm `dist.shasum` `f912f88fee5a8499eadac70ddcbf4a25660fbe76`. +- Local verification used **Node v24.11.1**; publish workflow uses **Node 20.x** on Ubuntu ([publish.yml](../.github/workflows/publish.yml)). Despite Node major difference, compiled `dist/` artifacts matched registry byte-for-byte. + +### AC 4 — CI, CodeQL, and Publish workflow (release gate) + +**npm `gitHead`:** `c28e346efafadbfef98891e523d566eebe957dad` — matches `git rev-parse v0.5.0^{commit}`. + +Workflow runs confirmed via the public [GitHub Actions](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions) UI on 2026-07-29 (listed as successful; no failure marker on these runs): + +| Check | Run | Link | +| ----- | --- | ---- | +| **CI** on merge commit `c28e346` (#236) | CI **#480** — “Updated documents for new release (#236)” on `main` | [CI workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/ci.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | +| **CodeQL** on `c28e346` | CodeQL **#505** — same commit on `main` | [CodeQL workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/codeql.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | +| **Publish to npm** for release `v0.5.0` | Publish **#11** — “Release v0.5.0 published” (~3m 25s) | [Publish workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/publish.yml?query=event%3Arelease) | + +Release: [v0.5.0](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/releases/tag/v0.5.0) · [Commit checks](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/commit/c28e346efafadbfef98891e523d566eebe957dad/checks) + +Publish path aligns with [RELEASING.md](./RELEASING.md): `workflow_call` to `ci.yml`, then `npm publish --provenance --access public`; `prepublishOnly` runs `npm run ci`. + +### AC 5 — Production-readiness statement + +**`@will-cppa/pinecone-read-only-mcp@0.5.0` on npm is production-ready** relative to repository tag `v0.5.0`: + +- Registry version and `gitHead` align with tag `c28e346`. +- Packed manifest (`version`, `files`, `exports`, `bin`) matches the tagged `package.json`. +- Published `dist/` matches a clean tag build (no test sources in artifact; `dist/**` hash parity with registry). +- CI (#480), CodeQL (#505), and Publish (#11) for the `v0.5.0` release completed successfully per GitHub Actions (see AC 4). + +No re-publish or version bump is required based on this verification. + +**Registry tarball:** `https://registry.npmjs.org/@will-cppa/pinecone-read-only-mcp/-/pinecone-read-only-mcp-0.5.0.tgz` (`dist.shasum` `f912f88fee5a8499eadac70ddcbf4a25660fbe76`). + +### AC 6 — Pull request and review + +Sign-off is committed under `docs/`. **Requires:** + +- [ ] PR opened with this file and doc index updates ([README.md](./README.md), [RELEASING.md](./RELEASING.md)) +- [ ] ≥ 1 reviewer approval +- [ ] PR URL recorded here after merge: _pending (assignee: branch, commit, push, open PR)_ + +## Commands reference (replay on tag) + +```powershell +git checkout v0.5.0 +npm ci +npm run build +npm pack --dry-run +npm pack +npm pack @will-cppa/pinecone-read-only-mcp@0.5.0 +``` + +Recommended before merge: `npm run docs:link-check` diff --git a/docs/release-verification.md b/docs/release-verification.md new file mode 100644 index 0000000..bd416c5 --- /dev/null +++ b/docs/release-verification.md @@ -0,0 +1,9 @@ +# Release verification + +Post-publish records that npm artifacts match tagged repository state and release CI gates. + +| Version | Sign-off | +| ------- | -------- | +| 0.5.0 | [release-verification-0.5.0.md](./release-verification-0.5.0.md) | + +Add a new `release-verification-X.Y.Z.md` after each production release and link it from this table. From 18beb80c6759c36475d7e8c15dc212eff833e5e2 Mon Sep 17 00:00:00 2001 From: zhao Date: Thu, 30 Jul 2026 02:00:27 +0800 Subject: [PATCH 4/8] update release-verification-0.5.0.md --- docs/release-verification-0.5.0.md | 42 +++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/release-verification-0.5.0.md b/docs/release-verification-0.5.0.md index 52e57e7..1648467 100644 --- a/docs/release-verification-0.5.0.md +++ b/docs/release-verification-0.5.0.md @@ -29,12 +29,33 @@ Compared packed `package/package.json` from: 1. Local `npm pack` after `npm ci` and `npm run build` on `v0.5.0` 2. Registry `npm pack @will-cppa/pinecone-read-only-mcp@0.5.0` -These fields are **identical** in both tarballs: +These fields are **identical** in both tarballs (local pack vs registry pack; no differences in `version`, `files`, `exports`, or `bin`): - `version`: `0.5.0` - `files`: `dist`, `README.md`, `LICENSE`, `CHANGELOG.md` -- `exports`: `.`, `./alliance`, `./package.json` (paths under `dist/` as in tag `package.json`) -- `bin.pinecone-read-only-mcp`: `dist/index.js` +- `bin`: + +```json +{ + "pinecone-read-only-mcp": "dist/index.js" +} +``` + +- `exports` (same object in both packed `package/package.json` files): + +```json +{ + ".": { + "types": "./dist/core/index.d.ts", + "import": "./dist/core/index.js" + }, + "./alliance": { + "types": "./dist/alliance/index.d.ts", + "import": "./dist/alliance/index.js" + }, + "./package.json": "./package.json" +} +``` `npm pack --dry-run` on the tag reported **208** packaged files; registry pack lists the same paths (no `src/`). @@ -63,12 +84,12 @@ Release: [v0.5.0](https://github.com/cppalliance/pinecone-read-only-mcp-typescri Publish path aligns with [RELEASING.md](./RELEASING.md): `workflow_call` to `ci.yml`, then `npm publish --provenance --access public`; `prepublishOnly` runs `npm run ci`. -### AC 5 — Production-readiness statement +### AC 5 — Artifact alignment (npm vs tag `v0.5.0`) -**`@will-cppa/pinecone-read-only-mcp@0.5.0` on npm is production-ready** relative to repository tag `v0.5.0`: +**Artifact verification is complete.** The published npm package matches repository tag `v0.5.0` (`c28e346`) for the checks in AC 1–4: - Registry version and `gitHead` align with tag `c28e346`. -- Packed manifest (`version`, `files`, `exports`, `bin`) matches the tagged `package.json`. +- Packed manifest (`version`, `files`, `exports`, `bin`) matches the tagged `package.json` (see AC 2). - Published `dist/` matches a clean tag build (no test sources in artifact; `dist/**` hash parity with registry). - CI (#480), CodeQL (#505), and Publish (#11) for the `v0.5.0` release completed successfully per GitHub Actions (see AC 4). @@ -76,13 +97,16 @@ No re-publish or version bump is required based on this verification. **Registry tarball:** `https://registry.npmjs.org/@will-cppa/pinecone-read-only-mcp/-/pinecone-read-only-mcp-0.5.0.tgz` (`dist.shasum` `f912f88fee5a8499eadac70ddcbf4a25660fbe76`). +**Process sign-off:** Organizational production-readiness sign-off (AC 6) remains **pending** until this documentation PR is approved and merged. Until then, do not treat the release process as fully signed off in-repo. + ### AC 6 — Pull request and review Sign-off is committed under `docs/`. **Requires:** -- [ ] PR opened with this file and doc index updates ([README.md](./README.md), [RELEASING.md](./RELEASING.md)) +- [x] PR opened with this file and doc index updates ([README.md](./README.md), [RELEASING.md](./RELEASING.md)) — [PR #242](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/pull/242) - [ ] ≥ 1 reviewer approval -- [ ] PR URL recorded here after merge: _pending (assignee: branch, commit, push, open PR)_ +- [ ] Approver and approval date recorded here after review +- [ ] Merged PR URL recorded here after merge (same as #242 when merged) ## Commands reference (replay on tag) @@ -95,4 +119,4 @@ npm pack npm pack @will-cppa/pinecone-read-only-mcp@0.5.0 ``` -Recommended before merge: `npm run docs:link-check` +**Docs link-check:** `npm run docs:link-check` — exit **0** on 2026-07-30 (local, after CodeRabbit doc updates). Add PR CI `quality` job URL here after you push. From 3daa26a3025bf26ebf32a659fa4037e707da232f Mon Sep 17 00:00:00 2001 From: zhao Date: Thu, 30 Jul 2026 03:05:33 +0800 Subject: [PATCH 5/8] fixed date --- docs/release-verification-0.5.0.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/release-verification-0.5.0.md b/docs/release-verification-0.5.0.md index 1648467..3d6aa23 100644 --- a/docs/release-verification-0.5.0.md +++ b/docs/release-verification-0.5.0.md @@ -11,11 +11,11 @@ ### AC 1 — npm registry version and publish time -| Field | Value | -| ----- | ----- | -| `npm view … version` | `0.5.0` | -| `dist-tags.latest` | `0.5.0` | -| `time['0.5.0']` (UTC) | `2026-07-24T18:41:39.687Z` | +| Field | Value | +| ---------------------- | ------------------------------------------------------------------------------------------------------- | +| `npm view … version` | `0.5.0` | +| `dist-tags.latest` | `0.5.0` | +| `time['0.5.0']` (UTC) | `2026-07-24T18:41:39.687Z` | | CHANGELOG release date | `2026-07-25` ([CHANGELOG.md](../CHANGELOG.md#050---2026-07-25); calendar date vs UTC publish timestamp) | Commands used: `npm view @will-cppa/pinecone-read-only-mcp@0.5.0 version gitHead dist-tags time --json` @@ -74,11 +74,11 @@ These fields are **identical** in both tarballs (local pack vs registry pack; no Workflow runs confirmed via the public [GitHub Actions](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions) UI on 2026-07-29 (listed as successful; no failure marker on these runs): -| Check | Run | Link | -| ----- | --- | ---- | -| **CI** on merge commit `c28e346` (#236) | CI **#480** — “Updated documents for new release (#236)” on `main` | [CI workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/ci.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | -| **CodeQL** on `c28e346` | CodeQL **#505** — same commit on `main` | [CodeQL workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/codeql.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | -| **Publish to npm** for release `v0.5.0` | Publish **#11** — “Release v0.5.0 published” (~3m 25s) | [Publish workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/publish.yml?query=event%3Arelease) | +| Check | Run | Link | +| --------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **CI** on merge commit `c28e346` (#236) | CI **#480** — “Updated documents for new release (#236)” on `main` | [CI workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/ci.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | +| **CodeQL** on `c28e346` | CodeQL **#505** — same commit on `main` | [CodeQL workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/codeql.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | +| **Publish to npm** for release `v0.5.0` | Publish **#11** — “Release v0.5.0 published” (~3m 25s) | [Publish workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/publish.yml?query=event%3Arelease) | Release: [v0.5.0](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/releases/tag/v0.5.0) · [Commit checks](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/commit/c28e346efafadbfef98891e523d566eebe957dad/checks) @@ -119,4 +119,4 @@ npm pack npm pack @will-cppa/pinecone-read-only-mcp@0.5.0 ``` -**Docs link-check:** `npm run docs:link-check` — exit **0** on 2026-07-30 (local, after CodeRabbit doc updates). Add PR CI `quality` job URL here after you push. +**Docs link-check:** `npm run docs:link-check` — exit **0** on 2026-07-29 (local). Add PR CI `quality` job URL here after you push. From 7a73debdb2fc57005b3aa759a2042f0c02b1be35 Mon Sep 17 00:00:00 2001 From: zhao Date: Thu, 30 Jul 2026 15:28:24 +0800 Subject: [PATCH 6/8] fixed all reviews --- .gitignore | 3 + docs/deprecation-policy.md | 4 +- docs/release-verification-0.5.0.md | 16 ++-- scripts/docs-link-check.mjs | 123 ++++++++++++++++++++++++++++- 4 files changed, 133 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index be845a0..3140983 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ coverage/ # Misc .npmrc +.temp/ +.tmp/ +cursor_logs/ # Scripts scripts/*.bat diff --git a/docs/deprecation-policy.md b/docs/deprecation-policy.md index 3a39155..633f216 100644 --- a/docs/deprecation-policy.md +++ b/docs/deprecation-policy.md @@ -32,7 +32,7 @@ Renames may ship the **replacement immediately** alongside the deprecated alias APIs deprecated **before** this policy was published follow the removal target recorded in CHANGELOG and source comments at deprecation time. The `paper_number` field on query result rows (use `document_id` instead) was deprecated in **0.2.0** with removal planned no earlier than the **next major** release after `1.0.0`; it will not be removed in a `0.y` minor without an explicit CHANGELOG entry and MIGRATION update. -### Active deprecations - legacy module facades +### Active deprecations: legacy module facades Module-level singleton facades delegate to `getDefaultServerContext()`. Migrate to **`ServerContext`** instance methods via `createServer(config)` and pass `{ context: ctx }` to `setupCoreServer` / `setupAllianceServer`. Deprecated in **`0.3.0`**; earliest removal **`0.5.0`** (per [Deprecation window](#deprecation-window) above). See [MIGRATION.md § Legacy module-facade deprecations](./MIGRATION.md#030-legacy-module-facade-deprecations). @@ -132,7 +132,7 @@ Each breaking bullet should state: - **What changed** — concrete behavior or schema difference. - **Who is affected** — MCP clients, library embedders, operators, etc. -- **Migration** — link to [MIGRATION.md](./MIGRATION.md#anchor) (or “see MIGRATION.md § …”). +- **Migration** — link to the specific [MIGRATION.md](./MIGRATION.md) heading covering the change (or “see MIGRATION.md § …”). ### Deprecated entries diff --git a/docs/release-verification-0.5.0.md b/docs/release-verification-0.5.0.md index 3d6aa23..d5240a3 100644 --- a/docs/release-verification-0.5.0.md +++ b/docs/release-verification-0.5.0.md @@ -65,7 +65,7 @@ These fields are **identical** in both tarballs (local pack vs registry pack; no - **No** `*.test.*` (or other test sources) under packed `dist/`. - **File path set** under `package/` (local vs registry): identical. - **`dist/**` content:** SHA-256 compared for every file under `dist/` in both tarballs → **0 mismatches** (functional parity of compiled output). -- **Tarball bytes:** full `.tgz` SHA-256 differs between local pack and registry (expected: npm metadata / root doc packaging); registry tarball matches npm `dist.shasum` `f912f88fee5a8499eadac70ddcbf4a25660fbe76`. +- **Tarball bytes:** full `.tgz` SHA-256 differs between local pack and registry. This is expected from tar entry ordering and gzip stream differences across npm/Node versions (local pack used Node v24.11.1; registry was published from CI on Node 20.x) — **not** content drift: `package.json`, `README.md`, `CHANGELOG.md`, and `LICENSE` are byte-identical between the packed tarball and `git show v0.5.0:` for each file (verified independently of the local `npm pack`, so this holds regardless of the packer's Node version). Registry tarball matches npm `dist.shasum` `f912f88fee5a8499eadac70ddcbf4a25660fbe76`. - Local verification used **Node v24.11.1**; publish workflow uses **Node 20.x** on Ubuntu ([publish.yml](../.github/workflows/publish.yml)). Despite Node major difference, compiled `dist/` artifacts matched registry byte-for-byte. ### AC 4 — CI, CodeQL, and Publish workflow (release gate) @@ -74,11 +74,13 @@ These fields are **identical** in both tarballs (local pack vs registry pack; no Workflow runs confirmed via the public [GitHub Actions](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions) UI on 2026-07-29 (listed as successful; no failure marker on these runs): -| Check | Run | Link | -| --------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **CI** on merge commit `c28e346` (#236) | CI **#480** — “Updated documents for new release (#236)” on `main` | [CI workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/ci.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | -| **CodeQL** on `c28e346` | CodeQL **#505** — same commit on `main` | [CodeQL workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/codeql.yml?query=commit%3Ac28e346efafadbfef98891e523d566eebe957dad) | -| **Publish to npm** for release `v0.5.0` | Publish **#11** — “Release v0.5.0 published” (~3m 25s) | [Publish workflow](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/workflows/publish.yml?query=event%3Arelease) | +| Check | Run | Link | +| ---------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **CI** on merge commit `c28e346` (#236) | CI **#480** — “Updated documents for new release (#236)” on `main` | [Run #480](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/runs/30117302054) — Success | +| **CodeQL** on `c28e346` | CodeQL **#505** — same commit on `main` | [Run #505](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/runs/30117301927) — Success | +| **Publish to npm** for release `v0.5.0` | Publish **#11** — “Release v0.5.0 published” (~3m 25s) | [Run #11](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions/runs/30117705749) — Success | + +Direct run permalinks (not `?query=` filters, which GitHub Actions silently ignores for this UI and would otherwise land on the unfiltered run list). Release: [v0.5.0](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/releases/tag/v0.5.0) · [Commit checks](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/commit/c28e346efafadbfef98891e523d566eebe957dad/checks) @@ -119,4 +121,4 @@ npm pack npm pack @will-cppa/pinecone-read-only-mcp@0.5.0 ``` -**Docs link-check:** `npm run docs:link-check` — exit **0** on 2026-07-29 (local). Add PR CI `quality` job URL here after you push. +**Docs link-check:** `npm run docs:link-check` — exit **0** on 2026-07-30 (local, after extending the checker with heading-anchor validation and fixing the anchors it flagged). **Pending:** record the PR #242 `quality` job run URL here once CI finishes on the pushed commit — see the [PR #242 checks tab](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/pull/242/checks). diff --git a/scripts/docs-link-check.mjs b/scripts/docs-link-check.mjs index 7f93db0..8ee051e 100644 --- a/scripts/docs-link-check.mjs +++ b/scripts/docs-link-check.mjs @@ -1,11 +1,17 @@ /** * Run markdown-link-check once for README, CHANGELOG, and every *.md under docs/. * Avoids per-file `npx` invocations (slow / flaky under registry hiccups). + * + * markdown-link-check verifies that link *targets* (files/URLs) exist, but does not + * reliably validate heading *fragments* (`file.md#some-heading`), especially across + * files (tcort/markdown-link-check#212, #225). `checkHeadingAnchors` below closes that + * gap with a self-contained GitHub-slug implementation (no extra dependency) so a + * renamed/retitled heading that leaves a dangling `#anchor` fails CI. */ import { spawnSync } from 'node:child_process'; -import { readdirSync, statSync } from 'node:fs'; -import { join } from 'node:path'; +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; /** @param {string} dir @returns {string[]} */ function walkMarkdownFiles(dir) { @@ -25,11 +31,120 @@ function walkMarkdownFiles(dir) { const paths = ['README.md', 'CHANGELOG.md', ...walkMarkdownFiles('docs')]; +/** + * ASCII + common Unicode punctuation stripped by GitHub's heading slugger + * (verified against `github-slugger@2` output for every heading in this repo's + * docs). Deliberately excludes `-` and `_`, which GitHub preserves. + */ +const SLUG_STRIP_RE = /[!-,./:-@[-^`{-~\u00A1-\u00BF\u00D7\u00F7\u2000-\u206F\u2190-\u21FF]/g; + +/** @param {string} text @returns {string} */ +function stripInlineMarkdown(text) { + const codeSpans = []; + let out = text.replace(/`([^`]*)`/g, (_m, inner) => { + codeSpans.push(inner); + return `\u0000${codeSpans.length - 1}\u0000`; + }); + out = out + .replace(/\*\*([^*]*)\*\*/g, '$1') + .replace(/\*([^*]*)\*/g, '$1') + .replace(/_([^_]*)_/g, '$1'); + return out.replace(/\u0000(\d+)\u0000/g, (_m, i) => codeSpans[Number(i)]); +} + +/** @param {string} text @returns {string} */ +function slugify(text) { + return stripInlineMarkdown(text).toLowerCase().replace(SLUG_STRIP_RE, '').replace(/ /g, '-'); +} + +/** @param {string} content @returns {Set} */ +function headingSlugs(content) { + const slugs = new Set(); + const occurrences = Object.create(null); + let inCodeBlock = false; + for (const line of content.split(/\r?\n/)) { + if (/^```/.test(line.trim())) { + inCodeBlock = !inCodeBlock; + continue; + } + if (inCodeBlock) continue; + const m = line.match(/^(#{1,6})\s+(.*)$/); + if (!m) continue; + const base = slugify(m[2]); + let slug = base; + if (Object.prototype.hasOwnProperty.call(occurrences, base)) { + occurrences[base]++; + slug = `${base}-${occurrences[base]}`; + } else { + occurrences[base] = 0; + } + slugs.add(slug); + } + return slugs; +} + +/** @param {string} content @returns {Array<{line: number, target: string}>} */ +function extractFragmentLinks(content) { + const links = []; + const linkRe = /\[[^\]]*\]\(([^)]+)\)/g; + let inCodeBlock = false; + content.split(/\r?\n/).forEach((line, idx) => { + if (/^```/.test(line.trim())) { + inCodeBlock = !inCodeBlock; + return; + } + if (inCodeBlock) return; + let m; + while ((m = linkRe.exec(line)) !== null) { + const target = m[1].trim(); + if (/^https?:\/\//.test(target) || target.startsWith('mailto:')) continue; + if (!target.includes('#')) continue; + links.push({ line: idx + 1, target }); + } + }); + return links; +} + +/** @returns {string[]} human-readable failure descriptions */ +function checkHeadingAnchors() { + const slugsByFile = new Map(); + for (const p of paths) { + slugsByFile.set(resolve(p), headingSlugs(readFileSync(p, 'utf8'))); + } + + const failures = []; + for (const p of paths) { + const abs = resolve(p); + const content = readFileSync(p, 'utf8'); + for (const { line, target } of extractFragmentLinks(content)) { + const hashIdx = target.indexOf('#'); + const filePart = target.slice(0, hashIdx); + const anchor = target.slice(hashIdx + 1); + const targetAbs = filePart === '' ? abs : resolve(dirname(abs), filePart); + const slugs = slugsByFile.get(targetAbs); + if (!slugs) continue; // target file outside the checked set (e.g. not markdown); skip + if (!slugs.has(anchor)) { + failures.push(`${p}:${line} -> ${target} (no heading slug "${anchor}" in ${relative('.', targetAbs)})`); + } + } + } + return failures; +} + const shell = process.platform === 'win32'; -const r = spawnSync( +const linkResult = spawnSync( 'npx', ['--yes', 'markdown-link-check@3', '-c', '.markdown-link-check.json', ...paths], { stdio: 'inherit', shell } ); +const linkExit = linkResult.status === null ? 1 : linkResult.status; + +const anchorFailures = checkHeadingAnchors(); +if (anchorFailures.length > 0) { + console.error(`\nERROR: ${anchorFailures.length} dead heading anchor(s) found!`); + for (const f of anchorFailures) console.error(` [✖] ${f}`); +} else { + console.log('\nAll heading anchors resolve.'); +} -process.exit(r.status === null ? 1 : r.status); +process.exit(linkExit !== 0 ? linkExit : anchorFailures.length > 0 ? 1 : 0); From f62463fe10e01808a04ac344d93ad79e92dd1ce6 Mon Sep 17 00:00:00 2001 From: zhao Date: Thu, 30 Jul 2026 19:00:27 +0800 Subject: [PATCH 7/8] addressed ai reviews --- scripts/docs-link-check.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/docs-link-check.mjs b/scripts/docs-link-check.mjs index 8ee051e..836d4ba 100644 --- a/scripts/docs-link-check.mjs +++ b/scripts/docs-link-check.mjs @@ -8,10 +8,11 @@ * gap with a self-contained GitHub-slug implementation (no extra dependency) so a * renamed/retitled heading that leaves a dangling `#anchor` fails CI. */ - import { spawnSync } from 'node:child_process'; +import { error, log } from 'node:console'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; +import process from 'node:process'; /** @param {string} dir @returns {string[]} */ function walkMarkdownFiles(dir) { @@ -38,18 +39,21 @@ const paths = ['README.md', 'CHANGELOG.md', ...walkMarkdownFiles('docs')]; */ const SLUG_STRIP_RE = /[!-,./:-@[-^`{-~\u00A1-\u00BF\u00D7\u00F7\u2000-\u206F\u2190-\u21FF]/g; +const CODE_SPAN_PLACEHOLDER = '@@CODESPAN'; + /** @param {string} text @returns {string} */ function stripInlineMarkdown(text) { const codeSpans = []; let out = text.replace(/`([^`]*)`/g, (_m, inner) => { codeSpans.push(inner); - return `\u0000${codeSpans.length - 1}\u0000`; + return `${CODE_SPAN_PLACEHOLDER}${codeSpans.length - 1}@@`; }); out = out .replace(/\*\*([^*]*)\*\*/g, '$1') .replace(/\*([^*]*)\*/g, '$1') .replace(/_([^_]*)_/g, '$1'); - return out.replace(/\u0000(\d+)\u0000/g, (_m, i) => codeSpans[Number(i)]); + const placeholderRe = new RegExp(`${CODE_SPAN_PLACEHOLDER}(\\d+)@@`, 'g'); + return out.replace(placeholderRe, (_m, i) => codeSpans[Number(i)]); } /** @param {string} text @returns {string} */ @@ -141,10 +145,10 @@ const linkExit = linkResult.status === null ? 1 : linkResult.status; const anchorFailures = checkHeadingAnchors(); if (anchorFailures.length > 0) { - console.error(`\nERROR: ${anchorFailures.length} dead heading anchor(s) found!`); - for (const f of anchorFailures) console.error(` [✖] ${f}`); + error(`\nERROR: ${anchorFailures.length} dead heading anchor(s) found!`); + for (const f of anchorFailures) error(` [✖] ${f}`); } else { - console.log('\nAll heading anchors resolve.'); + log('\nAll heading anchors resolve.'); } process.exit(linkExit !== 0 ? linkExit : anchorFailures.length > 0 ? 1 : 0); From c7d52c736a4eb8653c9221e5ba4b65714f477d2f Mon Sep 17 00:00:00 2001 From: zhao Date: Thu, 30 Jul 2026 23:50:56 +0800 Subject: [PATCH 8/8] add a test and addressed all reviews --- .gitignore | 1 + CHANGELOG.md | 4 ++ CONTRIBUTING.md | 4 +- docs/CI_CD.md | 2 +- docs/release-verification-0.5.0.md | 23 ++++++++ eslint.config.mjs | 7 +++ package.json | 8 +-- scripts/docs-link-check.mjs | 93 ++++++++++++++++++------------ scripts/docs-link-check.test.mjs | 62 ++++++++++++++++++++ scripts/test-search.ts | 4 +- 10 files changed, 162 insertions(+), 46 deletions(-) create mode 100644 scripts/docs-link-check.test.mjs diff --git a/.gitignore b/.gitignore index 3140983..1125218 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ cursor_logs/ # Scripts scripts/*.bat scripts/*.sh +scripts/fix-core-imports.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ef916..e0ab3d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ## [Unreleased] +### Changed + +- **`docs:link-check`:** validates Markdown heading fragment anchors (GitHub-style slugs) in addition to file/URL existence; unit tests cover slug helpers in `scripts/docs-link-check.mjs`. + ## [0.5.0] - 2026-07-25 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b4ced6..f4fc186 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,9 +28,9 @@ npm run ci | ------ | ------- | | `npm run build` | Clean `dist/` and `tsc` compile | | `npm run typecheck` | `tsc --noEmit` | -| `npm run lint` | ESLint on `src/` | +| `npm run lint` | ESLint on `src/` and `scripts/` | | `npm run lint:fix` | ESLint with `--fix` | -| `npm run format` | Prettier write (`src/**/*.ts`, config JSON) | +| `npm run format` | Prettier write (`src/**/*.ts`, `scripts/**/*.{mjs,ts}`, config JSON) | | `npm run format:check` | Prettier check | | `npm test` | Vitest once | | `npm run test:coverage` | Vitest + coverage thresholds (`vitest.config.ts`) | diff --git a/docs/CI_CD.md b/docs/CI_CD.md index 1243b18..ad1918b 100644 --- a/docs/CI_CD.md +++ b/docs/CI_CD.md @@ -21,7 +21,7 @@ **Job `quality`** -- Ubuntu + Node 20: `npm audit --audit-level=moderate` (continue-on-error) → `npm pack --dry-run` → **`npm run docs:link-check`** (single `npx markdown-link-check` over `README.md`, `CHANGELOG.md`, and all `docs/**/*.md`). +- Ubuntu + Node 20: `npm audit --audit-level=moderate` (continue-on-error) → `npm pack --dry-run` → **`npm run docs:link-check`** (`markdown-link-check` for file/URL targets plus heading-anchor validation in `scripts/docs-link-check.mjs`). --- diff --git a/docs/release-verification-0.5.0.md b/docs/release-verification-0.5.0.md index d5240a3..f4d512b 100644 --- a/docs/release-verification-0.5.0.md +++ b/docs/release-verification-0.5.0.md @@ -119,6 +119,29 @@ npm run build npm pack --dry-run npm pack npm pack @will-cppa/pinecone-read-only-mcp@0.5.0 + +# Extract local and registry tarballs; compare dist/** SHA-256 (expect 0 mismatches) +$localTgz = (Get-ChildItem -Name 'will-cppa-pinecone-read-only-mcp-0.5.0.tgz' | Select-Object -First 1) +$registryTgz = (Get-ChildItem -Name 'will-cppa-pinecone-read-only-mcp-*.tgz' | Where-Object { $_ -ne $localTgz } | Select-Object -First 1) +Remove-Item -Recurse -Force .tmp\verify-local, .tmp\verify-registry -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force .tmp\verify-local, .tmp\verify-registry | Out-Null +tar -xf $localTgz -C .tmp\verify-local +tar -xf $registryTgz -C .tmp\verify-registry +$localDist = '.tmp\verify-local\package\dist' +$registryDist = '.tmp\verify-registry\package\dist' +$localFiles = Get-ChildItem $localDist -Recurse -File | ForEach-Object { $_.FullName.Substring((Resolve-Path $localDist).Path.Length + 1) } +$registryFiles = Get-ChildItem $registryDist -Recurse -File | ForEach-Object { $_.FullName.Substring((Resolve-Path $registryDist).Path.Length + 1) } +Compare-Object $localFiles $registryFiles +$mismatches = @() +foreach ($rel in $localFiles) { + $lHash = (Get-FileHash -Algorithm SHA256 (Join-Path $localDist $rel)).Hash + $rHash = (Get-FileHash -Algorithm SHA256 (Join-Path $registryDist $rel)).Hash + if ($lHash -ne $rHash) { $mismatches += $rel } +} +if ($mismatches.Count -gt 0) { throw "dist hash mismatches: $($mismatches -join ', ')" } +'dist/** SHA-256 parity: 0 mismatches' ``` +On Ubuntu (publish workflow host), after extracting both tarballs under `.tmp/verify-local/package/dist` and `.tmp/verify-registry/package/dist`, compare with `sha256sum` on each relative path; expect identical digests for every file under `dist/`. + **Docs link-check:** `npm run docs:link-check` — exit **0** on 2026-07-30 (local, after extending the checker with heading-anchor validation and fixing the anchors it flagged). **Pending:** record the PR #242 `quality` job run URL here once CI finishes on the pushed commit — see the [PR #242 checks tab](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/pull/242/checks). diff --git a/eslint.config.mjs b/eslint.config.mjs index aa6d2af..0039f47 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,10 +3,12 @@ import tseslint from 'typescript-eslint'; import prettier from 'eslint-config-prettier'; export default tseslint.config( + { ignores: ['scripts/fix-core-imports.mjs'] }, eslint.configs.recommended, ...tseslint.configs.recommended, prettier, { + ignores: ['scripts/**/*.mjs'], rules: { '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': [ @@ -17,5 +19,10 @@ export default tseslint.config( }, ], }, + }, + { + files: ['scripts/**/*.mjs'], + ...eslint.configs.recommended, + languageOptions: { ecmaVersion: 'latest', sourceType: 'module' }, } ); diff --git a/package.json b/package.json index dbf4fb0..de70692 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,10 @@ "benchmark": "tsx benchmarks/latency.ts", "benchmark:smoke": "tsx benchmarks/latency.ts --smoke", "test:search": "tsx scripts/test-search.ts", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", - "format": "prettier --write \"src/**/*.ts\" \"*.json\" \".prettierrc\"", - "format:check": "prettier --check \"src/**/*.ts\" \"*.json\" \".prettierrc\"", + "lint": "eslint src/ scripts/", + "lint:fix": "eslint src/ scripts/ --fix", + "format": "prettier --write \"src/**/*.ts\" \"scripts/**/*.{mjs,ts}\" \"*.json\" \".prettierrc\"", + "format:check": "prettier --check \"src/**/*.ts\" \"scripts/**/*.{mjs,ts}\" \"*.json\" \".prettierrc\"", "docs:link-check": "node scripts/docs-link-check.mjs", "typecheck": "tsc --noEmit", "typecheck:benchmarks": "tsc -p benchmarks/tsconfig.json", diff --git a/scripts/docs-link-check.mjs b/scripts/docs-link-check.mjs index 836d4ba..07cb3d5 100644 --- a/scripts/docs-link-check.mjs +++ b/scripts/docs-link-check.mjs @@ -13,6 +13,7 @@ import { error, log } from 'node:console'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; import process from 'node:process'; +import { fileURLToPath } from 'node:url'; /** @param {string} dir @returns {string[]} */ function walkMarkdownFiles(dir) { @@ -30,7 +31,10 @@ function walkMarkdownFiles(dir) { return out; } -const paths = ['README.md', 'CHANGELOG.md', ...walkMarkdownFiles('docs')]; +/** @returns {string[]} */ +export function defaultMarkdownPaths() { + return ['README.md', 'CHANGELOG.md', ...walkMarkdownFiles('docs')]; +} /** * ASCII + common Unicode punctuation stripped by GitHub's heading slugger @@ -42,27 +46,24 @@ const SLUG_STRIP_RE = /[!-,./:-@[-^`{-~\u00A1-\u00BF\u00D7\u00F7\u2000-\u206F\u2 const CODE_SPAN_PLACEHOLDER = '@@CODESPAN'; /** @param {string} text @returns {string} */ -function stripInlineMarkdown(text) { +export function stripInlineMarkdown(text) { const codeSpans = []; let out = text.replace(/`([^`]*)`/g, (_m, inner) => { codeSpans.push(inner); return `${CODE_SPAN_PLACEHOLDER}${codeSpans.length - 1}@@`; }); - out = out - .replace(/\*\*([^*]*)\*\*/g, '$1') - .replace(/\*([^*]*)\*/g, '$1') - .replace(/_([^_]*)_/g, '$1'); + out = out.replace(/\*\*([^*]*)\*\*/g, '$1').replace(/\*([^*]*)\*/g, '$1'); const placeholderRe = new RegExp(`${CODE_SPAN_PLACEHOLDER}(\\d+)@@`, 'g'); return out.replace(placeholderRe, (_m, i) => codeSpans[Number(i)]); } /** @param {string} text @returns {string} */ -function slugify(text) { +export function slugify(text) { return stripInlineMarkdown(text).toLowerCase().replace(SLUG_STRIP_RE, '').replace(/ /g, '-'); } /** @param {string} content @returns {Set} */ -function headingSlugs(content) { +export function headingSlugs(content) { const slugs = new Set(); const occurrences = Object.create(null); let inCodeBlock = false; @@ -87,10 +88,24 @@ function headingSlugs(content) { return slugs; } +/** @param {string} line @param {number} lineNo @param {Array<{line: number, target: string}>} links */ +function extractFragmentLinksFromLine(line, lineNo, links) { + const linkRe = /\[[^\]]*\]\(([^)]+)\)/g; + line.split('`').forEach((segment, i) => { + if (i % 2 === 1) return; + let m; + while ((m = linkRe.exec(segment)) !== null) { + const target = m[1].trim(); + if (/^https?:\/\//.test(target) || target.startsWith('mailto:')) continue; + if (!target.includes('#')) continue; + links.push({ line: lineNo, target }); + } + }); +} + /** @param {string} content @returns {Array<{line: number, target: string}>} */ -function extractFragmentLinks(content) { +export function extractFragmentLinks(content) { const links = []; - const linkRe = /\[[^\]]*\]\(([^)]+)\)/g; let inCodeBlock = false; content.split(/\r?\n/).forEach((line, idx) => { if (/^```/.test(line.trim())) { @@ -98,19 +113,13 @@ function extractFragmentLinks(content) { return; } if (inCodeBlock) return; - let m; - while ((m = linkRe.exec(line)) !== null) { - const target = m[1].trim(); - if (/^https?:\/\//.test(target) || target.startsWith('mailto:')) continue; - if (!target.includes('#')) continue; - links.push({ line: idx + 1, target }); - } + extractFragmentLinksFromLine(line, idx + 1, links); }); return links; } -/** @returns {string[]} human-readable failure descriptions */ -function checkHeadingAnchors() { +/** @param {string[]} [paths] @returns {string[]} human-readable failure descriptions */ +export function checkHeadingAnchors(paths = defaultMarkdownPaths()) { const slugsByFile = new Map(); for (const p of paths) { slugsByFile.set(resolve(p), headingSlugs(readFileSync(p, 'utf8'))); @@ -126,29 +135,41 @@ function checkHeadingAnchors() { const anchor = target.slice(hashIdx + 1); const targetAbs = filePart === '' ? abs : resolve(dirname(abs), filePart); const slugs = slugsByFile.get(targetAbs); - if (!slugs) continue; // target file outside the checked set (e.g. not markdown); skip + if (!slugs) continue; if (!slugs.has(anchor)) { - failures.push(`${p}:${line} -> ${target} (no heading slug "${anchor}" in ${relative('.', targetAbs)})`); + failures.push( + `${p}:${line} -> ${target} (no heading slug "${anchor}" in ${relative('.', targetAbs)})` + ); } } } return failures; } -const shell = process.platform === 'win32'; -const linkResult = spawnSync( - 'npx', - ['--yes', 'markdown-link-check@3', '-c', '.markdown-link-check.json', ...paths], - { stdio: 'inherit', shell } -); -const linkExit = linkResult.status === null ? 1 : linkResult.status; - -const anchorFailures = checkHeadingAnchors(); -if (anchorFailures.length > 0) { - error(`\nERROR: ${anchorFailures.length} dead heading anchor(s) found!`); - for (const f of anchorFailures) error(` [✖] ${f}`); -} else { - log('\nAll heading anchors resolve.'); +function main() { + const paths = defaultMarkdownPaths(); + const shell = process.platform === 'win32'; + const linkResult = spawnSync( + 'npx', + ['--yes', 'markdown-link-check@3', '-c', '.markdown-link-check.json', ...paths], + { stdio: 'inherit', shell } + ); + const linkExit = linkResult.status === null ? 1 : linkResult.status; + + const anchorFailures = checkHeadingAnchors(paths); + if (anchorFailures.length > 0) { + error(`\nERROR: ${anchorFailures.length} dead heading anchor(s) found!`); + for (const f of anchorFailures) error(` [✖] ${f}`); + } else { + log('\nAll heading anchors resolve.'); + } + + process.exit(linkExit !== 0 ? linkExit : anchorFailures.length > 0 ? 1 : 0); } -process.exit(linkExit !== 0 ? linkExit : anchorFailures.length > 0 ? 1 : 0); +const isMain = + process.argv[1] && resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1]); + +if (isMain) { + main(); +} diff --git a/scripts/docs-link-check.test.mjs b/scripts/docs-link-check.test.mjs new file mode 100644 index 0000000..feec830 --- /dev/null +++ b/scripts/docs-link-check.test.mjs @@ -0,0 +1,62 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { describe, expect, it } from 'vitest'; +import { + checkHeadingAnchors, + extractFragmentLinks, + headingSlugs, + slugify, +} from './docs-link-check.mjs'; + +describe('slugify', () => { + it('preserves inline code and strips version punctuation', () => { + expect(slugify('0.5.0: `list_sources` response shape')).toBe('050-list_sources-response-shape'); + }); + + it('slugifies em-dash headings', () => { + expect(slugify('AC 1 — npm registry version and publish time')).toBe( + 'ac-1--npm-registry-version-and-publish-time' + ); + }); + + it('slugifies colon-separated deprecation heading', () => { + expect(slugify('Active deprecations: legacy module facades')).toBe( + 'active-deprecations-legacy-module-facades' + ); + }); +}); + +describe('headingSlugs', () => { + it('deduplicates repeated headings with numeric suffixes', () => { + const content = '# Title\n## Section\n## Section\n'; + expect(headingSlugs(content)).toEqual(new Set(['title', 'section', 'section-1'])); + }); +}); + +describe('extractFragmentLinks', () => { + it('collects relative fragment links outside inline code', () => { + const content = 'See [docs](./other.md#anchor) for details.\n'; + expect(extractFragmentLinks(content)).toEqual([{ line: 1, target: './other.md#anchor' }]); + }); + + it('ignores fragment links inside inline code spans', () => { + const content = 'Example `[foo](#bar)` in prose.\n'; + expect(extractFragmentLinks(content)).toEqual([]); + }); +}); + +describe('checkHeadingAnchors', () => { + it('reports missing anchors and accepts valid ones', () => { + const dir = mkdtempSync(join(tmpdir(), 'docs-link-check-')); + const source = join(dir, 'source.md'); + const target = join(dir, 'target.md'); + writeFileSync(source, '[link](./target.md#good)\n[bad](./target.md#missing)\n'); + writeFileSync(target, '## Good\n'); + + const failures = checkHeadingAnchors([source, target]); + expect(failures).toHaveLength(1); + expect(failures[0]).toContain('#missing'); + expect(failures[0]).not.toContain('#good'); + }); +}); diff --git a/scripts/test-search.ts b/scripts/test-search.ts index 2b401d9..53e7eb0 100644 --- a/scripts/test-search.ts +++ b/scripts/test-search.ts @@ -172,9 +172,7 @@ async function test() { console.log( `⚠️ Keyword search skipped: sparse index has no namespaces (or index unavailable).` ); - console.log( - ` Ensure the sparse index (PINECONE_INDEX_NAME-sparse) exists and has data.` - ); + console.log(` Ensure the sparse index (PINECONE_INDEX_NAME-sparse) exists and has data.`); } else { const sparseTestNamespace = sparseNamespaces[0].namespace; console.log(`\n🔤 Test 5: Keyword search (sparse-only index)`);