Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ jobs:
- 'mvnw'
- 'mvnw.cmd'
- '.github/workflows/ci.yml'
# The knowledge pack is generated from the compiled classes and
# gated in build-and-test. Without this a PR touching only the
# extractor, the classification rules or a generated surface
# matches no filter, skips build-and-test, and skips the gate with
# it — the gate would be absent on exactly the PRs that change the
# gate. Deliberately NOT added to `jvm`: the check is
# JDK-independent, so a knowledge-only PR wants one JDK 17 job
# rather than the full matrix.
- 'knowledge/**'
# Markdown is a build input even though it compiles nothing:
# DocumentationSnippetCompileTest compiles the literal java fences in
# docs/, and the guard suites read README / CONTRIBUTING / docs. Kept
Expand Down Expand Up @@ -205,6 +214,49 @@ jobs:
# only examples / benchmarks are excluded (their own CI jobs cover them).
run: ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose,:graph-compose-bundle,:graph-compose-qa,:graph-compose-coverage -am

# The knowledge pack describes the public API of the tree being built, so
# it is checked here rather than in a job of its own: every job gets a
# fresh runner, and a separate one would have no */target/classes to read.
# Guarded to the baseline JDK because the surface is JDK-independent —
# running it three times would only triple the chance of a flake.
- name: Set up Node for the knowledge pack
if: matrix.java == '17'
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Knowledge pack — classification fixtures
if: matrix.java == '17'
run: node knowledge/tools/api-surface/test/classifier.test.mjs

# Blocking, and blocking from the first run. A warn-only gate is one
# nobody reads, which is the failure mode this pack exists to remove: the
# allow-list drifted through an entire major because nothing was watching.
# Treat a failure here as a lockfile that was not regenerated.
- name: Knowledge pack — API surface is current
if: matrix.java == '17'
run: node knowledge/tools/api-surface/extract-api.mjs --from-reactor --check

- name: Knowledge pack — claim parser fixtures
if: matrix.java == '17'
run: node knowledge/tools/claims/test/claims.test.mjs

# A claim is a promise a published page makes to a reader. This fails when
# a page names API that no surface has — which a reader would discover by
# trying to compile it — and when a proof points at a test that has since
# been renamed away. Missing claims are only ever reported, never failed.
- name: Knowledge pack — documentation claims hold
if: matrix.java == '17'
run: node knowledge/tools/claims/check-claims.mjs --check

# A wrong route does not fail to compile — it sends every reader down the
# wrong path with the authority of a generated artifact. So a route is only
# served once its anchor resolves, its symbols exist, and its constraints
# name a documented behaviour that some test actually holds up.
- name: Knowledge pack — routing table holds up
if: matrix.java == '17'
run: node knowledge/tools/routing/check-routes.mjs

- name: Generate Javadoc
# Run only on the baseline JDK — Javadoc output is identical
# across JVMs and one pass is enough to catch broken @link
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ jobs:
- name: Verify (gate the release on a green build)
run: ./mvnw -B -ntp clean verify

# The pack describes the API of the commit being tagged, and `verify`
# above has already produced the classes it reads. --check first, so a tag
# cannot ship a pack that disagrees with the code it claims to describe;
# the write that follows is then a no-op for tracked files and exists only
# to produce target/knowledge/provenance.json, which the bundle carries.
- name: Set up Node for the knowledge bundle
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Build the knowledge bundle
run: |
node knowledge/tools/api-surface/extract-api.mjs --from-reactor --check
node knowledge/tools/api-surface/extract-api.mjs --from-reactor
# The bundle carries claims/ and routing/ too, so a correct API surface
# is not enough to publish it: a hand-authored route or a stale claims
# index would ship as a release asset unchallenged. The tag re-checks
# them against the exact commit being tagged rather than trusting that
# a PR run covered it. (check-routes has no --check: routes have no
# generated counterpart, so validating them is the check.)
node knowledge/tools/claims/check-claims.mjs --check
node knowledge/tools/routing/check-routes.mjs
node knowledge/tools/bundle/build-bundle.mjs --verify

- name: Extract CHANGELOG section for the tag
id: notes
run: |
Expand Down Expand Up @@ -99,3 +123,12 @@ jobs:
--verify-tag \
${PRERELEASE_FLAG}
fi

# The archive and its checksum go up as two assets. The checksum sits
# BESIDE the archive, never inside it: a hash stored in the file it
# describes is rewritten by whatever rewrote the file.
- name: Attach the knowledge bundle to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${GITHUB_REF_NAME}" target/knowledge/graph-compose-knowledge-*.zip target/knowledge/graph-compose-knowledge-*.zip.sha256 --clobber
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ documents do not say, and points at the rest.
Read when the change reaches them:

