Skip to content

Releases: valthon/zigapagos

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 21:18
0bd0295

Added

  • Publish native arm64 release archives for Linux and macOS, with matching npm
    packages and shell-installer support. Apple Silicon and Linux arm64 installs
    now receive binaries built and exercised on their native runner architecture.
  • zigapagos release --summary: after a build, print on stdout an inventory of the files it
    emitted, grouped by category — pages, page aliases and alternatives, page assets, site
    assets, build assets, SPA shells, SPA routing manifests and the SPA 404 fallback. Every entry
    is recorded where the file is written, and tests/summary/summary.sh compares the printed set
    against the emitted tree, so the report cannot describe a tree the build did not produce. A
    build with rendering errors prints a one-line refusal instead of an inventory — on stdout too,
    so --summary >file answers on the same stream whatever the build's outcome.
  • A shell installer, curl -fsSL https://valthon.github.io/zigapagos/install.sh | sh, now the
    headline install method on the README and the download page. It installs a complete
    zigapagos — the binary, the @z/runtime tree it renders islands and SPAs through, and Bun and
    ZigBase when the host has neither — under ~/.local/share/zigapagos, with a generated launcher
    in ~/.local/bin. No sudo, no edits to shell startup files, and nothing written until each
    download has been verified against the release's published SHA-256 sums. It is idempotent: a
    second run installs alongside the first and repoints the launcher. --version, --prefix,
    --bin-dir, --no-bun and --no-zigbase cover the rest. Windows hosts are refused with the
    same wording the npm package uses, rather than being given an emulated build that looks native.
  • A runtime.tar.xz release asset: the @z/runtime tree with its dependencies vendored. This is
    what makes an install outside npm able to render an island at all — the per-target archives
    carry the binary alone, and the sidecar, bundlers and slicers are scripts inside that tree. It
    is staged by the same code that stages npm's copy (npm/stage-runtime.mjs), so the two
    channels ship the same files by construction.
  • zigapagos release builds the per-site islands runtime slice. The second
    pass over the built island bundles (/islands/_runtime.js) used to run only
    as a build-graph step, so a toolchain-free build silently shipped the full
    shared runtime to every island page.
  • zigapagos release emits host config and the strict-CSP artifacts. The
    per-namespace server config (.spa marker + zigbase.static_routes.zig,
    nginx.nginx.conf, .htaccess) and the site-wide csp.{nginx.conf,apache.conf,zigbase.txt}
    are written over the finished output tree. Every npm-path build until now
    shipped a tree with neither, which loses SPA deep-link fallback and serves a
    CSP that blocks the site's own inline import map.
  • zigapagos release --source-maps, replacing Options.source_maps. Still
    opt-in and off by default.
  • docs/runtime-dependencies.md — what the standalone binary needs at run
    time, stated once instead of inferred. A table covering every command and the
    external programs it requires; when zigapagos release actually needs Bun
    (the condition is the configuration, not whether the site has islands — with
    ZIGAPAGOS_RUNTIME_DIR set, a site with none still spawns the sidecar); how
    the pinned ZigBase is resolved, cached and fetched, including the curl and
    tar the fetch shells out to; and what each distribution supplies. Notably:
    a release archive carries the binary alone, so islands and SPAs built from one
    need an @z/runtime tree pointed at by ZIGAPAGOS_RUNTIME_DIR@z/runtime
    is private: true and cannot be installed from npm on its own.
  • tests/meta/runtime-deps-doc.sh checks that page against the sources every
    claim came from: the command table against src/main.zig's Command enum,
    the ZigBase pin and cache path against src/cli/zigbase.zig, the environment
    variable against src/cli/release.zig, @z/runtime's privacy against
    runtime/package.json, the binary-only release archive against
    build/release.zig, and every flag the page names against the file that
    parses it.

