Repository navigation
feat(compilers/openapi): read each document's schema identifiers - #1227
Merged
Merged
Conversation
An $anchor, $dynamicAnchor or $id another document declared named nothing, and a $dynamicRef written there never expanded. The binder read identifiers in the source alone, while internal/anchors and declaresResourceIDAbove walked the raw tree a second time with a looser idea of where a schema is. That second reading missed an $id past the merge bound, so a reference expanded across a resource boundary, named the wrong bound in its warning, counted an anchor in an example, and read a property named $id as a boundary. The binder now reads each document's identifiers and answers both questions, and the second reading is gone. An OpenAPI document is read by the grammar; any other is read as a document of schemas. Whether a URI is an identifier depends on what has been read, and what is read depends on the URIs, so references are answered in passes against a frozen set of URIs. A pass runs again if a URI it asked about gains a declaration, and only the last one's answers stand, so no declaration order reaches an answer. The first read of an absolute URI waits for the pass to end, so an identifier is never fetched, and a document only a discarded pass read is no Sources entry. ComponentSchema and ComponentRef return one state, so no caller can report a component keyed "" in another's words.
This was referenced Nov 1, 2026
This was referenced Nov 1, 2026
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 1, 2026
The Kept accessor only handed back a pointer, and it took lowering.Ctx to the 20 methods internal/archtest allows, one short of what #1227 adds, so whichever of the two landed second would have failed the cap with no merge conflict to warn of it. A field does the same work and adds no method. KeptOnce's own fields are private, so writes still go through Once.
This was referenced Nov 1, 2026
main reads a null $dynamicAnchor and a null $id as the keyword left out in internal/anchors and declaresResourceIDAbove, both of which this branch removes. internal/bind reads a null as absent already, so the null-keywords rule now cites a bind test of the same spellings, and the #572 test comment names ids.ComponentSchema rather than the classifier this branch replaced. The dynamic-ref rule keeps this branch's text and main's sentence on a keyword beside an expanded reference. Two fixes no conflict marks: referenceOutcomeKeywords takes the three returns dynamicExpansion now has, and schema.go keeps slices for writesType.
The warning a merge past the bound draws said that a $dynamicRef in "that document" is not expanded. It fires once, at the source, for a mapping anywhere the table reads, while only a schema merging past the bound keeps its document's $dynamicRef from expanding. With a response past the bound, or a schema past it in another document, the source's references expanded under a warning saying they were not. It now names the case where that holds.
The passes kept every URI any document declared, a document a pass read as a location before another declared that location as an $id among them. No later pass reaches such a document and it is no Sources entry, yet its $id still counted, so a URI it and a reached document both declare named neither. Its merge past the bound drew the table's warning too. Each pass now reads against what the documents the pass before it reached declare, so a document no pass reaches any longer withdraws its URIs and its truncation. Where what is reached turns on what is declared, passes need not settle: a document reached only through a URI it declares a second time is unreached when counted and reached when not. Once a pass would return to declarations an earlier one read against, every document read counts, as before, which only grows, so the passes end. The lowering rule no longer says an identifier is never fetched: the documents absolute URIs name are read together, so one of them can declare another's URI.
No committed spec reads an identifier across documents, so the harness oracles, the order oracle among them, never met one. The fixture's other document declares a $dynamicAnchor, an $anchor under an $id and the URI that $id names, and the source reaches each from outside, the URI with external references off. On main the source draws two unresolved-ref errors there, which the in-repo sweep reports.
Whether an $id spelled as an absolute path names the file a relative $ref reaches turns on whether the spec was named by an absolute path: the two keys meet only then. That held for an $id in the source, and reading another document's identifiers reaches it there too, so the references rule's status says so beside #1040, which weighs how to name the spec once. Also rewraps the dynamic-ref rule's lines past 100 columns, and has the census table give the binder, not the removed anchors package, as what reads a $dynamicAnchor.
Takes the typed collection bound beside a $ref and the intersected redeclared field bounds; neither touches the identifier reading.
The documents absolute URIs name are read together once a pass finds nothing it reached answering for them, so one of them may declare another's URI, which is then read for nothing. Reading them one at a time would spend a pass on each. reread now says so, as the references rule does.
Two tests said a URI a document declares is never requested, as their read waits for every document that could declare it. The held documents are read together, so one of them can declare another's URI after it was fetched. Each comment now says what its case shows, that a URI a document read during the pass declares is not fetched, and the end-to-end test drops "Never" from its name.
Takes the engine's content-addressed IR cache, whose archtest rules sit beside the anchors entry this branch removes.
Each pass walks the source again, and only the first reads what it declares. No test needed more than one pass with an identifier in the source, so a walk that declared on every pass, making the source's own $id and plain names duplicates from the second pass on, left the suite green. The test asks for both where a URI only another document declares runs a second pass.
Table.InOwnResource passed holding a bare true. It reads the position as a schema, so its own $id counts, which a comment at the argument now says.
The sentence on which documents are Sources entries ran to 111 columns.
A pass asked its budget only at the top of each round, so one the budget ran out in during its last round ended as if whole. The next pass then began with nothing left to spend, and its empty table replaced the one the budget had cut, which answered every site it had reached. The pass the budget runs out in is now the last, wherever in it the budget does, and a document read for the pass after it is not one it reached, so no input lists it. The references rule says that every pass spends the one budget and what a cut keeps: an answer the cut pass gave stands, though a later pass might have changed it. readHeld, split from reread, also states what reading the held documents together costs: a document one of them names waits for the pass after, so a chain of them costs a pass a link.
The references rule's status names #1349: a document a held document names waits for a pass of its own, so a chain of documents each named by an absolute URI costs a pass a link, and its work grows with the square of its length. readHeld points there too, and attribute's comment is rewrapped to the width of those beside it. TestBind_StopsAtItsBudget's claim that a cut answer is the full one or one the budget stopped holds because its spec is read in one pass, and its comment now says so.
A pass held back each document an absolute URI names until its walks were done, then began another pass to read it: every reference read again, and a document the held one names held in turn. A chain of such documents cost a pass a link, so its work grew with the square of its length, and the budget cut chains a single pass reads easily (#1349). A pass now reads what it held once its walks are done and goes on, for as long as what the documents it reached declare changes no URI it asked for. Only the held hops and mapping values are read again. A chain that meets a held hop waits and remembers nothing, and is followed once nothing is held, so a chain of held documents is followed once. A drain counts only what the pass reached since the last one (identifiers.grow), identifyAll starts past the documents it has read, and each of those loops is charged to the budget, so a rescan shows in the work. A pass is read again only when a URI's declarations change. Every answer a continued pass keeps is one a new pass would give: no URI it asked for changed, an anchor or a definition answers from its own document, and what a pass reaches only grows. TestBind_ThePassTheBudgetRunsOutInStands now drives its second pass by a declaration, as held documents no longer need one.
grow skipped a URI it had answered and removed one left with no declaration. The first saves nothing a set of the URIs affected does not, and the second cannot happen: each URI grow answers is declared by a document it now counts. goOn stopped before requeueing when reading the held documents spent the budget, which the pass loop's own check already does, with the same table, now that nothing answers what waits.
# Conflicts: # compilers/openapi/internal/load/phases.go # docs/compilers/openapi.md
The references rule said a chain of documents each named by an absolute URI "costs what reading it once does", which reads as costing what it does on main. It is read in one pass, its cost growing with its length, and the rule now says that. A line of its Tests list ran past the width the document wraps at.
Two readings of another document's identifiers had no test, and planting the wrong one turned nothing red. An $id naming the URI of the document it is written in, below another $id, returns what is below it to that document's resource. Returning to the source's resource there, as main does where only the source is read, or keeping the outer $id's, misplaces a plain name below it. TestBind_AnIDNamingItsDocumentReturnsToIt fails on both. A position's own $id starts its resource wherever it is read as a schema, as one a $ref lands on is, though the grammar reads it as raw YAML. Reading it by the grammar alone lets a $dynamicRef under such an $id expand across the resource boundary. A row of TestInOwnResource_ReadsTheResourceAPositionIsIn now fails on that.
lowerSchemaBody expanded a $dynamicRef only where resolveAt resolved its anchor, and otherwise lowered the schema's body. Where that happened the $dynamicRef was lost unrecorded: recordUnexpandedDynamicRef asks dynamicExpansion again, which still says it expands, so it keeps nothing. A planted failure of resolveAt for another document's anchors lowered the reference to `any`, with no Unmodeled entry and no diagnostic. No input reaches that path today, since anchorHolds settles the anchor by what resolveAt reads in the document's own context. The guard is gone, so if the two ever part, the expansion's empty target fails irverify's type-ref-no-target instead, as the same planted failure now does.
The references rule said a pass reads the documents it held together and goes on, so a chain of documents each named by an absolute URI is read in one pass. Neither holds where one of them declares a URI a reference asked for, a held reference included: the pass is read again. Two documents held together, one declaring the other's URI, take two passes, and a chain in which each document declares the URI the one before it asked for takes a pass per link, 21 for 20 links, so its cost grows with the square of its length. The rule now says one pass where none declares such a URI, the Status and reread's comment name the cost of the chain that does, and #1372 tracks making it linear.
identifiers.reading, the key that tells the passes they have returned to what an earlier pass read against, held the number of declarations read as well as the documents counted. That number answers nothing: a pass reads against what the counted documents declare, each read whole before a drain counts it, and what a document read but not counted declares answers no reference. So the count only delayed the fallback to counting every document read, past a reading the passes had already returned to, and the comment that said it named the declarations was wrong. Keyed on the documents counted alone, the fallback comes at the first pass that would read against what an earlier one read, as the references rule states. The input no pass settles takes three passes rather than five, with the same answers. Over 300,000 generated tangles of documents whose schemas declare, ask for and pass on a shared pool of URIs, every answer is the same either way, 2,011 of them reach the fallback under both, and 1,754 reach it in fewer passes. TestIdentifiers_AReadingIsTheDocumentsCounted pins the rule: a document read and left uncounted since a pass began changes no reading. It fails with the count put back, with a reading never recorded, and with the fallback taken at any change.
reading's comment said what a document read and not counted declares answers nothing. That holds only until the passes fall back to counting every document read, when it answers too, as the identifiers struct's comment says. commit consults a reading only before that, so the comment now says so.
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 2, 2026
Brings in #1227, which resolves a $ref against the schema identifiers each document declares and expands a $dynamicRef through the binding table. The merge is clean, and #1227 takes no IRVersion, so this branch's stays 0.31.0. Every reader this branch adds reaches a target through the bindings (the admission read, lentBy, Use.lentKeys), so each follows #1227's resolution with the lowering: a refinement over a $ref an $id resolves reads that target as its $ref spelling does. Swept over testdata, the reference corpus and the golden spec, #1227 changes one spec on main and the same one here, its own identifiers_in_another_document, with the same IR and diagnostics on both. A $dynamicRef still ends a refinement's read, which keeps such an allOf the composition it was.
17 tasks done
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.
Summary
Two readings of the same facts lived side by side.
internal/bindalready knew where schemas are, which resource holds a position, and when a merge past the bound may hide a key.internal/anchorsandschema.declaresResourceIDAboveeach walked the raw tree again with their own, looser idea of all three, and another document's identifiers were read by neither. Most of what is closed here follows from that one split:$anchor,$dynamicAnchoror$idin another document named nothing (openapi: an $anchor or $id another document declares names nothing #1049), and a$dynamicRefwritten there could not expand (openapi: a $dynamicRef written in another document is never expanded #1031).$idfrom the second walk, so a$dynamicRefexpanded across a resource boundary (openapi: a merge chain past MergeDepthLimit hides an $id, expanding a $dynamicRef across a resource boundary #401).$dynamicAnchorin anexampleor anx-extension counted as a declaration, so a reference read "declared 3 times", and a property named$idread as a resource boundary.bindnow reads each document's identifiers, andinternal/anchorsanddeclaresResourceIDAboveare gone.dynamicExpansionasks the table for the sole anchor (Table.DynamicAnchors) and for the resource a position is in (Table.InOwnResource), so a$dynamicRefand a$refread one set of facts.Reading another document
A document whose root names an
openapiversion is read by the grammar, as the source is, so an example or an extension in it declares nothing. Any other document has no grammar. Its root and any key of no schema vocabulary are read as holding schemas, and within a schema a keyword reads as it does in the source, so a literal, a value or an extension declares nothing. Reading only a pure schema document would not cover the shape #1049 is filed on, a map of schemas under keys of its own.$idgives its base below itself by the same reading, in every document.Reading in passes
Whether a URI is an identifier depends on which documents were read, and which are read depends on what the URIs name. OAS 3.1.1 §4.3.1 requires complete documents before a reference is deemed unresolvable. So
Bindruns passes: each answers every reached reference against the URIs the documents the pass before it reached declare, and, as it goes on past what it held, those the documents it reaches since declare, while no URI it asked for changes. A pass runs again if what the documents it reached declare changes a URI it asked about: it was told nobody declares that URI, a second schema now does, or a document no pass reaches any longer withdrew it. Only the last pass's answers stand. Each pass is a set operation, so no declaration order reaches an answer, and a document that neither declares nor names such a URI runs one pass.A document a pass read as a location before another declared that URI is reached by no later pass, so it declares nothing, lowers nothing and is no
Sourcesentry. Where what is reached turns on what is declared, passes need not settle: a document reached only through a URI it declares a second time is unreached when counted and reached when not. Once a pass would return to declarations an earlier one read against, every document read counts, which only grows, so the passes end, and that URI names neither schema.The first read of an absolute URI waits until the pass's walks are done, so a URI a document read during the pass declares is never fetched. The documents held are then read together, a round for all of them rather than one each, so one of them may declare another's URI, which was then read for nothing, and the pass is read again. With a local server holding both,
A: {$ref: 'http://127.0.0.1:18765/u1.yaml#/S'}andB: {$ref: 'http://127.0.0.1:18765/u2.yaml'}, whereu1.yaml'sSwrites$id: 'http://127.0.0.1:18765/u2.yaml', the server logs a request for each,Bresolves to thatS, andu2.yamlis noSourcesentry. Reading them one at a time would spend a round on each.A pass that held documents reads them once its walks are done and goes on, for as long as what the documents it reached declare changes no URI it asked for. Only the hops and mapping values that waited for them are read again. A chain that meets a held hop waits and remembers nothing until nothing is held, so it is followed once, and each drain counts only what the pass reached since the last (
identifiers.grow). A pass is read again only when a URI's declarations change, so a chain of documents each named by an absolute URI, none declaring a URI a reference asked for, is read in one pass, its cost growing with its length (#1349). It is the name that counts: a relative$refin a document fetched by URL, or under an absolute$id, is held the same way. Every answer a continued pass keeps is one a new pass would give, since no URI it asked for changed, an anchor or a definition answers from its own document, and what a pass reaches only grows.Counted in the binding's steps, a chain of 512 such documents costs 11,278 here and 5,645 on
main, where beginning a pass for each held document cost 1,323,533; a chain of 16,000 costs 352,014, all resolved. Compiling a chain offile://documents with external references allowed,GOMAXPROCS=1 GOGC=off, median instructions retired of three alternating runs a side, at 5fa0d5f againstmainat 02c972b, and the pass per link at 1db320a:mainThe few percent over
mainis the identifiers each document is read for.The golden spec, which names no document by an absolute URI, takes 862,702 binding steps here and 862,700 on
main, and 106.9 G instructions here against 104.7 G onmain(medians of three, collector on, ranges 105.3–107.5 G and 103.9–107.8 G, overlapping).Every pass spends the one budget. The pass the budget runs out in is the last, wherever in it the budget runs out, and its answers stand. A reference a later pass would have answered otherwise keeps the cut pass's answer:
Y: {$ref: 'a.yaml'}, beside az.yamlwhose schema writes$id: 'a.yaml', resolves to that schema, but toa.yamlitself under a budget the first pass alone uses up. That ismain's answer, and the compile reportsopenapi/budget-exceeded.This departs from the rounds sketched on #1049, which open every document each document's references spell. That reads documents nothing reaches, and an unreached document's
$idcould then turn a resolving URI into a duplicate. Passes read only what a reached reference names.Other decisions
other.yaml#tagto name an anchor declared under an$id, "as the same schemas written inspec.yamlresolve". They do not: the same document onmainrefuses#tagfrom outside that$id, as JSON Schema says. SoTagin that example stays unresolved, with a true reason, whilea,b,ByIdandhttps://x.test/pet#tagresolve.$dynamicRefin another document may name an anchor at any position there. The source's restriction to a component rests on its names, and what another document declares has IDs from its pointer. Nothing but a reference builds a schema of another document, so the binder lands on the anchor as a reference would (finder.landOnAnchors).$refreaches, and draws the same findings.$dynamicRefin that document is kept verbatim, wheremainwarned and expanded anyway. The one warning that remains names the bound that fired.ids.ComponentSchemaandresolve.Scope.ComponentRefreturn anids.ComponentState, andComponentSchemaNameandComponentSchemaNamedEmptyare gone, so no caller can report a component keyed""in another's words.Behaviour to check on review
$dynamicAnchorin an example or an extension no longer makes a name a duplicate, and a property named$idor an$idin raw YAML no longer stops an expansion. Each of those references now expands.$ref, so$dynamicAnchor: 'pct%name'no longer expands. The validator already reports that document as an error.$idin the source and in a document it reaches resolved to the source's onmain, since another document's was never read, and isopenapi/unresolved-refhere.$idin another document now gives the base a relative$refbelow it is read against, as one in the source already did (openapi: an absolute-path $ref in a remote document is read as a local path #936).schemas/pet.jsonwriting$id: https://example.com/schemas/pet.jsonand$ref: tag.jsonread the sibling file onmain; here that$refnameshttps://example.com/schemas/tag.json, refused by default as a network read. openapi: let the caller map an identifier's URL to a local location #1051 proposes letting a caller map such a URL to a local file.$dynamicRef, so a response past the bound leaves the source's references expanding.Limitations
$idspelled as an absolute path names the file a relative$refreaches turns on whether the spec was named by an absolute path, since the two keys meet only then. An$idin the source does this onmain; reading another document's identifiers reaches it there too. openapi: whether an absolute $ref names the spec depends on how the working directory is spelled #1040 weighs naming the spec once, which settles every such site, so this PR states it in thereferencesrule's status rather than choosing for it.$id, the URI the one before it asked for is read again for each link, 21 passes for 20 links, so its cost grows with the square of its length: 256 suchfile://links retire 11.8 G instructions at fba9c3f, where 1,024 links declaring nothing retire 1.8 G.mainreads none of their identifiers, so it fails the chain's first and pays nothing. Thereferencesrule's Status andreread's comment state it, and openapi: a chain of documents each declaring the URI the one before asked for costs a binding pass per link #1372 proposes the reading that makes it linear.Test plan
bind.TestBind_ReadsWhatAnotherDocumentDeclaresholds the identifier readings in both declaration orders,bind.TestBind_DeclarationOrderReachesNoAnswera tangle of three documents in three orders, andbind.TestBind_AURIASecondDocumentDeclaresNamesNeithera duplicate a pass apart.e2e.TestCompile_AnIdentifierIsNotRequestedrecords the reader with external references allowed:https://x.test/petis not requested.e2e.TestCompile_ADocumentNothingReachedIsNoSourceandbind.TestBind_ADocumentOnlyAnIdentifierReachesIsReachedhold both sides of whatSourceslists.e2e.TestCompile_ASchemaAnExpansionReachesIsValidatedcompares what a schema draws reached by an expansion and by a$ref.e2e.TestCompile_ADynamicRefExpandsInTheDocumentItIsWrittenInis openapi: a $dynamicRef written in another document is never expanded #1031's example with a two-order diff, andschema.TestDynamicRef_RestsOnNoReadTheMergeBoundCutis openapi: a merge chain past MergeDepthLimit hides an $id, expanding a $dynamicRef across a resource boundary #401's reproducer.bind.TestBind_ADocumentNoPassReachesDeclaresNothingholds, in both orders, that a document read as a location before another declared it adds no duplicate and no merge-bound verdict, andbind.TestBind_AReadingNoPassSettlesCountsEveryDocumentthat the passes end, within a budget, on the input no pass settles.bind.TestDynamicAnchors_SaysWhenTheCountProvesNothingholds that a response past the bound hides no anchor, andbind.TestDynamicAnchors_ANullAnchorNamesNothingthe null spellingsmainpinned in the removedanchorspackage.testdata/openapi/identifiers_in_another_documentputs the shapes above where the harness oracles and the sweep reach them. Its source draws twoopenapi/unresolved-referrors onmain, whichTestHarness_InRepoCorpusreports.$dynamicAnchorread as a name, and a merge past the bound anywhere counted as the document's.$idand plain names read as duplicates from the second pass on.bind.TestBind_TheSourceDeclaresOnceWhateverThePassesnow pins it.sweep.pyagainstmainat 737ffb5 overtestdataandmorphic-references(2,183 specs), and overtestdatawith external references allowed (229): only the new fixture differs, its twoopenapi/unresolved-referrors gone,PetandTagresolving intoother.yamlandLeaf's$dynamicRefexpanding. That shows no regression, not that the fix works: no corpus spec uses these shapes across documents.bind.TestBind_ThePassTheBudgetRunsOutInStandssweeps the budget across a binding whose second pass a declaration forces: one step short of the least budget that begins it, the first pass's table stands with every answer it gave. The code at 9fb547f also ran a pass with nothing to spend after reading held documents for it, its empty table replacing the cut one; a pass now reads what it held itself, so that case cannot arise.bind.TestBind_CountsWorkLinearAcrossHeldDocumentscounts the binding's steps at 500, 1,000 and 2,000 documents for a chain, a chain spelled relatively, a loop, a chain of mapping values and links naming the one before by its$id, and checks each document is read once and no identifier is. Every shape reddens on the code that began a pass per held document.bind.TestBind_AHeldDocumentMayDeclareAnothersURIpins two documents held together, one declaring the other's URI, in both orders.identifyAllrescanning from the first document, andgoOnrunning on a spent budget. Five survive, each with a reason: an unrecorded held mapping value, which a release recomputes too; the bound on releases, which correct code never reaches;rereadignoring a spent budget, as a pass begun with nothing to spend and one begun with a step left both answer nothing; an unchargedidentifyAllscan, which its cursor keeps linear; and the budget check inrun's rounds, a line asmainwrites it.sweep.pyagainst 1db320a, the head before this change: the 2,181 specs oftestdataandmorphic-referenceswith default flags, and the 227 oftestdatawith external references allowed, compile alike, IR, exit codes and diagnostics. Seven generated shapes of heldfile://documents compile to the same bytes as on that head, and the three declaring more than one component to the same IR in both declaration orders.bind.TestBind_AnIDNamingItsDocumentReturnsToItholds that an$idnaming its own document, below another$id, returns what is below it to that document's resource. It fails on returning to the source's resource, asmaindoes where it reads the source's identifiers alone, and on keeping the outer$id's, neither of which any test caught. A row ofbind.TestInOwnResource_ReadsTheResourceAPositionIsInholds that a position's own$idcounts where the grammar reads raw YAML: reading it by the grammar alone expanded a$dynamicRefacross that boundary, and nothing caught that either.resolveAtplanted to fail for another document's anchors, a$dynamicRefthatdynamicExpansionexpands lowered toanywith no record and no diagnostic, sincelowerSchemaBodyfell back to the body andrecordUnexpandedDynamicRefkeeps only one that does not expand. With the fallback gone the same plant fails--verifywithir/type-ref-no-target.$ids, anchors,$refchains, discriminator mappings and nested relative$ids, 30,000 of them: the tableBindreturns equals that of one fresh pass read against what its last pass reached, and three shuffled declaration orders give each the same answers. 8,409 took two passes or more and 62 counted every document read. A drain blind to a changed URI breaks the first at the seventh tangle.bind.TestIdentifiers_AReadingIsTheDocumentsCountedpins what makes the passes return to a reading: the documents counted, which settle every declaration a pass reads against, each read whole before a drain counts it. The key also held the number of declarations read, which answers nothing and only put the fallback to counting every document read off past a reading the passes had already returned to. Without it the input no pass settles takes three passes rather than five, and over 300,000 generated tangles of documents declaring, asking for and passing on a shared pool of URIs every answer is the same, 2,011 reach the fallback either way, and 1,754 reach it in fewer passes. The test fails with the count put back, with a reading never recorded, and with the fallback taken at any change.mainat 6996ce4.Closes #1049
Closes #1031
Closes #401
Closes #454
Closes #405
Closes #1349