You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
test(smoke): a release gate that asserts which dataset comes back (#538)
* test(smoke): a release gate that asserts which dataset comes back
Three releases went out today, two to repair the one before. The verification
between them was a sequence of commands rebuilt from memory, so each pass covered
something different — and every pass covered result counts. v0.4.122 shipped with
counts verified and ranking broken: "22 results" and "the right dataset first" are
different claims, and only the second is what a caller asked for.
`npm run smoke` makes that check a command. Twelve known-answer cases drawn from
real telemetry, each asserting which dataset must come back and naming the
regression it guards; the runner starts the built server over HTTP and calls each
tool the way a client does.
Checked against the defects it claims to catch: reintroducing the v0.4.121
wrapping rule fails 4 of 12 cases, and removing the parser probe from
ckan_find_relevant_datasets — the v0.4.122 regression — fails the case requiring
the two search tools to agree. Wired into CLAUDE.md step 2, before the tag, and
into the DEPLOYMENT.md checklist.
Two things the gate needed:
- the JSON format never exposed the query that actually ran, while Markdown always
has. `effective_query` now appears in ckan_package_search JSON when the server
rewrote the query, absent when it did not. Documented in docs/JSON-OUTPUT.md.
- relevance scores summed unrounded fractions, printing 8.299999999999999.
536 tests, 1 added. Not fixed, and recorded in LOG.md: the ranking model weights
every query term equally, so three datasets tie at 9.7 on the Lecce query and the
right one leads on Solr order. Making `tags` proportional was tried and reverted —
it promotes a dataset whose tags carry two of the three terms over the one that
actually answers the question. Term specificity is a design change, not a patch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEpSWpAwuMaGkfnMpQdqK2
* test(smoke): build before running, fail loudly on setup, compare tools for real
Four review findings:
- `npm run smoke` launched dist/index.js without building it. On a fresh checkout
there is nothing to launch; on a stale one it tests yesterday's code and calls it
a release gate. It builds first now.
- the runner suppressed the child's stderr, ignored its exit, and waited a fixed
1.5s. A missing build or an occupied port surfaced as twelve cases failing with
`fetch failed`, which reads like a broken server rather than a runner that never
started one. It now waits for `tools/list` to answer — the Node HTTP transport
exposes only /mcp, so a health path would never have worked — reports the child's
exit and last stderr lines, and exits 2 for a setup failure against 1 for a real
one.
- the case named "both search tools agree" called one tool and checked its count
against a threshold. It now calls both and compares: with the v0.4.122 regression
reintroduced it reports "ckan_package_search reports 87 and
ckan_find_relevant_datasets reports 0".
- `effective_query` is an externally visible response field and had no OpenSpec
proposal. Added as openspec/changes/add-effective-query-json, `openspec validate
--strict` green, with the scenarios for both branches — present when the query was
rewritten, absent when it was not.
536 tests, 12/12 smoke.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEpSWpAwuMaGkfnMpQdqK2
* test(smoke): readiness must recognise our server, not just a live socket
The probe accepted any 200 from POST /mcp, so a foreign service holding
SMOKE_PORT would have been treated as ready and the whole suite would have run
against it — turning a bind failure into twelve puzzling case failures, which is
exactly what the readiness check was added to prevent.
It now requires `tools/list` to name `ckan_package_search`. Verified by putting a
stub on 3099 that answers tools/list with a different tool: exit 2 and "something
else is answering on port 3099: it replied to tools/list without offering
ckan_package_search".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEpSWpAwuMaGkfnMpQdqK2
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@@ -228,11 +228,11 @@ The server (`src/index.ts`):
228
228
-`stdio.ts`: Standard input/output (Claude Desktop)
229
229
-`http.ts`: HTTP server (remote access)
230
230
231
-
6.**Validation Schema**
231
+
7.**Validation Schema**
232
232
- Uses Zod to validate all tool inputs
233
233
- Each tool has a strict schema that rejects extra parameters
234
234
235
-
7.**Output Formatting**
235
+
8.**Output Formatting**
236
236
- All tools support two formats: `markdown` (default) and `json`
237
237
- Markdown format optimized for human readability
238
238
- JSON format returns compact objects with only essential fields (~70% token reduction vs raw CKAN API)
@@ -460,20 +460,21 @@ npm cannot resolve relative paths from the tarball.
460
460
When releasing a new version:
461
461
462
462
1.**Update version**: Edit the version field in `package.json`, `package-lock.json`, `manifest.json`, **`server.json`** (in `server.json` there are **two** fields: top-level `version` and `packages[0].version` — both must match), **`src/server.ts`** (MCP server version) and **`src/worker.ts`** (`/health` response). The two source files are easy to forget: v0.4.119 shipped with them still at 0.4.118. Check with `grep -rn "<old version>" package.json manifest.json server.json src/server.ts src/worker.ts` — it must return nothing
463
-
2.**Update LOG.md**: Add entry with date and changes
464
-
3.**Commit changes on a branch**: `git checkout -b <type>/<description>` then `git add . && git commit -m "..."`. Code never goes straight to `main`; documentation-only changes may.
465
-
4.**Open a PR and merge it**: `git push -u origin <branch>`, `gh pr create`, wait for green checks, `gh pr merge --squash --delete-branch`, then `git checkout main && git pull`. `main` has a `non_fast_forward` rule: a commit pushed there by mistake needs a revert, not a force-push.
466
-
5.**Create tag**: `git tag -a v0.x.0 -m "..." && git push origin v0.x.0` — ⚠️ **this triggers the npm publish**, see step 9
467
-
6.**Build DXT**: `npm run pack:dxt` → produces `ckan-mcp-server.dxt`
468
-
7.**Build skill**: `npm run pack:skill` → produces `tmp/ckan-mcp.skill`
469
-
8.**Attach to release**: `gh release upload v0.x.0 ckan-mcp-server.dxt tmp/ckan-mcp.skill`
470
-
9.**npm publish happens automatically**: pushing the tag in step 5 starts `.github/workflows/release.yml`, which verifies the tag matches `package.json`, builds, tests, and runs `npm publish --provenance`. **Do not run `npm publish` by hand** — the two paths collide and the loser gets `EPUBLISHCONFLICT`. Watch the run: `gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId')`
471
-
10.**Publish to the MCP Registry** — only **after** the step 9 run has finished green, since the registry validates that the npm version exists: `mcp-publisher login github && mcp-publisher publish`. If login fails with `incorrect_device_code`, the local binary is stale: compare `mcp-publisher --version` against the [latest release](https://github.com/modelcontextprotocol/registry/releases) and update it — the device-auth flow has changed between versions, and the error does not say so
472
-
11.**Deploy to Cloudflare** (if code changed): `npm run deploy`
463
+
2.**Run the release gate**: `npm run smoke` — known-answer search queries against live portals, asserting *which* dataset comes back rather than how many. It must be green before tagging. It exists because v0.4.122 shipped with counts verified and ranking broken: "22 results" and "the right dataset first" are different claims
464
+
3.**Update LOG.md**: Add entry with date and changes
465
+
4.**Commit changes on a branch**: `git checkout -b <type>/<description>` then `git add . && git commit -m "..."`. Code never goes straight to `main`; documentation-only changes may.
466
+
5.**Open a PR and merge it**: `git push -u origin <branch>`, `gh pr create`, wait for green checks, `gh pr merge --squash --delete-branch`, then `git checkout main && git pull`. `main` has a `non_fast_forward` rule: a commit pushed there by mistake needs a revert, not a force-push.
467
+
6.**Create tag**: `git tag -a v0.x.0 -m "..." && git push origin v0.x.0` — ⚠️ **this triggers the npm publish**, see step 10
468
+
7.**Build DXT**: `npm run pack:dxt` → produces `ckan-mcp-server.dxt`
469
+
8.**Build skill**: `npm run pack:skill` → produces `tmp/ckan-mcp.skill`
470
+
9.**Attach to release**: `gh release upload v0.x.0 ckan-mcp-server.dxt tmp/ckan-mcp.skill`
471
+
10.**npm publish happens automatically**: pushing the tag in step 6 starts `.github/workflows/release.yml`, which verifies the tag matches `package.json`, builds, tests, and runs `npm publish --provenance`. **Do not run `npm publish` by hand** — the two paths collide and the loser gets `EPUBLISHCONFLICT`. Watch the run: `gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId')`
472
+
11.**Publish to the MCP Registry** — only **after** the step 10 run has finished green, since the registry validates that the npm version exists: `mcp-publisher login github && mcp-publisher publish`. If login fails with `incorrect_device_code`, the local binary is stale: compare `mcp-publisher --version` against the [latest release](https://github.com/modelcontextprotocol/registry/releases) and update it — the device-auth flow has changed between versions, and the error does not say so
473
+
12.**Deploy to Cloudflare** (if code changed): `npm run deploy`
473
474
474
475
See `docs/DEPLOYMENT.md` for detailed Cloudflare deployment instructions.
475
476
476
-
**Why steps 1 and 10 matter**: `server.json` feeds the official MCP Registry entry, which is what clients installing via the registry resolve. It is *not* updated by `npm publish`. Skipping it silently pins public installs to an old version: between v0.4.83 (2026-03-12) and v0.4.114 (2026-08-03) the registry advertised a build predating the v0.4.108 SSRF remediation, while npm was current. Verify after publishing:
477
+
**Why steps 1 and 11 matter**: `server.json` feeds the official MCP Registry entry, which is what clients installing via the registry resolve. It is *not* updated by `npm publish`. Skipping it silently pins public installs to an old version: between v0.4.83 (2026-03-12) and v0.4.114 (2026-08-03) the registry advertised a build predating the v0.4.108 SSRF remediation, while npm was current. Verify after publishing:
Copy file name to clipboardExpand all lines: LOG.md
+33Lines changed: 33 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,39 @@
2
2
3
3
## 2026-09-05
4
4
5
+
### A release gate that asserts which dataset comes back
6
+
7
+
Three releases went out today, two of them to repair the one before. The verification in
8
+
between was a sequence of commands rebuilt from memory each time, so it covered something
9
+
different on each pass — and what it always covered was result counts. v0.4.122 shipped
10
+
with counts verified and the ranking broken, because "22 results" and "the right dataset
11
+
first" are different claims and only the second is what a caller asked for.
12
+
13
+
`npm run smoke` is that check as a command. Twelve known-answer cases from real telemetry
14
+
in `tests/smoke/cases.json`, each asserting which dataset must come back and carrying the
15
+
regression it guards; the runner starts the built server over HTTP, calls each tool the
16
+
way a client does, and exits non-zero on the first failure.
17
+
18
+
Checked against the defects it claims to catch: reintroducing the v0.4.121 wrapping rule
19
+
fails 4 of 12, and removing the parser probe from `ckan_find_relevant_datasets` — the
20
+
v0.4.122 regression — fails the case requiring the two search tools to agree. Wired into
21
+
the release workflow in `CLAUDE.md` (step 2, before the tag) and the checklist in
22
+
`docs/DEPLOYMENT.md`.
23
+
24
+
Two things the gate needed on the way:
25
+
26
+
- the JSON format never exposed the query that actually ran, while Markdown has always
27
+
shown it. `effective_query` now appears in `ckan_package_search` JSON output when the
28
+
server rewrote the query, and is absent when it did not.
29
+
- relevance scores summed unrounded fractions, printing totals like `8.299999999999999`.
30
+
31
+
Known and not fixed: the ranking model weights every query term equally, so on
32
+
`defibrillatori Comune di Lecce` three datasets tie at 9.7 and the right one leads on
33
+
Solr order rather than on score. Making the `tags` field proportional like the others was
34
+
tried and reverted — it promotes "Elenco patrocini Comune di Lecce", whose tags carry two
35
+
of the three terms against the defibrillator dataset's one. The fix is term specificity,
36
+
a design change, not a patch.
37
+
5
38
### v0.4.123 - relevance scoring and shared parser probe
6
39
7
40
Ships #536: field scoring by share of matched terms, Italian stopwords with acronyms preserved, Unicode-aware term matching, a wider candidate window, the parser probe shared with `ckan_find_relevant_datasets`, and accent-safe filters in `ckan_organization_search` and `ckan_tag_list`. `openspec/specs/ckan-search/spec.md` rewritten around the query-building path.
Copy file name to clipboardExpand all lines: docs/JSON-OUTPUT.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -39,6 +39,7 @@ Error paths respect the requested format too: with `response_format: "json"` a f
39
39
| Field | Type | Notes |
40
40
|-------|------|-------|
41
41
|`count`| number | Total matching datasets |
42
+
|`effective_query`| string | Present **only when the server rewrote the query** — the caller sent `aria OR acqua` and Solr received `text:(aria OR acqua)`. Absent when the query ran unchanged, which is the common case. The Markdown format has always shown this as **Effective Query**. |
0 commit comments