| Date | 2026-07-27 |
| Pod commit | ef49c5d |
| Harness | solidproject/conformance-test-harness:latest, image sha256:601457122b3f, runner 1.2.2 (Quarkus 3.29.2, karate 1.5.1) |
| Bundled tests | solid-contrib/specification-tests v0.0.19 (2024-03-21) |
| Manifests | protocol + web-access-control (the two application.yaml links by default) |
| Command | ./conformance/run.sh |
Two consecutive runs produced identical counts. The numbers below are reproducible.
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 37 | 615 |
| harness's MUST-linked subset | 38 | 649 | 33 | 608 |
| Date | 2026-07-29 |
| Pod commit | 1fe4953 |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 43 | 609 |
Same 41 features, same 652 scenarios. Nothing aborted or got skipped that used to run. Passed rose by 6, not the 1 that dataset-valued resources alone account for. All three of Bucket 3's defects are gone, not just D3:
- D3 (named graphs dropped on write) is the one that work fixed:
content-negotiation-named-graphs(its one active scenario, at line 16) now passes. - D1 (
POSTignoringLink: rel="type") and D2 (noAllowon GET/HEAD) were already fixed, by0336247and3cb0723, both of which land afteref49c5d, the commit the first run measured, and neither run had re-measured the suite since. They are not part of this plan; they surface here because this is the first conformance run since they landed.
Bucket 3 is therefore 0 scenarios, and the totals reconcile exactly:
615 (bucket 1 + 2 + 3 + 4 at first run) − 1 (D3) − 1 (D1) − 4 (D2) = 609, which is what
this run measured. No other feature's pass/fail count moved, checked against every row in
Buckets 1 and 2 individually, not just the aggregate.
The harness's own ResultLogger counts only scenarios attached to a MUST requirement; the
karate totals count every scenario in every feature that ran. Use the karate row. It is
what reports/report.html shows.
After the second run, two feature slices were developed independently from the same base
(1fe4953): PR #1 on main added WAC-Allow, OPTIONS, and CORS response headers;
feat/non-rdf-resources added non-RDF (blob) resource storage. Each branch measured the
conformance suite against its own tree, without the other branch's work present. Neither
number below describes a tree anyone would ship, since each is missing the other branch's
feature, and each one's document names the other's feature as its own rank-1 open gap.
Presenting them as a third and then a fourth run would imply a sequence that never
happened; they are two data points about two different trees, both superseded by the
merged measurement that follows.
| Pod commit | 143584d |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 101 | 551 |
Passed rose by 58 over the second run, and all 58 land in a small set of features:
| Feature | Second run | This measurement | Δ |
|---|---|---|---|
wac/wac-allow/header-exists |
0 / 2 | 2 / 2 | +2 |
wac/wac-allow/user-access-direct |
2 / 14 | 14 / 14 | +12 |
wac/wac-allow/user-access-indirect |
2 / 14 | 14 / 14 | +12 |
wac/wac-allow/public-access-direct |
2 / 14 | 14 / 14 | +12 |
wac/wac-allow/public-access-indirect |
2 / 14 | 14 / 14 | +12 |
protocol/read-write-resource/read-method-support |
4 / 6 | 6 / 6 | +2 |
protocol/cors/preflight |
0 / 2 | 1 / 2 | +1 |
protocol/cors/access-control-headers |
0 / 6 | 5 / 6 | +5 |
All 50 WAC-Allow scenarios that were failing now pass, not merely the != null
assertion that used to stop them, including the case the header's shape depends on,
where the owner reads a resource whose ACL names only Bob and the user group still
reports control. The five CORS features that did not move (simple-requests,
acao-vary, preflight-requests, accept-acah, enumerate-headers) all build a
text/plain fixture in their Background and abort there on this tree, because this
branch has no non-RDF support; their CORS assertions never ran here.
| Pod commit | 7debec9 |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 479 | 173 |
Passed rose by 436 over the second run. The text/plain-fixture abort that used to
take out whole features is gone on this tree, so 540 scenarios that never reached an
assertion before ran to completion: 434 of them passed outright. The WAC
access-decision logic this pod already had turned out to be correct for the large
majority of the ~370 previously-unmeasured access-mode assertions. The other 106 failed
for four causes, none of them new in kind: 63 more PATCH failures (this branch has no
PATCH route), 20 more CORS-header failures and 11 more OPTIONS failures (this branch
has neither, main's work being absent here), and 12 new findings (6 POST
into an accessTo-only container, 6 DELETE via inherited-only access) that this
measurement was the first to surface, because they only exist once a resource can be
created to test them against. content-type-reject also went from 0/3 to 2/3,
independent of unblocking: it was never blocked by the fixture abort, it was wrong
and feat/non-rdf-resources fixed it.
Both measurements are complete records of what each slice did alone; both are superseded by the merge below wherever they disagree with it.
| Date | 2026-07-30 |
| Pod state | merge in progress: main (6654711, PR #1) united with feat/non-rdf-resources (a7f3627), common ancestor 751f7bf |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 557 | 95 |
Same 41 features, same 652 scenarios as every prior run. Of the 41, 27 pass every scenario and 14 have at least one failure. This is the current truth: the tree that ships is neither branch alone, and its number is neither 101 nor 479.
The gain is not a simple sum. 557 − 43 (second run) − 58 (main alone) − 436 (blob alone) = 20. Those 20 scenarios needed both slices to pass and could not appear in
either isolated delta: a CORS-header or OPTIONS-preflight assertion on a text/plain
fixture fails on the missing fixture in main alone and on the missing header in
feat/non-rdf-resources alone, and only passes where both are present. accept-acah
(0 in both isolated measurements' passing counts for this feature) and
enumerate-headers are now 3/3 and 1/1, fully passing only in the merge, which is
this effect directly, not a rounding artefact of the arithmetic.
| Cause | Scenarios | Bucket | Status |
|---|---|---|---|
No PATCH route |
66 | 1 | unchanged gap, both slices left it alone |
CORS Vary header ships as two separate header lines |
10 | 3 | new defect, measurable for the first time |
POST into a container whose grant is accessTo-only |
6 | 3 | unchanged from the blob-alone measurement |
DELETE of a container/fictive resource via inherited-only access |
6 | 2 + 3 | unchanged from the blob-alone measurement (3 defect, 3 pending-decision) |
post-target-not-found (ancestor materialization) |
4 | 2 | unchanged since the first run |
CORS/OPTIONS scheme-rewrite redirect, unreachable over http |
2 | 1 | unchanged in kind, now measured in a second feature |
containment:122 (trailing-slash pair) |
1 | 2 | unchanged since the first run |
| Total | 95 |
66 + 10 + 6 + 6 + 4 + 2 + 1 = 95. Every failure in this run has one of these seven
names; none is left over. See Bucket 4.
Two gaps that both isolated measurements called open are fully resolved in the merge
and do not appear above: WAC-Allow (58/58, every scenario in all five wac-allow
features) and non-RDF/blob resources (the fixture that used to abort 540 scenarios no
longer exists as a gap at all). OPTIONS and CORS response headers are resolved for
every case that does not also depend on the two findings below. See "Did main's and
the blob branch's rankings hold?".
Verified against conformance/.run/harness.log and the per-scenario detail in
conformance/.run/karate/karate-reports/*.html, feature by feature:
writing-resource/post-target-not-found(0/4): all four failures are the unchanged ancestor-materialization behaviour from the first run (:13→201not404;:27,:41,:55→409not404/405). See Bucket 2.cors/preflight(1/2): the one failure (:28) is the@http-redirectrow:[301, 308]expected,204returned, because this pod is dialled overhttpin the harness and the scheme-rewrite this scenario relies on is a no-op. Unreachable without anhttpsdeployment.cors/preflight-requests(1/4)::36(×2) fails onVary;:51fails on the same@http-redirectshape aspreflight:28, newly measured in this feature because it used to abort on thetext/plainfixture.writing-resource/content-type-reject(2/3): the third scenario isPATCHwith noContent-Type: still405, because there is noPATCHroute to reach theContent-Typegate. Same cause as every otherPATCHfailure, not a residual of the content-type-reject defect (which is otherwise fixed; see Bucket 3).writing-resource/containment(3/5)::38isPATCHwithapplication/sparql-update, unchanged from the first run;:122is the trailing-slash-pair rule, unchanged since the first run. See Bucket 2.authentication/header(5/6)::40is the anonymousPATCHrow, unchanged since the first run. The other five anonymous rows still pass,WWW-Authenticateincluded.cors/simple-requests(6/10): all four failures (:53×2,:71×2) are theVarydefect below.cors/acao-vary(8/12): all four failures (:28×2,:58×2) are the sameVarydefect.wac/protected-operation/write-access-public(40/53),write-access-agent(68/84),write-access-bob(68/84). Each splits into exactly three shapes, confirmed against the per-scenario HTML report:POSTinto anaccessTo-only container, 2 scenarios each (resource: Wandresource: A,container: no), thePOST-into-accessTo-only-container defect.PATCHagainst a resource the caller is otherwise entitled to write, 12 / 12 / 9 scenarios, thePATCHgap.DELETE, 2 scenarios each, bothcontainer: W/resource: inherited: oneBob cannot DELETE a fictive resource…(never created, Bucket 2) and oneBob cannot DELETE a container resource…(exists, Bucket 3 defect).2 + 12 + 2 = 16(write-access-agent,write-access-bob);2 + 9 + 2 = 13(write-access-public).16 + 16 + 13 = 45.
wac/protected-operation/read-access-agent,read-access-bob,read-access-public(80/90 each). Every failure (10 per feature, 30 total) ismethod PATCHat the scenario'sretry until responseStatus == 403step:405is never one of the awaited statuses, so karate exhausts its retries. No other cause appears in these three features.
45 (write-access-*) + 30 (read-access-*) = 75 of the 95 sit inside
wac/protected-operation; the other 20 are the CORS/OPTIONS/PATCH/pending-decision
scenarios outside it (post-target-not-found 4, preflight 1, preflight-requests 3,
content-type-reject 1, containment 2, header 1, simple-requests 4, acao-vary 4
4+1+3+1+2+1+4+4 = 20).
Neither ranking describes the merged tree, and each is wrong in a specific, checkable way:
main's ranking still lists non-RDF resources as rank 1, open. It is closed:feat/non-rdf-resourcesshipped it, in the other branch this document now merges.- The blob branch's ranking lists
WAC-Allow, CORS headers, andOPTIONSas its ranks 2, 3, and 4, all open.WAC-Allowis not open:main's work resolves every one of its 58 scenarios once the fixture that used to block them exists, with no residual.OPTIONSis likewise resolved as a route: no scenario in this run fails because the pod answers405toOPTIONS, and the only survivingOPTIONS-shaped failures are the two@http-redirectrows, which are anhttp-vs-httpstest environment limit, not a missing route. CORS response headers are resolved for three of sevencors/*features outright (access-control-headers,accept-acah,enumerate-headers, all passing every scenario) and for everyAccess-Control-Allow-Origin/Access-Control-Expose-Headersassertion in the other four; what remains in those four is theVarydefect below, a narrower and different claim than "no CORS response headers" (25 scenarios in the blob-alone count) ever was.
Ranked by scenarios still failing, the merged tree's gaps are:
| Rank | Gap | Scenarios | Bucket | Note |
|---|---|---|---|---|
| 1 | PATCH not implemented |
66 | 1 | unchanged since the second run; the dominant residual cause, same as the blob-alone measurement found |
| 2 | CORS Vary header ships as two lines |
10 | 3 | new, measurable only once both a non-RDF fixture and a CORS response existed together |
| 3 | DELETE via inherited-only access |
6 | 2 + 3 | unchanged from the blob-alone measurement (3 defect, 3 pending-decision) |
| 3 | POST into an accessTo-only container |
6 | 3 | unchanged from the blob-alone measurement |
| 5 | post-target-not-found |
4 | 2 | unchanged since the first run |
| 6 | CORS/OPTIONS scheme-rewrite redirect (needs https) |
2 | 1 | unchanged in kind; now measured in a second feature |
| 7 | containment:122, trailing-slash pair |
1 | 2 | unchanged since the first run |
66 + 10 + 6 + 6 + 4 + 2 + 1 = 95.
Resolved and off this list entirely: non-RDF resources (540 scenarios, feat/non-rdf-resources),
WAC-Allow (58 scenarios, PR #1), the acl-object family (12 scenarios, resolved by
feat/non-rdf-resources alone: these scenarios never touch WAC-Allow, OPTIONS, or CORS headers, so
they needed only the fixture, not the interaction), content-type-reject's PUT/POST
legs (Bucket 3), and OPTIONS as a route (only its https-only redirect edge case
remains, folded into rank 6 above). accept-acah and enumerate-headers, by contrast,
are the interaction: both build the same fixture but also assert CORS headers, so both
needed the merge specifically.
| Date | 2026-07-30 |
| Pod commit | 9d47b1a (the merged tree above plus this one fix) |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 567 | 85 |
Same 41 features, same 652 scenarios as every prior run. This is a re-measurement of the
merged tree above plus one commit, not a sixth parallel slice: 9d47b1a fixed the Vary
defect the merged run filed (Bucket 3, now resolved below): cors_layer now joins
Origin into the handler's existing Vary value as one field line instead of appending
a second line. 27 → 29 of 41 features now pass every scenario, 12 (was 14) have at least
one failure: cors/simple-requests and cors/acao-vary flip to fully green;
cors/preflight-requests gains two passes but still has one failure, so it stays in the
failing column. Exactly the 10 scenarios the finding predicted moved, and nothing else
did:
| Feature | Fourth run | Fifth run | Δ |
|---|---|---|---|
cors/simple-requests |
6/10 | 10/10 | +4 |
cors/acao-vary |
8/12 | 12/12 | +4 |
cors/preflight-requests |
1/4 | 3/4 | +2 |
557 + 10 = 567. Verified against conformance/.run/harness.log and
conformance/.run/karate/karate-reports/karate-summary-json.txt for this run: every other
feature's pass/fail split, including both write-access-* and read-access-* line
numbers within wac/protected-operation, matches the fourth run exactly, and is not assumed
from the arithmetic.
| Cause | Scenarios | Bucket | Status |
|---|---|---|---|
No PATCH route |
66 | 1 | unchanged gap |
DELETE of a container/fictive resource via inherited-only access |
6 | 2 + 3 | unchanged (3 defect, 3 pending-decision) |
POST into a container whose grant is accessTo-only |
6 | 3 | unchanged |
post-target-not-found (ancestor materialization) |
4 | 2 | unchanged since the first run |
CORS/OPTIONS scheme-rewrite redirect, unreachable over http |
2 | 1 | unchanged in kind |
containment:122 (trailing-slash pair) |
1 | 2 | unchanged since the first run |
| Total | 85 |
66 + 6 + 6 + 4 + 2 + 1 = 85. The Vary row that used to sit here is gone outright, not
folded into another cause; see Bucket 3. Every other row's count is identical to the
merged run's table above; only the Vary line was removed.
Feature-by-feature, only three rows changed from the merged run's list:
cors/simple-requests(10/10): the fourVaryfailures (:53×2,:71×2) are gone. Fully passing; moved to "What passed" below.cors/acao-vary(12/12): the fourVaryfailures (:28×2,:58×2) are gone. Fully passing; moved to "What passed" below.cors/preflight-requests(3/4): the twoVaryfailures at:36are gone;:51still fails, and it is the same@http-redirectshape ascors/preflight:28:[301, 308]expected,204returned, because this pod is dialled overhttpin the harness and the scheme-rewrite the scenario relies on is a no-op. Confirmed directly inharness.logfor this run.:51is the onlyERRORline left under this feature.
Every other feature's failing lines are byte-for-byte the same as the merged run:
confirmed against harness.log line by line for post-target-not-found, preflight,
content-type-reject, containment, authentication/header, and all six
wac/protected-operation features, including the exact scenario-line numbers cited in
the merged run's reconciliation above. None of them touch Vary, so none of them had
anywhere to move.
45 (write-access-*) + 30 (read-access-*) = 75 of the 85 still sit inside
wac/protected-operation, unchanged; the other 10 are post-target-not-found 4,
preflight 1, preflight-requests 1, content-type-reject 1, containment 2, header
1 (4+1+1+1+2+1 = 10). 75 + 10 = 85.
With Vary resolved, PATCH is more dominant than ever. It is now 78% of every
remaining failure (66 of 85), up from 69% in the merged run. What used to be rank 2
(Vary, 10) is gone outright rather than replaced; second and third place are now a
genuine tie, both inherited from the blob-alone measurement and both worth 6 scenarios:
| Rank | Gap | Scenarios | Bucket | Note |
|---|---|---|---|---|
| 1 | PATCH not implemented |
66 | 1 | dominant residual cause, more so now that Vary is gone |
| 2 | DELETE via inherited-only access |
6 | 2 + 3 | tied with rank 2 below (3 defect, 3 pending-decision) |
| 2 | POST into an accessTo-only container |
6 | 3 | tied with rank 2 above |
| 4 | post-target-not-found |
4 | 2 | unchanged since the first run |
| 5 | CORS/OPTIONS scheme-rewrite redirect (needs https) |
2 | 1 | unchanged in kind |
| 6 | containment:122, trailing-slash pair |
1 | 2 | unchanged since the first run |
66 + 6 + 6 + 4 + 2 + 1 = 85. Honestly: there is no second-place gap on its own anymore:
just two 6-scenario defects, neither larger than the other, both already filed in Bucket 3
and unrelated to this fix.
| Date | 2026-07-30 |
| Pod commit | d485135976795d0f5a6029a8f69029462b1993dd (the fifth run's tree plus the N3 Patch work), re-measured at 83f270e |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 632 | 20 |
| harness's MUST-linked subset | 38 | 649 | 621 | 20 |
Four whole-branch review fixes landed after this run: a container's LDP type is re-asserted after a patch, a formula predicate whose object names no formula is refused, and two guards gained the tests that pin them. The suite was run again at 83f270e and produced the identical 632 / 20, with the same seven failing features at the same counts, so every number below holds for the branch as it merges rather than only for the commit it was first measured at.
Same 41 features, same 652 scenarios as every prior run. 34 of the 41 now pass every scenario and 7 have at least one failure, against 29 and 12 in the fifth run. 85 residual failures fall to 20.
One deviation in method, stated up front: this run's karate-reports/ directory holds only
the summary pages (karate-summary.html, karate-summary-json.txt, karate-tags.html,
karate-timeline.html) and no per-feature HTML, which earlier runs used for per-scenario
detail. conformance/reports/report.html carries the same detail as EARL/RDFa: every
scenario's title, every step, and each step's earl#passed / earl#failed, and is what the
per-scenario claims below are checked against, alongside harness.log for the response lines.
Eight features gained; four features that still fail did not move at all. A feature that
gained has no ERROR line left under it in harness.log and its own failed: 0 banner; a
feature that did not move carries the same ERROR lines, at the same scenario line numbers,
as the fifth run. Both were read out of the log, not inferred from the totals.
| Feature | Fifth run | Sixth run | Δ |
|---|---|---|---|
wac/protected-operation/read-access-agent |
80/90 | 90/90 | +10 |
wac/protected-operation/read-access-bob |
80/90 | 90/90 | +10 |
wac/protected-operation/read-access-public |
80/90 | 90/90 | +10 |
wac/protected-operation/write-access-agent |
68/84 | 80/84 | +12 |
wac/protected-operation/write-access-bob |
68/84 | 80/84 | +12 |
wac/protected-operation/write-access-public |
40/53 | 49/53 | +9 |
protocol/authentication/header |
5/6 | 6/6 | +1 |
protocol/writing-resource/content-type-reject |
2/3 | 3/3 | +1 |
10 + 10 + 10 + 12 + 12 + 9 + 1 + 1 = 65, and 567 + 65 = 632. Every one of the 65 is a
PATCH row, accounted for individually in the next section.
Did not move. Named individually rather than left as a remainder:
| Feature | Fifth run | Sixth run | ERROR lines in harness.log |
|---|---|---|---|
protocol/writing-resource/post-target-not-found |
0/4 | 0/4 | :13, :27, :41, :55 |
protocol/writing-resource/containment |
3/5 | 3/5 | :38, :122 |
protocol/cors/preflight |
1/2 | 1/2 | :28 |
protocol/cors/preflight-requests |
3/4 | 3/4 | :51 |
The other 29 features were fully green in the fifth run and are fully green here. Each one's
passedCount equals its scenarioCount in this run's karate-summary-json.txt, checked row
by row, and none of them appears in harness.log's >>> failed features: block. 29 + 5 newly green + 4 unmoved + 3 write-access-* still partially red = 41.
65 of the 66 are green. One is not, and it is not a defect. Every row the fifth run filed
under "No PATCH route" is named here:
| Rows | Where | Outcome |
|---|---|---|
| 30 | read-access-{agent,bob,public}, 10 each |
Green |
| 33 | write-access-{agent,bob} 12 each, write-access-public 9 |
Green |
| 1 | protocol/authentication/header:40 |
Green |
| 1 | protocol/writing-resource/content-type-reject:19 |
Green |
| 1 | protocol/writing-resource/containment:38 |
Still red, moved to Bucket 1 |
30 + 33 + 1 + 1 + 1 = 66.
- The 30
read-access-*rows are theBob cannot PATCH …/Public cannot PATCH …outlines atread-access-{agent,bob}Examples rows95–104andread-access-public99–108. Each sendsContent-Type: text/n3with asolid:InsertDeletePatchbody and runsretry until responseStatus == 403(401for the Public subject). All 30 reportearl#passedon that step, and all three features are90/90with noERRORline.authorizeruns before the body is parsed, which is all these rows ever needed. - The 33
write-access-*rows are the Examples rows atwrite-access-{agent,bob}109–120andwrite-access-public85–93. They split three ways, and all three parts are green, including the 18 that were a measurement rather than a prediction:- 18 rows await
2xx:… can PATCH to a rdf resource …and… can PATCH to a fictive resource …, 6 per feature (109–114,109–114,85–90), underWandAgrants both direct and inherited. These are the only rows in the suite that drive parse → mode check → apply → write end to end, and thefictiveones exercise creation through a patch against an absent target. The design (§14) declined to predict them; they pass. - 9 rows await
403: theControl-only grants (115–117,115–117,91–93). - 6 rows await
401: the Public subject against a non-public resource (118–120inwrite-access-agentandwrite-access-bob;write-access-publichas no such row, which is why it contributes 9 and not 12).
- 18 rows await
authentication/header:40:Unauthenticated user gets an appropriate response on PATCH,Then status 401,earl#passed. The route exists, so the auth layer answers before axum's method dispatch can say405.content-type-reject:19:Server rejects PATCH requests without Content-Type,earl#passed. The patch handler'sContent-Typegate is now reachable and answers400for a non-empty body with no type, the same answerPUTandPOSTalready gave.containment:38:PATCH creates a grandchild resource and intermediate containers, the only row in the suite that sendsapplication/sparql-update. Still red; see Bucket 1.
Neighbouring PATCH rows that were never part of the 66 did not regress. Six rows per
read-access-* feature (Examples 122–124 and 131–133; 126–128 and 135–137 for
read-access-public) send Content-Type: text/plain and accept
[403, 405, 415], or [401, 405, 415] for the Public subject. They passed on 405 when there
was no route; they pass now that there is one. That is 18 scenarios which could have gone red
on a careless gate and did not.
| Cause | Scenarios | Bucket | Status |
|---|---|---|---|
POST into a container whose grant is accessTo-only |
6 | 3 | unchanged |
DELETE of a container/fictive resource via inherited-only access |
6 | 2 + 3 | unchanged (3 defect, 3 pending-decision) |
post-target-not-found (ancestor materialization) |
4 | 2 | unchanged since the first run |
CORS/OPTIONS scheme-rewrite redirect, unreachable over http |
2 | 1 | unchanged in kind |
containment:122 (trailing-slash pair) |
1 | 2 | unchanged since the first run |
containment:38 (application/sparql-update) |
1 | 1 | reclassified from "No PATCH route" |
| Total | 20 |
6 + 6 + 4 + 2 + 1 + 1 = 20. The PATCH-route row that dominated every table since the
second run is gone from this one; nothing was folded into another cause to make it disappear.
Verified line by line, feature by feature:
writing-resource/post-target-not-found(0/4): the same four responses as every run since the first::13→status code was: 201, expected: 404;:27and:41→409not in[404, 405];:55→409not in[404, 405, 415]. Bucket 2.writing-resource/containment(3/5)::122→status code was: 409, expected: 404, … response: another resource already exists whose URI differs from this one only in the trailing slash, unchanged (Bucket 2).:38is theapplication/sparql-updaterow, now Bucket 1.cors/preflight(1/2) andcors/preflight-requests(3/4)::28and:51, both[301, 308] contains responseStatusfailing on204. The@http-redirectrows: this pod is dialled overhttpin the harness, so the scheme rewrite is a no-op. Bucket 1, unchanged.wac/protected-operation/write-access-{agent,bob,public}(80/84, 80/84, 49/53): four failures each, and only two shapes remain now that thePATCHrows are green:POST, 2 per feature.harness.log:When method POST/too many retry attempts: 3atwrite-access-agent:62,write-access-bob:62,write-access-public:47, twice each. The Examples rows are78and80(agent,bob) and62and64(public):… can write a container resource (POST) and cannot read it, when … has no access to the container and W / A access to the resource. TheaccessTo-only defect, Bucket 3.DELETE, 2 per feature.When method DELETE/too many retry attempts: 3atwrite-access-agent:128,write-access-bob:127,write-access-public:100, twice each. Examples rows147/151(agent),146/150(bob),119/123(public): one… cannot DELETE a fictive resource …(Bucket 2) and one… cannot DELETE a container resource …(Bucket 3), bothW access to the container/inherited access to the resource.2 + 2 = 4per feature,4 × 3 = 12.
12 of the 20 sit inside wac/protected-operation; the other 8 are post-target-not-found
4, containment 2, preflight 1, preflight-requests 1. 12 + 8 = 20.
PATCH was 78% of the fifth run's residual. It is now one scenario, and that scenario is a
media type the Protocol never defined. The two 6-scenario findings that shared rank 2 are
promoted to a shared rank 1 unchanged, since neither has been touched since the blob-alone
measurement first surfaced them:
| Rank | Gap | Scenarios | Bucket | Note |
|---|---|---|---|---|
| 1 | POST into an accessTo-only container |
6 | 3 | tied with rank 1 below |
| 1 | DELETE via inherited-only access |
6 | 2 + 3 | tied with rank 1 above (3 defect, 3 pending-decision) |
| 3 | post-target-not-found |
4 | 2 | unchanged since the first run |
| 4 | CORS/OPTIONS scheme-rewrite redirect (needs https) |
2 | 1 | unchanged in kind; needs an https deployment, not a code change |
| 5 | containment:122, trailing-slash pair |
1 | 2 | unchanged since the first run |
| 5 | containment:38, application/sparql-update |
1 | 1 | new to this list; refused by design |
6 + 6 + 4 + 2 + 1 + 1 = 20.
What is left has changed in shape as well as in size. Of the 20, 9 are defects
(Bucket 3), 8 are decisions this pod made deliberately and has written down (Bucket 2),
and 3 are gaps it will not close (Bucket 1: two need https, one needs a media type the
specification does not contain). There is no longer a single dominant cause, and no
unimplemented feature on the list at all.
| Date | 2026-07-30 |
| Pod commit | 35ee057 (RDF 1.2 on the wire, merged with main) |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 632 | 20 |
| harness's MUST-linked subset | 38 | 649 | 621 | 20 |
Identical to the sixth run, and that is the result being reported. RDF 1.2 support adds a
version media-type parameter to responses and three new refusals to the write path; the
question this run answers is whether any of it reached a scenario that was passing before.
None did: same 632 / 20, same seven failing features.
That outcome is designed rather than lucky. the pod emits the version
parameter only on resources that classify above RDF 1.1, and no conformance scenario creates
one, so every response the harness sees is byte-identical to the sixth run's, which is a strict
Content-Type: text/turtle comparison never meets a parameter it did not expect. The design's
first draft stamped the parameter on every RDF response; five of this repo's own tests caught
that before the harness could, and the rule was narrowed (design §5, §11 risk 3). The write-side
refusals (400 for a body richer than it declared, 415 for an unknown version label or one
the store cannot hold, 409 for a write below a resource's own version) are all unreachable
from a 1.1 client, which is what the harness is.
An earlier measurement of this work reported 567 / 85. That number described the pre-merge
branch, which forked before N3 Patch and shape validation reached main; it is superseded by
this one rather than contradicted by it.
The 20 residual failures are unchanged and are the same ones reconciled below.
| Date | 2026-08-03 |
| Pod commit | 247d24f |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 632 | 20 |
| harness's MUST-linked subset | 38 | 649 | 621 | 20 |
Identical to the seventh run, and that is the result being reported. Six feature slices
landed on main between 35ee057 and this commit without the suite being run once:
shape validation, Accept-Put/Accept-Post, auth caching, CLI config and the
persistent store, the request-scoped guard, and change events. This run asks one
question, whether any of them reached a scenario that was passing before, and the answer is no.
Nothing moved in either direction.
Shape validation was the named risk, and it is the one worth stating the evidence for. It
is the only slice of the six that can refuse a write, and the suite writes fixtures
constantly. A refusal inside a feature's Background does not fail one scenario; it aborts
the whole feature before any assertion runs, the mechanism that took out 540 scenarios at the
text/plain fixture through the first two runs. That did not happen: harness.log reports
features: 41 | skipped: 0 and scenarios: 652, the same 41 and 652 as every run since the
first, so every feature reached its assertions. No feature aborted, and none was skipped.
The 20 failures are line-identical to the sixth and seventh runs, not merely equal in
count: every ERROR line in harness.log carries the same feature and scenario line
number as before:
| Feature | Scenario lines | Scenarios | Cause |
|---|---|---|---|
writing-resource/post-target-not-found |
:13, :27, :41, :55 |
4 | ancestor materialization (Bucket 2) |
wac/protected-operation/write-access-{agent,bob,public} |
:62, :62, :47, ×2 each |
6 | POST into an accessTo-only container (Bucket 2, reclassified from Bucket 3) |
wac/protected-operation/write-access-{agent,bob,public} |
:128, :127, :100, ×2 each |
6 | DELETE via inherited-only access (3 Bucket 3, 3 Bucket 2) |
cors/preflight |
:28 |
1 | @http-redirect, needs https (Bucket 1) |
cors/preflight-requests |
:51 |
1 | same |
writing-resource/containment |
:122 |
1 | trailing-slash pair (Bucket 2) |
writing-resource/containment |
:38 |
1 | application/sparql-update (Bucket 1) |
| Total | 20 |
4 + 6 + 6 + 1 + 1 + 1 + 1 = 20. The response lines are unchanged too, quoted from this run's
own log rather than carried over: post-target-not-found:13 → status code was: 201, expected: 404; containment:122 → status code was: 409, expected: 404 … another resource already exists whose URI differs from this one only in the trailing slash; both
@http-redirect rows → [301,308] does not contain 204; containment:38 → did not evaluate to 'true': responseStatus >= 200 && responseStatus < 300, still with no response
line, for the reason Bucket 1 names.
The per-feature counts behind those failures are unchanged as well:
write-access-{agent,bob} at 80/84 each and write-access-public at 49/53, read out of
this run's karate-summary-json.txt. 34 of 41 features pass every scenario, 7 have at least
one failure, the same 34 and 7 as the sixth and seventh runs.
One deviation in method, the same one the sixth run declared: this run's karate-reports/
directory holds only the summary pages, no per-feature HTML. The claims above come from
harness.log's ERROR lines and its per-feature failed: banners, and from
conformance/reports/report.html for per-scenario detail.
The buckets hold the same 20 scenarios, but one row changed bucket after this run, and not
because the pod or the measurement moved, but because the specification was looked up. The
six POST-into-an-accessTo-only-container scenarios moved from Bucket 3 (defect) to
Bucket 2 (pending decision), making the split at that point 3 expected gap, 14 pending
decision, 3 defect: 3 + 14 + 3 = 20, against 3 + 8 + 9 before. The ninth run below
undoes that move for a better reason than the one that made it; the entry carries the whole
story.
| Date | 2026-08-08 |
| Features | Scenarios | Passed | Failed | |
|---|---|---|---|---|
| karate (everything that ran) | 41 | 652 | 638 | 14 |
| harness's MUST-linked subset | 38 | 649 | 627 | 14 |
Same 41 features, same 652 scenarios, skipped: 0. No feature aborted in its
Background, which is the failure mode a change to the write path could have caused. 20
residual failures fall to 14, and every one of the six that moved is a
POST-into-an-accessTo-only-container row:
| Feature | Eighth run | Ninth run | Δ |
|---|---|---|---|
wac/protected-operation/write-access-agent |
80/84 | 82/84 | +2 |
wac/protected-operation/write-access-bob |
80/84 | 82/84 | +2 |
wac/protected-operation/write-access-public |
49/53 | 51/53 | +2 |
632 + 6 = 638. 34 of 41 features pass every scenario, 7 have at least one failure:
the same 34 and 7 as the three runs before, because those three write-access-* features
still carry their DELETE failures and so stay in the failing column.
The change was to post_impl alone: the second authorization, against the newly-allocated
child, is gone. PUT and PATCH are untouched. Both directions of the rule are pinned in
src/http/tests/wac.rs: post_needs_only_access_to_append_on_the_container and
post_is_refused_when_append_is_granted_only_to_the_children. The second is the one that
matters: it fails if the fix is ever "simplified" into dropping the container check too.
| Cause | Scenarios | Bucket | Status |
|---|---|---|---|
DELETE of a container/fictive resource via inherited-only access |
6 | 2 + 3 | unchanged (3 defect, 3 pending-decision) |
post-target-not-found (ancestor materialization) |
4 | 2 | unchanged since the first run |
CORS/OPTIONS scheme-rewrite redirect, unreachable over http |
2 | 1 | unchanged in kind |
containment:122 (trailing-slash pair) |
1 | 2 | unchanged since the first run |
containment:38 (application/sparql-update) |
1 | 1 | unchanged, refused by design |
| Total | 14 |
6 + 4 + 2 + 1 + 1 = 14, and the buckets reconcile: 3 expected gap, 8 pending decision,
3 defect. Bucket 4 stays empty.
Features this pod deliberately does not have, or environment limits the harness cannot clear.
PUT/POST with a non-RDF Content-Type stores the body as a blob (docs/architecture.md, Storage model) instead of
answering 415. Every feature this used to abort now runs to completion.
What used to fail here (first and second run)
| Feature | Scenarios | How it failed |
|---|---|---|
wac/protected-operation/read-access-agent |
90 | callonce common.feature:122, Failed to create …/*.txt, response=415 |
wac/protected-operation/read-access-bob |
90 | same |
wac/protected-operation/read-access-public |
90 | same |
wac/protected-operation/write-access-agent |
84 | same |
wac/protected-operation/write-access-bob |
84 | same |
wac/protected-operation/write-access-public |
53 | same |
protocol/cors/acao-vary |
12 | Background createResource('.txt', …, 'text/plain') → 415 |
protocol/cors/simple-requests |
10 | same |
protocol/cors/preflight-requests |
4 | same |
protocol/cors/accept-acah |
3 | same |
protocol/cors/enumerate-headers |
1 | same |
wac/acl-object/container-none |
3 | callonce setup → 415 |
wac/acl-object/container-access-to |
3 | same |
wac/acl-object/container-default |
3 | same |
wac/acl-object/container-access-to-default |
3 | same |
protocol/cors/access-control-headers:31 |
1 | credentialed POST text/plain → 415 |
protocol/writing-resource/containment:14 |
1 | PUT *.txt text/plain → 415, expected 201 |
protocol/writing-resource/delete-protect-nonempty-container:23 |
1 | fixture createResource('.txt') → 415 |
protocol/writing-resource/delete-remove-containment:50 |
1 | same |
protocol/writing-resource/slash-semantics-exclude:40 |
1 | PUT foo text/plain → 415 |
protocol/writing-resource/slash-semantics-exclude:99 |
1 | POST text/plain → 415 |
wac/protected-operation/acl-propagation:32 |
1 | PUT *.txt text/plain → 415, expected 201 |
The pod emits WAC-Allow on every response the five wac-allow features check:
58 of 58 scenarios pass, not just the != null assertion that used to stop most of them.
Includes the case the header's shape depends on: the owner reads a resource whose ACL
names only Bob, and the user group still reports control.
OPTIONS answers correctly everywhere the suite checks it as a route:
read-write-resource/read-method-support is 6/6, and no cors/* failure in this run
is a 405 on OPTIONS. The two that remain are the @http-redirect scenarios in
cors/preflight:28 and cors/preflight-requests:51: both rewrite the target's scheme to
http and expect a 301/308 redirect to the https original. This pod is dialled
over http in the harness, so the rewrite is a no-op and the request is answered 204.
Not reachable without an https deployment, unchanged in kind since the first run,
newly measured in the second feature because preflight-requests used to abort on its
text/plain fixture.
access-control-headers, accept-acah, and enumerate-headers, 10 of 38 cors/*
scenarios, pass every case, including Access-Control-Allow-Origin,
Access-Control-Expose-Headers (present and not *), and Access-Control-Allow-Headers
mirroring what was requested. Every Access-Control-Allow-Origin and
Access-Control-Expose-Headers assertion in the other four cors/* features also
passes. What remained after the merged run, 10 scenarios across simple-requests,
preflight-requests, and acao-vary, was a single defect in how Vary was emitted, and not
a missing header; 9d47b1a fixed it, and simple-requests/acao-vary are now fully
green (see Bucket 3 and the fifth run above). The only cors/* failures left are the two
@http-redirect scenarios noted above, unrelated to response headers.
PATCH: RESOLVED as a route (N3 Patch); 1 scenario remains, and it is a media type this pod refuses by design
The pod implements N3 Patch: PATCH with a text/n3 body against an RDF document, per
Solid Protocol §5.3.1. patch_impl
(src/http.rs:484) authorizes before it parses, gates on Content-Type, and applies the
patch, including against an absent target, which creates the resource. Accept-Patch: text/n3 travels with Allow on every GET/HEAD/OPTIONS (src/http.rs:182). 65 of the
66 scenarios this section used to list are green as of the sixth run.
What remains is protocol/writing-resource/containment:38, and it is a deliberate gap rather
than a defect:
| Test sends | PATCH with Content-Type: application/sparql-update and body INSERT DATA { <#hello> <#linked> <#world> . }, asserting responseStatus >= 200 && responseStatus < 300 |
| Pod does | 415, since the Content-Type gate accepts text/n3 and nothing else (src/http.rs:509-511) |
| Why | application/sparql-update does not appear in the Solid Protocol at all: not as a MUST, not as a MAY, not as a mention. It is pre-N3-Patch ecosystem behaviour that the bundled specification-tests v0.0.19 still encodes. Accepting it would mean executing a client-authored database command against a store holding every resource, every ACL, and the server's own system graphs, separated from a DROP ALL only by a rejection list that must stay exhaustive against a spargebra AST that may gain a variant in any minor release (docs/decisions.md, ADR-8). This is the only row in the whole suite that uses this media type. Adding it later remains possible and would be its own design. |
One honest note on the evidence: harness.log records this row as did not evaluate to 'true': responseStatus >= 200 && responseStatus < 300. Karate prints a status only for
status and match steps, and not for assert, so there is no response line in the log for
this scenario. The 415 is attributed from the request the per-scenario EARL detail in
conformance/reports/report.html shows being sent (header Content-Type = 'application/sparql-update', method PATCH, both earl#passed) together with the gate at
src/http.rs:509-511 and its unit test (the_content_type_gate_matches_classify_body,
text/turtle → 415). That is a source attribution, not a measured response line, and it is
named as one.
What used to fail here (first through fifth run)
Before the route existed, axum's method dispatch answered 405 before authentication,
Content-Type, or anything else about the request was ever inspected:
| Feature | PATCH failures |
|---|---|
wac/protected-operation/read-access-agent |
10 |
wac/protected-operation/read-access-bob |
10 |
wac/protected-operation/read-access-public |
10 |
wac/protected-operation/write-access-agent |
12 |
wac/protected-operation/write-access-bob |
12 |
wac/protected-operation/write-access-public |
9 |
protocol/writing-resource/containment:38 |
1 |
protocol/authentication/header:40 |
1 |
protocol/writing-resource/content-type-reject:19 |
1 |
10×3 + 12×2 + 9 + 1 + 1 + 1 = 66. Each wac/protected-operation row was a
retry until <expected-status> step that never converged, because 405 was never one of the
awaited statuses (403, 401, or [200, 201, 204, 205] depending on the row). Karate gave
up after 3 attempts and reported too many retry attempts: 3. It was the largest single gap
in the suite for four consecutive runs.
The pod behaves deliberately and differently from the test. Do not change these without a decision.
wac/protected-operation/write-access-{agent,bob,public}, the POST … type: container
rows, 2 per feature. Filed as a defect, reclassified as a pending decision on the strength
of a literal reading of WAC, and now resolved as a defect after all. The reading was
wrong.
post_impl used to authorize twice: Mode::Append on the container, then again on the
newly-allocated child, which can only inherit and so is scored against acl:default alone
(src/wac/pdp.rs:61). The second check is gone. A POST authorizes an operation on the
container: the server picks the child's name, so no client can be expected to hold a grant
naming it.
Three things settle it, none of which turns on how the sentence is parsed:
- The inverse case.
web-access-control-tests' companion test grantsacl:defaultRead+Append+Write+Control and noaccessToAppend, and expects403. Under the child-as-subject reading there is plenty to match, so it would be201. The reading does not merely refuse too much: it also permits what every implementation refuses. It is not a stricter version of the rule, it is a different rule. - The canonical inbox.
acl:accessTo <./>; acl:mode acl:Appendwith noacl:defaultis what node-solid-server writes as the inbox ACL of every pod it creates. Under the literal reading, LDN delivery into any such inbox never worked. - The wording's own history. "on the resource to be created" replaced "on the container
for new members" in
solid/web-access-control-spec#95, a change its editor approved with "about the same to me - a bit redundant". No difference in meaning was intended, and a proposal in that same PR to tie the requirement toacl:defaultwas rejected outright.
solid/specification#186 does not support the old reading either. It asks whether an
acl:accessTo rule naming a child applies to that child from the parent's ACL, i.e.
which rules inherit downward. Here the Authorization names the container and the container
is the request target, so nothing inherits and the inheritance rule never engages. Citing
it silently swapped the subject of the check, which is precisely the disputed point.
PUT and PATCH keep their child-level check: there the client names the resource, and
WAC asks for both, Append/Write on the container as well as Write on the new resource.
That is the one place the two-subject formula is written down.
What is left is a defect in the specification's text rather than in any server: the
container reading lives entirely in non-normative Notes and issue threads, while the one
normative sentence says something else. Worth an issue against
solid/web-access-control-spec; solid-contrib/specification-tests#124 asked it from the
wrong end and should be withdrawn.
| Test wants | 403 for DELETE of a fictive resource (one the suite reserved a name for but never created) authorized only through an ancestor's acl:default grant |
| Pod does | 404 |
| Why | authorize (src/http.rs:993) grants Write via the same inherited acl:default rule that lets the identical setup delete a plain or rdf resource correctly. With authorization settled, aux::delete_subject finds nothing to remove and returns Ok(false), mapped to 404. The pod's stated policy is that authorize runs before any existence check so a caller without access learns nothing; here the caller has access, and what it learns (nothing exists at this name) is defensible rather than a leak: 404 for "authorized, but not there" is a defensible answer the suite happens to score against 403. |
Confirmed still present, one instance per write-access-{agent,bob,public} feature: the
DELETE … type: fictive, container: W, resource: inherited row in each.
| Line | Test wants | Pod does | Why |
|---|---|---|---|
:13 |
404 for a POST to a container that does not exist |
201 |
The pod materialises missing ancestors on write. A reserved-but-absent container is created rather than reported missing. |
:27, :41, :55 |
404 or 405 for a POST to a non-container URL |
409 |
post_impl answers 409 from the request path shape alone, before any existence check, so an unauthorized caller cannot use POST as an existence oracle (src/http.rs:372-383). 404 would require disclosing existence; 405 would be a method claim the pod does not want to make about resource URLs. |
| Test wants | 404 for POST …/dahut3/foo/ where dahut3 exists as a resource |
| Pod does | 409 with body another resource already exists whose URI differs from this one only in the trailing slash |
| Why | Ancestor materialisation would have to create dahut3/ beside the existing dahut3, which Protocol §3.1 forbids; the slash-pair check in Guard::materialize (src/wac/guard.rs:319-328) stops it. |
This is the only failure the trailing-slash-pair rule touches, and it did not create
it. Without the rule the pod would have materialised dahut3/ and answered 201,
still not the 404 the test asserts. The rule changed the shape of an already-failing
scenario, not the count. It belongs with post-target-not-found: the same
ancestor-materialisation decision, surfacing as 409 instead of 201.
Previously filed here as a pending decision (see the second run's document, now
superseded): "format_for_content_type is the single gate … RFC 9110 supports 415 …
the suite reads Protocol's 'MUST reject' as 400." All three legs of that reasoning
fail, and the correction is:
- The conflation argument does not survive the three-way gate. It held only while "unsupported type" and "absent type" meant the same thing: no write. §8.1 of the design separates them: an unsupported type is now a blob, not a refusal. What is left at the gate is "absent" and "malformed", which were never the same question.
- The RFC 9110 citation was half-read. §8.3 covers an absent type with a MAY
(
application/octet-stream) and no415, since that status is for an unsupported one. Citing it for "absent" was reading the wrong paragraph. - Solid Protocol §2.2 names
400in its normative text, not merely implies it: "Server MUST rejectPUT,POST, andPATCHrequests that contain content but lack theContent-Typeheader field, with a status code of400."
PUT, POST and PATCH without Content-Type all answer 400, and content-type-reject is
3/3 as of the sixth run. The PATCH leg was the last to arrive: until the route existed,
axum's method check fired before Content-Type was ever inspected and the answer was 405,
which was a residual of the PATCH gap and not of this defect.
slash-semantics-exclude:65 (1 scenario). post_impl never reads the Link header
(src/http.rs:370-406); container::child_name always yields a slash-free segment, so a
POST always creates a resource and Location never ends in /.
read-method-allow:12, :19, :26, :33 (4 scenarios). The spec makes this a MUST;
cheapest item on the whole list, filed as a defect rather than a gap because it was never
a deliberate omission, where WAC-Allow is a feature.
content-negotiation-named-graphs:16 (1 scenario). rdf::parse used to iterate quads
and discard q.graph_name, flattening everything into the resource's single graph. Plan
9 replaced the graph-only parse/serialize/ETag path with Format and Skolemized, which
carry graph names through the whole round trip.
wac/protected-operation/write-access-{agent,bob,public}, the DELETE … type: container
row, container: W (direct + acl:default), resource: inherited (no ACL of its own).
| Test wants | 403, since deleting a container must not be authorized purely by an ancestor's acl:default grant |
| Pod does | 204, deleting it |
| Why | delete_impl's only authorization for the target (src/http.rs:993) is authorize(target, Mode::Write), and authorize does not distinguish "Write reached via my own ACL" from "Write reached via an ancestor's acl:default" (src/wac/pdp.rs:54, by design, for ordinary resources). The identical setup deletes a plain or rdf resource correctly. Deleting a container is where the suite draws a line this pod does not: removing a whole subtree apparently needs more than the mode an ancestor happens to cascade down. |
The fictive counterpart of this test, same setup with a DELETE of a resource that was
never created rather than an existing container, fails for a different reason (404
instead of 403, not an incorrect allow); it is filed in Bucket 2 above.
protocol/cors/simple-requests:53 (×2), :71 (×2); protocol/cors/preflight-requests:36
(×2); protocol/cors/acao-vary:28 (×2), :58 (×2).
| Test wanted | On a CORS-eligible, content-negotiated GET/HEAD, one Vary header whose value contains Origin (alongside Accept) |
| Pod did | Sent Access-Control-Allow-Origin and Access-Control-Expose-Headers correctly, but the response the harness read back had Vary: Accept only |
| Why | cors_layer (src/http.rs:73-90) is deliberately the outermost layer so it can add CORS fields after the handler has already set Vary: Accept for content negotiation (§6.3). Its comment said why it used append rather than insert: "a negotiated read has already set Vary: Accept, and replacing it would make a cache serve the wrong representation." But HeaderMap::append added a second, separate Vary header line rather than combining the value into the existing one. RFC 9110 §5.3 permits a recipient to treat repeated header fields as equivalent to one comma-joined field, but the harness's HTTP client reads only the first Vary line it receives, Accept, and never sees Origin. Confirmed by the per-scenario report for cors/simple-requests:53: the request did send Origin, and Access-Control-Allow-Origin/Access-Control-Expose-Headers both passed (proving cors_layer ran and reached the Vary line), yet the Vary assertion still failed on the split header. This is the record of why a two-line Vary is a defect and not a legal-but-unusual choice: RFC 9110 permits repeating a list-valued field, but a client that reads the first line and stops sees half the list, and the conformance harness is exactly such a client. |
New when first measured: previously unmeasurable in main alone (CORS features
that reach this code path build a text/plain fixture and aborted on 415) and
unmeasurable in feat/non-rdf-resources alone (the CORS headers this defect concerns did
not exist on that tree at all). It explained why access-control-headers, accept-acah,
and enumerate-headers were fully passing while simple-requests, preflight-requests,
and acao-vary were not: only the latter three exercise a GET/HEAD where content
negotiation has already written a Vary: Accept before cors_layer appends Origin.
Fixed by 9d47b1a: cors_layer now reads whatever Vary value is already present,
splits it on commas, adds Origin only if it is not already listed, and writes the whole
set back with a single insert: one field line, e.g. Vary: Accept, Origin, instead of
two. Re-run confirms all 10 scenarios now pass: cors/simple-requests and cors/acao-vary
are 10/10 and 12/12; the two Vary failures at preflight-requests:36 are gone (see
the fifth run above).
Every one of the first run's 615 failures was attributed to a named cause, verified
against either the harness log's response line or the pod's source. As of the second run,
609 remained, all attributed. As of the merged run, all 95 remaining failures were
attributed, cross-checked against conformance/.run/harness.log's response lines and the
per-scenario detail in conformance/.run/karate/karate-reports/*.html for every one of
the 14 features with a failure, not sampled. As of the fifth run, 9d47b1a removed the
Vary cause outright (10 scenarios) rather than folding it into another one, leaving 85,
66 + 6 + 6 + 4 + 2 + 1 = 85, re-verified the same way against this run's own
harness.log and karate-summary-json.txt, feature by feature, for all 12 features with
a failure. Nothing left over.
As of the sixth run, 20 remain (6 + 6 + 4 + 2 + 1 + 1 = 20), and every one is attributed
to a named cause: each of the 7 features with a failure was read out of harness.log's
ERROR lines and cross-checked against the per-scenario EARL detail in
conformance/reports/report.html, which also supplied the Examples-row line numbers for the
wac/protected-operation outlines. The 65 scenarios that went green were checked the same
way rather than deduced from the totals: each was located by scenario title in
report.html with earl#passed on its asserting step, and each of the six features involved
carries a failed: 0 banner in harness.log. One attribution is deliberately weaker than
the rest and says so: containment:38's 415 comes from the source gate and its unit test,
because karate prints no status for a failed assert step. Bucket 4 stays empty, but that
row is the closest thing in this run to an inference.
if-none-match-asterisk |
2 / 2 |
post-uri-assignment |
1 / 1 |
post-uri-assignment-slug |
1 / 1 |
uri-assignment |
1 / 1 |
method-not-allowed |
2 / 2 |
describedby-unique |
2 / 2 |
content-negotiation-turtle |
2 / 2 |
content-negotiation-jsonld |
2 / 2 |
content-negotiation-named-graphs |
1 / 1 |
read-method-allow |
4 / 4 |
read-method-support |
6 / 6 |
slash-semantics-exclude |
4 / 4 |
delete-protect-nonempty-container |
3 / 3 |
delete-remove-containment |
3 / 3 |
all five wac-allow features |
58 / 58 |
all four acl-object features |
12 / 12 |
cors/access-control-headers |
6 / 6 |
cors/accept-acah |
3 / 3 |
cors/enumerate-headers |
1 / 1 |
cors/simple-requests |
10 / 10 (as of the fifth run, 9d47b1a) |
cors/acao-vary |
12 / 12 (as of the fifth run, 9d47b1a) |
authentication/header |
6 / 6 (as of the sixth run, d485135; the anonymous PATCH row included, WWW-Authenticate present) |
content-type-reject |
3 / 3 (as of the sixth run, d485135) |
all three read-access-* features |
90 / 90 each (as of the sixth run, d485135) |
The new auxiliary URL shape works, including for blobs. The harness's
PREPARE SERVER step created a container and set an ACL on it through the
Link-advertised /.aux/{path}.acl URL, and the five wac-allow features exercise ACL
writes and reads repeatedly with every access decision correct. No failure in this run
traces to the ACL URL shape or to the blob path specifically: every blob-involving
failure traces to PATCH, POST, or DELETE, all of which apply identically to RDF
resources. Vary was the fourth such gap; 9d47b1a fixed it (see Bucket 3).
WAC access decisions are correct for the large majority of newly-measured cases. 434
of the 540 scenarios the non-RDF work unblocked, 80% of them, passed outright at the merged run,
when read-access-{agent,bob,public} each reached 80/90, write-access-{agent,bob}
68/84, and write-access-public 40/53. As of the sixth run the three read-access-*
features are 90/90, write-access-{agent,bob} are 80/84, and write-access-public is
49/53, with 12 failures across the six, all of them the POST/DELETE findings above. No
failure in these features has ever been an ordinary Read/Write/Append/Control decision coming
out wrong.
The trailing-slash-pair rule still costs nothing. Exactly one scenario mentions it
(containment:122), and that scenario failed before the rule existed too; see Bucket 2.
./conformance/run.sh # ~4 min including the buildArtifacts:
conformance/reports/report.html human-readable, per-request
conformance/reports/report.ttl EARL, for diffing against the next run
conformance/.run/harness.log every failure with its response line
conformance/.run/karate/karate-reports/ karate's own summary + karate-summary-json.txt
karate-summary-json.txt is the fastest way to regenerate the per-feature table:
jq -r '.featureSummary[] | "\(.passedCount)/\(.scenarioCount)\t\(.relativePath)"' \
conformance/.run/karate/karate-reports/karate-summary-json.txt | sort