Changed

  • Build the x86_64 macOS archive natively on macos-15-intel instead of
    cross-compiling it on an arm64 macos-latest runner.
  • Release and development builds now share the checked-in Wuffs translation
    shims instead of release builds invoking zig translate-c. This removes a
    hand-maintained divergence and avoids the Zig 0.16 translation crash that
    blocked native arm64 artifacts.
  • docs/runtime-dependencies.md's distribution table gains an install.sh column, and its
    gate (tests/meta/runtime-deps-doc.sh) gains a rule that fails the build if that column
    ever describes a script or an asset that no longer exists.
  • zigapagos init now scaffolds only the frontmatter a page needs. .author
    and .draft are gone from every template and .date remains only on the blog
    posts and devlog years whose listing layouts render one — .title and
    .layout are the only required fields, and the scaffold no longer models the
    optional ones as obligatory. The sample homepage and the quick start now say
    which fields have defaults.
  • zigapagos migrate --help now states outright that the command converts
    nothing: it reads the Astro project, writes a MIGRATION.md worklist, and
    the port itself is manual — --scaffold being the one exception, and only
    for islands. The README bullet and the site's overview page said or implied
    otherwise.
  • zigapagos dev is zero-config. Run it in a site directory with no
    arguments and it works:
    • --site defaults to public, the same directory a bare zigapagos release
      writes to;
    • the rebuild command defaults to this binary's own release, resolved by
      absolute path rather than by name on PATH, so an npm install and a
      downloaded release tarball both work (it was zig build, which named a
      toolchain a standalone user never installed);
    • the island/SPA source directories to watch are derived from the entries
      release discovers, so a component edit rebuilds without --watch-dir;
    • a missing zigbase is fetched from the pinned release into the cache
      (SHA256-verified) instead of failing with instructions. --no-download
      restores the previous behaviour for offline machines and for CI that pins
      its own binary. zigapagos e2e is unchanged: it still fetches only on
      --download-zigbase, because an unannounced network fetch in CI is a
      surprise rather than a convenience.
  • zigapagos init now points a new site at zigapagos dev rather than at the
    bare command.

