feat(knowledge): route the two questions a signature cannot answer - #664
Merged
Merged
Conversation
A row with no column spec, no grow spacer and the default START arrangement splits its inner width into equal shares, one per child, so add(icon.node(13)).addParagraph(...) hands a 13pt icon half the row and wraps the text into twice the lines. horizontal(100) is 100 points and not 100 percent, and nothing clips a line to the slot it was given -- in a 135.7pt sidebar column that rule is drawn 32.15pt past the column edge, with no warning and a clean render. Neither fact is in a signature, and neither had a route, so the pack could only answer "this method exists". layout.icon-beside-text recommends columns(auto(), weight(1)) and layout.rule-beside-a-heading recommends fill() in a weight column; both carry the alternatives with what each one costs, and both name the ways the repair fails. Two sections in the layered-page-design recipe hold the claims, with fences written in the session form so a reader who copies one gets a document rather than a builder that goes nowhere. The proofs are arithmetic wherever the engine allows it: the left-hand child of a flex row is a fixed-width shape rather than a heading, so no assertion rests on the default font's metrics, and widths are pinned rather than only the direction of an overflow. Where a coordinate genuinely is font-dependent -- a non-START arrangement spreads the leftover as a leading offset, and the leftover is the row minus the paragraph's measured width -- only the widths are asserted, and the class says so. Three failure modes are pinned alongside the happy path: a grow spacer neither shrinks a fixed rule nor gives fill() a slot, a non-START arrangement enters the same flex path with no spacer present, and an aligned heading in the auto() column leaves the weight column zero, so the rule is not misplaced but absent. Both routes ship confirmedBy: null. Whether the recommendation is the right one is not something the mechanical checks can answer.
confirmedBy is the one gate check-routes cannot close: the anchors resolve, the symbols exist and the constraints are proven, but whether fill() in a weight column is the right answer for a rule beside a heading is a reading of the engine, not a fact about it. Both routes were served with their status attached until that reading was made. It has been. Recorded here rather than in the commit that wrote them, so the field means what the file says it means -- the same shape as the first three routes, which shipped null and were confirmed separately.
The three routes written first recorded confirmedBy: "DemchaAV", the GitHub handle; the two confirmed since recorded "Artem Demchyshyn", the name already used as @author throughout the sources and in the pom's <developers>. Both are real committer identities on the same account, so nothing changes hands here -- but two spellings of one person in a five-entry file read as two reviewers, and check-routes cannot tell them apart: it only asks that the field name somebody. All five now say "Artem Demchyshyn". Kept as its own commit because it edits three attestations that predate this branch.
LineBuilder.fill() promised "in a row column the line spans the whole slot" with no exception, and that javadoc is the only prose an API-first reader gets: the knowledge surface carries the signature and nothing else, so an IDE tooltip and javadoc.io are the whole story. A flex row has no slot. With a grow spacer or a non-START arrangement the row sizes every non-grow child to its content, a filled line answers with the row's whole available width, and it is then placed after the children before it and runs past the edge -- the opposite of what the sentence promised, and now pinned by LineWidthUnitsContractTest. Javadoc only; no signature moves, so the surfaces are unchanged.
Two gaps between what the routing gate claimed and what it did, and the apparatus to keep them closed. check-routes' header said the recommendation "is checked as far as a machine can -- the recommendedBecause must cite a real anchor". It did not: the field was tested for emptiness only. That check now exists, over every prose field api-query prints -- intent, recommendedBecause and each alternative's useWhen and tradeoffs -- sharing one resolve function with the `docs:` list so a rule added to the anchor check cannot land in one copy and miss the other. The rule it resolves with is GitHub's, which the previous one was not. anchorOf collapsed whitespace runs to a single hyphen while GitHub emits one per whitespace character, so the gate disagreed with the rendered page on every heading with a spaced em dash -- red on a link that works, green on a link that 404s. It also read a "#" comment inside a fenced block as a heading, and matched the docs/ tail of an absolute URL as a repo path, rejecting a route for a page that exists. All of it is now lib/anchors.mjs with fixtures, and the Java implementation in DocumentationLinkGuardTest was brought onto the same rule: it left a non-breaking space in the anchor as a literal U+00A0 and dropped non-decimal numerals. The four inputs on which the two disagreed are asserted from both sides, so changing either reddens its own fixture. Staleness compared version strings, so the SNAPSHOT-to-release flip at tag time reported every route as older than the pack -- including routes signed against exactly that code. Versions are ordered by their numeric head in lib/pack-version.mjs, and the two states that are not age are failures rather than notes: a version that cannot be ordered, and one ahead of the surfaces. A pack carrying no version failed open and now does not, and the pack version is read from a sorted listing rather than from whichever file the filesystem happened to yield first. api-query printed a route's version with nothing to compare it against, and then compared it as a raw string -- reintroducing, in the copy that ships, exactly the false staleness the gate had just stopped doing. It cannot import the shared module: build-bundle publishes that one file as bin/query.mjs with nothing beside it. So the duplicate is held by pack-version.test.mjs, which runs the same table through both. Its `read:` line now also says when the page it names is not inside the bundle. A malformed citation used to kill the gate with an EISDIR stack trace naming no route, and a mis-cased one passed on Windows to fail on the runner as "no such page"; both now report what is actually wrong. The fixture suites are found by glob rather than listed one step each, because the suite added in this commit would otherwise have run only if someone remembered to edit the workflow -- and a suite that never runs looks exactly like one that passes.
LineBuilder.fill() points a reader at a section of the recipe page, and it is the only citation of that shape in production Java. Nothing resolved it: the markdown half of this guard reads [text](target) links, which is every reference a page can make and none a Javadoc block can write. So renaming that heading fixes the page's own links and both copies in the routing table, and leaves the Javadoc one dangling -- in the copy with the widest audience, since the knowledge surfaces carry signatures and no prose. The scan is held to its root rather than to a citation count: there may legitimately be none one day, and "the tree moved and this read nothing" must not look like "every citation resolves". Its pattern carries the same leading boundary as the routing gate's, so the docs/ tail of a branch-pinned GitHub URL is not validated against the current worktree -- three sources already write such links, and this class deliberately leaves https:// targets alone. Verified by renaming the heading in the Javadoc and watching it go red naming both the source file and the page.
The cycle's entry was opened on develop by the composed-table-cell fix; this adds a Documentation subsection beside its Layout one. Without it the recipe sections and the LineBuilder.fill() Javadoc caveat would be invisible when the 2.4.0 release notes are assembled from this file, and both are the class of change this changelog already logs: v2.3.0's Documentation subsection carries a Javadoc-only preset-caps entry.
The rule was rewritten twice on reasoning and was wrong both times. This
version was measured: each heading below was posted to api.github.com/markdown
and the id it generated was read back.
Zebra<NBSP>alternating rows -> zebraalternating-rows
Row<EM SPACE>span -> rowspan
A<TAB>B -> ab
Item <U+2460> first -> item--first
Cafe<COMBINING ACUTE> rules -> café-rules
Zebra — alternating row fills -> zebra--alternating-row-fills
So GitHub *drops* every kind of whitespace except the ASCII space, rather than
hyphenating it, and drops a numeral that is not a decimal digit while keeping
a combining mark. The previous version hyphenated all Unicode whitespace and
kept every numeral, on the theory that GitHub would -- it does neither, and
the widening to \p{N} made `Item ① first` worse than the rule it replaced,
under a Javadoc paragraph that asserted the opposite. Both implementations now
carry the measured rule, and the table is asserted from both sides with the
values GitHub returned rather than the ones the code produces.
The em-dash case that started all of this is unaffected and still correct.
Two consequences fall out. Because non-ASCII whitespace is now removed by the
same class on both sides, the trim difference between JS and Java -- Unicode
one side, ASCII the other -- no longer changes any anchor, which closes the
last measured divergence between the twins. And both matchers were widened to
accept what the producers can now emit: an ASCII-only anchor group truncated
`#café-rules` to `#caf`, so the gate rejected a route whose link opens, while
the identical reference written in `docs:` passed because that path never went
through the regex.
The class contract that still described the deleted \w rule now describes this
one, and the orphaned comment left above the field is gone.
…pository Three defects in the path handling the previous commit added, all of them in the direction of failing a citation that is fine. A valid non-canonical spelling -- "./docs/x.md", "docs//x.md", both of which anchorRefsIn emits from prose -- was compared raw against canonical output and reported as "the citation's casing does not match" when nothing was mis-cased. The citation is normalised first now. The casing check realpath'd the cited file but not the repository root, so a checkout reached through a symlink, a bind mount or a Windows subst drive made every citation in the file look mis-cased: run under `subst W:` the gate reported ten problems and exited 1 against a clean run. Both sides of the comparison go through the same resolution. And nothing confined a citation to the checkout: "../docs/x.md#y" resolved outside it and, with a sibling tree present, was validated against a file in another repository while the gate printed "routes hold up". It is refused before the filesystem is touched, and again after realpath, so a symlink inside the tree cannot smuggle one out either.
…ctory The note that tells a bundle reader the cited page is not in the archive was suppressed by any directory named `docs` one level above the pack. The bundle is normally unpacked inside another project, and a project with documentation of its own is the common case -- so the note disappeared exactly where it was needed and handed the reader a GraphCompose path that appeared to resolve against their own tree. The self-sufficiency fixture could not see it either: it unpacks into an empty scratch directory. Each cited page is now resolved instead, which is what the note is about.
Replacing three named steps with a glob fixed hand-registration and opened two new ways to run nothing: the glob was one level deep, so a suite under test/<sub>/ would never run, and its only floor was "at least one exists", so deleting or moving a suite left the step green on the survivors. Since this step is now the only thing in the repository that executes a *.test.mjs, both failures look exactly like passing. Discovery is recursive, and the floor is a count that has to be raised deliberately: drop a suite and the step goes red instead of quietly covering less. Verified both directions locally against the real block.
Three symbols the new routes hand over -- DocumentRowColumn.fixed, RowBuilder.arrangement and ParagraphBuilder.align -- were taught in the page's prose and claimed nowhere, so the reverse index would not name that page when they change. A rename would still have gone red on the route's own symbol check; what was missing is the "which pages does this API change invalidate" answer the claims index exists to give. The tools README listed two of six directories and named the routing gate nowhere -- not check-routes.mjs, which decides whether a route merges, nor the two libraries it now rests on. It lists them, and says how a fixture suite gets run, since that is no longer visible from the workflow by name.
The last pass measured six headings against GitHub and got the rule right for
all six. It was still wrong twice, because the six all put the odd character
between two words.
Asking GitHub about the edges and the two numeral classes:
<EM SPACE><SPACE>A -> -a (not `a`: nothing is trimmed away)
A<SPACE><EM SPACE> -> a-
Chapter <U+2160> here -> chapter-ⅰ-here (a letter numeral survives)
a<U+203F>f b -> a‿f-b (a connector survives)
So the keep-set was too narrow -- \p{Nl} and \p{Pc} belong in it, and dropping
them was a regression over the \w rule this replaced -- and the JavaScript
trim() was wrong: it strips Unicode whitespace, GitHub strips none, and Java's
String.trim() stops at U+0020. That last one is the divergence the previous
commit's message claimed to have closed. It had not: the twins disagreed on
every heading with a non-ASCII space at either end, and the ten pinned inputs
could not see it because none of them put the space at an edge.
Both implementations now carry the measured rule, both tables pin the edges
and the numerals, and anchorOf's output was diffed against all eighteen
answers GitHub gave -- it matches every one. The prose and Javadoc matchers
were widened to accept what the producers can emit, since a citation of a
heading in any other script was being truncated.
Five ways the routing tools reported the wrong thing, none of which any existing route trips. api-query's packVersion() wrapped every surface in one try with a bare catch, so a single unparseable file returned null and the staleness note vanished from every answer while four good surfaces still carried the stamp -- fail-open in the copy that ships, where the fail-closed gate does not travel. The try is now per file. And renderTask forwarded only the "behind" verdict, so an unorderable version and one ahead of the surfaces printed exactly like a route re-read today; both are named now. check-routes reached Object.prototype for a constraint spelled `toString`, then died on it with a TypeError naming no route and losing every error collected so far. A `docs:` written as a bare string was iterated character by character -- one bogus "no such page" per letter, and the "no docs" guard satisfied by a non-empty string's length. Both say what is actually wrong now. And a root-relative citation (`/docs/x.md#y`) in prose matched nothing at all, so a dangling anchor spelled that way was never checked while the gate said every route holds up -- the identical string in `docs:` meanwhile failed as a casing mismatch. A single leading slash is accepted and normalised; `//` is still refused, because that is how the authority half of a URL gets in.
Discovery replaced hand-registration and traded one silent-skip mode for another: nothing pinned a suite by name any more, and the floor equalled the count, so renaming one out of the glob while adding another kept the arithmetic and stopped a suite running. The set is now compared against a declared list, so any change -- add, rename, delete -- goes red until the list says so too. That is hand-maintenance again, but gated: the previous version was hand-maintenance that failed open. release.yml ran none of the suites. build-bundle --verify asserts exit codes, which cannot see whether the version comparison inside the shipped bin/query.mjs is right, and that copy exists precisely because the bundle cannot import the shared module -- so at a tag the one thing pinning it never ran. It runs there now, beside the claims and routing gates that are already re-checked at the tagged commit for the same reason.
A `proof=test:` target resolved by class name only, so the gate caught a renamed class and nothing else: delete the one method whose assertions hold a behaviour and the claim goes on reading as proven, because the file is still there. That is the shape of rot this pack exists to stop -- a claim that looks held and is not. `test:Class#method` is now resolved too, and stays optional so no existing claim has to change. Verified by pointing a claim at a method that does not exist and watching it name both the method and the file.
…rything The check/failures/passes harness had been copy-pasted byte-identically into all four fixture suites -- four places to improve a failure message, and four chances for one to drift into saying something slightly different about the same kind of failure. It is one module now; the suites report identically and their counts are unchanged (23 / 18 / 35 / 33). The tools README's index listed twelve of the twenty-two modules. The six missing ones included the whole changeset tool, named in no tracked file at all, and api-surface/lib/surfaces.mjs, which knowledge/manifest.json points readers at by path. Every non-test module is listed now, and the note about how a fixture suite gets run says what the workflow actually does.
Two claims that were broader than the code behind them. The icon route called columns(auto(), weight(1)) "the only form that stays right when the column is resized". Measured at 135.7pt and 260pt, columns(fixed(13), weight(1)) gives 13.00pt at both — exactly as stable. The route's own alternative already objects to fixed() on duplication grounds, not stability, so the sentence contradicted the entry two lines below it. It now says what auto() is actually better than: weights(...), where the share is fixed and the icon's slot moves with the row. And `proof=test:Class#method` shipped with no claim using it, so the defect it was added for — a claim still reading as proven after the assertion holding it is deleted — was live for all eight proofs. The two claims that a single method genuinely holds now name it. Verified by renaming one away and watching check-claims report the claim false; the rest stay class-level because more than one method holds them, which is the honest shape.
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.
Why
A generated surface says what exists; it cannot say which of several ways is right. Two questions the pack could not answer produced two of the three dominant defects in an AI Flow CV run:
A row with no column spec splits its inner width into equal shares, one per child — content plays no part. So
row.add(icon.node(13)).addParagraph(...)gives a 13pt icon half the row and wraps the text into twice the lines. Nothing looks broken: the icon still draws at 13pt, it merely occupies five times the width it needs, and the paragraph pays in line count. In a 135.7pt sidebar that is a 63.85pt slot each.LineBuilder.horizontal(width)takes points, not a percentage — and nothing clips a line to the space it was given. Every neighbouring number is a ratio (weights(0.34, 0.66),leftColumn(0.34, ...)), sohorizontal(100)reads as "the full width"; it is drawn at 100pt, 32.15pt past the column, across whatever is beside it, with a clean render and no warning.Neither fact is visible in a signature, and
knowledge/routing/tasks.jsonis the only layer that can carry it.What the routes say
layout.icon-beside-textrecommendscolumns(auto(), weight(1)):auto()is the icon's own width,weight(1)is everything left, so the icon keeps its own size whatever the column is resized to.weights(...)is listed as the alternative it is not — a weight is a share, not a size, soweights(0.1, 0.9)at 135.7pt hands a 13pt icon a 12.77pt slot, and the figure moves with the row. (fixed(13)is as stable, and is the alternative for a rail of icons that must line up.)layout.rule-beside-a-headingrecommendsfill()in a weight column, with the two repairs that look right and are not: aflexSpacer()moves a fixed rule without sizing it, andfill()in a flex row takes the row's whole available width and ends further out than the rule it replaced.auto()column, leaving the weight column zero — the rule is not misplaced, it is not drawn.docs/recipes/layered-page-design.mdgains both sections with compiled fences (doc-example, notdoc-example-ignore), andLineBuilder.fill()'s Javadoc now names the flex-row case it used to promise away — that Javadoc is the only prose an API-first reader gets, since the surfaces carry signatures and nothing else.What the gate now does
Writing the routes exposed that the gate admitting them was wrong in ways no existing route happened to trip.
docs/— failing CI on a link that works and passing one that 404s. It is now measured, not derived: headings were posted toapi.github.com/markdownand the generated ids read back. GitHub keeps\p{L} \p{M} \p{Nd} \p{Nl} \p{Pc},-and the ASCII space, drops everything else including every other kind of whitespace, hyphenates only the ASCII space, and trims nothing.Zebra — alternating row fillsis#zebra--alternating-row-fills;A<TAB>Bis#ab;<EM SPACE><SPACE>Ais#-a.DocumentationLinkGuardTest.slugandrouting/lib/anchors.mjs— and cannot call itself across the language boundary, so both assert the same measured table and a change to either reddens its own fixture.docs::intent,recommendedBecauseand every alternative'suseWhen/tradeoffsgo through one resolver, so a renamed heading cannot be fixed in the gated copy and left dangling in the prose an agent actually reads.2.4.0while a route signed during the cycle still reads2.4.0-SNAPSHOT; string inequality reported the whole file stale on every release, which is the same as reporting none of it. A version that cannot be ordered, or one ahead of the surfaces, is now a failure rather than a note.api-querycarries the pack's own version with the answer. It ships in the bundle withoutcheck-routes, so a barechecked against: 2.2.3-SNAPSHOTgave a reader nothing to compare against. Its copy of the ordering rule is deliberate —build-bundlepublishes that one file asbin/query.mjswith nothing beside it — andpack-version.test.mjsruns the same table through both so the copy cannot drift.docs:killed the gate with anEISDIRstack trace naming no route; a constraint spelledtoStringreachedObject.prototypeand died on it, losing every error collected so far; adocs:written as a bare string was walked character by character; a mis-cased citation passed on Windows to fail on the runner as "no such page"; a root-relative/docs/x.md#yin prose matched nothing at all and was never checked.proof=test:Class#method— a proof resolved by class name only, so deleting the one method that held a behaviour left the claim reading as proven. The two claims a single method genuinely holds now name it; the rest stay class-level because more than one method holds them.release.ymlruns them too, sincebuild-bundle --verifyasserts exit codes and cannot see whether the shipped copy orders versions correctly.Verification
./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am→ BUILD SUCCESS.Knowledge gates:
check-routesexit 0 (5 routes),check-claims --checkcurrent (46 claims, 8 proofs),build-bundle --verifyanswers with no repository present. Fixture suites 23 / 18 / 35 / 33.New tests, and what each pins:
RowWidthDistributionContractTest(7) — the even split and its cost in line count;auto()+weight(1)as the repair; a grow spacer and every non-STARTarrangement entering the same intrinsic-sizing path;STARTas the boundary that does not.LineWidthUnitsContractTest(8) —horizontal(n)is points and is not clipped;fill()spans its slot; a spacer neither shrinks a fixed rule nor givesfill()a slot; a flex row and a weight column are refused together; an aligned heading collapses the rule to zero width.DocumentationLinkGuardTest(+2 methods) — the onedocs/*.md#anchorcited from production Java resolves (the guard is fail-closed on the scan root, not on citation count), and the anchor rule agrees with its JavaScript twin on the measured table.anchors.test.mjs,pack-version.test.mjs— new suites for the two libraries.The geometry assertions are slot arithmetic, not font metrics: the left-hand child of a flex row is a fixed-width shape, and where a coordinate genuinely is font-dependent — a non-
STARTarrangement spreads the leftover as a leading offset — only the widths are asserted, and the class says so.Notes for review
verifiedAgainst: 2.2.3-SNAPSHOT. The new ordering makes them announce themselves as not re-read, which is honest — re-stamping them without actually re-reading would be a false attestation. Left for a deliberate pass.fill()in a flex row still overflows. It is documented, claimed and pinned by a test rather than fixed:RowSlots.growOfonly recognisesSpacerNode, so a repair is a new engine rule that moves rendered output for existing documents (TocBuilderalready composes afill()leader) — a behaviour decision, not an omission.src/mainedit is Javadoc, soknowledge/api/needed no regeneration. The CHANGELOG entry sits under develop's existing## v2.4.0 — Planned, beside its### Layout.Lane: canonical (
knowledge/routing +docs/recipes) with a shared-engine read-only touch — the two contract tests assertRowSlotsbehaviour without changing it.