Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This file is the single source of truth for agents entering this repository. Rea
- References and current plans: `docs/references.md`, `docs/code-review-guidelines.md`, `specs/current/maintainability-roadmap.md`, `specs/current/ci.md` (CI scope confidence methodology — required before changing confidence or guard fields in `scripts/scopes.ts`).
- Directory-level agent guidance: `.github/AGENTS.md`, `apps/AGENTS.md`, `packages/AGENTS.md`, `tools/AGENTS.md`, `e2e/AGENTS.md`.
- Packaged auto-update architecture and high-confidence local harness: read `tools/pack/AGENTS.md` section "Packaged auto-update architecture and harness" before touching packaged updater code, release-channel identity, installer behavior, or updater UI.
- Packaged build cache contract: `tools/pack/CACHE.md` (determinant rules, materialization-time parameters, confidence grading — required before changing any build-cache node key).

## Workspace directories

Expand Down
2 changes: 2 additions & 0 deletions tools/pack/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Follow the root `AGENTS.md` and `tools/AGENTS.md` first. This tool owns the repo-external packaged build/start/stop/logs command surface.

Read `tools/pack/CACHE.md` before changing any build-cache node key, adding a cache node, or changing what a cached node reads or writes. It is the source of truth for the build-graph cache under `--cache-dir`: determinant rules, materialization-time parameters, the signing boundary, and confidence grading.

## Owns

- Local packaging orchestration for packaged Open Design artifacts.
Expand Down
175 changes: 175 additions & 0 deletions tools/pack/CACHE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# tools-pack build cache contract

This file is the source of truth for the `tools-pack` build-graph cache: the
cache under `--cache-dir` that stores packaged build artifacts as
content-addressed nodes. Read it before changing any node key, adding a node,
or changing what a node writes.

It records **current state only**, not change history — same convention as
`specs/current/ci.md`.

Out of scope: the GitHub Actions cache that wraps this store. That layer is
owned by `.github/`; it restores and saves the store as an opaque directory and
carries no correctness obligation (see **Why coarse restore is safe** below).

## Cache model

A node is `{ id, key, outputs, build, invalidate }`, acquired through
`ToolPackCache` (`src/cache.ts`).

- `keyHash = hash(node.id + "\n" + node.key)`; the entry lives at
`entries/<node.id>/<keyHash>/` with a `manifest.json`.
- An entry is accepted only when `manifest.key === node.key` exactly. A
mismatch is reported as `key mismatch` and the node rebuilds
(`cache.ts:270-282`). There is no fuzzy or prefix matching inside the store.
- `invalidate` is an additional per-node veto applied to an otherwise-valid
entry (for example `win.packaged-app` re-validates its native rebuild
output).
- `materialize` copies entry outputs to their workspace locations. Steps that
run on the materialization path execute on **both** the hit and the miss
path.

### Why coarse restore is safe

Because acceptance is an exact `node.key` comparison, a restored store is a
*pool of candidates*, not an authority. Entries that do not match are ignored.
An outer layer may therefore restore a broader or older store than strictly
requested without risking a wrong build — it only affects how much work is
skipped, never what is produced.

This property is what the surrounding CI caching depends on. Do not weaken it
by introducing prefix or best-effort matching inside the store.

## Node inventory

The build-graph cache is almost entirely Windows-specific.

| Node | Platform |
| --- | --- |
| `<platform>.workspace-build` | all |
| `win.resource-tree` | win |
| `win.workspace-tarballs` | win |
| `win.packaged-app` | win |
| `win.electron-builder-dir` | win |
| `win.nsis-payload-base` | win |
| `win.nsis-payload-overlay` | win |
| `win.nsis-installer` | win |
| `win.portable-zip` | win |
| `win.launcher-payload-base` | win |
| `win.launcher-payload` | win |

`mac` and `linux` have `<platform>.workspace-build` only.

## Determinant rules

**R1 — A node key must cover every input that determines the node's output.**
Inputs include file content, configuration values, tool versions, and process
environment. An input that is read by `build` but absent from `key` is a
defect, not an optimization.

**R2 — A node key must carry the key of every upstream node it consumes.**
Re-deriving an upstream node's own inputs is not a substitute: upstream keys
carry inputs that are not file content (see R3), so re-derivation silently
drops them.

