From e59c358029c22c5c28bcc3db6774e38973465d49 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:35 -0400 Subject: [PATCH 1/7] docs: add a changelog for the specification CHANGELOG.md at the repository root records changes to the schemas under schemas/. The schemas ship inside @ethdebug/format, so the file is keyed by that package's version. Each entry gives a summary and the pull request, then three sub-items: the schemas the change touches, what it means for producers, and what it means for consumers. The producer and consumer lines each start with one of three fixed prefixes: - "no change needed." when nothing valid becomes invalid or changes meaning for that party - "optional:" when the change adds a capability that obliges nobody - "required:" when earlier valid output stops validating, the specification adds a must, or the meaning of valid data changes; the line names the keyword or prose that imposes the obligation An impact line states the net effect for a party that moves from the previous published version to the version of its section. A change inside a schema that is new in that version obliges nobody, and an obligation that a later change in the same version reverses does not appear. Sections are only Added and Changed; they do not signal obligations, the prefixes do. The file has an entry for every pull request that changed schemas/ between the 0.1.0-0 publication and the 0.1.0-1 tag, each checked against the schema trees of those two versions, and an Unreleased section for the changes merged since (#284, #286). --- CHANGELOG.md | 592 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 592 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..88fd8b26c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,592 @@ +# Changelog + +This file tracks changes to the `ethdebug/format` specification: the schemas +under `schemas/`. Those schemas ship inside the `@ethdebug/format` package, +whose build generates its distributed copies from `schemas/`, so the spec +version is that package's version and this file is keyed by it. Each published +package that implements the spec (`@ethdebug/pointers`, `@ethdebug/evm`, and so +on) has its own `CHANGELOG.md` under `packages//`; this file covers only +the spec itself. + +Each entry gives a summary of what changed. Three sub-items follow: + +- `Schemas:` the fully qualified name(s) of the schema(s) the change touches. +- `Producers:` what the change means for an emitter of `ethdebug/format` data (a + compiler such as solc or bugc). +- `Consumers:` what the change means for a reader of `ethdebug/format` data (a + debugger such as soldb). + +Each `Producers:` and `Consumers:` sub-item starts with one of three prefixes: + +- `no change needed.` Nothing that was valid becomes invalid, and nothing + changes meaning for this party. One short reason may follow. +- `optional:` The change adds a capability. Nothing that was valid in the + previous published version changes. No party is obliged to do anything. The + sub-item says what the party may now do. +- `required:` Output that was valid in the previous published version no longer + validates, or the specification adds or changes a normative **must**, or the + meaning of data that was already valid changes. The sub-item names the schema keyword, or + quotes the specification prose, that imposes the obligation. Prose that says + **should** or "preferred" is never `required:`. + +A consumer has an obligation only where producers can now emit something that a +conforming consumer would otherwise misread or reject, or where the meaning of +data that was already valid changed. A consumer that does not use a new optional +feature has no obligation. + +A new branch in a closed `oneOf` that a consumer must interpret to read the data +at all (for example a new pointer expression or collection) is `required:` for +consumers, because producers may start to emit it at any time. An addition that +a consumer can skip without misreading anything else (for example a new kind of +context) is `optional:`. + +Each impact line states the net effect for a party that moves from the previous +published version to the version of the section. A change inside a schema that +is new in that version obliges nobody, so its lines are `optional:` or +`no change needed.` and describe how the new schema works. An obligation that a +later change in the same version reverses does not appear in an impact line; the +summary may tell the history. + +Each version has at most two sections. `### Added` holds a new schema, or a new +keyword or capability. `### Changed` holds a change to something that exists. +The sections do not signal obligations; the prefixes do. + +## Unreleased + +### Changed + +- A segment's `offset` is no longer limited to a value below `$wordsize`: an + offset at or past `$wordsize` now carries into later slots. The default + `length` is now `$wordsize - (offset mod $wordsize)`, which runs to the end of + the slot in which the segment begins and equals the earlier default for an + offset inside the slot. A `length` that spans slots was already defined. A new + segment example shows the carry, and a new pointer example collapses a long + storage string to one multi-slot region ([#284]). + - Schemas: **ethdebug/format/pointer/scheme/segment**, + **ethdebug/format/pointer** + - Producers: optional: a producer may emit an `offset` at or past `$wordsize` + and let it carry into later slots. Every pointer that was valid before keeps + its meaning. + - Consumers: required: find the slot and byte of an `offset` by division and + remainder against `$wordsize`, and compute the default `length` as + `$wordsize - (offset mod $wordsize)`. The `offset` description no longer + says that the offset "**must** begin inside the slot". + +- A property lookup through `$this` (for example `{ ".length": "$this" }`) must + not be circular: the property it reads has to be resolvable without depending + on the value being defined. A struct-array example that named its own region + the long way around now uses `$this` ([#284]). + - Schemas: **ethdebug/format/pointer/expression**, + **ethdebug/format/pointer** + - Producers: required: do not emit a circular property lookup through `$this`. + A new "must not be circular" in the description imposes this; no validator + catches it. A pointer that already resolves is not affected. + - Consumers: no change needed. The rule constrains producers only, and no + valid data changes meaning. + +- Expressions now evaluate to one of two sorts, an unbounded integer or + definite-width bytes, and the `$concat` and `$keccak256` operands **must** be + width-bearing bytes rather than bare integers. A hexadecimal literal with an + even number of digits is bytes of exactly the width written, a literal with an + odd number of digits is an integer, and widths are never inferred from + context; the earlier text let a literal omit leading zeroes and padded it to + the width of its context. No schema keyword changes, so a validator accepts + the same documents as before, but an expression that passes a bare integer to + `$concat` or `$keccak256`, as the schema's own earlier examples did, no longer + conforms. Those examples are corrected ([#286]). + - Schemas: **ethdebug/format/pointer/expression** + - Producers: required: give each bare-integer operand of `$concat` or + `$keccak256` a width first with `$wordsized` or a `$sizedN` form, and write + a literal that is meant as bytes with its full number of digits (a new + "**must** be width-bearing" in the description). No validator catches this. + - Consumers: required: the meaning of literals changed ("Widths are never + inferred from context"). Read an even-digit hexadecimal literal as bytes of + exactly the width written, and an odd-digit literal or a JSON number as an + integer. Arithmetic results are unbounded integers. The specification does + not say what a consumer does with a bare-integer operand. + +## 0.1.0-1 — 2026-09-16 + +### Added + +- A character encoding is a label defined by the WHATWG Encoding Standard, and + an omitted optional encoding field means `utf-8`. + **ethdebug/format/materials/source** and + **ethdebug/format/type/elementary/string** both reference the new primitive + instead of accepting a free-form string. The label requirement is a normative + tightening that no validator catches: the schema is still `type: string`, so + a label the Standard does not define validates anyway. ([#285]) + - Schemas: **ethdebug/format/materials/encoding**, + **ethdebug/format/materials/source**, + **ethdebug/format/type/elementary/string** + - Producers: required: an `encoding` value must be a label that the Standard + defines (a new **must** in the description); no validator catches this. The + canonical lowercase name (`utf-16le`, not `utf-16`) is only preferred. + - Consumers: optional: a consumer may pass the value straight to + `new TextDecoder(label)`. No validator rejects a label from outside the + Standard. An omitted field means `utf-8`, as it did before. + +- A context may list the compiler transformations that produced an instruction — + `inline`, `tailcall`, `fold`, `coalesce`, with an extensible identifier set + and repeats allowed. A transform annotates rather than replaces the semantic + contexts, and composes flat beside them on one context object. The + **ethdebug/format/program/context/function/invoke** page now describes how a + debugger reconstructs activations from `invoke` and `return` contexts, and + says that a compiler must emit the two as a bracket around a body. ([#216]) + - Schemas: **ethdebug/format/program/context/transform**, + **ethdebug/format/program/context**, + **ethdebug/format/program/context/function/invoke** + - Producers: optional: the **ethdebug/format/program/context** schemas are new + in 0.1.0-1. A producer that emits `invoke` and `return` **must** emit them + as a bracket: `invoke` on the first instruction of a body, `return` on its + last. `transform` is optional. + - Consumers: optional: the schemas are new in 0.1.0-1. A consumer may ignore + `transform`, or use it to reconstruct inlined and tail-call activations. It + should keep an unfamiliar identifier as an opaque label. + +- An optional `activation` string pairs an invocation with the return or revert + that ends it. Distinct activations carry distinct values, unique within the + program. ([#245]) + - Schemas: **ethdebug/format/program/context/function/invoke**, + **ethdebug/format/program/context/function/return**, + **ethdebug/format/program/context/function/revert** + - Producers: optional: the **ethdebug/format/program/context/function** + schemas are new in 0.1.0-1. A producer may put one `activation` string on an + `invoke` and on the `return` or `revert` that ends it, distinct for each + activation. + - Consumers: optional: the schemas are new in 0.1.0-1. Where `activation` is + present, a consumer that reads them may pair a call with its return or + revert by that value instead of by strict nesting in trace order. + +- Contexts mark the function-call lifecycle — an invocation of exactly one kind + (`jump` for an internal call, `message` for an external message call, `create` + for a contract creation), a successful return, or a revert — each carrying + optional function identity (`identifier`, `declaration`, `type`). + **ethdebug/format/type/specifier** names the "full type or `{ id }` reference" + pattern that **ethdebug/format/type/wrapper** and context variables now share. + The `context` description in **ethdebug/format/program/instruction** also + gained a sentence that tied context pointers to the machine state at that + instruction's trace step; [#281] later replaced it with the postcondition + convention. ([#154]) + - Schemas: **ethdebug/format/program/context/function**, + **ethdebug/format/program/context/function/invoke**, + **ethdebug/format/program/context/function/return**, + **ethdebug/format/program/context/function/revert**, + **ethdebug/format/type/specifier**, **ethdebug/format/type/wrapper**, + **ethdebug/format/program/context**, + **ethdebug/format/program/context/variables**, + **ethdebug/format/program/instruction** + - Producers: optional: the **ethdebug/format/program/context/function** + schemas and **ethdebug/format/type/specifier** are new in 0.1.0-1. An + `invoke` gives exactly one of `jump`, `message` and `create`. + **ethdebug/format/type/wrapper** accepts the same data as at 0.1.0-0. + - Consumers: optional: the function contexts are new in 0.1.0-1; a consumer + may read them to follow calls, returns and reverts. + **ethdebug/format/type/wrapper** validates the same data as at 0.1.0-0. + +- A context may carry a `name` label, now wired into the context dispatcher; it + is most useful for telling `pick` alternatives apart. ([#179]) + - Schemas: **ethdebug/format/program/context/name**, + **ethdebug/format/program/context**, + **ethdebug/format/program/context/pick** + - Producers: optional: **ethdebug/format/program/context/name** is new in + 0.1.0-1. A producer that emits contexts may put a `name` string on one, for + example to tell `pick` alternatives apart. + - Consumers: no change needed. The schema is new in 0.1.0-1; a consumer that + reads it treats `name` as an opaque label with no format-imposed semantics. + +- An array type may state a fixed element `count`; omitting it means the array + is dynamically sized. ([#168]) + - Schemas: **ethdebug/format/type/complex/array** + - Producers: required: for fixed-size arrays only. State `count`, because an + array type without `count` now means a dynamic array ("When omitted, the + array is dynamically sized"). `count` is not in `required`. + - Consumers: required: read a missing `count` as a dynamic array and a present + `count` as the fixed number of elements. The `count` description changed the + meaning of an array type without `count`. + +- A pointer may declare templates inline with a `templates`/`in` pair, and a + template reference may remap the region names a template produces through + `yields`; unmapped names pass through unchanged. ([#158]) + - Schemas: **ethdebug/format/pointer/collection/templates**, + **ethdebug/format/pointer/collection/reference**, + **ethdebug/format/pointer**, **ethdebug/format/pointer/collection** + - Producers: optional: a producer may declare templates inline with + `templates` and `in`, which makes a pointer self-contained, and may add + `yields` to a template reference to reuse it without region-name collisions. + - Consumers: required: a consumer that resolves pointers must support both + forms, because a valid pointer can now contain them (a new `oneOf` branch + for `templates` in **ethdebug/format/pointer/collection**; a new `yields` + property). Templates in `templates` are available by name inside `in`. + `yields` renames regions; unmapped names pass through unchanged. + +- A `{ "$concat": [...] }` expression evaluates to the concatenation of its + operands' bytes, preserving each operand's byte width; an empty operand list + is permitted. ([#156]) + - Schemas: **ethdebug/format/pointer/expression** + - Producers: optional: a producer may use `$concat` to build a byte sequence + from several operands, for example a storage slot key or the input to a + hash. + - Consumers: required: a consumer that resolves pointers must support + `$concat`, because a valid pointer can now contain it (a new `Concat` branch + in the schema's `oneOf`). Operands join in list order and keep their byte + widths; no padding is added or removed. + +- A `pick` lists two or more alternative contexts of which one holds, a `gather` + lists two or more contexts that all hold simultaneously, and `frame` names, + as a bare string, the compilation frame a context's facts belong to — for + example `"ir"` or `"source"`. ([#144]) + - Schemas: **ethdebug/format/program/context/pick**, + **ethdebug/format/program/context/gather**, + **ethdebug/format/program/context/frame**, + **ethdebug/format/program/context** + - Producers: optional: the **ethdebug/format/program/context** schemas are new + in 0.1.0-1. A `pick` or `gather` list needs at least two members + (`minItems: 2`). `gather` is needed only where two facts use the same key. + - Consumers: optional: the schemas are new in 0.1.0-1. A consumer that reads + them reads a `pick` as alternatives of which one is true, reads all members + of a `gather` as true together, and may separate facts by `frame`. + +- One schema covers a non-negative integer given either as a JSON number or as a + `0x`-prefixed hex string; **ethdebug/format/materials/source-range** offsets + and lengths, **ethdebug/format/pointer/expression** literals, and + **ethdebug/format/program/instruction** offsets and operation arguments all + reference it. ([#126]) + - Schemas: **ethdebug/format/data/value**, + **ethdebug/format/materials/source-range**, + **ethdebug/format/pointer/expression**, + **ethdebug/format/program/instruction** + - Producers: optional: **ethdebug/format/materials/source-range** `offset` and + `length` may now be `0x`-prefixed hex strings. The new + **ethdebug/format/program/instruction** takes either form. + - Consumers: required: accept `0x`-prefixed hex strings as well as JSON + numbers in **ethdebug/format/materials/source-range** `offset` and `length` + (`type: number` at 0.1.0-0; now **ethdebug/format/data/value**). + **ethdebug/format/program/instruction** is new in 0.1.0-1 and takes both + forms too. + +- A context may carry a human-readable `remark` string, intended primarily for + humans to use as an annotation and not for compilers to use directly. ([#125]) + - Schemas: **ethdebug/format/program/context/remark**, + **ethdebug/format/program/context** + - Producers: optional: **ethdebug/format/program/context** is new in 0.1.0-1. + A producer that emits a context may put a `remark` string on it; a context + may hold only a `remark`. + - Consumers: optional: the schema is new in 0.1.0-1. A consumer that reads it + may display the `remark`. It is an annotation for humans and has no other + meaning. + +- A new **ethdebug/format/info** schema can represent all debugging information + of one compilation as one standalone document that holds its `compilation`, + its `programs`, and by-name lookup tables for `types` and `pointers`. A new + **ethdebug/format/info/resources** schema holds only the lookup tables (and an + optional `compilation`), for compilers that give the other data elsewhere in + their output. The same change raised the compilation `id` uniqueness + requirement from **should** to **must** (relaxed back to **should** by + [#131]). ([#123]) + - Schemas: **ethdebug/format/info**, **ethdebug/format/info/resources**, + **ethdebug/format/materials/compilation** + - Producers: optional: **ethdebug/format/info** and + **ethdebug/format/info/resources** are new in 0.1.0-1. A resources object + gives `types` and `pointers` (`required`); an **ethdebug/format/info** + document also gives `compilation` and `programs`. Compilation `id` + uniqueness stays **should**. + - Consumers: optional: the schemas are new in 0.1.0-1. A consumer that reads + either form may look up types and pointer templates by name in `types` and + `pointers`; how a reference resolves against them is not specified. + +- A program describes one bytecode of a compilation — its `contract`, its + `environment` (`call` or `create`), the `context` holding before its first + instruction, and its `instructions`, each with an `offset`, an `operation`, + and a `context` carrying source-range and variable facts. ([#113]) + - Schemas: **ethdebug/format/program**, + **ethdebug/format/program/instruction**, + **ethdebug/format/program/context**, + **ethdebug/format/program/context/code**, + **ethdebug/format/program/context/variables**, + **ethdebug/format/program/context/name** + - Producers: optional: **ethdebug/format/program** is new in 0.1.0-1. A + producer that emits one per bytecode gives `contract`, `environment` and + `instructions`, and an `offset` per instruction (`required`). For EOF + bytecode, `offset` **must** count from the container start. A variable + `type`, when given, **must** be a full type or an `{ id }` reference. + - Consumers: no change needed. Every schema here is new in 0.1.0-1, and no + schema from 0.1.0-0 references them. A consumer that reads a program finds + each instruction by its `offset`. + +- A pointer template is a pointer parameterised over the variables it lists in + `expect`, and a `{ "template": ... }` collection instantiates one by name. + ([#103]) + - Schemas: **ethdebug/format/pointer/template**, + **ethdebug/format/pointer/collection/reference**, + **ethdebug/format/pointer/collection** + - Producers: optional: a producer may write a repeated pointer shape one time + as a template and refer to it with a `template` collection. Definitions can + go in **ethdebug/format/info/resources** `pointers` ([#123]) or an inline + `templates` collection ([#158]). + - Consumers: required: a consumer that resolves pointers must support the + `template` collection (a new `oneOf` branch in + **ethdebug/format/pointer/collection**): find the template by name and bind + its `expect` variables from the scope at the reference. + +- Two primitives cover a `0x`-prefixed hex string of at least one digit and a + non-negative JSON integer. **ethdebug/format/materials/source-range** `offset` + and `length`, **ethdebug/format/pointer/expression** literals, and + **ethdebug/format/type/elementary/bytes** `size` reference them. This tightens + source-range values from any number to a non-negative integer and makes the + non-negative limit on integer literals effective. The `bits` and `places` of + the elementary numeric types change from `type: number` to `type: integer`. + ([#104]) + - Schemas: **ethdebug/format/data/hex**, **ethdebug/format/data/unsigned**, + **ethdebug/format/materials/source-range**, + **ethdebug/format/pointer/expression**, + **ethdebug/format/type/elementary/bytes**, + **ethdebug/format/type/elementary/int**, + **ethdebug/format/type/elementary/uint**, + **ethdebug/format/type/elementary/fixed**, + **ethdebug/format/type/elementary/ufixed** + - Producers: required: in **ethdebug/format/materials/source-range** `offset` + and `length`, and in an expression's integer literal, a JSON number must be + a non-negative integer (`type: integer`, `minimum: 0`). A bytes type `size` + (`0` is now valid) and the `places` of `fixed` and `ufixed` must be + integers; `bits` accepts the same values. + - Consumers: no change needed. Every instance that stays valid keeps its + meaning; the only newly valid value from this entry is a bytes type `size` + of `0`. + +### Changed + +- An instruction's `context` is its **postcondition** — both its semantic + facts and the pointers it contains resolve against the machine state after + that instruction has executed. The program-level `context` is the base case of + the chain: the precondition to instruction 0. ([#281]) + - Schemas: **ethdebug/format/program/instruction**, + **ethdebug/format/program** + - Producers: optional: **ethdebug/format/program** is new in 0.1.0-1. A + producer that emits it gives each instruction a `context` that holds after + the instruction; the program-level `context` holds before instruction 0. + - Consumers: no change needed. The schema is new in 0.1.0-1; a consumer that + reads it applies the context of instruction _i − 1_ at step _i_, and the + program-level `context` before the first instruction. + +- An object whose property set the format fully defines rejects unknown + properties. **ethdebug/format/materials/compilation**'s `settings` object + stays open, since it holds settings in a format native to the compiler, and + **ethdebug/format/info/resources** stays lax when validated alone because + **ethdebug/format/info** composes it by `$ref`. ([#280]) + - Schemas: **ethdebug/format/info**, **ethdebug/format/program**, + **ethdebug/format/program/instruction**, + **ethdebug/format/materials/compilation**, + **ethdebug/format/materials/source**, + **ethdebug/format/program/context/function/return**, + **ethdebug/format/program/context/function/revert**, + **ethdebug/format/pointer/collection/list** + - Producers: required: remove unknown keys from + **ethdebug/format/materials/compilation** (top level and `compiler`; + `settings` stays open) and **ethdebug/format/materials/source** + (`unevaluatedProperties: false`), and from `list` of a list collection + (`additionalProperties: false`). The other five schemas are new in 0.1.0-1. + - Consumers: no change needed. The change only narrows what producers can + emit. + +- An elementary type **must not** carry `contains`, which is what discriminates + it from a complex type; a `function` complex type requires `kind` and + `contains`; `contract`'s `library` and `interface` flags are booleans; a + context `name` must be non-empty; and a region requires `location`. In a + `function` complex type, the `parameters` wrapper and an external function's + `contract` wrapper each also accept an `{ id }` type reference + (**ethdebug/format/type/reference**) in place of an inline type, matching what + `returns` already allowed. ([#279]) + - Schemas: **ethdebug/format/type/elementary**, + **ethdebug/format/type/complex/function**, + **ethdebug/format/type/elementary/contract**, + **ethdebug/format/program/context/name**, **ethdebug/format/pointer/region** + - Producers: required: these no longer validate: an elementary type with + `contains` (`not`), a `function` type with no `contains` (`required`), and a + non-boolean `library` or `interface` (`type: boolean`). A region with no + `location` already failed. **ethdebug/format/program/context/name** is new + in 0.1.0-1. The `{ id }` references are optional. + - Consumers: required: a consumer that reads function types must accept an + `{ id }` reference in the `contains.parameters` wrapper and in an external + function's `contains.contract` wrapper (a new `oneOf` in each), and resolve + it like any other type reference. + +- Schema examples are corrected — a struct member's field is `name`, a `list` + index is referenced bare rather than through `$read`, packed `uint128` members + are 16 bytes and not 128 — and `additionalItems`, which JSON Schema 2020-12 + does not define, is dropped where it was inert. + **ethdebug/format/pointer/collection/scope**'s `define` validates its keys by + referencing **ethdebug/format/pointer/identifier** rather than an inline copy + of that pattern. The **ethdebug/format/type/definition** description now + states the rule that its `anyOf` enforces: at least one of `name` or + `location`. ([#277]) + - Schemas: **ethdebug/format/type/base**, **ethdebug/format/type/definition**, + **ethdebug/format/pointer/collection/list**, + **ethdebug/format/pointer/collection/scope**, + **ethdebug/format/info/resources**, + **ethdebug/format/program/context/function/invoke**, + **ethdebug/format/program**, **ethdebug/format/info**, + **ethdebug/format/pointer/template**, + **ethdebug/format/program/context/gather**, + **ethdebug/format/program/context/pick**, + **ethdebug/format/program/context/variables** + - Producers: no change needed. No instance changes validity; the reworded + **must** states what the `anyOf` already enforced. + - Consumers: no change needed. The accepted shape of every valid instance is + unchanged. + +- A `name` is an optional identifier, meant to be unique within a program, that + today acts only as a label — the format defines no way to reference a + context by name, so a declared name is inert. Establishing the identifier is + groundwork for a future name-based `pick` selection. ([#251]) + - Schemas: **ethdebug/format/program/context/name** + - Producers: optional: **ethdebug/format/program/context/name** is new in + 0.1.0-1. A producer that emits it gives a `name` that is meant to be unique + within a program; that is an intent, not a **must**. + - Consumers: no change needed. The schema is new in 0.1.0-1; a consumer that + reads it treats a `name` as a label, because the format defines no reference + by name. + +- The `target` field is optional on an internal (`jump`) invocation, for the + cases with no meaningful target pointer — most notably the first instruction + of an inlined body, whose JUMP has been elided. External calls still require + it; a contract creation has no `target` field. ([#242]) + - Schemas: **ethdebug/format/program/context/function/invoke** + - Producers: optional: **ethdebug/format/program/context/function/invoke** is + new in 0.1.0-1. A producer that emits it may omit `target` on an internal + (`jump`) invocation. An external (`message`) call requires `target`. + - Consumers: no change needed. The schema is new in 0.1.0-1; a consumer that + reads it accepts an internal (`jump`) invoke context that has no `target`. + +- A function return context's `data` field is optional, so a bare `return: {}` + is permitted where a return is known to happen but no return value is + observable — a void function, or a tail-call-optimized back-edge. ([#211]) + - Schemas: **ethdebug/format/program/context/function/return** + - Producers: optional: **ethdebug/format/program/context/function/return** is + new in 0.1.0-1. A producer that emits it may omit `data` when no return + value is observable: a void function or a tail-call-optimized back-edge. + - Consumers: no change needed. The schema is new in 0.1.0-1; a consumer that + reads it accepts a return context that has no `data`, down to a bare + `return: {}`. + +- An id identifies a compilation resource such as a source file or a + compilation, and **must** be unique within the scope where it appears — source + ids within one compilation, for instance. Before, only + **ethdebug/format/materials/source** stated that requirement, and only for + source ids; the value stays numeric-or-string, as it already was. ([#172]) + - Schemas: **ethdebug/format/materials/id** + - Producers: required: do not reuse an id within one scope. A new **must** in + the **ethdebug/format/materials/id** description imposes this; at 0.1.0-0 + only source ids had that **must**. No validator catches a collision. + - Consumers: no change needed. A consumer may now rely on an id as a key + within its scope. + +- The array type schema carries a description of itself, of `contains`, and of + each of its examples. ([#170]) + - Schemas: **ethdebug/format/type/complex/array** + - Producers: no change needed. The change adds only descriptions and example + comments. + - Consumers: no change needed. The set of valid array types is unchanged. + +- The context schema dispatches on each property with `if`/`then` clauses + instead of an `anyOf` over the member schemas, so a general-purpose validator + reports one error against the relevant member rather than a failure of the + whole union. The rewrite is also what made an empty context (`{}`) validate, + matching the relaxation [#145] had documented: every `anyOf` member required + its own key, so an explicit `{}` still failed until here. ([#150]) + - Schemas: **ethdebug/format/program/context** + - Producers: optional: **ethdebug/format/program/context** is new in 0.1.0-1. + A producer that emits it may give an explicit `"context": {}`; the + `if`/`then` clauses accept the empty object. + - Consumers: no change needed. The schema is new in 0.1.0-1; a consumer that + reads it accepts an explicit `"context": {}` as the empty context. The + change in validator errors needs no action. + +- An instruction's `context` is optional, and omitting it is equivalent to the + empty context (`{}`). The program-level `context` was already optional and + gains the same statement. The change removed `context` from an instruction's + `required` and removed `minProperties: 1` from the context schema. An explicit + `{}` still did not validate: the context schema's `anyOf` rejected it, because + each of its members requires its own key, until [#150] replaced the union. + ([#145]) + - Schemas: **ethdebug/format/program**, + **ethdebug/format/program/instruction**, **ethdebug/format/program/context** + - Producers: optional: **ethdebug/format/program/instruction** is new in + 0.1.0-1. A producer that emits it may omit `context` on an instruction that + carries no facts; `context` is not in the `required` list. + - Consumers: no change needed. The schemas are new in 0.1.0-1; a consumer that + reads them treats a missing instruction `context` as the empty context + (`{}`). + +- A compilation's `id` is `required`. ([#138]) + - Schemas: **ethdebug/format/materials/compilation** + - Producers: required: emit `id` on every compilation; `id` is now in the + `required` list. At 0.1.0-0 the description called `id` optional. + - Consumers: no change needed. A consumer may now rely on `id` as present in + every valid compilation. + +- A compilation's `id` is described as its identifier rather than as optional, + and its uniqueness requirement returns to **should** after [#123] had raised + it to **must**. The `required` list is not actually changed here; [#138] does + that. ([#131]) + - Schemas: **ethdebug/format/materials/compilation** + - Producers: no change needed. Uniqueness of `id` is **should** at 0.1.0-0 and + at 0.1.0-1. The net change is that the `id` description no longer says + "Optional"; [#138] adds `id` to `required`. + - Consumers: no change needed. The uniqueness of `id` is **should** in both + releases, so a consumer has no new case to handle. + +- A variable object must carry at least one property, and must not carry + properties the format does not define. ([#127]) + - Schemas: **ethdebug/format/program/context/variables** + - Producers: optional: **ethdebug/format/program/context/variables** is new in + 0.1.0-1. A producer that emits it gives no empty variable object + (`minProperties: 1`) and no extension keys (`unevaluatedProperties: false`). + - Consumers: no change needed. The schema is new in 0.1.0-1, and the rule only + narrows what producers can emit. + +- The `range` field may be omitted as shorthand for a range spanning the entire + source contents, and is otherwise required. ([#107]) + - Schemas: **ethdebug/format/materials/source-range** + - Producers: optional: a source range that spans the entire source contents + may omit `range`. + - Consumers: required: treat a source range with no `range` as offset 0 + through the full length of the contents of the referenced source. `range` is + no longer in the `required` list. + +## 0.1.0-0 — 2024-07-04 + +First published version of the specification. + +[#103]: https://github.com/ethdebug/format/pull/103 +[#104]: https://github.com/ethdebug/format/pull/104 +[#107]: https://github.com/ethdebug/format/pull/107 +[#113]: https://github.com/ethdebug/format/pull/113 +[#123]: https://github.com/ethdebug/format/pull/123 +[#125]: https://github.com/ethdebug/format/pull/125 +[#126]: https://github.com/ethdebug/format/pull/126 +[#127]: https://github.com/ethdebug/format/pull/127 +[#131]: https://github.com/ethdebug/format/pull/131 +[#138]: https://github.com/ethdebug/format/pull/138 +[#144]: https://github.com/ethdebug/format/pull/144 +[#145]: https://github.com/ethdebug/format/pull/145 +[#150]: https://github.com/ethdebug/format/pull/150 +[#154]: https://github.com/ethdebug/format/pull/154 +[#156]: https://github.com/ethdebug/format/pull/156 +[#158]: https://github.com/ethdebug/format/pull/158 +[#168]: https://github.com/ethdebug/format/pull/168 +[#170]: https://github.com/ethdebug/format/pull/170 +[#172]: https://github.com/ethdebug/format/pull/172 +[#179]: https://github.com/ethdebug/format/pull/179 +[#211]: https://github.com/ethdebug/format/pull/211 +[#216]: https://github.com/ethdebug/format/pull/216 +[#242]: https://github.com/ethdebug/format/pull/242 +[#245]: https://github.com/ethdebug/format/pull/245 +[#251]: https://github.com/ethdebug/format/pull/251 +[#277]: https://github.com/ethdebug/format/pull/277 +[#279]: https://github.com/ethdebug/format/pull/279 +[#280]: https://github.com/ethdebug/format/pull/280 +[#281]: https://github.com/ethdebug/format/pull/281 +[#284]: https://github.com/ethdebug/format/pull/284 +[#285]: https://github.com/ethdebug/format/pull/285 +[#286]: https://github.com/ethdebug/format/pull/286 From 80656392ff95191d4861c23e3396d3599a8e26b0 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:36 -0400 Subject: [PATCH 2/7] web: show the specification changelog on the docs site The page at /spec/changelog imports the root CHANGELOG.md, so the site and the repository always show the same text. The file must stay valid MDX: no HTML comments, and no bare "<" or "{" outside code. --- packages/web/spec/changelog.mdx | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/web/spec/changelog.mdx diff --git a/packages/web/spec/changelog.mdx b/packages/web/spec/changelog.mdx new file mode 100644 index 000000000..cea320f7a --- /dev/null +++ b/packages/web/spec/changelog.mdx @@ -0,0 +1,9 @@ +--- +title: Changelog +hide_title: true +sidebar_position: 6 +--- + +import Changelog from "../../../CHANGELOG.md"; + + From 1fb21488fccfe0fef334fc50c4a14fb56925ef41 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:37 -0400 Subject: [PATCH 3/7] docs: add a changelog to each published package Each of the seven public packages gets a CHANGELOG.md keyed by its own version, because the packages are versioned independently. Entries describe changes that a user of the package can see. @ethdebug/format and @ethdebug/pointers were on npm at 0.1.0-0, so their files carry the history from that publication to 0.1.0-1, reconstructed from a comparison of the published tarballs. The other five packages were first published at 0.1.0-1. --- packages/bugc-react/CHANGELOG.md | 20 +++++ packages/bugc/CHANGELOG.md | 25 +++++++ packages/evm/CHANGELOG.md | 12 +++ packages/format/CHANGELOG.md | 106 +++++++++++++++++++++++++++ packages/pointers-react/CHANGELOG.md | 24 ++++++ packages/pointers/CHANGELOG.md | 87 ++++++++++++++++++++++ packages/programs-react/CHANGELOG.md | 23 ++++++ 7 files changed, 297 insertions(+) create mode 100644 packages/bugc-react/CHANGELOG.md create mode 100644 packages/bugc/CHANGELOG.md create mode 100644 packages/evm/CHANGELOG.md create mode 100644 packages/format/CHANGELOG.md create mode 100644 packages/pointers-react/CHANGELOG.md create mode 100644 packages/pointers/CHANGELOG.md create mode 100644 packages/programs-react/CHANGELOG.md diff --git a/packages/bugc-react/CHANGELOG.md b/packages/bugc-react/CHANGELOG.md new file mode 100644 index 000000000..3dbc80ae2 --- /dev/null +++ b/packages/bugc-react/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +This file tracks changes to the `@ethdebug/bugc-react` npm package, +React components for visualizing BUG compiler output. Changes to the +specification itself are tracked in the root +[`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +### Changed + +- `yarn watch` now copies stylesheet changes into `dist/` on every + edit instead of only at startup. Development-only, no effect on + the published package ([#299]). + +## 0.1.0-1 — 2026-09-16 + +First publication. + +[#299]: https://github.com/ethdebug/format/pull/299 diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md new file mode 100644 index 000000000..efeebdc54 --- /dev/null +++ b/packages/bugc/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +This file tracks changes to the `@ethdebug/bugc` npm package, the +BUG language compiler with `ethdebug/format` debug information +support. Changes to the specification itself are tracked in the root +[`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +### Changed + +- The pointer expressions that compute a mapping or array-element slot + now word-size their `$keccak256` operand, as the two-sorted + expression semantics require. Emitted debug information does not + change: nothing reaches that code path yet ([#286]). +- Normalized the `bin` path in `package.json` to the form npm + expects, silencing an auto-correction warning at publish time + ([#298]). + +## 0.1.0-1 — 2026-09-16 + +First publication. + +[#286]: https://github.com/ethdebug/format/pull/286 +[#298]: https://github.com/ethdebug/format/pull/298 diff --git a/packages/evm/CHANGELOG.md b/packages/evm/CHANGELOG.md new file mode 100644 index 000000000..bb99f6fc1 --- /dev/null +++ b/packages/evm/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +This file tracks changes to the `@ethdebug/evm` npm package (EVM +execution and state access for `ethdebug/format`). Changes to the +specification itself are tracked in the root +[`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +## 0.1.0-1 — 2026-09-16 + +First publication. diff --git a/packages/format/CHANGELOG.md b/packages/format/CHANGELOG.md new file mode 100644 index 000000000..b96cb1dc8 --- /dev/null +++ b/packages/format/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +This file tracks changes to the `@ethdebug/format` npm package (the schemas +distributed for programmatic use). Changes to the specification itself are +tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +## 0.1.0-1 — 2026-09-16 + +### Added + +- The entry point exports TypeScript types and guards for the spec's data model: + the `Program`, `Pointer`, `Type`, `Materials` and `Data` namespaces, their + `isProgram`, `isPointer`, `isType`, `isSource`, `isValue` and sibling guards, + and the `mayHaveClass` and `hasKind` helpers. `0.1.0-0` exported only + `describeSchema`, `referencesId`, `referencesYaml`, `schemas`, `schemaIds` and + the `Schema` type. ([`59a5c38`]) +- `Program.Context` covers the whole context vocabulary, each member with its + own guard: `pick` ([`74c7d5f`]), `frame` ([`fc976ed`]), `gather` + ([`e469502`]), the function-call contexts `invoke`, `return` and `revert` + ([#154]), `name` ([#246]) and `transform` ([#216]). +- The function-call context types carry an optional `activation` identifier that + correlates an invocation with its return or revert ([#245]), and treat + `invoke.target` ([#242]) and `return.data` ([#211]) as optional. +- The guards use `Array.isArray` instead of `instanceof Array`, so arrays from + another realm pass ([#231]). `Pointer.isIdentifier` matches the identifier + schema's pattern, and `Type.Elementary.isContract` carries and validates the + `library` and `interface` flags and rejects a type that sets both ([#276]). +- `describeSchema` accepts a schema id as a plain string, so + `describeSchema({ schema: "schema:ethdebug/format/program" })` works where + `0.1.0-0` required `{ schema: { id } }`. `referencesId` narrows a string as + well as an `{ id }` object, and a `SchemaId` type is exported. ([`e523e09`], + [`f87f678`]) +- The tarball includes `LICENSE` and `README.md`, and the manifest gives a full + `repository` entry with `directory: packages/format` ([#293]). + +### Changed + +- The bundled schemas are those of spec version `0.1.0-1`; see the root + `CHANGELOG.md`. `schemaIds` grows from 47 ids to 72, adding the + **ethdebug/format/program** family (including every `program/context/*` schema + and **ethdebug/format/program/instruction**), **ethdebug/format/info** and + **ethdebug/format/info/resources**, **ethdebug/format/data/value**, + **ethdebug/format/data/unsigned**, **ethdebug/format/data/hex**, + **ethdebug/format/type/specifier**, **ethdebug/format/materials/encoding**, + **ethdebug/format/pointer/template** and the + **ethdebug/format/pointer/collection/reference** and + **ethdebug/format/pointer/collection/templates** schemas. No schema id was + removed. +- `describeSchema` parses schema YAML with merge keys enabled, so the `<<:` + anchor merges that the schemas use resolve into the returned `schema` and + `rootSchema` objects instead of surviving as a literal `<<` property + ([`0ef2f37`]). +- The `SchemaInfo` that `describeSchema` returns omits `pointer` entirely when + no pointer was requested, where `0.1.0-0` always set the property, to + `undefined` if need be ([`f87f678`]). +- The tarball no longer contains declaration maps (`*.d.ts.map`), so editors + step into the shipped `.d.ts` files rather than the original sources ([#293]). +- Build and dependency housekeeping with no effect on the published API: + Prettier and ESLint passes, the move to Vitest, TypeScript project references, + `json-schema-typed` pinned to `8.0.1` and `yaml` raised to `^2.8.2` ([#293]). + +### Breaking + +- The package is an ES module: the manifest sets `"type": "module"` and `dist` + holds `import`/`export` statements, where `0.1.0-0` shipped CommonJS with + `require` calls. `require("@ethdebug/format")` now works only on Node versions + that support `require(esm)` (Node 20.19 and later); on earlier Node, and in a + CommonJS-only bundler pipeline, consumers have to switch to `import`. + ([`a5a7d6c`]) +- The shipped declarations import the package's own `#`-prefixed specifiers + (`#describe`, `#schemas`, `#types`), so TypeScript resolves the types only + under `moduleResolution` `node16`, `nodenext` or `bundler`. A project on the + legacy `node10` resolution gets "Cannot find module '#describe'" and no + exported members. ([`21e532e`], [#293]) +- The manifest declares `engines: { node: ">=20" }`, which `0.1.0-0` did not + ([#293]). +- The layout under `dist` changed, which breaks deep imports: + `dist/src/schemas.js` is now `dist/src/schemas/index.js`, `dist/yamls.js` is + now `dist/src/schemas/yamls.js` ([`10ab103`]), and the `dist/src/bundle.js` + module with its `bundleSchemas` function is gone ([`59a5c38`]). + +## 0.1.0-0 — 2024-07-04 + +First publication. + +[#154]: https://github.com/ethdebug/format/pull/154 +[#211]: https://github.com/ethdebug/format/pull/211 +[#216]: https://github.com/ethdebug/format/pull/216 +[#231]: https://github.com/ethdebug/format/pull/231 +[#242]: https://github.com/ethdebug/format/pull/242 +[#245]: https://github.com/ethdebug/format/pull/245 +[#246]: https://github.com/ethdebug/format/pull/246 +[#276]: https://github.com/ethdebug/format/pull/276 +[#293]: https://github.com/ethdebug/format/pull/293 +[`0ef2f37`]: https://github.com/ethdebug/format/commit/0ef2f37 +[`10ab103`]: https://github.com/ethdebug/format/commit/10ab103 +[`21e532e`]: https://github.com/ethdebug/format/commit/21e532e +[`59a5c38`]: https://github.com/ethdebug/format/commit/59a5c38 +[`74c7d5f`]: https://github.com/ethdebug/format/commit/74c7d5f +[`a5a7d6c`]: https://github.com/ethdebug/format/commit/a5a7d6c +[`e469502`]: https://github.com/ethdebug/format/commit/e469502 +[`e523e09`]: https://github.com/ethdebug/format/commit/e523e09 +[`f87f678`]: https://github.com/ethdebug/format/commit/f87f678 +[`fc976ed`]: https://github.com/ethdebug/format/commit/fc976ed diff --git a/packages/pointers-react/CHANGELOG.md b/packages/pointers-react/CHANGELOG.md new file mode 100644 index 000000000..a74a5b09d --- /dev/null +++ b/packages/pointers-react/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +This file tracks changes to the `@ethdebug/pointers-react` npm +package, React components for visualizing `ethdebug/format` pointer +resolution. Changes to the specification itself are tracked in the +root [`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +### Changed + +- Fixed the documented CSS import paths in comments; the stylesheets + ship under `dist/src/components/`, and `variables.css` must be + imported first ([#298]). +- `yarn watch` now copies stylesheet changes into `dist/` on every + edit instead of only at startup. Development-only, no effect on + the published package ([#299]). + +## 0.1.0-1 — 2026-09-16 + +First publication. + +[#298]: https://github.com/ethdebug/format/pull/298 +[#299]: https://github.com/ethdebug/format/pull/299 diff --git a/packages/pointers/CHANGELOG.md b/packages/pointers/CHANGELOG.md new file mode 100644 index 000000000..8f30000c0 --- /dev/null +++ b/packages/pointers/CHANGELOG.md @@ -0,0 +1,87 @@ +# Changelog + +This file tracks changes to the `@ethdebug/pointers` npm package, the reference +implementation of `ethdebug/format` pointers. Changes to the specification +itself are tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +### Changed + +- `dereference` resolves a region whose segment `offset` carries past the end of + a slot and whose `length` spans the slots that follow, instead of reading + within the starting slot only ([#284]). +- A pointer that passes a bare integer to `$concat` or `$keccak256` now throws, + naming the operand, where it used to hash the wrong preimage: a literal took + its minimal byte width, so a bare slot number hashed as fewer than 32 bytes. + Word-size the operand with `$wordsized` or a `$sizedN` form. ([#286]) + +## 0.1.0-1 — 2026-09-16 + +### Added + +- `dereference` resolves pointer templates. `DereferenceOptions` takes a + `templates` map, and the dereferencer handles a template reference collection + (`{ template, yields }`) and an inline `{ templates }` collection, reporting + an unknown template name or a missing expected variable as a thrown error. + ([`29df1e1`], [`1a8a752`], [`4e9d0ac`]) +- A template reference's `yields` mapping renames the regions the template + produces, so `Cursor.Regions.named` and `Cursor.Regions.lookup` find them + under the caller's names ([`1a8a752`]). +- Expression evaluation supports the `$concat` operator, concatenating its + operands' bytes ([`3866cec`]). +- The tarball includes `LICENSE`, the manifest gives a full `repository` entry + with `directory: packages/pointers`, and the `README.md` states the supported + TypeScript resolution modes ([#293]). + +### Changed + +- `@ethdebug/format` is a runtime dependency (`^0.1.0-1`), where `0.1.0-0` + listed it only as a devDependency. The shipped `evaluate` and dereference + modules import the `Pointer` guards from it, so the package no longer installs + usefully on its own. ([#293]) +- `Data` registers its custom inspect hook with + `Symbol.for("nodejs.util.inspect.custom")` rather than a top-level + `await import("util")`, so the module loads in environments without top-level + await and without Node's `util` ([`0697233`]). +- The tarball holds only `dist`, the manifest, `README.md` and `LICENSE`. + `0.1.0-0` also shipped the TypeScript sources under `src`, every test and + integration test (compiled and as source), the `test` helpers, + `bin/run-example.ts`, `tsconfig.json`, `typings.d.ts`, `jest.config.cjs` and + the declaration maps. ([#293]) +- Build and dependency housekeeping with no effect on the published API: + Prettier and ESLint passes, the move from Jest to Vitest, TypeScript project + references, type-only imports, and upgrades to `ethereum-cryptography` + `^2.2.1` and the dev toolchain ([#293]). + +### Breaking + +- `Pointer` and `isPointer` are no longer exported. The entry point now exports + `dereference`, `Data`, and the `DereferenceOptions`, `Cursor` and `Machine` + types only; the pointer type and its guard live in `@ethdebug/format`. + ([`59a5c38`]) +- The shipped declarations import the package's own `#`-prefixed specifiers + (`#cursor`, `#data`, `#dereference`, `#machine`), so TypeScript resolves the + types only under `moduleResolution` `node16`, `nodenext` or `bundler`. A + project on the legacy `node10` resolution gets "Cannot find module + '#dereference'" and no exported members. ([`21e532e`], [#293]) +- The manifest declares `engines: { node: ">=20" }`, which `0.1.0-0` did not + ([#293]). +- Deep imports into the shipped tree break: `dist/src/pointer.js`, + `dist/src/test-cases.js`, `dist/bin/run-example.js` and everything under + `dist/test` are gone ([`59a5c38`], [#293]). + +## 0.1.0-0 — 2024-07-04 + +First publication. + +[#284]: https://github.com/ethdebug/format/pull/284 +[#286]: https://github.com/ethdebug/format/pull/286 +[#293]: https://github.com/ethdebug/format/pull/293 +[`0697233`]: https://github.com/ethdebug/format/commit/0697233 +[`1a8a752`]: https://github.com/ethdebug/format/commit/1a8a752 +[`21e532e`]: https://github.com/ethdebug/format/commit/21e532e +[`29df1e1`]: https://github.com/ethdebug/format/commit/29df1e1 +[`3866cec`]: https://github.com/ethdebug/format/commit/3866cec +[`4e9d0ac`]: https://github.com/ethdebug/format/commit/4e9d0ac +[`59a5c38`]: https://github.com/ethdebug/format/commit/59a5c38 diff --git a/packages/programs-react/CHANGELOG.md b/packages/programs-react/CHANGELOG.md new file mode 100644 index 000000000..8433c6d09 --- /dev/null +++ b/packages/programs-react/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +This file tracks changes to the `@ethdebug/programs-react` npm +package, React components for visualizing `ethdebug/format` program +annotations. Changes to the specification itself are tracked in the +root [`CHANGELOG.md`](../../CHANGELOG.md). + +## Unreleased + +### Changed + +- Fixed the documented CSS import paths in comments; the stylesheets + ship under `dist/src/components/` ([#298]). +- `yarn watch` now copies stylesheet changes into `dist/` on every + edit instead of only at startup. Development-only, no effect on + the published package ([#299]). + +## 0.1.0-1 — 2026-09-16 + +First publication. + +[#298]: https://github.com/ethdebug/format/pull/298 +[#299]: https://github.com/ethdebug/format/pull/299 From bd660acc3f07674509ebfabaa67e8e3e5d96131e Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:38 -0400 Subject: [PATCH 4/7] packages: ship CHANGELOG.md in the published tarballs npm always includes package.json, README* and LICENSE* in a tarball, but not CHANGELOG.md. Each public package now lists the file in "files", and the tarball allow-list in bin/packlist.ts accepts CHANGELOG* so that bin/check-tarballs.ts does not reject it. --- RELEASING.md | 10 ++++++---- bin/packlist.test.ts | 1 + bin/packlist.ts | 1 + packages/bugc-react/package.json | 3 ++- packages/bugc/package.json | 3 ++- packages/evm/package.json | 3 ++- packages/format/package.json | 3 ++- packages/pointers-react/package.json | 3 ++- packages/pointers/package.json | 3 ++- packages/programs-react/package.json | 3 ++- 10 files changed, 22 insertions(+), 11 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index fbfa02229..d2c454189 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -240,10 +240,12 @@ GitHub Actions the script does not pass `--provenance`. - `bin/check-tarballs.ts` (CI, `run-tests` job) lists the files that `npm pack` would put in each public package's tarball and fails if any file lies outside `package.json`, `README*`, `LICENSE*`, - `dist/src/` or `dist/bin/`, or if any `.test.` or `.tsbuildinfo` - file is included. It catches a wrong `files` field, test files - leaking into the package, and stale build output. The publish - script runs the same check before every publish. + `CHANGELOG*`, `dist/src/` or `dist/bin/`, or if any `.test.` or + `.tsbuildinfo` file is included. It catches a wrong `files` field, + test files leaking into the package, and stale build output. The + publish script runs the same check before every publish. npm ships + `package.json`, `README*` and `LICENSE*` whatever `files` says, but + not `CHANGELOG.md`, so every public package lists it in `files`. - `bin/smoke-tarballs.ts` (CI, `run-tests` job) packs every public package, installs each tarball into a throwaway consumer project together with the tarballs of its sibling dependencies, imports diff --git a/bin/packlist.test.ts b/bin/packlist.test.ts index beeeab145..be0aa10bf 100644 --- a/bin/packlist.test.ts +++ b/bin/packlist.test.ts @@ -8,6 +8,7 @@ describe("checkPackList", () => { "package.json", "README.md", "LICENSE", + "CHANGELOG.md", "dist/src/index.js", "dist/src/a/b.d.ts", "dist/bin/bugc.js", diff --git a/bin/packlist.ts b/bin/packlist.ts index 74fdc31ba..fc0d89587 100644 --- a/bin/packlist.ts +++ b/bin/packlist.ts @@ -4,6 +4,7 @@ const allowed = [ /^package\.json$/, /^README[^/]*$/, /^LICENSE[^/]*$/, + /^CHANGELOG[^/]*$/, /^dist\/src\//, /^dist\/bin\//, ]; diff --git a/packages/bugc-react/package.json b/packages/bugc-react/package.json index 35c5225f1..25ed9c913 100644 --- a/packages/bugc-react/package.json +++ b/packages/bugc-react/package.json @@ -13,7 +13,8 @@ "license": "MIT", "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" diff --git a/packages/bugc/package.json b/packages/bugc/package.json index c491c62ef..18753d677 100644 --- a/packages/bugc/package.json +++ b/packages/bugc/package.json @@ -17,7 +17,8 @@ }, "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" diff --git a/packages/evm/package.json b/packages/evm/package.json index 540777384..e7ead3d5b 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -13,7 +13,8 @@ "license": "MIT", "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" diff --git a/packages/format/package.json b/packages/format/package.json index 3867b2232..1e4cc87ac 100644 --- a/packages/format/package.json +++ b/packages/format/package.json @@ -12,7 +12,8 @@ "license": "MIT", "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" diff --git a/packages/pointers-react/package.json b/packages/pointers-react/package.json index 0bbfde890..2bb890652 100644 --- a/packages/pointers-react/package.json +++ b/packages/pointers-react/package.json @@ -13,7 +13,8 @@ "license": "MIT", "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" diff --git a/packages/pointers/package.json b/packages/pointers/package.json index 8cebbbf4d..30dd0652e 100644 --- a/packages/pointers/package.json +++ b/packages/pointers/package.json @@ -12,7 +12,8 @@ "license": "MIT", "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" diff --git a/packages/programs-react/package.json b/packages/programs-react/package.json index aa2e0b14d..c1786c05e 100644 --- a/packages/programs-react/package.json +++ b/packages/programs-react/package.json @@ -13,7 +13,8 @@ "license": "MIT", "files": [ "dist", - "!dist/**/*.tsbuildinfo" + "!dist/**/*.tsbuildinfo", + "CHANGELOG.md" ], "engines": { "node": ">=20" From 461f0031e9e4ebeaab23dc479b7d668804a5f0f9 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:41 -0400 Subject: [PATCH 5/7] ci: require a changelog entry for schema and package changes bin/check-changelog.ts compares a pull request with its base branch. It fails when the diff changes schemas/ without the root CHANGELOG.md, or changes the src/, bin/ or package.json of a public package without that package's CHANGELOG.md. Colocated test files do not count. The label "changelog: skip" exempts a pull request. The job has its own workflow that also runs on "labeled" and "unlabeled" events: Actions cannot filter those events by label name, so in ci.yml each label change would run the whole suite again. --- .github/workflows/changelog.yml | 37 +++++ bin/check-changelog.test.ts | 230 ++++++++++++++++++++++++++++++++ bin/check-changelog.ts | 147 ++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 .github/workflows/changelog.yml create mode 100644 bin/check-changelog.test.ts create mode 100644 bin/check-changelog.ts diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 000000000..3cb50282c --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,37 @@ +name: Changelog +on: + pull_request: + # labeled and unlabeled re-run this workflow when the + # "changelog: skip" label is applied or removed. Actions cannot + # filter those events by label name, so the changelog check lives + # here rather than in ci.yml, where every label change would + # re-run the whole suite. + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + +jobs: + changelog: + runs-on: ubuntu-latest + if: >- + ${{ !contains(github.event.pull_request.labels.*.name, + 'changelog: skip') }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn + cache-dependency-path: yarn.lock + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Check changelogs are updated + run: >- + yarn tsx bin/check-changelog.ts + "origin/${{ github.base_ref || 'main' }}" diff --git a/bin/check-changelog.test.ts b/bin/check-changelog.test.ts new file mode 100644 index 000000000..a9264f582 --- /dev/null +++ b/bin/check-changelog.test.ts @@ -0,0 +1,230 @@ +import { describe, expect, it } from "vitest"; +import { + changelogMessage, + missingPackageChangelogs, + needsChangelog, +} from "./check-changelog.js"; + +const packagePrefixes = ["packages/format", "packages/pointers"]; + +describe("needsChangelog", () => { + it("is false when nothing relevant changed", () => { + expect(needsChangelog(["README.md"])).toBe(false); + }); + + describe("schemas rule", () => { + it("is false when no schema files changed", () => { + expect(needsChangelog(["packages/pointers/src/read.ts"])).toBe(false); + }); + + it("is false when schemas changed alongside CHANGELOG.md", () => { + expect( + needsChangelog(["schemas/pointer.schema.yaml", "CHANGELOG.md"]), + ).toBe(false); + }); + + it("is true when schemas changed without CHANGELOG.md", () => { + expect(needsChangelog(["schemas/pointer.schema.yaml"])).toBe(true); + }); + + it("ignores paths that merely contain the word schemas", () => { + expect(needsChangelog(["packages/format/schemas-notes.md"])).toBe(false); + }); + }); + + describe("per-package rule", () => { + it("is false when a package's src/ changed with its CHANGELOG.md", () => { + expect( + needsChangelog( + ["packages/pointers/src/read.ts", "packages/pointers/CHANGELOG.md"], + packagePrefixes, + ), + ).toBe(false); + }); + + it("is true when a package's src/ changed without its CHANGELOG.md", () => { + expect( + needsChangelog(["packages/pointers/src/read.ts"], packagePrefixes), + ).toBe(true); + }); + + it("is true when a package's bin/ changed without its CHANGELOG.md", () => { + expect( + needsChangelog(["packages/pointers/bin/cli.ts"], packagePrefixes), + ).toBe(true); + }); + + it("is true when a package.json changed without its CHANGELOG.md", () => { + expect( + needsChangelog(["packages/pointers/package.json"], packagePrefixes), + ).toBe(true); + }); + + it("ignores a colocated test file under src/", () => { + expect( + needsChangelog(["packages/pointers/src/read.test.ts"], packagePrefixes), + ).toBe(false); + }); + + it("ignores a colocated .tsx test file", () => { + expect( + needsChangelog( + ["packages/pointers-react/src/cursor.test.tsx"], + ["packages/pointers-react"], + ), + ).toBe(false); + }); + + it("ignores an examples test suite", () => { + expect( + needsChangelog( + ["packages/pointers/src/pointer.examples.test.ts"], + packagePrefixes, + ), + ).toBe(false); + }); + + it("is true when a test file accompanies a source change", () => { + expect( + needsChangelog( + [ + "packages/pointers/src/read.ts", + "packages/pointers/src/read.test.ts", + ], + packagePrefixes, + ), + ).toBe(true); + }); + + it("is false for a package not in the public list", () => { + expect( + needsChangelog(["packages/conformance/src/index.ts"], packagePrefixes), + ).toBe(false); + }); + + it("is false for files outside src/, bin/, and package.json", () => { + expect( + needsChangelog(["packages/pointers/README.md"], packagePrefixes), + ).toBe(false); + }); + }); + + describe("both rules at once", () => { + it("is true when schemas and a package both need entries", () => { + expect( + needsChangelog( + ["schemas/pointer.schema.yaml", "packages/pointers/src/read.ts"], + packagePrefixes, + ), + ).toBe(true); + }); + + it("is false when both changelogs are present", () => { + expect( + needsChangelog( + [ + "schemas/pointer.schema.yaml", + "packages/pointers/src/read.ts", + "CHANGELOG.md", + "packages/pointers/CHANGELOG.md", + ], + packagePrefixes, + ), + ).toBe(false); + }); + }); +}); + +describe("missingPackageChangelogs", () => { + it("lists only the packages that were touched", () => { + expect( + missingPackageChangelogs( + ["packages/pointers/src/read.ts"], + packagePrefixes, + ), + ).toEqual(["packages/pointers/CHANGELOG.md"]); + }); + + it("lists every touched package that is missing its file", () => { + expect( + missingPackageChangelogs( + ["packages/pointers/src/read.ts", "packages/format/package.json"], + packagePrefixes, + ), + ).toEqual([ + "packages/format/CHANGELOG.md", + "packages/pointers/CHANGELOG.md", + ]); + }); + + it("omits a package whose CHANGELOG.md is already in the diff", () => { + expect( + missingPackageChangelogs( + ["packages/pointers/src/read.ts", "packages/pointers/CHANGELOG.md"], + packagePrefixes, + ), + ).toEqual([]); + }); +}); + +describe("changelogMessage", () => { + it("lists only the changed schema paths", () => { + const message = changelogMessage([ + "schemas/pointer.schema.yaml", + "schemas/pointer/expression.schema.yaml", + "packages/pointers/src/read.ts", + ]); + expect(message).toContain("schemas/pointer.schema.yaml"); + expect(message).toContain("schemas/pointer/expression.schema.yaml"); + expect(message).not.toContain("packages/pointers/src/read.ts"); + }); + + it("points at the Unreleased section and the skip label", () => { + const message = changelogMessage(["schemas/pointer.schema.yaml"]); + expect(message).toContain("## Unreleased"); + expect(message).toContain("changelog: skip"); + }); + + it("names the skip label for a package-only failure", () => { + const message = changelogMessage( + ["packages/pointers/src/read.ts"], + packagePrefixes, + ); + expect(message).toContain("changelog: skip"); + }); + + it("names the skip label once when both rules apply", () => { + const message = changelogMessage( + ["schemas/pointer.schema.yaml", "packages/pointers/src/read.ts"], + packagePrefixes, + ); + expect(message.match(/changelog: skip/g)).toHaveLength(1); + }); + + it("mentions the skip label last", () => { + const message = changelogMessage(["schemas/pointer.schema.yaml"]); + expect(message.trimEnd().split("\n").at(-1)).toContain("changelog: skip"); + }); + + it("is empty when nothing is missing", () => { + expect(changelogMessage(["README.md"], packagePrefixes)).toBe(""); + }); + + it("names each missing package CHANGELOG.md", () => { + const message = changelogMessage( + ["packages/pointers/src/read.ts", "packages/format/package.json"], + packagePrefixes, + ); + expect(message).toContain("packages/pointers/CHANGELOG.md"); + expect(message).toContain("packages/format/CHANGELOG.md"); + }); + + it("combines the schemas and package sections when both apply", () => { + const message = changelogMessage( + ["schemas/pointer.schema.yaml", "packages/pointers/src/read.ts"], + packagePrefixes, + ); + expect(message).toContain("schemas/pointer.schema.yaml"); + expect(message).toContain("packages/pointers/CHANGELOG.md"); + }); +}); diff --git a/bin/check-changelog.ts b/bin/check-changelog.ts new file mode 100644 index 000000000..04cfb0d23 --- /dev/null +++ b/bin/check-changelog.ts @@ -0,0 +1,147 @@ +import { execFileSync } from "node:child_process"; +import { relative } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { readWorkspaces } from "./publish-tagged.js"; + +const defaultBase = "origin/main"; + +function touchesSchemas(changedPaths: string[]): boolean { + return changedPaths.some((path) => path.startsWith("schemas/")); +} + +// colocated test files carry no consumer-visible change, and this also +// covers the *.examples.test.ts suites +const testFile = /\.test\.tsx?$/; + +function touchesPackage(changedPaths: string[], prefix: string): boolean { + return changedPaths.some( + (path) => + !testFile.test(path) && + (path.startsWith(`${prefix}/src/`) || + path.startsWith(`${prefix}/bin/`) || + path === `${prefix}/package.json`), + ); +} + +export function missingPackageChangelogs( + changedPaths: string[], + packagePrefixes: string[], +): string[] { + const changed = new Set(changedPaths); + return packagePrefixes + .filter((prefix) => touchesPackage(changedPaths, prefix)) + .map((prefix) => `${prefix}/CHANGELOG.md`) + .filter((path) => !changed.has(path)); +} + +export function needsChangelog( + changedPaths: string[], + packagePrefixes: string[] = [], +): boolean { + const schemasViolation = + touchesSchemas(changedPaths) && !changedPaths.includes("CHANGELOG.md"); + return ( + schemasViolation || + missingPackageChangelogs(changedPaths, packagePrefixes).length > 0 + ); +} + +export function changelogMessage( + changedPaths: string[], + packagePrefixes: string[] = [], +): string { + const lines: string[] = []; + + if (touchesSchemas(changedPaths) && !changedPaths.includes("CHANGELOG.md")) { + const schemaPaths = changedPaths.filter((path) => + path.startsWith("schemas/"), + ); + lines.push( + "This PR changes schema files without updating CHANGELOG.md:", + ...schemaPaths.map((path) => ` ${path}`), + "", + 'Add an entry under "## Unreleased" in CHANGELOG.md.', + ); + } + + const missing = missingPackageChangelogs(changedPaths, packagePrefixes); + if (missing.length > 0) { + if (lines.length > 0) { + lines.push(""); + } + lines.push( + "This PR changes package files without updating the matching", + "package CHANGELOG.md:", + ...missing.map((path) => ` ${path}`), + "", + 'Add an entry under "## Unreleased" in each file listed above.', + ); + } + + if (lines.length > 0) { + lines.push( + "", + 'Apply the "changelog: skip" label to this PR if no entry is needed.', + ); + } + + return lines.join("\n"); +} + +function resolvesToCommit(root: string, ref: string): boolean { + try { + execFileSync( + "git", + ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], + { + cwd: root, + stdio: "ignore", + }, + ); + return true; + } catch { + return false; + } +} + +function changedPaths(root: string, base: string): string[] { + const stdout = execFileSync( + "git", + ["diff", "--name-only", `${base}...HEAD`], + { cwd: root, encoding: "utf8" }, + ); + return stdout + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} + +function publicPackagePrefixes(root: string): string[] { + return readWorkspaces(root) + .filter((workspace) => !workspace.private) + .map((workspace) => relative(root, workspace.dir)); +} + +export function main(argv: string[]): number { + const base = argv[0] ?? defaultBase; + const root = fileURLToPath(new URL("..", import.meta.url)); + if (!resolvesToCommit(root, base)) { + console.error( + `check-changelog: cannot resolve the base ref "${base}"; fetch it` + + " first, or name one that exists.", + ); + return 1; + } + const paths = changedPaths(root, base); + const packagePrefixes = publicPackagePrefixes(root); + if (needsChangelog(paths, packagePrefixes)) { + console.error(changelogMessage(paths, packagePrefixes)); + return 1; + } + console.log("changelog: ok"); + return 0; +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main(process.argv.slice(2))); +} From 3f03fece39fd2bf854b38c0f7a055f27bcdd15ce Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:44 -0400 Subject: [PATCH 6/7] bin: check the format of the specification changelog Each "Producers:" and "Consumers:" sub-item in the root CHANGELOG.md must start with "no change needed.", "optional:", or "required:", and each section heading must be "Added" or "Changed". The check reads the file on every run, whether or not the diff changes schemas/, and reports each problem with its line number. The bullets in the intro that describe the sub-items are not sub-items, and the check ignores them. --- bin/check-changelog.test.ts | 145 ++++++++++++++++++++++++++++++++++++ bin/check-changelog.ts | 78 ++++++++++++++++++- 2 files changed, 220 insertions(+), 3 deletions(-) diff --git a/bin/check-changelog.test.ts b/bin/check-changelog.test.ts index a9264f582..45659d062 100644 --- a/bin/check-changelog.test.ts +++ b/bin/check-changelog.test.ts @@ -1,8 +1,11 @@ import { describe, expect, it } from "vitest"; import { changelogMessage, + formatProblemsMessage, + impactLineProblems, missingPackageChangelogs, needsChangelog, + sectionProblems, } from "./check-changelog.js"; const packagePrefixes = ["packages/format", "packages/pointers"]; @@ -228,3 +231,145 @@ describe("changelogMessage", () => { expect(message).toContain("packages/pointers/CHANGELOG.md"); }); }); + +describe("impactLineProblems", () => { + const entry = (producers: string, consumers: string): string => + [ + "## Unreleased", + "", + "### Changed", + "", + "- A summary of the change ([#1]).", + " - Schemas: **ethdebug/format/pointer**", + ` - Producers: ${producers}`, + ` - Consumers: ${consumers}`, + "", + ].join("\n"); + + it("accepts the no change needed. prefix", () => { + expect( + impactLineProblems( + entry("no change needed.", "no change needed. One short reason."), + ), + ).toEqual([]); + }); + + it("accepts the optional: prefix", () => { + expect( + impactLineProblems( + entry("optional: a producer may emit it.", "optional: may read it."), + ), + ).toEqual([]); + }); + + it("accepts the required: prefix", () => { + expect( + impactLineProblems( + entry("required: `minimum` forbids it.", "required: must read it."), + ), + ).toEqual([]); + }); + + it("accepts a prefix on the line below a bare label", () => { + const text = [ + "- A summary.", + " - Producers:", + " optional: a producer may emit it.", + "", + ].join("\n"); + expect(impactLineProblems(text)).toEqual([]); + }); + + it("reports a bare imperative with its line number", () => { + const problems = impactLineProblems( + entry("emit the new field.", "no change needed."), + ); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain("line 7:"); + expect(problems[0]).toContain('"Producers:"'); + expect(problems[0]).toContain('"no change needed."'); + expect(problems[0]).toContain('"optional:"'); + expect(problems[0]).toContain('"required:"'); + }); + + it("reports each offending sub-item", () => { + const problems = impactLineProblems( + entry("emit the new field.", "Required: read the new field."), + ); + expect(problems).toHaveLength(2); + expect(problems[0]).toContain("line 7:"); + expect(problems[1]).toContain("line 8:"); + expect(problems[1]).toContain('"Consumers:"'); + }); + + it("reports a prefix that runs into the text after it", () => { + expect( + impactLineProblems(entry("optional:emit it.", "no change needed.")), + ).toHaveLength(1); + }); + + it("reports a bare label with no prefix below it", () => { + const text = ["- A summary.", " - Consumers:", " read it.", ""].join( + "\n", + ); + expect(impactLineProblems(text)).toEqual([ + expect.stringContaining("line 2:"), + ]); + }); + + it("ignores the intro bullets that describe the sub-items", () => { + const text = [ + "# Changelog", + "", + "- `Schemas:` the schema(s) the change touches.", + "- `Producers:` what the change means for an emitter of data (a", + " compiler such as solc or bugc).", + "- `Consumers:` what the change means for a reader.", + "", + "Each `Producers:` and `Consumers:` sub-item starts with a prefix:", + "", + "- `no change needed.` Nothing changes.", + "", + ].join("\n"); + expect(impactLineProblems(text)).toEqual([]); + }); + + it("accepts a file with no entries", () => { + expect(impactLineProblems("# Changelog\n\n## Unreleased\n")).toEqual([]); + expect(impactLineProblems("")).toEqual([]); + }); +}); + +describe("formatProblemsMessage", () => { + it("is empty when there are no problems", () => { + expect(formatProblemsMessage([])).toBe(""); + }); + + it("names the file and lists each problem", () => { + const message = formatProblemsMessage(["line 7: first", "line 9: second"]); + expect(message).toContain("CHANGELOG.md"); + expect(message).toContain(" line 7: first"); + expect(message).toContain(" line 9: second"); + }); +}); + +describe("sectionProblems", () => { + it("accepts Added and Changed sections", () => { + const text = "## Unreleased\n\n### Added\n\n### Changed\n"; + expect(sectionProblems(text)).toEqual([]); + }); + + it("rejects any other section with its line number", () => { + const text = "## Unreleased\n\n### Breaking\n"; + const problems = sectionProblems(text); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain("line 3"); + expect(problems[0]).toContain('"### Added"'); + expect(problems[0]).toContain('"### Changed"'); + }); + + it("ignores version headings and deeper headings", () => { + expect(sectionProblems("# Changelog\n\n## 0.1.0-1\n")).toEqual([]); + expect(sectionProblems("#### Breaking\n")).toEqual([]); + }); +}); diff --git a/bin/check-changelog.ts b/bin/check-changelog.ts index 04cfb0d23..6df4554ef 100644 --- a/bin/check-changelog.ts +++ b/bin/check-changelog.ts @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process"; -import { relative } from "node:path"; +import { readFileSync } from "node:fs"; +import { join, relative } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { readWorkspaces } from "./publish-tagged.js"; @@ -88,6 +89,67 @@ export function changelogMessage( return lines.join("\n"); } +const impactPrefixes = ["no change needed.", "optional:", "required:"]; + +// a real sub-item is indented two spaces and has a bare label; the intro +// bullets that describe the sub-items start at column 0 with a code span +const impactLabel = /^ {2}- (Producers|Consumers):(.*)$/; + +function startsWithImpactPrefix(text: string): boolean { + return impactPrefixes.some( + (prefix) => + text.startsWith(prefix) && + (text.length === prefix.length || /\s/.test(text[prefix.length])), + ); +} + +export function impactLineProblems(text: string): string[] { + const lines = text.split("\n"); + const allowed = impactPrefixes.map((prefix) => `"${prefix}"`).join(", "); + return lines.flatMap((line, index) => { + const match = impactLabel.exec(line); + if (!match) { + return []; + } + const [, label, rest] = match; + // the text starts on the label line after one space, or, when the + // label stands alone, on the continuation line below it + const conforms = + rest.trim().length > 0 + ? rest.startsWith(" ") && startsWithImpactPrefix(rest.slice(1)) + : startsWithImpactPrefix((lines[index + 1] ?? "").trimStart()); + return conforms + ? [] + : [`line ${index + 1}: "${label}:" must start with one of: ${allowed}`]; + }); +} + +const sectionNames = ["Added", "Changed"]; + +// the prefixes carry the obligations, so a section only says whether a +// change adds something new or alters something that exists +export function sectionProblems(text: string): string[] { + const allowed = sectionNames.map((name) => `"### ${name}"`).join(", "); + return text.split("\n").flatMap((line, index) => { + if (!line.startsWith("### ")) { + return []; + } + return sectionNames.includes(line.slice(4).trim()) + ? [] + : [`line ${index + 1}: section heading must be one of: ${allowed}`]; + }); +} + +export function formatProblemsMessage(problems: string[]): string { + if (problems.length === 0) { + return ""; + } + return [ + "CHANGELOG.md does not follow the entry format:", + ...problems.map((problem) => ` ${problem}`), + ].join("\n"); +} + function resolvesToCommit(root: string, ref: string): boolean { try { execFileSync( @@ -134,8 +196,18 @@ export function main(argv: string[]): number { } const paths = changedPaths(root, base); const packagePrefixes = publicPackagePrefixes(root); - if (needsChangelog(paths, packagePrefixes)) { - console.error(changelogMessage(paths, packagePrefixes)); + // the format lint runs on every diff, not only on schema changes + const changelog = readFileSync(join(root, "CHANGELOG.md"), "utf8"); + const problems = [ + ...impactLineProblems(changelog), + ...sectionProblems(changelog), + ]; + const messages = [ + changelogMessage(paths, packagePrefixes), + formatProblemsMessage(problems), + ].filter((message) => message.length > 0); + if (messages.length > 0) { + console.error(messages.join("\n\n")); return 1; } console.log("changelog: ok"); From 9eb46657b138571f11b156ea6b71a8bcd90ed889 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 17 Sep 2026 00:50:48 -0400 Subject: [PATCH 7/7] docs: add the changelog cut to the release steps Before the version bump, the operator renames "## Unreleased" to the new version and date in the root file and in the file of each package that Lerna will bump, and leaves an empty Unreleased heading above it. "yarn lerna changed" shows which packages those are. A package that is bumped only because a dependency changed gets one "Changed" entry that names the dependency, so that each published version has a section of its own. In the root file the operator also reconciles the Unreleased entries against the previous published version, because the impact lines state the net effect between published versions. --- RELEASING.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index d2c454189..c5003f527 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -43,7 +43,53 @@ guards that run in CI live in `bin/check-tarballs.ts` and yarn lerna list --all --json ``` -3. Bump to an explicit version. Lerna commits the result as +3. Update the changelogs before bumping. The root `CHANGELOG.md` + tracks the spec version; each public package under + `packages/*/CHANGELOG.md` tracks that package's own version. + + See which packages the next step will move: + + ```console + yarn lerna changed + ``` + + The bump in step 4 moves every package that command lists, plus + every package that depends on one of them. + + For the root file, and for each package about to be bumped, + rename its `## Unreleased` heading to + `## `: that package's own new version, + then today's date. Leave a fresh, empty `## Unreleased` heading + above the section you renamed. A package that is not being bumped + needs no change. + + When you rename `## Unreleased` in the root file, reconcile its + entries against the previous published version. Each `Producers:` + and `Consumers:` line states the net effect for a party that + moves from that version to the new one. If one Unreleased entry + reverses an obligation of another Unreleased entry, neither + impact line keeps that obligation; the summaries may still tell + the history. + + A package that is bumped only because a dependency of it changed + has nothing under `## Unreleased`. Give it a `### Changed` entry + reading "Updated `@ethdebug/` to ``.", so that every + published version has a section of its own. + + Commit the renamed files on their own, right before the version + bump in the next step: + + ```console + git add CHANGELOG.md packages/*/CHANGELOG.md + git commit -m "docs: cut changelog entries for " + ``` + + Pre-flight check: in each file you touched, the only remaining + `## Unreleased` section is the empty one at the top. Never publish + with entries still sitting under `## Unreleased` in a changelog + for a package (or the spec) being released. + +4. Bump to an explicit version. Lerna commits the result as `Publish` and creates one tag per bumped workspace on that commit: @@ -62,22 +108,22 @@ guards that run in CI live in `bin/check-tarballs.ts` and - `--no-push`: Lerna would otherwise run `git push --follow-tags --no-verify --atomic ` and, when the error text mentions "atomic", silently retry - WITHOUT `--atomic`. The push happens by hand in step 5 instead: + WITHOUT `--atomic`. The push happens by hand in step 6 instead: no non-atomic fallback, no `--no-verify` skipping the pre-push - hooks, and step 4's inspection happens before anything reaches + hooks, and step 5's inspection happens before anything reaches the remote. - `--yes`: skips the confirmation prompt. Preview with the command in step 2 first; do not use `--no-git-tag-version` as a preview, because it still rewrites every `package.json`. -4. Inspect the result before pushing: +5. Inspect the result before pushing: ```console git show --stat HEAD git tag --points-at HEAD # expect one tag per bumped workspace ``` -5. Push the commit and the tags in one atomic operation: +6. Push the commit and the tags in one atomic operation: ```console git push --atomic origin main --follow-tags @@ -110,7 +156,7 @@ guards that run in CI live in `bin/check-tarballs.ts` and git push origin :refs/tags/ ``` -6. Watch the workflow and confirm the result on the registry: +7. Watch the workflow and confirm the result on the registry: ```console gh run list --workflow publish.yml --limit 3