Repository navigation
Guide figures render with a bundled font instead of the host's - #5668
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
506b447 to
8a91850
Compare
Cloudflare Preview
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a91850b97
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de62de4b98
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2989351749
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 365153a366
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
691841e to
2e7dd10
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e7dd10fe6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
2e7dd10 to
f02771e
Compare
6832347 to
1bc8e6a
Compare
212956c to
6d0560c
Compare
1bc8e6a to
ae956d3
Compare
b96d132 to
d82c0ea
Compare
ae956d3 to
eecf05d
Compare
d82c0ea to
42189c3
Compare
335a54a to
f915089
Compare
b88150d to
5028cb2
Compare
facd9ed to
15e0c1e
Compare
None of the 24 generated figures could be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels differing. Two consecutive local runs are byte-identical, so the generator is deterministic on one host; the variable is the font. `PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes and every glyph changes. The differences are exactly that: for `flow-layout.png` every differing pixel sits in y 10-130, the title and label rows, while the colored blocks below match to the pixel. So the byte-exact gate has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and at the scale the guide's remaining ~260 app screenshots would need, that is untenable. The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont` reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath rather than from an installed-font lookup. This is also what the project's font rule requires everywhere: never `createSystemFont`, always the `native:` scheme. A null return refuses loudly rather than falling back to a host font, because a silent fallback would restore the exact non-determinism this removes. The committed baselines are regenerated from a Mac. CI byte-compares them on Linux, so the check either passes -- proving host independence rather than asserting it -- or fails and says so immediately. Composition, dimensions and colours are unchanged; only the typeface moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only one containing a `TextField`. The other 23 matched Linux byte for byte, so the bundled-font change worked -- it just did not reach far enough. `applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the theme's default font, which resolves through the host, so the text field and its hint were still host-dependent. Enumerating the types that carry text would have left the next one added broken the same way, so the walk now pins the face on every component it visits, plus the hint label, which is painted by a `Label` that is not in the component tree and so is never reached by the walk. The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px against the theme default's 13px the field grew and squeezed "Submit" down to a clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the composition is unchanged and only the typeface moves. Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed at a family that does not exist -- which is what a machine without Arial looks like to the port. All 24 come out byte-identical, so nothing in these figures reads an installed font any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so there is no way to tell a real regression from an environment difference without adding a debugging round trip to CI. The generated directory is now uploaded as an artifact when the step fails. Needed immediately: one figure still differs between a Mac and the runner after the font fix, and host fonts, JDK version and working directory have each been ruled out locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two review findings on this PR. The first says `native:MainRegular` does not give host-independent output, because `loadTrueTypeFont` resolves native fonts to the first installed SF or Helvetica family before reaching the bundled Roboto. The branch is real, but it is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose systemFontFamily contains "helvetica" -- and this generator never loads a skin. The measurement agrees: figures rendered on a Mac match the Linux runner byte for byte, which could not happen if one side were resolving Helvetica Neue and the other Roboto. So the conclusion does not hold today, but the risk is real for tomorrow: a change that loads a skin here would put host fonts back into the output with no other symptom. The generator now refuses to run under an iOS platform, and says why, rather than leaving that to a comment nobody reads. The second finding is straightforwardly right and is the more serious of the two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the `Determine changed components` filter named only two scripts, so a pull request touching any other script here started the workflow with `docs` false and skipped the steps that script governs. A change to compare-screenshots.py could have merged without the screenshot check ever running it -- a gate that skips itself. The filter now covers the whole directory, which also removes the two-copies-drift the surrounding comment already warns about. Verified the guard changes no output: all 24 figures are unchanged, and the comparer still passes against the runner's own bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a sidecar written the way this one was -- a large delta bound paired with a small area bound -- that leaves an unbounded hole, and review gave the exploit in the figures' own palette: recolouring the green #06a806 to #a608a6 moves every channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and a dramatically different image reports zero mismatches. Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the image, at a worst delta of exactly 160 -- and passed. The two bounds are now independent. maxMismatchPercent limits how much of the image may change at all, counting every differing pixel; maxChannelDelta caps how far any single pixel may move. The measured legitimate noise -- 173 pixels, 0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a five-pixel solid overwrite fails on delta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts toward the area budget and the channel delta is a separate ceiling. The sidecar still explained the old behaviour -- that the area bound did the work and the delta was meaningless on its own -- which is now wrong in a file whose whole purpose is to justify the numbers beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The committed figures are genuine RGBA PNGs -- four channels, currently opaque everywhere -- so that dropped a real channel, and any change confined to alpha was invisible to the tolerance path. A regression that turned the whole figure transparent while leaving every colour channel intact reported zero changed pixels and passed. Byte equality, which every figure without a sidecar is still held to, always caught this. Only a figure carrying a sidecar could reach the weakened path, so today the exposure was one image -- but that image is exactly the one whose comparison is relaxed. Verified: making layered-layout.png fully transparent with its RGB channels untouched now reports 100% of pixels changed, the real runner output still passes, and the recolour and untoleranced-difference probes still fail as they did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
15e0c1e to
815afcf
Compare
* Guide figures render with a bundled font instead of the host's
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Pin the font on every component, not just the ones that get block colours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Publish what the runner actually rendered when a screenshot fails
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Pin MigLayout's platform, and stop demanding byte equality of a glyph edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Guard the iOS font branch, and stop the comparator gating itself out
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Count every changed pixel toward the area budget, not just the loud ones
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Correct the sidecar's rationale to match how the bounds now work
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Compare figures in RGBA, because they have an alpha channel
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Gates for the guide defects Asciidoctor cannot see
Four checks, split out of the structural repairs they were written alongside so
each can be reviewed on its own terms. Every one was verified to fail when the
defect it describes is reintroduced, and to pass again when it is reverted.
check-guide-structure.py -- every document is in the book or declared out of it;
no chapter silently becomes a part, a subsection, or a duplicate; and, by
rendering the book and reading its headings back, every included chapter's title
survives into the output. That last check is what would have caught a whole
chapter rendering as subsections of the one before it.
check-guide-xrefs.py -- every internal link resolves, and none renders as a bare
"[some-id]". Asciidoctor reports neither: a reference carrying link text renders
as an ordinary link to nowhere.
check-missing-code-blocks.py -- prose that promises a listing where a hole
follows. 406 exist, from the snippet extraction in bbdc6058f0, recoverable from
bbdc6058f0~1.
check-guide-links.py -- links checked against the paths the site actually
serves, derived from _redirects and the Hugo content tree and followed through
every redirect rule to what it produces, rather than accepted for matching one.
The two ratchets may only shrink; growing one needs --allow-new, so recording
debt is deliberate rather than a side effect of regenerating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Bank the four holes the constraints chapter no longer has
The stacked content change restored the video capture constraints examples, so
their baseline entries went stale and the ratchet refused to pass until they
were removed. 406 to 402.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* An unrelated subsection must not stand in for a swallowed chapter
The outcome check compared counts of normalized headings at any depth, which a
collision satisfies: "Analytics" is a chapter and also a subsection of Commerce,
so the rendered book contains it twice. If the chapter were swallowed, the
subsection kept the count at one and the check -- the central safeguard of this
change -- passed. Measured on the current book: analytics appears twice at any
level and once at chapter level; "getting started" four times at any level and
never at chapter level.
Manifest entries are now counted against chapter-level headings only. Nested
fragments still use the any-level count, because their depth is whatever their
parent gives them.
Second finding: resolves() tried every matching redirect rule and passed if any
chain worked. The host applies the FIRST match and stops, so a link matching an
early rule that leads somewhere deleted would have passed on the strength of a
later rule the reader never reaches. It now returns the first match's result. No
guide link changes verdict today; the point is that the model matches the
behaviour it claims to model.
Also declined, with the reasoning left in the code rather than a review thread:
adding Markdown's three-backtick fence to the literal-block list. The guide
contains none, and validate-guide-snippets.py requires every listing to be
[source,LANG] with a bare include:: inside ---- delimiters, so such a block
would fail that gate before reaching this one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The ifeval branch of the conditional test could never match
The pattern required empty brackets, which is right for ifdef and ifndef -- with
content they are the single-line form and guard only that line -- but wrong for
ifeval, which has no single-line form and always carries its expression in the
brackets. So the directive was listed and never recognised.
Latent: the manifest contains no ifeval, and its only conditionals are two
single-line ifdef attribute assignments, which are still correctly treated as
not opening a block. Fixed because a pattern that names three directives and
handles two is a bug in what the code claims, not a missing feature.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record why commented-out URLs are checked like any other
Review asked for AsciiDoc comment-state tracking so that a URL inside a `//`
line or a `////` block is skipped. Declining, and recording it where the next
reader of this loop will look rather than in a review thread nobody reads.
The guide has 120 files, zero commented-out URLs and zero `////` blocks, so the
tracking would govern nothing that exists. It would also weaken the gate: with
comments skipped, commenting a line out makes its finding disappear and lets the
ratchet shrink, banking a "fix" while the dead link stays in the source waiting
to be uncommented. Deleting the link is the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Four links left the book to re-enter it, and nothing checked the anchor
Review asked for fragment validation on same-site URLs. Measuring first: the
guide holds 42 same-site URLs with a fragment, and 38 are /javadoc/ -- the one
tree this script cannot enumerate, because it is generated from the framework
sources at build time. The other four all pointed at
`developer-guide.html#<anchor>`: this book, linking into its own body.
Those are the same defect as the fifteen `/manual/*.html` deep links already
converted in this stack, missed because they use a different path. They send a
reader out to the web to fetch the page they are reading, they do not work in
the PDF, and a renamed section breaks them silently -- the anchor lives in the
rendered book, so no amount of site-path checking can see it.
So the fix is not to validate the fragment, it is to stop writing the link:
all four become `<<anchor,text>>` xrefs, which check-guide-xrefs.py already
resolves against the rendered anchors. All three targets exist today, so this
repairs no live 404 -- it moves four unguarded links under a gate.
The rule closes the class rather than the four instances: any codenameone.com
link whose path is one of this book's own routes and which carries a fragment is
now reported. Verified non-vacuous by restoring one of the four and watching it
fail, then reverting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Two silent blind spots in the guards become loud refusals
Both findings describe constructs the guide does not contain, so neither is a
live defect. Both are real holes in the checkers though, and each closes for a
few lines, so they close rather than getting argued with.
Conditional includes (check-guide-structure.py). A manifest entry inside an
ifdef/ifeval was skipped by the duplicate count -- correctly, since the same
chapter under two exclusive branches is one chapter in the output -- and also by
the rendered-title check, since only one branch renders. Skipping both means a
chapter included twice inside a single ACTIVE branch would pass silently. Rather
than teach the checker to distinguish exclusive branches for a construct that
does not exist (measured: zero include:: lines sit inside a conditional anywhere
in the guide), a conditional manifest entry is now refused outright, with a
message saying what to extend if one is ever wanted.
Trailing slashes on file paths (check-guide-links.py). Site paths were compared
with the trailing slash stripped, so a link to `/x.jar/` was validated against
`/x.jar`. The reviewer's premise checks out: _redirects declares the two forms as
separate routes and spells both out where both work, 32 such pairs. Preserving
the slash everywhere would break the many legitimate directory links, so the
narrow case is reported instead -- a last path segment containing a dot, wearing
a trailing slash. Directory routes such as /blog/ and /javadoc/com/codename1/io/
have no dot in the last segment and are untouched.
Both rules verified non-vacuous by introducing an instance and reading the real
exit status rather than a pipeline's: structure and links each exit 1 on the
probe and 0 once it is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Check what swallows a heading, not whether its title turns up somewhere
Review is right that the rendered-title check has slack: it counts normalized
titles across the whole book, so a nested fragment losing its heading can be
covered by an identical heading elsewhere. Confirmed rather than accepted --
removing the leveloffset from both Maven includes takes "Getting started" from 4
rendered headings to 3, while only one file declares it, so `3 >= 1` and the gate
passed a genuinely swallowed section.
Rather than give nested fragments a rendered identity, which the HTML does not
carry, this checks the cause. The spacing rule already existed but ran only on
the root manifest, and its condition was "a blank line follows". Both were wrong.
Reproduced minimally to find the real rule -- a heading is absorbed only when ALL
of these hold:
* the include is followed immediately by content, and
* neither this include nor the following one carries leveloffset (asciidoctor
brackets the content with :leveloffset: attribute entries, and an attribute
entry closes the paragraph -- the one on the FOLLOWING include lands between
the paragraph and the heading, so it protects just as well), and
* the included file does not end on a blank line, and
* its last line is ordinary paragraph text -- a delimiter, table row, heading,
attribute entry or comment all close the paragraph. That is why
_generated-build-hints.adoc, which ends on "|===", does not eat the
"Versioned builds" heading directly after it.
Against the real book, all four combinations now agree with what asciidoctor
actually renders:
both leveloffset 4 headings renders ok gate passes
only current 4 headings renders ok gate passes
only following 4 headings renders ok gate passes
neither 3 headings SWALLOWED gate fails
An earlier draft exempted only the current include and failed the third row --
a false positive on markup that renders correctly. Every guide include is now
checked, not just the manifest's: measured across all 120 files, this leaves zero
findings, because the five adjacent includes in Maven-Project-Workflow.asciidoc
are protected by leveloffset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The slash rule rejected routes the site declares, and Hugo could move the rest
Three link findings, and only the first was a live defect -- in the rule I added
one commit earlier.
The trailing-slash rule was unconditional, so it reported "/videos.html/" as
broken even though _redirects declares that exact source and the site serves it.
The rules here all compile slash-insensitively ("^...$/?"), which is right for
matching but discards the distinction the site actually draws, so the raw sources
are now collected alongside them and a slashed file path is reported only when
_redirects does not spell it out. Probed both ways: "/videos.html/" passes,
"/files/CodenameOneBuildClient.jar/" fails.
Hugo route overrides. hugo.toml sets neither [permalinks] nor uglyURLs today, so
deriving a route as "section path + slug" is currently correct; if either
appears, every route underneath moves and this would go on accepting links to
paths Hugo no longer publishes. Modelling a configuration that is not there would
be guesswork, and reading the built public/ tree makes a local run depend on a
tree that may be stale or absent. So it now notices instead: either key present
aborts the check with what to do about it. Probed with each key in turn.
Protocol-relative links and fragments on ordinary same-site pages are declined,
with the measurements recorded in the code rather than in a review thread nobody
reads. The guide has no `link://` macro at all, and its one bare "//host/path" is
a JavaScript string inside a source block, so widening URL_RE to match "//" would
start reporting code as a broken link. And of the 42 same-site URLs carrying a
fragment, 38 are /javadoc/ -- generated at build time, exempt for that reason --
while the other four pointed into this book and are now xrefs, which
check-guide-xrefs.py resolves against the rendered anchors. Nothing is left that
this script could check without building the Hugo site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The PDF branch was never rendered, and an admonition counted as a listing
Two more holes in the guards. Neither has an instance in the guide today, and
both close cheaply enough that arguing about them would cost more.
check-guide-xrefs.py rendered the book once, with the default backend, so
anything inside ifdef::backend-pdf[] was dropped before a single reference in it
could be examined -- while the workflow publishes an asciidoctor-pdf build from
the same source. Native-Themes.asciidoc already has two such branches. It now
renders twice, the second time with -a backend-pdf, which selects exactly the
content the PDF build includes without needing asciidoctor-pdf here. The two runs
are checked SEPARATELY rather than pooled: an anchor that exists only in the HTML
branch must not satisfy a reference made in the PDF branch. Findings name the
branch they came from.
A/B with a dangling xref planted inside the backend-pdf branch of
Native-Themes.asciidoc:
old "Cross-references OK: 1741 anchors" -- exit 0
new "Native-Themes.asciidoc:506: <<definitely-not-a-real-anchor>> ...
(1 reference(s), pdf render)" -- exit 1
check-missing-code-blocks.py accepted any `[attribute]` line as the start of the
promised block, so an admonition standing where a listing used to be hid the
hole. An admonition is prose and can never be the listing a sentence promised, so
the five names are excluded. Excluded by name rather than by whitelisting the
kinds that ARE code: measured, the bracket lines legitimately answering a
promising sentence already span [source] (582), [cols=...] and [options=...] (56),
[listing] (3), [quote] (2) and an anchored image, and a whitelist would report the
next kind nobody anticipated.
A/B with each block planted after a promising sentence, using the real signature
of a removed listing -- the two blank lines it leaves behind:
[NOTE] old: none new new: reported as a hole
[source,java] old: none new new: none new
The baseline is unchanged at 402, because the guide currently has no admonition
sitting in a listing's place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Three links from the old wiki went nowhere, and a directive is not a separator
Two findings, and this time the first one is live.
The rendered book contains exactly three relative hrefs, and all three are
broken: css.asciidoc carried link:Images[], link:Fonts[] and
link:Supported-Properties#text-decoration[] over from the wiki this guide
replaced, naming pages that were never carried across with them. The guide
renders as a single page, so a relative href resolves against wherever that page
is served and reaches nothing that ships with it -- the reader gets a 404 while
every gate reported success. All three targets are sections in that same file:
[[Images]] already existed, [[Fonts]] is added to match it, and
[[text-decoration]] was already there and already used by a sibling reference.
check-guide-xrefs.py now reports any relative href, which after this change has
zero instances. Absolute URLs stay with check-guide-links.py, and root-relative
paths and fragments are excluded. A/B with one link restored: old printed
"Cross-references OK" and exited 0, new reports it and exits 1.
The second is a wrong claim in my own comment. check-guide-structure.py returned
early when an include was followed by ifdef/ifndef/ifeval/endif, on the reasoning
that "a preprocessor directive is not content and cannot absorb a paragraph".
True, and irrelevant: asciidoctor REMOVES the directive during preprocessing, so
it does not separate anything either. Measured -- with `ifdef::backend-html5[]`
between an include ending in prose and a following heading, the render contains
zero heading tags and the paragraph reads "...preferred tools. === A parent-local
heading". The rule now scans past directive lines to the first line that survives
preprocessing. A/B on that construct: old passed it, new reports it.
Note the rendered-title check cannot cover this case either, because a
parent-local heading is not the first heading of any included file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* A Cloudflare function serves /files and /demos, and the PDF surrogate has a limit
The first finding was live: the link ratchet held a working link.
docs/website/functions/[[path]].js runs after context.next() has already 404ed
and then redirects anything under /files/ or /demos/ to
download.codenameone.com. Those paths are served, so the model was missing a
whole class of route and had banked a real one -- /files/iOS_UI-Kit.psd -- as
broken. The destination is off-site, which puts it in the same bucket as every
other off-site redirect: reachable, not verifiable from this repository. The
rules are appended AFTER the _redirects rules because the function is a fallback
and the first matching rule wins, mirroring how the host evaluates. The baseline
shrinks 37 -> 36, and a new /files/ link is now accepted rather than failing CI.
The second is a real limit of the PDF surrogate, and one the command line cannot
remove. Setting backend-pdf makes ifndef::backend-pdf[] content disappear and
ifdef::backend-pdf[] content appear -- verified, and that is the only form this
guide uses. It does not undefine backend-html5: the HTML converter sets that
itself, after command-line attributes are applied, so even `-a backend-html5!`
leaves it defined. Measured both ways. Content guarded on the HTML backend would
therefore survive into the surrogate render and could satisfy a PDF-only
reference that the real asciidoctor-pdf build leaves dangling.
The guide has no such conditional, so rather than model a construct that is not
there -- or drive asciidoctor-pdf and try to read anchors back out of a PDF --
the checker refuses one if it appears, naming the alternative the rest of the
guide already uses. Probed: planting an ifdef::backend-html5[] block aborts the
check with that message.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* A five-dash block is still a literal block, and both scanners read inside one
Both scanners matched a delimiter of exactly four characters, so a block opened
with ----- was invisible to them and its contents were scanned as live markup.
The guide has 20 such lines -- eight in Desktop-Integration, six in
Working-With-iOS, six in appendix_goal_generate_archetype -- every one an
ordinary [source] block wrapping an include::. validate-guide-snippets.py already
accepted -{4,}; these two had drifted from it.
Nothing is misread today, because those includes name .java, .xml and .properties
files and the reachability walk skips non-asciidoc targets. The failure mode is
still the interesting one though: it is a FALSE POSITIVE generator, not a missed
defect. A/B with each construct planted inside a five-dash block:
a displayed include::Working-with-UWP.asciidoc[]
old reported a swallowed-heading error against markup that only appears
on the page as text
new clean
a displayed "The wrapper looks like this:" followed by blank lines
old reported it as a missing code block
new clean
Both now track the delimiter that opened the block and close on one of the same
character AND length, which is what asciidoctor does -- so a four-dash line
inside a five-dash block is content rather than the close. The set stays limited
to literal blocks: an example (====) or sidebar (****) block contains live
markup, so headings and includes inside one are real and must keep counting.
Baselines unchanged: 119 included documents, 402 holes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Guide figures render with a bundled font instead of the host's (#5668)
* Guide figures render with a bundled font instead of the host's
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Pin the font on every component, not just the ones that get block colours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Publish what the runner actually rendered when a screenshot fails
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Pin MigLayout's platform, and stop demanding byte equality of a glyph edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Guard the iOS font branch, and stop the comparator gating itself out
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Count every changed pixel toward the area budget, not just the loud ones
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Correct the sidecar's rationale to match how the bounds now work
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Compare figures in RGBA, because they have an alpha channel
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* A comment block is not content, and img/ really does ship beside the page
Two findings, one of them a false positive I introduced last round.
Comment blocks. Asciidoctor drops a //// block entirely, so an include inside
one does not put its target in the book and prose inside one promises the reader
nothing -- but both scanners read straight through. //// joins the fence set in
each. A/B with each construct planted inside a comment block: a displayed
include:: made the structure check fail, and a promising sentence made the
missing-block check fail; both are clean now.
Note this is deliberately NOT symmetric with check-guide-links.py, which still
reads URLs inside comments. The reasons differ and the code says so: a dead link
commented out is still dead debt in the source, and letting the ratchet shrink
for it would make "comment the line out" a way to silence that gate. Reachability
is the opposite -- calling a commented-out include reachable states something
about the book that is simply untrue.
Packaged assets. The relative-href rule added last round was too broad: the HTML
packaging step copies every subdirectory of docs/developer-guide next to
developer-guide.html, so link:img/example.png[] resolves for a reader who opens
the zip, and the gate would have rejected it. A relative href that names an
existing file under the guide directory is now allowed. sketch/ is not, because
that is the one directory the packaging loop skips.
Probed all four ways, since an exemption is only worth having if it still
rejects:
link:img/flow-layout.png[] exit 0 (real, packaged)
link:img/does-not-exist.png[] exit 1
link:sketch/whatever.svg[] exit 1 (not packaged)
link:Supported-Properties[] exit 1 (the wiki-era shape)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The packaging exemption trusted the repository, not the published output
Both findings are defects in the two rules added in the previous commit.
packaged_asset() asked whether the file exists here, which is not the question. A
link only works if the published output carries it, and the two outputs filter
differently:
* the HTML archive copies only the SUBDIRECTORIES of docs/developer-guide next
to developer-guide.html and skips sketch, so a root-level file is never in the
zip however much it exists in the tree;
* the website rsync excludes sketch/, *.asciidoc and *.adoc, so a source file
nested inside a packaged directory still does not reach the site.
So link:Introduction.asciidoc[] was being exempted and readers would follow it to
nothing. Both filters are now reproduced. The permissive direction is the
expensive one here -- it suppresses a finding for a link nobody can follow --
so all four cases are probed:
img/flow-layout.png exit 0 in both outputs
Introduction.asciidoc exit 1 in neither
img/does-not-exist.png exit 1
sketch/foo.svg exit 1 excluded by both
The backend-conditional rejection scanned raw lines, so a source block DISPLAYING
`ifdef::backend-html5[]` as example text aborted the whole gate. A displayed
directive takes no part in preprocessing. It now tracks literal and comment block
delimiters the way the other guide scanners do. A/B: displayed inside a source
block went 1 -> 0, while a genuinely active conditional still exits 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The Introduction taught a dead IDE menu, an iTunes walkthrough and 2018 statistics (#5671)
* The Introduction taught a dead IDE menu, an iTunes walkthrough and 2018 statistics
Six corrections to the chapter a new developer reads first:
* The JavaScript target was described twice and contradicted itself: the
overview said TeaVM did the translation, the port section said ParparVM with
TeaVM as a fallback. The port section is right -- `javascript.port` is
declared with `parparvm` as its default and `teavm` documented as "the
original builder as a compatibility fallback".
* `http://teavm.org:[TeaVM-based builder]` -- the stray colon made the macro a
bare host, and the site serves TLS. Removed from the link ratchet, which now
stands at 37.
* Bitcode was cited as something ParparVM absorbed without modification. Apple
has since withdrawn bitcode, which makes the point better than the arrival
did, so the sentence now names both moves.
* The device-fragmentation section rested on two 2018 Android share numbers,
followed by a sentence conceding they would be stale on arrival. The
structural claim survives without them.
* The iOS developer fee was dated with "for 10 years at the time of this
writing".
* The UDID instructions walked the reader through iTunes, with a screenshot of
iTunes on iOS 9.3.5 that also exposed a real device serial and UDID. iTunes
has not existed on macOS since Catalina, and the signing chapter already gives
the current answer, so this now points there.
* The device build was "a right click away" via the IDE plugin, illustrated by a
menu still offering Blackberry, J2ME and Windows Phone builds. Replaced with
the `cn1:buildAndroid` and `cn1:buildIos` goals, taken from the mojo names.
Both screenshots are deleted, so find_unused_images.py stays satisfied.
Verified: asciidoctor at --failure-level WARN, Vale at suggestion level,
LanguageTool (status ok, 0 matches, run under JDK 17 rather than the JDK 8
false green), paragraph capitalization, snippets, xrefs, structure, links and
missing-code-blocks all clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* "-pl common" made a device build a no-op that reported success
Review caught the command I had just written. AbstractBuildWrapperMojo.execute()
opens with:
if (!project.isExecutionRoot()) {
getLog().info("Skipping execution for non-root project");
return;
}
and BuildAndroidMojo, BuildIosMojo and BuildWin32Mojo all inherit it unchanged.
Under `-pl common` the selected module is not the execution root, so the goal
logs that line and stops. The design is deliberate -- the wrapper re-invokes
Maven on the root pom itself (`request.setPomFile(new File("pom.xml"))`) -- so
naming a module both skips the wrapper and defeats its purpose.
Measured rather than reasoned, against scripts/hellocodenameone:
mvn -pl common ...:buildWin32 -> "Skipping execution for non-root project"
BUILD SUCCESS, nothing built
mvn ...:buildWin32 -> no skip line, proceeds into the reactor
A silent success is the worst failure mode for a getting-started instruction, so
the Introduction now says where to run it and warns about the module form
explicitly.
Working-With-Windows carried the same `-pl common` shape and is fixed with it --
it is where I copied the form from, so leaving it would reintroduce the defect
the next time someone follows the pattern.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The Windows build wrappers named a platform no profile answers to
Review checked the command this PR documents and found it still builds nothing.
The wrapper passes `codename1.platform` to its nested Maven run, and that
property is what activates the module profile in a generated project's root pom.
The profile is `win`, matching the value the win module itself declares:
<profile><id>win</id>
<activation><property>
<name>codename1.platform</name><value>win</value>
</property></activation>
<modules><module>win</module></modules>
BuildWin32Mojo and BuildWindowsDeviceMojo both passed "windows", which matches no
profile at all. Nothing else in the plugin reads the platform as "windows", so
the value was simply inert: the win module never joined the reactor and the
nested build reported success having produced no Windows binary. Every other
wrapper already agrees with its profile -- android, ios, javascript, linux and
javase all match -- so these two were the only ones out of step.
Verified by A/B against scripts/hellocodenameone, reading the NESTED reactor
rather than the outer one:
before hellocodenameone, -common, -javase <- no win module
after hellocodenameone, -common, -win
The build TARGET is a separate namespace and stays "windows-device"
(Executor.BUILD_TARGET_WINDOWS_NATIVE), which is correct.
SpotBugs over codenameone-maven-plugin regenerated: 0 findings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The primary Win32 command had both defects the convenience goal just lost
Fixing BuildWin32Mojo made the sentence beside this snippet false: the goal now
submits a build, while the command the chapter leads with still could not. It
carried both defects at once --
mvn -pl common package -Dcodename1.platform=windows ...
-- `-pl common` builds that module instead of the reactor, so the win module is
never reached, and `windows` matches no profile even when the reactor is whole.
The two mistakes hid each other: with only the module selected, the platform
value had nothing left to activate.
Corrected to what the wrapper actually runs, read off AbstractBuildWrapperMojo:
goal `package`, `codename1.platform=win`, `codename1.buildTarget=windows-device`,
from the project root.
A/B against scripts/hellocodenameone, reading the reactor, which Maven prints
before it compiles:
old no win module in the reactor at all
new hellocodenameone, -common, -win
Both invocations then fail identically on this machine with "invalid target
release: 17", because tools/env.sh pins JDK 8 while the demo targets 17. That is
environmental and equal on both sides, so it does not affect the comparison.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Copyright headers for the two Windows wrappers, and duplicate includes per parent
check-copyright-headers went red once #5671 merged in: BuildWin32Mojo and
BuildWindowsDeviceMojo have carried no header since they were added in #5144, and
editing them brought them into the diff the gate examines. Both take the Codename
One GPL + Classpath header, copied from BuildMacNativeMojo in the same package.
I had run that gate locally and read "8 file(s) passed", which was worthless: I
ran it BEFORE `git add`, and it diffs committed refs, so the two files it needed
to see were still sitting unstaged in the working tree. A gate run against HEAD
while the change is uncommitted answers a question about the previous commit.
Separately, review is right that duplicate detection only covered the manifest.
The counter was keyed by target and incremented for root edges alone, so a nested
include repeated in the SAME parent was invisible -- and _visit() returns early on
a revisit, so the second edge left no trace at all. The outcome check then saw one
declaration against two rendered titles and passed, because it only asks whether a
title appears AT LEAST as often as it is declared.
Now keyed by (parent, target) and counted for every unconditional edge at any
depth. That keeps the distinction that matters: one parent including one file
twice renders it twice and is a defect; two different parents including the same
fragment is what a fragment is for. Conditional edges stay excluded, at the root
and nested, since two mutually exclusive branches are one rendering.
Probed all three:
nested duplicate, same parent old 0 -> new 1
root duplicate old 1 -> new 1 (regression intact)
same fragment from a 2nd parent new 0 (still allowed)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Refuse a conditional include at every depth, not only in the manifest
The previous commit refused conditional includes under `path == self.root` and
left exactly that hole one level down: a nested parent including the same file
twice inside one ACTIVE ifdef recorded neither edge, _visit() deduplicated the
second target, and the rendered-title check only requires the title once. Two
copies render and nothing objects.
The refusal now applies wherever the include sits. Nothing in the guide is
conditional -- still measured at zero include:: lines inside a conditional
anywhere -- and neither check that matters can validate one: the edge count has
to skip a conditional edge because the same file under two exclusive branches is
one rendering, and the rendered-title check has to skip it for the same reason.
Skipping both silently is what let the duplicate through, so the construct is
refused while nothing uses it.
The message also stops hardcoding "developer-guide.asciidoc" as the filename,
which was wrong for every nested parent.
nested duplicate inside one conditional old 0 -> new 1
root conditional old 1 -> new 1 (regression intact)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* A typo in the authority hid behind a correct hostname
urlsplit().hostname drops the port whether or not it is a number, so
https://www.codenameone.com:notaport/getting-started/ presented itself as an
ordinary same-site URL and every check below passed on something no browser can
open. Reading .port is what surfaces it: urlsplit defers that parse until the
attribute is read, and then raises.
A numeric nonstandard port is the quieter version of the same problem. The route
model here is derived from _redirects, the Hugo content tree and the static tree,
and all of that describes the site on its default port; :8443 is a different
endpoint the model says nothing about, so validating the path against it would be
accepting an unchecked URL.
Local services keep their ports. The guide documents http://localhost:11434
deliberately -- it is the Ollama endpoint in the AI chapter, and the one URL in
the guide with a port at all -- so the rule is scoped to SITE_HOSTS and leaves
LOCAL_HOSTS alone.
https://www.codenameone.com:notaport/getting-started/ old 0 -> new 1
https://www.codenameone.com:8443/getting-started/ old 0 -> new 1
https://www.codenameone.com/getting-started/ new 0
http://localhost:11434/api/tags new 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* A listing hides content from the reader, not directives from the preprocessor
Three findings, and chasing the middle one turned up that I had been patching on
a false premise for several commits.
I had been treating a literal block as a place where an include:: or an ifdef::
is merely DISPLAYED. It is not. Measured against asciidoctor rather than assumed:
inside ---- / ----- / .... inside ////
include:: PROCESSED dropped
ifdef:: / ifeval:: ACTIVE dropped
heading / prose literal text dropped
Preprocessor directives are resolved before block parsing, so one written inside
a listing fires exactly as it would outside it; only a comment block removes it.
A literal block hides CONTENT, which is a different question.
So the scanners now split the two. first_heading() and the missing-block scan
keep the full fence set -- a heading or a promising sentence inside a listing
really is only text. The include walk, the conditional test and the
backend-conditional rejection use a comment-block fence only. Two earlier commits
had extended literal-block skipping to those three, which made them disagree with
the renderer; verified against the real book:
include inside ---- 3 renders (a duplicate) gate now objects
include inside //// 2 renders (dropped) gate now quiet
That also means the review premise here -- that a displayed conditional inside a
source block misclassifies a later include -- does not hold, and the code says so
where the next reader will look.
The other two findings stand and are fixed. An explicit default port reaches the
identical endpoint, so https://www.codenameone.com:443/ is accepted while :8443
is still rejected. And ifeval::["{backend}" == "html5"] is as unmodellable as the
boolean attribute: the surrogate sets backend-pdf on an HTML render, so
backend-html5 stays defined AND {backend} still reads "html5". Both spellings are
now refused.
One crash caught by probing rather than by the clean run: COMMENT_FENCE_RE had no
capture group while the caller reads group(1). The guide has no //// block, so
that branch never executed and every gate stayed green over a latent IndexError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The DNS root dot walked straight past the same-site checks
urlsplit lowercases a host but keeps the root label's trailing dot, so
"https://www.codenameone.com./does-not-exist" carried the hostname
"www.codenameone.com." -- which is not in SITE_HOSTS. It therefore skipped the
route model, the self-link rule, the slash rule and the port rule in one go, and
was accepted, while the identical path without the dot is reported. DNS treats
the two spellings as the same name, so the dot is stripped before the host is
classified.
The guide contains no such URL today; this closes the bypass rather than fixing
an instance.
https://www.codenameone.com./does-not-exist old 0 -> new 1
https://www.codenameone.com/does-not-exist old 1 -> new 1
https://www.codenameone.com./getting-started/ new 0 (still fine)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read the fallback routes from the Function, and close the authority family
Two findings, plus the structural change the last several implied.
The /files and /demos fallbacks were restated here as a literal pair, so deleting
one from docs/website/functions/[[path]].js would have left this accepting links
to a route that no longer exists. They are now read out of the Function itself.
Verified the derivation tracks the file rather than merely looking as though it
does: a /demos/ link passes today, and renaming that startsWith in the Function
makes the same link fail. If the Function is ever rewritten in a shape this
cannot read, the derivation yields nothing and links under those prefixes start
failing -- loudly, which is the safe direction.
asciidoctor-pdf defines basebackend-pdf alongside backend-pdf, and the surrogate
defined only the latter, so ifdef::basebackend-pdf[] content vanished from the
very render meant to inspect it. Both attributes are now set: a dangling
reference inside such a branch is reported.
And the reason for the third change: the last several findings here were all one
shape -- a spelling urlsplit tolerates that SITE_HOSTS membership does not see.
Malformed port, default port, DNS root dot, each fixed individually. That family
has no natural end (userinfo, IPv6 literals, mixed case, IDN), so it is now one
rule instead: a same-site classification requires the authority to be the bare
host and, at most, its own port. Anything else is reported rather than quietly
routed around the model.
https://user@www.codenameone.com/getting-started/ reported
https://www.codenameone.com:8443/getting-started/ reported
https://www.codenameone.com/getting-started/ fine
https://www.codenameone.com:443/getting-started/ fine
https://www.codenameone.com./getting-started/ fine
Measured before adding it: the guide has no URL with userinfo, a non-ASCII host,
an IPv6 literal or mixed case in the authority, so this rejects nothing that
exists and forecloses the ones that do not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* An inline directive is content, and an attribute-built URL is unreadable
The spacing scan stepped over every preprocessor directive on its way to the
next surviving line. Only the BLOCK forms vanish, though: ifdef/ifndef with empty
brackets open a region and leave nothing behind, and endif closes one, but the
single-line form carries its content in the brackets and EXPANDS to it. Measured
-- `ifdef::feature[== Inline Chapter]` renders as a real chapter when the
attribute is set. So the scan was walking past a heading that the preceding
paragraph was about to swallow. It now steps over block forms only, and treats a
single-line directive as the content it becomes.
inline ifdef right after an include old 0 -> new 1
A URL assembled from an attribute is invisible to the link scan: the declaration
holds a valid site root and the use site holds only "{name}", so the path that
actually ships is never checked. Expanding attributes properly means
reimplementing asciidoctor's resolution, inheritance through includes included,
which is a great deal of machinery for a construct the guide does not use --
measured, zero URL-valued attribute declarations and zero link:{...} targets. So
both spellings are refused instead, which stops the gap opening quietly later.
:site: https://www.codenameone.com old 0 -> new 1
link:{site}/missing[label] old 0 -> new 1
an ordinary URL new 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Three Javadoc links pointed at a package that is spelt lowercase
The /javadoc/ prefix was accepted wholesale, on the stated grounds that the tree
is generated at build time and cannot be enumerated from the repository. It can:
build_javadocs.sh runs over two fixed source roots, so a package is a directory
and a class page is a .java file. Behind that exemption sat three live 404s --
the guide linked to /javadoc/com/codename1/JavaScript/ three times, and the only
tracked package is lowercase com/codename1/javascript, so a case-sensitive host
served nothing while the gate reported success. macOS hides this: the checkout
answers to both spellings, and only `git ls-files` tells the truth.
All three links are corrected. The exemption is replaced by a derivation over
CodenameOne/src and Ports/CLDC11/src, which rejects an unknown package or class
and still accepts what javadoc emits beyond that model -- top-level index pages,
class-use/ trees, package-summary and nested Outer.Inner.html. Measured against
every javadoc link in the guide: 423 checked, 0 false positives.
/javadoc/com/codename1/JavaScript/JSObject.html reported
/javadoc/com/codename1/ui/NoSuchClass.html reported
/javadoc/com/codename1/javascript/JSObject.html fine
/javadoc/com/codename1/ui/Form.Inner.html fine
/javadoc/com/codename1/ui/class-use/Form.html fine
/javadoc/index.html fine
An early draft let the wrong-case package through: the guard meant to admit
top-level index pages also admitted any unrecognised package. Probing is what
caught it -- the guide's own links all passed either way, because they had
already been fixed.
Root-relative targets are scanned too. `link:/does-not-exist[]` and
`href="/does-not-exist"` name a website route exactly as an absolute URL does,
but carry no scheme, so URL_RE never saw them, and check-guide-xrefs.py skips
hrefs starting with "/" because they are not same-page anchors. They now go
through the same route model. The guide has none today.
The link text says `com.codename1.javascript` in backticks: the package really is
lowercase, and unmarked it reads to LanguageTool as a misspelling of JavaScript.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The Javadoc index documented a package the build deliberately removes
Two gaps between what the model claims and what the generators produce.
build_javadocs.sh filters /com/codename1/impl/ out of its source list, passes
-exclude for the package, and then guards that it never reached the output --
three separate refusals. The index built here recorded every .java under the
source roots, so a link to /javadoc/com/codename1/impl/ARImpl.html was accepted
against a page the build goes out of its way not to produce. The exclusion is now
mirrored, subpackages included.
/javadoc/com/codename1/impl/ARImpl.html old 0 -> new 1
/javadoc/com/codename1/ui/Form.html new 0
Note the first probe of this proved nothing: ImplementationFactory is written
into tempJavaSources by the generator itself, so it was absent from the old index
too and both versions reported it. A class that really is in CodenameOne/src was
needed to tell them apart.
The publication test read only `date`, so Hugo's publishDate and expiryDate were
invisible: scheduling a page forward or expiring it removes the route from the
built site while a guide link to it went on passing. Both are honoured now.
A date-only expiry lapses at midnight, so the day itself is already too late.
no dates / past date / past publishDate / expires later published
future date / future publishDate / expired / draft not published
Neither construct appears in the repository today -- no guide link into impl, no
page carrying either field -- so both close a gap rather than fix an instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Javadoc documents public types, and the index was recording every source file
The generator runs javadoc with -protected, which documents public and protected
types only; a top-level type cannot be protected, so in practice it is public or
it gets no page. The index recorded a class for every .java under the source
roots, so a link to a package-private type was accepted against a page that is
never generated -- com.codename1.io.JSONSanitizer is declared `final class`, and
/javadoc/com/codename1/io/JSONSanitizer.html passed.
Only public top-level types are indexed now. Comments are stripped first, because
a sample inside a javadoc block can easily put the word "public" next to a class
name. package-info is dropped by name: it declares no type, and javadoc folds its
content into package-summary.html, which the package check already covers.
Validated against every javadoc link in the guide before trusting it -- 423
checked, 0 rejected -- so this narrows the index by 366 of 2211 sources without
touching a single real link.
/javadoc/com/codename1/io/JSONSanitizer.html old 0 -> new 1
/javadoc/com/codename1/io/Log.html new 0
/javadoc/com/codename1/ui/Form.Inner.html new 0
/javadoc/com/codename1/io/package-summary.html new 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* An uppercase scheme was invisible, and the two slash rules had drifted apart
URL_RE required a lowercase scheme, so link:HTTPS://www.codenameone.com/... was
extracted by nothing and skipped every check behind it. Schemes are
case-insensitive; the extraction is now too, and urlsplit already lowercases what
it returns, so nothing downstream changes.
link:HTTPS://... /does-not-exist old 0 -> new 1
link:HTTPS://... /getting-started/ new 0
lowercase, bad route unchanged
The trailing-slash rule existed only on the absolute-URL path, so a root-relative
link to a static file with a slash appended -- link:/favicon.ico/ -- was
normalised down to the existing asset and accepted, while the identical absolute
URL was reported. That is the sort of split that happens when the same rule is
written twice, so it is now written once and called from both.
link:/favicon.ico/ old 0 -> new 1
link:/videos.html/ new 0 (declared in _redirects)
link:/developer-guide/ new 0 (a directory route)
Neither spelling appears in the guide today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* A nested type was taken on trust, and an include hid inside an inline conditional
The nested-type branch split Outer.Inner.html at the first dot and checked only
that Outer.java exists, so any invented page was accepted --
/javadoc/com/codename1/ui/Component.DoesNotExist.html passed against a page
javadoc cannot generate. The innermost name is now required to be declared in the
outer type's own file, which is the only place javadoc could find it. Nested
declarations may be protected as well as public, and -protected documents both,
so the visibility test differs from the top-level one.
Validated against the guide before trusting it: 423 javadoc links checked, 0
rejected, including all six distinct nested pages -- ActionEvent.Type,
URLImage.ImageAdapter, TableLayout.Constraint, BrowserComponent.JSRef,
BrowserComponent.JSProxy and the doubly nested
LayeredLayout.LayeredLayoutConstraint.Inset.
Component.DoesNotExist.html old 0 -> new 1
ActionEvent.Type.html new 0
LayeredLayout.LayeredLayoutConstraint.Inset.html new 0
Separately, `ifdef::feature[include::chapter.adoc[]]` is expanded and processed by
asciidoctor, but INCLUDE_RE is anchored at the start of the line, so the walker
saw nothing there and the edge went uncounted -- a chapter included both this way
and normally would render twice with the counter still reading one. Refused, for
the same reason every other conditional include is: neither the duplicate count
nor the rendered-title check can model one. The guide contains none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* An invented middle level passed, and xref: was a third spelling nobody read
The nested-type check verified the ends of the chain and discarded everything
between, so an invented middle level was accepted:
/javadoc/com/codename1/ui/CommonProgressAnimations.Fake.CircleProgress.html
passed because both CommonProgressAnimations and CircleProgress are real, while
Fake is not declared anywhere. Every component is checked now.
What this deliberately does not verify is that the names nest in the ORDER given.
That needs brace-depth parsing of the outer source, and a page naming two real
siblings the wrong way round is a much less likely mistake than naming a level
that does not exist. The limit is written down rather than left to be rediscovered.
CommonProgressAnimations.Fake.CircleProgress.html old 0 -> new 1
CommonProgressAnimations.CircleProgress.html new 0
Component.DoesNotExist.html unchanged
And xref: is a third way to write a root-relative target, alongside link: and a
raw href=. Asciidoctor renders it as a root-relative href, which
check-guide-xrefs.py skips because it is not a same-page anchor, so the route
went unchecked by both gates. Added to the same extraction.
xref:/does-not-exist[label] old 0 -> new 1
xref:/developer-guide/[label] new 0
Re-validated against the guide: 423 javadoc links, 0 rejected. The guide has no
xref: macro at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The publication-date fix was inert: the parser never stored the keys
Two rounds ago is_published() learned to honour Hugo's publishDate and
expiryDate. It could not: front_matter() matches an ALLOWLIST --
url|slug|aliases|draft|date -- so neither key was ever stored and both lookups
read empty on every page. The check ran and decided nothing.
I verified that change by calling is_published() with a dict I wrote by hand,
which is exactly the half that already worked. The parser feeding it was never
exercised. Proven now by running both versions over a real file:
with the fix parsed ['publishdate'] -> published False
without it parsed [] -> published True
The keys are in the allowlist now, matched case-insensitively and stored
lowercased, because Hugo treats front-matter keys case-insensitively and this
tree mixes YAML and TOML. Eight cases pass end to end THROUGH front_matter --
YAML and TOML spellings, future and past publishDate, expired and not-yet-expired
expiryDate, a lowercase key, and draft. The site path count is unchanged at 4739,
so widening the allowlist moved nothing else.
The docstring now says the list is an allowlist, since that is the property that
made a whole check silently do nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Real nesting, real timestamps, and a comment stripper that ate braces
Four findings, and implementing the first one exposed a bug of my own that the
other checks had been quietly relying on not hitting.
Nesting is parsed now rather than approximated. Asking only whether each name is
declared SOMEWHERE in the outer file accepted two siblings written as if one
contained the other -- CommonProgressAnimations.CircleProgress.EmptyAnimation
names two real types, neither inside the other. The chain is walked with brace
depth and an enclosing stack, and recorded only when every level is public or
protected. I had deferred exactly this last round on the grounds that a wrong
ORDER was unlikely; the reviewer produced a concrete instance, so the argument
does not hold.
Doing it needed the sources parsed properly, which is where my comment stripping
turned out to be wrong: it blanked line comments BEFORE string literals, so the
"//" inside "https://..." ate the rest of that line -- opening braces included.
Brace depth went negative at URLImage.java:650 and the file's own class appeared
to close early, which is why three real nested types were rejected on the first
attempt. Blanking is now a single scan with no ordering to get wrong, and is
shared with is_public_type(), which had the same hazard. Checked across all 2211
sources: braces balance in every one.
CircleProgress.EmptyAnimation (siblings) reported
Fake.CircleProgress (invented middle) reported
Component.DoesNotExist reported
CircleProgress / ImageAdapter / JSRef fine
LayeredLayoutConstraint.Inset fine
Publication now follows Hugo. publishDate DECIDES availability when set and date
is only the fallback, where before a future `date` alongside a past `publishDate`
was rejected -- a page Hugo publishes. And the comparison keeps the time of day:
the site rebuilds once daily, so a page scheduled for later today is genuinely
absent until then, and truncating to the calendar day called it live. Nine cases
pass end to end through front_matter, both fence styles included.
An attributed admonition is an admonition. [NOTE,caption="Aside"] fell out of the
lookahead, which required the bracket immediately after the name, so it read as
the listing the preceding colon promised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The self-link rule existed on one branch and not the other
link:/developer-guide/#…
* Developer guide: a figure harness that renders themed app UI Completes the half of the screenshot work #5668 deliberately left out. That PR shipped the font fix and the tolerance-aware comparer but not the theming machinery, because its 24 figures are schematic layout diagrams -- green labelled blocks showing where a layout manager puts things -- and rendering those under a real theme would bury the teaching point under component chrome. They keep their own pass and their hand-rolled styling. GuideFigure is a pure factory: it builds a Form and never shows it, so the renderer owns the window size, the theme and the appearance and one figure renders on several devices in both appearances. FigureDevice takes its geometry from devices.json rather than inventing it, naming the entry each profile comes from, and renders at the device's logical resolution with no skin -- a skin would frame every picture in a bezel and, worse, set isIOS in JavaSEPort.loadTrueTypeFont, which resolves native: fonts against host-installed faces and makes the output depend on the machine. The density is read once during Display.init and cached on the implementation, so a process cannot render two profiles; each pass is its own JVM, driven by three exec executions. Dark mode is checked rather than trusted. Setting it without UIManager.refreshTheme() leaves styles cached at their light values and every dark frame renders light -- a failure invisible in review that repeats across every figure -- so the renderer refreshes, then asserts Display agrees and that the resulting background is actually dark. The gate no longer hardcodes 24. Generated figures live in img/generated/, which makes the directory the manifest: a golden that stops being produced is now an error instead of a silent gap, verified by removing one and watching the comparer fail. The native themes are compiled before the framework build, because the antrun copy that puts them in the javase jar is failonerror=false and otherwise fails open. Two real consumers rather than unexercised infrastructure. The Components chapter gets an iOS/Android pair, retiring a caption claiming iOS draws no border on a button, and the native themes chapter's "Light and dark mode" section gets the light/dark pair it had no visual evidence for at all. Neither figure uses a FontImage glyph, whose antialiased edges cannot be byte-reproduced across platforms, so neither needs a tolerance sidecar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Trigger the guide figure check on its native theme inputs The themed figures render against the compiled native themes, which makes those themes an input to this guide the same way the build hint sources already are -- and neither on.pull_request.paths nor the paths-filter mentioned them. A PR editing native-themes/<name>/theme.css, the CSS compiler that renders it, or build-native-themes.sh would have changed what the figures look like without this workflow running at all, leaving the committed images stale until some later docs or release build failed against them. Added to both lists, because they serve different purposes and the file already carries two comments about exactly this: the trigger starts the workflow, and the figure comparison step is separately gated on the docs filter, so an input present in only one of them still skips the check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Developer guide: regenerate three Components figures from their own samples The check/radio, MultiButton and SpanLabel figures were 320x480 captures from the HVGA era. They now render through the figure harness under Android Material at a current phone profile. The samples move with them. Each figure's build() carries the exact tagged region the chapter used to include from a snippet fixture, and the chapter now includes it from the figure class, so the listing beside a picture is the code that drew it. A compile-only fixture and a screenshot are two sources for one example, and nothing keeps them in step -- which is how the chapter came to caption an iOS button render with a claim about borders that the sample beside it never demonstrated. One canonical variant each, on Android Material, because the subject is the component rather than the platform. Android is the right single choice: these render through the port's bundled Roboto, which is what Material 3 specifies, while an iOS Modern render comes out in Roboto rather than SF and is typographically wrong -- worth having only where the platform difference is the point, as in the button pair. GuideFigure's contract now says a figure MAY show its form, which these samples do because that is what a reader would write. The renderer prepares the returned form at the target geometry either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Point the package javadoc images at where the guide actually publishes them com.codename1.ui's package documentation embeds 36 guide screenshots as absolute URLs under /img/developer-guide/. Nothing in this repository produces that path: scripts/website/build.sh rsyncs docs/developer-guide/ into the site's static/developer-guide/, preserving subdirectories, so these files are served from /developer-guide/img/ instead. The URLs are rewritten to that shape and each one is now checked to resolve to a file that ships. One of them was newly broken by this branch rather than merely misspelt. components-button.png is deleted by the figure harness change -- its caption claimed iOS draws no border on a button, which Liquid Glass made false -- and the javadoc still pointed at it. It now takes the iOS render of the pair that replaced it. The three figures this PR regenerates moved under img/generated/ and their URLs move with them. Because the URLs really are derived from the file layout, a comment beside the package declaration records that, so the next move under docs/developer-guide/img knows it has to bring them along. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Baseline the three icon figures on CI output and record why they need tolerance CI rendered all three differently from this Mac. Not a flake and not a docs bug: the differing pixels fall in narrow column runs exactly the width of an icon, at the icon positions, and every text column is byte-identical, with both renders the same size. Java2D rasterizes the same glyph from the same bundled font at the same size slightly differently on macOS and Linux. The three figures added alongside these that carry no FontImage glyph reproduced byte-for-byte, which is the control. Measured: 732/134561 pixels (0.544%) for the check and radio figure, 460/208181 (0.221%) for MultiButton, 368/129244 (0.285%) for SpanLabel, worst channel delta 134 to 136. The committed images are now the CI runner's own output, so the gate that actually runs compares byte-for-byte and the sidecar budget only covers regenerating a figure on a developer's Mac. Each budget is set just above its own measurement rather than one loose number shared by all three, and maxChannelDelta stays at the 160 the existing sidecar uses -- clear of the measured worst, and still failing a solid overwrite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Developer guide: regenerate 64 more figures from their own samples Takes the harness from two hand-written consumers to the whole set of figures that have a code sample beside them. 150 such pairs exist across the guide; 64 now render, and the remainder are recorded below rather than quietly dropped. Every one of these figures is generated from the snippet the chapter already printed: the sample moves into the figure's build(), still inside its original tag, and the chapter includes it from there. The listing beside a picture is therefore the code that drew it, and the compile-only fixture that used to hold that code is retired -- 49 of them here. A fixture and a screenshot are two sources for one example with nothing keeping them in step, which is how a caption came to describe an iOS button render that the sample beside it never demonstrated. What did not convert, and why: - 58 samples build no Form at all; they are fragments that add to an existing one, so there is nothing to photograph. - 14 do not compile once lifted out of their fixture, because they lean on helpers the fixture declared outside the tagged region. - 9 render, but render empty: a bare title bar, a lone microphone icon, a form whose content arrives from the network or a gesture. Their existing screenshots show real content, so replacing them with a blank phone would be a regression. The degenerate-render check that caught them looks for a near-uniform image rather than trusting that a successful render is a useful one. - 3 throw on the way up for the same reason -- a null toolbar, a null image. Two harness changes fall out of running it at this scale. The renderer now collects every failure and reports them together instead of stopping at the first, which turns "fix one, rediscover the next" into a single list. And the appearance assertion now checks only the dark direction: a dark render that silently comes out light is the expensive failure, while a figure is perfectly entitled to a dark background of its own in light mode, as the image-masking figure's red form is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the figures that photographed a launch form, and gate against it 23 of the converted figures came out byte-identical to another figure. The cause is one shape of sample: it does not build the state the caption describes, it ATTACHES it -- a dialog opened from a button listener, a browser callback fired by a link, a menu named but never opened, a zoom applied by a gesture. Rendering that offscreen photographs the launch form, so "with tint" and "with green tint" produced the same picture, as did the three toolbar animation states, the before and after of a browser callback, and the portrait and landscape multiline tables. None of the existing checks could see it. Each figure rendered without throwing, was not blank, and matched its own committed copy, so the render and the comparison both passed while the guide showed one picture under two captions. The degenerate-render check I added looks for a near-uniform image, which these are not. So the comparer now rejects byte-identical figures outright. Two guide figures that are the same image mean at least one is not showing what its caption says, and the message names the whole clashing group. Verified it bites by copying one figure over another and watching the run fail. The 23 go back to their previous screenshots, with their samples restored to the snippet fixtures they came from -- an old screenshot that shows the right thing beats a new one that does not. Producing them honestly needs the harness to drive the interaction before painting, which is a piece of work in its own right rather than something to bolt on here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop eight more figures that photographed the wrong thing The byte-identical check catches a launch form only when two of them collide. These eight did not collide with anything, so they passed every gate while showing the reader something other than the caption: a "Blur Dialog" that is a form with a Blur button, a "Popup Dialog" that is a form with a Show button, an AutoComplete "with pictures" whose field is empty because no one typed, a capture-photo figure that is a lone shutter icon, three ImageViewer figures whose image never loads offscreen, and a PWA add-app-banner figure showing an unrelated before-unload form. Found by looking for the shape rather than by eye: figures whose sample attaches an interaction -- a listener, a Dialog, a URLImage -- instead of building the state, then inspecting that shortlist. All eight go back to their previous screenshots with their samples restored to the fixtures. That leaves 38 figures whose picture is exactly what its printed sample builds, and the byte-identical gate to stop the collision case coming back. The rest need a harness that can drive an interaction and wait for it before painting, which is its own piece of work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin locale and time zone, and drop the figures that cannot be deterministic Two of these would have broken the byte-exact gate on their own, without anyone touching the code. The localization, picker and calendar figures render the current date, so the committed golden stops matching the moment the day rolls over. And the figure JVM pinned display scaling but not Locale.getDefault() or the time zone, which JavaSE's L10NManager is built from -- the committed localization figure carried Israeli currency and date formatting because that is where it was generated, and no other machine could reproduce it. The locale and time zone are now pinned to US and UTC before Display.init, alongside retinaScale, because the implementation reads them as it comes up. That is worth doing for every figure rather than only the visibly dated ones: number and date formatting leak into more places than it looks. Pinning cannot save a figure whose SUBJECT is the current date, so those three go back to their previous screenshots, along with three more the same review identified: both BrowserComponent figures, whose native peer is not rasterized by an offscreen Form paint and which therefore showed an empty viewport where the caption promised a website, and the SliderBridge download figure, which showed a zero-value slider because nobody pressed download. Verified the result is reproducible rather than assuming it: two consecutive full runs now produce byte-identical output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Trigger the figure check on the framework, and fix the javadoc image URLs The figures are rendered by the framework itself, so a change to how a component paints, how a layout measures, or how the JavaSE port rasterizes changes every generated PNG without touching docs/ at all. The workflow did not trigger on any of that, and the comparison is byte-exact, so the committed figures would have gone stale silently until some later docs or release build failed on them. CodenameOne, Ports/JavaSE and the two Maven modules join the trigger and the paths-filter -- both lists, since the comparison step is gated separately from the trigger. Nine javadoc image URLs broke in the previous commit: the images moved under img/generated/ and the URLs in com.codename1.ui's package documentation did not move with them. All 37 are now verified to resolve to a file that ships, which is how the earlier round should have been checked rather than by reading the diff. The Hi World figure kept a caption pointing at a blue iOS status bar that the render no longer contains. Rendering it on the iOS profile does not bring the bar back either: it came from an old iOS skin, and the modern theme does not paint one. The caption was stale iOS-7-era guidance, so the figure keeps the iOS profile its caption talks about and the caption now describes what is actually in the picture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Crop from preferred content height, and drop six figures caught measuring The crop was reading where a child was LAID OUT, and prepare() sizes the content pane to the whole device, so a form whose child sits in BorderLayout.CENTER had that child stretched to fill it. Every such figure reported the full height and cropped nothing: the table, CSV, filesystem and tree figures were 409x917 with most of the page blank. Reading what the content PREFERS instead takes them to 111-218 pixels. The six that go back to their old screenshots were found by measuring rather than by grepping for suspicious API calls, which is what the last two rounds did and is why they kept missing cases. Comparing each figure against the image it replaces exposed the ones that render a component in its resting state when the caption describes a state someone has to reach: a Tree with every branch collapsed, a FileSystemTree showing only "home", an Accordion showing three collapsed items, an AutoComplete with an empty field and no suggestions, a CodeEditor whose backend initializes on a later EDT turn so the editor area is simply blank, and a ScaleImageLabel that draws placeholder triangles because its images are not there. Note the measurement needed reading, not thresholding: it also flagged figures that are correct and merely shorter now that the crop works, so every flagged figure was looked at before being kept or reverted. That leaves 26. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Crop correctly for painted and title-only figures, and drop autosize The previous crop fix traded one wrong reading for another. Preferring the laid-out height cropped nothing for a child stretched by BorderLayout.CENTER; preferring the declared height cropped the Hi World figure to a 26-pixel sliver, because a component that paints itself declares no intrinsic size and reports zero while legitimately filling the viewport. Both shapes now work: a declared preferred height is used when there is one, and a component that declares none is taken at the height it was given. A third shape turned up while checking that fix. A form with an empty content pane -- the toolbar figures, whose content is entirely in the title area -- fell back to the content pane's stretched height and produced a toolbar with a blank page beneath it. It now crops just below the title area: 919 pixels down to 74, which is the toolbar and nothing else. autosize goes back to its old screenshot. It renders three purple bars with none of the six strings whose different automatic font sizes are the entire point of the figure. I had passed it in the previous round by looking at a montage scaled too small to see that the text was missing, which is the same mistake in a different form: the check has to be able to fail. That leaves 25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Baseline the figures on CI output, which is what the gate compares against The build failed on 17 figures: the goldens were rendered on a Mac and the runner does not reproduce them. Two separate causes, and only one of them is the familiar glyph difference. Fourteen differ in pixels only, between 0.08% and 1.22% of the image, in the same antialiased glyph edges as before. Those take measured tolerance sidecars, and since the committed image is now the runner's own output the budget covers regenerating a figure locally rather than the gate itself. Seven differ in HEIGHT, by two to five pixels. That one is mine: the crop is computed from the laid-out content, text component heights come from font metrics, and Java2D rounds those differently on the two platforms. No tolerance sidecar covers a size change, and it should not -- a different size is a different picture. So the size check now explains at the point of failure why a locally regenerated figure can differ, and says not to commit the local render to silence it, which would only move the failure back to CI. Verified both directions: the CI artifact now matches the committed set byte-for-byte, and a local regeneration lands within the sidecars for every figure that differs only in pixels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Stop one figure's global state from landing on the next The toolbar sample calls Toolbar.setGlobalToolbar(true), and every figure is built in the same process, so the nine figures registered after it were given a toolbar their own samples never asked for. The output depended on the order of the manifest, which is not a property any of these figures should have. The renderer now snapshots and restores that flag around each build, rather than patching the one sample, because the next global left set would repeat this with no symptom other than a figure quietly changing when an unrelated entry moves. Eight figures change as a result: badge-floating-button, floatinghint, signature2, csv-parsing, glasspane and the three fontimage figures. Their committed goldens are stale until the runner produces the new ones, which is where the goldens have to come from -- rendering them here and committing a Mac's output is what turned the build red last time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Render ComponentGroup where grouping exists, and make the sidecars bind ComponentGroup does nothing unless the theme sets ComponentGroupBool, which the iOS theme does by default and Android Material does not. Rendered on Android the sample came out as plain separate rows -- the opposite of the grouping the section exists to show. It now renders on the iOS profile, where the checkboxes form one connected list and the radios a button bar. The sidecars added in the previous commit all carried maxChannelDelta=255, which is the full range of a channel and therefore no bound at all: only the area budget was actually constraining anything, so a recolour confined to a small part of a mostly empty figure could have passed. Each sidecar now takes its own measured worst delta plus headroom -- 54, 80, 84, 88, 123, 158, 160, 165 -- and stays at 255 only for the three figures that genuinely flip a pixel between extremes, where the measurement leaves no room below it. The eight figures affected by the global-toolbar leak are baselined from the runner's output for that fix, rather than from a render on this machine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop five more figures whose captions promise a state the render cannot show Three came from review: the floating hint figure needs text in the first field for the comparison its caption draws, the share button figure needs the share sheet open, and the signature figure needs the drawing editor open with its Cancel, Reset and Save controls. All three render the component at rest. Two more came from finally doing the check I should have run at the start: reading every remaining figure's caption against its picture. Both toggle button figures are captioned "toggle button converted sample", and the modern themes do not style a toggle button at all -- the originals came from the legacy theme. Rendering them on the iOS profile changes nothing, which I verified rather than assumed, so the picture is a plain list of checkboxes under a caption about buttons. The ComponentGroup variant does show grouping on iOS, but its caption is about toggle buttons too, so it goes with it. That the caption audit found two figures my visual pass had approved is the point: I was checking whether a picture looked sensible, not whether it showed what the prose beside it claims. That leaves 20. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Check the figure URLs embedded in Java sources instead of remembering them com.codename1.ui's package documentation carries 37 absolute URLs to guide figures, and those URLs are derived from the file layout, because the site build rsyncs docs/developer-guide/ to /developer-guide/. Moving a figure breaks them with no symptom: nothing in the docs checks reads a Java file, and javadoc renders a missing image without complaint. They have now drifted three times in this branch -- once when figures moved into img/generated/, once when nine more followed, and again when reverting figures moved two of them back out while their URLs stayed behind. Two were still pointing at img/generated/components-signature2.png and img/generated/components-floatinghint.png, which no longer exist. Fixing those two by hand a fourth time is not a fix, so check-guide-links.py now walks CodenameOne/src for these URLs and fails when one names a file that does not ship. Verified it bites by pointing a URL at a file that is not there and watching it exit 1 -- the first probe was worthless because the file I picked happened to exist at the path I invented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Guarantee no simulator skin is active, rather than refusing only an iOS one The renderer refuses to run when the platform name is "ios", on the grounds that a skin puts the host's installed fonts back into the output. That check is too narrow: an Android skin reports "and" and sails through it, while still having decided platform name, fonts, geometry and device class before any figure is built. The ios pass could then render the iOS theme against Android simulator state and write it to the same golden file, so the figures would depend on whoever launched the generator -- a shell, an IDE run configuration, an inherited environment. JavaSEPort.hasSkins() is "skin != null || dskin != null", and every skin path, the stored preference included, sits behind it. Clearing both properties before Display.init is therefore what actually guarantees no skin loads, and it belongs beside the locale, time zone and retina scale that are pinned there for the same reason. The platform-name check stays as a second line of defence. Output is unchanged: with no skin set, which is how the generator is launched, this renders byte-identical to before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Shai Almog <shai.almog@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stacked on #5664. Review that one first; this diff is only the last commit.
The defect
None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.
Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:
PreAdvancedThemingScreenshotsstyled withFont.createSystemFont, which resolves throughJavaSEPort.fontFaceSystem—"Arial"on macOS and Linux alike (JavaSEPort.java:765).flow-layout.pngevery differing pixel sits iny 10–130— the title and label rows — while the coloured blocks below match to the pixel.So the byte-exact gate in
developer-guide-docs.ymlhas only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.The fix
The figures load
native:MainRegular, whichJavaSEPort.loadTrueTypeFont(:13551) reads from/com/codename1/impl/javase/Roboto-Medium.ttfon the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.This is also what the project's own font rule requires everywhere — never
createSystemFont, always thenative:scheme — so the violation and the non-determinism were the same bug.A
nullreturn refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.How this PR proves itself
The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Scope note
These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.