Removed

  • The bundled live server is gone, along with its --proxy reverse-proxy
    mode, the serve and server subcommands, and the bare-command entry point
    that started it (issue #56). zigapagos is a standalone executable, and a
    standalone executable has no default action: run bare it now prints its help
    and exits 0, which is what npx zigapagos does too. An argument that names no
    command prints the same menu and exits non-zero.

  • The consumer zig-build API is gone. zigapagos.website(), zigapagos.e2e()
    and zigapagos.dev(), the option types (Options, Island, Spa,
    BuildAsset, E2eOptions, DevOptions) and the whole build/ half that
    served them no longer exist. A site is built by RUNNING the zigapagos
    binary; zig build builds zigapagos itself and nothing else. Nothing in a
    zigapagos project needs a Zig toolchain, a build.zig, a build.zig.zon or a
    .path dependency on this repository any more.

    The replacements, all of which already existed:

    was now
    zigapagos.website(b, .{ .islands = …, .spas = … }) zigapagos release --island=SRC --spa='SRC|BASE'
    zigapagos.e2e(b, opts, .{}) + zig build e2e -- CMD zigapagos e2e --site=DIR -- CMD
    zigapagos.dev(b, opts, .{}) + zig build dev zigapagos dev
    Options.source_maps = true zigapagos release --source-maps
    Options.not_found --spa-not-found=NAME
    Options.build_assets --build-asset=NAME PATH [--install=P | --install-always=P]
  • zigapagos release --spa-chunks= and --spa-slice= are removed. They existed
    to hand release bundles the build graph had already produced; it now builds
    them itself.

  • zigapagos init --from-astro --zigapagos-path is removed with the
    build.zig.zon it filled in. The importer scaffolds a build.sh instead.

Fixed

  • zigapagos release now honours ZIGAPAGOS_HOT_ISLANDS, passing --hot to
    the island bundle driver when it is set. Nothing on the release path read
    the variable zigapagos dev sets, so a dev rebuild produced non-hot island
    bundles and an island hot-swap silently reset every useState instead of
    preserving it.
  • A CLI report no longer overwrites the command's own stderr when both streams are redirected
    to one file (cmd >f 2>&1). explain, doctor, validate and migrate --doctor built
    their buffered stdout writers with Io.File.writer, which writes positionally from an offset
    of its own, so the end-of-command flush landed on top of bytes stderr had already committed —
    silently corrupting the merged output. They now use writerStreaming.
  • Island bundles are minified. The build graph passed --minify to the
    shared runtime, both runtime slicers and every SPA bundle, and to islands
    alone did not. The four islands on this project's own marketing site shrink
    from 4113 to 2094 bytes.
  • The island-sidecar spawn diagnostics no longer send you to a build.zig
    that does not exist.
    All three ENOENT messages ended by pointing at the
    consumer build API's .islands table, which is gone; they now name the flag
    that actually configures each input (--bun, `--isla...
Read more

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 31 Jul 22:26
0052664

Added

  • npm distribution. npx zigapagos now scaffolds, serves and builds a content
    site with no Zig toolchain. Three packages, released together at
    build.zig.zon's version: @zigapagos/cli-<platform> carrying the prebuilt
    binary, @zigapagos/cli (canonical) resolving the right one at run time through
    optionalDependencies, and the unscoped zigapagos as a thin alias so npx zigapagos works. Prebuilt for macOS x64 and Linux x64 — the two targets
    build/release.zig ships. Every other host is refused with the reason rather
    than the bare fact, and arm64 (macOS or Linux) is refused rather than served the
    x64 binary: npm install fails with EBADPLATFORM because the launcher packages
    declare the os/cpu they have binaries for, so an unsupported host cannot end
    up with an install that looks clean and has no binary in it. Everything builds
    from npm i zigapagos alone
    — content, islands, native SPAs and zigapagos dev:
    @zigapagos/cli ships the @z/runtime sources and the Bun SSR sidecar, and
    declares bun, typescript and @zigbase/server as optional dependencies, so
    the tools it shells out to are installed rather than asked for. npm puts
    node_modules/.bin on PATH, and the launcher appends it to the child's, so the
    zigbase locator finds the server with no flag, no global install and nothing
    downloaded. --omit=optional still builds; it loses dev's server and the SPA
    runtime slice. The remaining difference from a Zig build is caching, not
    capability. The published READMEs say so. See npm/README.md.
  • The zigbase dependency is the scoped @zigbase/server, at exactly the
    pinned_version in src/cli/zigbase.zig (currently ZigBase v0.12.0) — the same
    release --download-zigbase fetches, so zigapagos dev runs one zigbase however
    it was installed. npm/check-toolchain.mjs fails the build if those two ever
    disagree.
  • zigapagos doctor [DIR]: audits a BUILT output tree (default public, read-only — never
    builds, never touches site source) for authoring mistakes that are only visible in the final
    emitted HTML. Ships two checks: abs-url-meta (a root-relative Open Graph / Twitter / canonical
    URL — crawlers can't resolve it, so this is an error) and dangling-internal-link (a
    root-relative href/src with no file behind it in the tree, including under --url-prefix
    a warn, since a client-routed SPA route legitimately has no file). Exit code: any error
    finding, or a file doctor could not read, exits non-zero; warn-only findings exit 0 unless
    --strict is passed.
  • zigapagos validate [OPTIONS]: a fast, in-memory subset of zigapagos release's checks (issue
    #45). Parses and analyzes the site — frontmatter/Ziggy schema, SuperMD parse, layout resolution,
    content-side $link.page/asset references, output-URL collisions, template SuperHTML/Scripty
    parse, the : directive lint, and template RENDER errors (a failing Scripty expression, a
    $site.page(...) naming no page) — WITHOUT bundling islands, spawning the Bun sidecar, or
    writing an output tree. It does not cover island SSR, the typed island props check, SPA route
    enumeration/spec checks, asset installation, or CSS minification — those stay release-only, so
    a green validate is a subset guarantee, not a green release. Measured (this repo's
    examples/tsx-site, warm caches): a content-only edit loop goes from zig build's ~2s to
    validate's ~0.02–0.03s — and unlike zig build, validate needs no bun, node_modules,
    build.zig, or consumer build graph, and never writes the output tree.
  • zigapagos explain <route>: resolves one output route to its content source, layout extends
    chain (for a route that is one of a page's alternatives, that alternative's OWN layout, not the
    page's), effective frontmatter (after schema defaults), islands (as declared in the markup, not
    SSR-verified), page-owned assets (referenced vs. pruned), and EMITTED PATHS relative to the
    output directory (issue #47). Runs the same kind of fast in-memory build as validate. Content
    routes only — a memory build never prerenders SPAs, so a client-routed SPA route is not covered;
    the miss message says so.
  • zigapagos languages: lists every code-fence language registered for syntax
    highlighting.
  • zigapagos release --format=json emits build diagnostics as NDJSON on stderr — one
    minified JSON object per line, {"code","severity","file","line","col","message","help"}
    — instead of the historical multi-line prose. The consumer this is for is an unattended
    agent: it can now tell which diagnostic fired without pattern-matching English. Default
    is --format=text and text mode is byte-for-byte unchanged.
  • The diagnostic code is the stability guarantee; message and help explicitly are not.
    src/diag-codes.frozen is the append-only ledger that makes that a gate rather than a
    promise — a code is never renamed and never reused for a different meaning after
    retirement, enforced against the enum on every build.
  • zigapagos explain-code <CODE> prints the long form of any code: what condition produced
    the diagnostic and what to change in the source. zigapagos explain-code with no argument lists
    every registered code with a one-line summary. Every code is required by the compiler to
    have both, so the listing cannot go stale relative to what the build emits.
  • The two : directive lints get one code each rather than a shared one —
    ZP_TEMPLATE_ELSE_DIRECTIVE and ZP_TEMPLATE_BRANCHING_WITHOUT_END_TAG — because they
    are unrelated failures with unrelated fixes and code is what a consumer switches on.
  • docs/diagnostics.md is the consumer contract: the wire schema, what is and is not
    stable, and an explicit inventory of what is not converted and why — including the
    rule that matters most, skip a stderr line that does not parse as JSON rather than
    failing the run
    , since the Bun sidecar and the usage-menu path legitimately write
    prose to the same stream.
  • Islands can now be embedded directly in .smd content, not only in
    layouts: inside a fenced code block whose fence info is =html (SuperMD's
    existing validated raw-HTML escape hatch), use the hyphenated
    <z-island src="…" client:load :props='…'></z-island> spelling — the
    islands pass treats it identically to <island> in a layout (SSR,
    data-z-props, the import map, the runtime script, the tsc props gate,
    and the dev island-usage manifest all apply unchanged). The hyphen is
    required: superhtml's .html-mode validator (used to vet the fence body)
    rejects a non-hyphenated custom element name per the HTML spec, unlike the
    lax .superhtml layout mode where <island> has always worked. See
    docs/islands.md, "Islands in content (.smd)".
  • Opt-in auto_heading_ids site setting (Site/MultilingualSite in zigapagos.ziggy):
    injects a GitHub-compatible slug id into every heading that doesn't already carry an
    explicit $heading.id(...)/$section.id(...), so a same-page #anchor or cross-page
    /page#anchor link written against a doc's existing GitHub rendering keeps working
    without hand-writing an id on every heading. Off by default; an explicit id always wins
    and is never overwritten. See docs/migration/astro-to-zigapagos.md's "Heading anchors:
    auto_heading_ids" section.
  • $site.asset(...).absLink() / $page.asset(...).absLink(): like link(),
    but always returns an absolute URL (host_url + url_path_prefix + asset
    path), and installs the asset the same way link() does. Use it for URLs
    consumed outside the page itself — og:*/twitter:* meta tags, canonical
    links, feeds — since link()'s output is root-relative and scrapers do not
    resolve those (#25).
  • .asset_fingerprint = true in zigapagos.ziggy installs every linked site asset under a
    content-hashed filename (assets/style.css/style.a1b2c3d4.css), and every seam that
    prints a site-asset URL — .link()/.absLink(), the ![](…) content directives, and
    spa.head hrefs — resolves to that name through one shared formatter, so an installed file
    and a link to it cannot drift apart. A changed file is a changed URL, which is what lets a
    deploy put Cache-Control: immutable on the asset tree. Opt-in and release-only;
    static_assets entries, build assets, page assets and the in-memory live server keep verbatim
    names. See docs/assets.md.
  • --allow-missing-pages (zigapagos release and the live server; for a zigapagos dev
    loop set allow_missing_pages in your build.zig, since dev re-runs your rebuild
    command rather than building the site itself — the tolerance is identical either way,
    so a green dev preview and a CI release agree):
    tolerate a $link.page/$link.sibling/$link.sub (content) or
    $site.page(...) (template) reference to a page that doesn't exist YET, instead of
    hard-failing the build. The reference renders as the real, url_prefix-aware href
    the target page will have once it's written (a 404 until then), and the build log gets
    a warning naming the ref and the computed href instead of a fatal error. This is the
    fix for incremental authoring: previously, adding a navigation link before its target
    page existed broke the entire build (one dangling link → zero pages built), which is
    exactly what "site under construction" always looks like.
  • A relative .aliases entry that basenames as 404.html, robots.txt, or sitemap.xml
    now prints a build-time warning showing where it actually resolves. Alias resolution
    itself is unchanged — a relative entry still joins to the page's own output directory,
    exactly as before; this only flags the common mistake of meaning a site-wide override
    (e.g. "/404.html" to replace the SPA fallback) but writing the bare relative form
    instead.
  • A layout route now receives its matched child as a `childr...
Read more

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 28 Jul 01:42
7e3b518

Internal

  • Changelog entries are now recorded as one fragment file per change in changelog.d/,
    assembled into a version section by scripts/assemble-changelog.sh at release, so
    parallel pull requests never conflict on CHANGELOG.md. See changelog.d/README.md.

  • Four byte-identical private copies of escapeRegExp in build-time TypeScript are
    gone, replaced by the right tool for each of the two contexts they were serving.
    The three JavaScript call sites (lint-island-imports.ts, react-alias.ts,
    sidecar/bundle-island.ts) now use the standard RegExp.escape, which is
    specified for exactly the ECMAScript RegExp position they feed. The fourth,
    emit-host-config.ts, emits Apache RewriteRule patterns — PCRE, a different
    dialect — so it gets a purpose-named escapePcre whose contract matches its
    output language and which keeps a deployed .htaccess readable
    (^app/.*$, not ^\x61pp/.*$).

  • Generated Apache config now has a validation net rather than only literal-string
    greps: emit-host-config.test.ts runs each emitted RewriteRule pattern through
    a real Perl-compatible regex engine and asserts it matches the URLs it should and
    rejects the near-misses an unescaped . would have swallowed. buildAllow gained
    the metacharacter-escaping test it never had.

  • Eleven of the fourteen test scripts tests/meta/unrun-scripts.txt inventoried as knowingly
    unrun now run in CI: the non-browser examples/tsx-site/test/*.sh — island SSR and the
    bundle/import-map wiring, SSR↔CSR parity, byte-parity against a raw bun build, depfile
    incrementality, the props-check gate in both directions, migrate --doctor, and the four SPA
    prerender scripts (routing manifest, nginx/zigbase host config, code splitting, baked flag
    defaults, guarded routes, nested layouts) — plus the live-server smoke test.

    They are a step in the existing e2e-dev-loop job rather than a tests/<area>/ shim, and the
    distinction is the whole point. Every one of them runs zig build inside examples/tsx-site,
    i.e. a full consumer build of zigapagos-as-a-dependency, and e2e-dev-loop is the only job
    that already pays for one — its tests/serve/dev.sh step drives that project's own
    zig build dev. A shim would have put them in e2e-rest, which builds the repo and not the
    example, buying a cold ~265s consumer build and making that job the run's critical path.
    Measured against the warm tree the job already has, the eleven cost 49s in CI (29s
    locally) against the 468s the dev.sh step above them takes. serve.sh alone was
    inventoried at 76.1s; behind dev.sh it is ~5s, which is the placement argument in one
    number.

    The list is literal, not a glob, for the opposite reason e2e-rest uses a glob: a new sibling
    in that directory should NOT be adopted onto the PR path automatically — it might be the next
    one that needs a browser or four minutes. Being unnamed there is exactly what makes
    script-coverage.sh stop and ask.

  • tests/meta/script-coverage.sh no longer counts a script as CI-run because a workflow
    comment names it. Its rule (b) was a plain git grep over .github/workflows/, so prose
    saying "these two are deliberately not run here" would have vouched for precisely the scripts
    it was disclaiming — and, since both are also inventoried, would have failed the gate with
    "run by CI but also listed". Rule (b) now applies the same non-comment filter rule (c) already
    had. The two Playwright paths are spelled in full in that comment on purpose: they pin the
    filter, because removing it turns the gate red by name.

    (site/test/build.sh and the two Playwright scripts were the three still inventoried at this
    point; all three were wired up before this release shipped — see the entry below for where
    each ended up and why.)

  • The typescript devDependency moves 5.9.3 → 6.0.3, the final JavaScript-based TypeScript
    line. The compiler API that runtime/scripts/slice-host.ts and
    runtime/sidecar/hot-transform.ts parse with is fully present, so neither needed
    re-platforming, and the runtime suite is unchanged at 617 passing. site/bun.lock and
    examples/tsx-site/bun.lock are regenerated in step: each embeds its own copy of
    @z/runtime's resolved dependencies and bun does not refresh them for a linked package on a
    plain install, so left alone they would have kept the props-check gate running 5.9.3 while the
    runtime was tested on 6.0.3.

  • TypeScript 7.x is capped out via a Dependabot ignore on >=7.0.0. 7.0 is the Go rewrite and
    its npm package no longer ships the JavaScript compiler API — import ts from "typescript"
    resolves to lib/version.cjs and yields only {version, versionMajorMinor}, taking the
    runtime suite to 566 passing / 51 failing. The cap is deliberately a version bound rather than
    a major-block, which is what let 6.x through. It lifts when a 7.x ships a usable programmatic
    API (7.1 at the earliest).

  • The tsconfig.json files were audited against TypeScript 6.0's deprecation list and needed no
    changes: none uses baseUrl, outFile, downlevelIteration, target: es5,
    moduleResolution: node|node10|classic, module: amd|umd|system|none, or an explicitly false
    esModuleInterop / allowSyntheticDefaultImports / alwaysStrict. No source file uses the
    module namespace keyword or import assert syntax. ignoreDeprecations is therefore not
    needed, and the config surface is already clean for whatever 7.x removes.

  • Dependabot no longer groups major version bumps with routine ones. The bun groups for
    runtime/ are restricted to minor and patch, and a new runtime-majors group collects every
    major into its own pull request, so a breaking major can no longer block unrelated updates
    from merging. github-actions deliberately keeps its single group: every uses: is pinned to
    a bare major tag, so majors are the only update it can produce and splitting would reintroduce
    per-action pull-request spam.

  • happy-dom and @happy-dom/global-registrator move to 20.11.0.

  • tests/meta/unrun-scripts.txt is empty. All 36 tracked test scripts are now run by CI;
    the inventory that started at 14 rows and was cut to 3 is at 0. The file stays because the
    gate reading it is the point, not the list.

    site/test/build.sh moved into pages.yml, between Build site and Upload artifact. That
    makes it a deploy gate: a failed assertion fails the build job, the artifact is never
    uploaded, and deploy (which needs: build) never runs, so the previous good deployment
    stays live. It is nearly free there — the workflow has already built site/, so the script's
    own install and build are warm no-ops and the five greps measured 1.7s — against ~120s in any
    ci.yml job, because site/ is a third consumer project with its own .zig-cache that
    nothing else warms. The residual gap is stated rather than glossed: pages.yml triggers on
    push to main and manual dispatch only, so these assertions gate the deploy and not the PR.

    examples/tsx-site/test/{hydrate,spa_slice}.sh moved into a new browser-e2e.yml on a
    nightly schedule: plus workflow_dispatch:. Scheduled rather than PR-gating because each is
    ~125s on top of a ~265s cold consumer build plus a ~150MB browser install, and because what
    they catch — a real-browser hydration or runtime-slicing regression — arrives with a
    runtime/src change or a dependency bump, unattended. Each script gets its own matrix runner
    (fail-fast: false): spa_slice.sh opens by rm -rfing .zig-cache and zig-out, so the
    two cannot share a build, and separate runners make that hazard structurally impossible rather
    than merely avoided.

    One correction to the plan the inventory carried: the install step is
    playwright install --with-deps chrome, not chromium. All ten *_playwright.py helpers
    launch with channel="chrome", which on Linux resolves to /opt/google/chrome/chrome — the
    bundled Chromium build satisfies none of them, and the run would have died at browser launch
    after paying for the whole consumer build.

  • tests/meta/script-coverage.sh gained a self-test, tests/meta/script-coverage.test.sh,
    in the shape of scripts/check-allocator-contracts.test.sh: seven cases against throwaway git
    repos in $TMPDIR. That gate has shipped three defects already — a self-vouching inventory, a
    pipefail + grep -q SIGPIPE race, and a comment filter applied to one rule but not the
    other — and every one made it pass when it should have failed.

    Two of the cases exist because emptying the inventory silently removed the only thing pinning
    the comment filter. That filter is what stops a script a workflow merely mentions in prose
    from counting as run, and it was pinned by accident: while the two Playwright scripts were
    inventoried, dropping the filter made the gate see them as both CI-run and listed and fail by
    name. With the inventory empty, removing the filter now breaks nothing in the tree —
    confirmed by deleting the filter line and watching the real gate still pass 36/36. Case 5
    makes the pin deliberate; case 6 is its guard rail, that comment-awareness has not become
    "never believe a workflow".

  • contract/test/drift.sh — the test that proves the cross-tier codegen gate is not vacuous —
    was itself vacuous, and now runs in CI. Its Case B asserted only that tsc --noEmit exited
    non-zero, which a compiler that fails to launch also does: contract/ has no
    node_modules, so bun x tsc resolved tsc off PATH, hit mise's shim, and died with
    No version is set for shim: tsc — exit 1, nothing type-checked, PASS Case B printed. Cases
    A and B now assert on the diagnostic text (the experimentsvariants hunk in api-check's
    staged diff; both assignability directions of the _assert.ts tripwire, and no diagnostic
    from anywhere else), and a new Case D ...

Read more