Repository navigation
Consumer probing: port TypeKit's prober tooling (route A), or BLite's lightweight way with hand-written axes and MSTest probe sweeps (route B) #105
Description
Two routes
This issue records both ways of getting consumer probing here. Neither is started; the choice is open.
- Route A, port the tooling (the inventory below):
tools/Prober, an MSTest verdict and adapter, the harness,.claude/probe-axes.md, the probe-triage skill. About 5,000 lines over 3 to 4 PRs before the first round. Gives replay per commit by coverage footprint, pairwise covering arrays and verdicts by lookup. - Route B, the BLite way (the comment of 2026-10-18 below, with the evidence): no tooling. A hand-written axes list in
test/oracles/axes.md, probe classes as ordinary MSTestDynamicDatasweeps over the product of those axes, each registered inoracles.jsonwith a seed and named in theheldByof the claims it covers, findings throughcandidates.jsonas today. About one agent session per probe class. In BLite this produced 28 issues in 14 hours; it gives up replay by footprint, pairwise arrays and the ledger.
ValueRanges already has the rest of BLite's machinery (#54): claims, seeds, the candidate registry, trailers. Route B adds only the probes and the axes.
Problem
The /probe skill and the prober agent (consumer probes over a pairwise covering array of axes, replayed per commit with no model) can't run here. They need more than .claude/probe-axes.md: a repository without tools/Prober and tools/probe/ "has no probing set up", and the agent stops. All of that tooling lives only in CodoMetis.TypeKit, and it's written for xunit v3 and TypeKit's knowledge base. This repository uses MSTest 4 and has neither design/knowledge/ nor ADRs.
This issue records what a port would take, so it can be weighed later. Nothing has been ported, and no probing round has run.
What probing would add over the sweeps we already have
The claim catalogue, ShapeMatrixParityTests and the differential fuzzing each check the cases someone wrote down: one claim, one shape pair, one generator. Probing checks every pair of consumer settings (JsonSerializerOptions members by reflection, culture, TypeConverter/configuration binding, MVC vs minimal-API binding, Npgsql DisableDateTimeInfinityConversions, BCL vs NodaTime element, Native AOT) against each documented claim. A coverage footprint then re-runs only the probes a commit touched.
Inventory (in TypeKit) and what each piece needs here
| Piece | TypeKit size | Port effort | Adaptation |
|---|---|---|---|
tools/Prober (ledger, universe, covering arrays, replay, round, gate, seeds, lock, schemas) |
~3,900 lines + 15 JSON schemas | medium | BCL only and stack-neutral apart from ProbeAdapter.cs (the .NET adapter, configured from the axes file's replay object). Change the root marker from CodoMetis.TypeKit.slnx to CodoMetis.ValueRanges.slnx (Program.cs). SourceLint/register validate Cite.Fact/Cite.Record against design/knowledge/ and design/adr/; either drop those cite kinds or point them at test/oracles/ (claims, candidates). Its tests (CodoMetis.TypeKit.Prober.Tests) come along |
tools/Shared + tools/Directory.Build.{props,targets} (source fingerprint, so a stale tool refuses to run) |
~260 lines + 2 MSBuild files | small | Change the root marker. The tools need a slnf or solution entries, and the conventions tests must not treat tools/ projects as packages |
Verdict tool (tools/BuildVerdict: one-line build/test verdict, exit codes, failing tests by name) |
~780 lines | medium | Coupled to xunit v3 (it strips XunitException:, handles xunit v3's refusal of mixed filter kinds). Needs MSTest/MTP output parsing and the MTP --filter syntax. Alternatively, point the adapter's build/test templates straight at dotnet test and drop the tool |
Coverage footprint (Microsoft.Testing.Extensions.CodeCoverage injected through CustomAfterMicrosoftCommonTargets) |
config only | small | MSTest 4 runs on MTP, so the same extension and cobertura output should work. Check it against the probe project |
Probe harness (Row, Evidence, Check, Rec.Run, NoteKeys, Cite, Rows.Data, Registry, ReplayProbe) |
~1,000 generic lines of the ~23,000 in tools/probe/corpus/harness/ (the rest is TypeKit's generated shapes, hosts and EF/JSON helpers) |
medium-large | Rewrite for MSTest: [Theory, MemberData] becomes [DataTestMethod, DynamicData], and skip-with-reasons becomes Assert.Inconclusive, which AGENTS.md forbids for disagreements, so the outcome mapping needs care. NewProbe.cs emits xunit skeletons; give it an MSTest template |
Per-surface helpers (JSON options builder, TypeConverter/config binder, TestServer hosts, EF model builder, culture switch) |
n/a | large, over several rounds | ValueRanges-specific. Existing tests to reuse: test/CodoMetis.ValueRanges.AspNetCore.Tests, LiteralCultureContractTests, ShapeMatrixParityTests, and the EF translation tests. The live PostgreSQL surface needs Docker, so either treat it as out of reach or run it on its own replay family |
Shape generators (tools/probe/generators/*.cs, file-based apps) |
~6 apps | small to start | Range types are fixed, not generated, so the catalogue is data: 5 shapes × 8 range types, RangeSet sizes (0, 1, 2, many), value set families × wrapper arities. Maybe no generator is needed at first |
tools/probe/enumerate.cs (generated axes by reflection) |
56 lines | small | Enumerate JsonSerializerOptions, NpgsqlDataSourceBuilder/NpgsqlDbContextOptionsBuilder and MvcOptions. With no Directory.Packages.props here, DeriveAxes can't take versions from central package management; read them from the csprojs instead, or add CPM |
tools/probe/corpus/ (claims, registry, rows, seeds, dismissed) |
data | small | Seed claims.json from test/oracles/claims.json (each entry already quotes its doc sentence and names the test that holds it) and add surfaces. Probe seeds overlap test/oracles/seeds/*.patch; decide whether one seed directory serves both |
.claude/skills/probe-triage/SKILL.md |
261 lines | small-medium | Step 2 ("a fact in design/knowledge/", docs-query entry KB-NNN, test/CodoMetis.TypeKit.Facts/KB*.cs) has no counterpart here. Map it to test/oracles/candidates.json plus a facts test project, or drop it. Step 4's finding should go through Candidates.Verdict into candidates.json with its issue, as AGENTS.md requires |
.claude/probe-axes.md |
~750 lines in TypeKit | medium | Write it last, once the harness exists. Its replay object configures the adapter |
DocsQuery (tools/DocsQuery, KB lookups) |
separate tool | skip | Only needed if a knowledge base is adopted |
Rough total: about 5,000 lines carried over or rewritten before the first round, then the per-surface helpers. That's realistically 3–4 PRs (Prober + Shared; verdict/adapter + harness; triage skill + corpus seeded from claims.json; axes file + first round).
Out of scope until this is decided
No .claude/probe-axes.md, no probe corpus, no round. The decision not to replicate TypeKit's tooling yet was taken on 2026-10-17.
Activity
CaffeinatedCoder commentedon Oct 17, 2026
The lightweight alternative: how BLite did it without the prober
Evidence: the BLite session transcripts of 2026-10-16/17 (the "TypeKit bug-detection adoption" session and its twelve side sessions), the upstream branches
test/oracle-tests…test/oracle-encryptedandtest/wasm-browser-harness, and the issues on EntglDb/BLite.What BLite decided, and why
On 2026-10-16 the question was the same as this issue's: port TypeKit's bug-finding machinery. The session read TypeKit's own fix history (133 fixes: 89 from review sessions, 10 from the code-analyst agent, 3 real plus 1 false from the prober at about a week and 6,617 lines of tool and harness; harness traps outnumbered product defects roughly ten to one) and recommended against lifting the prober. It kept the parts of the method that had paid off and dropped the tooling:
.claude/probe-axes.mdand axes enumerated by reflection (enumerate.cs)prober rows)not-promised,inapplicable,excludeprober roundFound-By:/Defect-Class:commit trailersproberagent, evidence per rowThe method, step by step
TheoryDataof named cases × the "ways" a case runs), and the row's name carries its coordinates. The product is complete, not pairwise, because it stays small enough to run in minutes:W("count < 9.5 (double bound)", x => x.Count < 9.5)) × {indexed, plain} × {sync, async}, against LINQ-to-Objects.INAPPLICABLEwith a reason and must be listed.Candidateshelper: a row that disagrees and is unlisted fails; listed with an empty issue it fails with[known candidate: <family>]; listed with an issue it passes and logsSKIPPED(xunit 2 has no dynamic skip); a listed row that agrees fails withcandidate fixed: remove the entry.Seeds/candidate-issues.md(rows, repro, expected and actual, mechanism withfile:line, affected versions, fix options) → repro run as a throwawayFactand confirmed from the code → cross-check of open and closed issues and PRs and the README → filed with labels → number into the registry → the fix PR removes the entry and carriesFound-By: OT.Seeds/<Test>.<defect>.patch, a revert of a known fix or a hand-made defect, header says what it breaks).OracleSeedsTests(58 lines) holds in CI without a build that every test has one, it touches onlysrc/, and it still applies;tools/oracle-seeds.cs(78 lines) proves locally that each is caught.tests/BLite.Tests/Oracles/*Tests.cs,Seeds/*.patch,Candidates.cs,OracleSeedsTests.cs,tools/oracle-seeds.cs,docs/ORACLE_TESTS.md(243 lines: why, what, how to use, how detection grows), CONTRIBUTING trailers. Fork-only and never sent upstream:.claude/oracle-catalogue.md(27 lines, written and not-yet-written oracles) and.claude/skills/oracle-triage/SKILL.md.What it produced
28 issues in about 14 wall-clock hours (2026-10-16 21:09 to 2026-10-17 11:22, including a night), 27 of them straight from failing rows:
Plus 7 test PRs (#250, #259, #263, #272, #277, #294, #295), 12 fix PRs (#251–#253, #278–#286), epic #288 with an "oracle coverage by library section" table and a planned README stability matrix. Cost in code: about 5,600 lines on the stacked branch (two of the test files are over 1,300 lines each), plus the dynamic tests, the AOT tool and the browser harness; framework code is 188 lines in total.
What it gave up compared with the tooling
<remarks>states the contract; the release rule is "registry empty and oracle merged" per section.Where ValueRanges stands, and the lightweight route here
ValueRanges already has BLite's machinery, adopted from it in #54:
test/oracles/claims.json(126 claims, 2 unheld),oracles.json(28 sweeps), 24 seeds,candidates.json(8 rows, all with an issue),Candidates.Verdictwith the same three outcomes,tools/oracles.cs seeds|status, trailers,docs/oracles.md. What this repository does not have is the layer BLite's catalogue tests supply: consumer-shaped tests whose rows are the product of the settings a consumer varies. The 10.0.0 bug hunt found #114 ("every entry point"), #116 (variant type × minimal API) and #117 (infinity spelling × another writer) by exactly such combinations, as one-off probes that were not kept.So the lightweight route is not a port; it is three additions to what exists:
test/oracles/axes.md(next toclaims.json; about 60 lines; not.claude/probe-axes.md, which the/probeskill would read and then stop on for lack oftools/Prober). One section per surface family ofclaims.json(literals and JSON, TypeConverter and binding, EF Core, hosting and AOT), each axis with its values and the test that already varies it, and the pairs no test varies. Values come from the existing sweeps where they exist: five cultures (LiteralCultureContractTests), shape pairs (ShapeMatrixParityTests), every type and shape (JsonDefaultContractTests),Npgsql.DisableDateTimeInfinityConversions(the InfinityDisabled project).*ProbeTests,[DataTestMethod]+[DynamicData]over the hand-written product, ending inCandidates.Verdict, registered inoracles.jsonwith a seed like every other oracle, and named in theheldByof the claims they cover. Each class lives in the project that already has the harness: the conventions project for type discovery,AspNetCore.TestsforTestServer, the integration project for a live server. Three first candidates, in order of what the bug hunt showed:object, record property, dictionary value} × {default, camelCase, snake_case,ReferenceHandler.Preserve, source-generated context,NumberHandling}, oracle: the wire form equals the literal (claims J*);Parse,TypeConverter, configuration binding, MVC[FromQuery], minimal API, JSON body} × {invariant, de-DE, th-TH} × every type including a variant-typed parameter, oracle: one value on every path or a loud refusal (Minimal API binds a variant-typed parameter as null for a valid literal and answers 200 #116 is one missing pair of this product);24:00:00, >28 digits} × every parse entry point, oracle:psql -Aton the same text (the bug hunt's second technique; An inverted range literal parses as empty on every entry point; PostgreSQL rejects it #114 and Decimal text beyond 28 significant digits is rounded silently when a literal or JSON array is read #115 are rows of it).candidates.jsonwith the issue,found-by/oracle, the fix PR removes the row. Add BLite's fixed triage order todocs/oracles.mdas one more step ("both paths agree with each other but not with the reference: a semantics question for the docs before an issue").Effort, from BLite's record: about one agent session per probe class including seed and drafts (400 to 900 lines each), and an hour for the axes list. Against this issue's inventory: 3 to 4 PRs and about 5,000 lines before the first round. What the lightweight route forgoes is listed above; the one that matters here is replay by footprint, so each probe class has to stay under about a minute, and the Docker-bound one runs with the integration suite.
[-]Port the probe tooling from CodoMetis.TypeKit (MSTest adapter) so /probe can run here[/-][+]Consumer probing: port TypeKit's prober tooling (route A), or BLite's lightweight way with hand-written axes and MSTest probe sweeps (route B)[/+]CaffeinatedCoder commentedon Oct 17, 2026
Route B, step 1 is implemented in #121 (PR open):
test/oracles/axes.mdandJsonOptionsProbeTests. Steps 2 and 3 (entry point × culture × type inAspNetCore.Tests; literal corpus × entry point against PostgreSQL in the integration project) remain;axes.mdnames them under "pairs no test varies".CaffeinatedCoder commentedon Oct 17, 2026
Step 2 done in #125 (issue #124): entry point × culture × type.
CultureEntryPointProbeTests(conventions project): every discovered type, as union and variant. It reads and writes throughParse,TryParse,TypeDescriptor, configuration binding, JSON and interpolation, under four current cultures. That is 52 rows and ~10,300 checks in 0.3 s.HostCultureProbeTests(AspNetCore project): 107 declared types, bound from a query, nullable query, route value, form field and JSON body. It covers MVC and a minimal API, under four request cultures set byRequestLocalization. That is 52 rows and ~12,400 requests in ~3 s.parser-reads-the-current-culture. Both classes catch it.LiteralProviderContractTestscatches it only on a machine whose own culture isn't invariant.axes.md): entry point × culture × type, current culture × JSON, host × request culture, configuration × every type, host × variant-typed parameter.TryParsesources of a minimal API, answers 200 withnullfor the right shape and for another shape alike. It's registered as 11 candidates against Minimal API binds a variant-typed parameter as null for a valid literal and answers 200 #116. The culture played no part in any row.Still open in
axes.md:AllowInputFormatterExceptionMessagesCaffeinatedCoder commentedon Oct 17, 2026
Every
bugissue against the oracles on main (3ecfa10)This checks all 16
bugissues, open and closed, against every class inoracles.json. Each issue now has a comment with the class, the row, and how I verified it. Run means:src/diff was reverse-applied on 3ecfa10, then every registered oracle was run, live PostgreSQL included.For each reverted miss, the fix's own regression tests failed, which shows the revert put the defect back. Labels follow the decision of 2026-10-18. A catch gets
found-by/oracle: "added" marks a retroactive catch (the issue was found some other way), and "already" marks an issue the machinery found at the time. A miss gets no label.RangeLiteralGrammarContractTests,LiteralGrammarParityTestsSetElementWhiteSpaceContractTests,LiteralGrammarParityTestsDecorativeElementContractTests(5 candidate rows)SetElementWhiteSpaceContractTests,LiteralGrammarParityTests(162 candidate lines)DifferentialFuzzParityTests(8 lines, sampled)DifferentialFuzzParityTests(354 lines)DifferentialFuzzParityTests(2 lines, sampled)HostCultureProbeTests(#125, merged since), 11 candidate rowsCapturedCollectionProbeTests(#138), seedset-collection-sent-as-one-parameterCapturedCollectionProbeTests(#138), 1,540 linesCapturedCollectionProbeTests(#138), candidate, alone:, 1,710 linesSECURITY.mdis not a claim sourceUpdate (after #125 merged and #138 opened): 11 of 16 are caught. #125 adds #116.
CapturedCollectionProbeTests(#138, issue #136) closes the captured collection gap on the EF Core Operand position axis and adds #46, #71 and #76. Its first run also found #137:Any/Countover an empty captured collection throws. Still pending: #114 and #115 (expected from step 3), #117 (another writer's spelling × read path), and #57 and #79 (expected misses).One catalogue error turned up along the way: claim E02 names
ContinuousMultirangeParityTestsas a holder, but that sweep runs no predicate (#134).CaffeinatedCoder commentedon Oct 17, 2026
Route B, step 3 is in PR #135 (#123):
LiteralCorpusProbeTests. It covers literal corpus × entry point × type, judged by PostgreSQL's own text input on the same text.Parse/TryParseover a string and a span, JSON,TypeConverter, the EF JSON document reader), plus the EF write-back as a SQL constant and a JSON document. That is 29,495 cells. The authority ispg_input_is_validand the canonical text, one batched statement per type. The sweep takes about 6 s.DecimalRange, its multirange andDecimalSet. GuidSet accepts whitespace inside an element that PostgreSQL's uuid_in rejects #78 shows as U+00A0 in a uuid.psqlon postgres:17.GuidSetfollowsGuid.Parse, notuuid_in) and StringSet accepts a NUL character, which PostgreSQL text cannot hold #133 (NUL accepted intotext).0x/0o/0b/_integers refused), Value set literals refuse array_in's dimension decoration ([1:1]={…}) and {{}} #128 (array dimension decoration and{{}}refused), LocalDateTime bounds and elements refuse Z and an offset that tsrange and DateTimeRange read #130 (LocalDateTimerefuses an offset), DateTimeOffset bounds and elements refuse offsets beyond ±14:00 that timestamptz reads #131 (DateTimeOffsetrefuses offsets beyond ±14:00) and Document (,max] at the discrete domain boundary: PostgreSQL raises integer out of range #132 ((,max]boundary row undocumented).24:00:00cell agrees: the library refuses, and the server's value can't be aTimeOnly.axes.md): corpus × a value set's JSON array beyond[E], and corpus × dictionary key.CaffeinatedCoder commentedon Oct 17, 2026
Update to step 3 (#135): the corpus grew to 4,715 literals and 30,699 cells. The new rows put infinite temporal bounds next to finite ones (#117 also reproduces on the literal and JSON-document paths) and quoted or escaped whitespace bounds and elements. The retroactive check over every
bugissue finds that this sweep catches #45 and #70 (proven with the reverts of their fixes) and #114, #115, #78 and #117 (open;candidates.jsonrows). Each of those carriesfound-by/oracleand a comment naming its row.