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/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 diff --git a/RELEASING.md b/RELEASING.md index fbfa02229..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 @@ -240,10 +286,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/check-changelog.test.ts b/bin/check-changelog.test.ts new file mode 100644 index 000000000..45659d062 --- /dev/null +++ b/bin/check-changelog.test.ts @@ -0,0 +1,375 @@ +import { describe, expect, it } from "vitest"; +import { + changelogMessage, + formatProblemsMessage, + impactLineProblems, + missingPackageChangelogs, + needsChangelog, + sectionProblems, +} 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"); + }); +}); + +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 new file mode 100644 index 000000000..6df4554ef --- /dev/null +++ b/bin/check-changelog.ts @@ -0,0 +1,219 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join, 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"); +} + +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( + "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); + // 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"); + return 0; +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main(process.argv.slice(2))); +} 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/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-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/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/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/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/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/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/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/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-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/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/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/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 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" 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"; + +