- [`docs/api-stability.md`](docs/api-stability.md) — before changing public API
- [`knowledge/api/`](knowledge/api/) — what the public API *is*, generated from
the compiled classes. Ask it rather than read it:
`node knowledge/tools/api-query/api-query.mjs --exists Type.method`
(exit 0 found, 3 absent). Split by surface — `authoring` is the one a
compose task wants; `backends`, `templates`, `testing` and `extension-spi`
are opt-in.
- [`docs/architecture/backend-capability-matrix.md`](docs/architecture/backend-capability-matrix.md)
— before changing what a PDF, PPTX or DOCX export can do, and to record it
- [`docs/adr/`](docs/adr/) — before revisiting a decision already taken
Expand Down Expand Up @@ -61,6 +67,33 @@ Read `git status --porcelain --untracked-files=all` before committing and remove
such files by name. Do not run `git clean -f`: it deletes new source files that
have not been staged yet along with the junk.

## Regenerate the knowledge pack with the API change

`knowledge/api/*.json` and their Markdown views are generated from the compiled
classes and committed. Treat them exactly like a lockfile: **a PR that changes
public API regenerates the pack in the same commit.**

```bash
./mvnw -q -DskipTests install -pl :graph-compose-core,:graph-compose-templates,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-testing -am
node knowledge/tools/api-surface/extract-api.mjs --from-reactor
```

CI runs `--check` on the same input and fails the build when the committed pack
does not match the tree. That failure is not a flake and is not the gate being
strict: it means the surfaces on disk describe an API that no longer exists.

Never hand-edit a generated file to make the check pass. The generator is the
thing to fix — a hand-edit survives until the next regeneration and, in the
meantime, makes the file look verified when it is not. That is not hypothetical:
this pack exists because a generated allow-list was wrong for an entire major
version while being quoted to agents as authoritative.

If a public type is new and the run fails with *"matched no classification
rule"*, the classifier is telling you it does not know whether the type is API.
Add a rule in `knowledge/tools/api-surface/lib/surfaces.mjs` — the surface it
belongs to, or an exclusion with a written reason. Do not widen a package prefix
to make the message go away.

## Verification

Run the smallest relevant tests while working:
Expand Down
17 changes: 17 additions & 0 deletions docs/recipes/layered-page-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ cleanly and one that fights the engine. This page is the decision guide.

## The four tools

<!-- claim: capability=layout.choose-the-layer -->
<!-- claim: symbol=AbstractFlowBuilder.addLayerStack -->
<!-- claim: symbol=AbstractFlowBuilder.addCanvas -->
<!-- claim: symbol=CanvasLayerBuilder.position -->

| Tool | Reach for it when | API |
| --- | --- | --- |
| **Page background** | A fill must sit behind everything and repeat on every page — a sidebar tint, a header band, a watermark wash. Ratio-based; never participates in layout. | `pageBackgrounds(List.of(PageBackgroundFill...))` |
Expand All @@ -17,9 +22,18 @@ cleanly and one that fights the engine. This page is the decision guide.

### Sidebar: page background vs. row

<!-- claim: capability=layout.two-columns -->
<!-- claim: symbol=AbstractFlowBuilder.addRow -->
<!-- claim: symbol=RowBuilder.weights -->
<!-- claim: symbol=DocumentSession.pageBackgrounds -->
<!-- claim: symbol=PageBackgroundFill.leftColumn -->
<!-- claim: behavior=row.rejects-a-nested-row proof=test:RowBuilderTest -->
<!-- claim: behavior=row.auto-column-rejects-right-aligned-text proof=test:AutoColumnRightAlignContractTest -->

A **tinted** sidebar that must repeat on every page is a page background — it
costs nothing at layout time and never shifts content:

