Repository navigation
Fix resolver correctness gaps (decoding, @version, bare search, URL templates) - #33
Merged
Merged
Conversation
- Decoding (L1): the REST handler decoded a query Hono had already decoded, so a literal '%' failed as malformed and %41 became A. REST and MCP now go through resolveQuery: as-is first, one percent-decode only if that does not resolve (SPEC §8). - Unknown @Version (M2): no longer returned as found. Listed-versions source -> related with the available list; case-only mismatch -> corrected to the source's spelling; source with no versions -> corrected, version dropped and explained; version_required miss -> related with versions_available (was not_found). Result SecIDs never carry an unhonoured version. - Bare search (L4): a bare term matching nothing is not_found with search guidance instead of 'Invalid type'. - lookup_table (L5): own-property check. - Notes (L6): namespace- and type-scoped search pass a URL-less item's note. - Regex cache (L2): compiled patterns are cached by source string. - URL templates (L3): non-absolute or non-http(s) templates are refused; substituted values are encoded per URL part so they cannot add ?, #, &, backslash or percent-encoded dot-segments, and literal dot-segments in a path are refused. A value is never re-scanned for placeholders.
resolveQuery can resolve a query twice (as-is, then percent-decoded), and both attempts received the demand capture, so a namespace miss whose query contained a %XX escape wrote two Analytics Engine points. Each attempt now collects its misses and only the attempt whose result is returned records. Tests prove the capture still reaches the namespace-miss branch through REST and all three MCP tools (resolve, lookup, describe). Co-Authored-By: Claude <noreply@anthropic.com>
API-RESPONSE-FORMAT.md: message is present on not_found and error and absent on found and corrected, because the results speak for themselves. The version corrections (a case-only version difference, and a version dropped from a source that lists none) now follow that contract; the correction is visible in the result SecIDs. Co-Authored-By: Claude <noreply@anthropic.com>
A version alias now resolves to the release it names. aicm@1.1#LOG-15 and aicm@v1.1#LOG-15 return secid:.../aicm@1.1.1#LOG-15, the release the data comes from; aicm-caiq@1.1 returns 1.1.0. secid_query still echoes what the client sent. The alias comes from versions_available[].aliases (which carries on_match), with the tree as a fallback: a version node matched by a pattern other than its patterns[0] literal is an alias of that literal. on_match "redirect" returns corrected with empty results and the canonical SecID in the message, as ADR-015 and REGISTRY-JSON-FORMAT.md specify. No registry entry uses redirect yet; a synthetic registry covers it. An unknown version with a subpath is now not_found rather than related, for both version_required sources and sources that list versions_available: the resolver never returns another version's item (ADR-015 rule 8). The message lists the known versions with status, date and aliases, and points to submit_feedback and the SecID issues page. Without a subpath the query is a discovery question and stays related, with the version list. Co-Authored-By: Claude <noreply@anthropic.com>
kurtseifried
force-pushed
the
fix/resolver-correctness
branch
from
September 25, 2026 20:03
46765c0 to
ac20ef6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes since review
Four commits now, rebased onto
mainat 1651737 (after #38 and #39).src/mcp.ts: kept this PR'sresolveQueryrename andmain'sFeedbackRateLimitedErrorimport.src/api.ts:resolveQuery(kv, decoded, { demand: c.env.secid_DEMAND, channel: "rest" }), which is this PR's rename withmain's capture object.src/kv-resolve.ts: this PR'stoRegExpimport next tomain'srecordDemandMiss.mainnow typechecks the tests, so three casts intest/resolver-correctness.test.tsbecameas unknown as.resolveQuerypasses the capture through to the namespace-miss branch. Because it can try a query twice (as-is, then decoded), a miss whose query contained%XXwrote two Analytics Engine points. Each attempt now collects its misses, and only the attempt whose result is returned records them. New tests intest/demand.test.tssend a miss through REST and through the MCPresolve,lookupanddescribetools (viaworker.fetchwith a fakesecid_DEMAND). They check one point each, on the right channel. They fail if the capture is dropped.messageoncorrected(528acfe). API-RESPONSE-FORMAT.md saysmessageis only fornot_foundanderror, so the case-only version correction and the dropped-version correction no longer carry one. The one exception is an ADR-015redirect(see 4), which that ADR and REGISTRY-JSON-FORMAT.md define ascorrectedwith the canonical SecID in the message.aicm@1.1#LOG-15andaicm@v1.1#LOG-15returnsecid:control/cloudsecurityalliance.org/aicm@1.1.1#LOG-15, andaicm-caiq@1.1returns@1.1.0.@1.1.0is unchanged.secid_querystill echoes the input.versions_available[].aliases, which carrieson_match. The tree is the fallback: a version node matched by a pattern other than itspatterns[0]literal counts as an alias of that literal.on_match: "redirect"returnscorrectedwith empty results and the canonical SecID in the message. No registry entry uses it yet, so a synthetic registry tests it.not_found, notrelated. This applies toversion_requiredsources and to sources that listversions_available. ADR-015 rule 8 says the resolver never returns item data from a version the caller did not ask for, and the reviewed version returned the current version's item as "nearest". The message lists the known versions (status, date, aliases) and points tosubmit_feedbackand https://github.com/CloudSecurityAlliance/SecID/issues, as ADR-015 and VERSIONING.md specify. Without a subpath, the query staysrelatedwith the version list.Summary
Review findings L1, M2, L4, L5, L6, L2, L3. Each was reproduced before being fixed.
L1: double decoding. Hono decodes the query string, and
api.tsthen decoded it a second time. Reproduced: a literal%returned "Malformed percent-encoding", and%41becameA. REST and all three MCP tools now go through a newresolveQuery, which follows SPEC §8:foundorcorrected, it percent-decodes once and keeps the decoded result if it ranks higher.secid_queryalways echoes what the client sent.describealso cuts the subpath at an encoded%23.M2: unknown
@versionaccepted as found. Reproduced withsecid:advisory/mitre.org/cve@bogus#CVE-2024-1234. I read SecIDdocs/reference/VERSIONING.md(four outcomes, "Version-Specific Resolution"). This change does not depend on SecID #134 (version aliases).Updated for ADR-015 (see above):
aicm@1.1)foundwith the canonical version in the result SecIDs (@1.1.1), orcorrectedwith no results foron_match: "redirect"versions_available, orversion_requirednot_found, with known versions,submit_feedbackand the issues link. Never another version's item.versions_available, orversion_requiredrelated, with the source and its versions (top10@boguspreviously returnedfound)@REV5)correctedto the source's spelling (@rev5), no messagecorrected: the version is dropped, no messageResult SecIDs never carry a version that wasn't honoured.
L4: bare nonsense returned "Invalid type". A bare term with no
secid:prefix that matches nothing is nownot_found. The message explains what bare search covers and suggests a scoped query. If the term contains a slash, it also lists the valid types. An explicitsecid:frobnicate/...still reports the invalid type.L5:
lookup_table[subpath]now uses an own-property check.L6: namespace-scoped and type-scoped search now pass through
notefor URL-less items, matchingmatchChildrenAndResolve.L2: compiled RegExps are cached by pattern string.
toRegExpis exported and also used by thekv-resolveindex scans. Patterns come only from the registry, so the cache is bounded by registry size.L3: URL templates.
Non-absolute or non-http(s) templates now return
null. The registry has none; I checked every template.Substituted values are encoded according to where they land, in one left-to-right pass. That pass also fixes a small bug where a value containing
{year}was re-expanded.? # % \whitespace, control characters# & + % \whitespace, control characters# % \whitespace, control charactersA literal
.or..segment in a path value makes the resultnull.%is encoded, so%2e%2ecannot smuggle a dot-segment.:/&stay literal in paths, soRHSA-2024:1234, DOIs andA&A-01URLs are unchanged.Two existing
buildUrltests asserted the old pass-through behaviour, and I updated them.Test plan
npm test: 667 passed (16 files). The registry snapshot was built from SecIDmain7518fb9, which includes the AICM 1.1.1 version nodes. It is gitignored.npm run typecheckis clean.resolveQuerydrops the capture (6 failures) or records both attempts (1 failure). I mutation-checked this locally.UNSAFE_IN,DOT_SEGMENT, the placeholder scan) are character classes or anchored alternations with no nested quantifiers.patternLiteralis a linear character scan, not a regex.