Existing links: `win.packaged-app` carries `tarballsKey`;
`win.electron-builder-dir` carries `packagedAppKey` and `resourceTreeKey`;
`win.nsis-installer` carries `basePayloadKey` and `overlayPayloadKey`;
`win.launcher-payload` carries `sourceKey`.

**R3 — Build outputs are never direct key inputs.** `hashPackageSourcePath`
excludes `dist`, `.next`, `out`, `node_modules`, and `.od`. A node that
consumes another node's build output must obtain that output's identity
through R2, not by hashing the output tree.

**R4 — A node key must not restate a list that already exists as a
constant.** Derive key inputs from the constant instead. Two independent
restatements of the same list drift silently and produce stale entries with no
signal.

**R5 — Every declared input needs a witness.** See **Changing a cache node**.

## Materialization-time parameters

Some values deliberately do **not** enter node keys. They are stamped onto the
output every time the node is materialized, so cached content stays
parameter-agnostic and is specialized on the way out.

Current materialization-time parameters:

- **App version.** `win.packaged-app` omits it. It is applied through
electron-builder `extraMetadata.version`, then rewritten on materialization
by `rewriteUnpackedAppPackageVersion` and `rewriteWinExecutableVersion`, then
verified by `assertMaterializedUnpackedVersionConsistency` — a fail-closed
check over the app `package.json` version, the `open-design-config.json`
`appVersion`, and the Windows executable fixed file version.
- **Namespace / channel and runtime endpoints.**
`win.electron-builder-dir` omits them. `open-design-config.json` — which
carries `namespace`, `amrProfile`, `telemetryRelayUrl`, `updateMetadataUrl`,
`posthogKey`/`posthogHost`, `webOutputMode`, and `namespaceBaseRoot` — is
regenerated on the materialization path by `writePackagedConfig`.

Downstream archive nodes (`win.nsis-*`, `win.portable-zip`,
`win.launcher-payload*`) carry `namespace` and the full `packagedVersion` in
their keys, because their content is the already-stamped payload.
Comment thread
PerishCode marked this conversation as resolved.
Outdated

**Requirement.** A value may be a materialization-time parameter only when
both hold:

1. it is re-applied unconditionally on the materialization path, so the hit
and miss paths converge; and
2. the applied value is verified by a fail-closed assertion.

Adding a materialization-time parameter without (2) is not permitted.

> Known asymmetry: app version satisfies (2); namespace currently satisfies
> only (1) — it is rewritten but not asserted.
Comment thread
PerishCode marked this conversation as resolved.
Outdated

## Signing boundary

Signing material never enters cache content. `resolveWinSigningCacheKey`
(`src/win/sign.ts`) contributes only the certificate SHA-1, digest algorithm,
timestamp algorithm, and timestamp URL, and appears in the keys of
`win.nsis-payload-overlay`, `win.nsis-installer`, and `win.portable-zip`.

`win.nsis-payload-base` correctly omits signing: it is built before
`ensureSignedUnpacked()`, while the overlay is built after. Keep that ordering
when changing the payload split.

## Confidence tiers

Borrowed from `specs/current/ci.md`.

- **`certain`** — the key provably covers the node's full input closure, and a
witness test demonstrates it (see below). Only `certain` nodes may
participate in cross-run cache reuse policies built on top of this store.
- **low confidence** — anything else.

**Grading is fail-closed: a node is low confidence until a witness proves
otherwise.** A newly added node is low confidence by default.

## Declared low-confidence points

Recorded so that grading stays checkable. These are known and accepted; do not
extend them.

- `<platform>.workspace-build` — `pnpm-workspace.yaml` is not a key input;
file mode (executable bit) is not hashed by `hashPackageSourcePath`.
- `win.launcher-payload` — the `seed: "nsis-base"` branch takes content from
the NSIS base payload but carries only the literal `"nsis-base"`, not
`WIN_ARCHIVE_CACHE_VERSION`. Bumping that constant without bumping the
launcher payload cache versions mismatches.

## Changing a cache node

1. Read this file and `AGENTS.md` in this directory.
2. If the change alters what `build` reads or writes, update the key in the
same commit.
3. Bump the node's `schemaVersion` / cache-version constant whenever key
semantics change.
4. Add or update the node's witness test in `tests/`. A witness proves both
halves:
- mutating **each declared input** changes the key;
- mutating a **known non-input** (for example a package's `dist` tree)
leaves the key unchanged.
5. Never introduce prefix or best-effort matching inside the store.
Loading