<!-- doc-example-ignore: shown as two lines to contrast with the row below; wrapping it in a session would hide the comparison this section exists to make -->
```java
document.pageBackgrounds(List.of(
PageBackgroundFill.leftColumn(0.34, sidebarTint)));
Expand All @@ -28,6 +42,7 @@ document.pageBackgrounds(List.of(
A sidebar that holds **content** (skills, contacts, dates) is a row column — it
flows and paginates with the main column:

<!-- doc-example-ignore: the row form of the same sidebar, paired with the background above for contrast -->
```java
document.pageFlow()
.addRow(row -> row
Expand All @@ -45,6 +60,7 @@ column over it.
A badge centred on a card, sizing to the card, is a **layer stack** — use
alignment, not coordinates:

<!-- doc-example-ignore: calls badge(), a stand-in for whatever the reader is overlaying -->
```java
document.pageFlow()
.addContainer(card -> card
Expand All @@ -56,6 +72,7 @@ document.pageFlow()
A badge at an exact spot in a fixed certificate is a **canvas** — use
coordinates:

<!-- doc-example-ignore: the canvas counterpart of the layer stack above; badge() is the same stand-in -->
```java
document.pageFlow()
.addCanvas(523, 300, canvas -> canvas.position(badge(), 430, 40))
Expand Down
26 changes: 26 additions & 0 deletions docs/recipes/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ table paginates.

## Cell content — one line, several lines, or a node

<!-- claim: capability=table.cell-content -->
<!-- claim: symbol=DocumentTableCell.text -->
<!-- claim: symbol=DocumentTableCell.lines -->
<!-- claim: symbol=DocumentTableCell.node -->
<!-- claim: behavior=table.composed-cell-paints-but-has-no-snapshot-node proof=test:ComposedCellSnapshotContractTest -->

Three factories, three shapes. Pick by what the cell holds, not by
what the text looks like:

Expand Down Expand Up @@ -62,11 +68,15 @@ child shorter than one page's content area.

## Row span — merge a cell vertically

<!-- claim: capability=table.row-span -->
<!-- claim: behavior=table.spanning-cell-height-is-sum-of-covered-rows proof=test:TableBuilderRowSpanTest -->

Spanning cells declare how many rows they cover via `rowSpan(int)`.
The layout layer skips occupied grid positions when interpreting
subsequent source rows, so authors only specify the cells that are
not yet covered by a prior spanning cell.

<!-- doc-example-ignore: a fragment shown inside a flow — addTable is called on an ambient builder, and giving it a receiver here would bury the cell API this section is about -->
```java
import com.demcha.compose.document.table.DocumentTableCell;
import com.demcha.compose.document.table.DocumentTableColumn;
Expand Down Expand Up @@ -98,10 +108,15 @@ remaining rows / columns, or leaves a gap.

## Zebra — alternating row fills

<!-- claim: capability=table.zebra-striping -->
<!-- claim: symbol=TableBuilder.zebra -->
<!-- claim: behavior=table.explicit-row-style-beats-zebra proof=test:TableBuilderZebraAndTotalsTest -->

`zebra(odd, even)` paints odd-indexed rows (0, 2, 4 — first, third,
fifth visually) in one fill and even-indexed rows (1, 3, 5) in
another. Either argument may be `null` to skip painting that parity.

<!-- doc-example-ignore: the two zebra colours compile, the ambient addTable call around them does not -->
```java
import com.demcha.compose.document.style.DocumentColor;

Expand All @@ -122,6 +137,7 @@ addTable(table -> table
A two-arg overload accepts full `DocumentTableStyle` values when the
zebra row needs more than just a fill colour:

<!-- doc-example-ignore: a continuation of the builder chain above, deliberately shown without its receiver -->
```java
.zebra(
DocumentTableStyle.builder().fillColor(zebraOdd).padding(DocumentInsets.of(8)).build(),
Expand All @@ -134,11 +150,14 @@ already have an explicit `rowStyle(idx, ...)` override, so

## Totals row — bold + subtle fill

<!-- claim: symbol=TableBuilder.totalRow -->

`totalRow(values...)` appends the row at the end of the table and
assigns a default totals style (bold text + a subtle gray-blue fill).
A two-arg overload takes a custom `DocumentTableStyle` for branded
totals rows.

<!-- doc-example-ignore: reads rule, ink and bordered from the surrounding recipe so the totals style stays the subject -->
```java
DocumentTableStyle goldenTotal = DocumentTableStyle.builder()
.fillColor(DocumentColor.rgb(232, 220, 180))
Expand Down Expand Up @@ -167,11 +186,18 @@ regardless of parity.

## Repeated header on page break

<!-- claim: capability=table.repeat-header-across-pages -->
<!-- claim: symbol=TableBuilder.repeatHeader -->
<!-- claim: symbol=TableBuilder.headerRow -->
<!-- claim: behavior=table.header-repeats-on-every-continuation-page proof=test:TableBuilderRepeatHeaderTest -->
<!-- claim: behavior=table.repeat-header-defaults-to-zero proof=test:TableBuilderRepeatHeaderTest -->

`repeatHeader()` repeats the first row at the top of every
continuation page when the table is split across pages.
`repeatHeader(int)` repeats N leading rows — useful when you have
both a title row AND a column-header row that should both repeat.

<!-- doc-example-ignore: the put-it-together fragment, on an ambient builder like the rest of this page -->
```java
addTable(table -> {
TableBuilder t = table
Expand Down
Loading
Loading