Execution checklist
Wave 0 — Repository foundation (sequential)
| ID | Task | Deliverables | Verification | Done |
|---|---|---|---|---|
| T0.1 | Initialize repository and toolchain pins | .gitignore, .node-version (26.8.1), rust-toolchain.toml (1.98.1, rustfmt, clippy), .editorconfig | git status clean after commit; node --version matches | [o] |
| T0.2 | Development rules | AGENTS.md, AGENTS.ko.md: English and Korean document pairs, contracts in docs/spec/, status in docs/features.md, procedures in docs/operations/, changes in CHANGELOG.md, simplest implementation, idempotent commands, code/test separation, red-green defect procedure, writing style (direct action names, subject and object stated, one-sentence causes, no figurative wording) | make docs-check | [o] |
| T0.3 | Top-level documents | README.md(.ko), CHANGELOG.md(.ko), docs/index.md(.ko), docs/features.md(.ko) with every feature row not-started/pending/not-deployed and evidence links | make docs-check | [o] |
| T0.4 | Workspace and Makefile | package.json (workspaces packages/*, scripts lint, test, docs:check), Makefile with targets help check lint build-ts build-go build-rust build-php test-ts test-go test-rust test-php conformance parity test-browser ext test-ext schema-check docs-check docs docs-verify-idempotent bench bench-ts bench-php bench-go bench-rust clean; targets whose package does not exist yet print a not implemented line and exit 1 | make help | [o] |
| T0.5 | Document checker | scripts/check-documents.mjs: required English/Korean pairs, relative links resolve, code blocks identical across languages, feature status fields valid | node scripts/check-documents.mjs | [o] |
| T0.6 | This checklist | docs/plans/execution-checklist.md(.ko) | make docs-check | [o] |
| T0.7 | Operations documents | docs/operations/development.md(.ko): toolchain install, make targets, commit procedure; docs/operations/documentation.md(.ko): document rules and checker | make docs-check | [o] |
Wave 1 — Specification (parallel)
| ID | Task | Deliverables | Content | Verification | Done |
|---|---|---|---|---|---|
| T1.1 | Lexical specification | docs/spec/lexical.md(.ko) | UTF-8 and BOM handling; line terminators; tag start rule ({ followed by horizontal whitespace and a sigil, or {: followed by an identifier and an assignment operator); text otherwise; escape \{ consumed only where a tag would start; comment {* *}; tag end at the first close delimiter outside a string literal at expression depth zero; standalone line removal rule for non-echo tags; examples for JavaScript and CSS braces that stay text | make docs-check, node tests/runner/delimiter-matrix.mjs | [o] |
| T1.2 | Tag grammar | docs/spec/grammar.md(.ko) | EBNF for echo, loop, if, elseif, else, close, include, block, if-block, assignment; block nesting and closing rules; {:} inside a loop as the empty branch; error conditions per rule; bare and quoted path token definition; block tag token order (optional id, optional path, scope items name or name:postfix) | make docs-check | [o] |
| T1.3 | Expression specification | docs/spec/expressions.md(.ko) | Token table; EBNF; precedence table (pipe, ternary and elvis, coalesce with trailing shorthand, or, and, equality, comparison and in, additive, multiplicative, unary, postfix); path access and lookup rule; loop meta access name.index_ key_ value_ last_ first_ size_ parsed as a dedicated node; + rule (numeric addition unless either side is a string, then concatenation); other arithmetic numeric only; equality, strict equality, ordering, truthiness; list and map literals with => and spread | make docs-check | [o] |
| T1.4 | Data model | docs/spec/data-model.md(.ko) | Value types; number as IEEE 754 double with safe integer range; string to number conversion grammar; number to string rule identical to ECMAScript Number::toString with per-language construction notes; boolean and null output; ordered map; host binding tables for JSON, JavaScript, PHP, Go, Rust; rejection codes | make docs-check | [o] |
| T1.5 | Functions | docs/spec/functions.md(.ko) | Signature, arity, semantics and error cases for every built-in function; pipe desugaring; number decimal rounding rule; json escaping rule; date token table and time zone rule; now from environment; host registration contract; safe-value rules for raw and json | make docs-check | [o] |
| T1.6 | Runtime | docs/spec/runtime.md(.ko) | Engine API in four languages; render call shape (template name or AST, assign data, define map, environment); flat per-file local scope; loop variable binding and restoration; include with shared scope; template definition with isolated context (root assign, definition data, scope arguments); definition entries (string path, template with optional data, or html); layout target selection; redefinition rule; loader interface and name resolution; strict and lenient modes; limits (iterations, depth, output size, expression depth); output escaping; HTML embedding of assign data for the browser | make docs-check | [o] |
| T1.7 | AST schema | docs/spec/ast.md(.ko), schema/ast.schema.json, schema/README.md(.ko) | Node list with fields and span byte offsets; desugaring of pipes, compound assignment and increment; serialization rules (number literal as JSON number, string literal decoded); JSON Schema draft-07 covering every node | node scripts/check-schema.mjs (schema self-validation) | [o] |
| T1.8 | Errors | docs/spec/errors.md(.ko) | Error object fields; complete code list by stage (lex, parse, load, data, runtime); which fields conformance compares; mode behaviour per code | make docs-check | [o] |
| T1.9 | Conformance | docs/spec/conformance.md(.ko) | Fixture directory layout and file roles; CLI contract (parse FILE, render FILE --data --define --env --root, exit codes, stdout/stderr); comparison rules (AST structural, HTML byte-exact, error fields); runner options; expression fixture format (tokens, AST, values) | make docs-check | [o] |
| T1.10 | Examples | docs/spec/examples.md(.ko) | One complete page: layout, header partial, footer definition with scope arguments, list with loop meta, card definition, assign data file, define file, expected output, AST excerpt | make docs-check | [o] |
| T1.11 | Specification review (sequential) | Edits across docs/spec/* | Term consistency; every error code used in text exists in errors.md; every function in expressions.md/examples.md exists in functions.md; every rule has an ID; both languages carry the same information | make docs-check; review record in CHANGELOG.md | [o] |
Wave 2 — Conformance assets (parallel)
| ID | Task | Deliverables | Verification | Done |
|---|---|---|---|---|
| T2.1 | Runner drivers | tests/runner/drivers.mjs: per-language command, build command, working directory, binary path | node tests/runner/conformance.mjs --list | [o] |
| T2.2 | Conformance runner | tests/runner/conformance.mjs: case enumeration, per-language invocation with timeout, AST structural diff, HTML byte comparison, error field comparison, options --langs, --case, --list, --update, table output, non-zero exit on failure | node tests/runner/conformance.mjs --list | [o] |
| T2.3 | Parity runner | tests/runner/parity.mjs: language-to-language output comparison without expected files, client/server axis report | node tests/runner/parity.mjs --help | [o] |
| T2.4 | Schema checker | scripts/check-schema.mjs: validates schema/ast.schema.json with ajv and every tests/cases/**/expected.ast.json against it | node scripts/check-schema.mjs | [o] |
| T2.5 | Fixtures: text | tests/cases/text/: plain text, braces that stay text ({ debug: true }, {}, ${x}, {a:1}, newline after {), escape at tag position, escape elsewhere, comment removal, multi-line comment, BOM removal, CRLF input. AST by hand for 4 cases | node scripts/check-schema.mjs | [o] |
| T2.6 | Fixtures: echo | tests/cases/echo/: path, nested path, numeric index, missing key, HTML escaping of five characters, raw, number formatting boundaries, boolean and null output, list output error. AST by hand for 3 cases | node scripts/check-schema.mjs | [o] |
| T2.7 | Fixtures: if | tests/cases/if/: basic, elseif chain, else, nested, truthiness table ("0", empty list, empty map, null), else outside block error, duplicate else error | node scripts/check-schema.mjs | [o] |
| T2.8 | Fixtures: loop | tests/cases/loop/: list, map order, loop meta fields, nested loops with two metas, empty branch, null iterable, loop variable restoration after loop, elseif inside loop error, unknown loop meta error | node scripts/check-schema.mjs | [o] |
| T2.9 | Fixtures: include and block | tests/cases/include/, tests/cases/block/: include sharing locals, include cycle error, block from registry, block with path and registration, block with scope arguments (name and name:value), block isolation of locals, html registry entry, undefined block error, redefinition error, if-block true and false, bare path with ../, path outside root error | node scripts/check-schema.mjs | [o] |
| T2.10 | Fixtures: expressions | tests/cases/expr/: precedence, + with numbers, + with a string on either side, + with list error, division by zero error, modulo, ternary, elvis, trailing ??, in for list, map and string, equality table, strict equality, ordering error for mixed types, list and map literals, spread, pipe chain, call arity error | node scripts/check-schema.mjs | [o] |
| T2.11 | Fixtures: functions | tests/cases/functions/: one case per built-in function including boundary inputs; number(2.675, 2), number(1.005, 2), json with <, & and U+2028, url reserved characters, date tokens and offsets, now from environment, unknown function error | node scripts/check-schema.mjs | [o] |
| T2.12 | Fixtures: data model | tests/cases/data/: map key order with integer-like keys, 0.1+0.2, 1e21, 1e-7, -0, safe integer boundary rejection, astral character length and ordering, code point comparison, invalid UTF-8 rejection | node scripts/check-schema.mjs | [o] |
| T2.13 | Fixtures: whitespace | tests/cases/whitespace/: standalone tag lines, multiple closes on one line, echo not standalone, indentation inside loops, comment-only lines, inline tags keep the line | node scripts/check-schema.mjs | [o] |
| T2.14 | Fixtures: errors | tests/cases/errors/: unterminated tag, unterminated string, unterminated comment, unexpected token, unexpected close, unclosed block, reserved name assignment, invalid escape, invalid number; each with expected.error.json | node scripts/check-schema.mjs | [o] |
| T2.15 | Expression fixtures | tests/fixtures/expr/cases.json (30 cases: tokens, AST, evaluation values) and tests/fixtures/expr/README.md(.ko) | node scripts/check-schema.mjs | [o] |
| T2.16 | Rule coverage checker | scripts/check-rules.mjs: every case.json rule exists in docs/spec, report of uncovered rules; Makefile target rules-check in check | node scripts/check-rules.mjs | [o] |
| T2.17 | Fixtures: wrapped tags | tests/cases/wrapper/: quoted, comment and HTML-comment wrappers, {{ without wrapper stays text, "{= x}" keeps quotes, missing wrapper closer error, wrapped tag on a standalone line | node scripts/check-schema.mjs | [o] |
| T2.18 | Fixtures: delimiters | tests/cases/delimiters/: options.json delimiters, file directive, directive overriding the option, close delimiter used by the grammar ([= a[0]]), directive not first error, invalid delimiter character error, escape with a custom open delimiter | node scripts/check-schema.mjs | [o] |
Wave 3 — TypeScript implementation
| ID | Task | Deliverables | Tests | Verification | Depends on | Done |
|---|---|---|---|---|---|---|
| T3.1 | Package scaffold | package.json (build with tsup: esm, cjs, dts, neutral platform; exports ., ./render, ./node), tsconfig.json (lib: ["ES2020"], types: []), vitest.config.ts, root eslint.config.mjs | tests/smoke.test.ts | npm run build -w @polyspec/template | — | [o] |
T3.2 parallel | Value model | src/value/value.ts (types, truthiness, equality, ordering), src/value/number.ts (to-number, ECMAScript to-string), src/value/bind.ts (host input conversion) | tests/value/*.test.ts | npm test -w @polyspec/template | T3.1 | [o] |
T3.3 parallel | Errors | src/errors.ts (error class, codes, position) | tests/errors.test.ts | same | T3.1 | [o] |
T3.4 parallel | Escape and output | src/escape.ts, src/output.ts (builder with size limit, safe values) | tests/escape.test.ts | same | T3.1 | [o] |
| T3.5 | Template lexer | src/lexer/template.ts (text, tag start rule, escape, comment, tag body extraction with string awareness, positions) | tests/lexer/template.test.ts | same | T3.3 | [o] |
T3.6 parallel | Expression lexer and parser | src/expr/lexer.ts, src/expr/parser.ts, src/expr/ast.ts | tests/expr/lexer.test.ts, tests/expr/parser.test.ts, tests/expr/fixtures.test.ts (loads tests/fixtures/expr/cases.json) | same | T3.3 | [o] |
| T3.7 | Template parser | src/parser/parser.ts (tags, block stack, else and empty branches), src/parser/standalone.ts (line removal), src/parser/block-tag.ts (id, path, scope items), src/ast.ts (node types, JSON serialization) | tests/parser/*.test.ts, tests/ast-schema.test.ts (every parsed fixture validates against schema/ast.schema.json) | same | T3.5, T3.6 | [o] |
| T3.8 | Built-in functions | src/functions/index.ts, one file per group (string.ts, collection.ts, number.ts, encoding.ts, date.ts) | tests/functions/*.test.ts | same | T3.2, T3.4 | [o] |
T3.9 parallel | Loaders | src/loader.ts (interface, MapLoader, name resolution, root check), src/node/loader.ts (FsLoader) | tests/loader.test.ts | same | T3.3 | [o] |
| T3.10 | Renderer | src/render/engine.ts (engine, template definition registration, caching by name and version), src/render/context.ts (frames, loop metas, template definition registry, limits, mode), src/render/statements.ts, src/render/expressions.ts | tests/render/*.test.ts | same | T3.7, T3.8, T3.9 | [o] |
| T3.11 | CLI | bin/template.mjs implementing the CLI contract | tests/cli.test.ts | node packages/template-ts/bin/template.mjs parse tests/cases/text/plain/input.tpl | T3.10 | [o] |
| T3.12 | Conformance in-process | tests/conformance.test.ts runs every case in tests/cases | same | npm test -w @polyspec/template; node tests/runner/conformance.mjs --langs ts | T3.11 | [o] |
| T3.13 | Generate and review remaining ASTs | expected.ast.json for fixtures without one, produced by node tests/runner/conformance.mjs --langs ts --update ast, reviewed case by case | — | node scripts/check-schema.mjs | T3.12 | [o] |
| T3.14 | Browser build check | tests/browser/index.html, tests/browser/render.spec.ts, playwright.config.ts; page loads dist/index.mjs and renders ten cases | Playwright | make test-browser | T3.11 | [o] |
| T3.15 | Fixture expansion | Cases added for every defect found in T3.12–T3.14 | — | node tests/runner/conformance.mjs --langs ts | T3.12 | [o] |
| T3.16 | Package documentation | packages/template-ts/README.md(.ko): API, CLI, browser usage | — | make docs-check | T3.11 | [o] |
Wave 4 — Go, Rust and PHP implementations (three parallel tracks)
Track G — Go (packages/template-go, module github.com/polyspec/template/packages/template-go)
| ID | Task | Deliverables | Tests | Verification | Done |
|---|---|---|---|---|---|
| T4.G.1 | Module scaffold | go.mod (go 1.27.1, no dependencies), README.md(.ko) | — | go vet ./... | [o] |
| T4.G.2 | Value model | template/value/value.go, ordered.go (insertion-ordered map), number.go (to-string via shortest round-trip digits), bind.go (any, JSON decoder preserving order, structs by field order) | template/value/*_test.go (external test package) | go test ./template/value/... | [o] |
| T4.G.3 | Errors | template/errors.go | template/errors_test.go | go test ./template/... | [o] |
| T4.G.4 | Template lexer | template/lexer/lexer.go | template/lexer/lexer_test.go | same | [o] |
| T4.G.5 | Expression lexer and parser | template/expr/lexer.go, parser.go, ast.go | template/expr/*_test.go, fixture test loading tests/fixtures/expr/cases.json | same | [o] |
| T4.G.6 | Template parser and AST | template/parser/parser.go, standalone.go, blocktag.go, template/ast/ast.go with JSON marshalling | template/parser/*_test.go, schema validation test | same | [o] |
| T4.G.7 | Functions | template/functions/*.go one file per group | template/functions/*_test.go | same | [o] |
| T4.G.8 | Loader | template/loader.go (Loader, MapLoader, FSLoader over fs.FS) | template/loader_test.go | same | [o] |
| T4.G.9 | Renderer | template/render/engine.go, context.go, statements.go, expressions.go | template/render/*_test.go | same | [o] |
| T4.G.10 | CLI | cmd/template/main.go | cmd/template/main_test.go | go build -o template ./cmd/template | [o] |
| T4.G.11 | Conformance | template/conformance_test.go over tests/cases | same | make test-go; node tests/runner/conformance.mjs --langs ts,go | [o] |
Track R — Rust (packages/template-rust, crate polyspec-template)
| ID | Task | Deliverables | Tests | Verification | Done |
|---|---|---|---|---|---|
| T4.R.1 | Crate scaffold | Cargo.toml (edition 2024, serde, serde_json with preserve_order), Cargo.lock, README.md(.ko), #![deny(missing_docs)] | — | cargo build --locked | [o] |
| T4.R.2 | Value model | src/value/mod.rs, number.rs, bind.rs | tests/value.rs | cargo test --locked | [o] |
| T4.R.3 | Errors | src/error.rs | tests/error.rs | same | [o] |
| T4.R.4 | Template lexer | src/lexer.rs | tests/lexer.rs | same | [o] |
| T4.R.5 | Expression lexer and parser | src/expr/lexer.rs, parser.rs, ast.rs | tests/expr.rs with fixture loading | same | [o] |
| T4.R.6 | Template parser and AST | src/parser/mod.rs, standalone.rs, block_tag.rs, src/ast.rs with serde | tests/parser.rs, schema validation test | same | [o] |
| T4.R.7 | Functions | src/functions/*.rs | tests/functions.rs | same | [o] |
| T4.R.8 | Loader | src/loader.rs | tests/loader.rs | same | [o] |
| T4.R.9 | Renderer | src/render/mod.rs, context.rs, statements.rs, expressions.rs | tests/render.rs | same | [o] |
| T4.R.10 | CLI | src/bin/template.rs | tests/cli.rs | cargo build --locked --release --bin template | [o] |
| T4.R.11 | Conformance | tests/conformance.rs | same | make test-rust; node tests/runner/conformance.mjs --langs ts,go,rust | [o] |
Track P — PHP (packages/template-php, composer polyspec/template)
| ID | Task | Deliverables | Tests | Verification | Done |
|---|---|---|---|---|---|
| T4.P.1 | Package scaffold | composer.json (php ^8.2, PSR-4 Polyspec\Template\, phpunit), phpunit.xml, pint.json, README.md(.ko) | — | composer install; vendor/bin/pint --test | [o] |
| T4.P.2 | Value model | src/Value/Value.php, Number.php, Bind.php (array_is_list, key string restoration, safe integer check, UTF-8 check) | tests/Value/*Test.php | vendor/bin/phpunit | [o] |
| T4.P.3 | Errors | src/TemplateError.php, src/ErrorCode.php | tests/ErrorTest.php | same | [o] |
| T4.P.4 | Template lexer | src/Lexer/TemplateLexer.php | tests/Lexer/TemplateLexerTest.php | same | [o] |
| T4.P.5 | Expression lexer and parser | src/Expr/Lexer.php, Parser.php, Node.php | tests/Expr/*Test.php with fixture loading | same | [o] |
| T4.P.6 | Template parser and AST | src/Parser/Parser.php, Standalone.php, BlockTag.php, src/Ast/*.php with toArray() | tests/Parser/*Test.php, schema validation test | same | [o] |
| T4.P.7 | Functions | src/Functions/*.php | tests/Functions/*Test.php | same | [o] |
| T4.P.8 | Loader | src/Loader/LoaderInterface.php, ArrayLoader.php, FilesystemLoader.php | tests/Loader/*Test.php | same | [o] |
| T4.P.9 | Renderer | src/Render/Engine.php, Context.php, Statements.php, Expressions.php | tests/Render/*Test.php | same | [o] |
| T4.P.10 | CLI | bin/template.php | tests/CliTest.php | php bin/template.php parse ... | [o] |
| T4.P.11 | Conformance | tests/ConformanceTest.php | same | make test-php; node tests/runner/conformance.mjs --langs ts,go,rust,php | [o] |
Cross-language gate (sequential after the three tracks)
| ID | Task | Verification | Done |
|---|---|---|---|
| T4.X.1 | Parity run with zero divergence | node tests/runner/parity.mjs | [o] |
| T4.X.2 | Full gate | make check (docs-check, lint, four unit suites, conformance) | [o] |
| T4.X.3 | Status update | docs/features.md(.ko) rows for template-go, template-rust, template-php, conformance with evidence links; CHANGELOG.md(.ko) entry | make docs-check |
Wave 5 — PHP extension (sequential)
| ID | Task | Deliverables | Verification | Done |
|---|---|---|---|---|
| T5.1 | Toolchain check | Record in docs/operations/development.md(.ko) whether the Rust PHP binding supports the installed PHP version (8.5); if not, mark T5.2–T5.6 blocked with the missing version | php-config --version | [o] |
| T5.2 | Extension crate | packages/template-php-ext/Cargo.toml (cdylib, depends on the Rust crate), src/lib.rs (Polyspec\Template\Native\Engine with parse, render, register), src/convert.rs (zval to value and back) | cargo build --locked --release | [o] |
| T5.3 | Stubs and package wiring | stubs/polyspec_template.stub.php; packages/template-php documents how to select the native engine explicitly | make docs-check | [o] |
| T5.4 | CLI and tests | bin/template-ext.php; tests/ExtConformanceTest.php run with php -d extension=... | make test-ext | [o] |
| T5.5 | Makefile targets | ext, test-ext; runner driver php-ext | node tests/runner/conformance.mjs --langs php-ext | [o] |
| T5.6 | Status update | docs/features.md(.ko) row template-php-ext; CHANGELOG.md(.ko) | make docs-check | [o] |
Wave 6 — Benchmarks, documentation site, status (parallel)
| ID | Task | Deliverables | Verification | Done |
|---|---|---|---|---|
| T6.1 | Performance measurements | Per-language AST and generated measurements; output equality check before timing | make showcase | [o] |
| T6.2 | Documentation coverage checker | scripts/check-doc-coverage.mjs (exported symbols documented in four packages), run by make doc-coverage | make doc-coverage | [o] |
| T6.3 | Documentation site | docs/.vitepress/config.mts, gated static GitHub Pages job, generated API docs excluded from git | make docs-static-check; make docs-verify-idempotent | [o] |
| T6.4 | CI workflow | .github/workflows/ci.yml invoking existing Makefile targets and deploying Pages only after every required job passes | workflow file lint | [o] |
| T6.5 | Publication procedure | docs/operations/publication.md(.ko): local immutable publication of Go, npm and composer packages | make docs-check | [o] |
| T6.7 | Executable example site | examples/site/ scenarios and static page; tools/showcase/build.mjs; AST/generated-program parity, repeatability and same-condition mode benchmark JSON artifacts | make showcase; make showcase-check | [o] |
| T6.6 | Final status | docs/features.md(.ko) with test revisions; CHANGELOG.md(.ko) | make check | [o] |
Wave 7 — Generated compiler completion and package installation
| ID | Task | Verification | Done |
|---|---|---|---|
| T7.1 | Define one compiler/runtime manifest, generated declarations, ownership and support-level diagrams; reject structural drift in TypeScript, Go, Rust and PHP | make compiler-interface-check; make runtime-interface-check | [o] |
| T7.2 | Replace generated callbacks and showcase-only generation with one compiler pipeline and four host backends; remove compatibility options and fallback paths | package tests; compiler mutation tests | [o] |
| T7.3 | Support every specified node, expression, built-in and host function in generated execution | generated compiler tests | [o] |
| T7.4 | Run every canonical case (216 when the task was done, 244 on 2026-10-02) through AST and generated execution in TypeScript, Go, Rust and PHP | make conformance-all-modes | [o] |
| T7.5 | Verify artifact refresh at the build boundary: dev always rebuilds, true rebuilds on digest change, false reads no source | artifact lifecycle tests | [o] |
| T7.6 | Install npm, Go, Cargo and Composer artifacts in isolated temporary projects and render the same assign/define page | make install-check | [o] |
| T7.7 | Generate parser-backed showcase highlighting, bounded artifact/source views and the React island example from production artifacts | make showcase-check | [o] |
| T7.8 | Rerun equal-output AST/generated performance measurements with production artifacts | make bench; make showcase | [o] |
| T7.9 | Synchronize specifications, feature status, changelog, generated Mermaid, static documentation and completion evidence | make docs-check; make docs-verify-idempotent | [o] |
| T7.10 | Enforce the commercial release test pyramid: lexer/parser/IR/runtime units, generated-source compile tests, full mode matrix, positioned errors and recovery regressions, mutation rejection, isolated package installation and browser output, and equal-output performance regressions | make release-test-matrix | [o] |
| T7.11 | Pass the clean-checkout release gate and deploy the static site | make release-check; CI and Pages success | [o] |
Wave 8 — Assigned object and class function execution
| ID | Task | Verification | Done |
|---|---|---|---|
| T8.1 | Add the common object value boundary without copying host classes | four-language value and binding tests | [o] |
| T8.2 | Resolve declared public fields and instance methods from assigned native instances | member lookup tests | [o] |
| T8.3 | Resolve declared logical class functions through the same registry contract | class-call tests | [o] |
| T8.4 | Execute member and class calls in AST and generated programs | AST/generated parity tests | [o] |
| T8.5 | Verify output, arity, type, unknown-member and thrown-error behavior across all four languages | make generated-native-check; full native call matrix | [o] |
| T8.5-1 | Fail json of a native object in every runtime: the PHP runtime raised E_INTERNAL (it called entries() on the object), while TypeScript, Go, Rust and the PHP extension wrote null, a stand-in that presents an opaque value (VAL-19) as null. FUN-26 now states that a value that is or holds a native object at any depth has no JSON text and fails with E_RUNTIME_TYPE at the call; the five runtimes and, through their function registries, the generated programs raise it. tests/fixtures/native-object/json.tpl renders json(['k' => [order]]), and host-values.json expects E_RUNTIME_TYPE in the AST tests of every package, the extension and scripts/check-generated-native-calls.mjs. Red: the PHP AST test failed with E_INTERNAL. Green: the host-value tests of TypeScript, Go, Rust, PHP and the extension pass | node scripts/run-tests.mjs vitest --cwd packages/template-ts -- tests/render/host-values.test.ts; go test . -run HostValues in packages/template-go; cargo test --test host_values; vendor/bin/phpunit --filter HostValues in packages/template-php and, on the built extension, in packages/template-php-ext; CI runs make generated-native-check | [o] |
| T8.5-2 | Regenerate the showcase artifacts after T8.5-1: the release job of CI failed showcase-check with examples/site/index.html is stale; run make showcase. The AST artifact manifests of examples/site/scenarios/*/compiled/ast record compilerDigest, a digest of the template-ts build, which T8.5-1 changed; install-check (through typed-generator and showcase-compile --refresh true) rewrote the five manifests in the CI checkout before showcase-site-check, whose page shows them. make showcase-compile and tools/showcase/build-site.mjs write the five manifests and the page; only the five digests change, and the benchmark results are not measured again | node tools/showcase/compile.mjs --refresh true leaves the tree unchanged, node tools/showcase/build-site.mjs --check; CI runs make showcase-check | [o] |
| T8.6 | Add object-call declarations and generated Mermaid interface diagrams | interface check | [o] |
| T8.7 | Add a showcase page with native instance assign, field access, member call and class call output | make showcase-check; static HTML checks | [o] |
| T8.8 | Synchronize specifications, feature status, changelog, Pages artifacts and completion evidence | make check; make docs-verify-idempotent; make docs-static-check | [o] |
Wave 9 — Template comments in the AST
| ID | Task | Verification | Done |
|---|---|---|---|
| T9.1 | List every comment in Template.comments with its value and span (AST-9) in the TypeScript, Go, Rust and PHP parsers and the PHP extension; add the schema definition and the shared cases text/comment-list and delimiters/comment-custom | make conformance; make check | [o] |
Wave 10 — Install check workspace removal
| ID | Task | Verification | Done |
|---|---|---|---|
| T10.1 | Remove the install check workspace, including the Go module cache created with -modcacherw, and fail the check with the path of a workspace that cannot be removed; scripts/check-install-workspace.mjs runs in make install-check | make install-check; make check | [o] |
Wave 11 — Editor language service, LSP server and CodeMirror adapter
| ID | Task | Verification | Done |
|---|---|---|---|
| T11.1 | Return the tags and expression tokens accepted before the first error with analyzePrefix() of @polyspec/template (EDT-6) | make test-ts | [o] |
| T11.2 | Rename @polyspec/template-format to @polyspec/template-language; add openDocument() with diagnostics, tokens, tag ranges, folding ranges, highlights, matching tags, format and lineIndentation (EDT-4 to EDT-13); move the folding and matching tag rules out of the VS Code extension; add the editor fixtures (EDT-17) | make test-language | [o] |
| T11.3 | Add the LSP server @polyspec/template-lsp with the command template-lsp (EDT-14) and test it through its protocol against the editor fixtures | make test-lsp | [o] |
| T11.4 | Make the VS Code extension a client of the bundled LSP server; keep the TextMate grammar, tag backgrounds and the matching tag command (EDT-15) | make test-vscode; make test-vscode-integration | [o] |
| T11.5 | Add the CodeMirror 6 adapter @polyspec/template-codemirror (EDT-16), test it with EditorState against the editor fixtures and in a browser | make test-codemirror | [o] |
| T11.6 | Reject an adapter that declares or imports @polyspec/template (EDT-2) or a language service that imports an editor, Node.js or DOM module (EDT-3); record rule evidence, feature status, documents and dependency policy | make check | [o] |
Wave 12 — Ternary with an identifier before the colon
| ID | Task | Verification | Done |
|---|---|---|---|
| T12.1 | Add a conformance case with c ? a : b, c ? a : b.x and nested ternaries whose then-branch ends with an identifier; check it fails in every implementation that has the defect; parse a class call only when the identifier is followed by :: and fix every implementation | make check | [o] |
Wave 13 — Four task states
| ID | Task | Verification | Done |
|---|---|---|---|
| T13.1 | Accept only the four task states in scripts/check-documents.mjs, with the cause and the retry condition of a bypassed task; check it fails on the rows marked with the letter x; write every done task with the done state of AGENTS; state the checklist rules in AGENTS | make docs-check | [o] |
| T13.1-1 | Make the checklist hold only tasks and its markers only as task states: scripts/check-documents.mjs fails, with the file, the line and the column and with no exception, on a line of docs/plans/execution-checklist.md other than its title, the translation link, a heading or a task table row, and on a state marker of AGENTS or a task list marker (the letter x or X in square brackets) anywhere other than the start of the last cell of a task row; it reads derived sub-items such as T14.2-1 as task rows. Move the prose of the checklist (the introduction, the dependency overview, the dependencies, causes and exit criteria of each wave, the parallelism summary and the definition of done with its evidence) into docs/plans/execution-plan.md in both languages; move its rules (the task ID format and the content of a task row) into AGENTS, which defines the states, and remove its legend; write the states in words in the texts of W13, T13.1, T17.1 and T17.1-1. Cause: the legend and four texts of the checklist wrote 14 state markers and 2 task list markers in each language, and a tool that counted markers reported tasks in progress that did not exist; 93 lines of each language were not tasks; the checker validated only the last cell of rows whose ID had no derived part, so it checked neither where a marker stands nor the states of the 9 derived sub-items. Red: in tests/scripts/check-documents.test.mjs, a fixture checklist with markers in a legend, prose, the text of a task, inline code and the cause of a bypassed task, a fixture with a paragraph under a heading and a derived sub-item with the state done all passed the checker with 7 document pairs passed (3 of the 5 cases failed with 0 !== 1). Green: the 5 cases pass: the first fixture fails with its 16 marker locations (such as docs/plans/execution-checklist.md:10:27) and its 4 line locations, the paragraph fails at line 7, the derived sub-item fails with invalid task state for T1.2-1: done, the checklist of a title, a link, a heading and a task table passes, and the checker passes this repository | node scripts/run-tests.mjs node -- tests/scripts/check-documents.test.mjs | [o] |
| T13.1-2 | Fail scripts/check-documents.mjs on the translation link line of the checklist, which then holds only headings and task tables, and remove that line from both languages. Cause: T13.1-1 accepted a link on the third line of the checklist as the translation link, although AGENTS states that every document has a .ko.md file. Red: the new case a translation link line fails with its location of tests/scripts/check-documents.test.mjs failed with 0 !== 1, because the checker passed a checklist with [한국어](/ko/plans/execution-checklist). on its third line; the changed checker reported docs/plans/execution-checklist.md:3:1 and the same place in the Korean file before the line was removed. Green: the 6 cases pass, and the checker passes this repository | node scripts/run-tests.mjs node -- tests/scripts/check-documents.test.mjs | [o] |
| T13.1-3 | Link the Korean execution plan to the checklist by its Korean site route /ko/plans/execution-checklist, and run make docs-static-check with make docs-check before a commit that changes a document under docs/. Cause: make check failed in docs-static-check with ko/plans/execution-plan.html: link points to a source-only or legacy route: ./execution-checklist.ko, because T13.1-1 linked execution-checklist.ko.md relatively; the commit ran only make docs-check and make docs, which build the site without checking its links. AGENTS and docs/operations/documentation.md state the rule and that a Korean document links a Korean page by its route /ko/<path>; T13.1-4 records the selection of owner checks from changed paths as the next step. Red: make docs-static-check failed with the same error on the tree of T13.1-2. Green: it prints documentation static site: 57 pages, 28 Korean locale pages passed, and make docs-check passes | make docs-check docs-static-check | [o] |
| T13.1-4 | Select the owner checks of a commit from the paths it changes: scripts/owner-checks.json declares, for globs of paths, the make targets and node test files that own them, and make owner-check runs exactly the owners of the changed paths (the uncommitted changes, PATHS or the paths since BASE), never the full suite; before any owner runs it fails on a tracked path that no rule owns, a glob without a path, a target that the Makefile lacks and a full-suite target, naming each. A change under tests/cases runs make conformance-cases CASES=..., the AST and generated conformance of those cases only. Give every tracked path an owner: tests/runner/function-inventory.mjs, which no target ran, runs in the new target function-inventory-check of make check; .node-version, rust-toolchain.toml, .gitignore and .editorconfig get tests of what they declare; .editorconfig declares the byte-exact test files and the generated showcase files, and the Go examples of six documents are indented with spaces. AGENTS refers to make owner-check instead of the remembered rule of T13.1-3. Red: with scripts/owner-check.mjs and scripts/conformance-cases.mjs absent, the 6 cases of tests/scripts/owner-check.test.mjs failed and tests/scripts/conformance-cases.test.mjs failed to load; tests/scripts/editorconfig.test.mjs listed 338 lines and files that broke .editorconfig (tab indentation in six documents and the generated site, space indentation in five generated Go files, and the encodings, line ends, final newlines and trailing spaces of five case files); .node-version set to 25.0.0 failed with .node-version 25.0.0 differs from the major version of engines.node >=26, and .gitignore without packages/template-ts/dist.inputs failed with outputs that .gitignore does not ignore: packages/template-ts/dist.inputs. Green: the tests pass: a docs path runs exactly docs-check and docs-static-check, an unowned tracked path and a new unowned path fail with their names before any target runs, a glob without a path and the target check fail, a rule with a variable passes its paths, and scripts/owner-checks.json owns every tracked path; make owner-check of this change ran its selected owners and passed | node scripts/run-tests.mjs node -- tests/scripts/owner-check.test.mjs tests/scripts/conformance-cases.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/ignored-files.test.mjs tests/scripts/editorconfig.test.mjs | [o] |
| T13.1-4-1 | Make make owner-check select nothing for a removed path that no rule owns, and run a test that is the changed path itself only while it exists. Cause: renaming scripts/build-ts.mjs and tests/scripts/build-ts.test.mjs in T18.8-1 made the owner check fail with scripts/build-ts.mjs: the path matches no owner and select the removed test file. Red: with the script of T13.1-4, the new cases a removed path that no rule owns selects nothing and a removed test file is not run failed. Green: the 8 cases of tests/scripts/owner-check.test.mjs pass | node scripts/run-tests.mjs node -- tests/scripts/owner-check.test.mjs | [o] |
Wave 14 — Data binding cost
| ID | Task | Verification | Done |
|---|---|---|---|
| T14.1 | Check the strings, map keys and define ids of host binding with mb_check_encoding and keep Utf8::firstInvalid where a position is reported; add a test that compares both on every 1-byte and 2-byte sequence and on the boundary 3-byte and 4-byte sequences | make check | [o] |
| T14.2 | Bind data once for several renders (VAL-22, RT-4, RT-24, RT-61, ERR-14): a bound map type with bind and merge in TypeScript, the browser entry, Go, Rust, PHP and the PHP extension; packages/template-compiler/interface.json lists the type and its names for every implementation, including new php-extension and javascript-esm mappings, and the operation or parameter type through which each implementation passes a bound map to render and prepare, and the interface checks fail on another public operation of the type; the TypeScript build shares one runtime module that holds the bound map class across its entries and both module formats, because the CommonJS build of tsup does not split chunks and index.cjs and render.cjs each hold a copy of the runtime today; an export that already has the name of a bound map operation, such as the TypeScript and Rust bind, is renamed or removed and the backends and generated programs are regenerated; render and prepare accept a bound assign and bound definition data without binding them again, also in generated programs and the PHP extension, and typed programs convert them to their records (RT-68); host binding fails with E_DATA_UNSUPPORTED_TYPE on a bound map at any other position or from another implementation; a VAL-22 and ERR-14 group in tests/rule-evidence.json with tests in every implementation and a shared fixture of merge order, rejected positions and byte equality; the bound-data feature becomes implemented; the guide shows bind and merge; a changelog entry. Progress: implemented in TypeScript, the browser entry, Go, Rust, PHP, the PHP extension and the four generated programs, with packages/template-compiler/interface.json, its checks and the bound-data evidence group; the T14.2 tests of every runtime, scripts/check-generated-bound-data.mjs, the compiler interface check with its mutations and scripts/check-rules.mjs pass. Remaining: make check | make check | [o] |
| T14.2-1 | Name the bound inputs of a Rust request as a struct: T14.2 returned a tuple of the root, the definition registry and the environment from a closure in prepare_request, which cargo clippy -D warnings rejects as type_complexity (exit 101 on main T15.3); BoundRequest names the three fields, and clippy and the bound map tests pass | cargo clippy --all-targets -- -D warnings | [o] |
| T14.2-2 | Write the CommonJS files of the TypeScript package into the output directory of the build: T14.2 wrote dist/<entry>.cjs relative to the working directory, so a build of the package with --out-dir failed with ENOENT: dist/index.cjs; onSuccess writes into --out-dir, else dist, and tsup with and without --out-dir writes index.cjs, render.cjs and node.cjs there | npx tsup --out-dir <dir> | [o] |
Wave 15 — Binding errors that differ between runtimes
| ID | Task | Verification | Done |
|---|---|---|---|
| T15.1 | Add a conformance case whose assign is JSON null and that renders as an empty map; check it fails in Rust; make Rust render, render_values and prepare and the PHP extension render, which renders through render_values and render, accept a null assign as an empty map (bind_map, php_to_map); state the rule in RT-4 | make check | [o] |
| T15.1-1 | Make the generated TypeScript and PHP programs bind a null assign as an empty map, as their AST programs do (RT-4); make --case of the four generated conformance runners select the case and fail when it selects none, because the runners read only a positional filter and --case data/null-assign ran 0 cases and passed | make check | [o] |
| T15.2 | Add a test in every generated runtime that renders a request whose assign and definition data do not match the declared types; check that the runtimes report it differently; make every runtime report it as ERR-13 states, an argument error of its language that passes unchanged and is not an ERR-1 error | make check | [o] |
| T15.2-1 | Make the Rust part of scripts/check-typed-generator.mjs read the RequestError that Program::render returns since T15.2: the generated test declared Result<String, TemplateError>, so typed-generator-compile-check, part of make check, failed to compile with E0308 | node scripts/check-typed-generator.mjs | [o] |
| T15.3 | Add tests that create a Rust engine and parse in Rust with a delimiter option that is not a delimiter pair, and the same in the PHP extension; check that Rust AstProgram::new panics, Rust parse returns an ERR-1 error and the extension raises an \Exception; make them report the argument error of ERR-13: AstProgram::new returns Result<AstProgram, ArgumentError>, parse returns RequestError::Argument and the extension raises an \InvalidArgumentException as the PHP package does | cargo test --test delimiters, run-tests.sh --filter Delimiters | [o] |
| T15.3-1 | Make Go Parse return the argument error of Go for a delimiter option that is not a delimiter pair, as NewAstProgram does: delimitersOf returned an *errs.Error with E_DATA_UNSUPPORTED_TYPE, an ERR-1 error, where ERR-13 names an error that is not an *errs.Error; add a Go test that fails first, fix Parse, and state the Go form in ERR-13 | TestInvalidDelimiterIsArgumentError | [o] |
| T15.4 | Make the Go typed generated program read an absent optional field as null, as the TypeScript generated program does: the Go backend reads an optional field through valueOrZero, so {= c ?? '-'} with a string? field c that the assign data lacks renders an empty string and not -; add the case in every generated runtime, check that it fails in Go, then fix the backend | make check | [o] |
Wave 16 — Concurrent renders
| ID | Task | Verification | Done |
|---|---|---|---|
| T16.1 | Decide whether renders of every runtime may share one program and state the rule in the runtime specification; when they may, add a race test that renders one program concurrently in Go and make the template cache of every runtime safe for it; when they may not, state that a host uses one program per thread or goroutine | make check | [o] |
Wave 17 — Test runs
| ID | Task | Verification | Done |
|---|---|---|---|
| T17.1 | State in AGENTS that a task in progress runs only the Red and Green tests that own the change, that a commit also runs make docs-check, that a task becomes done after its owning command passes and that make check runs once when every active task is done; state that every test prints its progress and has its own timeout and that no whole run has a time limit; name the owning command in the Verification column; add the section "Decision and acceptance rules" to AGENTS.ko.md | make docs-check | [o] |
| T17.2 | Add the test runner scripts/run-tests.mjs with scripts/test-progress/ for node, vitest, go, cargo and phpunit, which prints each test with its elapsed time and stops a test at its own timeout, with its tests in tests/scripts/run-tests.test.mjs; run test-go, test-rust and test-php through it, remove the 120 s package limit of test-go and enforce the PHPUnit time limit | node scripts/run-tests.mjs node -- tests/scripts/run-tests.test.mjs | [o] |
| T17.3 | Make tests/runner/conformance.mjs print <case> [<lang>] pass|fail (<ms>) when a case finishes in a language, and keep the summary at the end | node scripts/run-tests.mjs node -- tests/scripts/conformance-progress.test.mjs | [o] |
| T17.4 | Give every case of the four generated conformance runners its own deadline and a result line with its elapsed time: Go and Rust through scripts/run-tests.mjs with Rust tests named by case id, PHP php -l and the case process under a timeout, TypeScript tsc under a deadline with a step line and each render in a worker under a timeout; remove the 600 s limits | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/generated-conformance.test.mjs | [o] |
| T17.4-1 | Remove the 300 s deadline of the tsc step in tests/runner/conformance-generated-ts.mjs; keep its start line, its still-running lines and its result line with the elapsed time, judge it by the exit code of tsc, and keep the 30 s deadline of each render | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/generated-conformance.test.mjs | [o] |
| T17.5 | Give every VS Code launch and profile installation of packages/template-vscode/tests/integration/run.mjs a deadline that kills the process, and every check of suite/index.cjs a timeout through Promise.race with the elapsed time on its ok and not ok line | npm run test:integration -w polyspec-template | [o] |
| T17.5-1 | End a VS Code launch by the result of its suite: VS Code 1.138 took up to more than 300 s to exit after the suite had printed its result, so make check failed at the launch deadline; once the suite line [suite] N of M checks passed appears, VS Code has 20 s to exit, then its process group is killed and the launch takes the suite result; the 20 s start after the suite result has been printed, so they end VS Code as cleanup and do not decide whether the launch passes | npm run test:integration -w polyspec-template | [o] |
| T17.5-2 | Remove the deadlines of the profile installation (120 s) and of the VS Code launch (300 s) in packages/template-vscode/tests/integration/run.mjs; print the start, a line every 10 s while a step runs and the result of the download, each installation and each launch with its elapsed time, and judge each step by its exit code and its suite result; keep the 20 s check timeout and the 20 s exit grace after the suite result | node scripts/run-tests.mjs node --cwd packages/template-vscode -- tests/integration-step.test.mjs | [o] |
| T17.6 | State in AGENTS that a long-running operation prints step logs and has no timeout, also no deadline for missing output, and is judged by its exit status, its result and its errors, while a test case keeps its own timeout; state in AGENTS.md that English documents are canonical, as AGENTS.ko.md states | make docs-check | [o] |
| T17.7 | Run the build of a driver in tests/runner/drivers.mjs as a logged step without the 600 s limit: print its start, its output and its result with its elapsed time on standard error and judge it by its exit code; keep the 10 s timeout of each CLI call | node scripts/run-tests.mjs node -- tests/scripts/long-steps.test.mjs | [o] |
| T17.8 | Run each command of scripts/check-package-installs.mjs (600 s) and scripts/check-install-workspace.mjs (120 s) as a logged step without a time limit, judged by its exit code | node scripts/run-tests.mjs node -- tests/scripts/long-steps.test.mjs | [o] |
| T17.9 | Make tests/runner/delimiter-matrix.mjs print <pair> [<lang>] pass|fail (<ms> ms) when a delimiter pair finishes in a language, and keep the summary at the end | node scripts/run-tests.mjs node -- tests/scripts/delimiter-matrix-progress.test.mjs | [o] |
| T17.10 | Run the vitest tests of test-ts, test-language, test-lsp and test-codemirror and the node --test tests of test-vscode through scripts/run-tests.mjs | node scripts/run-tests.mjs node -- tests/scripts/test-targets.test.mjs | [o] |
| T17.11 | Start the static server of make test-browser in tests/browser/run.mjs as a step without a time limit instead of the Playwright webServer with its 30 s startup deadline (Playwright reads timeout: 0 as 60 s); print the server output, judge readiness by the line listening on http://127.0.0.1:4173 and fail when the server exits first | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/browser-server.test.mjs | [o] |
| T17.1-1 | Enforce the single full run with the guard scripts/full-run.mjs, which make check starts before any step: it refuses while a task row of this checklist is in progress and lists each active ID with its task, refuses while tracked changes are uncommitted, refuses a second full run of the same tree (git rev-parse HEAD^{tree}) and names the earlier run, runs each target of CHECK_TARGETS with make <target> to its end and writes var/full-run.json (tree, commit, result, each target with its status and times) before and after each target, so a stopped run stays incomplete; make rerun-failed reruns only the targets of the current tree that did not pass and completes the result when they pass. Cause: AGENTS stated that make check runs once, when every active task is done, and nothing enforced it: a run with a task in progress, a run of uncommitted changes and a second run of the same tree all started the suite. Red: make -n check printed the first step of docs-check, so the suite started whatever the checklist held, make -n rerun-failed failed with No rule to make target, and tests/scripts/full-run.test.mjs failed with ERR_MODULE_NOT_FOUND for scripts/full-run.mjs. Green: its 10 cases pass: make -n check prints only the guard, a fixture checklist with a task in progress, a dirty tree, a second run of one tree and rerun-failed without a record are refused before any stub target runs, and rerun-failed runs only the stub targets that failed or did not finish | node scripts/run-tests.mjs node -- tests/scripts/full-run.test.mjs | [o] |
| T17.1-2 | Make CHECK_TARGETS the one list of the full suite and run it everywhere through the guard: make release-test-matrix ran seven layers of other targets and stopped at its first failure, so CI reported only dependency-policy-check and skipped six layers, while make check did not run dependency-audit, benchmark-check, benchmark-smoke and docs-verify-idempotent, and CI ran neither test-scripts, language-test-matrix, function-contract-check, delimiter-matrix nor generated-native-check. CHECK_TARGETS holds the four targets; make release-test-matrix runs node scripts/full-run.mjs run $(CHECK_TARGETS); the release job of .github/workflows/ci.yml runs xvfb-run -a make check and uploads var/full-run.json; every step of a verifying job runs after a failing step (!cancelled()) and a make with several targets runs with -k; scripts/owner-check.mjs treats a target that starts scripts/full-run.mjs or scripts/check-clean-release.mjs as a full-suite target, and tests/scripts/clean-release.test.mjs owns scripts/check-clean-release.mjs | node scripts/run-tests.mjs node -- tests/scripts/suite-parity.test.mjs tests/scripts/clean-release.test.mjs tests/scripts/owner-check.test.mjs | [o] |
| T17.1-3 | Make a push impossible while a task of this checklist is in progress: AGENTS stated that a push happens only when every active task is done, and nothing enforced it. The tracked pre-push hook .githooks/pre-push runs scripts/push-gate.mjs hook, which reads the checklist of each pushed commit and of the working tree with activeItems of scripts/full-run.mjs and refuses the push, naming each task, and also when it cannot read a checklist; every make invocation sets core.hooksPath to .githooks, make hooks installs and checks it, make hooks-check fails when it is not installed and runs before make owner-check, and the guard refuses make check without it; the workflow .github/workflows/push-gate.yml (job push-gate) runs scripts/push-gate.mjs commit on every push to any branch and on every pull request and fails, naming each task, also when .githooks/pre-push is not tracked as an executable file. The workflow follows the rules of T20.1-8, T20.1-9 and T20.1-12: its step runs make ci-targets TARGETS="push-gate-commit", its actions are pinned by commit, it runs on ubuntu-24.04, uploads the report var/report/ci-targets under !cancelled() and cancels the running gate of its workflow and ref on a new push | node scripts/run-tests.mjs node -- tests/scripts/push-gate.test.mjs tests/scripts/full-run.test.mjs tests/scripts/toolchain-files.test.mjs | [o] |
| T17.1-4 | Make the make dry runs of tests/scripts/full-run.test.mjs, tests/scripts/suite-parity.test.mjs and tests/scripts/test-targets.test.mjs print only the commands on every make: CI failed in test-scripts, because GNU Make 4 in the make of make check printed make[2]: Entering directory and Leaving directory around the guard command, which macOS make 3.81 does not print without -w. The dry runs pass --no-print-directory and run with MAKEFLAGS=w, so the condition holds on every make | node scripts/run-tests.mjs node -- tests/scripts/full-run.test.mjs tests/scripts/suite-parity.test.mjs tests/scripts/test-targets.test.mjs | [o] |
| T17.1-5 | Run the document checks in the job push-gate: the check that the ruleset main requires ran only the gate of tasks, so a commit whose checklist had a state marker outside a task state, or whose documents broke another document, feature or rule check, passed it and could reach main. The job runs make ci-targets TARGETS="push-gate-commit documents-check feature-check rules-check", and tests/scripts/push-gate.test.mjs requires these targets and that each runs a script of the checkout whose imports are Node.js modules only, since the job installs nothing else | node scripts/run-tests.mjs node -- tests/scripts/push-gate.test.mjs | [o] |
| T17.1-6 | Catch stale compiled artifacts before main: T8.5-1 changed a source of template-ts and left the AST artifacts of the showcase with the digest of the previous compiler, and nothing before the release job of CI noticed, because development runs unit tests, the job push-gate ran only the gate and the document checks, and showcase-ast-check compiles with --refresh false, which keeps a stale digest. The AST compiler digest is now computed from the tracked sources and build configuration of packages/template-ts, package-lock.json and the artifact writer instead of the built dist, and make artifact-digest-check (scripts/check-artifact-digests.mjs) fails, naming each manifest with the recorded and the current digest and make showcase, when one of the 30 committed artifacts records the digest of another compiler; it needs Node.js alone, so the job push-gate runs it, and showcase-check runs it first. The 30 artifacts and the page are compiled again for the new digest; only the digests change. Red: the check named all 30 artifacts after the digest change. Green: it passes after make showcase-compile, typed-generator and the page | node scripts/run-tests.mjs node -- tests/scripts/artifact-digests.test.mjs tests/scripts/push-gate.test.mjs, make artifact-digest-check | [o] |
| T17.1-7 | Publish every change through a pull request and the merge queue. The ruleset main of .github/ruleset.json requires a pull request without approval (merge, squash and rebase allowed), the merge queue with the method REBASE, a linear history and the checks push-gate and every job of ci.yml (release, docs, typescript, browser, editor, go, rust, php (8.2), php (8.5)) of GitHub Actions, and refuses a force-push and a deletion without a bypass actor; the declaration also sets allow_rebase_merge, allow_auto_merge and delete_branch_on_merge, which make github-ruleset applies and make github-ruleset-check compares. ci.yml runs on pull_request and merge_group, push-gate.yml on a push outside gh-readonly-queue/**, pull_request and merge_group, both cancel only pull request runs, and the deployment of the documentation site moved to pages.yml on a push to main. | node scripts/run-tests.mjs node -- tests/scripts/github-ruleset.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/push-gate.test.mjs | [o] |
| T17.1-9 | Declare the triggers of each workflow exactly: ci.yml runs on pull_request, merge_group and workflow_dispatch, so every check also runs on a manual run; push-gate.yml runs on a push outside gh-readonly-queue/**, pull_request and merge_group; pages.yml builds and deploys the documentation site on a push to main and on workflow_dispatch; dependency-review.yml runs on its schedule and on workflow_dispatch. The ruleset main requires push-gate and every job of ci.yml. tests/scripts/toolchain-files.test.mjs requires the on: block of each workflow exactly and no other workflow | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/push-gate.test.mjs tests/scripts/github-ruleset.test.mjs | [o] |
| T17.1-10 | Run each target of the full suite in exactly one CI job: the job release of ci.yml ran make check, every target of CHECK_TARGETS, also the 15 targets that the docs, typescript, browser, editor, go, rust and php jobs run, and took 670 s, the longest job of the workflow. The jobs scripts, generated, delimiters and showcase run the targets that no other job runs with make ci-targets, and ci-passed needs them. CHECK_TARGETS names the targets that the jobs run: lint-js, lint-go, lint-rust, lint-showcase-format, lint-showcase-warnings and lint-php in place of lint, and conformance-ts, conformance-go, conformance-rust, conformance-php and the four conformance-generated-* targets in place of conformance-all-modes, whose AST conformance of php-ext test-ext runs. tests/scripts/suite-parity.test.mjs fails unless the targets of the make ci-targets steps of ci.yml are CHECK_TARGETS with each target in one job, and fails on a job that runs make check; it failed on the ci.yml with the job release, with 20 targets in no make ci-targets step and five targets outside CHECK_TARGETS. make ci-targets writes the versions of Node.js, npm, Go, cargo, PHP and Composer of its job into toolchains.json and summary.md of its report (toolchainVersions of scripts/target-report.mjs), so every CI job records the PHP patch that setup-php installed, which only var/full-run.json of the job release recorded; tests/scripts/target-report.test.mjs requires them | node scripts/run-tests.mjs node -- tests/scripts/suite-parity.test.mjs tests/scripts/target-report.test.mjs tests/scripts/full-run.test.mjs tests/scripts/toolchain-files.test.mjs | [o] |
Wave 18 — Test resources of one run
| ID | Task | Verification | Done |
|---|---|---|---|
| T18.1 | Make tests/browser/server.mjs listen on a port that the system assigns and print it in its line listening on http://127.0.0.1:<port>; tests/browser/run.mjs passes that address to Playwright in TEMPLATE_BROWSER_URL, which playwright.config.ts requires | node scripts/run-tests.mjs node -- tests/scripts/browser-port.test.mjs | [o] |
| T18.2 | Give the VS Code directory .vscode-test a holder lock .vscode-test.lock: the integration run holds it from the download to its last launch, a second run fails with the holder, make clean removes the directory only while it holds the lock, and a lock of an ended process is reported and removed by make vscode-test-unlock | node scripts/run-tests.mjs node -- tests/scripts/holder-lock.test.mjs | [o] |
| T18.3 | Make tests/scripts/browser-server.test.mjs run tests/browser/run.mjs against the package build of make test-scripts instead of make test-browser, whose package build (tsup clean: true) emptied packages/template-ts/dist while the other tests of make test-scripts read it: make check failed in tests/scripts/generated-conformance.test.mjs with TS7016 for @polyspec/template | node scripts/run-tests.mjs node -- tests/scripts/browser-server.test.mjs | [o] |
| T18.4 | Install every npm dependency as a copy without bin links: npm linked the five workspace packages into node_modules and wrote 37 bin links into node_modules/.bin. .npmrc sets install-links=true and bin-links=false; the root package.json declares the packages of packages/ as file: dependencies with overrides instead of npm workspaces, which npm always links, and holds their development tools; each build target installs the copy of its package again; the recipes and scripts start every tool with the file of its package (scripts/tools.mjs); the dependency policy reads the root manifest only; tests/scripts/no-symlinks.test.mjs fails on any symbolic link below node_modules or vendor | node scripts/run-tests.mjs node -- tests/scripts/no-symlinks.test.mjs | [o] |
| T18.5 | Install template-fmt with make install-cli under the declared prefix CLI_PREFIX without a symbolic link: a copy of the formatter package in <prefix>/lib/polyspec-template-fmt and an executable script <prefix>/bin/template-fmt that runs its entry by absolute path; make uninstall-cli removes both. npm link and npm install --global write a bin link, and since T18.4 a global install fails because npm resolves @polyspec/template 0.0.1 from the registry | node scripts/run-tests.mjs node -- tests/scripts/install-cli.test.mjs | [o] |
| T18.6 | Update eslint to the latest stable release 10.12.0: make dependency-policy-check failed with outdated npm dependency has no exception: eslint 10.11.0 < 10.12.0, so make release-check failed; record the digest of the changed package-lock.json in the generated JavaScript showcase manifests and the example page | make dependency-policy-check lint-js | [o] |
| T18.7 | Build the Rust crates of each checkout into its own target: the Makefile does not pass an inherited CARGO_TARGET_DIR to cargo, and the Rust tests resolve the repository root from CARGO_MANIFEST_DIR at run time, not at compile time; remove the stale artifacts of the crate from the target of the main checkout. Cause: make check failed in test-rust, where 6 of the 7 tests of tests/bound.rs panicked with repository root: NotFound: a worktree of T17 had built its tests with CARGO_TARGET_DIR set to the target of the main checkout, env!("CARGO_MANIFEST_DIR") embedded the path of that worktree, which was removed later, and cargo judged the 5 test binaries built there fresh for the main checkout, whose sources were older. cargo clean -p polyspec-template removed the artifacts of the crate from the target of the main checkout (11236 files, 600.2 MiB), the stale test binaries included, and kept its dependencies; AGENTS states the rule. Red: in the main checkout, node scripts/run-tests.mjs cargo --cwd packages/template-rust -- --locked --test bound finished without compiling and failed in 6 of 7 tests with repository root: NotFound; tests/scripts/cargo-target.test.mjs failed in its 2 cases, because a stub cargo of make build-rust and make test-rust received the inherited CARGO_TARGET_DIR. Green: the 2 cases pass with CARGO_TARGET_DIR=unset; make test-rust in the worktree of this task compiled polyspec-template into its own target and passed clippy and 49 tests, the 7 of tests/bound.rs included | node scripts/run-tests.mjs node -- tests/scripts/cargo-target.test.mjs and make test-rust | [o] |
| T18.7-1 | Install the Rust toolchain in one explicit step and let no cargo install it: CI failed in test-scripts, because the release job had no installed 1.98.1 and three test files of one node --test run started cargo at once; each rustup auto-installed the toolchain and one cargo failed with the 'cargo' binary, normally provided by the 'cargo' component, is not applicable to the '1.98.1-x86_64-unknown-linux-gnu' toolchain. make install runs rustup toolchain install --no-self-update after npm ci, the Makefile exports RUSTUP_AUTO_INSTALL=0, the CI workflows set it, the release job runs make install and the rust and php jobs rustup toolchain install --no-self-update. The summary of scripts/run-tests.mjs names the error lines of a tool that exits without a failing test; the PHPUnit timeout test installs the PHPUnit that it runs | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/run-tests.test.mjs | [o] |
| T18.8 | Publish the build of the TypeScript package so that a reader of packages/template-ts/dist never finds a file missing, and build it only when its inputs change: scripts/build-ts.mjs builds into a directory next to dist and moves each file into dist with a rename, the modules that the entries import first and the entries last, then removes the files of the previous build that the new one does not have, and skips the build when the hash of its inputs equals the recorded one. Cause: while make check rebuilt dist with tsup, a reader of dist failed, because tsup with clean: true empties dist before the build, so index.d.ts, render.d.ts and node.d.ts were missing until the declaration build ended; one make check ran that build 17 times with the same inputs. docs/operations/development.md states the publication and its limit: a reader that reads several files while a build of changed sources publishes can combine files of the two builds. Red: a reader that polled a complete dist every 2 ms while tsup rebuilt it in place found the declarations index.d.ts, render.d.ts and node.d.ts missing in 413 of 1559 polls and the six entry modules missing in 32 to 34 polls; tests/scripts/build-ts.test.mjs failed in its 2 cases, because scripts/build-ts.mjs did not exist. Green: the 2 cases pass: during a forced build the reader polled dist 839 times and found no file missing, the build wrote the same files with the same contents as the build before it, and a build with unchanged inputs printed that dist is current and left index.mjs untouched; make build-ts built and installed once, and a second make build-ts built and installed nothing | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/build-ts.test.mjs | [o] |
| T18.8-1 | Build template-language, template-lsp, template-codemirror and the VS Code bundle of template-vscode with the mechanism of T18.8: scripts/build-ts.mjs becomes scripts/build-package.mjs --package <package>, whose inputs also hold the root package-lock.json and the installed copies of the packages of this repository that a package depends on, whose record dist.inputs.json lists the files of the build, and which moves each file after the relative modules it imports. Cause: no repository outside this one reads these packages (git grep of the repositories that use this repository found none), but one make check built them again with the same inputs: tsup ran 7 times for template-language, 3 times for template-lsp and 2 times for template-codemirror, and esbuild bundled template-vscode 2 times, each emptying or rewriting dist that the next target read. Red: the 11 cases of tests/scripts/build-package.test.mjs failed with Unknown option '--package'. Green: the 12 cases pass: for each of the five packages a reader that polled dist during a forced build found no file missing, the build wrote the same files as the build before it, and a build with unchanged inputs did not build; the inputs of template-vscode hold the installed template-lsp, template-language and template packages; an unknown package fails with its name. A second make build-vscode build-codemirror built and installed nothing | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/build-package.test.mjs | [o] |
| T18.9 | Update phpunit/phpunit to the latest stable release 11.5.57 in packages/template-php and packages/template-php-ext: CI failed in make release-test-matrix because make dependency-policy-check failed with outdated Composer dependency has no exception: packages/template-php/composer.json phpunit/phpunit 11.5.56 < 11.5.57; composer outdated --direct --locked reported the same version in packages/template-php-ext, which the check did not reach. composer update phpunit/phpunit --with-dependencies also updates nikic/php-parser to 5.9.0 in both locks | make dependency-audit test-php test-ext | [o] |
| T18.10 | Check the dependency state against the review recorded in the repository, without a network, so that one tree gives one result at any time: make dependency-policy-check queried the registries on every run and failed on tree T18.8 after phpunit 11.5.57 was released, with no change of the tree. scripts/check-dependency-policy.mjs reads the manifests, the locks, config/dependency-policy.json and the new review record config/dependency-review.json (the time, the sha256 and advisories of each lock, the locked and latest stable version of each registry dependency) and reports every finding with its rule and fix; composer validate runs with COMPOSER_DISABLE_NETWORK=1. The developer command make dependency-review (scripts/dependency-review.mjs) asks the registries for newer stable releases and advisories, RECORD=1 writes the record and UPDATE=1 updates first; make dependency-audit runs no live audit, and the scheduled workflow .github/workflows/dependency-review.yml runs the review every day. file: packages and platform requirements are outside the review by its scope; the gate checks the lock of a file: package against its version | node scripts/run-tests.mjs node -- tests/scripts/dependency-policy.test.mjs, make dependency-audit | [o] |
| T18.8-2 | Make tests/scripts/build-package.test.mjs create what it depends on and install the copy of a package atomically: on a checkout after npm ci with nothing built, 7 of its cases failed with TS2307: Cannot find module '@polyspec/template' and Could not resolve "@polyspec/template-lsp", because the installed copies had no dist and the test relied on make test-scripts having built them, which make owner-check and a direct run do not do. The test builds and installs template-ts, template-language and template-lsp with scripts/build-package.mjs --install before its cases. --install reinstalled a copy with rm -rf and npm install, so a reader of the copy found 20 files missing during a reinstall; it now writes each changed file into node_modules/<name>.next-<pid>, moves it into the copy with a rename, package.json last, removes the files that the package no longer has, and fails with npm install as the fix when the dependencies of the package differ from the copy | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/build-package.test.mjs | [o] |
Wave 19 — Results that one tree decides
| ID | Task | Verification | Done |
|---|---|---|---|
| T19.1 | Make the package install check read no source of the network and use the toolchains of the checkout: the Go install project resolved modules from proxy.golang.org, verified checksums against sum.golang.org and ran with GOTOOLCHAIN=auto, which downloaded the Go toolchain 1.27.1 into the module cache of every run on a machine with Go 1.27.0; the Rust install project had no lock, so cargo resolved serde and serde_json to the newest releases of the run, and it built in the system temporary directory, outside rust-toolchain.toml, with the default toolchain of the machine (1.98.0, the checkout pins 1.98.1); the PHP install project could read Packagist. The Go install project uses only the proxy of the run, GOSUMDB=off, GOTOOLCHAIN=local and the go directive of packages/template-go/go.mod; the Rust install project copies rust-toolchain.toml and runs cargo run --locked with a lock derived from packages/template-rust/Cargo.lock (installCargoLock of scripts/install-workspace.mjs); the PHP install project disables Packagist | node scripts/run-tests.mjs node -- tests/scripts/package-installs.test.mjs | [o] |
| T19.2 | Run every recipe with the toolchains that the checkout declares: make install-tools (scripts/install-tools.mjs) installs the npm of packageManager in package.json (12.2.0) and the Go of the go directive of packages/template-go/go.mod (1.27.1, module golang.org/toolchain) into var/tools with wrapper scripts and skips an install of a present version; the Makefile puts var/tools/bin first on PATH, exports GOTOOLCHAIN=local, starts npm as $(NPM) because GNU Make 3.81 finds the program of a line without shell syntax with the PATH that make started with, and make install depends on install-tools; PHP is pinned by its minor version in config/toolchain.json, because setup-php cannot pin a patch, Composer exactly, and var/full-run.json records the versions of each run; the workflows pin every action by its commit, run on ubuntu-24.04 and install with make install; tests/scripts/toolchain-files.test.mjs runs for every change | node --test tests/scripts/toolchain-files.test.mjs tests/scripts/install-tools.test.mjs tests/scripts/full-run.test.mjs | [o] |
| T19.3 | Print the compiler errors of a Go package that does not build: goEvents of scripts/run-tests.mjs dropped the build-output and build-fail events of go test -json, so a module that did not compile printed only that its package failed; the runner prints both and the summary line names the compiler errors | node --test tests/scripts/run-tests.test.mjs | [o] |
| T19.4 | Fail a check that verifies nothing: progress.close of scripts/test-progress/progress.mjs passed a run in which no test ran, and the node and vitest reporters now fail their process on it; packages/template-ts/tests/cli.test.ts skipped its tests when dist was absent and now builds it; make lint skipped gofmt, cargo fmt, and pint, and make docs-check the documentation coverage, when their directory or file was absent; selectLanguages of tests/runner/drivers.mjs dropped a language whose package was absent; tests/scripts/no-silent-skips.test.mjs fails on each | node --test tests/scripts/no-silent-skips.test.mjs tests/scripts/run-tests.test.mjs | [o] |
| T19.5 | Install the Composer packages of template-php before every target that runs PHP with them: conformance-generated-php, contract-check, benchmark-smoke, generated-native-check, bench and showcase did not depend on build-php, so on a checkout without packages/template-php/vendor make generated-native-check failed on vendor/autoload.php | node --test tests/scripts/test-targets.test.mjs | [o] |
| T19.6 | Build what a run reads every time, with builds that do nothing on unchanged inputs: prepare of tests/runner/drivers.mjs built the CLI of a language only when its binary was absent, so a binary built from older sources was used; tests/scripts/install-cli.test.mjs and tests/scripts/generated-conformance.test.mjs relied on builds of make test-scripts and failed on a checkout without dist; the tests build and install what they read before their cases | node --test tests/scripts/drivers.test.mjs tests/scripts/install-cli.test.mjs tests/scripts/generated-conformance.test.mjs | [o] |
| T19.7 | Publish files and directories that others read at once and keep the previous version on failure: make showcase and make bench wrote examples/site/data/mode-benchmark.json with a shell redirection, which empties the file first, and now run benchmark-modes.mjs --output, which writes a temporary file and renames it; make docs-verify-idempotent copied its first build to the fixed docs/.vitepress/dist.first and now copies it to a directory of its run from mktemp -d; scripts/install-cli.mjs removed the previous install before npm install and now prepares <prefix>/lib/.polyspec-template-fmt.next-<pid> and renames it | node --test tests/scripts/atomic-publish.test.mjs tests/scripts/install-cli.test.mjs | [o] |
| T19.8 | Report every failure of one run: make stopped a recipe at its first failed command and a run at its first failed target, and the language check scripts stopped at their first failed language; the Makefile sets MAKEFLAGS += -k, scripts/full-run.mjs runs make -k <target>, every target of the full suite and of its prerequisites runs at most one command, its checks being prerequisites (lint-go, test-ts-types, documents-check, ...), and scripts/check-generated-*.mjs, scripts/check-typed-generator.mjs and scripts/check-package-installs.mjs run every language through checkLanguages of scripts/language-checks.mjs; tests/scripts/failure-accumulation.test.mjs checks the rules | node --test tests/scripts/failure-accumulation.test.mjs | [o] |
| T19.9 | Select every check that reads a changed path: scripts/owner-checks.json did not select conformance, the generated conformance or lint for the packages, test-ext for template-rust, the language, LSP, CodeMirror, VS Code and browser tests for template-ts, the generated checks and showcase-check for packages/template-compiler, benchmark-smoke for its scripts, docs-check for scripts/generate-showcase-contract.mjs and test-vscode-integration for the VS Code packages; inputs now declares the paths that each check target reads, and scripts/owner-check.mjs --validate fails on an input path whose rules do not select its target | node --test tests/scripts/owner-check.test.mjs | [o] |
| T19.10 | Name what failed with the expected and the actual value or the fix: the generated Go and Rust conformance runners said "the failed cases are listed above" and now name each failed case; scripts/run-tests.mjs ended with an unhandled error event when a tool could not start and now names the command and the install fix; a CLI call of the conformance drivers that outlived its limit returned the error of spawnSync and now names the limit and the command; the Go and PHP package install projects failed with a message that named neither output and now print the expected and the actual output; the PHP extension tests ran through run-tests.sh and now run through scripts/run-tests.mjs phpunit --php-extension | node --test tests/scripts/failure-messages.test.mjs | [o] |
| T19.11 | Make the tests independent of the output format of tools and of the user: tests/scripts/dependency-policy.test.mjs asserted the wording of composer and asserts the message of the gate; scripts/check-package-installs.mjs took the name of the npm archive from the last line of npm pack and computes it from the manifest; the loader tests of TypeScript, Go, Rust, PHP and the PHP extension expected a file of mode 0o000 to be unreadable, which root reads, and assert that it fails to load exactly when the process cannot read it and that only root can; scripts/check-install-workspace.mjs made a removal fail with a directory without write permission, which root removes, and removes a workspace that no longer exists | node --test tests/scripts/package-installs.test.mjs tests/scripts/dependency-policy.test.mjs | [o] |
| T19.6-1 | Run the builds that changed, not every new file of a build: cargo links target/release again on every build and go build writes a new file, and macOS checks a new executable file on its first run, so after the builds of T19.6 the first conformance calls of the Go and Rust CLIs outlived their 10 s limit in make delimiter-matrix; scripts/publish-build.mjs publishes a build to var/build only when its bytes change, and the drivers, make test-ext and the benchmark run the published files | node --test tests/scripts/publish-build.test.mjs tests/scripts/drivers.test.mjs | [o] |
| T19.12 | Lock the record of the full run: scripts/full-run.mjs read var/full-run.json, decided and wrote it without a lock, so two guards that started together both ran and wrote the record in turn; the guard holds the holder lock var/full-run.json.lock of scripts/holder-lock.mjs from reading the record to its last write and refuses with the holder while another guard holds it, and a released holder lock leaves no process handler behind; the copy of the first documentation build has a directory of its run since T19.7 | node --test tests/scripts/full-run.test.mjs tests/scripts/holder-lock.test.mjs | [o] |
| T19.13 | Leave no process of a group that a check started: runBounded and runStep of tests/runner/bounded.mjs resolved when the command closed and runStep of the VS Code integration test when VS Code exited, while a background process of the group could still run and write into the directory that the caller removes next; they resolve after stopProcessGroup of scripts/process-group.mjs, moved from tests/scripts, finds no process of the group | node --test tests/scripts/process-group-callers.test.mjs tests/scripts/process-group.test.mjs | [o] |
| T19.14 | Build Go into the build cache of the checkout: the generated checks, scripts/check-typed-generator.mjs and scripts/check-compiler-interface.mjs set GOCACHE to the fixed /tmp/template-go-cache, which every checkout and run shared, without a requirement to share it; the Makefile exports GOCACHE=var/go/cache, the scripts use the environment of the recipe, and make clean removes var/go | node --test tests/scripts/toolchain-files.test.mjs | [o] |
| T19.15 | State the rules of idempotency in AGENTS, one rule for each class of defect of Wave 19: toolchains, network, builds, shared state, nothing verified, every failure in one run, owners, messages and independence from tool output and the user | make docs-check | [o] |
| T19.8-1 | Give the probe makefile of tests/scripts/failure-accumulation.test.mjs as a file of the run: the test passed it to make as -f /dev/stdin with the input of spawnSync, which Node.js passes through a socket; macOS duplicates the descriptor and the test passed with GNU Make 3.81 and 4.4.1, but Linux opens /proc/self/fd/0 and cannot open a socket, so in CI make failed with /dev/stdin: No such device or address, ran no probe target (stdout []) and the status assertion passed on that failure. The test writes the probe into a temporary directory outside the checkout, asserts the output of the probe targets before the status and names the probe file and the stderr of make; tests/scripts/device-paths.test.mjs, in always of the owner check, fails on a device path in a tracked source. Class: dependence on the outside world (device paths of the system) | node scripts/run-tests.mjs node -- tests/scripts/failure-accumulation.test.mjs tests/scripts/device-paths.test.mjs | [o] |
Wave 20 — Tests that share nothing with other runs
| ID | Task | Verification | Done |
|---|---|---|---|
| T20.1 | Create every temporary directory and generated test of the checks and runners under the system temporary directory, never in the checkout: nine runners and scripts made their temporary directory with mkdtemp in the repository root, the Go module or the Rust crate, five made a Go package directory in packages/template-go, and six wrote a Rust integration test into packages/template-rust/tests, so tests/scripts/owner-check.test.mjs failed in 4 of 4 runs of make test-scripts on the files of another test, and a concurrent cargo test could compile a half-written test. scripts/temporary-workspace.mjs makes a Node.js workspace with a copy of the installed @polyspec/template, a Rust crate that depends on packages/template-rust by path with the versions of its lock, and a Go module that replaces the module of packages/template-go with that directory; tests/scripts/temporary-files.test.mjs fails on a script that makes such a directory or file in the checkout. Class: tests that write shared state | node scripts/run-tests.mjs node -- tests/scripts/temporary-files.test.mjs; make generated-native-check typed-generator-compile-check conformance-all-modes | [o] |
| T20.2 | Make tests/browser/run.mjs start the server with an explicit command, the Node.js of the process unless --server-command names another, and make tests/scripts/browser-server.test.mjs pass its slow wrapper that way, assert that the wrapper ran, and assert that run.mjs runs no build step instead of that packages/template-ts/dist keeps its inode: the server was looked up on PATH, so whether the test's slow stub ran depended on PATH resolution (one run started the server in 0.6 s), and the prerequisite build of T18.8-2 republished that dist during the run, so the inode assertion failed. Classes: tests that depend on the environment; races on shared state | node scripts/run-tests.mjs node -- tests/scripts/browser-server.test.mjs | [o] |
| T20.3 | Make tests/scripts/long-steps.test.mjs remove its temporary directory only after the processes that it started have exited: the case of a long package install step killed the process group of the check with SIGKILL and removed the directory at once, and failed in 1 of 4 runs of make test-scripts with ENOTEMPTY on that directory, because a process of the group still wrote into it. The helper tests/scripts/process-group.mjs sends the signal, waits for the leader to exit and polls until no process of the group is left. Class: leaked processes | node scripts/run-tests.mjs node -- tests/scripts/process-group.test.mjs tests/scripts/long-steps.test.mjs | [o] |
| T20.3-1 | Keep the directory of tests/scripts/process-group.test.mjs small: its writers created nested directories without bound, and under the load of make test-scripts the removals of 20 attempts took longer than the 30 s of one test, which failed with test timed out after 30000ms in 1 of 3 runs. The writers create directories among 50 names each, and the test has its own time limit. A helper that does not wait still fails it with ENOTEMPTY | node scripts/run-tests.mjs node -- tests/scripts/process-group.test.mjs | [o] |
| T20.1-1 | Download the crates of every Cargo.lock in make install: rustWorkspace of scripts/temporary-workspace.mjs and the generated checks and runners resolve their temporary crates with cargo --offline, which finds a crate only when an earlier cargo command of the machine downloaded it, so in one CI run the Rust runner case of tests/scripts/generated-conformance.test.mjs ran before any cargo had downloaded serde_json and failed with no matching package named serde_json found, while in another CI run it ran later and passed; with an empty temporary CARGO_HOME it fails every time. make install runs cargo fetch --locked for every tracked Cargo.lock, the checks stay offline, a failed cargo metadata --offline names run make install and rustWorkspace removes its directory when it fails. The other --offline cargo calls read crates of rustWorkspace; the Go checks read only the module proxy of their run and packages/template-go, which requires no module, and the Node.js workspaces copy installed packages, so neither reads a cache that another command fills. Classes: dependence on the outside world; races on shared state | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs; rustWorkspace with an empty temporary CARGO_HOME fails before cargo fetch --locked --manifest-path packages/template-rust/Cargo.toml into it and passes after | [o] |
| T20.1-2 | Run cargo, go and npm offline in every recipe: the cargo commands of the checks (build, clippy, rustc, test, package, run, check of the Makefile, the drivers, the package install projects and the showcase contract) ran without --offline, so with an empty CARGO_HOME a recipe's cargo metadata --locked of packages/template-rust printed Updating crates.io index and Downloading crates and passed; go read https://proxy.golang.org,direct and npm the registry. The Makefile exports CARGO_NET_OFFLINE=true, GOPROXY=off and npm_config_offline=true, and only the downloads of install, install-tools and dependency-review lift them with $(ONLINE); with an empty temporary CARGO_HOME the same recipe fails offline, and after cargo fetch --locked into it it passes. build-go, test-go-vet, lint-showcase-warnings, build-rust, test-rust-clippy, ext, test-ext-clippy, install-workspace-check, package-installs-check, contract-check and install-cli pass offline. The other network reads: go reads no module outside the checkout (packages/template-go requires none, the install projects set the file proxy of their run); npm installs only local archives and file: packages in checks; cargo's offline failure says to retry without --offline, and only rustWorkspace names make install; composer install of build-php and ext reads Packagist when vendor is absent, and test-vscode-integration downloads VS Code, both outside make install. Class: dependence on the outside world | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs; a recipe's cargo metadata --locked of packages/template-rust with an empty temporary CARGO_HOME fails offline before cargo fetch --locked into it and passes after | [o] |
| T20.1-3 | Install the Composer packages in make install and run Composer offline in the checks: build-php and build-ext-php ran composer install, which read Packagist when vendor was absent, so with an empty vendor and an empty Composer cache make build-php downloaded 24 MB and passed. scripts/composer-install.mjs <directory> runs composer install and names the directory and, with the network disabled, run make install when it fails; make install runs it for packages/template-php and packages/template-php-ext with $(ONLINE), the Makefile exports COMPOSER_DISABLE_NETWORK=1, and build-php, build-ext-php, the PHP driver and the tests that install PHPUnit run the script. With an empty vendor and an empty Composer cache make build-php fails offline with that message, and after the step of make install into that cache it passes offline. Class: dependence on the outside world | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/test-targets.test.mjs; make build-php of a copy of packages/template-php without vendor, with an empty COMPOSER_CACHE_DIR, fails naming make install and passes after the install step | [o] |
| T20.1-4 | Install the VS Code of the integration test in make install and never download it in the check: tests/integration/run.mjs downloaded VS Code 1.138.0 with @vscode/test-electron into .vscode-test on its first run, so with an empty cache directory the run validated the version at update.code.visualstudio.com and started a download of 302 MB. scripts/install-vscode.mjs <directory> downloads the copy into <directory>/.next-<pid> and renames it to vscode-<platform>-<version>, does nothing for a complete copy, and keeps the copy of a concurrent install; make install runs it for var/tools/vscode with $(ONLINE), and the run takes --vscode var/tools/vscode, starts the installed copy and fails with VS Code <version> is not installed in <directory>; run make install without it, before any download. An installed copy never changes, so the holder lock of .vscode-test, make clean-vscode-test and make vscode-test-unlock are removed. Class: dependence on the outside world | node scripts/run-tests.mjs node -- tests/scripts/install-vscode.test.mjs tests/scripts/toolchain-files.test.mjs; make test-vscode-integration passes with the installed copy | [o] |
| T20.1-5 | Name make install for a missing crate in every target that runs cargo: the recipes run cargo offline, and with an empty CARGO_HOME make build-rust failed with cargo's help: if this error is too confusing you may wish to retry without --offline. cargo-downloads-check (scripts/check-cargo-downloads.mjs) runs cargo fetch --locked --offline for every tracked Cargo.lock, which reads no network, and fails with each lock, the first error line of cargo and run make install, which downloads them; the 26 targets that run cargo depend on it, so under make -k a target whose crates are missing does not run. tests/scripts/cargo-downloads.test.mjs, in always of the owner check, finds the targets that run cargo, by $(CARGO), run-tests.mjs cargo or a file that starts cargo directly or through a declaration that it imports or a file that it names, and fails on one without the prerequisite; before the prerequisites it named those 26 targets. With an empty temporary CARGO_HOME make build-rust fails with that message and runs no cargo, and after cargo fetch --locked of the three locks into it the check passes. Class: failures that do not explain themselves | node scripts/run-tests.mjs node -- tests/scripts/cargo-downloads.test.mjs; make build-rust with an empty temporary CARGO_HOME fails naming make install and make cargo-downloads-check passes after the fetch | [o] |
| T20.1-6 | Install VS Code only where the integration test runs: after T20.1-4 make install downloaded VS Code in all nine CI jobs, and seven of them never start it. make install-vscode runs scripts/install-vscode.mjs with $(ONLINE) after make install, which no longer downloads VS Code; the release and editor jobs of ci.yml, which run make check and make test-vscode-integration, run it before the test, and the integration run without the copy fails with run make install-vscode. A case of tests/scripts/toolchain-files.test.mjs fails when make install downloads VS Code, when a job that reaches test-vscode-integration through its make targets, their prerequisites and the full suite of check lacks make install-vscode before that step, and when another job runs it; it failed on make install before the change. Class: dependence on the outside world | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/install-vscode.test.mjs | [o] |
| T20.1-7 | Wait for the extension in the VS Code integration checks without a time limit of their own: eventually of tests/integration/suite/index.cjs gave up after 100 polls of 50 ms and returned false, so under a load average of 41 the check of format on type failed after 5141 ms with + false - '<ul>\n {@ x = xs}' in 1 of 5 runs, and the message showed no text. tests/integration/suite/wait.cjs polls until the condition holds; only the 20 s timeout of the check ends a wait, stops its polling and names what it waited for, and textBecomes names the expected and the actual text of the document; the checks then compare the text of the document with the expected text. tests/integration-wait.test.mjs applies a format to a document after 5.5 s: with the former wait it failed after 5.2 s with the former text, and it passes now; it also checks the message of the timeout and that a stopped wait polls no more. The message then showed the cause of the failures under load: in 1 of 3 runs at a load average of 73 to 88 the check waited 20 s for "<ul>\n {@ x = xs}" while the document still had "<ul>\n", so the command type, which types into the focused editor, had typed nothing into the new editor; typeInto now waits until the editor is the active one before it types, and 5 of 5 runs passed at a load average of 63 to 75. Classes: dependence on time; failures that do not explain themselves | node scripts/run-tests.mjs node --cwd packages/template-vscode -- tests/integration-wait.test.mjs; make test-vscode-integration | [o] |
| T20.1-8 | Run every tool of CI through make: seven steps of ci.yml started node directly, node node_modules/@playwright/test/cli.js install --with-deps chromium in the release, browser and editor jobs and node tests/runner/conformance.mjs --langs ts, ts,go, ts,rust and ts,php in the language jobs, so the offline settings of the recipes and cargo-downloads-check did not apply to them. make install-browsers installs Chromium with $(ONLINE), make conformance-ts, conformance-go, conformance-rust and conformance-php run the conformance of TypeScript and one other language after cargo-downloads-check and the builds that they read, and the jobs run these targets. A case of tests/scripts/toolchain-files.test.mjs fails on a step of a workflow that starts node, npm, npx, cargo, go, php, composer or rustup without make, also after xvfb-run and in a step of several lines; it named the seven steps before the change. Class: dependence on the outside world | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs; make conformance-ts conformance-go conformance-rust conformance-php | [o] |
| T20.1-9 | Leave the reason of every failure of CI: only the release job uploaded anything, its record var/full-run.json, and no job kept the output of a target beyond the job log; the docs, typescript, browser, go, rust, php and pages jobs ran their targets as separate steps or one make -k. scripts/target-report.mjs writes the output of each make -k <target> to targets/<target>.log and summary.md with each target, its result and time and the first failure lines of each failed target, also to the job summary; make check writes it to var/report/full-run, and make ci-targets TARGETS="..." (scripts/ci-targets.mjs) runs every target to its end and writes var/report/ci-targets. Every job of ci.yml runs its checks through one of the two and uploads the report under if: !cancelled() with if-no-files-found: error, the release job also its record. tests/scripts/target-report.test.mjs runs a failing and a passing probe target and finds both logs and the failure line in the summary and the job summary; a case of tests/scripts/toolchain-files.test.mjs fails on a job that runs a check outside the two runners or uploads no report, and named every job before the change. Class: failures that do not explain themselves | node scripts/run-tests.mjs node -- tests/scripts/target-report.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/full-run.test.mjs | [o] |
| T20.1-10 | Run no full or end-to-end check locally: AGENTS required make owner-check before every commit, the owning command of a task, often a conformance or integration check, before a task is done, and make check before a push, and docs/operations/development.md said that CI does not run make check. AGENTS and docs/operations/development.md now state that development runs unit tests only, that no make check, make rerun-failed, make owner-check, conformance, browser or VS Code integration run happens locally before a commit or a push, and that CI runs them after the push, make check in the release job and make ci-targets in the other jobs, with a report of every failure; a task is done when its unit tests pass and CI passed its push | node scripts/run-tests.mjs node -- tests/scripts/check-documents.test.mjs | [o] |
| T20.1-11 | Fix the three failures of CI on T20.1-9: make ci-targets failed in every job before its first target with ENOENT on var/report/ci-targets.lock.<pid>.<random>, because the holder lock writes beside the report and a new checkout has no var/report, so no job uploaded a report; tests/scripts/target-report.test.mjs expected the log of a target to start with its output, while GNU Make 4 prints make[2]: Entering directory first under another make, the class of T17.1-4; and two rows of the checklist held ${{ !cancelled() }} in inline code, which VitePress evaluates as a Vue interpolation, so the build printed _ctx.cancelled is not a function for both checklist pages and still passed, as {{ msg }} of docs/operations/browser.md rendered as nothing. scripts/ci-targets.mjs creates the parent of the report before the lock; the test puts the report under a missing var/report and accepts the line of GNU Make 4, and fails with ENOENT under MAKELEVEL=1 with GNU Make 4.4.1 before the fix; scripts/check-documents.mjs fails on {{ outside a fenced code block or a v-pre element of a page under docs, which named the four lines before the fix. Classes: dependence on the machine; false greens | node scripts/run-tests.mjs node -- tests/scripts/target-report.test.mjs tests/scripts/check-documents.test.mjs | [o] |
| T20.1-12 | Let a new push cancel the running CI of its workflow and ref, because the runners are few: ci.yml cancelled runs only in the group ci-<ref>, a name that another workflow on push could reuse. Every workflow that runs on push or pull request has concurrency with the group <workflow>-<ref> and cancel-in-progress: true; dependency-review.yml runs on a schedule and by hand only. A case of tests/scripts/toolchain-files.test.mjs fails on a workflow on push or pull request without that concurrency, and failed on ci.yml before the change. Class: races on shared state | node --test --test-name-pattern="cancels the running CI" tests/scripts/toolchain-files.test.mjs | [o] |
| T20.1-13 | Run every combination of a CI matrix to its end: in CI the job PHP 8.2 failed and GitHub cancelled the job PHP 8.5, whose result was lost, because a matrix cancels its other jobs at the first failure unless fail-fast is false. The php job of ci.yml sets fail-fast: false, and a case of tests/scripts/toolchain-files.test.mjs requires it of every job with a matrix; it failed on the previous ci.yml. Kind: steps that stop at the first failure | node --test --test-name-pattern="matrix runs every combination" tests/scripts/toolchain-files.test.mjs | [o] |
Wave 21 — PHP extension in C
| ID | Task | Verification | Done |
|---|---|---|---|
| T21.1 | Build the PHP extension in C and parse templates with it: packages/template-php-ext/src holds the C sources, config.m4 and polyspec_template.stub.php with the classes TemplateError and Engine (__construct, parse, parseToJson). make ext-arginfo generates polyspec_template_arginfo.h from the stub with gen_stub.php of the PHP build; the header is committed for builds from the package. scripts/build-php-extension.mjs builds the extension with phpize, configure and make in a temporary directory with every compiler warning as an error, publishes the library with a rename only when its bytes change, skips a build whose inputs (sources, stub, PHP build) are unchanged, and fails with the fix when the header was not generated from the stub or when php-config and php are not one PHP. The parser gives the AST of all 227 conformance cases with an AST and the error of the 21 parse error cases. The Rust extension stays the extension of make ext until T21.4 | node --test tests/scripts/build-php-extension.test.mjs; node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so, then node scripts/run-tests.mjs phpunit --php-extension var/build/polyspec_template.so --cwd packages/template-php-ext -- --filter EngineTest::testParse and --filter 'EngineTest::test.*Delimiters' | [o] |
| T21.2 | Render templates with JSON data in the C extension: Engine::renderJson reads assign data, definitions and environment as JSON text in document order with the number, UTF-8 and depth rules of VAL-2, VAL-12 and VAL-20, and renders with the filesystem loader and its version cache (RT-9, RT-10, RT-40), template names (RT-7, RT-8), every statement, expression and limit of the specification and every built-in function. json of a native object writes null, like the TypeScript and Rust runtimes. All 248 conformance cases render to their expected output or error | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so, then node scripts/run-tests.mjs phpunit --php-extension var/build/polyspec_template.so --cwd packages/template-php-ext -- --filter with ExtConformanceTest, LoadFailureTest::testJson, EngineTest::testRenderJson and EngineTest::testLimits | [o] |
| T21.3 | Bind PHP values in the C extension: Engine::render binds assign data, definitions and environment given as PHP values (VAL-11, VAL-14, VAL-17 to VAL-20) and takes a bound map without binding it again (VAL-22); register and registerClass register host functions and logical class functions (FUN-43, FUN-44), which receive their arguments in the host form of VAL-21 and fail with E_RUNTIME_HOST_FUNCTION for a thrown exception, while a native TemplateError of host code passes unchanged; a native object keeps its instance, exposes its public properties and methods whatever the scope of the caller and is equal only to itself (VAL-18, VAL-19, EXP-39); BoundMap::bind and merge create bound maps that PHP code cannot instantiate, clone or unserialize (ERR-14, RT-26). The extension tests EngineTest, BoundMapTest, HostBindingTest, HostValuesTest, LoadFailureTest and ExtConformanceTest pass unchanged on the C build | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so, then node scripts/run-tests.mjs phpunit --php-extension var/build/polyspec_template.so --cwd packages/template-php-ext -- --filter with EngineTest, BoundMapTest, HostBindingTest, HostValuesTest, LoadFailureTest and ExtConformanceTest | [o] |
| T21.4 | Replace the Rust PHP extension with the C extension: make ext builds the C sources with scripts/build-php-extension.mjs into var/build/polyspec_template.so, test-ext runs the conformance cases through the command line and the PHPUnit suite on that build, the conformance runner builds the php-ext driver the same way, and make install, lint and test-ext no longer fetch, format or lint a Rust extension (lint-ext and test-ext-clippy are removed). The Rust sources, Cargo.toml, Cargo.lock, build.rs and the ext-php-rs dependency are removed; the stub moved to src/ for the PIE build path, and composer.json has the type php-ext. CliTest loads the published library instead of the cargo output, and the test that every Rust method runs inside the panic boundary is removed because the C extension has no Rust code; the other extension tests are unchanged. The README, docs/operations/development.md, docs/spec/conformance.md, the feature contract and AGENTS describe the C build | make test-ext-unit; node --test tests/scripts/build-php-extension.test.mjs tests/scripts/cargo-downloads.test.mjs tests/scripts/failure-messages.test.mjs tests/scripts/ignored-files.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/owner-check.test.mjs; CI runs make test-ext | [o] |
| T21.4-1 | Build the C extension with the GCC of the CI runner: CI failed test-ext on PHP 8.2 because GCC, unlike the clang of macOS, warned with -Werror that the locals assign and options of Engine::render might be clobbered by longjmp (-Wclobbered) and that seconds of date may be used uninitialized (-Wmaybe-uninitialized). The work of each run (parse, renderJson, render) is a function of its own, so no local of the frame that calls setjmp changes after it, and every local that a function fills through an out parameter starts initialized | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so, then make test-ext-unit; CI runs make test-ext with GCC on PHP 8.2 and 8.5 | [o] |
| T21.4-2 | Keep the inputs of a run in memory across setjmp: after T21.4-1, CI still failed test-ext on PHP 8.2 and 8.5 with GCC -Wclobbered for assign and options of Engine::render, which the parameter parsing sets conditionally in the frame that calls setjmp. Every run goes through pt_run_work, the one function that calls setjmp; it takes the work and a pointer to a structure of its inputs, so no input of a method is a register local of that frame | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so, then make test-ext-unit; CI runs make test-ext with GCC on PHP 8.2 and 8.5 | [o] |
Wave 22 — Tag releases
| ID | Task | Verification | Done |
|---|---|---|---|
| T22.1 | Release a tagged commit of main: every change reaches main through the merge queue with the required checks, and the maintainer releases by tagging a commit of main vX.Y.Z (the Go module packages/template-go/vX.Y.Z) after a version-bump pull request. The changelog keeps ## Unreleased above the released versions, the Go module path matches its directory, the job ci-passed of ci.yml passes only when every other job passed and the ruleset main requires exactly push-gate and ci-passed, and .github/workflows/release.yml checks the tagged commit and creates the GitHub Release with the package archives (T22.1-1, T22.1-2, T22.1-3, T22.1-4, T22.1-5) | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/github-ruleset.test.mjs tests/scripts/release.test.mjs, make docs-check | [o] |
| T22.1-1 | Keep ## Unreleased above the released versions in CHANGELOG.md and CHANGELOG.ko.md: the newest entries stood without a heading above the language link, and ## Unreleased held the oldest entries, including those of the tag v0.0.1. The language link stands below the title, the entries of v0.0.1 form ## 0.0.1, the entries after it stay under ## Unreleased, and every change adds its entry there | make docs-check | [o] |
| T22.1-2 | Name the Go module after its directory: packages/template-go/go.mod declared the module github.com/polyspec/template, which go get looks for at the root of the repository, where no go.mod is. The module is github.com/polyspec/template/packages/template-go, every import of the package, its tests, the showcase adapter, the Go backend of the compiler with the generated programs and their manifests, the example page, the generated checks and the documents use that path, and a tag packages/template-go/vX.Y.Z releases it. tests/scripts/toolchain-files.test.mjs fails when a tracked go.mod whose module path has a domain does not name github.com/polyspec/template/<its directory> | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs, make artifact-digest-check | [o] |
| T22.1-3 | Require one check for ci.yml: the ruleset main named every job of ci.yml (release, docs, typescript, browser, editor, go, rust, php (8.2), php (8.5)), so a job added to ci.yml was not required until the ruleset named it. The last job ci-passed needs every other job, runs under if: ${{ always() }} on ubuntu-24.04 and runs make ci-passed RESULTS='${{ toJSON(needs) }}' in a step without a condition of its own; scripts/ci-passed.mjs fails unless every needed job has the result success, naming each other job and result. The ruleset requires exactly push-gate and ci-passed. tests/scripts/toolchain-files.test.mjs fails when ci-passed is missing, is not the last job, lacks if: ${{ always() }}, does not need every other job, runs on another runner or runs another step | node scripts/run-tests.mjs node -- tests/scripts/ci-passed.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/github-ruleset.test.mjs tests/scripts/suite-parity.test.mjs | [o] |
| T22.1-4 | Release a tag through a workflow: no workflow ran on a tag, so a release had no check that its commit passed the required checks on main, that the manifests and the changelog carried its version, or that its archives were built from it. .github/workflows/release.yml, which is not the job release of ci.yml, runs on the push of a tag v* or */v* with the permission contents: write and runs make release-verify (the tagged commit is an ancestor of origin/main, and the latest check runs push-gate and ci-passed of the commit concluded success), make release-versions (X.Y.Z in every manifest of MANIFESTS, a composer.json without version taking the tag, and ## X.Y.Z in CHANGELOG.md; the module path of packages/template-go/go.mod for packages/template-go/vX.Y.Z), make release-assets (npm pack of the built npm packages, git archive zips of the Composer packages and cargo package --no-verify --locked, named <package name>-<version>.<ext>) and make release-publish (gh release create --verify-tag with the section as notes); scripts/release.mjs names each failed check and each file with both versions. tests/scripts/release.test.mjs runs the steps against fakes of gh, npm and cargo and requires every tracked manifest to be released or declared as not released; tests/scripts/toolchain-files.test.mjs requires the trigger, the permission and the order of the steps. AGENTS states the release procedure | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/suite-parity.test.mjs | [o] |
| T22.1-5 | Release the Go module tags at any depth and only npm and Composer archives: in a tag filter * does not match /, so the trigger tags: ['v*', '*/v*'] missed packages/template-go/vX.Y.Z, and make release-assets built a .crate with cargo package, which rewrites git dependencies into crates.io requirements that do not resolve, and built the npm packages for a Go module tag too. The trigger is tags: ['v*', '**/v*'], and the release assets are npm tarballs and Composer zips only; MANIFESTS of scripts/release.mjs lists packages/template-rust/Cargo.toml as not released as an archive; consumed by git tag. A Go module tag builds and attaches nothing: release-assets depends on the npm builds only for a tag without /, and its steps start no cargo. tests/scripts/release.test.mjs fails on a Cargo archive, a Cargo manifest with another release, or an npm build for a Go module tag, and tests/scripts/toolchain-files.test.mjs fails on the trigger */v* | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs tests/scripts/toolchain-files.test.mjs, make docs-check | [o] |
| T22.2 | Release 0.0.2: every manifest of MANIFESTS of scripts/release.mjs (the root package.json, the npm packages template-ts, template-language, template-lsp and template-codemirror, the VS Code extension and the Rust crate) declares 0.0.2, the dependencies between the npm packages of the repository and the version of the C extension name 0.0.2, package-lock.json, packages/template-rust/Cargo.lock and tools/showcase/adapters/rust/Cargo.lock record 0.0.2 for the packages of the repository, and ## Unreleased of CHANGELOG.md and CHANGELOG.ko.md becomes ## 0.0.2 below a new empty ## Unreleased. scripts/check-package-installs.mjs read the Rust crate archive polyspec-template-0.0.1.crate, the stubs of npm pack and cargo package of tests/scripts/package-installs.test.mjs wrote archives of version 0.0.1, and tests/scripts/release.test.mjs checked the tag v0.0.1; the check reads the version of packages/template-rust/Cargo.toml, the stubs name their archives after the version of the packed manifest, and the release test checks the version of the root package.json. Red: make release-versions failed for v0.0.2 with version 0.0.1 in seven manifests and no section ## 0.0.2, and for packages/template-go/v0.0.2 with no section ## 0.0.2; with the version 0.0.2, tests/scripts/package-installs.test.mjs failed for TypeScript and Rust and tests/scripts/release.test.mjs failed for the tag v0.0.1. Green: all pass | TAG=v0.0.2 make release-versions, TAG=packages/template-go/v0.0.2 make release-versions, node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs tests/scripts/package-installs.test.mjs, make docs-check | [o] |
| T22.2-1 | Pass CI with the version 0.0.2: the compiler digest covers the version of packages/template-ts, so the AST manifests of examples/site/scenarios and the JavaScript manifests of tools/showcase/adapters/generated/javascript recorded the digest of the compiler of 0.0.1, config/dependency-review.json recorded the sha256 of the lock before the version change, and tests/scripts/dependency-policy.test.mjs set packages/template-lsp to 0.0.2 to make it differ from the lock. scripts/dependency-review.mjs read npm view --json as one object, and npm 12 prints several fields as an array of one object, so the review failed for every npm dependency. make showcase compiles the artifacts again; the review reads both forms, the npm stub answers in the form of npm 12, and make dependency-review UPDATE=1 raises typescript-eslint to 8.71.1 and source-map-js to 1.2.2 (advisory GHSA-68fv-2mgg-jv7q) and records the review; the gate test sets a version that differs from the tree. Red: make artifact-digest-check named 10 of 30 artifacts, make dependency-policy-state-check named the changed lock, tests/scripts/artifact-digests.test.mjs and tests/scripts/dependency-policy.test.mjs failed, and with the npm 12 stub the review tests failed. Green: all pass | make artifact-digest-check, make dependency-policy-state-check, make typed-generator-check, make contract-check, node scripts/run-tests.mjs node -- tests/scripts/artifact-digests.test.mjs tests/scripts/dependency-policy.test.mjs | [o] |
| T22.2-2 | Keep the release notes within the limit of GitHub: make release-publish passed the whole section ## X.Y.Z of CHANGELOG.md as the body of gh release create, and GitHub refuses a release body over 125000 characters. NOTES_LIMIT of scripts/release.mjs is 125000 characters; releaseNotes keeps a section of at most NOTES_LIMIT characters whole and replaces a longer one with one line that links CHANGELOG.md at the tag, with the tag encoded per path segment and the anchor of ## X.Y.Z, the version without its dots. Red: tests/scripts/release.test.mjs failed without NOTES_LIMIT and releaseNotes. Green: a section over the limit gives the line with the link for vX.Y.Z and for packages/template-go/vX.Y.Z, a section at the limit stays whole, and publish sends the line | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, make docs-check | [o] |
| T22.2-3 | Install the release assets outside the repository: packages/template-php/composer.json and packages/template-php-ext/composer.json declared no version, so an artifact repository of Composer could not read their release zips, and no check read the packed manifests. A consumer downloads the release assets and installs them together, so each published manifest names its polyspec dependencies by exact version, and each archive carries it unchanged. Each published composer.json declares the version of the release, its composer.lock records the content hash, make release-versions requires the version of every composer.json, and composer validate of scripts/check-dependency-policy.mjs does not report the version field (--no-check-version). checkAssets of scripts/release.mjs, which make release-assets runs, fails on a packed manifest that differs from its manifest at the tagged commit, on an @polyspec/* dependency of dependencies, peerDependencies or optionalDependencies, or a polyspec/* package of require, that is a path (file:, link:, workspace:), a git source, a URL, a range, @dev or another version than the release, and on a composer.json without the version or with repositories; treeManifestProblems applies the same rules to the published manifests of the tree. docs/operations/publication.md states the install with file: tarballs and with an artifact or package repository, and the development layout. Red: tests/scripts/release.test.mjs failed without checkAssets, manifestProblems, packedManifest and treeManifestProblems, make release-versions passed a composer.json without a version, and Composer did not install polyspec/template from an artifact repository of the zips. Green: the packed assets of the tree install outside the repository with npm install from an empty cache and with an artifact repository, and a changed or repository-only manifest fails the check | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, make dependency-policy-state-check, make docs-check | [o] |
| T22.3 | Release 0.0.3: every manifest of MANIFESTS of scripts/release.mjs (the root package.json, the npm packages template-ts, template-language, template-lsp and template-codemirror, the VS Code extension, the Composer packages template-php and template-php-ext and the Rust crate) declares 0.0.3, the dependencies between the npm packages of the repository and the version of the C extension name 0.0.3, package-lock.json, the composer.lock of each Composer package, packages/template-rust/Cargo.lock and tools/showcase/adapters/rust/Cargo.lock record 0.0.3 or the content hash of their manifest, and ## Unreleased of CHANGELOG.md and CHANGELOG.ko.md becomes ## 0.0.3 below a new empty ## Unreleased. make showcase compiles the AST and JavaScript manifests for the compiler digest of 0.0.3, and make dependency-review RECORD=1 records the changed locks. Red: make release-versions failed for v0.0.3 with version 0.0.2 in nine manifests and no section ## 0.0.3. Green: make release-versions passes for v0.0.3 and packages/template-go/v0.0.3, and make artifact-digest-check and make dependency-policy-state-check pass | TAG=v0.0.3 make release-versions, TAG=packages/template-go/v0.0.3 make release-versions, make artifact-digest-check, make dependency-policy-state-check, node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs tests/scripts/package-installs.test.mjs | [o] |
| T22.3-1 | Install the release assets from committed consumer projects: the install test of tests/scripts/release.test.mjs wrote its npm and Composer projects at run time without a lock, and git archive of a tree wrote the current time into each zip, so no lock could pin a zip. tests/fixtures/release-consumer holds an npm project (package.json, package-lock.json) that names the tarballs of @polyspec/template and @polyspec/template-language as file: and a Composer project (composer.json, composer.lock) with an artifact repository of the zips; make release-consumer-lock (scripts/release-consumer.mjs) writes both locks from the archives of the manifests of the tree, committed at a fixed date with placeholder files, so a lock changes only with the manifests. The test copies each project and the archives into a directory outside the repository and runs npm ci with an empty cache and the scope @polyspec on an unreachable registry and composer install with an empty COMPOSER_HOME and cache. make release-assets archives each Composer package at the time of the tagged commit (--mtime). AGENTS states that a registry query whose result depends on time never runs in a check and that a download pinned by a lock is installation. The change of T22.2-3 removed nine cases of tests/scripts/release.test.mjs (a Go tag, publish, the declarations of the tracked manifests, the kinds of the assets, the version of the tree, the make steps and the notes limit); they run again, and NOT_RELEASED declares the two consumer manifests. Red: the Composer project failed on the shasum of its lock and the zips of two runs differed. Green: npm ci and composer install install from the committed locks with empty caches, and the zips are the same bytes on every run | make release-consumer-lock, node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, TAG=v0.0.3 make release-versions, make docs-check | [o] |
| T22.3-2 | Install every npm package of the release from the built archives: the npm consumer project installed @polyspec/template and @polyspec/template-language only, the archives under test held placeholder files so that their hashes stayed stable, and AGENTS said that a check reads no download. The npm consumer project of tests/fixtures/release-consumer installs @polyspec/template, @polyspec/template-language, @polyspec/template-lsp and @polyspec/template-codemirror, and its lock pins their registry dependencies, the peer dependencies of @polyspec/template-codemirror included, by exact version and integrity. scripts/release-consumer.mjs packs the built packages of the tree with packArchives of scripts/release.mjs, the step of make release-assets, and its locks record the archives under test by name and version only, without integrity in npm and with an empty shasum in Composer, so a lock changes only with a version or a dependency. npm ci of the test runs online with --offline=false, make release-consumer-lock runs with $(ONLINE) after the npm builds, and make test-scripts builds template-codemirror. AGENTS and the Makefile state that a registry query whose result depends on time never runs in a check and that a download pinned by a lock by exact version and integrity is installation, which a check may run. Red: the committed consumer project named two of the four tarballs, and its locks recorded the hashes of the placeholder archives. Green: npm ci installs the four tarballs and their pinned registry packages, composer install installs polyspec/template, and make release-consumer-lock writes the same locks again | make release-consumer-lock, node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, TAG=v0.0.3 make release-versions, make docs-check | [o] |
| T22.3-3 | Publish the compiler as the npm package @polyspec/template-compiler: the compiler modules and the function contract were files of the repository that no release asset carried, so a consumer could not declare them as a dependency. packages/template-compiler holds the compiler modules, its backends, interface.json and the one function contract functions.json; its package.json exports compiler.mjs, ast-artifact.mjs, type-manifest.mjs and functions.json and depends on @polyspec/template by exact version, and ast-artifact.mjs imports the parser from @polyspec/template. The scripts, runners, showcase tools and the interface tests of every language read the package directory. The compiler digest names the files of the package by their paths in the package; in the repository it reads the sources of packages/template-ts without a build, and installed it reads the manifest and dist of the installed @polyspec/template. tools/showcase/delivery-digest.mjs computes the digest of the JavaScript delivery of the showcase. scripts/release.mjs packs the package (PACKAGES, MANIFESTS), and the npm consumer project of tests/fixtures/release-consumer installs its tarball with npm ci, imports its three entry points and functions.json by the package name and compiles a template into an AST graph. Red: no release asset carried the compiler, and the npm consumer project named four tarballs. Green: make release-assets packs polyspec-template-compiler-X.Y.Z.tgz, its packed manifest passes the published-manifest check, and the consumer project installs it from the tarball and compiles with it | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, make test-scripts, make artifact-digest-check, make docs-check, make rules-check | [o] |
| T22.3-4 | Keep the moved compiler paths valid for the documentation site and the Rust format check: the spec and operations pages linked interface.json and functions.json by the relative path ../../packages/template-compiler/..., which the documentation site does not serve, so scripts/check-docs-static.mjs failed with link points to a source-only or legacy route, and two manifest path lines of packages/template-rust/tests/compiler_interface.rs exceeded the cargo fmt width, so make lint-rust failed. The pages link the files at https://github.com/polyspec/template/blob/main/packages/template-compiler/, as they link other source files, and cargo fmt formats the test. Red: CI run 37623320990 failed lint-rust and the static documentation check. Green: make lint-rust, make docs-check and make docs-static-check pass | make lint-rust, make docs-check, make docs-static-check | [o] |
| T22.3-5 | Release 0.0.4: every manifest of MANIFESTS of scripts/release.mjs (the root package.json, the npm packages template-ts, template-language, template-lsp, template-codemirror and template-compiler, the VS Code extension, the Composer packages template-php and template-php-ext and the Rust crate) declares 0.0.4, the dependencies between the npm packages of the repository and the version of the C extension name 0.0.4, package-lock.json, the composer.lock of each Composer package, packages/template-rust/Cargo.lock and tools/showcase/adapters/rust/Cargo.lock record 0.0.4 or the content hash of their manifest, the consumer projects of tests/fixtures/release-consumer install the archives of 0.0.4 with the locks that make release-consumer-lock writes, and ## Unreleased of CHANGELOG.md and CHANGELOG.ko.md becomes ## 0.0.4 below a new empty ## Unreleased. make showcase compiles the AST and JavaScript manifests for the compiler digest of 0.0.4, and make dependency-review RECORD=1 records the changed locks. Red: make release-versions failed for v0.0.4 with version 0.0.3 in ten manifests and no section ## 0.0.4. Green: make release-versions passes for v0.0.4 and packages/template-go/v0.0.4, and make artifact-digest-check and make dependency-policy-state-check pass | TAG=v0.0.4 make release-versions, TAG=packages/template-go/v0.0.4 make release-versions, make artifact-digest-check, make dependency-policy-state-check, node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs | [o] |
| T22.3-6 | Update @codemirror/language to its latest stable release: config/dependency-policy.json kept @codemirror/language at 6.12.4 with an exception, because 6.13.0 imports @codemirror/streamparser without declaring it, so npm does not install it and the CodeMirror adapter does not load. 6.13.1 declares @codemirror/streamparser as a dependency. The exception is removed, make dependency-review UPDATE=1 RECORD=1 raises @codemirror/language to 6.13.1 in package.json and package-lock.json, which installs @codemirror/streamparser 6.0.0, and records the review; the review cases of tests/scripts/dependency-policy.test.mjs no longer list a newer @codemirror/language among the dependencies that an exception keeps. make showcase compiles the committed artifacts again for the lock. Red: without the exception, make dependency-review reported @codemirror/language 6.12.4 < 6.13.1: a newer stable release exists. Green: the review finds no newer stable release without an exception, and make test-codemirror passes with 6.13.1 | make dependency-review, make test-codemirror, make dependency-policy-check, make artifact-digest-check, node scripts/run-tests.mjs node -- tests/scripts/dependency-policy.test.mjs | [o] |
| T22.4 | Add a Python implementation: packages/template-python holds the engine, the built-in functions and the command line interface as the package polyspec-template under the import name polyspec.template, the conformance and parity runners and the CI cover it like every other language, and the compiler emits Python for a generated program | T22.4-1, T22.4-2, T22.4-3, T22.4-4, T22.4-5, T22.4-6 and the CI verification T22.4-7 (open) | make test-python, make conformance-python, make conformance-generated-python, make docs-check |
| T22.4-1 | Hold the package: pyproject.toml names polyspec-template with requires-python >= 3.11 and pinned setuptools, the namespace package polyspec.template ships py.typed, .python-version names the Python minor release of config/toolchain.json, and the package owns its README(.ko), AGENTS.md(.ko), CHANGELOG(.ko) and documentation manifest | packages/template-python/**, .python-version, config/toolchain.json | make docs-check |
| T22.4-2 | Implement the engine: the lexer, parser and renderer of the TypeScript binding as parse, analyze, analyze_prefix, AstProgram, Engine, bind, merge, parse_json, the built-in functions of the shared contract and TemplateError with the shared codes and byte spans; the unit tests hold the API and the string, number and date rules | packages/template-python/** | make test-python |
| T22.4-3 | Implement the command line interface of CNF-4: python -m polyspec.template.cli parse FILE [--root DIR] [--delimiters OC] and render FILE [--data F] [--define F] [--env F] [--root DIR] [--delimiters OC] print the AST as JSON and the rendered document, and a template error prints its fields as JSON to standard error and exits with status 2 | packages/template-python/** | make conformance-python |
| T22.4-4 | Register the language: drivers.mjs drives the Python CLI, conformance-generated-python.mjs runs the generated program of every case through a Python process, the compiler backend backends/python.mjs emits the module of a source graph, contracts/features.json and the compiler interface name Python among the languages, the function contract reads the Python arity table, and the delimiter matrix runs it | tests/runner/**, packages/template-compiler/**, contracts/features.json, scripts/check-function-contract.mjs | make conformance-generated-python, make function-contract-check |
| T22.4-5 | Run it in CI and release it: the python job of ci.yml runs the unit tests, the conformance and the generated conformance on 3.11 and the pinned minor, CHECK_TARGETS holds the three targets, the owner map owns the new paths, and the release version check reads pyproject.toml with the other manifests | .github/workflows/ci.yml, Makefile, scripts/owner-checks.json, scripts/release.mjs, tests/scripts/release.test.mjs | node --test tests/scripts/ |
| T22.4-6 | Document the Python package in the repository README and the usage guide: README.md and docs/guide.md name Python among the implementations, the guide gives a render example checked against the names of polyspec.template, and the package README gives the install line for the first tag that contains packages/template-python (v<X.Y.Z>) and the checkout install until that tag exists | README.md, README.ko.md, docs/guide.md, docs/guide.ko.md, packages/template-python/README.md, packages/template-python/README.ko.md, CHANGELOG.md, CHANGELOG.ko.md | make docs-check |
| T22.4-7 | Check the python job of ci.yml with a unit test: it sets up Python 3.11 and the minor of .python-version, runs test-python, conformance-python and conformance-generated-python, and ci-passed needs it, so a failing Python run fails the check that the release requires; the CI run of the push to main is the run evidence of the done rows T22.4 and T22.4-1 to T22.4-6 | .github/workflows/ci.yml, tests/scripts/toolchain-files.test.mjs | node --test tests/scripts/toolchain-files.test.mjs |
| T22.4-8 | Pin the actions/setup-python action of the python job to the commit that its version tag names: ci.yml pins ece7cb06caefa5fff74198d8646806c4678c61a1 for v6.3.0, which GitHub does not hold; the pin is ece7cb06caefa5fff74198d8649806c4678c61a1, and tests/scripts/action-pins.test.mjs checks every action pin of every workflow against the commit of its version | .github/workflows/ci.yml, tests/scripts/action-pins.test.mjs, scripts/owner-checks.json | node --test tests/scripts/action-pins.test.mjs |
| T22.4-9 | Record the Python support of each feature only where its verification runs Python: contracts/features.json names unsupported for a feature whose verification commands run no Python, and partial for one whose commands run Python until the CI merge-group run of T22.4-7 passes; tests/scripts/feature-python-coverage.test.mjs derives the coverage from the Makefile and the scripts of each command | contracts/features.json, docs/features.md, docs/features.ko.md, tests/scripts/feature-python-coverage.test.mjs, scripts/owner-checks.json | node --test tests/scripts/feature-python-coverage.test.mjs |
| T22.4-10 | Export every public name of polyspec.template in __all__: ParseOptions, analyze and analyze_prefix are public and missing, PreparedRender appears twice, and every name that the documents import from polyspec.template must be exported; packages/template-python/tests/test_exports.py checks these | packages/template-python/src/polyspec/template/__init__.py, packages/template-python/tests/test_exports.py, Makefile | make test-python |
| T22.4-11 | Format the package unit test file in the style of the package: packages/template-python/tests/test_api.py has a blank line after nearly every line, so it is reformatted without changing any statement; the abstract syntax tree of the file is unchanged and make test-python passes | packages/template-python/tests/test_api.py | make test-python |
| T22.4-12 | Keep the python job of ci.yml on the pinned minor of .python-version: the job matrix holds 3.11 and the minor of .python-version, which the other jobs read from their own version files, and tests/scripts/toolchain-files.test.mjs fails when the matrix omits that minor | .github/workflows/ci.yml, tests/scripts/toolchain-files.test.mjs | node --test tests/scripts/toolchain-files.test.mjs |
| T22.4-13 | Keep the language test matrix consistent with the Python support: contracts/features.json lists Python in verification_matrix.languages while its semantic features declare Python partial, so node scripts/check-language-test-matrix.mjs fails; the matrix lists the four languages that its checker requires until the CI merge-group run of T22.4-7 passes, and Python joins it then | contracts/features.json, scripts/check-language-test-matrix.mjs | node scripts/check-language-test-matrix.mjs |
| T22.4-14 | Declare the py.typed marker of polyspec.template under its package name: pyproject.toml names template in package-data, which names no package under src, and the key is polyspec.template; tests/test_package_data.py checks that every key names a package and that polyspec.template declares py.typed, and make test-python runs it | packages/template-python/pyproject.toml, packages/template-python/tests/test_package_data.py, Makefile | make test-python |
| T22.4-15 | Document the Python release form: the package polyspec-template is consumed by git tag like the Rust crate, because no registry holds it; publication.md gives its local install line and its tag install line, and AGENTS.md lists packages/template-python/pyproject.toml among the version-bump manifests | docs/operations/publication.md, docs/operations/publication.ko.md, AGENTS.md, AGENTS.ko.md | node scripts/check-documents.mjs |
| T22.4-16 | Keep the test-python target to one command: the CI job scripts fails tests/scripts/failure-accumulation.test.mjs because the target runs three commands, and every target of the full suite runs at most one command; the target runs the three unit tests in one loop that keeps the failure status of each | Makefile | node --test tests/scripts/failure-accumulation.test.mjs |
| T22.4-17 | Regenerate the committed compiled artifacts after the contract change of T22.4-9 and T22.4-13: the contract digest of the compiled AST manifests and of the generated manifests no longer matches the contract, so the CI jobs generated and showcase fail generated-artifact-check, typed-generator-check, contract-check and showcase-site-check; the regeneration writes make showcase-compile, make typed-generator and node tools/showcase/build-site.mjs | examples/site, tools/showcase/adapters, examples/site/index.html | node scripts/check-generated-artifact.mjs |
| T22.4-18 | Add Python to the language test matrix now that the CI merge-group run of T22.4-7 passes: contracts/features.json verification_matrix.languages and scripts/check-language-test-matrix.mjs name python with the other four languages, and the Python support of the semantic features that the matrix covers is no longer partial where the matrix requires it | contracts/features.json, scripts/check-language-test-matrix.mjs | node scripts/check-language-test-matrix.mjs |
| T22.4-19 | Show Python on the features page: scripts/features/build.mjs lists python among the clients beside go, php, rust and typescript, so the Python statuses of contracts/features.json reach docs/features.md and docs/features.ko.md | scripts/features/build.mjs, docs/features.md, docs/features.ko.md | node scripts/features/build.mjs --check |
| T22.4-19-1 | Declare the Python run of each verification command in contracts/features.json ("python": true) instead of finding python in the words of the Makefile and the scripts: the features page lists python as a client, so the words matched the Python coverage check for docs-check, and the CI job scripts failed in feature-python-coverage. Red: the CI job scripts on the head before this row. Green: the declared commands are make conformance-all-modes, make delimiter-matrix and make release-test-matrix; template-function-contract declares python unsupported, as its command reads the Python sources without running Python. | contracts/features.json, tests/scripts/feature-python-coverage.test.mjs, docs/features.md, docs/features.ko.md | node --test tests/scripts/feature-python-coverage.test.mjs |
| T22.4-20 | Cover Python in the install and generated checks: check-package-installs.mjs (install-check), check-generated-native-calls.mjs, check-typed-generator.mjs, check-generated-arguments.mjs, check-generated-bound-data.mjs and check-showcase-contract.mjs get a Python entry derived from their Go, Rust and PHP entries, and check-generated-typed-values.mjs, which runs the same four languages, gets one too. A check that fails for Python is fixed in the implementation, never recorded as unsupported. Python runs as python<minor> of .python-version (scripts/python-toolchain.mjs), and tests/scripts/python-check-coverage.test.mjs fails for a check without a Python entry | scripts/python-toolchain.mjs, scripts/check-package-installs.mjs, scripts/check-generated-native-calls.mjs, scripts/check-generated-typed-values.mjs, scripts/check-generated-arguments.mjs, scripts/check-generated-bound-data.mjs, scripts/check-typed-generator.mjs, scripts/check-showcase-contract.mjs, tests/scripts/python-check-coverage.test.mjs | node --test tests/scripts/python-check-coverage.test.mjs |
| T22.4-20-1 | Cover Python in scripts/check-generated-typed-values.mjs, which compiles tests/fixtures/typed-values to Python and renders it with the host function describe. Red: the Python entry fails with in requires a list, map or string on the right, because the Python backend emits _generated_bind_assign with if True: (emitRuntime sets dynamic = true), so a typed root leaves every field of Assign unset. Green: emitRuntime uses context.program.dynamicRoot. The Python interpreter is python<minor> of .python-version, and the CI jobs scripts, generated and showcase set it up, which python-check-coverage.test.mjs checks | scripts/python-toolchain.mjs, scripts/check-generated-typed-values.mjs, packages/template-compiler/backends/python.mjs, .github/workflows/ci.yml, scripts/owner-checks.json, tests/scripts/python-toolchain.test.mjs, tests/scripts/python-check-coverage.test.mjs | node scripts/check-generated-typed-values.mjs |
| T22.4-20-2 | Cover Python in scripts/check-generated-arguments.mjs: a request whose assign or definition data does not match the declared types fails with a ValueError, the argument error of Python, which is no TemplateError (ERR-13). Red: the Python entry reports template E_INTERNAL, because GeneratedProgram.prepare ran the binding inside internal_boundary, which turns every ValueError into E_INTERNAL. Green: prepare binds outside that boundary, which only render keeps; docs/spec/errors.md names the Python argument error | scripts/check-generated-arguments.mjs, packages/template-compiler/backends/python.mjs, docs/spec/errors.md, docs/spec/errors.ko.md, tests/scripts/python-check-coverage.test.mjs | node scripts/check-generated-arguments.mjs |
| T22.4-20-3 | Cover Python in scripts/check-generated-bound-data.mjs: the generated Python program renders a bound map (bind, merge) as assign and as definition data with the bytes of the host map (VAL-22, RT-61). Red: the Python entry fails with 'dict' object has no attribute 'k' for {= m.k}, because the backend emitted an attribute read for the member of a typed record, which the generated program holds as a map of its present fields. Green: the member of a typed record reads through _generated_member, which returns null for a null record and an absent optional field, as ?. does in TypeScript and ?-> in PHP | scripts/check-generated-bound-data.mjs, packages/template-compiler/backends/python.mjs, tests/scripts/python-check-coverage.test.mjs | node scripts/check-generated-bound-data.mjs |
| T22.4-20-4 | Cover Python in scripts/check-generated-native-calls.mjs: the generated Python program calls members and class functions of an assigned host object (NativeObject), passes the object unchanged to included templates, blocks and definition data, and reports the error matrix and the host argument values (VAL-18, VAL-21, EXP-39) as the other languages do | scripts/check-generated-native-calls.mjs, tests/scripts/python-check-coverage.test.mjs | node scripts/check-generated-native-calls.mjs |
| T22.4-20-8 | Make the generated Python program a Program that Engine renders: Engine(GeneratedProgram()).render(target, assign) failed with 'RenderOptions' object has no attribute 'get', because GeneratedProgram read its options as a dict, while Engine and AstProgram take the RenderOptions of the package. Red: the Python entries of the four generated checks render through Engine with RenderOptions. Green: GeneratedProgram extends Program, _PreparedRender extends PreparedRender, and both read options.define and options.env; the generated Python conformance runner passes RenderOptions | packages/template-compiler/backends/python.mjs, tests/runner/conformance-generated-python.mjs, scripts/check-generated-typed-values.mjs, scripts/check-generated-arguments.mjs, scripts/check-generated-bound-data.mjs, scripts/check-generated-native-calls.mjs | node scripts/check-generated-typed-values.mjs |
| T22.4-20-5 | Cover Python in scripts/check-package-installs.mjs: an install project outside the checkout builds the wheel of a copy of packages/template-python with pip wheel --no-index, installs only that wheel into a new virtual environment, and renders the scenario scope-precedence in AST mode (FsLoader) and with the committed generated Python program to the bytes of expected.html. The build backend of pyproject.toml comes from var/python/wheelhouse, which make install fills through scripts/python-wheelhouse.mjs from packages/template-python/build-requirements.json (exact version, file and SHA-256 digest), so the check reads no index. Red: the Python entry is missing from check-package-installs.mjs, which python-check-coverage.test.mjs reports. The commands of contracts/features.json that run Python (make generated-native-check, make typed-generator-compile-check, make showcase-check) declare "python": true, and the features that they verify declare python partial | scripts/check-package-installs.mjs, scripts/python-wheelhouse.mjs, packages/template-python/build-requirements.json, Makefile, contracts/features.json, docs/features.md, docs/features.ko.md, docs/operations/development.md, docs/operations/development.ko.md, tests/scripts/python-wheelhouse.test.mjs, tests/scripts/toolchain-files.test.mjs, tests/scripts/python-check-coverage.test.mjs | node scripts/check-package-installs.mjs |
| T22.4-20-9 | Regenerate the features pages and the showcase artifacts after the Python checks and the compiler interface mapping are on one tree, so the contract digests of the committed artifacts name the compiler of the tree | docs/features.md, docs/features.ko.md, examples/site/, tools/showcase/adapters/generated/ | node scripts/check-artifact-digests.mjs |
| T22.4-20-6 | Add the Python implementation of the showcase adapter contract and cover it in scripts/check-showcase-contract.mjs: interface.json maps python (python.py, generated/render_adapter.py, snake case operation names), generate-showcase-contract.mjs writes the Python declarations (RenderAdapter with dataclasses), tools/showcase/adapters/python.py implements Adapter over Engine with AstProgram or the generated program, compile-generated.mjs compiles the five scenarios to generated/typed/<id>.py, and the checker parses the Python sources and runs the adapter on every scenario in AST and generated mode. Red: node scripts/check-showcase-contract.mjs fails with python does not declare support levels. The interface change updates the contract digest of the compiled artifacts, which are regenerated | packages/template-compiler/interface.json, scripts/generate-showcase-contract.mjs, scripts/check-showcase-contract.mjs, tools/showcase/adapters/python.py, tools/showcase/adapters/generated, tools/showcase/compile-generated.mjs, examples/site, docs/operations/showcase.md, docs/operations/showcase.ko.md, tests/scripts/python-check-coverage.test.mjs | node scripts/check-showcase-contract.mjs |
| T22.4-20-7 | Cover Python in scripts/check-typed-generator.mjs: the committed Python programs of the five scenarios (generated/typed/<id>.py) parse, render the scenario through Engine with the bytes of expected.html, use the shared runtime semantics (RuntimeBindings, _runtime.binary, _runtime.call) and keep a positioned E_RUNTIME_LIMIT template error when the output limit is exceeded. Red: test_output_limit_is_a_positioned_template_error of packages/template-python/tests/test_api.py fails, because Output.write called the limit callback, which returns the error, and never raised it, so a render beyond outputBytes returned its full output. Green: Output.write raises the returned error | scripts/check-typed-generator.mjs, packages/template-python/src/polyspec/template/output.py, packages/template-python/tests/test_api.py, tools/showcase/adapters/generated/typed, docs/operations/showcase.md, docs/operations/showcase.ko.md, tests/scripts/python-check-coverage.test.mjs | node scripts/check-typed-generator.mjs |
| T22.4-21 | Add a Python lint and format check: make lint-python (scripts/kit/lint-python.mjs, ruff.pyproject of config/toolchain.json) runs ruff check and ruff format --check on packages/template-python with the ruff==0.16.10 pin of the dev extra of its pyproject.toml, which make install-tools installs into var/tools/python, and the rules E4, E7, E9 and F and the target py311 come from its [tool.ruff] tables. lint-python is in CHECK_TARGETS and runs in the CI job showcase; make test-python runs the python3 of PATH, or PYTHON. Red: make lint-python reported 21 unused imports and 36 files that ruff format changes. Green: the imports are removed, the package is formatted with no change to its statements, make lint-python and make test-python pass, and the review record holds setuptools==84.0.0 and ruff==0.16.10 | Makefile, packages/template-python, config/toolchain.json, config/dependency-review.json | make lint-python |
| T22.4-22 | Test the Python install from the tag: config/release.json releases packages/template-python/pyproject.toml by git tag (manifests) and declares its install (proof.gitTag: pip install "polyspec-template @ git+https://github.com/polyspec/template@vX.Y.Z#subdirectory=packages/template-python" into a new virtual environment, then python -c "import polyspec.template"), which make release-proof TAG=vX.Y.Z runs after the release exists, and tests/scripts/release-config.test.mjs checks that the packages, the crate, the Python package and the Go module of config/release.json are the names that their manifests declare. The consumer projects of tests/fixtures/release-consumer and their locks are written for the archive names <package>-<language>-<version>.<ext> by make release-consumer-lock TAG=v0.0.4, and make release-consumer TAG=v0.0.4 installs the six archives and passes the smoke command of each | config/release.json, tests/fixtures/release-consumer, tests/scripts/release-config.test.mjs, docs/operations/publication.md | node --test tests/scripts/release-config.test.mjs |
| T22.4-23 | Remove python from the core support level of packages/template-compiler/interface.json: the level names python without a language mapping in languages, so check-compiler-interface.mjs fails compiler mapping is incomplete and the CI job showcase fails its checks, which the mutation check of the bound map reports as a missing message; the core level names the four languages that the mapping declares until T22.4-24 adds python | packages/template-compiler/interface.json | node scripts/check-compiler-interface.mjs |
| T22.4-24 | Declare the Python language mapping in packages/template-compiler/interface.json (backend, program, error, runtimeServices, runtimeServiceOperationNames, runtime environment fields and operations, frame and scope fields and operations, and the bound map of the Python runtime) and move python back into supportLevels.core, so the compiler contract checks the Python backend like the other four core languages; packages/template-python/tests/test_compiler_interface.py compares the Python declarations with the mapping, RuntimeServices becomes a class of the package that RuntimeEnvironment implements, and scripts/check-compiler-interface.mjs and its mutation check run the test and mutate the mapping | packages/template-compiler/interface.json, packages/template-python/tests/test_compiler_interface.py, packages/template-python/src/polyspec/template/runtime_environment.py, scripts/check-compiler-interface.mjs, scripts/check-compiler-interface-mutations.mjs | node scripts/check-compiler-interface.mjs |
| T22.4-24-1 | Regenerate the committed compiled artifacts after the interface change of T22.4-24: the contract digest of the compiled AST manifests, the generated manifests and the example site no longer matches the contract, so the CI jobs generated and showcase fail; make showcase-compile, make typed-generator and node tools/showcase/build-site.mjs write the regeneration | examples/site, tools/showcase/adapters, examples/site/index.html | node scripts/check-generated-artifact.mjs |
| T22.4-25 | Regenerate the committed compiled artifacts after the interface change of T22.4-23: the contract digest of the compiled AST manifests, the generated manifests and the example site no longer matches the contract, so the CI jobs generated and showcase fail; make showcase-compile, make typed-generator and node tools/showcase/build-site.mjs write the regeneration | examples/site, tools/showcase/adapters, examples/site/index.html | node scripts/check-generated-artifact.mjs |
| T22.4-27 | Use the shared tools of polyspec/kit v0.0.4: scripts/kit/, tests/kit/, kit.json and .kit/kit.lock.json hold the vendored copy (make kit-sync KIT_TAG=v0.0.4; a second run prints unchanged), and the repository copies of the tools that kit provides are removed with their tests: the dependency review and gate, the push gate, the full-run guard, the owner check, the release and its consumer install, install-tools, ci-targets, ci-passed, target-report, run-tests, check-documents, holder-lock, git-hooks, github-ruleset and check-cargo-downloads. config/ holds toolchain, checklist, documents, commits, owner-checks, release and dependency-policy in the shape of kit, Makefile includes scripts/kit/kit.mk, every document pair holds the same doc-id and the Korean file the source-sha256 of the English file (scripts/features/build.mjs writes both for the features page), and contracts/features.json, package.json and the operations documents name the kit files. Version 0.x has no pull request, merge queue or GitHub ruleset: .github/ruleset.json and push-gate.yml are removed, ci.yml runs on a push to main and on a manual run and its job docs runs push-gate-commit, release.yml requires the check ci-passed, config/release.json lists the npm, Composer, PHP extension, Python, Rust and Go packages with the assets <package>-<language>-<version>.<ext>, and AGENTS.md states the 0.x rules and the ownership of the shared tools | Makefile, scripts/kit, tests/kit, kit.json, .kit/kit.lock.json, config, .github/workflows, AGENTS.md, docs/operations | make kit-check |
| T22.4-28 | Record the dependency review of the kit gate: config/dependency-policy.json has the shape of kit (pythonManifests names packages/template-python/pyproject.toml), config/toolchain.json declares cargoAudit and govulncheck for the advisories of the Cargo locks and the Go modules, @playwright/test is at 1.64.0 in package.json and package-lock.json, and make dependency-review RECORD=1 writes the review of the npm, Composer and Cargo locks, setuptools==84.0.0 and the registry dependencies. make dependency-policy-check and make dependency-policy-mutation-check pass | config/dependency-policy.json, config/dependency-review.json, config/toolchain.json, package.json, package-lock.json | make dependency-policy-check |
| T22.4-29 | Regenerate the committed compiled artifacts after the lock change of T22.4-28: the compiler digest covers package-lock.json, so the compiled AST manifests, the generated JavaScript manifests and examples/site/index.html recorded the digest of the compiler before @playwright/test 1.64.0, and node scripts/check-artifact-digests.mjs failed; make showcase-compile, make typed-generator and node tools/showcase/build-site.mjs write the regeneration | examples/site, tools/showcase/adapters/generated/javascript | node scripts/check-artifact-digests.mjs |
| T22.4-30 | Use polyspec/kit v0.0.6 (make kit-sync KIT_TAG=v0.0.6; a second run prints unchanged): the dependency gate reads a package of this repository from a file: lock entry that is a copy, and make documents-stamp writes the source-sha256 of the Korean files | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json | make kit-check |
| T22.4-31 | Use polyspec/kit v0.0.7 (make kit-sync KIT_TAG=v0.0.7; a second run prints unchanged): the dependency gate accepts a local npm package that the manifest requires as file:<its directory> | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json | make kit-check |
| T22.4-32 | Use polyspec/kit v0.0.8 (make kit-sync KIT_TAG=v0.0.8; a second run prints unchanged): make install-tools TOOLS="npm go" installs only the named tools | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json | make kit-check |
| T22.4-33 | Install only the tools that each CI job sets up: the run of a142548 failed in every job at make install, because kit installed every declared tool: ruff needs python3.14, which only the jobs with setup-python provide (the jobs typescript, docs, browser, go, rust, delimiters, editor, both php jobs and the job python on 3.11 failed with python3.14 ENOENT), and cargo-audit needs the Rust toolchain that make install installs after install-tools (the jobs scripts, generated, showcase and python on 3.14 failed with toolchain '1.98.1' is not installed); the later failures (Cannot find module of node_modules, ajv, tsup, eslint, a missing vendor and the Rust toolchain) followed. make install runs install-tools TOOLS="npm go", the job showcase runs make install-tools TOOLS=ruff after setup-python, make dependency-review installs the Rust toolchain (rust-toolchain) before cargo-audit and its workflow sets up Python, and make test-python runs the python3 of the job again (T22.4-21 had set it to python3.14, which the job python on 3.11 lacks) | Makefile, .github/workflows/ci.yml, .github/workflows/dependency-review.yml, tests/scripts/toolchain-files.test.mjs | node --test tests/scripts/toolchain-files.test.mjs |
| T22.4-34 | Read the Python registry in the spelling that ruff format writes: scripts/check-function-contract.mjs matched 'name': BuiltIn(<min>, <max> with single quotes, and the format of T22.4-21 wrote the keys of packages/template-python/src/polyspec/template/functions.py in double quotes, so the checker found no Python function and make function-contract-registries-check failed in the CI job scripts with the contract as the missing names. Red: tests/scripts/function-contract-registries.test.mjs failed. Green: the pattern accepts both quotes and a line break after BuiltIn(, and the checker reports 37 canonical functions in five languages | scripts/check-function-contract.mjs, tests/scripts/function-contract-registries.test.mjs | node --test tests/scripts/function-contract-registries.test.mjs |
| T22.5-1 | Release 0.0.5: every manifest of config/release.json (the root package.json, the npm packages template-ts, template-language, template-lsp, template-codemirror and template-compiler, the VS Code extension, the Composer packages template-php and template-php-ext, the Rust crate and the Python package) declares 0.0.5, the dependencies between the npm packages and the version of the C extension name 0.0.5, package-lock.json, the composer.lock of each Composer package and the two Cargo.lock files record 0.0.5 or the content hash of their manifest, ## Unreleased of CHANGELOG.md and CHANGELOG.ko.md becomes ## 0.0.5 below a new empty ## Unreleased, the compiled AST manifests, the generated JavaScript manifests and examples/site/index.html record the compiler digest of the tree, config/dependency-review.json records the review of the locks, and the consumer projects of tests/fixtures/release-consumer and their locks name the archives of 0.0.5 (make release-assets TAG=v0.0.5, make release-consumer-lock TAG=v0.0.5) | config/release.json, every manifest it lists, package-lock.json, CHANGELOG.md, examples/site, tools/showcase/adapters/generated/javascript, tests/fixtures/release-consumer | make release-versions TAG=v0.0.5 |
| T22.5-2 | Use polyspec/kit v0.0.21 (make kit-sync KIT_TAG=v0.0.21; a second run prints unchanged). The vendored tools read the wheel package kind, buildBackend, consumers.wheel, the pythonBuild and pythonBackend declarations, and the ruff declaration of config/toolchain.json (version 0.16.10 and the sha256 of its wheel by platform, so the pin is not read from a package manifest). config/dependency-policy.json names its npmLocks (package-lock.json), which the kit schema requires | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json, config/toolchain.json, config/dependency-policy.json | make kit-check |
| T22.5-3 | Publish the Python package as a wheel asset of its release. RED: make release-assets TAG=v0.0.6 on the committed tree before this change fails with config/release.json: proof.gitTag.packages/template-python/pyproject.toml.kind is "python", the allowed values are rust, so no wheel is built. GREEN: config/release.json lists packages/template-python as a wheel package named polyspec-template with the manifest mode archive, declares the buildBackend of kit and consumers.wheel with the smoke import polyspec.template; make release-assets TAG=v0.0.6 writes polyspec_template-0.0.6-py3-none-any.whl into var/release/assets, and make release-consumer TAG=v0.0.6 installs it into a clean virtual environment and imports it. The package declares no dependency, so the wheel metadata holds no Requires-Dist. requires-python is >=3.14, the minor of .python-version, and the CI job python sets up .python-version with no matrix (python-requires.test.mjs, toolchain-files.test.mjs). release.yml runs make install-tools TOOLS="python pythonBackend" before make release-assets | config/release.json, packages/template-python/pyproject.toml, .github/workflows/release.yml, .github/workflows/ci.yml, tests/scripts/python-requires.test.mjs, docs/operations/publication.md | make release-consumer TAG=v0.0.6 |
| T22.5-4 | Release 0.0.6: every manifest of config/release.json declares 0.0.6, the locks and the consumer projects of the release assets record 0.0.6, ## Unreleased of CHANGELOG.md and CHANGELOG.ko.md becomes ## 0.0.6 below a new empty ## Unreleased, and T22.5-3 is complete with its GREEN run at the tag v0.0.6 of the release commit | config/release.json, every manifest it lists, package-lock.json, CHANGELOG.md, CHANGELOG.ko.md, docs/plans/execution-checklist.md | make release-versions TAG=v0.0.6 |
| T22.5-5 | Run every Python check on the pinned interpreter of var/tools. RED: on a checkout without var/tools/bin/python3, make install-tools TOOLS=ruff fails with ruff needs var/tools/bin/python3, the Python that config/toolchain.json pythonBuild declares, so the CI job showcase fails at ruff. GREEN: make install-tools TOOLS="python ruff" installs the pinned Python and then ruff, and make lint-python passes; make install installs npm, Go and python; scripts/python-toolchain.mjs returns var/tools/bin/python3 and fails with its path and the fix when it is missing; the conformance drivers, scripts/check-compiler-interface.mjs and the PYTHON of the Makefile run that interpreter; no workflow uses actions/setup-python | .github/workflows/ci.yml, .github/workflows/dependency-review.yml, Makefile, scripts/python-toolchain.mjs, scripts/check-compiler-interface.mjs, tests/runner/drivers.mjs, tests/runner/conformance-generated-python.mjs, config/toolchain.json, docs/operations/development.md, tests/scripts/python-toolchain.test.mjs, tests/scripts/python-check-coverage.test.mjs, tests/scripts/toolchain-files.test.mjs, tests/scripts/action-pins.test.mjs | node --test tests/scripts/python-toolchain.test.mjs tests/scripts/python-check-coverage.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/action-pins.test.mjs |
| T22.5-5-1 | Copy the modules that the drivers import into the fixtures of the driver tests. RED: make test-scripts on the committed tree fails in tests/scripts/drivers.test.mjs, tests/scripts/failure-messages.test.mjs and tests/scripts/no-silent-skips.test.mjs with Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../scripts/python-toolchain.mjs' imported from .../tests/runner/drivers.mjs, because the three tests copy tests/runner/drivers.mjs into a temporary checkout without scripts/python-toolchain.mjs, which T22.5-5 added to the imports. GREEN: tests/scripts/driver-fixture.mjs names the files of the drivers once (DRIVER_FILES) and copyDriverFiles copies them; the three tests use it; the drivers test fails when the import walk of tests/runner/drivers.mjs reaches a module that DRIVER_FILES does not list; make test-scripts passes | tests/scripts/driver-fixture.mjs, tests/scripts/drivers.test.mjs, tests/scripts/failure-messages.test.mjs, tests/scripts/no-silent-skips.test.mjs, tests/runner/drivers.mjs, scripts/python-toolchain.mjs | make test-scripts |
| T22.5-6 | Correct the records of 0.0.6. CHANGELOG.md and CHANGELOG.ko.md record the ruff 0.17.0 declaration of config/toolchain.json under T22.5-4 and move the T22.5-5 entry into ## 0.0.6, below an empty ## Unreleased. docs/operations/development.md and its Korean file name ruff 0.17.0 as declared in config/toolchain.json, installed by make install-tools TOOLS="python ruff" on var/tools/bin/python3, with the rules and target of [tool.ruff] in packages/template-python/pyproject.toml; that file has no dev extra that pins ruff. Completed items T22.4-21 and T22.5-2 are not edited | CHANGELOG.md, CHANGELOG.ko.md, docs/operations/development.md, docs/operations/development.ko.md, docs/plans/execution-checklist.md, docs/plans/execution-checklist.ko.md | make documents-check |
| T22.5-7 | Treat EPERM on a process group as a member in exit, and wait for ESRCH. Cause: on macOS a process group answers EPERM while a member is in exit; the code of 97bf51f took EPERM for a group that had ended, so it stopped waiting while the member still answered kill(pid, 0), and an earlier version signaled a group that answered EPERM. RED: tests/scripts/process-group-gone.test.mjs fails on the committed module of 97bf51f in two cases: an EPERM probe ended the wait without a further probe (the wait did not probe again after EPERM until ESRCH), and an EPERM that persists did not return an error (a group that stays exiting must not resolve as ended). GREEN: signalGroup returns exiting for EPERM and ended for ESRCH; stopProcessGroup sends no signal after exiting, probes until ESRCH, and rejects with the pgid, errno and elapsed time when EPERM persists past 10 s. tests/runner/bounded.mjs and the VS Code integration step signal through signalGroup. tests/scripts/process-group-exit-window.test.mjs records the probe result of that window and asserts that stopProcessGroup resolves only after kill(pid, 0) fails. node --test tests/scripts/process-group-gone.test.mjs tests/scripts/process-group.test.mjs tests/scripts/process-group-callers.test.mjs tests/scripts/process-group-exit-window.test.mjs tests/scripts/long-steps.test.mjs passes | scripts/process-group.mjs, tests/runner/bounded.mjs, packages/template-vscode/tests/integration/step.mjs, tests/scripts/process-group-gone.test.mjs, tests/scripts/process-group-exit-window.test.mjs | node --test tests/scripts/process-group-gone.test.mjs tests/scripts/process-group.test.mjs tests/scripts/process-group-callers.test.mjs tests/scripts/process-group-exit-window.test.mjs tests/scripts/long-steps.test.mjs |
| T22.5-8 | Set the limits of the tests in tests/scripts to about 100 times their expected time, and no limit below 20 times it. The template-ts reader test of tests/scripts/build-package.test.mjs has the 30 s runner default: run alone three times on a loaded machine, it took 28.6 s, 32.3 s and 37.9 s, so under make test-scripts it passes its limit while it still works. GREEN: its limit is 3,300 s, 100 times the mean of those runs, and the other build tests have limits of 100 times their measured time. Each other explicit limit or runner default below 20 times the elapsed time of its test in one run of tests/scripts is corrected to about 100 times it, and the CHANGELOG records each value. Deliberate deadlines that a test expects to fire are not pass limits and are not corrected. The comment of each changed limit states what it protects. node --test tests/scripts/build-package.test.mjs tests/scripts/long-steps.test.mjs tests/scripts/cargo-target.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/conformance-progress.test.mjs passes | tests/scripts/build-package.test.mjs, tests/scripts/long-steps.test.mjs, tests/scripts/cargo-target.test.mjs, tests/scripts/toolchain-files.test.mjs, tests/scripts/conformance-progress.test.mjs, tests/scripts/clean-release.test.mjs, tests/scripts/atomic-publish.test.mjs, tests/scripts/browser-port.test.mjs, tests/scripts/browser-server.test.mjs, tests/scripts/package-installs.test.mjs, tests/scripts/generated-conformance.test.mjs, tests/scripts/process-group-callers.test.mjs | node --test tests/scripts/build-package.test.mjs tests/scripts/long-steps.test.mjs tests/scripts/cargo-target.test.mjs tests/scripts/toolchain-files.test.mjs tests/scripts/conformance-progress.test.mjs |
| T22.5-9 | Record T22.5-5-1, T22.5-7 and T22.5-8 in ## 0.0.6. The tag v0.0.6 is created on the commit that holds them, so CHANGELOG.md and CHANGELOG.ko.md list them under ## 0.0.6, below an empty ## Unreleased | CHANGELOG.md, CHANGELOG.ko.md | node scripts/kit/release.mjs versions v0.0.6 |
| T22.5-10 | Write the member pids from the group leader. Cause: the leader of process-group-callers and process-group-exit-window exits after 300 ms, and the background member writes its own pid file only after it starts running, so under load the file does not exist when the test stops the group and reads the file, which fails with ENOENT before its assertions. RED: the member writes its pid only after MEMBER_START_MS (5 s), and node --test tests/scripts/process-group-callers.test.mjs fails its four cases with ENOENT ... background.pid, node --test tests/scripts/process-group-exit-window.test.mjs fails with ENOENT ... member.pid. GREEN: the leader writes the pid of the member it spawned (member.pid of the spawn) into the file before it continues, so the file exists before the leader exits; the member keeps the delay, and both tests pass with it. No other test of tests/scripts reads a pid file of a child. The case after its deadline starts /bin/sh as its leader, which writes the member pid at fork time and then execs sleep, so the leader does not depend on the start of Node. Its deadline is 10 s, about 100 times the time the leader needs before the file exists (up to 98.5 ms under load 40); the deadline protects that the member exists before it fires. No wait or timer is added | tests/scripts/process-group-callers.test.mjs, tests/scripts/process-group-exit-window.test.mjs | node --test tests/scripts/process-group-callers.test.mjs tests/scripts/process-group-exit-window.test.mjs |
| T22.5-11 | Signal a process group until it is gone. Cause: a member forked while the group SIGKILL was delivered survives it, and stopProcessGroup sent that signal once and then waited for kill(-pgid, 0) to fail, so a member that kept running held the wait forever. Evidence: the listing printed by the stalled wait (waiting for the processes of group 25105 to exit, then sh -c for n in 1 2 3; do (while :; do mkdir -p ... with ppid 1 and a (bash) child, both alive one second after the signal), then the 300 s timeout of a stopped process group writes nothing into a directory that is then removed. RED: stopProcessGroup signals the group again while a member is still running fails with actual: [ 'SIGKILL' ], expected: [ 'SIGKILL', 'SIGKILL' ]. GREEN: while the probe answers 'sent', the loop sends the signal again on each probe. A process group id is not reused while one of its members exists, and the loop stops sending once the probe answers ESRCH; the one window is between a successful probe and the next signal, in which the last member may exit and a new group may take the id. The waiting note lists the group members with ps | scripts/process-group.mjs, tests/scripts/process-group.test.mjs | node --test tests/scripts/process-group.test.mjs tests/scripts/process-group-gone.test.mjs tests/scripts/process-group-callers.test.mjs tests/scripts/process-group-exit-window.test.mjs |
| T22.6 | Land the language-parity work of the branch feat/parity-T22.6 (47 commits whose merge base is T22.5-10) on the current layout and kit 0.0.52, and delete the branch. What main or kit already holds is not repeated: the declaration of the implementations and the native binding of every test are T24.7, T24.8 and T24.11, the Python bound-data test and the Python showcase exist, and the symbol snapshots and extractors of the branch are replaced by the symbol extractors of kit (symbols of an implementation in config/parity.json). The work that remains is the error-code catalog (T22.6-15), the exports of the Python package (T22.6-14-1), the counts of implementations in the documents (T22.6-11 to T22.6-13), the API units (T22.6-14), the Python benchmark (T22.6-17), the remaining API concepts (T22.6-18) and the missing runtime rules (T22.6-19). The option cases of the branch (tests/fixtures/options/cases.json) are not landed: the conformance cases cover the define, env and delimiter options and the includes, and the feature units limits, host-function-failures, delimiters, delimiter-argument-error, functions-date, object-calls, map-loader and loader-errors give each implementation a native test of the other options; the four rules that the cases found missing are T22.6-19. The branch is deleted when each part is landed or recorded with its condition in a row | T22.6-15, T22.6-14-1, T22.6-11, T22.6-12, T22.6-13, T22.6-14, T22.6-17, T22.6-18, T22.6-19; git branch --list feat/parity-T22.6 prints nothing | [ ] |
| T22.6-15 | Generate the error codes of every implementation from one catalog. The codes of docs/spec/errors.md are written by hand in packages/ts/src/errors.ts, packages/go/errs/errs.go, packages/rust/src/error.rs and packages/python/src/polyspec/template/errors.py, and the PHP package and the PHP extension hold no list, only a string literal at each place that raises an error, so config/parity.json declares no errors and R7 compares nothing. Acceptance: the code tables of docs/spec/errors.md are the catalog of errors; scripts/generate-error-codes.mts writes from them the list of each implementation: the union ErrorCode of TypeScript, the constants of errs in Go, the variants of ErrorCode with the condition of the table and their names in Rust, ERROR_CODES in Python, the class Polyspec\Template\ErrorCode in PHP and the header pt_error_codes.h of the PHP extension; the PHP package and the PHP extension raise each error through these constants and hold no code literal elsewhere; each implementation names its list in errors. RED: with errors declared, make parity-declarations-check reports R7 for php and php-ext. GREEN: make error-codes-check regenerates every list with no difference, make parity-declarations-check reports no R7 finding, and node --test tests/scripts/error-codes.test.mts passes | make error-codes-check; make parity-declarations-check; node --test tests/scripts/error-codes.test.mts | |
| T22.6-14-1 | Export the program and loader interfaces of the Python package. polyspec.template does not export Program, the program interface of engine.py that Engine renders (RT-1, RT-69), nor Loader and LoadResult, the loader protocol of loader.py and its result (RT-3, RT-10), while every other implementation exports them; the base class AstProgramCore of AstProgram is public by name only. test_every_public_function_and_class_is_exported reads only the names that the package module defines, so it cannot see these gaps. Acceptance: Program, Loader and LoadResult are in __all__, AstProgramCore is _AstProgramCore, and test_every_public_class_of_the_engine_and_loader_modules_is_exported checks every public class of engine.py and loader.py against __all__. RED: the new test names AstProgramCore, LoadResult, Loader and Program. GREEN: the export tests and the Python package tests pass | make test-python; make lint-python | |
| T22.6-11 | Count the five languages of the core level in the specification. packages/compiler/interface.json declares typescript, go, rust, php and python at the core level, and the interface gate, the function contract, the generated backends, the showcase and the native-call fixture cover Python, but these sentences count four or list four without Python: docs/spec/compiler.md:8, :84 (the showcase artifacts and "the fixture does not run Python yet"), :92; docs/spec/functions.md:10; docs/spec/runtime.md:49 (RT-69), :179 (RT-56), :188, :190; docs/spec/data-model.md:232 omits Python, whose merge also checks its arguments inside the operation; docs/spec/conformance.md:49 names the parser of four command-line interfaces by language and that of TypeScript by module only; and the Korean files hold the same sentences. Acceptance: each sentence names the five languages or states the subset that is true, the Korean files say the same. RED: with documents of config/parity.json declared over docs/spec, make parity-declarations-check reports R10 for each of these lines. GREEN: it reports no R10 finding for docs/spec except those of T22.6-12-1 | make parity-declarations-check; make docs-check | |
| T22.6-12 | Count the five languages in the operations documents and declare R9 and R10. docs/operations/showcase.md:50 and :161 count four languages of the showcase, which renders five (SHOWCASE_LANGS, examples/site/data/results.json), :173 lists four targets of the closed template dispatch, which the Python backend emits too, docs/operations/testing.md:24 counts four languages of the mode matrix, which has five, and the Korean files hold the same sentences (testing.ko.md:20 counts four AST and four generated programs). docs/operations/benchmark.md names four implementations because the benchmark measures no Python yet, which is true until T22.6-17. Acceptance: config/parity.json declares languageLists for SHOWCASE_LANGS of the Makefile (profile manifest) and documents over the README files, docs, docs/spec and docs/operations with the nouns of a count in English and Korean; the benchmark documents are outside its globs until T22.6-17, and the files of T22.6-12-1 until its kit item; each other count and list names the five languages. RED: make parity-declarations-check with the declaration reports 31 R10 findings. GREEN: it reports none | make parity-declarations-check; make docs-check | |
| T22.6-12-1 | R10 of kit counts sentences that count no implementation. docs/spec/data-model.ko.md:229 says that the Go module, the Rust crate, the PHP package, the PHP extension and the Python package are each one implementation (각각 한 구현), which R10 reads as a count of 1 against five names; docs/operations/development.ko.md:22 lists the toolchains of the recipes (Node.js, npm, Go, Rust, the PHP minor version, Composer), which R10 reads as a list of the implementations go, rust and php. The sentences are true and stay as they are; the kit item: a distributive count (each, 각각) is not a count of the implementations of the sentence, and a name of a toolchain in a list of tools is not a name of an implementation. Until a kit release reads them so, documents of config/parity.json leaves the two files out of its globs. Acceptance: on that kit release the two files are in the globs, and make parity-declarations-check reports no R10 finding. Waits for kit | make parity-declarations-check | [ ] |
| T22.6-13 | Name the five languages of the compiler in its README. packages/compiler/README.md:12 and README.ko.md:13 say that compileSource compiles into a TypeScript, Go, Rust or PHP program, while the compiler emits Python too (packages/compiler/src/backends/python.mts). Acceptance: the sentence names TypeScript, Go, Rust, PHP and Python in both files. RED: with the documents declaration of T22.6-12, make parity-declarations-check reports R10 for both lines. GREEN: it reports none for them | make parity-declarations-check; make docs-check | |
| T22.6-14 | Declare the API units of the eleven concepts of the runtime specification with the symbol extractors of kit: api-parse, api-engine, api-program, api-ast-program, api-prepared-render, api-bound-map, api-bind, api-merge, api-template-error, api-page-cache and api-loader, each with the symbol of each implementation of its profile, and declare every other public symbol in nativeOnly with its reason and spec anchor (R1, R11). config/toolchain.json declares symbolExtractors go and php and the Rust nightly of the rustdoc JSON as an exception of the scope packages/rust, and each implementation names symbols. Blocked: make install-tools fails to build the go extractor of kit (go build of scripts/kit/symbols/go in var/tools/symbols-go.next-*) with current directory is contained in a module that is not one of the workspace modules listed in go.work, because the root go.work of template covers the build directory; the kit item is to build the extractor outside the workspace (GOWORK=off). R11 also requires every public symbol, with its members, in a unit or in nativeOnly: the Python extractor gives 305 ids and the rustdoc JSON of the crate 993, so the declaration needs the concept decisions of T22.6-18 first. Acceptance: on the kit release that builds the go extractor under a go.work, and after T22.6-18, the declaration holds and make parity-check reports no R1 and no R11 finding. Waits for kit and T22.6-18 | make install-tools; make parity-check | [ ] |
| T22.6-17 | Measure Python in the benchmark. examples/showcase/adapters/python.py has no benchmark mode (SHOWCASE_BENCH_ITERATIONS), examples/showcase/benchmark-modes.mts measures four implementations, and docs/operations/benchmark.md and benchmark.ko.md name four, which is why they lie outside documents of config/parity.json (T22.6-12). Acceptance: the Python adapter has the benchmark mode of the PHP adapter, tested without a benchmark run; make bench, run once when the maintainer allows it, measures five implementations and writes examples/site/data/mode-benchmark.json and the tables of both benchmark documents with five rows; the benchmark documents are in the globs of documents, and make parity-declarations-check and make benchmark-check pass. Waits for a make bench run that the maintainer allows | make benchmark-check; make parity-declarations-check | [ ] |
| T22.6-18 | Decide the shared API concepts beyond the eleven of T22.6-14. Each public symbol of an implementation is a concept that every implementation of a profile exports, or a native symbol with its reason (R11). The candidates are the concepts that two or more implementations export: the error code, the load result, path resolution, the internal boundary, JSON parsing of text and of bytes, the bind of a value, of a map and of data, the check of text, the bind error, the host function, the function context, the environment, the limits, the runtime environment and the runtime bindings, and the frame, scope and render context that TypeScript and Python export and the others keep internal. For each, the decision adds the symbol to the implementations that lack it or removes an export that is no public concept, from the runtime specification. Acceptance: each candidate has its decision in docs/spec/runtime.md or docs/spec/data-model.md, the exports follow it, and T22.6-14 declares the result. Waits for the decision of each concept | make parity-check | [ ] |
| T22.6-19 | Write the four runtime rules that the option cases of the branch found missing; they describe what every implementation does, so no behavior changes. (1) RT-33 names the keys of limits: iterations, depth, outputBytes and expressionDepth, which TypeScript, PHP, the PHP extension and Python take as written, while Go and Rust name the fields of their Limits type by the convention of the language (OutputBytes, output_bytes); (2) RT-34 states the unit of the expression depth: the parser counts one level for each expression, each postfix expression and each prefix operator that it enters; (3) RT-1 writes program.register(name, fn), since every implementation registers its host functions on the program that Engine owns (RT-69), instead of engine.register; (4) RT-6 states that the options of TypeScript, PHP, the PHP extension and Python keep the default of an omitted key and that Go and Rust start their Limits value from DefaultLimits and Limits::default(). Acceptance: the rules are in docs/spec/runtime.md and its Korean file, and the expression vectors depth-limit-parentheses (31 parentheses parse), error-depth-parentheses (32 fail), depth-limit-unary (62 prefix operators parse) and error-depth-unary (63 fail) of tests/fixtures/expr/cases.json pass in the six implementations. The two parsing vectors hold no evaluation, because the binding of the PHP extension evaluates an expression inside json(...), which adds two levels. GREEN: the expression bindings of the six implementations, make rules-check and make documents-check pass | make rules-check; make docs-check; make parity-check | |
| T22.6-19-1 | Evaluate the expression vectors in the PHP extension at the depth of the other bindings. The extension exposes no expression evaluator of its own, so packages/php-ext/tests/ExprFixturesTest.php rendered {= json(<expr>) | raw}, where the expression is a call argument two levels deeper than the expression of the vector; so depth-limit-parentheses and depth-limit-unary of T22.6-19 failed with E_RUNTIME_LIMIT in the extension and lost their evaluation in every implementation. Acceptance: the binding assigns the expression in its own tag, {:value = <expr>}{= json(value) | raw}, so the expression is at level 1 as in the other bindings and only the read of value is wrapped; both vectors hold their evaluation again. RED: with the evaluations restored, both vectors fail in the extension with the current binding. GREEN: they pass in the six expression bindings | make test-ext; make parity-check |
Wave 23 — The editor stack as a standard
| ID | Task | Verification | Done |
|---|---|---|---|
| T23.1 | The parser is the only grammar. analyze and analyzePrefix return fully classified tokens and the parts of each tag body, and template-language deletes the lexical tables it restates beside the parser: SIGILS and OPERATOR in tokens.ts, WRAPPERS in tags.ts, LOOP_HEAD and ASSIGN_HEAD in body.ts, RESERVED and IDENT in expression.ts, and the duplicated delimiter-directive regex. The editor boundary check gains the rule that the language service declares no lexical table. Grounding: two readings of one grammar drift, and the parser already decides every class (orm docs/plans/editor-stack.md, ruling 1). | make check: the language service tests pass on the parser's tokens alone; the boundary check fails on a lexical table added to template-language | [o] |
| T23.2 | Token analysis and the formatter in every runtime. The formatting style of docs/operations/editor-tools.md becomes spec rules; the conformance cases carry the expected tokens and the formatted text; Go, Rust, PHP, the PHP extension and Python implement analyze and format (Python extends its analyze), each holding the AST invariant (parse(format(x)) equals parse(x)) and idempotence (format(format(x)) == format(x)); the parity and conformance runners compare tokens and formatted text; docs/features.md lists template-language as passing in every runtime. Grounding: format() exists only in TS, and a standard of the template language is a standard of every runtime. | conformance and parity compare tokens and formatted text in all six implementations; the invariant tests pass | [o] |
| T23.3 | No grammar in the VS Code extension. Its tmLanguage keeps only the HTML host (text.html.basic with embedded CSS and JS); the tag injection rules are deleted and tags are coloured by the semantic tokens the extension already maps; configurationDefaults enables semantic highlighting; the grammar tests (vscode-tmgrammar-test, and captureSyntaxTokens for a tag in an attribute value) are rewritten as semantic-token assertions; docs/operations/editor-tools.md (+ko) is updated. Grounding: a regex grammar has no parser context and disagrees with analyze. | the vscode integration test asserts the semantic tokens of a tag, also inside an attribute value; no tag rule remains in the tmLanguage | [o] |
| T23.4 | Adopt the kit editor tools. Take the editor boundary checker and the VS Code installer from kit (kit item T128-K, after kit K71 makes kit a TS package) with the kit update, delete scripts/check-editor-boundaries.mjs and scripts/install-vscode.mjs, and declare template's rules (@polyspec/template, src/cli) in config. Grounding: a tool that two repositories need lives in kit, never as copies. | the boundary check and the vscode integration run through the kit tools; the two scripts are gone | [o] |
| T23.5 | Host-less tools resolve include paths by a declared root marker. template-root.json ({"schema": 1}) marks the root by nearest ancestor, for the tools with no caller that names a root: the language service, the LSP and the CodeMirror host through its option. The compiler keeps its root argument, the filesystem loader its root, the map and artifact loaders stay unchanged, and the conformance CLI keeps --root (CNF-2). An invalid marker (invalid JSON, another schema, an unknown key) is a diagnostic on the marker's URI. With no marker, every include path, and every block path with a path token, gets the diagnostic "no template root declared (template-root.json)". The language service diagnoses an include, and a block with a path token, whose path is no regular file under the root; a block of an id alone and a target that define supplies stay runtime (RT-24). Paths keep RT-8's normalization; duplicate detection by file identity and symlink rejection are amended RT-10 rules with conformance cases in every runtime. The language service resolves through an injected host and stays node-free (EDT-3). An EDT rule states that the editor resolves against the marker and does not check a host's loader root. | marker vectors accept and reject; the language service reports a missing include and the missing marker; RT-10 conformance cases pass in every runtime | [o] |
Wave 24 — The shared tools as the package @polyspec/kit
| ID | Task | Verification | Done |
|---|---|---|---|
| T24.1 | Install the shared tools as the npm package @polyspec/kit. The Makefile includes scripts/kit/kit-bootstrap.mk instead of the vendored scripts/kit/kit.mk; the copies of kit.copies are written by make kit-sync; the vendored scripts/kit/*, tests/kit/*, .kit/kit.lock.json and kit.json and every reference to them are deleted; the recipes run the tools of kit as node $(KIT)/<tool>.$(KIT_EXT); the pre-push hook calls make push-gate-hook; CI installs with make install-tools TOOLS=npm and make install-packages; @polyspec/kit is a devDependency pinned to its release asset with its integrity. | make kit-check | [o] |
| T24.2 | Let npm write its links. Delete the keys install-links and bin-links of .npmrc and the workarounds they forced: the root package.json declares the JavaScript packages as npm workspaces instead of file: dependencies with overrides, scripts/build-package.mjs no longer installs a built package again, the recipes and scripts start a tool from node_modules/.bin, and tests/scripts/no-symlinks.test.mjs is deleted. | make toolchain-check | [o] |
| T24.3 | Raise the toolchain floors to the pins of kit: .node-version 26.11.1 and engines.node >=26.11.1, Composer require.php >=8.5 and config.platform.php 8.5, requires-python >=3.14, go.mod go 1.27.2 with no toolchain line, Cargo rust-version 1.99.0, rust-toolchain.toml 1.99.0 and typescript 7.0.2. | make toolchain-check | [o] |
| T24.4 | Resolve the Go package of the showcase adapter through go.work instead of a replace of its go.mod. | make layout-check | [o] |
| T24.5 | Lint the Python with the root ruff.toml of kit only: delete [tool.ruff] of packages/template-python/pyproject.toml. | make layout-check | [o] |
| T24.6 | Lint the TypeScript with the oxlint, oxlint-tsgolint and knip configuration of kit (make lint-ts) instead of eslint and typescript-eslint; delete eslint.config.mjs, make lint-js and the eslint packages. | make lint-ts | [o] |
| T24.7 | Declare the implementations and feature units of template in config/parity.json (the schema of kit parity) and run make parity-declarations-check and make parity-check; delete the feature registry of template: contracts/features.json, scripts/features/, tests/runner/parity.mjs and their targets. | make parity-declarations-check | [o] |
| T24.8 | Wire the native tests of every implementation to config/parity.json: each suite binding reports each case under the name of its runner (a subtest TestX/<case> in Go, a test <symbol>::<case> in cargo, a top-level test <case> in vitest, a test method test_<case> in PHPUnit and unittest), Python gets a native test of the conformance cases and the PHP extension one of the expression vectors, which removes both exclusions, and every other test file of an implementation is bound as a feature unit. RED: make parity-declarations-check reports 67 findings, 65 of them R5 undeclared-test and the two R8 exclusions. | make parity-declarations-check, make parity-check | [o] |
| T24.9 | Own tests/scripts/driver-fixture.mjs in config/owner-checks.json: the tests that copy the drivers with it (tests/scripts/drivers.test.mjs, tests/scripts/failure-messages.test.mjs, tests/scripts/no-silent-skips.test.mjs) run when it changes. RED: make owner-validate fails with tests/scripts/driver-fixture.mjs: the path matches no owner. | make owner-validate | [o] |
| T24.10 | Make the folders follow S5 and S7 of kit's layout (red recorded by K79 on kit 0.0.38): make layout-check reports 141 findings, the first schema: error: layout-root-folders: S5: the root holds only packages/, contracts/, docs/, scripts/, tests/, examples/, benchmarks/ and config/; the clients are named packages/template-<runtime>, root tests/ holds package tests, three scripts set PYTHONPATH, and infrastructure scripts are not TypeScript (S9). | make layout-check | [o] |
| T24.11 | Bind every test file to a parity unit (red recorded by K79 on kit 0.0.38): make parity-declarations-check reports 67 findings, the first R5 undeclared-test ts packages/template-ts/tests/cli.test.ts: the test file of ts is the test of no binding, so no check runs it. | make parity-declarations-check | [o] |
| T24.12 | Move to kit 0.0.42 and mise (reds on kit 0.0.38): make test-vscode-integration and tests/scripts/install-vscode.test.mjs fail with ERR_PACKAGE_PATH_NOT_EXPORTED for @polyspec/kit/install-vscode, make dependency-policy-check crashes on the workspace pattern packages/template-ts, and make dependency-review RECORD=1 fails in install-tools because uv reports two Python 3.14.8 installations. Go, Python, uv, ruff, vulture, alint, golangci-lint, deadcode, govulncheck, cargo-audit and Composer become tools of mise.toml locked in mise.lock, config/toolchain.json keeps php, pythonBackend, the exceptions and mise, .python-version is deleted, the Composer platform is the 8.5.0 of the policy, and the review record is written by make dependency-review RECORD=1. | make toolchain-check, make test-vscode-integration, make dependency-review RECORD=1, make dependency-policy-check | [o] |
| T24.13 | Move to kit 0.0.43: pin @polyspec/kit 0.0.43 with the integrity of its published file and write its copies with make kit-sync (the lint configuration of K138, K139 and K142). Bind the tokens and formatting suites of php and php-ext to the PHPUnit method whose data sets are the cases (K137): ConformanceTest::testTokens and ConformanceTest::testFormat of the PHP package, AnalyzeTest::testEqualsTheAnalysisOfThePhpPackage and FormatTest::testFormatsTheCase of the PHP extension, and delete the four exclusions of config/parity.json that waited for it. | make kit-check; make parity-declarations-check names no tokens or formatting binding of php or php-ext; the four bound methods pass with --filter | [o] |
| T24.14 | Move to kit 0.0.45: pin @polyspec/kit 0.0.45 with the integrity of its published file, write its copies with make kit-sync (knip.json and ruff.toml of K147, K152, the bootstrap copies of K150), let make install-tools install the Rust toolchain (K151) instead of the target rust-toolchain, remove .ruff_cache (K147), and declare the files that make runs outside the layout entries, the workspace entries of packages/ts, packages/codemirror and packages/vscode and the system tool gmake in config/knip-entries.json (K145, K152, K154). | make kit-check; make toolchain-check; make layout-check | [o] |
| T24.15 | Update the 7 dependencies that make dependency-policy-check reports as older than the latest stable release of the review of 2026-10-14T17:45:27.781Z, which kept the scripts job of main red in dependency-audit: @playwright/test 1.64.1, oxc-parser 0.154.0, tsup 9.0.0 and vitest 5.0.4 with npm, laravel/pint v1.32.1 of packages/php and phpunit/phpunit 13.4.2 of packages/php and packages/php-ext with Composer, and record the review with make dependency-review RECORD=1. The compiler digest covers package-lock.json, so make typed-generator and examples/showcase/build-site.mts write the digests of the committed artifacts and examples/site/index.html again. | make dependency-policy-check | [o] |
| T24.16 | Resolve the Composer packages through one development root, the Composer monorepo of crudui: the root composer.json, never published, requires polyspec/template through a path repository of packages/php, maps the classes and tests of packages/php-ext, a package of the type php-ext that Composer does not install, and locks the root vendor/ in the root composer.lock. Delete packages/php/composer.lock, packages/php-ext/composer.lock and the path repository that only a config.json of COMPOSER_HOME held for packages/php-ext (red: without it composer update polyspec/template in packages/php-ext fails with "found polyspec/template[0.0.6] in the lock file but not in remote repositories"). make test-php and make test-ext run PHPUnit of the root vendor/ (COMPOSER_VENDOR_DIR of kit run-tests). make lint-php runs PHPStan on each package with the root autoloader (kit K163), make dependency-review reviews the requirements of the published manifests, such as symfony/console of packages/php, at the versions of the root lock (kit K168), and kit allows the root composer.json and composer.lock (kit K167). | composer install at the root; make test-php test-ext; composer validate --strict --no-check-version in packages/php and packages/php-ext; make dependency-policy-check layout-check lint-php; make dependency-review names symfony/console of packages/php/composer.json | |
| T24.17 | Ship the PHP side of the native engine where a project installs it. Composer installs no package of the type php-ext and PIE installs only the extension, so NativeRuntime, NativeAnalyzer and bin/template-ext.php of packages/php-ext reached no project, and the command read the vendor/autoload.php of the checkout (red: vendor/bin/template-ext.php is absent from a Composer project that installs the release zip of polyspec/template). Move them into packages/php (namespace Polyspec\Template\Native) and declare the command in its bin, which starts with require $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';. Without the extension the classes throw ExtensionNotLoaded, which names PIE, and no other engine runs. packages/php-ext is the C extension only, with no php/ and no bin/. The release consumer runs both commands through the proxies of vendor/bin, the conformance driver and the CLI tests run the proxies, and the suite of the extension tests the native classes from the root autoloader. | make test-php test-ext; the Composer smoke command of config/release.json in a project with the zip of polyspec/template | |
| T24.18 | Move to kit 0.0.47: pin @polyspec/kit 0.0.47 with the integrity of its published file and write its copies with make kit-sync (.oxlintrc.json of K155, K156, K157, K161, K170, .golangci.yml of K164, config/layout.kit.yml of K167 and the bootstrap copies of K157, K158). Move the wheel backend to pythonBackend of config/toolchain.json only (K166). Type-check every TypeScript file that make lint-ts lints (the package configurations, the browser test, the VitePress configuration in docs/.vitepress/tsconfig.json and the React sample in examples/site/scenarios/tsconfig.json, which React and its types type-check as a knip entry), and fix each finding of the new type-safety rules in the hand-written TypeScript in code. | make kit-check; make toolchain-check; make release-coverage; make typecheck; make lint-ts without findings outside examples/showcase/adapters/generated/typed | |
| T24.19 | Write the compiler package in strict TypeScript (S9). packages/compiler/src/*.mts and src/backends/*.mts replace the untyped modules *.mjs: the repository runs them by type stripping, and make build-compiler builds the published package with tsup into dist/ with the declarations of tsc, so exports keeps its entry points (compiler.mjs, ast-artifact.mjs, type-manifest.mjs, functions.json). The compiler reads its JSON documents through ajv (src/documents.mts): the AST artifact manifest, the generated manifest, the type manifest and the function contract with schemas checked against their types by JSONSchemaType, and it reads each AST with decodeTemplate of @polyspec/template; the backends build a typed Target, and a block of the IR is a union that says when it has a definition and a target. The compiler digest covers src/ in the repository and the build installed. RED: tsc strict over the modules reports 879 errors (772 implicit any parameters). GREEN: make test-compiler-types builds the package and tsc is clean with no any, as unknown as, ! or @ts-expect-error, oxlint is clean, every committed generated source is unchanged byte for byte, and only the compiler digests of the manifests and of examples/site/index.html change | make test-compiler-types, make typecheck, make lint-ts, make artifact-digest-check, make typed-generator-check, make generated-native-check, make compiler-interface-check, make compiler-ir-check | [o] |
| T24.20 | Move to kit 0.0.48: pin @polyspec/kit 0.0.48 with the integrity of its published file and write its copies with make kit-sync (release.yml with the actions pinned by SHA of K169, the bootstrap copies and config/layout.kit.yml of K171). Each checkout runs its own Node.js (K171): node = "26.12.0" in mise.toml with its archives in mise.lock replaces .node-version, engines.node of every package.json is >=26.12.0, and every recipe of the Makefile starts node as $(NODE) and go as $(GO), never by the bare name; the environment of the Python package is read at parse time by $(BOOTSTRAP_NODE). The workflows take the Node.js that starts the bootstrap from mise.toml and pin actions/checkout v7.0.1, actions/setup-node v7.1.0 and shivammathur/setup-php 2.40.0 by SHA, as release.yml does; the job ci-passed installs the bootstrap and the packages before make ci-passed, which no run had passed because kit.mk was missing. RED: make kit-check reports 101 recipes that start node or go by the bare name, and tests/scripts/toolchain-files.test.mts fails 5 tests on .node-version | make kit-check; make toolchain-check; make layout-check; make lint-ts; tests/scripts/toolchain-files.test.mts | [o] |
| T24.21 | Bound every job of the workflows of the repository with timeout-minutes, so a job that stops making progress fails instead of holding the concurrency group of CI. The editor job of CI run 38648904745 (commit e06b6b13) ran for 1h50m until it was cancelled by hand, and every later run of main waited behind it, because no job declared timeout-minutes and GitHub's limit is 360 minutes. The hang was in make install-vscode, before any test: the first download of VS Code 1.138.0 by @vscode/test-electron 3.1.0 failed with aborted and the retry installed the copy, but the tar that read the aborted download was never ended, so node did not exit; kit K173 removes that cause in the installer of kit. Every job of ci.yml, pages.yml and dependency-review.yml (not the release.yml that kit copies) declares about twice the longest duration of its recent runs: scripts, delimiters and php 12, generated, showcase and python 8, rust 6, dependency review 10, and docs, typescript, browser, editor, go, ci-passed and pages 5. RED: tests/scripts/job-timeouts.test.mts names the 15 jobs that declare no timeout-minutes | tests/scripts/job-timeouts.test.mts; make owner-validate | [o] |
| T24.22 | Move to kit 0.0.50: pin @polyspec/kit 0.0.50 with the integrity of its published file and write its copies with make kit-sync (knip.json without ignoreBinaries of K176, kit-bootstrap.mk with $(NODE) of K185). The php bindings of parity run the PHPUnit of the root vendor/ of the Composer monorepo (K178), which reported each of them as R3 not-executed on kit 0.0.48. The binaries that the scripts and tests run go into binaries of config/knip-entries.json with their reasons: make, php and composer (K176). Every tracked composer.json is checked by the dependency policy (K181): packages/php-ext/composer.json gets its own entry in composerPlatforms and a composer.lock, since Composer does not load a package of the type php-ext through a path repository of the root ("found polyspec/template-php-ext[0.0.6] but these were not loaded"). RED: make parity-check in CI reports the php bindings as not executed, make lint-ts reports 10 unlisted binaries and make dependency-policy-check reports packages/php-ext/composer.json under composerCoverage | make kit-check; make toolchain-check; make dependency-policy-check; make lint-ts; make layout-check; make parity-check (CI) | [o] |
| T24.23 | Generate the AST validator without an oxlint disable comment. scripts/generate-ast-validator.mts wrote /* oxlint-disable no-unused-vars, unicorn/no-thenable */ into the header of packages/ts/src/ast-validator.mjs, though since kit K172 its glob in ignore of config/knip-entries.json (the ajv standalone output) keeps the file out of oxlint as well as knip, so the comment disabled rules that never read the file. The header names only the generator and the command that writes it. RED: packages/ts/src/ast-validator.mjs carries an oxlint-disable comment | make ast-validator-check; make lint-ts; make artifact-digest-check | [o] |
| T24.24 | Read the modules that the bundles of the VS Code extension load at run time from the metafile of esbuild. packages/vscode/tests/extension.test.mjs found the require calls of dist/extension.cjs and dist/server.cjs with a regular expression that skips a match after a quote, a word character or a dot, a reading of JavaScript of its own that a call in a template literal or after a comment would mislead. esbuild, which bundles them, records each import that it leaves to run time with external: true in its metafile: scripts/build-package.mts writes extension.meta.json and server.meta.json with the bundles into packages/vscode/dist, which publishes them atomically with the bundles and which .vscodeignore does not package, and the test compares the external imports of each output with the allowed modules. RED: the test fails when dist/extension.meta.json does not exist | make test-vscode-unit; tests/scripts/build-package.test.mts; make lint-ts | [o] |
| T24.25 | Release 0.0.7: every manifest of config/release.json declares 0.0.7, with the dependencies between the packages of the repository, the PHP extension header, package-lock.json, the Cargo locks and the Composer locks, each re-locked by composer update --lock in its folder (kit K180); the consumer projects of the release assets record 0.0.7 (make release-consumer-lock), and their smoke commands pass on the archives (make release-consumer), including vendor/bin/template-ext.php without the extension (T24.17); ## Unreleased of CHANGELOG.md and CHANGELOG.ko.md becomes ## 0.0.7 below a new empty ## Unreleased; the review records the new locks; the tags v0.0.7 and packages/go/v0.0.7 name the release commit. Superseded by T24.28: the release proof of v0.0.7 failed, so the tag v0.0.7 has no published release, and v0.0.8 meets the goal of the release. | make release-versions TAG=v0.0.7; make release-consumer TAG=v0.0.7; make dependency-policy-check; make artifact-digest-check | [o] |
| T24.26 | Release the wheel with its dependency pinned and locked for its consumer. Since T25.6-1 the Python package requires jsonschema>=4.26.0, a range, and its consumer lock packages/python/pylock.wheel.toml holds no package, so make release-assets refuses the wheel (kit K49: a Requires-Dist is name==version and the lock of the wheel holds it with every transitive dependency). pyproject.toml requires jsonschema==4.26.0, the version that pylock.toml pins, and consumers.wheel.lock of config/release.json names packages/python/pylock.toml, the universal lock with hashes that make python-lock writes from the requirements and constraints of the package; pylock.wheel.toml is gone. RED: make release-assets TAG=v0.0.7 fails with "Requires-Dist jsonschema>=4.26.0 is a range, not name==version" | make python-lock-check; make release-coverage; make dependency-policy-check; tests/scripts/release-config.test.mts; make release-assets | [o] |
| T24.27 | Start the PHP of the repository in every workflow, the release included. The release workflow that kit copies runs setup-php only when the checkout has a .php-version, and template had none: its CI jobs named 8.5 in each setup-php step, so the release job of v0.0.7 (run 38681972237) kept the PHP 8.3.6 of the runner, and Composer refused the consumer project of the proof ("Your Composer dependencies require a PHP version ">= 8.5.0""). .php-version holds 8.5, the minimum release of php of config/toolchain.json; every setup-php step of ci.yml and dependency-review.yml reads php-version-file: .php-version, except the php job, whose matrix lists the releases of config/toolchain.json. RED: tests/scripts/php-version.test.mts reports that .php-version does not exist and 5 setup-php steps that name 8.5 | tests/scripts/php-version.test.mts; tests/scripts/toolchain-files.test.mts; make toolchain-check; make owner-validate | [o] |
| T24.28 | Release 0.0.8, which supersedes 0.0.7: the tag v0.0.7 has no published release, since its proof ran the PHP of the runner (T24.27). Every manifest of config/release.json declares 0.0.8, with the dependencies between the packages of the repository, the PHP extension header, package-lock.json, the Cargo locks and the Composer locks, each re-locked in its folder (kit K180); the consumer projects of the release assets record 0.0.8 (make release-consumer-lock), and their smoke commands pass on the archives (make release-consumer); CHANGELOG.md and CHANGELOG.ko.md gain ## 0.0.8 below a new empty ## Unreleased; the review records the new locks; the tags v0.0.8 and packages/go/v0.0.8 name the release commit | make release-versions TAG=v0.0.8; make release-consumer TAG=v0.0.8; make dependency-policy-check; make artifact-digest-check | [o] |
| T24.29 | Emit only what a generated Rust or TypeScript program uses, as its consumer compiles it. template-benchmark compiles a program without definitions whose loops read no metadata, includes the Rust program as a private module of a binary crate and lints the TypeScript program; with the compiler of v0.0.8 clippy -D warnings reports DefinitionDataInputTpl, Definition and ArtifactManifest as never constructed, render_values as never used (a stub of a static root that only returns an error), and clippy::never_loop and clippy::match_single_binding in generated_bind_definitions, and oxlint no-unused-vars reports raw of the definition loop and the unused <loop>_size of each loop. The Rust backend declares Definition<T> only when the program declares a definition, DefinitionData<T> only for a template that a definition names, no ArtifactManifest and no render_values for a static root, and without definitions generated_bind_definitions fails on the first entry of the request instead of a loop with a single match arm; the TypeScript backend reads raw only when a definition is declared and declares <loop>_size only when the loop reads its size, its last item or has an empty branch. make generated-lint-check lints the Rust programs as a binary crate that renders each one and compiles tests/fixtures/generated-lint, a program without definitions whose loops read no metadata. RED: make generated-lint-check reports these 9 findings | make generated-lint-check; make conformance-generated-rust; make conformance-generated-ts; make artifact-digest-check; make typecheck; make lint-ts | [o] |
| T24.30 | Release 0.0.9 with the generated programs of T24.29. Every manifest of config/release.json declares 0.0.9, with the dependencies between the packages of the repository, the PHP extension header, package-lock.json, the Cargo locks and the Composer locks, each re-locked in its folder (kit K180); the consumer projects of the release assets record 0.0.9 (make release-consumer-lock), and their smoke commands pass on the archives (make release-consumer); CHANGELOG.md and CHANGELOG.ko.md gain ## 0.0.9 below a new empty ## Unreleased; the review records the new locks; the tags v0.0.9 and packages/go/v0.0.9 name the release commit | make release-versions TAG=v0.0.9; make release-consumer TAG=v0.0.9; make dependency-policy-check; make artifact-digest-check | [o] |
| T24.31 | Lint the generated Rust programs of the showcase as a consumer compiles them, and emit only the records, fields and conversions that a program uses. Since T24.29 make generated-lint-check lints only tests/fixtures/generated-lint as a private module of a binary crate; the 5 showcase scenarios stay public modules of a library, where clippy treats every public item as used. As a binary, clippy -D warnings reports in them: the record Slot never constructed (compiler-coverage, react-boundary), the fields template and data of Definition never read, to_value never used and the field page of Assign never read (empty-state). The check includes every scenario as a private module of the binary that renders it, and the Rust backend declares a record, a field and a conversion only when the program uses it, with no #[allow] and no name that starts with _. RED: make generated-lint-check with the scenarios in the binary reports these findings | make generated-lint-check; make conformance-generated-rust; make artifact-digest-check; make typed-generator-compile-check | [o] |
| T24.32 | Decode a template AST in a browser bundle without the code that the decoding does not need. hyper bundles decodeTemplate from @polyspec/template/render 0.0.8, and an esbuild bundle (browser, ES module, minified) of that import alone is 127,057 bytes, 13,479 gzip: the standalone validator of the AST schema is 110,656 bytes, because ajv inlines the 93 $ref of the schema into each use, and tsup puts every module that the entries share into one chunk whose top-level code the bundler cannot drop (15.5 KB of the runtime). ajv compiles the schema with inlineRefs: false, so each definition of the schema is one function (85,678 bytes); @polyspec/template/decode is a public entry of decodeTemplate and AstDecodeError, whose own chunk the bundle of the render entry takes without the runtime. The validator keeps every check and every error message. RED: tests/scripts/decode-bundle-size.test.mts reports the bundles of /render and /decode above the budget of 90,000 bytes minified and 7,900 gzip (86,538 and 7,490 after the change) | tests/scripts/decode-bundle-size.test.mts; make ast-validator-check; make test-ts; make lint-ts; make test-browser | [o] |
| T24.33 | Report an argument error of a request in one message format in every target. A request whose assign or definition data does not match the declared types fails with an argument error (ERR-13) whose text differs by target: TypeScript writes assign.title is not a string, Rust title: expected a string. The spec defines one format of the message, with the path of the invalid value from the request, and every generated backend and runtime writes it; cross-target vectors assert the exact text of each case | make generated-arguments-check; make docs-check | [o] |
| T24.34 | Release 0.0.10 with T24.31, T24.32 and T24.33. Every manifest of config/release.json declares 0.0.10, with the dependencies between the packages of the repository, the PHP extension header, package-lock.json, the Cargo locks and the Composer locks, each re-locked in its folder (kit K180); the consumer projects of the release assets record 0.0.10 (make release-consumer-lock), and their smoke commands pass on the archives (make release-consumer); CHANGELOG.md and CHANGELOG.ko.md gain ## 0.0.10 below a new empty ## Unreleased; the review records the new locks; the tags v0.0.10 and packages/go/v0.0.10 name the release commit | make release-versions TAG=v0.0.10; make release-consumer TAG=v0.0.10; make dependency-policy-check; make artifact-digest-check | [o] |
| T24.35 | Move to kit 0.0.52: pin @polyspec/kit 0.0.52 with the integrity of its published file and write its copies with make kit-sync (the release workflow of K193, which runs kit-check before any release step). The copy of knip.json keeps an ignore only where knip needs it (K188), so make lint-ts gives no hint about @polyspec/kit; npm runs with allow-remote=root from kit-bootstrap.mk (K189); .mise/locks/pypi-vulture is tracked with mise.lock (K192); and the checkout declares its PHP in .php-version, which every setup-php step reads (T24.27, K193) | make kit-check; make toolchain-check; make layout-check; make lint-ts; make dependency-policy-check | [o] |
| T24.36 | Report a define option that is not an object as the argument error define is not an object in every target whose request can hold it. The generated TypeScript program raised it as a BindError with E_DATA_UNSUPPORTED_TYPE, PHP failed with E_INTERNAL on the type of bindDefinitions, and Python with an AttributeError; the typed options of Go and Rust cannot hold such a value. E_DATA_UNSUPPORTED_TYPE stays a code of the data model, since binding raises it for a value that has no binding (ERR-5, ERR-14). RED: the request of tests/fixtures/typed-arguments/messages.json whose define is 5 fails in TypeScript, PHP and Python | make generated-arguments-check; make docs-check | [o] |
| T24.37 | Render a block of a template path whose argument has no root value in the generated PHP program. {# part.tpl name:title} gives the input name of part.tpl the value of title in the scope of the block, but the PHP backend built the input from the root value or the definition data only, threw generated input part.tpl.name is missing before it assigned the scoped value, and so failed with E_RUNTIME_TYPE where TypeScript, Go, Rust and Python render it. RED: the request of tests/fixtures/typed-arguments-scoped/messages.json fails in PHP | make generated-arguments-check; make conformance-generated-php | [o] |
| T24.38 | Release 0.0.11 with T24.35, T24.36 and T24.37. Every manifest of config/release.json declares 0.0.11, with the dependencies between the packages of the repository, the PHP extension header, package-lock.json, the Cargo locks and the Composer locks, each re-locked in its folder (kit K180); the consumer projects of the release assets record 0.0.11 (make release-consumer-lock), and their smoke commands pass on the archives (make release-consumer); CHANGELOG.md and CHANGELOG.ko.md gain ## 0.0.11 below a new empty ## Unreleased; the review records the new locks; the tags v0.0.11 and packages/go/v0.0.11 name the release commit | make release-versions TAG=v0.0.11; make release-consumer TAG=v0.0.11; make dependency-policy-check; make artifact-digest-check | [o] |
| T24.39 | Move to kit 0.0.53: pin @polyspec/kit 0.0.53 with the integrity of its published file and write its copies with make kit-sync, so that template uses the kit of the other repositories. The owner check selects the tests of a changed module by its importers (K191, optional fields that template does not need), kit-check requires .php-version for a checkout that needs PHP, which template has (K193), and the release proof lists each sibling wheel (K195); the review records the new lock | make kit-check; make toolchain-check; make layout-check; make lint-ts; make dependency-policy-check | |
| T24.40 | Point to the local links of 0.0.x. While polyspec versions are 0.0.x, development checkouts link installed polyspec packages to their source as kit docs/plans/local-links.md describes (kit K198), and AGENTS.md states this in one sentence without its content. Acceptance: AGENTS.md and AGENTS.ko.md hold the sentence beside the installation of @polyspec/kit. GREEN: make documents-check passes | make docs-check |
Wave 25 — Generated programs as code of the consumer
| ID | Task | Verification | Done |
|---|---|---|---|
| T25.1 | Emit only what the program uses, in the canonical form of each target. The generated Go, Rust, TypeScript and PHP programs carry no helper, type, method, import or variable that the program does not use, so the lint of a consumer passes on them (template-benchmark K79-3, the showcase lint findings of T24.6): Go has no unused helper, OrderedMap method or strconvQuote, Rust no dead_code, clippy::match_single_binding or clippy::never_loop, TypeScript no unused generatedToValue or loop metadata variable. The Go program starts with the header // Code generated by @polyspec/template-compiler. DO NOT EDIT. of the Go convention and is unchanged by gofmt. The PHP target writes one PSR-4 file per class, with PascalCase class names, under the namespace and the directory that the compile options name, and the artifact manifest lists every file; the showcase and the generated conformance runner load the classes through the PSR-4 autoload of Composer. The emitters produce that form themselves, with no formatter at compile time. RED: on a generated fixture of each target, gofmt -l, golangci-lint unused, clippy with -D warnings on a library crate that includes it, oxlint no-unused-vars, pint --test --preset psr12 and phpcs PSR12 report findings. The committed artifacts are compiled again | make generated-lint-check | [o] |
| T25.2 | Keep the generated PHP within the 120 columns of PSR-12, and state how generated Rust is consumed. PSR-12 sets a soft limit of 120 columns, which phpcs reports (Generic.Files.LineLength). The PHP emitter marks every argument list, array, condition of a control structure and operator chain (&&, ` | , ??and the ternary) as a group and splits a group whose line passes 120 columns as PSR-12 splits it, one item per line, indented once (sections 4.5, 4.7, 5 and 6); it binds no operand to a variable, so the right operand of&&, | |
| T25.3 | Emit the generated Python in the canonical form of ruff format, clean for ruff check and vulture. The Python emitter writes the form itself, with no formatter at compile time: double quotes unless a string holds more double than single quotes, two blank lines between top-level definitions, and a group (an argument or parameter list, a collection or a parenthesized operator chain) that does not fit in 88 columns split one item per line, a list with a trailing comma that ruff format keeps split; it keeps only the helpers, schema constants, imports and loop metadata that the program uses, and writes the definition of _generated_bind_assign that the root needs, with no constant condition. RED: on the generated Python of the showcase, ruff format --check reports 5 files, ruff check 4 unused imports (F401) and vulture an unsatisfiable if condition and the unused loop metadata key_ and value_. The committed artifacts are compiled again | make generated-lint-check | [o] |
| T25.4 | Keep the Go that the Go backend writes as it is in Go files. packages/compiler/backends/go/ holds OrderedMap and each of the 25 helpers in its own file of the package generated, in the format of gofmt, and backends/go.mjs reads the declarations of a file after its package clause and imports when it writes a program; make lint-go-format runs gofmt -l and make test-go-backend-vet runs go vet on the folder, and the compiler digest of Go covers its files. RED: backends/go.mjs writes these 26 Go sources as JavaScript template literals with two-space indentation under the tag gofmt, which turns the leading spaces into tabs only so that the module passes .editorconfig; that tag is the workaround. GREEN: the tag gofmt is gone, the generated Go of every committed artifact is unchanged byte for byte (only the compiler digests of the manifests and of examples/site/index.html change), and gofmt -l lists no file of the folder | make lint-go-format, make test-go-backend-vet, make artifact-digest-check, make typed-generator-check | [o] |
| T25.5 | Emit generated TypeScript that states its types without assertions. The TypeScript backend wrote as unknown as Value, as GeneratedType, as string, assertions that change no type and a Map<any, any> return, so kit's type-aware oxlint rules reported 196 findings in the generated programs of examples/showcase/adapters/generated/typed/*.ts, and the casts hid values of the wrong type, such as a record in the render scope. A value of a type of the type manifest now has its TypeScript type (an optional value is T | null, any is Value), the program turns a value that holds a record into a Value with an encoder of its record, and checks a Value that it reads back, from the scope or the runtime, with a decoder, which fails with E_RUNTIME_TYPE at the position of the value while the program renders; a block builds its input field by field and fails with E_RUNTIME_TYPE when a required field has no value, as the PHP backend does. RenderOptions.define and env of @polyspec/template are unknown, which the engine and the generated program check, failing with define is not an object or env is not an object on a value that is no plain object. RED: 196 oxlint findings in the generated TypeScript, and 10 errors of tsc strict. GREEN: make typed-generator-compile-check type checks the generated programs with --strict, --noUncheckedIndexedAccess and --exactOptionalPropertyTypes, oxlint reports no finding in them, and the showcase renders the same output | make lint-ts, make typed-generator-compile-check, make generated-native-check, make conformance-generated-ts, make test-ts, make showcase-build-check, make artifact-digest-check | [o] |
| T25.6 | Every runtime publishes the decode of a template AST from JSON. Go publishes ast.DecodeTemplate, but the TypeScript package publishes no such function, so a consumer that reads a compiled AST from JSON (the server and the client of hyper) can only cast it (JSON.parse(...) as Template) or write a shallow guard of its own; Rust derives Deserialize for its AST but publishes no decode, reads a struct tag without checking it, accepts unknown fields and reads a missing else, empty, id, path or then as null. TypeScript publishes decodeTemplate(json) from the package entry and the render-only entry, which validates with ajv against contracts/schema/ast.schema.json (bundled by the build, no second copy) through compile<Template> and throws an AstDecodeError whose path is the JSON Pointer of the innermost invalid node; Rust publishes decode_template(json), which rejects an unknown or a missing field of every node with serde and names the path with serde_path_to_error, up to the statement node, since an internally tagged node buffers its fields. PHP and Python are excluded by AST-10: their AST is arrays and dicts with no type of its own. docs/spec/ast.md (AST-10), docs/spec/runtime.md (RT-1) and the READMEs name the functions, and the parity unit parsed-ast binds the decode tests of TypeScript and Rust. RED: packages/ts/tests/decode.test.ts and packages/rust/tests/decode.rs, which decode the expected.ast.json of every conformance case and fail invalid inputs with their path, do not compile | packages/ts vitest tests/decode.test.ts, cargo test --test decode, make parity-declarations-check | [o] |
| T25.6-1 | Decode the AST JSON by one rule in every runtime: a runtime decodes a template AST from JSON by validating it against contracts/schema/ast.schema.json, and an invalid AST fails with the JSON Pointer of the invalid node. After T25.6, Rust did not check the identifier pattern, the kind of a literal value, minLength or minItems, and its path stopped at the statement node; Go's ast.DecodeTemplate named no path and accepted unknown fields; PHP and Python published no decode (AST-10 excluded them); and the showcase adapters of TypeScript, JavaScript, Rust, PHP and Python read the compiled AST with ajv of their own, JSON.parse, serde, json_decode and json.loads instead of their runtime. The schema selects the definition of a node by its type with if/then instead of oneOf, so a validator reports an error at the invalid value itself; every runtime validates with the JSON Schema validator of its ecosystem (ajv, santhosh-tekuri/jsonschema/v6, the crate jsonschema, opis/json-schema, jsonschema of PyPI, each at its latest stable release) and names the deepest instance location of the errors, the least in code point order at that depth. TypeScript bundles the contract file; Go (embed), Rust (include_str!), Python (package data) and PHP (resources/) hold a copy that a test of the package compares with it. PHP publishes AstProgram::decodeTemplate and Python decode_template, which return the arrays and dicts of their parser. Python's dependencies are pinned by packages/python/pylock.toml (make python-lock, checked by make python-lock-check under make dependency-audit), which make python-package installs and make install downloads. The showcase adapters decode with their runtime; the parity unit parsed-ast binds the decode test of every runtime and the unit ast-decode-errors the test of the shared invalid ASTs; AST-10 states the rule without the exclusion, and the PHP extension, which accepts no AST, publishes none. RED: with the shared fixture tests/fixtures/ast-decode/invalid.json, Rust fails on its paths, TypeScript on the old schema names /body/0 for an unknown node type, and the tests of Go (*ast.DecodeError), PHP and Python do not compile or import. GREEN: every runtime names the pointer of each invalid AST of the fixture and decodes every expected.ast.json | packages/ts vitest tests/decode.test.ts, go test ./ast, cargo test --test decode, phpunit tests/AstDecodeTest.php, unittest test_decode.py, make schema-check, make python-lock-check, make parity-declarations-check | [o] |
| T25.6-2 | Ship the AST validator of @polyspec/template without ajv at run time. Since T25.6 decodeTemplate compiled contracts/schema/ast.schema.json with ajv when it ran, so the package depended on ajv: the chunks of packages/ts/dist imported the bare specifier ajv, which the page of the browser test cannot resolve (index.html defines no renderCases function), the offline install of the packed archive failed with ENOTCACHED for ajv, and a Content-Security-Policy without 'unsafe-eval' would refuse the new Function of the compile; and no evidence group named AST-10, so make rules-check reported it uncovered. scripts/generate-ast-validator.mts writes ajv's standalone validation code (ajv/dist/standalone) of the schema, with the options of decodeTemplate and the runtime modules of ajv bundled by esbuild, into packages/ts/src/ast-validator.mjs (make ast-validator); make build-ts runs make ast-validator-check, which fails when the file is stale; ajv is a development dependency of the package; decodeTemplate keeps its JSON Pointer rule and its errors; the evidence group ast-decode of tests/rule-evidence.json names AST-10 with the decode tests of every runtime. RED: make test-browser, make install-check (ENOTCACHED for ajv in CI) and make rules-check (uncovered: AST-10) fail. GREEN: the three pass, with packages/ts vitest tests/decode.test.ts | make test-browser, make install-check, make rules-check, packages/ts vitest tests/decode.test.ts | |
| T25.7 | Fail a date string whose offset is out of range. An offset such as +25:00 or +05:60 matches the date pattern `(Z | [+-]\d{2}:\d{2}), the offset parser rejects it, and TypeScript (parseOffset(...) as number), Go (offset, _ = ParseOffset), PHP ((int) self::parseOffset) and the PHP extension (offset = 0) read the string with the offset 0, while Python subtracts Noneand fails withE_INTERNAL; FUN-38 says that an unparseable string fails with E_RUNTIME_TYPE. RED: the cases functions/date-offset-hour-error (+25:00) and functions/date-offset-minute-error (+05:60), which expect E_RUNTIME_TYPE, fail in TypeScript, Go, PHP, Python and the PHP extension, and so do the date unit tests of each runtime that assert the same. GREEN: every runtime fails with E_RUNTIME_TYPEon such a string with no default offset, Rust keeps the offset that its date parser read instead ofand_then(parse_offset).unwrap_or(env_offset), the generated programs call the runtime functions and pass the cases, and FUN-37 states the range 00to23and00to59` of an offset (en/ko) | the date unit tests of each runtime, node tests/runner/conformance.mts --case functions/date-offset-hour-error and functions/date-offset-minute-error, the generated runners with these cases, make parity-declarations-check |
| T25.8 | Decode a value of a generated program once, where it enters. Since T25.5 the TypeScript backend read a typed local from the render scope and checked its whole value with a decoder on every read, and the Go backend decoded it on every read by a JSON round trip, so a read inside a loop cost the size of the value and repeated a check that the type guarantees; a loop over a typed list encoded the list and decoded each item again on every read. A local is now a variable of the generated function that holds its typed value: a template input is its field, an assignment and a loop item declare a variable, a branch or a loop that may assign a local holds it afterwards in a variable of the type that the IR gives it there, and every read uses the variable without a check. The render scope keeps the Value of each local for includes and dynamic lookups (RT-21), so after an include each local that the included templates may assign is decoded once from the scope. A loop iterates a typed list or map as it is. An error of an intermediate result names its expression in the syntax of the template, such as label(page.title) is not a string, at the position of the expression, instead of index, + or the name of the function. RED: scripts/check-generated-typed-locals.mts (make generated-typed-locals-check, part of make generated-native-check) finds that the generated TypeScript and Go read a local from the render scope. GREEN: the generated source of both reads no local from the scope and decodes only the result of the host call, a wrong result fails with E_RUNTIME_TYPE at 1:12 and that message, and the showcase and the conformance cases render the same output | make generated-native-check, make typed-generator-compile-check, make conformance-generated-ts, make conformance-generated-go | [o] |
| T25.9 | Express by construction what T25.8 left to ?? defaults in the backends. The TypeScript and Go backends emitted each condition of an if as tests[index] ?? '', so a missing test would have emitted if () instead of an error; each branch now carries its test, emitted before the branch as the comment of the if requires, so no index is looked up. FunctionLocals.merged gave a local that the IR does not widen its type before (widened.get(name) ?? before.type) for an if or a loop too, where widened names each assigned local; an if-block, which the IR does not widen, now passes null, and a local missing from widened throws. The typed-locals check found the function of a line by indexing lines[at] ?? ''; it now walks the lines once. otherwise ?? [] and empty ?? [] stay: an if without else and a loop without empty have no statements, which the IR records as null. No output changes, so there is no RED; GREEN: every generated program and digest is unchanged | make owner-check of packages/compiler/** and scripts/check-generated-typed-locals.mts | [o] |