실행 체크리스트
Wave 0 — 저장소 기반 (순차)
| ID | 작업 | 산출물 | 검증 | 완료 |
|---|---|---|---|---|
| T0.1 | 저장소 초기화와 툴체인 고정 | .gitignore, .node-version (26.8.1), rust-toolchain.toml (1.98.1, rustfmt, clippy), .editorconfig | 커밋 후 git status 비어 있음; node --version 일치 | [o] |
| T0.2 | 개발 규칙 | AGENTS.md, AGENTS.ko.md: 영어·한국어 문서 쌍, 계약은 docs/spec/, 상태는 docs/features.md, 절차는 docs/operations/, 변경은 CHANGELOG.md, 가장 단순한 구현, 멱등한 명령, 코드·테스트 분리, 실패 테스트 후 수정 절차, 문체(동작 이름 직접 사용, 주체와 대상 명시, 원인은 한 문장, 비유 금지) | make docs-check | [o] |
| T0.3 | 최상위 문서 | README.md(.ko), CHANGELOG.md(.ko), docs/index.md(.ko), docs/features.md(.ko). 모든 기능 행은 not-started/pending/not-deployed와 근거 링크 | make docs-check | [o] |
| T0.4 | 워크스페이스와 Makefile | package.json (workspaces packages/*, 스크립트 lint, test, docs:check), Makefile 타겟 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; 패키지가 아직 없는 타겟은 not implemented 한 줄을 출력하고 1로 종료 | make help | [o] |
| T0.5 | 문서 검사기 | scripts/check-documents.mjs: 영어·한국어 쌍 필수, 상대 링크 해석, 두 언어의 코드 블록 동일, 기능 상태 필드 유효 | node scripts/check-documents.mjs | [o] |
| T0.6 | 이 체크리스트 | docs/plans/execution-checklist.md(.ko) | make docs-check | [o] |
| T0.7 | 운영 문서 | docs/operations/development.md(.ko): 툴체인 설치, make 타겟, 커밋 절차; docs/operations/documentation.md(.ko): 문서 규칙과 검사기 | make docs-check | [o] |
Wave 1 — 명세 (병렬)
| ID | 작업 | 산출물 | 내용 | 검증 | 완료 |
|---|---|---|---|---|---|
| T1.1 | 렉시컬 명세 | docs/spec/lexical.md(.ko) | UTF-8과 BOM 처리; 줄 구분자; 태그 시작 규칙({ 뒤에 수평 공백과 기호, 또는 {: 뒤에 식별자와 대입 연산자); 그 외는 텍스트; 태그가 시작될 자리에서만 읽히는 이스케이프 \{; 주석 {* *}; 문자열 리터럴 밖·표현식 깊이 0의 닫는 구분자로 태그 종료; echo 이외 태그의 standalone 줄 제거 규칙; 텍스트로 남는 JavaScript와 CSS 중괄호 예시 | make docs-check, node tests/runner/delimiter-matrix.mjs | [o] |
| T1.2 | 태그 문법 | docs/spec/grammar.md(.ko) | echo, loop, if, elseif, else, close, include, block, if-block, 대입의 EBNF; 블록 중첩과 닫기 규칙; 루프 안 {:}는 빈 분기; 규칙별 오류 조건; bare 경로와 따옴표 경로 토큰 정의; 블록 태그 토큰 순서(선택 id, 선택 경로, name 또는 name:postfix 형태의 scope 항목) | make docs-check | [o] |
| T1.3 | 표현식 명세 | docs/spec/expressions.md(.ko) | 토큰 표; EBNF; 우선순위 표(파이프, 삼항과 엘비스, 말미 축약을 포함한 병합, or, and, 동등, 비교와 in, 덧셈, 곱셈, 단항, postfix); 경로 접근과 조회 규칙; 전용 노드로 해석되는 루프 메타 name.index_ key_ value_ last_ first_ size_; + 규칙(한쪽이 문자열이면 결합, 아니면 숫자 덧셈); 그 외 산술은 숫자 전용; 동등, 엄격 동등, 순서, 진릿값; =>와 spread를 포함한 list·map 리터럴 | make docs-check | [o] |
| T1.4 | 데이터 모델 | docs/spec/data-model.md(.ko) | 값 타입; 안전 정수 범위를 가진 IEEE 754 double 숫자; 문자열→숫자 변환 문법; ECMAScript Number::toString과 동일한 숫자→문자열 규칙과 언어별 구성 방법; bool과 null 출력; 순서 있는 map; JSON, JavaScript, PHP, Go, Rust 바인딩 표; 거부 코드 | make docs-check | [o] |
| T1.5 | 함수 | docs/spec/functions.md(.ko) | 모든 내장 함수의 시그니처, 인자 수, 의미, 오류 경우; 파이프 변환; number 십진 반올림 규칙; json 이스케이프 규칙; date 토큰 표와 시간대 규칙; 환경에서 오는 now; 호스트 등록 계약; raw와 json의 safe 값 규칙 | make docs-check | [o] |
| T1.6 | 런타임 | docs/spec/runtime.md(.ko) | 4개 언어의 엔진 API; 렌더 호출 형태(템플릿 이름 또는 AST, assign 데이터, define map, 환경); 파일 단위 평탄한 로컬 스코프; 루프 변수 바인딩과 복원; 스코프를 공유하는 include; 격리 컨텍스트(assign 루트, define 데이터, scope 인자)의 템플릿 define; define 항목(문자열 경로, 선택 data를 가진 template, 또는 html); 레이아웃 target 선택; 재정의 규칙; 로더 인터페이스와 이름 해석; strict와 lenient 모드; 제한(반복, 깊이, 출력 크기, 표현식 깊이); 출력 이스케이프; 브라우저용 assign 데이터의 HTML 임베딩 | make docs-check | [o] |
| T1.7 | AST 스키마 | docs/spec/ast.md(.ko), schema/ast.schema.json, schema/README.md(.ko) | 필드와 span 바이트 오프셋을 가진 노드 목록; 파이프·복합 대입·증감의 변환; 직렬화 규칙(숫자 리터럴은 JSON 숫자, 문자열 리터럴은 해석된 값); 모든 노드를 다루는 JSON Schema draft-07 | node scripts/check-schema.mjs (스키마 자체 검증) | [o] |
| T1.8 | 오류 | docs/spec/errors.md(.ko) | 오류 객체 필드; 단계별(lex, parse, load, data, runtime) 전체 코드 목록; 적합성 검사가 비교하는 필드; 코드별 모드 동작 | make docs-check | [o] |
| T1.9 | 적합성 | docs/spec/conformance.md(.ko) | 픽스처 디렉터리 구조와 파일 역할; CLI 계약(parse FILE, render FILE --data --define --env --root, 종료 코드, stdout/stderr); 비교 규칙(AST 구조, HTML 바이트, 오류 필드); 러너 옵션; 표현식 픽스처 형식(토큰, AST, 값) | make docs-check | [o] |
| T1.10 | 예제 | docs/spec/examples.md(.ko) | 페이지 하나 전체: 레이아웃, 헤더 파셜, scope 인자를 가진 푸터 define, 루프 메타를 쓰는 목록, 카드 define, assign 데이터 파일, define 파일, 기대 출력, AST 발췌 | make docs-check | [o] |
| T1.11 | 명세 검토 (순차) | docs/spec/* 전반 수정 | 용어 일관성; 본문에 쓰인 모든 오류 코드가 errors.md에 존재; expressions.md/examples.md의 모든 함수가 functions.md에 존재; 모든 규칙에 ID; 두 언어가 같은 정보 | make docs-check; CHANGELOG.md에 검토 기록 | [o] |
Wave 2 — 적합성 자산 (병렬)
| ID | 작업 | 산출물 | 검증 | 완료 |
|---|---|---|---|---|
| T2.1 | 러너 드라이버 | tests/runner/drivers.mjs: 언어별 명령, 빌드 명령, 작업 디렉터리, 바이너리 경로 | node tests/runner/conformance.mjs --list | [o] |
| T2.2 | 적합성 러너 | tests/runner/conformance.mjs: 케이스 열거, 타임아웃이 있는 언어별 호출, AST 구조 diff, HTML 바이트 비교, 오류 필드 비교, 옵션 --langs, --case, --list, --update, 표 출력, 실패 시 0이 아닌 종료 | node tests/runner/conformance.mjs --list | [o] |
| T2.3 | 일치 러너 | tests/runner/parity.mjs: 기대 파일 없이 언어 간 출력 비교, 클라이언트/서버 축 보고 | node tests/runner/parity.mjs --help | [o] |
| T2.4 | 스키마 검사기 | scripts/check-schema.mjs: ajv로 schema/ast.schema.json을 검증하고 모든 tests/cases/**/expected.ast.json을 스키마로 검증 | node scripts/check-schema.mjs | [o] |
| T2.5 | 픽스처: text | tests/cases/text/: 일반 텍스트, 텍스트로 남는 중괄호({ debug: true }, {}, ${x}, {a:1}, { 뒤 개행), 태그 위치의 이스케이프, 그 외 위치의 이스케이프, 주석 제거, 여러 줄 주석, BOM 제거, CRLF 입력. 4건 AST 수기 | node scripts/check-schema.mjs | [o] |
| T2.6 | 픽스처: echo | tests/cases/echo/: 경로, 중첩 경로, 숫자 인덱스, 없는 키, 5문자 HTML 이스케이프, raw, 숫자 포맷 경계, bool과 null 출력, list 출력 오류. 3건 AST 수기 | node scripts/check-schema.mjs | [o] |
| T2.7 | 픽스처: if | tests/cases/if/: 기본, elseif 체인, else, 중첩, 진릿값 표("0", 빈 list, 빈 map, null), 블록 밖 else 오류, 중복 else 오류 | node scripts/check-schema.mjs | [o] |
| T2.8 | 픽스처: loop | tests/cases/loop/: list, map 순서, 루프 메타 필드, 두 메타를 쓰는 중첩 루프, 빈 분기, null 반복 대상, 루프 뒤 변수 복원, 루프 안 elseif 오류, 알 수 없는 루프 메타 오류 | node scripts/check-schema.mjs | [o] |
| T2.9 | 픽스처: include와 block | tests/cases/include/, tests/cases/block/: 로컬을 공유하는 include, include 순환 오류, 레지스트리 블록, 경로와 등록을 가진 블록, scope 인자(name과 name:value)를 가진 블록, 블록의 로컬 격리, html 레지스트리 항목, 미정의 블록 오류, 재정의 오류, if-block 참과 거짓, ../가 있는 bare 경로, 루트 밖 경로 오류 | node scripts/check-schema.mjs | [o] |
| T2.10 | 픽스처: 표현식 | tests/cases/expr/: 우선순위, 숫자 +, 한쪽이 문자열인 +, list + 오류, 0 나눗셈 오류, 나머지, 삼항, 엘비스, 말미 ??, list·map·string의 in, 동등 표, 엄격 동등, 혼합 타입 순서 오류, list·map 리터럴, spread, 파이프 체인, 호출 인자 수 오류 | node scripts/check-schema.mjs | [o] |
| T2.11 | 픽스처: 함수 | tests/cases/functions/: 경계 입력을 포함해 내장 함수마다 한 케이스; number(2.675, 2), number(1.005, 2), <·&·U+2028을 포함한 json, url 예약 문자, date 토큰과 오프셋, 환경에서 오는 now, 알 수 없는 함수 오류 | node scripts/check-schema.mjs | [o] |
| T2.12 | 픽스처: 데이터 모델 | tests/cases/data/: 정수형 키를 포함한 map 키 순서, 0.1+0.2, 1e21, 1e-7, -0, 안전 정수 경계 거부, 아스트랄 문자 길이와 순서, 코드포인트 비교, 잘못된 UTF-8 거부 | node scripts/check-schema.mjs | [o] |
| T2.13 | 픽스처: 공백 | tests/cases/whitespace/: standalone 태그 줄, 한 줄의 여러 닫기, standalone이 아닌 echo, 루프 안 들여쓰기, 주석만 있는 줄, 줄을 유지하는 인라인 태그 | node scripts/check-schema.mjs | [o] |
| T2.14 | 픽스처: 오류 | tests/cases/errors/: 미종료 태그, 미종료 문자열, 미종료 주석, 예상치 못한 토큰, 예상치 못한 닫기, 닫히지 않은 블록, 예약어 대입, 잘못된 이스케이프, 잘못된 숫자; 각각 expected.error.json | node scripts/check-schema.mjs | [o] |
| T2.15 | 표현식 픽스처 | tests/fixtures/expr/cases.json (30건: 토큰, AST, 평가 값)과 tests/fixtures/expr/README.md(.ko) | node scripts/check-schema.mjs | [o] |
| T2.16 | 규칙 커버리지 검사기 | scripts/check-rules.mjs: 모든 case.json 규칙이 docs/spec에 존재, 다루지 않은 규칙 보고; check에 포함되는 Makefile 타겟 rules-check | node scripts/check-rules.mjs | [o] |
| T2.17 | 픽스처: 래퍼 태그 | tests/cases/wrapper/: 따옴표·주석·HTML 주석 래퍼, 래퍼 없는 {{는 텍스트, "{= x}"는 따옴표 유지, 래퍼 닫기 누락 오류, standalone 줄의 래퍼 태그 | node scripts/check-schema.mjs | [o] |
| T2.18 | 픽스처: 구분자 | tests/cases/delimiters/: options.json 구분자, 파일 지시문, 옵션을 덮는 지시문, 문법이 쓰는 닫는 구분자([= a[0]]), 첫 태그가 아닌 지시문 오류, 잘못된 구분자 문자 오류, 사용자 여는 구분자의 이스케이프 | node scripts/check-schema.mjs | [o] |
Wave 3 — TypeScript 구현
| ID | 작업 | 산출물 | 테스트 | 검증 | 의존 | 완료 |
|---|---|---|---|---|---|---|
| T3.1 | 패키지 골격 | package.json (tsup 빌드: esm, cjs, dts, neutral 플랫폼; exports ., ./render, ./node), tsconfig.json (lib: ["ES2020"], types: []), vitest.config.ts, 루트 eslint.config.mjs | tests/smoke.test.ts | npm run build -w @polyspec/template | — | [o] |
T3.2 parallel | 값 모델 | src/value/value.ts (타입, 진릿값, 동등, 순서), src/value/number.ts (숫자 변환, ECMAScript 문자열화), src/value/bind.ts (호스트 입력 변환) | tests/value/*.test.ts | npm test -w @polyspec/template | T3.1 | [o] |
T3.3 parallel | 오류 | src/errors.ts (오류 클래스, 코드, 위치) | tests/errors.test.ts | 동일 | T3.1 | [o] |
T3.4 parallel | 이스케이프와 출력 | src/escape.ts, src/output.ts (크기 제한이 있는 빌더, safe 값) | tests/escape.test.ts | 동일 | T3.1 | [o] |
| T3.5 | 템플릿 렉서 | src/lexer/template.ts (텍스트, 태그 시작 규칙, 이스케이프, 주석, 문자열을 고려한 태그 본문 추출, 위치) | tests/lexer/template.test.ts | 동일 | T3.3 | [o] |
T3.6 parallel | 표현식 렉서와 파서 | 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 (tests/fixtures/expr/cases.json 로드) | 동일 | T3.3 | [o] |
| T3.7 | 템플릿 파서 | src/parser/parser.ts (태그, 블록 스택, else와 빈 분기), src/parser/standalone.ts (줄 제거), src/parser/block-tag.ts (id, 경로, scope 항목), src/ast.ts (노드 타입, JSON 직렬화) | tests/parser/*.test.ts, tests/ast-schema.test.ts (파싱한 모든 픽스처가 schema/ast.schema.json 검증 통과) | 동일 | T3.5, T3.6 | [o] |
| T3.8 | 내장 함수 | src/functions/index.ts, 그룹별 파일(string.ts, collection.ts, number.ts, encoding.ts, date.ts) | tests/functions/*.test.ts | 동일 | T3.2, T3.4 | [o] |
T3.9 parallel | 로더 | src/loader.ts (인터페이스, MapLoader, 이름 해석, 루트 검사), src/node/loader.ts (FsLoader) | tests/loader.test.ts | 동일 | T3.3 | [o] |
| T3.10 | 렌더러 | src/render/engine.ts (엔진, 템플릿 define 등록, 이름과 버전 기준 캐시), src/render/context.ts (프레임, 루프 메타, 템플릿 define 레지스트리, 제한, 모드), src/render/statements.ts, src/render/expressions.ts | tests/render/*.test.ts | 동일 | T3.7, T3.8, T3.9 | [o] |
| T3.11 | CLI | CLI 계약을 구현하는 bin/template.mjs | tests/cli.test.ts | node packages/template-ts/bin/template.mjs parse tests/cases/text/plain/input.tpl | T3.10 | [o] |
| T3.12 | 인프로세스 적합성 | tests/cases의 모든 케이스를 실행하는 tests/conformance.test.ts | 동일 | npm test -w @polyspec/template; node tests/runner/conformance.mjs --langs ts | T3.11 | [o] |
| T3.13 | 나머지 AST 생성과 검토 | AST가 없는 픽스처의 expected.ast.json을 node tests/runner/conformance.mjs --langs ts --update ast로 생성하고 케이스마다 검토 | — | node scripts/check-schema.mjs | T3.12 | [o] |
| T3.14 | 브라우저 빌드 검사 | tests/browser/index.html, tests/browser/render.spec.ts, playwright.config.ts; 페이지가 dist/index.mjs를 로드해 케이스 10건을 렌더 | Playwright | make test-browser | T3.11 | [o] |
| T3.15 | 픽스처 확장 | T3.12–T3.14에서 발견한 모든 결함에 대한 케이스 추가 | — | node tests/runner/conformance.mjs --langs ts | T3.12 | [o] |
| T3.16 | 패키지 문서 | packages/template-ts/README.md(.ko): API, CLI, 브라우저 사용법 | — | make docs-check | T3.11 | [o] |
Wave 4 — Go, Rust, PHP 구현 (병렬 트랙 3개)
트랙 G — Go (packages/template-go, 모듈 github.com/polyspec/template/packages/template-go)
| ID | 작업 | 산출물 | 테스트 | 검증 | 완료 |
|---|---|---|---|---|---|
| T4.G.1 | 모듈 골격 | go.mod (go 1.27.1, 의존성 없음), README.md(.ko) | — | go vet ./... | [o] |
| T4.G.2 | 값 모델 | template/value/value.go, ordered.go (삽입 순서 map), number.go (최단 왕복 자릿수로 문자열화), bind.go (any, 순서를 보존하는 JSON 디코더, 필드 순서의 구조체) | template/value/*_test.go (외부 테스트 패키지) | go test ./template/value/... | [o] |
| T4.G.3 | 오류 | template/errors.go | template/errors_test.go | go test ./template/... | [o] |
| T4.G.4 | 템플릿 렉서 | template/lexer/lexer.go | template/lexer/lexer_test.go | 동일 | [o] |
| T4.G.5 | 표현식 렉서와 파서 | template/expr/lexer.go, parser.go, ast.go | template/expr/*_test.go, tests/fixtures/expr/cases.json을 로드하는 픽스처 테스트 | 동일 | [o] |
| T4.G.6 | 템플릿 파서와 AST | template/parser/parser.go, standalone.go, blocktag.go, JSON 마샬링을 가진 template/ast/ast.go | template/parser/*_test.go, 스키마 검증 테스트 | 동일 | [o] |
| T4.G.7 | 함수 | 그룹별 파일 template/functions/*.go | template/functions/*_test.go | 동일 | [o] |
| T4.G.8 | 로더 | template/loader.go (Loader, MapLoader, fs.FS 위의 FSLoader) | template/loader_test.go | 동일 | [o] |
| T4.G.9 | 렌더러 | template/render/engine.go, context.go, statements.go, expressions.go | template/render/*_test.go | 동일 | [o] |
| T4.G.10 | CLI | cmd/template/main.go | cmd/template/main_test.go | go build -o template ./cmd/template | [o] |
| T4.G.11 | 적합성 | tests/cases를 실행하는 template/conformance_test.go | 동일 | make test-go; node tests/runner/conformance.mjs --langs ts,go | [o] |
트랙 R — Rust (packages/template-rust, 크레이트 polyspec-template)
| ID | 작업 | 산출물 | 테스트 | 검증 | 완료 |
|---|---|---|---|---|---|
| T4.R.1 | 크레이트 골격 | Cargo.toml (edition 2024, serde, preserve_order를 켠 serde_json), Cargo.lock, README.md(.ko), #![deny(missing_docs)] | — | cargo build --locked | [o] |
| T4.R.2 | 값 모델 | src/value/mod.rs, number.rs, bind.rs | tests/value.rs | cargo test --locked | [o] |
| T4.R.3 | 오류 | src/error.rs | tests/error.rs | 동일 | [o] |
| T4.R.4 | 템플릿 렉서 | src/lexer.rs | tests/lexer.rs | 동일 | [o] |
| T4.R.5 | 표현식 렉서와 파서 | src/expr/lexer.rs, parser.rs, ast.rs | 픽스처를 로드하는 tests/expr.rs | 동일 | [o] |
| T4.R.6 | 템플릿 파서와 AST | src/parser/mod.rs, standalone.rs, block_tag.rs, serde를 가진 src/ast.rs | tests/parser.rs, 스키마 검증 테스트 | 동일 | [o] |
| T4.R.7 | 함수 | src/functions/*.rs | tests/functions.rs | 동일 | [o] |
| T4.R.8 | 로더 | src/loader.rs | tests/loader.rs | 동일 | [o] |
| T4.R.9 | 렌더러 | src/render/mod.rs, context.rs, statements.rs, expressions.rs | tests/render.rs | 동일 | [o] |
| T4.R.10 | CLI | src/bin/template.rs | tests/cli.rs | cargo build --locked --release --bin template | [o] |
| T4.R.11 | 적합성 | tests/conformance.rs | 동일 | make test-rust; node tests/runner/conformance.mjs --langs ts,go,rust | [o] |
트랙 P — PHP (packages/template-php, composer polyspec/template)
| ID | 작업 | 산출물 | 테스트 | 검증 | 완료 |
|---|---|---|---|---|---|
| T4.P.1 | 패키지 골격 | 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 | 값 모델 | src/Value/Value.php, Number.php, Bind.php (array_is_list, 키 문자열 복원, 안전 정수 검사, UTF-8 검사) | tests/Value/*Test.php | vendor/bin/phpunit | [o] |
| T4.P.3 | 오류 | src/TemplateError.php, src/ErrorCode.php | tests/ErrorTest.php | 동일 | [o] |
| T4.P.4 | 템플릿 렉서 | src/Lexer/TemplateLexer.php | tests/Lexer/TemplateLexerTest.php | 동일 | [o] |
| T4.P.5 | 표현식 렉서와 파서 | src/Expr/Lexer.php, Parser.php, Node.php | 픽스처를 로드하는 tests/Expr/*Test.php | 동일 | [o] |
| T4.P.6 | 템플릿 파서와 AST | src/Parser/Parser.php, Standalone.php, BlockTag.php, toArray()를 가진 src/Ast/*.php | tests/Parser/*Test.php, 스키마 검증 테스트 | 동일 | [o] |
| T4.P.7 | 함수 | src/Functions/*.php | tests/Functions/*Test.php | 동일 | [o] |
| T4.P.8 | 로더 | src/Loader/LoaderInterface.php, ArrayLoader.php, FilesystemLoader.php | tests/Loader/*Test.php | 동일 | [o] |
| T4.P.9 | 렌더러 | src/Render/Engine.php, Context.php, Statements.php, Expressions.php | tests/Render/*Test.php | 동일 | [o] |
| T4.P.10 | CLI | bin/template.php | tests/CliTest.php | php bin/template.php parse ... | [o] |
| T4.P.11 | 적합성 | tests/ConformanceTest.php | 동일 | make test-php; node tests/runner/conformance.mjs --langs ts,go,rust,php | [o] |
교차 언어 검사 (세 트랙 뒤 순차)
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T4.X.1 | 분기 0건의 일치 실행 | node tests/runner/parity.mjs | [o] |
| T4.X.2 | 전체 검사 | make check (docs-check, lint, 단위 스위트 4개, conformance) | [o] |
| T4.X.3 | 상태 갱신 | docs/features.md(.ko)의 template-go, template-rust, template-php, conformance 행과 근거 링크; CHANGELOG.md(.ko) 항목 | make docs-check |
Wave 5 — PHP 확장 (순차)
| ID | 작업 | 산출물 | 검증 | 완료 |
|---|---|---|---|---|
| T5.1 | 툴체인 확인 | Rust PHP 바인딩이 설치된 PHP 버전(8.5)을 지원하는지 docs/operations/development.md(.ko)에 기록; 지원하지 않으면 T5.2–T5.6을 필요한 버전과 함께 blocked로 표시 | php-config --version | [o] |
| T5.2 | 확장 크레이트 | packages/template-php-ext/Cargo.toml (cdylib, Rust 크레이트 의존), src/lib.rs (parse, render, register를 가진 Polyspec\Template\Native\Engine), src/convert.rs (zval과 값의 상호 변환) | cargo build --locked --release | [o] |
| T5.3 | 스텁과 패키지 연결 | stubs/polyspec_template.stub.php; packages/template-php가 네이티브 엔진을 명시적으로 선택하는 방법을 문서화 | make docs-check | [o] |
| T5.4 | CLI와 테스트 | bin/template-ext.php; php -d extension=...로 실행하는 tests/ExtConformanceTest.php | make test-ext | [o] |
| T5.5 | Makefile 타겟 | ext, test-ext; 러너 드라이버 php-ext | node tests/runner/conformance.mjs --langs php-ext | [o] |
| T5.6 | 상태 갱신 | docs/features.md(.ko)의 template-php-ext 행; CHANGELOG.md(.ko) | make docs-check | [o] |
Wave 6 — 벤치마크, 문서 사이트, 상태 (병렬)
| ID | 작업 | 산출물 | 검증 | 완료 |
|---|---|---|---|---|
| T6.1 | 성능 측정 | 언어별 AST와 generated 측정값; 측정 전 출력 동일성 검사 | make showcase | [o] |
| T6.2 | 문서 커버리지 검사기 | make doc-coverage가 실행하는 scripts/check-doc-coverage.mjs (4개 패키지의 공개 심볼 문서화) | make doc-coverage | [o] |
| T6.3 | 문서 사이트 | docs/.vitepress/config.mts, 검증 뒤 실행되는 정적 GitHub Pages job, 생성된 API 문서는 git 제외 | make docs-static-check; make docs-verify-idempotent | [o] |
| T6.4 | CI 워크플로 | 기존 Makefile 타겟을 호출하고 모든 필수 job 통과 뒤에만 Pages를 배포하는 .github/workflows/ci.yml | 워크플로 파일 lint | [o] |
| T6.5 | 발행 절차 | docs/operations/publication.md(.ko): Go, npm, composer 패키지의 로컬 불변 발행 | make docs-check | [o] |
| T6.7 | 실행 가능한 예제 사이트 | examples/site/ 시나리오와 정적 페이지; tools/showcase/build.mjs; AST/generated program 일치성, 반복 렌더, 동일 조건 모드 벤치마크 JSON 결과물 | make showcase; make showcase-check | [o] |
| T6.6 | 최종 상태 | 테스트 리비전을 기록한 docs/features.md(.ko); CHANGELOG.md(.ko) | make check | [o] |
Wave 7 — generated compiler 완성과 패키지 설치 검증
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T7.1 | compiler/runtime manifest 하나, 생성 선언부, 소유 관계와 지원 수준 도표를 정의하고 TypeScript·Go·Rust·PHP 구조 이탈을 거부 | make compiler-interface-check; make runtime-interface-check | [o] |
| T7.2 | generated callback과 showcase 전용 생성을 하나의 compiler pipeline과 네 host backend로 교체하고 호환 옵션과 fallback 경로 제거 | package test; compiler mutation test | [o] |
| T7.3 | 모든 명세 node, expression, 내장 함수와 host 함수를 generated 실행에서 지원 | generated compiler test | [o] |
| T7.4 | 모든 정규 케이스(작업 완료 시 216개, 2026-10-02에 244개)를 TypeScript·Go·Rust·PHP의 AST와 generated 실행으로 검증 | make conformance-all-modes | [o] |
| T7.5 | build 경계 artifact 갱신 검증: dev는 항상 재생성, true는 digest 변경 시 재생성, false는 source를 읽지 않음 | artifact lifecycle test | [o] |
| T7.6 | npm·Go·Cargo·Composer artifact를 격리한 임시 프로젝트에 설치하고 같은 assign/define page 렌더 | make install-check | [o] |
| T7.7 | production artifact로 parser 기반 showcase 구문 강조, 크기 제한 artifact/source 보기와 React island 예제 생성 | make showcase-check | [o] |
| T7.8 | production artifact를 사용해 출력이 같은 AST/generated 성능 측정 재실행 | make bench; make showcase | [o] |
| T7.9 | 명세, 기능 상태, 변경 기록, 생성 Mermaid, 정적 문서와 완료 근거 동기화 | make docs-check; make docs-verify-idempotent | [o] |
| T7.10 | 상용 release test pyramid 강제: lexer/parser/IR/runtime 단위 검사, generated source compile 검사, 전체 mode matrix, 위치 오류와 실패 복구 회귀, mutation 거부, 격리 package 설치·browser 출력, 출력 동일 성능 회귀 | make release-test-matrix | [o] |
| T7.11 | 깨끗한 checkout의 release gate 통과와 정적 사이트 배포 | make release-check; CI와 Pages 성공 | [o] |
Wave 8 — assign 인스턴스와 클래스 함수 실행
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T8.1 | host 클래스를 복제하지 않는 공통 object value 경계를 추가 | 네 언어 value·binding 테스트 | [o] |
| T8.2 | assign native 인스턴스에서 선언된 public 필드와 멤버 메서드를 조회 | 멤버 조회 테스트 | [o] |
| T8.3 | 같은 registry 계약으로 선언된 논리 클래스 함수를 조회 | 클래스 호출 테스트 | [o] |
| T8.4 | AST와 generated 프로그램에서 멤버·클래스 호출을 실행 | AST/generated 일치 테스트 | [o] |
| T8.5 | 네 언어에서 출력, arity, type, unknown-member와 throw 오류 동작을 검증 | make generated-native-check; 전체 native 호출 matrix | [o] |
| T8.5-1 | 모든 runtime에서 native object의 json을 실패시킨다. PHP runtime은 object에 entries()를 호출해 E_INTERNAL을 냈고, TypeScript, Go, Rust, PHP extension은 불투명한 값(VAL-19)을 null로 보이게 하는 대체값 null을 썼다. 이제 FUN-26은 어느 깊이에서든 native object이거나 그것을 담은 값에는 JSON text가 없고 호출 위치에서 E_RUNTIME_TYPE으로 실패한다고 적으며, 다섯 runtime과 그 함수 registry를 쓰는 generated program이 이를 낸다. tests/fixtures/native-object/json.tpl은 json(['k' => [order]])를 render하고, host-values.json은 모든 package의 AST test, extension, scripts/check-generated-native-calls.mjs에서 E_RUNTIME_TYPE을 기대한다. Red: PHP AST test가 E_INTERNAL로 실패했다. Green: TypeScript, Go, Rust, PHP, extension의 host-value test가 통과한다 | node scripts/run-tests.mjs vitest --cwd packages/template-ts -- tests/render/host-values.test.ts; packages/template-go에서 go test . -run HostValues; cargo test --test host_values; packages/template-php와 build된 extension의 packages/template-php-ext에서 vendor/bin/phpunit --filter HostValues; CI가 make generated-native-check를 실행 | [o] |
| T8.5-2 | T8.5-1 뒤에 showcase artifact를 다시 만든다. CI의 release job은 showcase-check에서 examples/site/index.html is stale; run make showcase로 실패했다. examples/site/scenarios/*/compiled/ast의 AST artifact manifest는 template-ts build의 digest인 compilerDigest를 기록하며, T8.5-1이 그것을 바꿨다. install-check가(typed-generator와 showcase-compile --refresh true를 통해) showcase-site-check 전에 CI checkout의 manifest 다섯 개를 다시 썼고, 그 page가 manifest를 보여준다. make showcase-compile과 tools/showcase/build-site.mjs가 manifest 다섯 개와 page를 쓴다. digest 다섯 개만 바뀌고 benchmark 결과는 다시 측정하지 않는다 | node tools/showcase/compile.mjs --refresh true가 tree를 바꾸지 않음, node tools/showcase/build-site.mjs --check; CI가 make showcase-check를 실행 | [o] |
| T8.6 | object 호출 선언과 생성 Mermaid 인터페이스 도표를 추가 | interface 검사 | [o] |
| T8.7 | native 인스턴스 assign, 필드 조회, 멤버 호출과 클래스 호출 결과를 보여주는 showcase 페이지 추가 | make showcase-check; 정적 HTML 검사 | [o] |
| T8.8 | 명세, 기능 상태, changelog, Pages 산출물과 완료 근거 동기화 | make check; make docs-verify-idempotent; make docs-static-check | [o] |
Wave 9 — AST의 템플릿 주석
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T9.1 | TypeScript, Go, Rust, PHP parser와 PHP 확장에서 모든 주석을 값과 span과 함께 Template.comments에 나열(AST-9). schema 정의와 공유 case text/comment-list, delimiters/comment-custom 추가 | make conformance; make check | [o] |
Wave 10 — 설치 검사 작업 디렉터리 제거
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T10.1 | -modcacherw로 만든 Go module cache를 포함해 설치 검사 작업 디렉터리를 제거하고, 제거할 수 없는 작업 디렉터리의 경로와 함께 검사를 실패시킴. scripts/check-install-workspace.mjs가 make install-check에서 실행됨 | make install-check; make check | [o] |
Wave 11 — 에디터 언어 서비스, LSP 서버, CodeMirror 어댑터
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T11.1 | @polyspec/template의 analyzePrefix()로 첫 오류 전까지 받아들인 태그와 표현식 토큰을 돌려줌(EDT-6) | make test-ts | [o] |
| T11.2 | @polyspec/template-format을 @polyspec/template-language로 바꾸고, 진단, 토큰, 태그 범위, 접기 범위, 강조, 짝 태그, format, lineIndentation을 가진 openDocument()를 추가(EDT-4~EDT-13). 접기와 짝 태그 규칙을 VS Code 확장에서 옮기고 에디터 픽스처를 추가(EDT-17) | make test-language | [o] |
| T11.3 | 명령 template-lsp를 가진 LSP 서버 @polyspec/template-lsp를 추가하고(EDT-14) 프로토콜로 에디터 픽스처를 검사 | make test-lsp | [o] |
| T11.4 | VS Code 확장이 확장 안에 포함한 LSP 서버에 연결해 동작하도록 바꾸고, TextMate 문법, 태그 배경, 짝 태그 명령을 유지(EDT-15) | make test-vscode; make test-vscode-integration | [o] |
| T11.5 | CodeMirror 6 어댑터 @polyspec/template-codemirror를 추가하고(EDT-16) EditorState와 브라우저에서 에디터 픽스처로 검사 | make test-codemirror | [o] |
| T11.6 | @polyspec/template을 선언하거나 import하는 어댑터(EDT-2), 에디터·Node.js·DOM 모듈을 import하는 언어 서비스(EDT-3)를 거부하고, 규칙 증거, 기능 상태, 문서, 의존성 정책을 기록 | make check | [o] |
Wave 12 — 콜론 앞에 식별자가 오는 삼항 연산
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T12.1 | c ? a : b, c ? a : b.x, then 쪽이 식별자로 끝나는 중첩 삼항 연산을 담은 공통 사례를 추가하고, 결함이 있는 모든 구현에서 실패하는지 확인한 뒤, 식별자 뒤에 ::가 올 때만 클래스 호출로 파싱하도록 모든 구현을 고친다 | make check | [o] |
Wave 13 — 네 가지 작업 상태
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T13.1 | scripts/check-documents.mjs가 네 가지 작업 상태만 받고, 일시 우회 작업은 원인과 재시도 조건을 담게 한다. 글자 x로 표시한 행에서 실패하는지 확인한다. 모든 완료 작업을 AGENTS의 완료 상태로 쓴다. AGENTS에 checklist 규칙을 적는다 | make docs-check | [o] |
| T13.1-1 | checklist가 작업만 담고 표시는 작업 상태로만 쓰게 한다: scripts/check-documents.mjs는 docs/plans/execution-checklist.md의 제목, 번역 link, heading, 작업 표 행이 아닌 줄과, 작업 행 마지막 칸의 시작이 아닌 곳에 있는 AGENTS의 상태 표시나 task list 표시(대괄호 안의 글자 x 또는 X)에서 예외 없이 file, 줄, 열을 적으며 실패한다. T14.2-1 같은 하위 항목도 작업 행으로 읽는다. checklist의 본문(소개, 의존 관계 개요, 웨이브마다 의존 관계·원인·완료 기준, 병렬 요약, 완료 정의와 그 증거)은 두 언어 모두 docs/plans/execution-plan.md로 옮긴다. 규칙(작업 ID 형식, 작업 행의 내용)은 상태를 정하는 AGENTS로 옮기고 범례를 지운다. W13, T13.1, T17.1, T17.1-1의 text는 상태를 말로 적는다. Cause: checklist의 범례와 text 넷이 언어마다 상태 표시 14개와 task list 표시 2개를 적었고, 표시를 세는 도구가 존재하지 않는 진행 중 작업을 보고했다. 언어마다 93줄이 작업이 아니었다. 검사기는 ID에 하위 부분이 없는 행의 마지막 칸만 검사했으므로 표시가 어디에 있는지도, 하위 항목 9개의 상태도 검사하지 않았다. Red: tests/scripts/check-documents.test.mjs에서 범례, 본문, 작업 text, inline code, 일시 우회 작업의 원인에 표시가 있는 fixture checklist, heading 아래 문단이 있는 fixture, 상태가 done인 하위 항목이 모두 7 document pairs passed로 검사기를 통과했다(5개 case 중 3개가 0 !== 1로 실패). Green: 5개 case가 통과한다. 첫 fixture는 표시 위치 16개(docs/plans/execution-checklist.md:10:27 등)와 줄 위치 4개로 실패하고, 문단은 7번째 줄에서 실패하며, 하위 항목은 invalid task state for T1.2-1: done으로 실패하고, 제목, link, heading, 작업 표로 된 checklist는 통과하며, 검사기는 이 저장소를 통과시킨다 | node scripts/run-tests.mjs node -- tests/scripts/check-documents.test.mjs | [o] |
| T13.1-2 | checklist의 번역 link 줄에서 scripts/check-documents.mjs가 실패하게 해 checklist가 heading과 작업 표만 담게 하고, 두 언어에서 그 줄을 지운다. Cause: AGENTS는 모든 문서에 .ko.md file이 있다고 적지만, T13.1-1은 checklist 셋째 줄의 link를 번역 link로 받았다. Red: 셋째 줄에 [한국어](/ko/plans/execution-checklist).가 있는 checklist를 검사기가 통과시켰으므로 tests/scripts/check-documents.test.mjs의 새 case a translation link line fails with its location이 0 !== 1로 실패했다. 바꾼 검사기는 그 줄을 지우기 전에 docs/plans/execution-checklist.md:3:1과 한국어 file의 같은 곳을 보고했다. Green: 6개 case가 통과하고, 검사기는 이 저장소를 통과시킨다 | node scripts/run-tests.mjs node -- tests/scripts/check-documents.test.mjs | [o] |
| T13.1-3 | 한국어 실행 계획이 checklist를 한국어 site route /ko/plans/execution-checklist로 link하게 하고, docs/ 아래 문서를 바꾸는 commit 전에는 make docs-check와 함께 make docs-static-check를 실행한다. Cause: T13.1-1이 execution-checklist.ko.md를 상대 경로로 link했으므로 make check가 docs-static-check에서 ko/plans/execution-plan.html: link points to a source-only or legacy route: ./execution-checklist.ko로 실패했다. 그 commit은 site를 빌드하되 link를 검사하지 않는 make docs-check와 make docs만 실행했다. AGENTS와 docs/operations/documentation.md가 이 규칙과, 한국어 문서가 한국어 페이지를 route /ko/<path>로 link한다는 것을 적는다. 바뀐 경로에서 owner check를 고르는 일은 다음 단계로 T13.1-4에 기록한다. Red: T13.1-2의 tree에서 make docs-static-check가 같은 오류로 실패했다. Green: documentation static site: 57 pages, 28 Korean locale pages passed를 출력하고, make docs-check가 통과한다 | make docs-check docs-static-check | [o] |
| T13.1-4 | commit이 바꾸는 경로에서 owner check를 고른다: scripts/owner-checks.json이 경로 glob마다 그것을 소유하는 make target과 node test file을 선언하고, make owner-check는 바뀐 경로(commit하지 않은 변경, PATHS, 또는 BASE 이후의 경로)의 owner만 실행하며 전체 suite는 실행하지 않는다. 어떤 owner보다 먼저, 어느 규칙도 소유하지 않는 tracked 경로, 경로가 없는 glob, Makefile에 없는 target, 전체 suite target에서 각각을 적으며 실패한다. tests/cases 아래 변경은 그 case만의 AST와 생성 conformance인 make conformance-cases CASES=...를 실행한다. 모든 tracked 경로에 owner를 준다: 어떤 target도 실행하지 않던 tests/runner/function-inventory.mjs는 make check의 새 target function-inventory-check에서 실행한다. .node-version, rust-toolchain.toml, .gitignore, .editorconfig는 그것이 선언하는 것의 test를 얻는다. .editorconfig는 byte가 정확해야 하는 test file과 생성된 showcase file을 선언하고, 문서 여섯 개의 Go 예제는 space로 들여쓴다. AGENTS는 T13.1-3의 기억에 기대는 규칙 대신 make owner-check를 가리킨다. Red: scripts/owner-check.mjs와 scripts/conformance-cases.mjs가 없을 때 tests/scripts/owner-check.test.mjs의 6개 case가 실패했고 tests/scripts/conformance-cases.test.mjs는 load에 실패했다. tests/scripts/editorconfig.test.mjs는 .editorconfig를 어기는 줄과 file 338개를 나열했다(문서 여섯 개와 생성된 site의 tab 들여쓰기, 생성된 Go file 다섯 개의 space 들여쓰기, case file 다섯 개의 encoding, 줄 끝, 마지막 newline, 끝 공백). .node-version을 25.0.0으로 두면 .node-version 25.0.0 differs from the major version of engines.node >=26으로, .gitignore에서 packages/template-ts/dist.inputs를 빼면 outputs that .gitignore does not ignore: packages/template-ts/dist.inputs로 실패했다. Green: test가 통과한다. docs 경로는 정확히 docs-check와 docs-static-check를 실행하고, owner가 없는 tracked 경로와 새 경로는 어떤 target보다 먼저 그 이름과 함께 실패하며, 경로가 없는 glob과 target check는 실패하고, variable이 있는 규칙은 경로를 넘기며, scripts/owner-checks.json은 모든 tracked 경로를 소유한다. 이 변경의 make owner-check가 고른 owner를 실행하고 통과했다 | 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 owner-check가 어느 규칙도 소유하지 않는 지워진 경로에서는 아무것도 고르지 않고, 바뀐 경로 자신인 test는 그 경로가 있을 때만 실행하게 한다. Cause: T18.8-1에서 scripts/build-ts.mjs와 tests/scripts/build-ts.test.mjs의 이름을 바꾸자 owner check가 scripts/build-ts.mjs: the path matches no owner로 실패하고 지워진 test file을 골랐다. Red: T13.1-4의 script로는 새 case a removed path that no rule owns selects nothing와 a removed test file is not run이 실패했다. Green: tests/scripts/owner-check.test.mjs의 8개 case가 통과한다 | node scripts/run-tests.mjs node -- tests/scripts/owner-check.test.mjs | [o] |
Wave 14 — Data binding 비용
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T14.1 | host binding의 문자열, map key, define id를 mb_check_encoding으로 검사하고, 위치를 보고하는 곳은 Utf8::firstInvalid를 유지한다. 모든 1 byte와 2 byte 배열, 경계의 3 byte와 4 byte 배열에서 두 검사를 비교하는 test를 더한다 | make check | [o] |
| T14.2 | 여러 render에서 data를 한 번 binding한다(VAL-22, RT-4, RT-24, RT-61, ERR-14). TypeScript, browser entry, Go, Rust, PHP, PHP extension에 bind와 merge를 가진 bound map type을 둔다. packages/template-compiler/interface.json은 새 php-extension과 javascript-esm mapping을 포함한 모든 구현의 type과 이름을 적고, 각 구현이 bound map을 render와 prepare에 넘기는 연산이나 parameter type도 적는다. interface check는 그 type의 다른 public 연산에서 실패한다. TypeScript build는 bound map class를 가진 runtime module 하나를 모든 entry와 두 module format이 공유하게 한다. 지금은 tsup의 CommonJS build가 chunk를 나누지 않아 index.cjs와 render.cjs가 각자 runtime 사본을 가진다. TypeScript와 Rust의 bind처럼 bound map 연산의 이름을 이미 가진 export는 이름을 바꾸거나 없애고, backend와 generated program을 다시 만든다. render와 prepare는 generated program과 PHP extension에서도 bound assign과 bound 정의 data를 다시 binding하지 않고 받고, typed program은 그것을 record로 변환한다(RT-68). 다른 위치에 있거나 다른 구현이 만든 bound map은 host binding이 E_DATA_UNSUPPORTED_TYPE으로 실패시킨다. tests/rule-evidence.json에 VAL-22와 ERR-14 group을 두고 모든 구현의 test와 merge 순서, 거부하는 위치, byte 동일성의 공통 fixture를 둔다. bound-data feature를 implemented로 바꾸고, guide에 bind와 merge를 보이고, changelog를 쓴다. 진행: TypeScript, browser entry, Go, Rust, PHP, PHP extension과 generated program 네 개에 구현했고, packages/template-compiler/interface.json과 그 검사, bound-data evidence group을 더했다. 모든 runtime의 T14.2 test, scripts/check-generated-bound-data.mjs, mutation을 포함한 compiler interface 검사, scripts/check-rules.mjs가 통과한다. 남은 것: make check | make check | [o] |
| T14.2-1 | Rust 요청의 binding 입력을 struct로 이름 붙인다: T14.2는 prepare_request의 closure에서 root, 정의 registry, 환경의 tuple을 돌려줬고, cargo clippy -D warnings가 이를 type_complexity로 거부했다(main T15.3에서 exit 101). BoundRequest가 세 field에 이름을 붙이고, clippy와 bound map test가 통과한다 | cargo clippy --all-targets -- -D warnings | [o] |
| T14.2-2 | TypeScript package의 CommonJS 파일을 build의 출력 directory에 쓴다: T14.2는 dist/<entry>.cjs를 작업 directory 기준으로 썼으므로, --out-dir를 쓴 package build가 ENOENT: dist/index.cjs로 실패했다. onSuccess는 --out-dir, 없으면 dist에 쓰고, --out-dir이 있을 때와 없을 때 tsup이 그곳에 index.cjs, render.cjs, node.cjs를 쓴다 | npx tsup --out-dir <dir> | [o] |
Wave 15 — runtime마다 다른 binding 오류
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T15.1 | assign이 JSON null이고 빈 map으로 렌더하는 conformance case를 더한다. Rust에서 실패하는지 확인한다. Rust render, render_values, prepare와, render_values와 render로 렌더하는 PHP extension의 render가 null assign을 빈 map으로 받게 한다(bind_map, php_to_map). RT-4에 규칙을 적는다 | make check | [o] |
| T15.1-1 | generated TypeScript와 PHP program이 AST program처럼 null assign을 빈 map으로 binding하게 한다(RT-4). 네 generated conformance runner의 --case가 case를 고르고, 아무 case도 고르지 못하면 실패하게 한다. runner는 위치 인자 filter만 읽었으므로 --case data/null-assign이 case 0개를 실행하고 통과했다 | make check | [o] |
| T15.2 | assign과 정의 data가 선언한 type과 맞지 않는 request를 렌더하는 test를 모든 generated runtime에 더한다. runtime마다 보고가 다른지 확인한다. 모든 runtime이 ERR-13대로, 그대로 전달되고 ERR-1 오류가 아닌 자기 언어의 인자 오류로 보고하게 한다 | make check | [o] |
| T15.2-1 | scripts/check-typed-generator.mjs의 Rust 부분이 T15.2 이후 Program::render가 돌려주는 RequestError를 읽게 한다. 생성한 test가 Result<String, TemplateError>를 선언했으므로 make check에 포함된 typed-generator-compile-check가 E0308로 compile에 실패했다 | node scripts/check-typed-generator.mjs | [o] |
| T15.3 | 구분자 쌍이 아닌 구분자 옵션으로 Rust engine을 만들고 Rust에서 parse하는 test와 PHP extension의 같은 test를 추가한다. Rust AstProgram::new가 panic하고, Rust parse가 ERR-1 오류를 돌려주고, extension이 \Exception을 던지는지 확인한다. ERR-13의 인자 오류로 보고하게 한다: AstProgram::new는 Result<AstProgram, ArgumentError>를, parse는 RequestError::Argument를 돌려주고, extension은 PHP package처럼 \InvalidArgumentException을 던진다 | cargo test --test delimiters, run-tests.sh --filter Delimiters | [o] |
| T15.3-1 | NewAstProgram처럼 Go Parse도 구분자 쌍이 아닌 구분자 옵션에 Go의 인자 오류를 돌려주게 한다. delimitersOf는 ERR-1 오류인 E_DATA_UNSUPPORTED_TYPE의 *errs.Error를 돌려줬지만, ERR-13은 *errs.Error가 아닌 error를 요구한다. 먼저 실패하는 Go test를 더하고, Parse를 고치고, ERR-13에 Go의 형태를 적는다 | TestInvalidDelimiterIsArgumentError | [o] |
| T15.4 | TypeScript generated program처럼 Go typed generated program이 없는 optional field를 null로 읽게 한다. Go backend는 optional field를 valueOrZero로 읽으므로, assign 데이터에 없는 string? field c에 대해 {= c ?? '-'}가 -가 아니라 빈 문자열을 렌더한다. 모든 generated runtime에 case를 더하고 Go에서 실패하는지 확인한 뒤 backend를 고친다 | make check | [o] |
Wave 16 — 동시 render
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T16.1 | 모든 runtime의 render가 하나의 program을 공유할 수 있는지 정하고 runtime 명세에 규칙을 적는다. 공유할 수 있으면 Go에서 한 program을 동시에 렌더하는 race test를 더하고 모든 runtime의 template cache를 그에 맞게 안전하게 한다. 공유할 수 없으면 호스트가 thread나 goroutine마다 program 하나를 쓴다고 적는다 | make check | [o] |
Wave 17 — test 실행
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T17.1 | 진행 중인 작업은 변경을 소유한 Red와 Green test만 실행하고, 커밋 전에는 make docs-check도 실행하며, 작업은 소유 명령이 통과한 뒤 완료가 되고, make check는 활성 작업이 모두 끝났을 때 한 번 실행한다고 AGENTS에 적는다. 모든 test가 진행을 출력하고 자기 timeout을 가지며 실행 전체에는 시간 제한이 없다고 적는다. Verification 열에 소유 명령을 적는다. AGENTS.ko.md에 "결정과 수용 규칙" 절을 더한다 | make docs-check | [o] |
| T17.2 | node, vitest, go, cargo, phpunit용 test runner scripts/run-tests.mjs와 scripts/test-progress/를 더한다. runner는 각 test를 경과 시간과 함께 출력하고 test를 자기 timeout에서 멈추며, 그 test는 tests/scripts/run-tests.test.mjs에 둔다. test-go, test-rust, test-php를 runner로 실행하고, test-go의 package 120 s 제한을 없애고, PHPUnit 시간 제한을 강제한다 | node scripts/run-tests.mjs node -- tests/scripts/run-tests.test.mjs | [o] |
| T17.3 | tests/runner/conformance.mjs가 한 언어에서 case가 끝날 때마다 <case> [<lang>] pass|fail (<ms>)을 출력하게 하고, 요약은 끝에 그대로 둔다 | node scripts/run-tests.mjs node -- tests/scripts/conformance-progress.test.mjs | [o] |
| T17.4 | 네 generated conformance runner의 모든 case에 자기 deadline과 경과 시간이 붙은 결과 줄을 준다. Go와 Rust는 scripts/run-tests.mjs로 실행하고 Rust test 이름은 case id로 짓는다. PHP는 php -l과 case process에 timeout을 둔다. TypeScript는 tsc에 deadline과 단계 줄을 두고 각 render를 timeout이 있는 worker에서 실행한다. 600 s 제한을 없앤다 | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/generated-conformance.test.mjs | [o] |
| T17.4-1 | tests/runner/conformance-generated-ts.mjs의 tsc 단계 deadline 300 s를 없앤다. 시작 줄, 실행 중 줄, 경과 시간이 붙은 결과 줄은 유지하고, tsc의 exit code로 판단하며, 각 render의 30 s deadline은 유지한다 | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/generated-conformance.test.mjs | [o] |
| T17.5 | packages/template-vscode/tests/integration/run.mjs의 모든 VS Code 실행과 profile 설치에 넘으면 process를 kill하는 deadline을 두고, suite/index.cjs의 모든 check에 Promise.race로 timeout을 걸고 ok와 not ok 줄에 경과 시간을 붙인다 | npm run test:integration -w polyspec-template | [o] |
| T17.5-1 | VS Code 실행을 suite 결과로 끝낸다. VS Code 1.138은 suite가 결과를 출력한 뒤 종료하는 데 300 s를 넘기기도 했으므로 make check가 실행 deadline에서 실패했다. suite 줄 [suite] N of M checks passed가 나오면 VS Code는 20 s 안에 종료해야 하고, 그 뒤 process group을 kill하고 실행은 suite 결과를 따른다. 20 s는 suite 결과가 출력된 뒤에 시작하므로 VS Code를 끝내는 정리이며 실행의 통과 여부를 정하지 않는다 | npm run test:integration -w polyspec-template | [o] |
| T17.5-2 | packages/template-vscode/tests/integration/run.mjs의 profile 설치 deadline(120 s)과 VS Code 실행 deadline(300 s)을 없앤다. download, 각 설치, 각 실행의 시작, 단계가 도는 동안 10 s마다 한 줄, 결과를 경과 시간과 함께 출력하고, 각 단계를 exit code와 suite 결과로 판단한다. check timeout 20 s와 suite 결과 뒤 종료 유예 20 s는 유지한다 | node scripts/run-tests.mjs node --cwd packages/template-vscode -- tests/integration-step.test.mjs | [o] |
| T17.6 | 오래 걸리는 작업은 단계 log를 출력하고 timeout을 두지 않으며 출력이 없는 시간의 deadline도 두지 않고 exit status, 결과, 오류로 판단하며, test case는 자기 timeout을 유지한다고 AGENTS에 적는다. AGENTS.ko.md처럼 AGENTS.md에도 영어 문서가 정본이라고 적는다 | make docs-check | [o] |
| T17.7 | tests/runner/drivers.mjs의 driver build를 600 s 제한 없이 log가 있는 단계로 실행한다. 시작, 출력, 결과를 경과 시간과 함께 standard error에 출력하고 exit code로 판단한다. CLI 호출마다의 10 s timeout은 유지한다 | node scripts/run-tests.mjs node -- tests/scripts/long-steps.test.mjs | [o] |
| T17.8 | scripts/check-package-installs.mjs(600 s)와 scripts/check-install-workspace.mjs(120 s)의 각 명령을 시간 제한 없이 log가 있는 단계로 실행하고 exit code로 판단한다 | node scripts/run-tests.mjs node -- tests/scripts/long-steps.test.mjs | [o] |
| T17.9 | tests/runner/delimiter-matrix.mjs가 한 언어에서 구분자 쌍이 끝날 때마다 <pair> [<lang>] pass|fail (<ms> ms)를 출력하게 하고, 요약은 끝에 그대로 둔다 | node scripts/run-tests.mjs node -- tests/scripts/delimiter-matrix-progress.test.mjs | [o] |
| T17.10 | test-ts, test-language, test-lsp, test-codemirror의 vitest test와 test-vscode의 node --test test를 scripts/run-tests.mjs로 실행한다 | node scripts/run-tests.mjs node -- tests/scripts/test-targets.test.mjs | [o] |
| T17.11 | make test-browser의 정적 서버를 startup deadline 30 s가 있는 Playwright webServer 대신 tests/browser/run.mjs에서 시간 제한 없는 단계로 띄운다(Playwright는 timeout: 0을 60 s로 읽는다). 서버 출력을 출력하고, 준비를 listening on http://127.0.0.1:4173 줄로 판단하며, 서버가 먼저 종료하면 실패한다 | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/browser-server.test.mjs | [o] |
| T17.1-1 | 한 번의 전체 실행을 guard scripts/full-run.mjs로 강제한다. make check는 어떤 단계보다 먼저 이 guard를 시작한다. guard는 이 checklist의 작업 행이 진행 중이면 거부하고 활성 ID를 작업과 함께 나열하며, 추적 파일의 변경이 커밋되지 않았으면 거부하고, 같은 tree(git rev-parse HEAD^{tree})의 두 번째 전체 실행을 앞선 실행을 밝히며 거부한다. CHECK_TARGETS의 각 target을 make <target>으로 끝까지 실행하고, 각 target의 앞뒤에 var/full-run.json(tree, commit, 결과, 각 target의 상태와 시각)을 쓰므로 멈춘 실행은 incomplete로 남는다. make rerun-failed는 현재 tree에서 통과하지 못한 target만 다시 실행하고, 그것이 통과하면 결과를 완성한다. 원인: AGENTS는 make check를 활성 작업이 모두 끝났을 때 한 번 실행한다고 적었지만 이를 강제하는 것이 없었다. 진행 중인 작업이 있는 실행, 커밋되지 않은 변경의 실행, 같은 tree의 두 번째 실행이 모두 suite를 시작했다. Red: make -n check는 docs-check의 첫 단계를 출력했으므로 suite는 checklist의 내용과 상관없이 시작했고, make -n rerun-failed는 No rule to make target으로 실패했으며, tests/scripts/full-run.test.mjs는 scripts/full-run.mjs에 대한 ERR_MODULE_NOT_FOUND로 실패했다. Green: 그 10개 case가 통과한다. make -n check는 guard만 출력하고, 진행 중인 작업이 있는 fixture checklist, 더러운 tree, 한 tree의 두 번째 실행, record 없는 rerun-failed는 stub target이 실행되기 전에 거부되며, rerun-failed는 실패했거나 끝나지 않은 stub target만 실행한다 | node scripts/run-tests.mjs node -- tests/scripts/full-run.test.mjs | [o] |
| T17.1-2 | CHECK_TARGETS를 전체 suite의 유일한 목록으로 만들고 어디서나 guard로 실행한다. make release-test-matrix는 다른 target으로 된 일곱 계층을 실행하고 첫 실패에서 멈췄으므로 CI는 dependency-policy-check만 보고하고 여섯 계층을 건너뛰었다. make check는 dependency-audit, benchmark-check, benchmark-smoke, docs-verify-idempotent를 실행하지 않았고, CI는 test-scripts, language-test-matrix, function-contract-check, delimiter-matrix, generated-native-check를 실행하지 않았다. CHECK_TARGETS가 네 target을 담고, make release-test-matrix는 node scripts/full-run.mjs run $(CHECK_TARGETS)를 실행하며, .github/workflows/ci.yml의 release job은 xvfb-run -a make check를 실행하고 var/full-run.json을 올린다. 검증하는 job의 모든 step은 실패한 step 뒤에도 실행하고(!cancelled()), target이 여럿인 make는 -k로 실행한다. scripts/owner-check.mjs는 scripts/full-run.mjs나 scripts/check-clean-release.mjs를 시작하는 target을 전체 suite target으로 보고, tests/scripts/clean-release.test.mjs가 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 | 이 checklist의 작업이 진행 중인 동안 push를 불가능하게 한다: AGENTS는 모든 진행 중 작업이 끝났을 때만 push한다고 했지만 이를 강제하는 것이 없었다. 추적되는 pre-push hook .githooks/pre-push는 scripts/push-gate.mjs hook을 실행하고, 이 script는 push되는 각 commit과 working tree의 checklist를 scripts/full-run.mjs의 activeItems로 읽어 각 작업을 밝히며 push를 거부하고, checklist를 읽을 수 없을 때도 거부한다. 모든 make 실행은 core.hooksPath를 .githooks로 설정하고, make hooks는 이를 설치하고 검사하며, make hooks-check는 설치되지 않았을 때 실패하고 make owner-check보다 먼저 실행되며, guard는 설치되지 않은 상태에서 make check를 거부한다. workflow .github/workflows/push-gate.yml(job push-gate)은 모든 branch에 대한 각 push와 각 pull request에서 scripts/push-gate.mjs commit을 실행하고, 각 작업을 밝히며 실패하고, .githooks/pre-push가 실행 파일로 추적되지 않을 때도 실패한다. workflow는 T20.1-8, T20.1-9, T20.1-12의 규칙을 따른다. step은 make ci-targets TARGETS="push-gate-commit"를 실행하고, action은 commit으로 고정하며, ubuntu-24.04에서 실행하고, !cancelled()에서 report var/report/ci-targets를 upload하며, 새 push가 같은 workflow와 ref의 실행 중인 gate를 취소한다 | 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 | tests/scripts/full-run.test.mjs, tests/scripts/suite-parity.test.mjs, tests/scripts/test-targets.test.mjs의 make dry run이 어느 make에서나 명령만 출력하게 한다. make check의 make 안에서 GNU Make 4가 guard 명령 앞뒤로 make[2]: Entering directory와 Leaving directory를 출력했으므로 CI가 test-scripts에서 실패했다. macOS make 3.81은 -w 없이 그 줄을 출력하지 않는다. dry run은 --no-print-directory를 넘기고 MAKEFLAGS=w로 실행하므로 어느 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 | job push-gate에서 문서 검사를 실행한다. ruleset main이 요구하는 check는 작업 gate만 실행했으므로, checklist에 작업 상태가 아닌 곳의 state marker가 있거나 문서가 다른 문서, feature, rule 검사를 어기는 commit도 통과해 main에 갈 수 있었다. job은 make ci-targets TARGETS="push-gate-commit documents-check feature-check rules-check"를 실행하고, tests/scripts/push-gate.test.mjs는 이 target들과, job이 다른 것을 설치하지 않으므로 각 target이 import가 Node.js module뿐인 checkout의 script를 실행할 것을 요구한다 | node scripts/run-tests.mjs node -- tests/scripts/push-gate.test.mjs | [o] |
| T17.1-6 | 오래된 compiled artifact를 main 전에 잡는다. T8.5-1은 template-ts source를 바꿔 showcase의 AST artifact에 이전 compiler의 digest를 남겼고, CI release job 전에는 아무것도 이를 알아채지 못했다. 개발에서는 unit test만 실행하고, job push-gate는 gate와 문서 검사만 실행했으며, showcase-ast-check는 오래된 digest를 유지하는 --refresh false로 compile하기 때문이다. 이제 AST compiler digest는 build된 dist 대신 packages/template-ts의 추적 source와 build 설정, package-lock.json, artifact writer로 계산하고, make artifact-digest-check(scripts/check-artifact-digests.mjs)는 commit된 artifact 30개 중 하나가 다른 compiler의 digest를 기록하면 각 manifest를 기록된 digest와 현재 digest, make showcase와 함께 밝히며 실패한다. Node.js만 필요하므로 job push-gate가 실행하고, showcase-check가 먼저 실행한다. artifact 30개와 page를 새 digest로 다시 compile했으며 digest만 바뀐다. Red: digest 변경 뒤 검사가 artifact 30개를 모두 밝혔다. Green: make showcase-compile, typed-generator, 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 | 모든 변경을 pull request와 merge queue로 게시한다. .github/ruleset.json의 ruleset main은 승인 없는 pull request(merge, squash, rebase 허용), 방식 REBASE의 merge queue, 선형 history, GitHub Actions의 check push-gate와 ci.yml의 모든 job(release, docs, typescript, browser, editor, go, rust, php (8.2), php (8.5))을 요구하고, bypass actor 없이 force-push와 삭제를 거부한다. 선언은 allow_rebase_merge, allow_auto_merge, delete_branch_on_merge도 정하며, make github-ruleset이 적용하고 make github-ruleset-check가 비교한다. ci.yml은 pull_request와 merge_group에서, push-gate.yml은 gh-readonly-queue/** 밖의 push, pull_request, merge_group에서 실행되고, 둘 다 pull request 실행만 취소하며, 문서 site 배포는 main push에서 실행되는 pages.yml로 옮겼다. 변경은 git push, gh pr create, gh pr merge --auto --rebase로 게시한다 | 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 | 각 workflow의 trigger를 정확히 선언한다. ci.yml은 pull_request, merge_group, workflow_dispatch에서 실행되므로 모든 검사가 수동 실행에서도 실행되고, push-gate.yml은 gh-readonly-queue/** 밖의 push, pull_request, merge_group에서 실행되며, pages.yml은 main push와 workflow_dispatch에서 문서 site를 build하고 배포하고, dependency-review.yml은 일정과 workflow_dispatch에서 실행된다. ruleset main은 push-gate와 ci.yml의 모든 job을 요구한다. tests/scripts/toolchain-files.test.mjs는 각 workflow의 on: block을 정확히 요구하고 다른 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 | 전체 suite의 각 target을 정확히 한 CI job에서 실행한다. ci.yml의 job release는 CHECK_TARGETS의 모든 target인 make check를 실행했으므로 docs, typescript, browser, editor, go, rust, php job이 실행하는 target 15개도 다시 실행했고, workflow에서 가장 긴 670 s가 걸렸다. job scripts, generated, delimiters, showcase는 다른 job이 실행하지 않는 target을 make ci-targets로 실행하고, ci-passed는 이 job들을 needs로 둔다. CHECK_TARGETS는 job이 실행하는 target을 적는다. lint 대신 lint-js, lint-go, lint-rust, lint-showcase-format, lint-showcase-warnings, lint-php를, conformance-all-modes 대신 conformance-ts, conformance-go, conformance-rust, conformance-php와 conformance-generated-* target 네 개를 적으며, php-ext의 AST conformance는 test-ext가 실행한다. tests/scripts/suite-parity.test.mjs는 ci.yml의 make ci-targets 단계의 target이 각 target을 한 job에 두는 CHECK_TARGETS가 아니면 실패하고, make check를 실행하는 job에서도 실패한다. 이 test는 job release가 있는 ci.yml에서 make ci-targets 단계에 없는 target 20개와 CHECK_TARGETS 밖의 target 5개로 실패했다. make ci-targets는 자기 job의 Node.js, npm, Go, cargo, PHP, Composer version을 report의 toolchains.json과 summary.md에 쓴다(scripts/target-report.mjs의 toolchainVersions). 따라서 job release의 var/full-run.json만 기록하던 setup-php의 PHP patch를 모든 CI job이 기록한다. tests/scripts/target-report.test.mjs가 이를 요구한다 | 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 resource
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T18.1 | tests/browser/server.mjs가 system이 배정하는 port에서 listen하고 그 port를 listening on http://127.0.0.1:<port> 줄에 출력하게 한다. tests/browser/run.mjs는 그 주소를 playwright.config.ts가 요구하는 TEMPLATE_BROWSER_URL로 Playwright에 넘긴다 | node scripts/run-tests.mjs node -- tests/scripts/browser-port.test.mjs | [o] |
| T18.2 | VS Code directory .vscode-test에 holder lock .vscode-test.lock을 둔다. integration 실행은 내려받기부터 마지막 launch까지 lock을 잡고, 두 번째 실행은 holder를 밝히며 실패하며, make clean은 lock을 잡은 동안에만 directory를 지우고, 끝난 process의 lock은 보고되고 make vscode-test-unlock으로 지운다 | node scripts/run-tests.mjs node -- tests/scripts/holder-lock.test.mjs | [o] |
| T18.3 | tests/scripts/browser-server.test.mjs가 make test-browser 대신 make test-scripts의 package build로 tests/browser/run.mjs를 실행하게 한다. make test-browser의 package build(tsup clean: true)는 make test-scripts의 다른 test가 읽는 동안 packages/template-ts/dist를 비웠고, make check가 tests/scripts/generated-conformance.test.mjs에서 @polyspec/template의 TS7016으로 실패했다 | node scripts/run-tests.mjs node -- tests/scripts/browser-server.test.mjs | [o] |
| T18.4 | 모든 npm 의존성을 bin link 없는 사본으로 설치한다. npm은 다섯 workspace package를 node_modules에 link했고 node_modules/.bin에 bin link 37개를 썼다. .npmrc는 install-links=true와 bin-links=false를 정한다. root package.json은 npm이 언제나 link하는 npm workspace 대신 packages/의 package를 override와 함께 file: 의존성으로 선언하고 그 개발 도구를 가진다. 각 build target은 그 package의 사본을 다시 설치한다. recipe와 script는 모든 도구를 그 package의 파일로 실행한다(scripts/tools.mjs). dependency policy는 root manifest만 읽는다. tests/scripts/no-symlinks.test.mjs는 node_modules나 vendor 아래의 symbolic link 하나에도 실패한다 | node scripts/run-tests.mjs node -- tests/scripts/no-symlinks.test.mjs | [o] |
| T18.5 | make install-cli로 template-fmt를 symbolic link 없이 선언한 prefix CLI_PREFIX 아래에 설치한다. formatter package의 사본은 <prefix>/lib/polyspec-template-fmt에, 그 진입점을 절대 경로로 실행하는 실행 script는 <prefix>/bin/template-fmt에 둔다. make uninstall-cli는 둘 다 지운다. npm link와 npm install --global은 bin link를 쓰며, T18.4 이후 전역 설치는 npm이 @polyspec/template 0.0.1을 registry에서 찾으므로 실패한다 | node scripts/run-tests.mjs node -- tests/scripts/install-cli.test.mjs | [o] |
| T18.6 | eslint를 최신 안정 release 10.12.0으로 올린다. make dependency-policy-check가 outdated npm dependency has no exception: eslint 10.11.0 < 10.12.0으로 실패했으므로 make release-check가 실패했다. 바뀐 package-lock.json의 digest를 생성된 JavaScript showcase manifest와 예제 page에 기록한다 | make dependency-policy-check lint-js | [o] |
| T18.7 | checkout마다 Rust crate를 자기 target에 빌드한다: Makefile은 물려받은 CARGO_TARGET_DIR를 cargo에 넘기지 않고, Rust test는 저장소 root를 compile 시점이 아니라 실행 시점의 CARGO_MANIFEST_DIR에서 정한다. main checkout의 target에서 crate의 오래된 산출물을 지운다. Cause: make check가 test-rust에서 실패했다. tests/bound.rs의 test 7개 중 6개가 repository root: NotFound로 panic했다. T17의 worktree가 main checkout의 target을 CARGO_TARGET_DIR로 두고 test를 빌드했고, env!("CARGO_MANIFEST_DIR")가 나중에 지워진 그 worktree의 경로를 넣었으며, cargo는 소스가 더 오래된 main checkout에서 그곳에서 빌드된 test binary 5개를 최신으로 판정했다. cargo clean -p polyspec-template가 main checkout의 target에서 오래된 test binary를 포함한 crate의 산출물(11236개 file, 600.2 MiB)을 지우고 의존성은 남겼다. AGENTS가 규칙을 적는다. Red: main checkout에서 node scripts/run-tests.mjs cargo --cwd packages/template-rust -- --locked --test bound가 compile 없이 끝나고 test 7개 중 6개가 repository root: NotFound로 실패했다. make build-rust와 make test-rust의 stub cargo가 물려받은 CARGO_TARGET_DIR를 받았으므로 tests/scripts/cargo-target.test.mjs의 2개 case가 실패했다. Green: 2개 case가 CARGO_TARGET_DIR=unset으로 통과한다. 이 작업의 worktree에서 make test-rust가 polyspec-template을 자기 target에 compile하고 clippy와 tests/bound.rs의 7개를 포함한 test 49개를 통과했다 | node scripts/run-tests.mjs node -- tests/scripts/cargo-target.test.mjs and make test-rust | [o] |
| T18.7-1 | Rust toolchain을 명시적인 한 단계에서 설치하고 어떤 cargo도 설치하지 않게 한다. release job에 1.98.1이 설치되어 있지 않았고 한 node --test 실행의 test file 셋이 cargo를 동시에 시작했으므로 CI가 test-scripts에서 실패했다. 각 rustup이 toolchain을 자동 설치했고, cargo 하나가 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은 npm ci 뒤에 rustup toolchain install --no-self-update를 실행하고, Makefile은 RUSTUP_AUTO_INSTALL=0을 export하며, CI workflow도 이를 설정하고, release job은 make install을, rust와 php job은 rustup toolchain install --no-self-update를 실행한다. scripts/run-tests.mjs의 요약은 실패한 test 없이 끝난 tool의 오류 줄을 적고, PHPUnit timeout test는 자기가 실행할 PHPUnit을 설치한다 | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/run-tests.test.mjs | [o] |
| T18.8 | TypeScript package의 빌드를 packages/template-ts/dist의 reader가 빠진 file을 보지 않게 내보내고, 입력이 바뀔 때만 빌드한다: scripts/build-ts.mjs는 dist 옆 directory에 빌드하고 각 file을 rename으로 dist에 옮기되 entry가 import하는 module을 먼저, entry를 마지막에 옮긴 뒤, 새 빌드에 없는 이전 빌드의 file을 지우며, 입력의 hash가 기록된 hash와 같으면 빌드하지 않는다. Cause: make check가 tsup으로 dist를 다시 빌드하는 동안 dist의 reader가 실패했다. tsup의 clean: true는 빌드 전에 dist를 비우므로 declaration 빌드가 끝날 때까지 index.d.ts, render.d.ts, node.d.ts가 없었다. make check 한 번이 같은 입력으로 그 빌드를 17번 실행했다. docs/operations/development.md가 내보내는 방식과 그 한계를 적는다: 바뀐 소스의 빌드가 내보내는 동안 여러 file을 읽는 reader는 두 빌드의 file을 섞어 읽을 수 있다. Red: tsup이 완전한 dist를 제자리에서 다시 빌드하는 동안 2 ms마다 dist를 읽은 reader가 1559번 중 413번 declaration index.d.ts, render.d.ts, node.d.ts가 없음을, 32~34번 entry module 여섯 개가 없음을 보았다. scripts/build-ts.mjs가 없었으므로 tests/scripts/build-ts.test.mjs의 2개 case가 실패했다. Green: 2개 case가 통과한다. 강제 빌드 동안 reader는 dist를 839번 읽고 빠진 file을 보지 않았고, 빌드는 이전 빌드와 같은 file을 같은 내용으로 썼으며, 입력이 같은 빌드는 dist가 최신이라고 출력하고 index.mjs를 건드리지 않았다. make build-ts는 한 번 빌드하고 설치했으며, 두 번째 make build-ts는 빌드도 설치도 하지 않았다 | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/build-ts.test.mjs | [o] |
| T18.8-1 | T18.8의 방식으로 template-language, template-lsp, template-codemirror와 template-vscode의 VS Code bundle을 빌드한다: scripts/build-ts.mjs는 scripts/build-package.mjs --package <package>가 되고, 입력에 root package-lock.json과 package가 의존하는 이 저장소 package의 설치된 사본도 넣으며, 기록 dist.inputs.json은 빌드의 file을 나열하고, 각 file은 그것이 import하는 상대 module 뒤에 옮긴다. Cause: 이 저장소 밖에서 이 package를 읽는 저장소는 없다(이 저장소를 사용하는 저장소의 git grep에서 0건). 그러나 make check 한 번이 같은 입력으로 이 package를 다시 빌드했다: tsup이 template-language를 7번, template-lsp를 3번, template-codemirror를 2번 실행했고, esbuild가 template-vscode를 2번 bundle했으며, 매번 다음 target이 읽는 dist를 비우거나 다시 썼다. Red: tests/scripts/build-package.test.mjs의 11개 case가 Unknown option '--package'로 실패했다. Green: 12개 case가 통과한다. 다섯 package마다 강제 빌드 동안 dist를 읽은 reader가 빠진 file을 보지 않았고, 빌드는 이전 빌드와 같은 file을 썼으며, 입력이 같은 빌드는 빌드하지 않았다. template-vscode의 입력은 설치된 template-lsp, template-language, template package를 담는다. 모르는 package는 그 이름과 함께 실패한다. 두 번째 make build-vscode build-codemirror는 빌드도 설치도 하지 않았다 | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/build-package.test.mjs | [o] |
| T18.9 | packages/template-php와 packages/template-php-ext의 phpunit/phpunit을 최신 안정 release 11.5.57로 올린다. make dependency-policy-check가 outdated Composer dependency has no exception: packages/template-php/composer.json phpunit/phpunit 11.5.56 < 11.5.57으로 실패했으므로 CI가 make release-test-matrix에서 실패했다. 검사가 닿지 못한 packages/template-php-ext에서도 composer outdated --direct --locked가 같은 version을 보고했다. composer update phpunit/phpunit --with-dependencies는 두 lock의 nikic/php-parser도 5.9.0으로 올린다 | make dependency-audit test-php test-ext | [o] |
| T18.10 | 의존성 상태를 저장소에 기록된 review와 network 없이 대조해 같은 tree가 언제나 같은 결과를 내게 한다. make dependency-policy-check는 실행할 때마다 registry를 조회했고, phpunit 11.5.57이 나온 뒤 tree T18.8에서 tree의 변경 없이 실패했다. scripts/check-dependency-policy.mjs는 manifest, lock, config/dependency-policy.json, 새 review 기록 config/dependency-review.json(review 시각, lock마다 sha256과 보안 권고, registry 의존성마다 잠긴 version과 최신 안정 version)을 읽고 모든 발견을 규칙과 고치는 방법과 함께 보고한다. composer validate는 COMPOSER_DISABLE_NETWORK=1로 실행한다. 개발자 명령 make dependency-review(scripts/dependency-review.mjs)는 새 안정 release와 보안 권고를 registry에 묻고, RECORD=1은 기록을 쓰며 UPDATE=1은 먼저 갱신한다. make dependency-audit는 live audit을 실행하지 않고, 예약된 workflow .github/workflows/dependency-review.yml이 매일 review를 실행한다. file: package와 platform 요구 사항은 범위의 정의에 따라 review 밖에 있고, gate는 file: package의 lock을 그 version과 대조한다 | node scripts/run-tests.mjs node -- tests/scripts/dependency-policy.test.mjs, make dependency-audit | [o] |
| T18.8-2 | tests/scripts/build-package.test.mjs가 자기가 의존하는 것을 직접 만들게 하고 package 사본을 atomic하게 설치하게 한다. npm ci 뒤 아무것도 빌드하지 않은 checkout에서 case 7개가 TS2307: Cannot find module '@polyspec/template'와 Could not resolve "@polyspec/template-lsp"로 실패했다. 설치된 사본에 dist가 없었고, test는 make test-scripts가 미리 빌드했다고 기대했지만 make owner-check와 직접 실행은 그러지 않는다. test는 case 전에 scripts/build-package.mjs --install로 template-ts, template-language, template-lsp를 빌드하고 설치한다. --install은 사본을 rm -rf와 npm install로 다시 설치했으므로 다시 설치하는 동안 사본의 reader가 file 20개가 없음을 보았다. 이제 바뀐 file마다 node_modules/<name>.next-<pid>에 쓰고 rename으로 사본에 옮기되 package.json을 마지막에 옮기며, package에 더는 없는 file을 지우고, package의 의존성이 사본과 다르면 npm install을 고치는 방법으로 적으며 실패한다 | node scripts/run-tests.mjs node --timeout 300 -- tests/scripts/build-package.test.mjs | [o] |
Wave 19 — 한 tree가 정하는 결과
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T19.1 | package 설치 검사가 network의 source를 읽지 않고 checkout의 toolchain을 쓰게 한다. Go 설치 프로젝트는 module을 proxy.golang.org에서 해석하고 checksum을 sum.golang.org에 대조했으며 GOTOOLCHAIN=auto로 실행했으므로, Go 1.27.0인 기계에서는 실행마다 Go toolchain 1.27.1을 module cache에 내려받았다. Rust 설치 프로젝트에는 lock이 없었으므로 cargo가 serde와 serde_json을 실행 시점의 최신 release로 해석했고, rust-toolchain.toml 밖인 시스템 임시 디렉터리에서 기계의 기본 toolchain(1.98.0, checkout은 1.98.1을 고정한다)으로 빌드했다. PHP 설치 프로젝트는 Packagist를 읽을 수 있었다. Go 설치 프로젝트는 실행의 proxy만, GOSUMDB=off, GOTOOLCHAIN=local과 packages/template-go/go.mod의 go 지시문을 쓴다. Rust 설치 프로젝트는 rust-toolchain.toml을 복사하고 packages/template-rust/Cargo.lock에서 유도한 lock(scripts/install-workspace.mjs의 installCargoLock)으로 cargo run --locked를 실행한다. PHP 설치 프로젝트는 Packagist를 끈다 | node scripts/run-tests.mjs node -- tests/scripts/package-installs.test.mjs | [o] |
| T19.2 | 모든 recipe를 checkout이 선언한 toolchain으로 실행한다. make install-tools(scripts/install-tools.mjs)는 package.json packageManager의 npm(12.2.0)과 packages/template-go/go.mod go directive의 Go(1.27.1, module golang.org/toolchain)를 wrapper script와 함께 var/tools에 설치하고, 이미 있는 version의 설치는 건너뛴다. Makefile은 var/tools/bin을 PATH의 맨 앞에 두고 GOTOOLCHAIN=local을 export하며, GNU Make 3.81은 shell 문법이 없는 줄의 program을 make가 시작할 때의 PATH로 찾으므로 npm을 $(NPM)으로 시작하고, make install은 install-tools에 의존한다. setup-php가 patch를 고정할 수 없으므로 PHP는 config/toolchain.json에서 minor version으로, Composer는 정확히 고정하고, var/full-run.json은 각 실행의 version을 기록한다. workflow는 모든 action을 commit으로 고정하고 ubuntu-24.04에서 실행하며 make install로 설치한다. tests/scripts/toolchain-files.test.mjs는 모든 변경에 실행된다 | node --test tests/scripts/toolchain-files.test.mjs tests/scripts/install-tools.test.mjs tests/scripts/full-run.test.mjs | [o] |
| T19.3 | build되지 않는 Go package의 compiler error를 출력한다. scripts/run-tests.mjs의 goEvents는 go test -json의 build-output과 build-fail event를 버렸으므로, compile되지 않는 module은 package가 실패했다는 것만 출력했다. runner는 둘을 출력하고 요약 줄은 compiler error를 밝힌다 | node --test tests/scripts/run-tests.test.mjs | [o] |
| T19.4 | 아무것도 검증하지 않는 검사를 실패시킨다. scripts/test-progress/progress.mjs의 progress.close는 test가 하나도 실행되지 않은 실행을 통과시켰고, 이제 node와 vitest reporter도 그 경우 process를 실패시킨다. packages/template-ts/tests/cli.test.ts는 dist가 없으면 test를 건너뛰었고 이제 그것을 build한다. make lint는 directory나 file이 없으면 gofmt, cargo fmt, pint를, make docs-check는 문서 coverage를 건너뛰었다. tests/runner/drivers.mjs의 selectLanguages는 package가 없는 언어를 버렸다. tests/scripts/no-silent-skips.test.mjs는 각각에서 실패한다 | node --test tests/scripts/no-silent-skips.test.mjs tests/scripts/run-tests.test.mjs | [o] |
| T19.5 | template-php의 Composer package로 PHP를 실행하는 모든 target 앞에서 그 package를 설치한다. conformance-generated-php, contract-check, benchmark-smoke, generated-native-check, bench, showcase는 build-php에 의존하지 않았으므로, packages/template-php/vendor가 없는 checkout에서 make generated-native-check가 vendor/autoload.php에서 실패했다 | node --test tests/scripts/test-targets.test.mjs | [o] |
| T19.6 | 실행이 읽는 것을 매번 build하되, 입력이 바뀌지 않으면 build가 아무것도 하지 않게 한다. tests/runner/drivers.mjs의 prepare는 binary가 없을 때만 언어의 CLI를 build했으므로 더 오래된 source로 build된 binary를 썼다. tests/scripts/install-cli.test.mjs와 tests/scripts/generated-conformance.test.mjs는 make test-scripts의 build에 기대어 dist가 없는 checkout에서 실패했다. 이제 test가 case 전에 자기가 읽는 것을 build하고 설치한다 | node --test tests/scripts/drivers.test.mjs tests/scripts/install-cli.test.mjs tests/scripts/generated-conformance.test.mjs | [o] |
| T19.7 | 다른 것이 읽는 파일과 directory를 한 번에 게시하고 실패하면 이전 version을 남긴다. make showcase와 make bench는 파일을 먼저 비우는 shell redirection으로 examples/site/data/mode-benchmark.json을 썼고, 이제 임시 파일을 쓰고 rename하는 benchmark-modes.mjs --output을 실행한다. make docs-verify-idempotent는 첫 build를 고정된 docs/.vitepress/dist.first에 복사했고, 이제 mktemp -d로 만든 자기 실행의 directory에 복사한다. scripts/install-cli.mjs는 npm install 전에 이전 설치를 지웠고, 이제 <prefix>/lib/.polyspec-template-fmt.next-<pid>를 준비하고 rename한다 | node --test tests/scripts/atomic-publish.test.mjs tests/scripts/install-cli.test.mjs | [o] |
| T19.8 | 한 실행의 실패를 모두 보고한다. make는 recipe를 첫 실패 명령에서, 실행을 첫 실패 target에서 멈추었고, 언어 검사 script는 첫 실패 언어에서 멈추었다. Makefile은 MAKEFLAGS += -k를 설정하고, scripts/full-run.mjs는 make -k <target>을 실행하며, 전체 suite의 target과 그 prerequisite는 모두 명령을 많아야 하나 실행하고 검사들은 prerequisite(lint-go, test-ts-types, documents-check 등)가 된다. scripts/check-generated-*.mjs, scripts/check-typed-generator.mjs, scripts/check-package-installs.mjs는 scripts/language-checks.mjs의 checkLanguages로 모든 언어를 실행한다. tests/scripts/failure-accumulation.test.mjs가 규칙을 검사한다 | node --test tests/scripts/failure-accumulation.test.mjs | [o] |
| T19.9 | 바뀐 경로를 읽는 검사를 모두 선택한다. scripts/owner-checks.json은 package에 conformance, generated conformance, lint를, template-rust에 test-ext를, template-ts에 language, LSP, CodeMirror, VS Code, browser test를, packages/template-compiler에 generated 검사와 showcase-check를, benchmark-smoke의 script에 benchmark-smoke를, scripts/generate-showcase-contract.mjs에 docs-check를, VS Code package에 test-vscode-integration을 선택하지 않았다. 이제 inputs가 검사 target마다 읽는 경로를 선언하고, scripts/owner-check.mjs --validate는 규칙이 그 target을 선택하지 않는 input 경로에서 실패한다 | node --test tests/scripts/owner-check.test.mjs | [o] |
| T19.10 | 실패한 것을 기대값과 실제값 또는 해결 방법과 함께 밝힌다. generated Go와 Rust conformance runner는 "the failed cases are listed above"라고 했고, 이제 실패한 case를 하나씩 밝힌다. scripts/run-tests.mjs는 도구가 시작되지 못하면 처리되지 않은 error event로 끝났고, 이제 명령과 설치 방법을 밝힌다. 제한을 넘긴 conformance driver의 CLI 호출은 spawnSync의 오류를 돌려주었고, 이제 제한과 명령을 밝힌다. Go와 PHP 설치 프로젝트는 어느 출력도 밝히지 않는 message로 실패했고, 이제 기대 출력과 실제 출력을 출력한다. PHP extension test는 run-tests.sh로 실행되었고, 이제 scripts/run-tests.mjs phpunit --php-extension으로 실행된다 | node --test tests/scripts/failure-messages.test.mjs | [o] |
| T19.11 | test가 도구의 출력 형식과 사용자에 기대지 않게 한다. tests/scripts/dependency-policy.test.mjs는 composer의 문구를 검사했고, 이제 gate의 메시지를 검사한다. scripts/check-package-installs.mjs는 npm archive의 이름을 npm pack의 마지막 줄에서 읽었고, 이제 manifest에서 계산한다. TypeScript, Go, Rust, PHP, PHP extension의 loader test는 mode 0o000 파일을 읽을 수 없다고 기대했지만 root는 그것을 읽는다. 이제 process가 읽을 수 없을 때에만 load가 실패하고 root만 읽을 수 있음을 검사한다. scripts/check-install-workspace.mjs는 root가 지울 수 있는 쓰기 권한 없는 directory로 제거를 실패시켰고, 이제 더 이상 없는 workspace를 지운다 | node --test tests/scripts/package-installs.test.mjs tests/scripts/dependency-policy.test.mjs | [o] |
| T19.6-1 | build의 새 파일이 아니라 바뀐 build를 실행한다. cargo는 build할 때마다 target/release를 다시 link하고 go build는 새 파일을 쓰며, macOS는 새 실행 파일을 처음 실행할 때 검사하므로, T19.6의 build 뒤 make delimiter-matrix에서 Go와 Rust CLI의 첫 conformance 호출이 10 s 제한을 넘겼다. scripts/publish-build.mjs는 byte가 바뀔 때에만 build를 var/build에 게시하고, driver, make test-ext, benchmark는 게시된 파일을 실행한다 | node --test tests/scripts/publish-build.test.mjs tests/scripts/drivers.test.mjs | [o] |
| T19.12 | 전체 실행의 record를 lock한다. scripts/full-run.mjs는 lock 없이 var/full-run.json을 읽고 결정하고 썼으므로, 함께 시작한 두 guard가 모두 실행하고 record를 번갈아 썼다. 이제 guard는 record를 읽을 때부터 마지막으로 쓸 때까지 scripts/holder-lock.mjs의 holder lock var/full-run.json.lock을 쥐고, 다른 guard가 쥐고 있으면 holder를 밝혀 거부하며, 놓인 holder lock은 process handler를 남기지 않는다. 첫 문서 build의 복사본은 T19.7부터 그 실행의 directory를 쓴다 | node --test tests/scripts/full-run.test.mjs tests/scripts/holder-lock.test.mjs | [o] |
| T19.13 | 검사가 시작한 group의 process를 남기지 않는다. tests/runner/bounded.mjs의 runBounded와 runStep은 명령이 닫힐 때, VS Code integration test의 runStep은 VS Code가 끝날 때 resolve했으므로, group의 background process가 아직 실행되며 호출자가 다음에 지우는 directory에 쓸 수 있었다. 이제 tests/scripts에서 옮긴 scripts/process-group.mjs의 stopProcessGroup이 group의 process를 하나도 찾지 못한 뒤에 resolve한다 | node --test tests/scripts/process-group-callers.test.mjs tests/scripts/process-group.test.mjs | [o] |
| T19.14 | Go를 checkout의 build cache에 build한다. generated 검사, scripts/check-typed-generator.mjs, scripts/check-compiler-interface.mjs는 함께 써야 할 요구 없이 모든 checkout과 실행이 함께 쓰는 고정된 /tmp/template-go-cache를 GOCACHE로 설정했다. 이제 Makefile이 GOCACHE=var/go/cache를 export하고, script는 recipe의 환경을 쓰며, make clean은 var/go를 지운다 | node --test tests/scripts/toolchain-files.test.mjs | [o] |
| T19.15 | 멱등성 규칙을 AGENTS에 적는다. Wave 19의 결함 종류마다 규칙 하나를 둔다. toolchain, network, build, 공유 상태, 아무것도 검증하지 않음, 한 실행에 모든 실패, owner, 메시지, 도구 출력과 사용자로부터의 독립성이다 | make docs-check | [o] |
| T19.8-1 | tests/scripts/failure-accumulation.test.mjs의 probe makefile을 실행의 파일로 넘긴다. test는 spawnSync의 input으로 -f /dev/stdin에 넘겼고 Node.js는 input을 socket으로 넘긴다. macOS는 descriptor를 복제하므로 GNU Make 3.81과 4.4.1에서 통과했지만, Linux는 /proc/self/fd/0을 열며 socket을 열지 못한다. 그래서 CI에서 make는 /dev/stdin: No such device or address로 실패하고 probe target을 하나도 실행하지 않았으며(stdout []), status 단언은 그 실패로 통과했다. test는 probe를 checkout 밖의 임시 directory에 쓰고, status보다 먼저 probe target의 출력을 단언하며, probe 파일과 make의 stderr를 밝힌다. owner check의 always에 있는 tests/scripts/device-paths.test.mjs는 추적되는 source의 device path에서 실패한다. 종류: 바깥 세계 의존(system의 device path) | node scripts/run-tests.mjs node -- tests/scripts/failure-accumulation.test.mjs tests/scripts/device-paths.test.mjs | [o] |
Wave 20 — 다른 실행과 아무것도 공유하지 않는 test
| ID | Task | Verification | Done |
|---|---|---|---|
| T20.1 | 검사와 runner의 모든 임시 directory와 생성 test를 checkout이 아니라 system 임시 directory 아래에 만든다. runner와 script 아홉 개가 mkdtemp로 임시 directory를 저장소 root, Go module, Rust crate 안에 만들었고, 다섯 개가 packages/template-go에 Go package directory를 만들었으며, 여섯 개가 Rust 통합 test를 packages/template-rust/tests에 썼다. 그래서 make test-scripts 4번 중 4번 tests/scripts/owner-check.test.mjs가 다른 test의 file에서 실패했고, 동시에 실행되는 cargo test가 반쯤 쓴 test를 compile할 수 있었다. scripts/temporary-workspace.mjs는 설치된 @polyspec/template의 사본을 가진 Node.js workspace, lock의 version으로 packages/template-rust에 path로 의존하는 Rust crate, packages/template-go의 module을 그 directory로 바꾸는 Go module을 만든다. tests/scripts/temporary-files.test.mjs는 그런 directory나 file을 checkout에 만드는 script에서 실패한다. Class: 공유 상태를 쓰는 test | 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 | tests/browser/run.mjs가 명시적 명령으로 server를 시작하게 한다. --server-command가 다른 명령을 지정하지 않으면 그 process의 Node.js다. tests/scripts/browser-server.test.mjs는 느린 wrapper를 그렇게 넘기고, wrapper가 실행되었는지 단언하며, packages/template-ts/dist의 inode가 그대로인지 대신 run.mjs가 build 단계를 실행하지 않는지 단언한다. server를 PATH에서 찾았으므로 test의 느린 stub이 실행되는지가 PATH 해석에 달려 있었고(한 실행은 0.6 s에 server를 시작했다), T18.8-2의 전제 조건 build가 실행 중에 그 dist를 다시 내보냈으므로 inode 단언이 실패했다. Class: 환경에 의존하는 test, 공유 상태의 경합 | node scripts/run-tests.mjs node -- tests/scripts/browser-server.test.mjs | [o] |
| T20.3 | tests/scripts/long-steps.test.mjs가 시작한 process가 모두 끝난 뒤에만 임시 directory를 지우게 한다. 긴 package 설치 단계 case는 검사의 process group을 SIGKILL로 끝내고 바로 directory를 지웠으므로, group의 process가 아직 그 안에 쓰는 동안 make test-scripts 4번 중 1번 그 directory에서 ENOTEMPTY로 실패했다. helper tests/scripts/process-group.mjs는 signal을 보내고, leader가 끝나기를 기다리고, group의 process가 하나도 남지 않을 때까지 확인한다. Class: 새는 process | node scripts/run-tests.mjs node -- tests/scripts/process-group.test.mjs tests/scripts/long-steps.test.mjs | [o] |
| T20.3-1 | tests/scripts/process-group.test.mjs의 directory를 작게 유지한다. writer들이 제한 없이 중첩 directory를 만들었으므로 make test-scripts의 부하에서 20번 시도의 삭제가 한 test의 30 s보다 오래 걸렸고, 3번 중 1번 test timed out after 30000ms로 실패했다. writer들은 각각 50개 이름 중에서 directory를 만들고, test는 자기 시간 제한을 갖는다. 기다리지 않는 helper는 여전히 ENOTEMPTY로 이 test를 실패시킨다 | node scripts/run-tests.mjs node -- tests/scripts/process-group.test.mjs | [o] |
| T20.1-1 | make install에서 모든 Cargo.lock의 crate를 받아 둔다. scripts/temporary-workspace.mjs의 rustWorkspace와 generated check, runner는 임시 crate를 cargo --offline으로 resolve하며, 이것은 machine에서 먼저 실행된 cargo 명령이 받아 둔 crate만 찾는다. 그래서 한 CI run에서는 tests/scripts/generated-conformance.test.mjs의 Rust runner case가 어느 cargo도 serde_json을 받기 전에 실행되어 no matching package named serde_json found로 실패했고, 다른 CI run에서는 나중에 실행되어 통과했다. 비어 있는 임시 CARGO_HOME에서는 매번 실패한다. make install은 추적되는 모든 Cargo.lock에 cargo fetch --locked를 실행하고, 검사는 offline으로 남으며, 실패한 cargo metadata --offline은 run make install을 밝히고, rustWorkspace는 실패할 때 자기 directory를 지운다. 다른 --offline cargo 호출은 rustWorkspace의 crate를 읽는다. Go 검사는 자기 실행의 module proxy와 module을 require하지 않는 packages/template-go만 읽고, Node.js workspace는 설치된 package를 복사하므로, 둘 다 다른 명령이 채우는 cache를 읽지 않는다. 종류: 바깥 세계 의존, 공유 상태의 race | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs; 비어 있는 임시 CARGO_HOME에서 rustWorkspace는 그곳에 cargo fetch --locked --manifest-path packages/template-rust/Cargo.toml을 실행하기 전에 실패하고 실행한 뒤 통과한다 | [o] |
| T20.1-2 | 모든 recipe에서 cargo, go, npm을 offline으로 실행한다. 검사의 cargo 명령(Makefile, driver, package 설치 프로젝트, showcase contract의 build, clippy, rustc, test, package, run, check)은 --offline 없이 실행되었으므로, 비어 있는 CARGO_HOME에서 recipe의 packages/template-rust cargo metadata --locked는 Updating crates.io index와 Downloading crates를 출력하고 통과했다. go는 https://proxy.golang.org,direct를, npm은 registry를 읽었다. Makefile은 CARGO_NET_OFFLINE=true, GOPROXY=off, npm_config_offline=true를 export하고, install, install-tools, dependency-review의 download만 $(ONLINE)으로 이를 푼다. 비어 있는 임시 CARGO_HOME에서 같은 recipe는 offline으로 실패하고, 그곳에 cargo fetch --locked를 실행한 뒤 통과한다. 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, install-cli가 offline으로 통과한다. 그 밖의 network 읽기: go는 checkout 밖의 module을 읽지 않는다(packages/template-go는 require가 없고, 설치 프로젝트는 자기 실행의 file proxy를 둔다). npm은 검사에서 local archive와 file: package만 설치한다. cargo의 offline 실패는 --offline 없이 다시 하라고 말하며, make install을 밝히는 것은 rustWorkspace뿐이다. build-php와 ext의 composer install은 vendor가 없으면 Packagist를 읽고, test-vscode-integration은 VS Code를 받는다. 둘 다 make install 밖이다. 종류: 바깥 세계 의존 | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs; 비어 있는 임시 CARGO_HOME에서 recipe의 packages/template-rust cargo metadata --locked는 그곳에 cargo fetch --locked를 실행하기 전에 offline으로 실패하고 실행한 뒤 통과한다 | [o] |
| T20.1-3 | Composer package를 make install에서 설치하고 검사에서는 Composer를 offline으로 실행한다. build-php와 build-ext-php는 composer install을 실행했고, 이것은 vendor가 없으면 Packagist를 읽었다. 그래서 vendor와 Composer cache가 비어 있을 때 make build-php는 24 MB를 받고 통과했다. scripts/composer-install.mjs <directory>는 composer install을 실행하고, 실패하면 directory와, network가 꺼져 있으면 run make install을 밝힌다. make install은 packages/template-php와 packages/template-php-ext에 대해 이것을 $(ONLINE)으로 실행하고, Makefile은 COMPOSER_DISABLE_NETWORK=1을 export하며, build-php, build-ext-php, PHP driver, PHPUnit을 설치하는 test는 이 script를 실행한다. vendor와 Composer cache가 비어 있을 때 make build-php는 offline으로 그 메시지와 함께 실패하고, 그 cache에 make install의 단계를 실행한 뒤 offline으로 통과한다. 종류: 바깥 세계 의존 | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/test-targets.test.mjs; vendor가 없는 packages/template-php 사본의 make build-php는 비어 있는 COMPOSER_CACHE_DIR에서 make install을 밝히며 실패하고 install 단계 뒤에 통과한다 | [o] |
| T20.1-4 | integration test의 VS Code를 make install에서 설치하고 검사에서는 내려받지 않는다. tests/integration/run.mjs는 첫 실행에서 @vscode/test-electron으로 VS Code 1.138.0을 .vscode-test에 내려받았다. 그래서 cache directory가 비어 있으면 실행은 update.code.visualstudio.com에서 version을 확인하고 302 MB download를 시작했다. scripts/install-vscode.mjs <directory>는 사본을 <directory>/.next-<pid>에 내려받아 vscode-<platform>-<version>으로 rename하고, 완전한 사본이 있으면 아무것도 하지 않으며, 동시에 실행된 install의 사본을 유지한다. make install은 var/tools/vscode에 대해 이것을 $(ONLINE)으로 실행하고, 실행은 --vscode var/tools/vscode를 받아 설치된 사본을 시작하며, 사본이 없으면 어떤 download보다 먼저 VS Code <version> is not installed in <directory>; run make install로 실패한다. 설치된 사본은 바뀌지 않으므로 .vscode-test의 holder lock, make clean-vscode-test, make vscode-test-unlock을 지웠다. 종류: 바깥 세계 의존 | node scripts/run-tests.mjs node -- tests/scripts/install-vscode.test.mjs tests/scripts/toolchain-files.test.mjs; make test-vscode-integration이 설치된 사본으로 통과한다 | [o] |
| T20.1-5 | cargo를 실행하는 모든 target에서 없는 crate에 대해 make install을 밝힌다. recipe는 cargo를 offline으로 실행하고, 비어 있는 CARGO_HOME에서 make build-rust는 cargo의 help: if this error is too confusing you may wish to retry without --offline로 실패했다. cargo-downloads-check(scripts/check-cargo-downloads.mjs)는 추적되는 모든 Cargo.lock에 network를 읽지 않는 cargo fetch --locked --offline을 실행하고, 각 lock, cargo의 첫 error 줄, run make install, which downloads them과 함께 실패한다. cargo를 실행하는 target 26개가 이것에 의존하므로, make -k에서 crate가 없는 target은 실행되지 않는다. owner check의 always에 있는 tests/scripts/cargo-downloads.test.mjs는 $(CARGO), run-tests.mjs cargo, 또는 직접이나 import하는 선언이나 이름을 적은 파일을 통해 cargo를 시작하는 파일로 cargo를 실행하는 target을 찾고, prerequisite가 없는 target에서 실패한다. prerequisite를 넣기 전에는 그 26개 target을 밝혔다. 비어 있는 임시 CARGO_HOME에서 make build-rust는 그 메시지와 함께 실패하고 cargo를 실행하지 않으며, 그곳에 세 lock의 cargo fetch --locked를 실행한 뒤 검사가 통과한다. 종류: 스스로 설명하지 않는 실패 | node scripts/run-tests.mjs node -- tests/scripts/cargo-downloads.test.mjs; 비어 있는 임시 CARGO_HOME에서 make build-rust는 make install을 밝히며 실패하고 fetch 뒤 make cargo-downloads-check가 통과한다 | [o] |
| T20.1-6 | VS Code는 integration test를 실행하는 곳에서만 설치한다. T20.1-4 뒤 make install은 CI job 9개 모두에서 VS Code를 내려받았고, 그 가운데 7개는 VS Code를 시작하지 않는다. make install-vscode는 이제 VS Code를 내려받지 않는 make install 뒤에 scripts/install-vscode.mjs를 $(ONLINE)으로 실행한다. make check와 make test-vscode-integration을 실행하는 ci.yml의 release job과 editor job은 test 전에 이것을 실행하고, 사본이 없는 integration 실행은 run make install-vscode와 함께 실패한다. tests/scripts/toolchain-files.test.mjs의 case는 make install이 VS Code를 내려받을 때, make target, 그 prerequisite, check의 전체 suite를 통해 test-vscode-integration에 닿는 job이 그 단계 전에 make install-vscode를 실행하지 않을 때, 다른 job이 그것을 실행할 때 실패한다. 변경 전에는 make install에서 실패했다. 종류: 바깥 세계 의존 | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs tests/scripts/install-vscode.test.mjs | [o] |
| T20.1-7 | VS Code integration check에서 확장을 기다리는 데 자기 시간 제한을 두지 않는다. tests/integration/suite/index.cjs의 eventually는 50 ms 간격 100번 polling 뒤 포기하고 false를 돌려주었다. 그래서 load average 41에서 format on type check는 5번 중 1번 5141 ms 뒤 + false - '<ul>\n {@ x = xs}'로 실패했고, 메시지는 text를 보여주지 않았다. tests/integration/suite/wait.cjs는 조건이 성립할 때까지 polling한다. check의 20 s timeout만 기다림을 끝내고 polling을 멈추며 기다린 것을 밝히고, textBecomes는 문서의 기대 text와 실제 text를 밝힌다. 그 뒤 check는 문서의 text를 기대 text와 비교한다. tests/integration-wait.test.mjs는 5.5 s 뒤에 문서에 format을 적용한다. 이전 wait로는 5.2 s 뒤 이전 text와 함께 실패했고 지금은 통과한다. timeout 메시지와, 멈춘 wait가 더 polling하지 않는다는 것도 검사한다. 그 메시지가 부하에서의 실패 원인을 보여주었다. load average 73–88에서 3번 중 1번, check는 "<ul>\n {@ x = xs}"를 20 s 기다렸지만 문서는 여전히 "<ul>\n"였다. 즉 focus된 editor에 입력하는 type 명령이 새 editor에 아무것도 입력하지 않았다. 이제 typeInto는 그 editor가 active가 될 때까지 기다린 뒤 입력하며, load average 63–75에서 5번 중 5번 통과했다. 종류: 시간 의존, 스스로 설명하지 않는 실패 | node scripts/run-tests.mjs node --cwd packages/template-vscode -- tests/integration-wait.test.mjs; make test-vscode-integration | [o] |
| T20.1-8 | CI의 모든 도구를 make로 실행한다. ci.yml의 step 7개가 node를 직접 시작했다. release, browser, editor job의 node node_modules/@playwright/test/cli.js install --with-deps chromium과 언어 job의 node tests/runner/conformance.mjs --langs ts, ts,go, ts,rust, ts,php다. 그래서 recipe의 offline 설정과 cargo-downloads-check가 이것들에 적용되지 않았다. make install-browsers는 Chromium을 $(ONLINE)으로 설치하고, make conformance-ts, conformance-go, conformance-rust, conformance-php는 cargo-downloads-check와 읽는 build 뒤에 TypeScript와 다른 한 언어의 conformance를 실행하며, job은 이 target을 실행한다. tests/scripts/toolchain-files.test.mjs의 case는 make 없이 node, npm, npx, cargo, go, php, composer, rustup을 시작하는 workflow step에서 실패하며, xvfb-run 뒤와 여러 줄의 step도 본다. 변경 전에는 그 7개 step을 밝혔다. 종류: 바깥 세계 의존 | 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 | CI의 모든 실패 이유를 남긴다. release job만 무언가를 upload했고 그것도 기록 var/full-run.json뿐이었으며, 어떤 job도 job log 밖에 target 출력을 남기지 않았다. docs, typescript, browser, go, rust, php, pages job은 target을 별도 step이나 make -k 하나로 실행했다. scripts/target-report.mjs는 make -k <target>마다의 출력을 targets/<target>.log에 쓰고, target마다 결과와 시간, 실패한 target마다 첫 실패 줄을 담은 summary.md를 job summary에도 쓴다. make check는 이것을 var/report/full-run에 쓰고, make ci-targets TARGETS="..."(scripts/ci-targets.mjs)는 모든 target을 끝까지 실행하고 var/report/ci-targets를 쓴다. ci.yml의 모든 job은 이 둘 중 하나로 검사를 실행하고, if: !cancelled()와 if-no-files-found: error로 report를 upload하며, release job은 기록도 upload한다. tests/scripts/target-report.test.mjs는 실패하는 probe target과 통과하는 probe target을 실행하고 두 log와, summary와 job summary의 실패 줄을 확인한다. tests/scripts/toolchain-files.test.mjs의 case는 두 runner 밖에서 검사를 실행하거나 report를 upload하지 않는 job에서 실패하며, 변경 전에는 모든 job을 밝혔다. 종류: 스스로 설명하지 않는 실패 | 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 | 전체 검사와 end-to-end 검사를 로컬에서 실행하지 않는다. AGENTS는 commit마다 그 전에 make owner-check를, 작업을 끝내기 전에 흔히 conformance나 integration 검사인 작업의 소유 명령을, push 전에 make check를 요구했고, docs/operations/development.md는 CI가 make check를 실행하지 않는다고 적었다. 이제 AGENTS와 docs/operations/development.md는 개발에서 unit test만 실행하고, commit이나 push 전에 make check, make rerun-failed, make owner-check, conformance, browser, VS Code integration 실행을 로컬에서 하지 않으며, CI가 push 뒤에 release job의 make check와 다른 job의 make ci-targets로 이것을 실행하고 모든 실패의 report를 남긴다고 적는다. 작업은 unit test가 통과하고 CI가 그 push를 통과했을 때 끝난다 | node scripts/run-tests.mjs node -- tests/scripts/check-documents.test.mjs | [o] |
| T20.1-11 | T20.1-9에 대한 CI의 실패 세 가지를 고친다. make ci-targets는 모든 job에서 첫 target 전에 var/report/ci-targets.lock.<pid>.<random>의 ENOENT로 실패했다. holder lock은 report 옆에 쓰는데 새 checkout에는 var/report가 없기 때문이며, 그래서 어떤 job도 report를 upload하지 않았다. tests/scripts/target-report.test.mjs는 target log가 그 출력으로 시작한다고 기대했지만, GNU Make 4는 다른 make 아래에서 make[2]: Entering directory를 먼저 출력한다(T17.1-4의 종류). checklist의 두 행은 inline code 안에 ${{ !cancelled() }}를 담았고 VitePress는 이것을 Vue interpolation으로 평가한다. 그래서 build는 두 checklist page에 대해 _ctx.cancelled is not a function을 출력하고도 통과했으며, docs/operations/browser.md의 {{ msg }}는 아무것도 아닌 것으로 렌더되었다. scripts/ci-targets.mjs는 lock 전에 report의 상위 directory를 만든다. test는 report를 없는 var/report 아래에 두고 GNU Make 4의 줄을 받아들이며, 수정 전에는 GNU Make 4.4.1과 MAKELEVEL=1에서 ENOENT로 실패한다. scripts/check-documents.mjs는 docs 아래 page에서 fenced code block이나 v-pre element 밖의 {{에서 실패하며, 수정 전에는 네 줄을 밝혔다. 종류: machine 의존, false green | node scripts/run-tests.mjs node -- tests/scripts/target-report.test.mjs tests/scripts/check-documents.test.mjs | [o] |
| T20.1-12 | runner가 적으므로 새 push가 같은 workflow와 ref의 실행 중인 CI를 취소하게 한다. ci.yml은 group ci-<ref>에서만 실행을 취소했고, 이 이름은 push로 실행되는 다른 workflow가 다시 쓸 수 있었다. push나 pull request로 실행되는 모든 workflow는 group <workflow>-<ref>와 cancel-in-progress: true를 가진 concurrency를 둔다. dependency-review.yml은 schedule과 수동으로만 실행된다. tests/scripts/toolchain-files.test.mjs의 case는 그 concurrency가 없는 push나 pull request workflow에서 실패하며, 변경 전에는 ci.yml에서 실패했다. 종류: 공유 상태의 race | node --test --test-name-pattern="cancels the running CI" tests/scripts/toolchain-files.test.mjs | [o] |
| T20.1-13 | CI matrix의 모든 조합을 끝까지 실행한다. CI에서 PHP 8.2 job이 실패하자 GitHub가 PHP 8.5 job을 취소해 그 결과가 사라졌다. matrix는 fail-fast가 false가 아니면 첫 실패에서 다른 job을 취소하기 때문이다. ci.yml의 php job은 fail-fast: false를 두고, tests/scripts/toolchain-files.test.mjs의 case가 matrix를 가진 모든 job에 이것을 요구한다. 이 case는 이전 ci.yml에서 실패했다. 종류: 첫 실패에서 멈추는 단계 | node --test --test-name-pattern="matrix runs every combination" tests/scripts/toolchain-files.test.mjs | [o] |
Wave 21 — C로 구현한 PHP extension
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T21.1 | PHP extension을 C로 build하고 그것으로 template을 parse한다. packages/template-php-ext/src는 C source, config.m4, class TemplateError와 Engine(__construct, parse, parseToJson)을 담은 polyspec_template.stub.php를 둔다. make ext-arginfo는 PHP build의 gen_stub.php로 stub에서 polyspec_template_arginfo.h를 만든다. package에서 하는 build를 위해 이 header를 commit한다. scripts/build-php-extension.mjs는 임시 directory에서 모든 compiler 경고를 오류로 하여 phpize, configure, make로 extension을 build하고, library의 byte가 바뀔 때만 rename으로 게시하며, 입력(source, stub, PHP build)이 바뀌지 않은 build는 건너뛰고, header가 stub에서 만들어지지 않았거나 php-config와 php가 같은 PHP가 아니면 고치는 방법을 적어 실패한다. parser는 AST가 있는 conformance case 227개 모두의 AST와 parse 오류 case 21개의 오류를 낸다. T21.4까지는 Rust extension이 make ext의 extension으로 남는다 | 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 뒤에 node scripts/run-tests.mjs phpunit --php-extension var/build/polyspec_template.so --cwd packages/template-php-ext -- --filter EngineTest::testParse와 --filter 'EngineTest::test.*Delimiters' | [o] |
| T21.2 | C extension에서 JSON data로 template을 render한다. Engine::renderJson은 assign data, definition, environment를 JSON text로 받아 VAL-2, VAL-12, VAL-20의 숫자, UTF-8, 깊이 규칙에 따라 문서 순서대로 읽고, filesystem loader와 그 version cache(RT-9, RT-10, RT-40), template 이름(RT-7, RT-8), 사양의 모든 문장, 식, 한도, 모든 내장 함수로 render한다. native object의 json은 TypeScript와 Rust runtime처럼 null을 쓴다. conformance case 248개 모두가 기대한 출력이나 오류를 낸다 | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so 뒤에 node scripts/run-tests.mjs phpunit --php-extension var/build/polyspec_template.so --cwd packages/template-php-ext -- --filter를 ExtConformanceTest, LoadFailureTest::testJson, EngineTest::testRenderJson, EngineTest::testLimits로 실행 | [o] |
| T21.3 | C extension에서 PHP 값을 binding한다. Engine::render는 PHP 값으로 받은 assign data, definition, environment를 binding하고(VAL-11, VAL-14, VAL-17~VAL-20), bound map은 다시 binding하지 않고 받는다(VAL-22). register와 registerClass는 host 함수와 논리 class 함수를 등록한다(FUN-43, FUN-44). 이 함수들은 VAL-21의 host 형태로 인자를 받고, 던진 예외는 E_RUNTIME_HOST_FUNCTION으로 실패하며, host code의 native TemplateError는 그대로 전달된다. native object는 instance를 유지하고, 호출한 쪽의 scope와 관계없이 public property와 method를 보이며, 자기 자신과만 같다(VAL-18, VAL-19, EXP-39). BoundMap::bind와 merge는 PHP code가 생성, 복제, unserialize할 수 없는 bound map을 만든다(ERR-14, RT-26). extension test EngineTest, BoundMapTest, HostBindingTest, HostValuesTest, LoadFailureTest, ExtConformanceTest가 C build에서 변경 없이 통과한다 | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so 뒤에 node scripts/run-tests.mjs phpunit --php-extension var/build/polyspec_template.so --cwd packages/template-php-ext -- --filter를 EngineTest, BoundMapTest, HostBindingTest, HostValuesTest, LoadFailureTest, ExtConformanceTest로 실행 | [o] |
| T21.4 | Rust PHP extension을 C extension으로 바꾼다. make ext는 scripts/build-php-extension.mjs로 C source를 var/build/polyspec_template.so에 build하고, test-ext는 그 build로 명령줄을 통한 conformance case와 PHPUnit suite를 실행하며, conformance runner도 같은 방법으로 php-ext driver를 build한다. make install, lint, test-ext는 더 이상 Rust extension을 fetch, format, lint하지 않는다(lint-ext와 test-ext-clippy는 지운다). Rust source, Cargo.toml, Cargo.lock, build.rs, ext-php-rs 의존을 지우고, stub은 PIE build path를 위해 src/로 옮겼으며, composer.json은 type php-ext를 가진다. CliTest는 cargo 출력 대신 게시된 library를 load하고, 모든 Rust method가 panic 경계 안에서 실행되는지 보는 test는 C extension에 Rust code가 없으므로 지운다. 다른 extension test는 바꾸지 않는다. README, docs/operations/development.md, docs/spec/conformance.md, feature 계약, AGENTS가 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가 make test-ext를 실행 | [o] |
| T21.4-1 | CI runner의 GCC로 C extension을 build한다. CI는 PHP 8.2에서 test-ext가 실패했다. macOS의 clang과 달리 GCC는 -Werror에서 Engine::render의 지역 변수 assign과 options가 longjmp로 바뀔 수 있다고(-Wclobbered), date의 seconds가 초기화되지 않고 쓰일 수 있다고(-Wmaybe-uninitialized) 경고했기 때문이다. 각 실행(parse, renderJson, render)의 작업을 별도 함수로 옮겨 setjmp를 부르는 frame의 지역 변수가 그 뒤에 바뀌지 않게 하고, 함수가 out parameter로 채우는 지역 변수는 모두 초기화한 상태로 시작한다 | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so 뒤에 make test-ext-unit; CI가 PHP 8.2와 8.5의 GCC로 make test-ext를 실행 | [o] |
| T21.4-2 | 실행의 입력을 setjmp를 넘어 memory에 둔다. T21.4-1 뒤에도 CI는 PHP 8.2와 8.5에서 test-ext가 실패했다. GCC가 Engine::render의 assign과 options에 -Wclobbered를 냈기 때문이며, parameter parsing이 이 둘을 setjmp를 부르는 frame에서 조건부로 설정한다. 모든 실행은 setjmp를 부르는 유일한 함수 pt_run_work를 거친다. 이 함수는 작업과 입력 구조체의 pointer를 받으므로, method의 어떤 입력도 그 frame의 register 지역 변수가 아니다 | node scripts/build-php-extension.mjs packages/template-php-ext/src var/build/polyspec_template.so 뒤에 make test-ext-unit; CI가 PHP 8.2와 8.5의 GCC로 make test-ext를 실행 | [o] |
Wave 22 — Tag 릴리스
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T22.1 | main의 tag된 commit을 릴리스: 모든 변경은 필수 check와 함께 merge queue로 main에 도달하고, 메인테이너는 버전 올림 pull request 뒤에 main의 commit에 vX.Y.Z(Go module은 packages/template-go/vX.Y.Z) tag를 붙여 릴리스한다. changelog는 릴리스된 버전 위에 ## Unreleased를 두고, Go module 경로는 그 directory와 맞으며, ci.yml의 job ci-passed는 다른 모든 job이 통과했을 때만 통과하고 ruleset main은 정확히 push-gate와 ci-passed를 요구하며, .github/workflows/release.yml은 tag된 commit을 검사하고 package archive와 함께 GitHub Release를 만든다(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 | CHANGELOG.md와 CHANGELOG.ko.md에서 릴리스된 버전 위에 ## Unreleased 유지: 최신 항목은 언어 link 위에 heading 없이 있었고, ## Unreleased는 tag v0.0.1의 항목을 포함한 가장 오래된 항목을 담았다. 언어 link는 제목 아래에 있고, v0.0.1의 항목은 ## 0.0.1을 이루며, 그 뒤의 항목은 ## Unreleased 아래에 남고, 모든 변경은 항목을 그곳에 추가한다 | make docs-check | [o] |
| T22.1-2 | Go module의 이름을 그 directory로 정함: packages/template-go/go.mod는 module github.com/polyspec/template을 선언했고, go get은 그 module을 go.mod가 없는 저장소의 root에서 찾는다. module은 github.com/polyspec/template/packages/template-go이고, package와 test의 모든 import, showcase adapter, compiler의 Go backend와 generated program과 그 manifest, example page, generated check, 문서가 그 경로를 쓰며, tag packages/template-go/vX.Y.Z가 그 module을 릴리스한다. tests/scripts/toolchain-files.test.mjs는 module 경로에 domain이 있는 추적 go.mod가 github.com/polyspec/template/<그 directory>를 적지 않으면 실패한다 | node scripts/run-tests.mjs node -- tests/scripts/toolchain-files.test.mjs, make artifact-digest-check | [o] |
| T22.1-3 | ci.yml에 하나의 check를 요구: ruleset main은 ci.yml의 모든 job(release, docs, typescript, browser, editor, go, rust, php (8.2), php (8.5))을 적었으므로, ci.yml에 추가한 job은 ruleset이 이름을 적기 전까지 요구되지 않았다. 마지막 job ci-passed는 다른 모든 job을 need로 가지고, ubuntu-24.04에서 if: ${{ always() }}로 실행되며, 자기 조건이 없는 step에서 make ci-passed RESULTS='${{ toJSON(needs) }}'를 실행한다. scripts/ci-passed.mjs는 need로 가진 모든 job의 결과가 success가 아니면 다른 각 job과 결과를 적고 실패한다. ruleset은 정확히 push-gate와 ci-passed를 요구한다. tests/scripts/toolchain-files.test.mjs는 ci-passed가 없거나, 마지막 job이 아니거나, if: ${{ always() }}가 없거나, 다른 모든 job을 need로 가지지 않거나, 다른 runner에서 실행되거나, 다른 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 | workflow로 tag를 릴리스: tag에서 실행되는 workflow가 없었으므로, 릴리스에는 그 commit이 main에서 필수 check를 통과했는지, manifest와 changelog가 그 버전을 담는지, archive가 그 commit에서 만들어졌는지 확인하는 검사가 없었다. ci.yml의 job release가 아닌 .github/workflows/release.yml은 권한 contents: write로 tag v* 또는 */v*의 push에서 실행되고 make release-verify(tag된 commit이 origin/main의 조상이고 그 commit의 최신 check run push-gate와 ci-passed가 success로 끝남), make release-versions(MANIFESTS의 모든 manifest의 X.Y.Z. version이 없는 composer.json은 tag를 따름. CHANGELOG.md의 ## X.Y.Z. packages/template-go/vX.Y.Z에는 packages/template-go/go.mod의 module 경로), make release-assets(build한 npm 패키지의 npm pack, Composer 패키지의 git archive zip, cargo package --no-verify --locked. 이름은 <package name>-<version>.<ext>), make release-publish(section을 notes로 한 gh release create --verify-tag)를 실행한다. scripts/release.mjs는 실패한 check와 파일마다 두 버전을 적는다. tests/scripts/release.test.mjs는 gh, npm, cargo의 fake로 step을 실행하고 모든 추적 manifest가 릴리스되거나 릴리스하지 않는다고 선언되기를 요구하며, tests/scripts/toolchain-files.test.mjs는 trigger, 권한, step의 순서를 요구한다. AGENTS가 릴리스 절차를 적는다 | 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 | 어느 깊이의 Go module tag와 npm, Composer archive만 릴리스: tag filter에서 *는 /와 맞지 않으므로 trigger tags: ['v*', '*/v*']는 packages/template-go/vX.Y.Z와 맞지 않았고, make release-assets는 git 의존성을 해석되지 않는 crates.io 요구로 바꾸는 cargo package로 .crate를 만들었으며 Go module tag에서도 npm 패키지를 build했다. trigger는 tags: ['v*', '**/v*']이고 릴리스 asset은 npm tarball과 Composer zip뿐이다. scripts/release.mjs의 MANIFESTS는 packages/template-rust/Cargo.toml을 archive로 릴리스하지 않고 git tag로 사용하는 manifest로 적는다. Go module tag는 아무것도 만들거나 첨부하지 않는다. release-assets는 /가 없는 tag에서만 npm build에 의존하고, 그 step은 cargo를 실행하지 않는다. tests/scripts/release.test.mjs는 Cargo archive, 다른 릴리스 방식의 Cargo manifest, Go module tag의 npm build에서 실패하고, tests/scripts/toolchain-files.test.mjs는 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 | 0.0.2 릴리스: scripts/release.mjs의 MANIFESTS의 모든 manifest(root package.json, npm 패키지 template-ts, template-language, template-lsp, template-codemirror, VS Code extension, Rust crate)가 0.0.2를 선언하고, 저장소의 npm 패키지 사이의 의존성과 C extension의 버전이 0.0.2를 적으며, package-lock.json, packages/template-rust/Cargo.lock, tools/showcase/adapters/rust/Cargo.lock이 저장소의 패키지에 0.0.2를 기록하고, CHANGELOG.md와 CHANGELOG.ko.md의 ## Unreleased는 새 빈 ## Unreleased 아래의 ## 0.0.2가 된다. scripts/check-package-installs.mjs는 Rust crate archive polyspec-template-0.0.1.crate를 읽었고, tests/scripts/package-installs.test.mjs의 npm pack과 cargo package stub은 버전 0.0.1의 archive를 썼으며, tests/scripts/release.test.mjs는 tag v0.0.1을 검사했다. 검사는 packages/template-rust/Cargo.toml의 버전을 읽고, stub은 archive 이름을 pack한 manifest의 버전으로 정하며, release test는 root package.json의 버전을 검사한다. Red: make release-versions는 v0.0.2에서 manifest 일곱 개의 version 0.0.1과 section ## 0.0.2가 없음으로 실패했고, packages/template-go/v0.0.2에서 section ## 0.0.2가 없음으로 실패했다. 버전 0.0.2에서 tests/scripts/package-installs.test.mjs는 TypeScript와 Rust에서 실패했고 tests/scripts/release.test.mjs는 tag v0.0.1에서 실패했다. Green: 모두 통과한다 | 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 | 버전 0.0.2로 CI 통과: compiler digest는 packages/template-ts의 버전을 포함하므로 examples/site/scenarios의 AST manifest와 tools/showcase/adapters/generated/javascript의 JavaScript manifest는 0.0.1 compiler의 digest를 기록했고, config/dependency-review.json은 버전 변경 전 lock의 sha256을 기록했으며, tests/scripts/dependency-policy.test.mjs는 lock과 다르게 하려고 packages/template-lsp를 0.0.2로 정했다. scripts/dependency-review.mjs는 npm view --json을 한 object로 읽었고 npm 12는 여러 field를 object 하나의 array로 출력하므로, review는 모든 npm 의존성에서 실패했다. make showcase가 artifact를 다시 compile한다. review는 두 형식을 모두 읽고, npm stub은 npm 12의 형식으로 답하며, make dependency-review UPDATE=1이 typescript-eslint를 8.71.1로, source-map-js를 1.2.2(advisory GHSA-68fv-2mgg-jv7q)로 올리고 검토를 기록한다. gate test는 tree와 다른 버전을 정한다. Red: make artifact-digest-check는 artifact 30개 중 10개를 적었고, make dependency-policy-state-check는 바뀐 lock을 적었으며, tests/scripts/artifact-digests.test.mjs와 tests/scripts/dependency-policy.test.mjs가 실패했고, npm 12 stub에서 review test가 실패했다. Green: 모두 통과한다 | 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 | 릴리스 notes를 GitHub의 한도 안에 두기: make release-publish는 CHANGELOG.md의 section ## X.Y.Z 전체를 gh release create의 body로 넘겼고, GitHub는 125000자를 넘는 release body를 거부한다. scripts/release.mjs의 NOTES_LIMIT은 125000자다. releaseNotes는 NOTES_LIMIT자 이하의 section을 그대로 두고, 더 긴 section을 tag의 CHANGELOG.md를 가리키는 한 줄로 바꾼다. 이 줄의 tag는 경로 segment마다 encode되고, anchor는 ## X.Y.Z의 anchor인 점을 뺀 버전이다. Red: NOTES_LIMIT과 releaseNotes가 없어 tests/scripts/release.test.mjs가 실패했다. Green: 한도를 넘는 section은 vX.Y.Z와 packages/template-go/vX.Y.Z에서 link가 있는 줄이 되고, 한도의 section은 그대로 남으며, publish는 그 줄을 보낸다 | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, make docs-check | [o] |
| T22.2-3 | 릴리스 asset을 저장소 밖에서 설치: packages/template-php/composer.json과 packages/template-php-ext/composer.json은 버전을 선언하지 않았으므로 Composer의 artifact repository가 그 릴리스 zip을 읽지 못했고, pack된 manifest를 읽는 검사가 없었다. 사용자는 릴리스 asset을 내려받아 함께 설치하므로 각 발행 manifest는 polyspec 의존성을 정확한 버전으로 적고, 각 archive는 그 manifest를 바꾸지 않고 담는다. 각 발행 composer.json은 릴리스의 버전을 선언하고, 그 composer.lock은 content hash를 기록하며, make release-versions는 모든 composer.json의 버전을 요구하며, scripts/check-dependency-policy.mjs의 composer validate는 version field를 알리지 않는다(--no-check-version). make release-assets가 실행하는 scripts/release.mjs의 checkAssets는 pack된 manifest가 tag된 commit의 manifest와 다를 때, dependencies, peerDependencies, optionalDependencies의 @polyspec/* 의존성이나 require의 polyspec/* 패키지가 경로(file:, link:, workspace:), git source, URL, range, @dev, 릴리스와 다른 버전일 때, composer.json에 버전이 없거나 repositories가 있을 때 실패한다. treeManifestProblems는 tree의 발행 manifest에 같은 규칙을 적용한다. docs/operations/publication.md는 file: tarball과 artifact 또는 package repository로 설치하는 방법과 개발 구성을 적는다. Red: checkAssets, manifestProblems, packedManifest, treeManifestProblems가 없어 tests/scripts/release.test.mjs가 실패했고, make release-versions는 버전이 없는 composer.json을 통과시켰으며, Composer는 zip의 artifact repository에서 polyspec/template을 설치하지 못했다. Green: tree의 pack된 asset이 저장소 밖에서 빈 cache의 npm install과 artifact repository로 설치되고, 바뀐 manifest와 저장소에서만 해석되는 manifest는 검사에서 실패한다 | node scripts/run-tests.mjs node -- tests/scripts/release.test.mjs, make dependency-policy-state-check, make docs-check | [o] |
| T22.3 | 0.0.3 릴리스: scripts/release.mjs의 MANIFESTS의 모든 manifest(root package.json, npm 패키지 template-ts, template-language, template-lsp, template-codemirror, VS Code extension, Composer 패키지 template-php, template-php-ext, Rust crate)가 0.0.3을 선언하고, 저장소의 npm 패키지 사이의 의존성과 C extension의 버전이 0.0.3을 적으며, package-lock.json, 각 Composer 패키지의 composer.lock, packages/template-rust/Cargo.lock, tools/showcase/adapters/rust/Cargo.lock이 0.0.3 또는 manifest의 content hash를 기록하고, CHANGELOG.md와 CHANGELOG.ko.md의 ## Unreleased는 새 빈 ## Unreleased 아래의 ## 0.0.3이 된다. make showcase가 0.0.3의 compiler digest로 AST와 JavaScript manifest를 compile하고, make dependency-review RECORD=1이 바뀐 lock을 기록한다. Red: make release-versions는 v0.0.3에서 manifest 아홉 개의 version 0.0.2와 section ## 0.0.3이 없음으로 실패했다. Green: make release-versions가 v0.0.3과 packages/template-go/v0.0.3에서 통과하고, make artifact-digest-check와 make dependency-policy-state-check가 통과한다 | 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 | commit한 consumer project로 릴리스 asset 설치: tests/scripts/release.test.mjs의 설치 test는 npm과 Composer project를 실행 시점에 lock 없이 썼고, tree의 git archive는 각 zip에 현재 시각을 썼으므로 어떤 lock도 zip을 고정하지 못했다. tests/fixtures/release-consumer는 @polyspec/template과 @polyspec/template-language의 tarball을 file:로 적는 npm project(package.json, package-lock.json)와 zip의 artifact repository를 쓰는 Composer project(composer.json, composer.lock)를 담는다. make release-consumer-lock(scripts/release-consumer.mjs)은 고정된 날짜에 placeholder 파일과 함께 commit한 tree manifest의 archive로 두 lock을 쓰므로 lock은 manifest와 함께만 바뀐다. test는 각 project와 archive를 저장소 밖의 directory에 복사하고, 빈 cache와 연결되지 않는 @polyspec scope registry로 npm ci를, 빈 COMPOSER_HOME과 cache로 composer install을 실행한다. make release-assets는 각 Composer 패키지를 tag된 commit의 시각으로 archive한다(--mtime). AGENTS는 결과가 시간에 따라 달라지는 registry 조회는 검사에서 실행하지 않고 lock이 고정한 download는 설치라고 적는다. T22.2-3의 변경은 tests/scripts/release.test.mjs의 case 아홉 개(Go tag, publish, 추적 manifest의 선언, asset의 종류, tree의 버전, make step, notes 한도)를 지웠다. 이 case는 다시 실행되고, NOT_RELEASED는 consumer manifest 두 개를 선언한다. Red: Composer project는 lock의 shasum에서 실패했고 두 실행의 zip이 달랐다. Green: npm ci와 composer install이 빈 cache로 commit된 lock에서 설치하고, zip은 매 실행 같은 byte다 | 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 | 릴리스의 모든 npm 패키지를 build된 archive에서 설치: npm consumer project는 @polyspec/template과 @polyspec/template-language만 설치했고, test 대상 archive는 hash가 고정되도록 placeholder 파일을 담았으며, AGENTS는 검사가 download를 읽지 않는다고 적었다. tests/fixtures/release-consumer의 npm consumer project는 @polyspec/template, @polyspec/template-language, @polyspec/template-lsp, @polyspec/template-codemirror를 설치하고, 그 lock은 @polyspec/template-codemirror의 peer dependency를 포함한 registry 의존성을 정확한 버전과 integrity로 고정한다. scripts/release-consumer.mjs는 make release-assets의 step인 scripts/release.mjs의 packArchives로 tree의 build된 패키지를 pack하고, lock은 test 대상 archive를 npm에서는 integrity 없이, Composer에서는 빈 shasum으로 이름과 버전만 기록하므로 lock은 버전이나 의존성과 함께만 바뀐다. test의 npm ci는 --offline=false로 online에서 실행되고, make release-consumer-lock은 npm build 뒤 $(ONLINE)으로 실행되며, make test-scripts는 template-codemirror를 build한다. AGENTS와 Makefile은 결과가 시간에 따라 달라지는 registry 조회는 검사에서 실행하지 않고, lock이 정확한 버전과 integrity로 고정한 download는 검사가 실행할 수 있는 설치라고 적는다. Red: commit된 consumer project는 tarball 네 개 중 두 개를 적었고, lock은 placeholder archive의 hash를 기록했다. Green: npm ci가 tarball 네 개와 고정된 registry 패키지를 설치하고, composer install이 polyspec/template을 설치하며, make release-consumer-lock이 같은 lock을 다시 쓴다 | 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 | compiler를 npm 패키지 @polyspec/template-compiler로 발행: compiler module과 함수 계약은 어떤 릴리스 asset에도 들지 않은 저장소의 파일이어서 consumer가 의존성으로 선언할 수 없었다. packages/template-compiler는 compiler module, backend, interface.json, 하나뿐인 함수 계약 functions.json을 담는다. 그 package.json은 compiler.mjs, ast-artifact.mjs, type-manifest.mjs, functions.json을 export하고 @polyspec/template에 정확한 버전으로 의존하며, ast-artifact.mjs는 parser를 @polyspec/template에서 import한다. script, runner, showcase 도구와 모든 언어의 interface test는 패키지 directory를 읽는다. compiler digest는 패키지의 파일을 패키지 안의 경로로 적는다. 저장소에서는 build 없이 packages/template-ts의 소스를 읽고, 설치된 곳에서는 설치된 @polyspec/template의 manifest와 dist를 읽는다. tools/showcase/delivery-digest.mjs는 showcase의 JavaScript delivery digest를 계산한다. scripts/release.mjs는 패키지를 pack하고(PACKAGES, MANIFESTS), tests/fixtures/release-consumer의 npm consumer project는 그 tarball을 npm ci로 설치하고 진입점 세 개와 functions.json을 패키지 이름으로 import하며 템플릿 하나를 AST graph로 컴파일한다. Red: 어떤 릴리스 asset도 compiler를 담지 않았고, npm consumer project는 tarball 네 개를 적었다. Green: make release-assets가 polyspec-template-compiler-X.Y.Z.tgz를 pack하고, pack된 manifest가 발행 manifest 검사를 통과하며, consumer project가 tarball에서 설치해 컴파일한다 | 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 | 옮긴 compiler 경로가 문서 사이트와 Rust 형식 검사에서도 유효하게 한다: spec과 operations 문서가 interface.json과 functions.json을 문서 사이트가 제공하지 않는 상대 경로 ../../packages/template-compiler/...로 링크했으므로 scripts/check-docs-static.mjs가 link points to a source-only or legacy route로 실패했고, packages/template-rust/tests/compiler_interface.rs의 manifest 경로 두 줄이 cargo fmt 폭을 넘어 make lint-rust가 실패했다. 문서는 다른 소스 파일처럼 https://github.com/polyspec/template/blob/main/packages/template-compiler/의 파일로 링크하고, cargo fmt가 테스트를 정렬한다. Red: CI run 37623320990에서 lint-rust와 정적 문서 검사가 실패했다. Green: make lint-rust, make docs-check, make docs-static-check가 통과한다 | make lint-rust, make docs-check, make docs-static-check | [o] |
| T22.3-5 | 0.0.4 릴리스: scripts/release.mjs의 MANIFESTS의 모든 manifest(root package.json, npm 패키지 template-ts, template-language, template-lsp, template-codemirror, template-compiler, VS Code extension, Composer 패키지 template-php, template-php-ext, Rust crate)가 0.0.4를 선언하고, 저장소의 npm 패키지 사이의 의존성과 C extension의 버전이 0.0.4를 적으며, package-lock.json, 각 Composer 패키지의 composer.lock, packages/template-rust/Cargo.lock, tools/showcase/adapters/rust/Cargo.lock이 0.0.4 또는 manifest의 content hash를 기록하고, tests/fixtures/release-consumer의 consumer project가 make release-consumer-lock이 쓰는 lock으로 0.0.4의 archive를 설치하며, CHANGELOG.md와 CHANGELOG.ko.md의 ## Unreleased는 새 빈 ## Unreleased 아래의 ## 0.0.4가 된다. make showcase가 0.0.4의 compiler digest로 AST와 JavaScript manifest를 compile하고, make dependency-review RECORD=1이 바뀐 lock을 기록한다. Red: make release-versions는 v0.0.4에서 manifest 열 개의 version 0.0.3과 section ## 0.0.4가 없음으로 실패했다. Green: make release-versions가 v0.0.4와 packages/template-go/v0.0.4에서 통과하고, make artifact-digest-check와 make dependency-policy-state-check가 통과한다 | 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 | @codemirror/language를 최신 stable release로 올린다: 6.13.0은 @codemirror/streamparser를 선언하지 않고 import하여 npm이 그것을 설치하지 않고 CodeMirror adapter가 load되지 않으므로, config/dependency-policy.json은 exception으로 @codemirror/language를 6.12.4로 유지했다. 6.13.1은 @codemirror/streamparser를 dependency로 선언한다. exception을 지우고, make dependency-review UPDATE=1 RECORD=1이 package.json과 package-lock.json에서 @codemirror/language를 6.13.1로 올려 @codemirror/streamparser 6.0.0을 설치하고 review를 기록한다. tests/scripts/dependency-policy.test.mjs의 review case는 exception이 유지하는 dependency에 더 새로운 @codemirror/language를 더 이상 넣지 않는다. make showcase가 lock에 맞춰 commit된 artifact를 다시 compile한다. Red: exception이 없으면 make dependency-review가 @codemirror/language 6.12.4 < 6.13.1: a newer stable release exists를 보고했다. Green: review가 exception 없는 더 새로운 stable release를 찾지 않고, make test-codemirror가 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 | Python 구현을 추가합니다. packages/template-python이 engine, 내장 함수, 명령줄 인터페이스를 패키지 polyspec-template(import 이름 polyspec.template)로 담고, conformance·parity runner와 CI가 다른 언어와 같이 다루며, compiler가 generated program을 Python으로 내보냅니다 | 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 | 패키지를 둡니다. pyproject.toml이 requires-python >= 3.11과 핀으로 고정한 setuptools와 함께 polyspec-template을 이름 붙이고, namespace package polyspec.template가 py.typed를 포함하며, .python-version이 config/toolchain.json의 Python minor release를 적고, 패키지가 README(.ko), AGENTS.md(.ko), CHANGELOG(.ko), documentation manifest를 소유합니다 | packages/template-python/**, .python-version, config/toolchain.json | make docs-check |
| T22.4-2 | engine을 구현합니다. TypeScript binding의 lexer, parser, renderer를 parse, analyze, analyze_prefix, AstProgram, Engine, bind, merge, parse_json, 공유 계약의 내장 함수, 공유 code와 byte span을 가진 TemplateError로 옮기고, 단위 test가 API와 string·number·date 규칙을 지킵니다 | packages/template-python/** | make test-python |
| T22.4-3 | CNF-4의 명령줄 인터페이스를 구현합니다. python -m polyspec.template.cli parse FILE [--root DIR] [--delimiters OC]와 render FILE [--data F] [--define F] [--env F] [--root DIR] [--delimiters OC]가 AST를 JSON으로, 렌더링 결과를 출력하고, template 오류가 필드를 JSON으로 표준 오류에 쓰며 상태 2로 끝납니다 | packages/template-python/** | make conformance-python |
| T22.4-4 | 언어를 등록합니다. drivers.mjs가 Python CLI를 다루고, conformance-generated-python.mjs가 모든 사례의 generated program을 Python process로 실행하며, compiler backend backends/python.mjs가 source graph의 module을 내보내고, contracts/features.json과 compiler interface가 언어 사이에 Python을 밝히고, function contract가 Python arity 표를 읽고, delimiter matrix가 이를 실행합니다 | tests/runner/**, packages/template-compiler/**, contracts/features.json, scripts/check-function-contract.mjs | make conformance-generated-python, make function-contract-check |
| T22.4-5 | CI에서 실행하고 릴리스합니다. ci.yml의 python job이 단위 test, conformance, generated conformance를 3.11과 핀된 minor에서 실행하고, CHECK_TARGETS가 세 target을 가지며, owner map이 새 경로를 소유하고, release version 검사가 다른 manifest와 함께 pyproject.toml을 읽습니다 | .github/workflows/ci.yml, Makefile, scripts/owner-checks.json, scripts/release.mjs, tests/scripts/release.test.mjs | node --test tests/scripts/ |
| T22.4-6 | 저장소 README와 사용법 가이드에 Python 패키지를 문서화합니다. README.md와 docs/guide.md가 구현 목록에 Python을 넣고, 가이드는 polyspec.template의 이름으로 검사한 렌더 예제를 담으며, 패키지 README는 packages/template-python을 담은 첫 tag(v<X.Y.Z>)의 설치 줄과 그 tag가 생기기 전의 체크아웃 설치를 적습니다 | 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 | ci.yml의 python job을 단위 테스트로 검사합니다. 이 job은 Python 3.11과 .python-version의 minor를 설치하고 test-python, conformance-python, conformance-generated-python을 실행하며 ci-passed가 이 job을 need로 가지므로, Python 실행이 실패하면 release가 요구하는 check가 실패합니다. main push의 CI 실행이 완료 행 T22.4와 T22.4-1부터 T22.4-6의 실행 증거입니다 | .github/workflows/ci.yml, tests/scripts/toolchain-files.test.mjs | node --test tests/scripts/toolchain-files.test.mjs |
| T22.4-8 | python job의 actions/setup-python을 그 version tag가 가리키는 commit으로 고정합니다. ci.yml은 v6.3.0에 ece7cb06caefa5fff74198d8646806c4678c61a1을 고정하고 있었는데 GitHub에 없는 commit입니다. 고정 값은 ece7cb06caefa5fff74198d8649806c4678c61a1이며, tests/scripts/action-pins.test.mjs가 모든 workflow의 모든 action 고정 값을 version의 commit과 대조합니다 | .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 | 기능별 Python 지원은 그 검증이 Python을 실행하는 곳에만 적습니다. contracts/features.json은 검증 명령이 Python을 실행하지 않는 기능에 unsupported를, Python을 실행하는 명령을 가진 기능에 T22.4-7의 CI merge group 실행이 통과할 때까지 partial을 적습니다. tests/scripts/feature-python-coverage.test.mjs가 Makefile과 각 명령의 script에서 범위를 구합니다 | 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 | polyspec.template의 공개 이름을 모두 __all__에 넣습니다. ParseOptions, analyze, analyze_prefix는 공개인데 빠져 있고, PreparedRender는 두 번 들어 있으며, 문서가 polyspec.template에서 가져오는 이름은 모두 export되어야 합니다. packages/template-python/tests/test_exports.py가 이를 검사합니다 | packages/template-python/src/polyspec/template/__init__.py, packages/template-python/tests/test_exports.py, Makefile | make test-python |
| T22.4-11 | 패키지의 단위 test 파일을 패키지 서식으로 맞춥니다. packages/template-python/tests/test_api.py는 거의 모든 줄 뒤에 빈 줄이 있으므로, 어떤 문장도 바꾸지 않고 서식만 고칩니다. 파일의 abstract syntax tree는 그대로이고 make test-python이 통과합니다 | packages/template-python/tests/test_api.py | make test-python |
| T22.4-12 | ci.yml의 python job을 .python-version의 핀된 minor에 맞춥니다. job matrix는 3.11과 .python-version의 minor를 담고, 다른 job이 각자의 버전 파일에서 읽는 것과 같이 맞추며, matrix가 그 minor를 빠뜨리면 tests/scripts/toolchain-files.test.mjs가 실패합니다 | .github/workflows/ci.yml, tests/scripts/toolchain-files.test.mjs | node --test tests/scripts/toolchain-files.test.mjs |
| T22.4-13 | 언어 test matrix를 Python 지원과 맞춥니다. contracts/features.json의 verification_matrix.languages가 Python을 담는데 의미 기능은 Python을 partial로 선언하므로 node scripts/check-language-test-matrix.mjs가 실패합니다. T22.4-7의 CI merge group 실행이 통과할 때까지 matrix는 검사기가 요구하는 네 언어를 담고, 그 뒤 Python이 들어갑니다 | contracts/features.json, scripts/check-language-test-matrix.mjs | node scripts/check-language-test-matrix.mjs |
| T22.4-14 | polyspec.template의 py.typed 표식을 패키지 이름 아래에 선언합니다. pyproject.toml의 package-data는 template이라고 적었고 src에 그런 패키지가 없으며, key는 polyspec.template이어야 합니다. tests/test_package_data.py가 모든 key가 패키지를 가리키는지와 polyspec.template이 py.typed를 선언하는지 검사하고, make test-python이 이를 실행합니다 | packages/template-python/pyproject.toml, packages/template-python/tests/test_package_data.py, Makefile | make test-python |
| T22.4-15 | Python 배포 형식을 문서화합니다. 패키지 polyspec-template는 registry가 없으므로 Rust crate처럼 git tag로 소비합니다. publication.md는 로컬 설치 줄과 tag 설치 줄을 주고, AGENTS.md는 버전 올림 manifest 목록에 packages/template-python/pyproject.toml을 둡니다 | docs/operations/publication.md, docs/operations/publication.ko.md, AGENTS.md, AGENTS.ko.md | node scripts/check-documents.mjs |
| T22.4-16 | test-python target을 명령 하나로 유지합니다. CI의 scripts job이 target에 명령이 세 개 있어서 tests/scripts/failure-accumulation.test.mjs에 실패합니다. 전체 suite의 모든 target은 명령을 최대 하나만 실행하며, 이 target은 세 unit test를 실패 상태를 각각 남기는 loop 하나로 실행합니다 | Makefile | node --test tests/scripts/failure-accumulation.test.mjs |
| T22.4-17 | T22.4-9와 T22.4-13의 계약 변경 뒤 commit된 컴파일 artifact를 다시 생성합니다. 컴파일된 AST manifest와 generated manifest의 계약 digest가 계약과 맞지 않아 CI job generated와 showcase가 generated-artifact-check, typed-generator-check, contract-check, showcase-site-check에서 실패합니다. 재생성은 make showcase-compile, make typed-generator, node tools/showcase/build-site.mjs로 씁니다 | examples/site, tools/showcase/adapters, examples/site/index.html | node scripts/check-generated-artifact.mjs |
| T22.4-18 | contracts/features.json의 verification_matrix.languages와 scripts/check-language-test-matrix.mjs에 T22.4-7의 CI merge group 실행이 통과한 뒤 python을 나머지 네 언어와 함께 적습니다. 이 matrix가 요구하는 의미 기능의 Python 지원도 partial에서 바꿉니다 | contracts/features.json, scripts/check-language-test-matrix.mjs | node scripts/check-language-test-matrix.mjs |
| T22.4-19 | 기능 페이지에 Python을 표시합니다. scripts/features/build.mjs의 client 목록에 go, php, rust, typescript와 함께 python을 넣어 contracts/features.json의 Python 상태가 docs/features.md와 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 | Python 실행 여부를 검증 명령마다 contracts/features.json에 선언합니다("python": true). Makefile과 스크립트의 단어에서 python을 찾는 방식을 쓰지 않습니다. 기능 페이지가 python을 client로 나열하므로 그 단어가 docs-check의 Python 검사에 걸렸고, CI job scripts가 feature-python-coverage에서 실패했습니다. Red: 이 행 이전 head의 CI job scripts. Green: 선언된 명령은 make conformance-all-modes, make delimiter-matrix, make release-test-matrix입니다. template-function-contract는 Python 소스를 읽기만 하고 Python을 실행하지 않으므로 python을 unsupported로 선언합니다. | 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 | install과 generated 검사에 Python을 넣습니다. check-package-installs.mjs(install-check), check-generated-native-calls.mjs, check-typed-generator.mjs, check-generated-arguments.mjs, check-generated-bound-data.mjs, check-showcase-contract.mjs는 Go, Rust, PHP 항목에서 유도한 Python 항목을 가지며, 같은 네 언어를 실행하는 check-generated-typed-values.mjs도 가집니다. Python에서 실패하는 검사는 구현에서 고치며 unsupported로 기록하지 않습니다. Python은 .python-version의 python<minor>로 실행하며(scripts/python-toolchain.mjs), tests/scripts/python-check-coverage.test.mjs는 Python 항목이 없는 검사에서 실패합니다 | 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 | Python을 scripts/check-generated-typed-values.mjs에 넣습니다. 이 검사는 tests/fixtures/typed-values를 Python으로 compile하고 host function describe로 render합니다. Red: Python 항목이 in requires a list, map or string on the right로 실패합니다. Python backend가 _generated_bind_assign을 if True:로 내보내기 때문이며(emitRuntime이 dynamic = true로 둡니다), typed root에서 Assign의 모든 필드가 설정되지 않습니다. Green: emitRuntime이 context.program.dynamicRoot를 씁니다. Python interpreter는 .python-version의 python<minor>이며, CI job scripts, generated, showcase가 이를 설정하고 python-check-coverage.test.mjs가 이를 검사합니다 | 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 | Python을 scripts/check-generated-arguments.mjs에 넣습니다. assign이나 definition data가 선언한 type과 맞지 않는 request는 Python의 argument error인 ValueError로 실패하며 TemplateError가 아닙니다(ERR-13). Red: Python 항목이 template E_INTERNAL을 보고합니다. GeneratedProgram.prepare가 binding을 internal_boundary 안에서 실행하고, 이 경계는 모든 ValueError를 E_INTERNAL로 바꾸기 때문입니다. Green: prepare는 그 경계 밖에서 binding하고, 경계는 render만 유지합니다. docs/spec/errors.md가 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 | Python을 scripts/check-generated-bound-data.mjs에 넣습니다. generated Python program은 bound map(bind, merge)을 assign과 definition data로 host map과 같은 bytes로 render합니다(VAL-22, RT-61). Red: Python 항목이 {= m.k}에서 'dict' object has no attribute 'k'로 실패합니다. backend가 typed record의 member를 attribute read로 내보냈지만, generated program은 typed record를 존재하는 필드의 map으로 가집니다. Green: typed record의 member는 _generated_member로 읽으며, TypeScript의 ?.와 PHP의 ?->처럼 null record와 없는 optional 필드에 null을 반환합니다 | 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 | Python을 scripts/check-generated-native-calls.mjs에 넣습니다. generated Python program은 assign한 host object(NativeObject)의 member와 class function을 호출하고, object를 include된 template, block, definition data에 그대로 전달하며, 다른 언어와 같게 error matrix와 host argument value를 보고합니다(VAL-18, VAL-21, EXP-39) | scripts/check-generated-native-calls.mjs, tests/scripts/python-check-coverage.test.mjs | node scripts/check-generated-native-calls.mjs |
| T22.4-20-8 | generated Python program을 Engine이 render하는 Program으로 만듭니다. Engine(GeneratedProgram()).render(target, assign)가 'RenderOptions' object has no attribute 'get'로 실패했습니다. GeneratedProgram이 options를 dict로 읽는데 Engine과 AstProgram은 package의 RenderOptions를 받기 때문입니다. Red: generated 검사 네 개의 Python 항목이 Engine과 RenderOptions로 render합니다. Green: GeneratedProgram이 Program을, _PreparedRender가 PreparedRender를 상속하고 둘 다 options.define과 options.env를 읽으며, generated Python conformance runner는 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 | Python을 scripts/check-package-installs.mjs에 넣습니다. checkout 밖의 install project가 packages/template-python 사본의 wheel을 pip wheel --no-index로 build하고, 그 wheel만 새 virtual environment에 설치하며, scenario scope-precedence를 AST mode(FsLoader)와 commit된 generated Python program으로 render해 expected.html의 bytes와 비교합니다. pyproject.toml의 build backend는 var/python/wheelhouse에서 오며, make install이 scripts/python-wheelhouse.mjs로 packages/template-python/build-requirements.json(정확한 version, file, SHA-256 digest)에서 채우므로 검사는 index를 읽지 않습니다. Red: check-package-installs.mjs에 Python 항목이 없고 python-check-coverage.test.mjs가 이를 보고합니다. Python을 실행하는 contracts/features.json의 명령(make generated-native-check, make typed-generator-compile-check, make showcase-check)은 "python": true를 선언하고, 이 명령이 검증하는 feature는 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 | Python 검사와 compiler interface 매핑이 한 tree에 있으므로 기능 페이지와 showcase 산출물을 다시 생성해, commit된 산출물의 계약 digest가 tree의 compiler를 가리키게 합니다 | docs/features.md, docs/features.ko.md, examples/site/, tools/showcase/adapters/generated/ | node scripts/check-artifact-digests.mjs |
| T22.4-20-6 | showcase adapter contract의 Python 구현을 추가하고 scripts/check-showcase-contract.mjs에 넣습니다. interface.json이 python을 매핑하고(python.py, generated/render_adapter.py, snake case operation 이름), generate-showcase-contract.mjs가 Python 선언(dataclass를 쓰는 RenderAdapter)을 쓰며, tools/showcase/adapters/python.py가 AstProgram 또는 generated program을 쓰는 Engine 위에 Adapter를 구현하고, compile-generated.mjs가 다섯 scenario를 generated/typed/<id>.py로 compile하며, checker가 Python source를 parse하고 모든 scenario에서 adapter를 AST mode와 generated mode로 실행합니다. Red: node scripts/check-showcase-contract.mjs가 python does not declare support levels로 실패합니다. interface 변경은 compile된 artifact의 contract digest를 바꾸므로 artifact를 다시 생성합니다 | 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 | Python을 scripts/check-typed-generator.mjs에 넣습니다. 다섯 scenario의 commit된 Python program(generated/typed/<id>.py)이 parse되고, Engine으로 scenario를 expected.html과 같은 bytes로 render하며, 공통 runtime semantics(RuntimeBindings, _runtime.binary, _runtime.call)를 쓰고, output limit을 넘으면 위치가 있는 E_RUNTIME_LIMIT template error를 유지합니다. Red: packages/template-python/tests/test_api.py의 test_output_limit_is_a_positioned_template_error가 실패합니다. Output.write가 error를 반환하는 limit callback을 호출만 하고 raise하지 않아, outputBytes를 넘는 render도 전체 출력을 반환했습니다. Green: Output.write가 반환된 error를 raise합니다 | 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 | Python lint와 format 검사를 추가합니다. make lint-python(scripts/kit/lint-python.mjs, config/toolchain.json의 ruff.pyproject)은 packages/template-python에 ruff check와 ruff format --check를 실행합니다. ruff는 그 pyproject.toml의 dev extra에 있는 ruff==0.16.10 pin이며 make install-tools가 var/tools/python에 설치하고, 규칙 E4, E7, E9, F와 target py311은 그 [tool.ruff] table에서 옵니다. lint-python은 CHECK_TARGETS에 있고 CI job showcase에서 실행되며, make test-python은 PATH의 python3 또는 PYTHON으로 실행합니다. Red: make lint-python이 쓰지 않는 import 21개와 ruff format이 바꾸는 파일 36개를 보고했습니다. Green: import를 지우고 문장은 바꾸지 않은 채 패키지를 format하며, make lint-python과 make test-python이 통과하고, 검토 기록이 setuptools==84.0.0과 ruff==0.16.10을 담습니다 | Makefile, packages/template-python, config/toolchain.json, config/dependency-review.json | make lint-python |
| T22.4-22 | tag에서 Python 설치를 test합니다. config/release.json은 packages/template-python/pyproject.toml을 git tag로 릴리스하고(manifests) 그 설치(proof.gitTag: pip install "polyspec-template @ git+https://github.com/polyspec/template@vX.Y.Z#subdirectory=packages/template-python"를 새 virtual environment에 실행한 뒤 python -c "import polyspec.template")를 선언하며, 이것은 릴리스가 만들어진 뒤 make release-proof TAG=vX.Y.Z가 실행합니다. tests/scripts/release-config.test.mjs는 config/release.json의 패키지, crate, Python 패키지, Go module이 manifest가 선언한 이름인지 검사합니다. tests/fixtures/release-consumer의 consumer project와 lock은 archive 이름 <package>-<language>-<version>.<ext>로 make release-consumer-lock TAG=v0.0.4가 쓰고, make release-consumer TAG=v0.0.4가 archive 여섯 개를 설치해 각 smoke 명령을 통과합니다 | 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 | packages/template-compiler/interface.json의 core support level에서 python을 뺍니다. 이 level은 languages에 mapping이 없는 python을 적어서 check-compiler-interface.mjs가 compiler mapping is incomplete로 실패하고, CI job showcase가 실패합니다. 같은 실패가 bound map의 mutation 검사에서는 메시지 누락으로 나타납니다. T22.4-24가 python을 더하기 전까지 core level은 mapping이 선언된 네 언어만 적습니다 | packages/template-compiler/interface.json | node scripts/check-compiler-interface.mjs |
| T22.4-24 | packages/template-compiler/interface.json에 Python language mapping을 선언합니다(backend, program, error, runtimeServices, runtimeServiceOperationNames, runtime environment의 field와 operation, frame과 scope의 field와 operation, Python runtime의 bound map). 그리고 python을 supportLevels.core에 다시 넣어 컴파일러 계약이 Python backend를 다른 네 core 언어와 같게 검사하게 합니다. packages/template-python/tests/test_compiler_interface.py가 Python 선언을 mapping과 비교하고, RuntimeServices는 RuntimeEnvironment가 구현하는 package의 class가 되며, scripts/check-compiler-interface.mjs와 그 mutation 검사는 이 test를 실행하고 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 | T22.4-24의 interface 변경 뒤 commit된 컴파일 산출물을 다시 생성합니다. 컴파일된 AST manifest, generated manifest와 예제 사이트의 계약 digest가 계약과 맞지 않아 CI job generated와 showcase가 실패합니다. 재생성은 make showcase-compile, make typed-generator, node tools/showcase/build-site.mjs로 씁니다 | examples/site, tools/showcase/adapters, examples/site/index.html | node scripts/check-generated-artifact.mjs |
| T22.4-25 | T22.4-23의 interface 변경 뒤 commit된 컴파일 산출물을 다시 생성합니다. 컴파일된 AST manifest, generated manifest와 예제 사이트의 계약 digest가 계약과 맞지 않아 CI job generated와 showcase가 실패합니다. 재생성은 make showcase-compile, make typed-generator, node tools/showcase/build-site.mjs로 씁니다 | examples/site, tools/showcase/adapters, examples/site/index.html | node scripts/check-generated-artifact.mjs |
| T22.4-27 | polyspec/kit v0.0.4의 공유 도구를 사용합니다. scripts/kit/, tests/kit/, kit.json, .kit/kit.lock.json이 vendored 복사본을 담고(make kit-sync KIT_TAG=v0.0.4, 두 번째 실행은 unchanged를 출력), kit이 제공하는 도구의 저장소 복사본을 그 test와 함께 지웁니다. 의존성 review와 gate, push gate, full-run guard, owner check, release와 그 consumer 설치, install-tools, ci-targets, ci-passed, target-report, run-tests, check-documents, holder-lock, git-hooks, github-ruleset, check-cargo-downloads입니다. config/가 toolchain, checklist, documents, commits, owner-checks, release, dependency-policy를 kit의 형식으로 담고, Makefile이 scripts/kit/kit.mk를 include하며, 모든 문서 쌍이 같은 doc-id를 가지고 한글 파일이 영어 파일의 source-sha256을 가지며(기능 페이지는 scripts/features/build.mjs가 둘 다 씁니다), contracts/features.json, package.json, 운영 문서가 kit 파일을 가리킵니다. 버전 0.x에는 pull request, merge queue, GitHub ruleset이 없습니다. .github/ruleset.json과 push-gate.yml을 지우고, ci.yml은 main에 대한 push와 수동 실행에서 실행되며 job docs가 push-gate-commit을 실행하고, release.yml은 check ci-passed를 요구하며, config/release.json이 npm, Composer, PHP extension, Python, Rust, Go 패키지를 asset <package>-<language>-<version>.<ext>와 함께 나열하고, AGENTS.md가 0.x 규칙과 공유 도구의 소유를 적습니다 | Makefile, scripts/kit, tests/kit, kit.json, .kit/kit.lock.json, config, .github/workflows, AGENTS.md, docs/operations | make kit-check |
| T22.4-28 | kit gate의 의존성 review를 기록합니다. config/dependency-policy.json이 kit의 형식을 가지고(pythonManifests가 packages/template-python/pyproject.toml을 가리킴), config/toolchain.json이 Cargo lock과 Go module의 권고를 위해 cargoAudit과 govulncheck를 선언하며, package.json과 package-lock.json의 @playwright/test가 1.64.0이고, make dependency-review RECORD=1이 npm, Composer, Cargo lock, setuptools==84.0.0, registry 의존성의 review를 씁니다. make dependency-policy-check와 make dependency-policy-mutation-check가 통과합니다 | config/dependency-policy.json, config/dependency-review.json, config/toolchain.json, package.json, package-lock.json | make dependency-policy-check |
| T22.4-29 | T22.4-28의 lock 변경 뒤 commit된 컴파일 산출물을 다시 생성합니다. compiler digest가 package-lock.json을 포함하므로 컴파일된 AST manifest, generated JavaScript manifest와 examples/site/index.html이 @playwright/test 1.64.0 이전 compiler의 digest를 기록해 node scripts/check-artifact-digests.mjs가 실패했습니다. 재생성은 make showcase-compile, make typed-generator, node tools/showcase/build-site.mjs로 씁니다 | examples/site, tools/showcase/adapters/generated/javascript | node scripts/check-artifact-digests.mjs |
| T22.4-30 | polyspec/kit v0.0.6을 사용합니다(make kit-sync KIT_TAG=v0.0.6, 두 번째 실행은 unchanged). 의존성 gate가 이 저장소의 패키지를 복사본인 file: lock 항목에서 읽고, make documents-stamp가 한글 파일의 source-sha256을 씁니다 | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json | make kit-check |
| T22.4-31 | polyspec/kit v0.0.7을 사용합니다(make kit-sync KIT_TAG=v0.0.7, 두 번째 실행은 unchanged). 의존성 gate가 manifest가 file:<자기 directory>로 요구하는 로컬 npm 패키지를 받아들입니다 | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json | make kit-check |
| T22.4-32 | polyspec/kit v0.0.8을 사용합니다(make kit-sync KIT_TAG=v0.0.8, 두 번째 실행은 unchanged). make install-tools TOOLS="npm go"는 이름을 준 도구만 설치합니다 | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json | make kit-check |
| T22.4-33 | CI job마다 그 job이 준비하는 도구만 설치합니다. a142548의 실행은 모든 job이 make install에서 실패했습니다. kit이 선언된 도구를 모두 설치하는데 ruff는 python3.14가 필요하고 이는 setup-python이 있는 job에만 있으며(job typescript, docs, browser, go, rust, delimiters, editor, 두 php job, 3.11의 job python은 python3.14 ENOENT로 실패), cargo-audit는 install-tools 뒤에 make install이 설치하는 Rust toolchain이 필요했습니다(job scripts, generated, showcase, 3.14의 python은 toolchain '1.98.1' is not installed로 실패). 이후의 실패(node_modules의 Cannot find module, ajv, tsup, eslint, 없는 vendor, Rust toolchain)는 그 결과입니다. make install은 install-tools TOOLS="npm go"를 실행하고, job showcase는 setup-python 뒤에 make install-tools TOOLS=ruff를 실행하며, make dependency-review는 cargo-audit 전에 Rust toolchain(rust-toolchain)을 설치하고 그 workflow는 Python을 준비하며, make test-python은 다시 job의 python3로 실행합니다(T22.4-21이 python3.14로 바꿨는데 3.11의 job python에는 없습니다) | 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 | ruff format이 쓰는 철자로 Python registry를 읽습니다. scripts/check-function-contract.mjs는 'name': BuiltIn(<min>, <max>를 작은따옴표로만 찾는데 T22.4-21의 format이 packages/template-python/src/polyspec/template/functions.py의 key를 큰따옴표로 써서, 검사기가 Python 함수를 하나도 찾지 못하고 make function-contract-registries-check가 CI job scripts에서 계약 전체를 빠진 이름으로 보이며 실패했습니다. Red: tests/scripts/function-contract-registries.test.mjs가 실패했습니다. Green: pattern이 두 따옴표와 BuiltIn( 뒤의 줄바꿈을 받아들이고, 검사기가 다섯 언어의 canonical 함수 37개를 보고합니다 | scripts/check-function-contract.mjs, tests/scripts/function-contract-registries.test.mjs | node --test tests/scripts/function-contract-registries.test.mjs |
| T22.5-1 | 0.0.5를 릴리스합니다. config/release.json의 모든 manifest(루트 package.json, npm 패키지 template-ts, template-language, template-lsp, template-codemirror, template-compiler, VS Code 확장, Composer 패키지 template-php, template-php-ext, Rust crate, Python 패키지)가 0.0.5를 선언하고, npm 패키지 사이의 의존성과 C 확장의 버전이 0.0.5를 가리키며, package-lock.json, 각 Composer 패키지의 composer.lock, 두 Cargo.lock이 0.0.5 또는 manifest의 content hash를 기록하고, CHANGELOG.md와 CHANGELOG.ko.md의 ## Unreleased가 ## 0.0.5가 되며 그 위에 비어 있는 새 ## Unreleased가 생기고, 컴파일된 AST manifest, generated JavaScript manifest, examples/site/index.html이 tree의 compiler digest를 기록하고, config/dependency-review.json이 lock의 review를 기록하며, tests/fixtures/release-consumer의 consumer project와 lock이 0.0.5의 archive를 가리킵니다(make release-assets TAG=v0.0.5, make release-consumer-lock TAG=v0.0.5) | config/release.json, 그것이 나열한 모든 manifest, 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 | polyspec/kit v0.0.21을 사용합니다(make kit-sync KIT_TAG=v0.0.21; 두 번째 실행은 unchanged를 출력합니다). vendored tool은 wheel package kind, buildBackend, consumers.wheel, pythonBuild와 pythonBackend 선언, config/toolchain.json의 ruff 선언(version 0.16.10과 플랫폼별 wheel의 sha256이므로 pin을 package manifest에서 읽지 않습니다)을 읽습니다. config/dependency-policy.json은 kit schema가 요구하는 npmLocks(package-lock.json)를 선언합니다 | scripts/kit, tests/kit, kit.json, .kit/kit.lock.json, config/toolchain.json, config/dependency-policy.json | make kit-check |
| T22.5-3 | Python 패키지를 릴리스의 wheel asset으로 게시합니다. RED: 변경 전 커밋된 트리에서 make release-assets TAG=v0.0.6은 config/release.json: proof.gitTag.packages/template-python/pyproject.toml.kind is "python", the allowed values are rust로 실패하며, wheel을 만들지 않습니다. GREEN: config/release.json은 packages/template-python을 이름 polyspec-template의 wheel package로 나열하고 manifest mode archive를 가지며, kit의 buildBackend와 smoke import polyspec.template의 consumers.wheel을 선언합니다. make release-assets TAG=v0.0.6은 polyspec_template-0.0.6-py3-none-any.whl을 var/release/assets에 쓰고, make release-consumer TAG=v0.0.6은 이를 깨끗한 virtual environment에 설치하고 import합니다. 이 package는 의존성을 선언하지 않으므로 wheel metadata에 Requires-Dist가 없습니다. requires-python은 .python-version의 minor인 >=3.14이며, CI job python은 matrix 없이 .python-version을 설정합니다(python-requires.test.mjs, toolchain-files.test.mjs). release.yml은 make release-assets 전에 make install-tools TOOLS="python pythonBackend"을 실행합니다 | 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.ko.md | make release-consumer TAG=v0.0.6 |
| T22.5-4 | 0.0.6을 릴리스합니다. config/release.json의 모든 manifest가 0.0.6을 선언하고, lock과 릴리스 asset의 consumer project가 0.0.6을 기록하며, CHANGELOG.md와 CHANGELOG.ko.md의 ## Unreleased는 새 빈 ## Unreleased 아래에서 ## 0.0.6이 됩니다. T22.5-3은 릴리스 커밋의 tag v0.0.6에서 GREEN 실행으로 완료됩니다 | config/release.json, 그것이 나열한 모든 manifest, package-lock.json, CHANGELOG.md, CHANGELOG.ko.md, docs/plans/execution-checklist.ko.md | make release-versions TAG=v0.0.6 |
| T22.5-5 | 모든 Python 검사를 var/tools의 pinned interpreter로 실행합니다. RED: var/tools/bin/python3가 없는 checkout에서 make install-tools TOOLS=ruff는 ruff needs var/tools/bin/python3, the Python that config/toolchain.json pythonBuild declares로 실패하고, CI job showcase는 ruff에서 실패합니다. GREEN: make install-tools TOOLS="python ruff"는 pinned Python을 설치한 뒤 ruff를 설치하고, make lint-python이 통과합니다. make install은 npm, Go와 python을 설치합니다. scripts/python-toolchain.mjs는 var/tools/bin/python3를 반환하며, 없으면 경로와 해결 방법을 적고 실패합니다. conformance driver, scripts/check-compiler-interface.mjs와 Makefile의 PYTHON은 그 interpreter를 실행합니다. 어떤 workflow도 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 | 드라이버 테스트의 fixture에 drivers가 import하는 모듈을 복사합니다. RED: 커밋된 트리의 make test-scripts가 tests/scripts/drivers.test.mjs, tests/scripts/failure-messages.test.mjs, tests/scripts/no-silent-skips.test.mjs에서 실패합니다: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../scripts/python-toolchain.mjs' imported from .../tests/runner/drivers.mjs. 세 테스트가 scripts/python-toolchain.mjs 없이 tests/runner/drivers.mjs를 임시 checkout에 복사하기 때문이며, 이 import는 T22.5-5가 추가했습니다. GREEN: tests/scripts/driver-fixture.mjs가 드라이버 파일을 한 곳(DRIVER_FILES)에 적고 copyDriverFiles가 복사합니다. 세 테스트는 이를 사용합니다. drivers 테스트는 tests/runner/drivers.mjs의 import 순회가 DRIVER_FILES에 없는 모듈에 닿으면 실패합니다. make test-scripts가 통과합니다 | 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 | 0.0.6 기록을 바로잡습니다. CHANGELOG.md와 CHANGELOG.ko.md는 config/toolchain.json의 ruff 0.17.0 선언을 T22.5-4 아래에 기록하고, T22.5-5 항목을 빈 ## Unreleased 아래의 ## 0.0.6으로 옮깁니다. docs/operations/development.md와 그 한국어 파일은 ruff 0.17.0을 config/toolchain.json에 선언된 값으로 적고, 설치는 var/tools/bin/python3 위의 make install-tools TOOLS="python ruff", 규칙과 target은 packages/template-python/pyproject.toml의 [tool.ruff]라고 적습니다. 그 파일에는 ruff를 pin하는 dev extra가 없습니다. 완료된 T22.4-21과 T22.5-2는 편집하지 않습니다 | 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 | EPERM을 종료 중인 프로세스 그룹의 구성원으로 처리하고 ESRCH를 기다립니다. 원인: macOS에서 프로세스 그룹은 구성원이 종료 중일 때 EPERM을 반환합니다. 97bf51f의 코드는 EPERM을 종료된 그룹으로 보아 대기를 멈췄고, 구성원은 여전히 kill(pid, 0)에 응답했습니다. 그 전 버전은 EPERM을 반환한 그룹에 signal을 보냈습니다. RED: tests/scripts/process-group-gone.test.mjs는 97bf51f의 커밋된 모듈에서 두 경우가 실패합니다. EPERM probe가 추가 probe 없이 대기를 끝냈고(the wait did not probe again after EPERM until ESRCH), 지속되는 EPERM이 오류를 반환하지 않았습니다(a group that stays exiting must not resolve as ended). GREEN: signalGroup은 EPERM에 exiting, ESRCH에 ended를 반환합니다. stopProcessGroup은 exiting 뒤에 signal을 보내지 않고, ESRCH까지 probe하며, EPERM이 10 s를 넘게 지속되면 pgid, errno, 경과 시간을 담은 오류로 거부합니다. tests/runner/bounded.mjs와 VS Code 통합 step은 signalGroup으로 signal을 보냅니다. tests/scripts/process-group-exit-window.test.mjs는 그 구간의 probe 결과를 기록하고, stopProcessGroup이 kill(pid, 0)이 실패한 뒤에만 반환함을 확인합니다. 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가 통과합니다 | 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 | tests/scripts의 시간 제한을 예상 시간의 약 100배로 맞추고, 어떤 제한도 예상 시간의 20배 미만으로 두지 않습니다. tests/scripts/build-package.test.mjs의 template-ts reader 테스트는 runner 기본값 30 s를 받습니다. 부하가 큰 기기에서 단독으로 세 번 실행하면 28.6 s, 32.3 s, 37.9 s가 걸리므로, make test-scripts에서는 작동 중인데도 제한을 넘깁니다. GREEN: 이 테스트의 제한은 그 평균의 100배인 3,300 s이며, 나머지 build 테스트는 측정 시간의 100배 제한을 받습니다. tests/scripts의 한 번 실행에서 테스트의 경과 시간보다 20배 미만인 다른 명시적 제한 또는 runner 기본값은 약 100배로 고치고, CHANGELOG가 각 값을 기록합니다. 테스트가 발동하기를 기대하는 의도된 deadline은 통과 제한이 아니므로 고치지 않습니다. 바뀐 각 제한의 주석은 무엇을 보호하는지 적습니다. 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가 통과합니다 | 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 | T22.5-5-1, T22.5-7, T22.5-8을 ## 0.0.6에 기록합니다. tag v0.0.6은 그 항목을 담은 commit에 만들므로, CHANGELOG.md와 CHANGELOG.ko.md는 그 항목을 빈 ## Unreleased 아래의 ## 0.0.6에 적습니다 | CHANGELOG.md, CHANGELOG.ko.md | node scripts/kit/release.mjs versions v0.0.6 |
| T22.5-10 | 멤버의 pid를 group leader가 기록합니다. 원인: process-group-callers와 process-group-exit-window의 leader는 300 ms 뒤에 종료하고, background 멤버는 실행을 시작한 뒤에야 자신의 pid 파일을 씁니다. 그래서 부하가 큰 기기에서는 테스트가 그룹을 멈추고 파일을 읽을 때 파일이 없어, assertion 전에 ENOENT로 실패합니다. RED: 멤버는 MEMBER_START_MS(5 s) 뒤에야 pid를 쓰며, node --test tests/scripts/process-group-callers.test.mjs의 네 case는 ENOENT ... background.pid로 실패하고, node --test tests/scripts/process-group-exit-window.test.mjs는 ENOENT ... member.pid로 실패합니다. GREEN: leader는 spawn한 멤버의 pid를 계속 실행하기 전에 파일에 씁니다. 그래서 leader가 종료하기 전에 파일이 존재하며, 멤버는 지연을 유지한 채 두 테스트가 통과합니다. tests/scripts의 다른 테스트 중 child의 pid 파일을 읽는 테스트는 없습니다. deadline 이후 case의 leader는 /bin/sh이며, fork 시점에 멤버 pid를 쓰고 sleep을 exec합니다. 그래서 leader가 Node 시작에 의존하지 않습니다. 이 case의 deadline은 10 s로, leader가 파일이 생기기 전까지 필요한 시간(부하 40에서 최대 98.5 ms)의 약 100배입니다. deadline은 deadline 전에 멤버가 존재함을 보호합니다. wait나 timer는 추가하지 않습니다 | 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을 보냅니다. 원인: 그룹 SIGKILL이 전달되는 동안 fork된 멤버는 그 signal을 받지 못하고 살아남으며, stopProcessGroup은 그 signal을 한 번만 보낸 뒤 kill(-pgid, 0)이 실패하기를 기다려, 계속 실행 중인 멤버가 기다림을 영원히 붙잡았습니다. 근거: 멈춘 기다림이 출력한 목록(waiting for the processes of group 25105 to exit, 이어서 ppid 1의 sh -c for n in 1 2 3; do (while :; do mkdir -p ...와 (bash) 자식이 signal 후 1초에도 살아 있음), 그리고 a stopped process group writes nothing into a directory that is then removed의 300 s timeout. RED: stopProcessGroup signals the group again while a member is still running이 actual: [ 'SIGKILL' ], expected: [ 'SIGKILL', 'SIGKILL' ]로 실패합니다. GREEN: probe가 'sent'를 반환하는 동안 loop는 probe마다 signal을 다시 보냅니다. 프로세스 그룹 id는 구성원이 하나라도 있는 동안 재사용되지 않으며, loop는 probe가 ESRCH를 반환하면 보내기를 멈춥니다. 남는 구간은 성공한 probe와 다음 signal 사이이며, 그 사이에 마지막 구성원이 끝나고 새 그룹이 같은 id를 가져갈 수 있습니다. 기다림 안내는 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 | branch feat/parity-T22.6(merge base가 T22.5-10인 commit 47개)의 language parity 작업을 현재 layout과 kit 0.0.52 위에 반영하고 branch를 삭제합니다. main이나 kit에 이미 있는 것은 다시 하지 않습니다. implementation 선언과 모든 test의 native binding은 T24.7, T24.8, T24.11이며, Python bound-data test와 Python showcase는 이미 있고, branch의 symbol snapshot과 extractor는 kit의 symbol extractor(config/parity.json에서 implementation의 symbols)로 대체됩니다. 남은 작업은 error-code catalog(T22.6-15), Python package의 export(T22.6-14-1), 문서의 implementation 수(T22.6-11에서 T22.6-13), API unit(T22.6-14), Python benchmark(T22.6-17), 남은 API concept(T22.6-18), 빠진 runtime 규칙(T22.6-19)입니다. branch의 option case(tests/fixtures/options/cases.json)는 반영하지 않습니다. conformance case가 define, env, delimiter option과 include를 다루고, feature unit limits, host-function-failures, delimiters, delimiter-argument-error, functions-date, object-calls, map-loader, loader-errors가 각 implementation에 다른 option의 native test를 주며, case가 빠졌다고 찾은 규칙 네 개는 T22.6-19입니다. 각 부분이 반영되거나 조건과 함께 row에 기록되면 branch를 삭제합니다 | 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 | 모든 implementation의 error code를 하나의 catalog에서 생성합니다. docs/spec/errors.md의 code는 packages/ts/src/errors.ts, packages/go/errs/errs.go, packages/rust/src/error.rs, packages/python/src/polyspec/template/errors.py에 손으로 쓰여 있고, PHP package와 PHP extension에는 목록이 없이 error를 내는 곳마다 string literal만 있습니다. 그래서 config/parity.json은 errors를 선언하지 않고 R7은 아무것도 비교하지 않습니다. 완료 조건: docs/spec/errors.md의 code table이 errors의 catalog이고, scripts/generate-error-codes.mts가 이로부터 각 implementation의 목록을 씁니다. TypeScript의 union ErrorCode, Go errs의 상수, Rust ErrorCode의 variant와 table의 조건 및 그 이름, Python의 ERROR_CODES, PHP의 class Polyspec\Template\ErrorCode, PHP extension의 header pt_error_codes.h입니다. PHP package와 PHP extension은 모든 error를 이 상수로 내고 다른 곳에 code literal을 두지 않으며, 각 implementation은 자신의 목록을 errors에 적습니다. RED: errors를 선언하면 make parity-declarations-check가 php와 php-ext에 R7을 보고합니다. GREEN: make error-codes-check가 모든 목록을 차이 없이 다시 생성하고, make parity-declarations-check가 R7 finding을 보고하지 않으며, node --test tests/scripts/error-codes.test.mts가 통과합니다 | make error-codes-check; make parity-declarations-check; node --test tests/scripts/error-codes.test.mts | |
| T22.6-14-1 | Python package의 program interface와 loader interface를 export합니다. polyspec.template은 Engine이 렌더하는 engine.py의 program interface Program(RT-1, RT-69)도, loader.py의 loader protocol과 그 결과인 Loader와 LoadResult(RT-3, RT-10)도 export하지 않지만, 다른 모든 implementation은 이를 export합니다. AstProgram의 base class AstProgramCore는 이름으로만 public입니다. test_every_public_function_and_class_is_exported는 package module이 정의하는 이름만 읽으므로 이 빈틈을 보지 못합니다. 완료 조건: Program, Loader, LoadResult가 __all__에 있고, AstProgramCore는 _AstProgramCore이며, test_every_public_class_of_the_engine_and_loader_modules_is_exported가 engine.py와 loader.py의 모든 public class를 __all__과 비교합니다. RED: 새 test가 AstProgramCore, LoadResult, Loader, Program을 보고합니다. GREEN: export test와 Python package test가 통과합니다 | make test-python; make lint-python | |
| T22.6-11 | specification에서 core level의 다섯 언어를 셉니다. packages/compiler/interface.json은 core level에 typescript, go, rust, php, python을 선언하고, interface gate, function contract, generated backend, showcase, native-call fixture가 Python을 포함하지만, 다음 문장은 넷을 세거나 Python 없이 넷을 나열합니다. docs/spec/compiler.md:8, :84(showcase artifact와 "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는 merge가 연산 안에서 인자를 검사하는 Python을 빠뜨리고, docs/spec/conformance.md:49는 네 command-line interface의 parser를 언어로 적지만 TypeScript의 parser는 module로만 적으며, 한국어 파일도 같은 문장을 갖습니다. 완료 조건: 각 문장은 다섯 언어를 적거나 참인 부분집합을 적고, 한국어 파일도 같습니다. RED: docs/spec에 대해 config/parity.json의 documents를 선언하면 make parity-declarations-check가 이 줄마다 R10을 보고합니다. GREEN: T22.6-12-1의 것을 빼고 docs/spec에 R10 finding을 보고하지 않습니다 | make parity-declarations-check; make docs-check | |
| T22.6-12 | operations 문서에서 다섯 언어를 세고 R9와 R10을 선언합니다. docs/operations/showcase.md:50과 :161은 다섯 언어를 렌더하는 showcase(SHOWCASE_LANGS, examples/site/data/results.json)의 언어를 넷으로 세고, :173은 Python backend도 내보내는 닫힌 template dispatch의 target을 넷으로 나열하며, docs/operations/testing.md:24는 다섯 언어인 mode matrix의 언어를 넷으로 세고, 한국어 파일도 같은 문장을 갖습니다(testing.ko.md:20은 AST와 generated program을 네 개씩 셉니다). docs/operations/benchmark.md는 benchmark가 아직 Python을 측정하지 않으므로 네 implementation을 적으며, 이는 T22.6-17까지 참입니다. 완료 조건: config/parity.json이 Makefile의 SHOWCASE_LANGS에 대한 languageLists(profile manifest)와 README 파일, docs, docs/spec, docs/operations에 대한 documents를 영어와 한국어의 count noun과 함께 선언합니다. benchmark 문서는 T22.6-17까지, T22.6-12-1의 파일은 그 kit item까지 glob 밖에 두고, 다른 모든 count와 list는 다섯 언어를 적습니다. RED: 선언과 함께 make parity-declarations-check가 R10 finding 31개를 보고합니다. GREEN: 아무것도 보고하지 않습니다 | make parity-declarations-check; make docs-check | |
| T22.6-12-1 | kit의 R10은 implementation을 세지 않는 문장을 셉니다. docs/spec/data-model.ko.md:229는 Go module, Rust crate, PHP package, PHP extension, Python package가 각각 한 구현이라고 말하는데(각각 한 구현), R10은 이를 다섯 이름에 대한 1의 count로 읽습니다. docs/operations/development.ko.md:22는 recipe의 toolchain(Node.js, npm, Go, Rust, PHP minor version, Composer)을 나열하는데, R10은 이를 implementation go, rust, php의 list로 읽습니다. 문장은 참이므로 그대로 둡니다. kit item: 배분하는 count(each, 각각)는 문장의 implementation count가 아니고, 도구 list 안의 toolchain 이름은 implementation 이름이 아닙니다. kit release가 그렇게 읽을 때까지 config/parity.json의 documents는 두 파일을 glob 밖에 둡니다. 완료 조건: 그 kit release에서 두 파일이 glob 안에 있고, make parity-declarations-check가 R10 finding을 보고하지 않습니다. kit을 기다립니다 | make parity-declarations-check | [ ] |
| T22.6-13 | compiler의 README에서 compiler의 다섯 언어를 적습니다. packages/compiler/README.md:12와 README.ko.md:13은 compileSource가 TypeScript, Go, Rust 또는 PHP program으로 compile한다고 하지만, compiler는 Python도 내보냅니다(packages/compiler/src/backends/python.mts). 완료 조건: 두 파일의 문장이 TypeScript, Go, Rust, PHP, Python을 적습니다. RED: T22.6-12의 documents 선언과 함께 make parity-declarations-check가 두 줄에 R10을 보고합니다. GREEN: 두 줄에 아무것도 보고하지 않습니다 | make parity-declarations-check; make docs-check | |
| T22.6-14 | kit의 symbol extractor로 runtime specification의 concept 열한 개의 API unit을 선언합니다. 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, api-loader이며, 각각 그 profile의 implementation마다 symbol을 갖고, 다른 모든 public symbol은 이유와 spec anchor와 함께 nativeOnly에 선언합니다(R1, R11). config/toolchain.json은 symbolExtractors go와 php, 그리고 rustdoc JSON의 Rust nightly를 scope packages/rust의 exception으로 선언하고, 각 implementation은 symbols를 적습니다. 막힘: make install-tools가 kit의 go extractor를 build하지 못합니다(var/tools/symbols-go.next-*에서 scripts/kit/symbols/go의 go build). template root의 go.work가 build directory를 덮으므로 current directory is contained in a module that is not one of the workspace modules listed in go.work로 실패합니다. kit item은 extractor를 workspace 밖에서 build하는 것입니다(GOWORK=off). R11은 또한 member를 포함한 모든 public symbol을 unit이나 nativeOnly에 요구합니다. Python extractor는 id 305개를, crate의 rustdoc JSON은 993개를 내므로, 선언에는 먼저 T22.6-18의 concept 결정이 필요합니다. 완료 조건: go.work 아래에서 go extractor를 build하는 kit release에서, 그리고 T22.6-18 뒤에, 선언이 성립하고 make parity-check가 R1과 R11 finding을 보고하지 않습니다. kit과 T22.6-18을 기다립니다 | make install-tools; make parity-check | [ ] |
| T22.6-17 | benchmark에서 Python을 측정합니다. examples/showcase/adapters/python.py에는 benchmark mode(SHOWCASE_BENCH_ITERATIONS)가 없고, examples/showcase/benchmark-modes.mts는 네 implementation을 측정하며, docs/operations/benchmark.md와 benchmark.ko.md는 넷을 적습니다. 그래서 두 문서는 config/parity.json의 documents 밖에 있습니다(T22.6-12). 완료 조건: Python adapter가 benchmark 실행 없이 test된 PHP adapter의 benchmark mode를 갖고, maintainer가 허락할 때 한 번 실행한 make bench가 다섯 implementation을 측정해 examples/site/data/mode-benchmark.json과 두 benchmark 문서의 표를 다섯 행으로 쓰며, benchmark 문서가 documents의 glob 안에 있고, make parity-declarations-check와 make benchmark-check가 통과합니다. maintainer가 허락하는 make bench 실행을 기다립니다 | make benchmark-check; make parity-declarations-check | [ ] |
| T22.6-18 | T22.6-14의 열한 개를 넘는 공유 API concept을 결정합니다. implementation의 각 public symbol은 profile의 모든 implementation이 export하는 concept이거나 이유를 가진 native symbol입니다(R11). 후보는 둘 이상의 implementation이 export하는 concept입니다. error code, load result, path resolution, internal boundary, text와 bytes의 JSON parsing, 값, map, data의 bind, text 검사, bind error, host function, function context, environment, limits, runtime environment, runtime bindings, 그리고 TypeScript와 Python은 export하고 다른 implementation은 내부에 두는 frame, scope, render context입니다. 각 후보에 대해 runtime specification으로부터 symbol이 없는 implementation에 추가할지, public concept이 아닌 export를 제거할지 결정합니다. 완료 조건: 각 후보의 결정이 docs/spec/runtime.md나 docs/spec/data-model.md에 있고, export가 이를 따르며, T22.6-14가 결과를 선언합니다. 각 concept의 결정을 기다립니다 | make parity-check | [ ] |
| T22.6-19 | branch의 option case가 빠졌다고 찾은 runtime 규칙 네 개를 씁니다. 규칙은 모든 implementation이 하는 동작을 적으므로 동작은 바뀌지 않습니다. (1) RT-33은 limits의 key iterations, depth, outputBytes, expressionDepth를 적습니다. TypeScript, PHP, PHP extension, Python은 이를 그대로 받고, Go와 Rust는 Limits type의 field를 언어 관례로 적습니다(OutputBytes, output_bytes). (2) RT-34는 expression depth의 단위를 적습니다. parser는 들어가는 expression, postfix expression, prefix 연산자마다 한 level을 셉니다. (3) RT-1은 engine.register 대신 program.register(name, fn)을 적습니다. 모든 implementation이 Engine이 소유한 program(RT-69)에 host function을 등록하기 때문입니다. (4) RT-6은 TypeScript, PHP, PHP extension, Python의 option이 생략한 key의 기본값을 유지하고 Go와 Rust가 Limits 값을 DefaultLimits와 Limits::default()에서 시작한다고 적습니다. 완료 조건: 규칙이 docs/spec/runtime.md와 그 한국어 파일에 있고, tests/fixtures/expr/cases.json의 expression vector depth-limit-parentheses(괄호 31겹 파싱), error-depth-parentheses(32겹 실패), depth-limit-unary(prefix 연산자 62개 파싱), error-depth-unary(63개 실패)가 여섯 implementation에서 통과합니다. 파싱하는 두 vector는 evaluation을 갖지 않습니다. PHP extension의 binding이 expression을 json(...) 안에서 평가해 두 level을 더하기 때문입니다. GREEN: 여섯 implementation의 expression binding, make rules-check, make documents-check가 통과합니다 | make rules-check; make docs-check; make parity-check | |
| T22.6-19-1 | PHP extension에서 expression vector를 다른 binding과 같은 depth로 평가합니다. extension은 자체 expression evaluator를 내보내지 않으므로 packages/php-ext/tests/ExprFixturesTest.php는 {= json(<expr>) | raw}를 렌더했고, 여기서 expression은 vector의 expression보다 두 level 깊은 call 인자입니다. 그래서 T22.6-19의 depth-limit-parentheses와 depth-limit-unary가 extension에서 E_RUNTIME_LIMIT로 실패했고 모든 implementation에서 evaluation을 잃었습니다. 완료 조건: binding은 expression을 자신의 tag에 assign하여({:value = <expr>}{= json(value) | raw}) expression이 다른 binding처럼 level 1에 있고 value를 읽는 부분만 감싸며, 두 vector가 다시 evaluation을 갖습니다. RED: evaluation을 되돌리면 현재 binding에서 두 vector가 extension에서 실패합니다. GREEN: 여섯 expression binding에서 통과합니다 | make test-ext; make parity-check |
Wave 23 — 표준으로서의 editor stack
| ID | Task | Verification | Done |
|---|---|---|---|
| T23.1 | parser가 유일한 grammar다. analyze와 analyzePrefix는 완전히 분류한 token과 각 tag body의 부분을 돌려주고, template-language는 parser 옆에 다시 적은 lexical table을 지운다: tokens.ts의 SIGILS와 OPERATOR, tags.ts의 WRAPPERS, body.ts의 LOOP_HEAD와 ASSIGN_HEAD, expression.ts의 RESERVED와 IDENT, 중복된 delimiter-directive regex. editor boundary check에 language service가 lexical table을 선언하지 않는다는 rule을 더한다. 근거: 한 grammar의 두 읽기는 어긋나며, parser가 이미 모든 class를 정한다(orm docs/plans/editor-stack.md, 판정 1). | make check: language service test가 parser의 token만으로 통과한다; template-language에 lexical table을 더하면 boundary check가 실패한다 | [o] |
| T23.2 | 모든 runtime의 token 분석과 formatter. docs/operations/editor-tools.md의 formatting style을 spec rule로 만든다; conformance case가 기대 token과 format된 text를 가진다; Go, Rust, PHP, PHP extension, Python이 analyze와 format을 구현하며(Python은 자기 analyze를 확장), 각자 AST 불변식(parse(format(x))가 parse(x)와 같음)과 멱등(format(format(x)) == format(x))을 지킨다; parity와 conformance runner가 token과 format된 text를 비교한다; docs/features.md는 template-language를 모든 runtime에서 통과로 적는다. 근거: format()은 TS에만 있으며, template 언어의 표준은 모든 runtime의 표준이다. | conformance와 parity가 여섯 구현 모두에서 token과 format된 text를 비교한다; 불변식 test가 통과한다 | [o] |
| T23.3 | VS Code extension에 grammar를 두지 않는다. tmLanguage는 HTML host(text.html.basic과 embedded CSS, JS)만 남긴다; tag injection rule을 지우고 tag는 extension이 이미 대응시킨 semantic token으로 색칠한다; configurationDefaults가 semantic highlighting을 켠다; grammar test(vscode-tmgrammar-test, attribute 값 안 tag의 captureSyntaxTokens)를 semantic token 단언으로 다시 쓴다; docs/operations/editor-tools.md(+ko)를 고친다. 근거: regex grammar는 parser 문맥이 없어 analyze와 어긋난다. | vscode integration test가 tag의 semantic token을 attribute 값 안에서도 단언한다; tmLanguage에 tag rule이 남지 않는다 | [o] |
| T23.4 | kit의 editor tool을 받아들인다. editor boundary checker와 VS Code installer를 kit에서 받고(kit 항목 T128-K, kit K71이 kit을 TS package로 만든 뒤), scripts/check-editor-boundaries.mjs와 scripts/install-vscode.mjs를 지우며, template의 rule(@polyspec/template, src/cli)을 config에 선언한다. 근거: 두 repository가 필요한 tool은 copy가 아니라 kit에 둔다. | boundary check와 vscode integration이 kit tool로 돈다; 두 script가 없다 | [o] |
| T23.5 | host 없는 tool은 선언된 root marker로 include 경로를 해석한다. template-root.json({"schema": 1})은 가장 가까운 조상으로 root를 표시하며, root를 지정하는 호출자가 없는 tool만 쓴다: language service, LSP, 그리고 option을 통한 CodeMirror host. compiler는 root 인자를, filesystem loader는 자기 root를 유지하고, map과 artifact loader는 바뀌지 않으며, conformance CLI는 --root(CNF-2)를 유지한다. 잘못된 marker(잘못된 JSON, 다른 schema, 모르는 key)는 marker URI의 diagnostic이다. marker가 없으면 모든 include 경로와 path token을 가진 모든 block 경로가 "no template root declared (template-root.json)" diagnostic을 받는다. language service는 경로가 root 아래 regular file이 아닌 include와 path token을 가진 block을 진단한다; id만 가진 block과 define이 공급하는 target은 runtime(RT-24)으로 남는다. 경로는 RT-8의 정규화를 따른다; file identity로 중복을 찾는 것과 symlink 거부는 고친 RT-10 rule이며 모든 runtime에 conformance case가 있다. language service는 주입된 host로 해석하므로 node 없이 남는다(EDT-3). EDT rule은 editor가 marker로 해석하며 host의 loader root는 검사하지 않는다고 적는다. | marker vector가 받아들이고 거부한다; language service가 없는 include와 없는 marker를 보고한다; RT-10 conformance case가 모든 runtime에서 통과한다 | [o] |
Wave 24 — package @polyspec/kit인 공유 tool
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T24.1 | 공유 tool을 npm package @polyspec/kit으로 설치한다. Makefile은 vendored scripts/kit/kit.mk 대신 scripts/kit/kit-bootstrap.mk를 include한다. kit.copies의 copy는 make kit-sync가 쓴다. vendored scripts/kit/*, tests/kit/*, .kit/kit.lock.json, kit.json과 그것을 가리키는 모든 참조를 지운다. recipe는 kit의 tool을 node $(KIT)/<tool>.$(KIT_EXT)로 실행한다. pre-push hook은 make push-gate-hook을 호출한다. CI는 make install-tools TOOLS=npm과 make install-packages로 설치한다. @polyspec/kit은 release asset과 integrity로 고정한 devDependency다. | make kit-check | [o] |
| T24.2 | npm이 link를 쓰게 한다. .npmrc의 key install-links와 bin-links, 그리고 그것이 강요한 우회를 지운다: root package.json은 JavaScript package를 override가 붙은 file: 의존성 대신 npm workspace로 선언하고, scripts/build-package.mjs는 build한 package를 다시 설치하지 않으며, recipe와 script는 tool을 node_modules/.bin에서 시작하고, tests/scripts/no-symlinks.test.mjs를 지운다. | make toolchain-check | [o] |
| T24.3 | toolchain floor를 kit의 pin으로 올린다: .node-version 26.11.1과 engines.node >=26.11.1, Composer require.php >=8.5와 config.platform.php 8.5, requires-python >=3.14, toolchain 줄 없는 go.mod go 1.27.2, Cargo rust-version 1.99.0, rust-toolchain.toml 1.99.0, typescript 7.0.2. | make toolchain-check | [o] |
| T24.4 | showcase adapter의 Go package를 go.mod의 replace 대신 go.work로 해석한다. | make layout-check | [o] |
| T24.5 | Python은 kit의 root ruff.toml로만 lint한다: packages/template-python/pyproject.toml의 [tool.ruff]를 지운다. | make layout-check | [o] |
| T24.6 | TypeScript는 eslint와 typescript-eslint 대신 kit의 oxlint, oxlint-tsgolint, knip 설정으로 lint한다(make lint-ts). eslint.config.mjs, make lint-js, eslint package를 지운다. | make lint-ts | [o] |
| T24.7 | template의 implementation과 feature unit을 config/parity.json(kit parity의 schema)에 선언하고 make parity-declarations-check와 make parity-check를 실행한다. template의 feature registry를 지운다: contracts/features.json, scripts/features/, tests/runner/parity.mjs와 그 target. | make parity-declarations-check | [o] |
| T24.8 | 모든 구현의 native test를 config/parity.json에 연결한다: 각 suite binding은 각 case를 runner의 이름으로 보고하고(Go의 subtest TestX/<case>, cargo의 test <symbol>::<case>, vitest의 top-level test <case>, PHPUnit과 unittest의 test method test_<case>), Python은 적합성 케이스의 native test를, PHP 확장은 표현식 vector의 native test를 가져 두 exclusion을 지우며, 구현의 다른 모든 test file은 feature unit으로 bind한다. RED: make parity-declarations-check가 finding 67개를 보고하며, 그중 65개는 R5 undeclared-test, 두 개는 R8 exclusion이다. | make parity-declarations-check, make parity-check | [o] |
| T24.9 | config/owner-checks.json에서 tests/scripts/driver-fixture.mjs를 소유한다: 그것으로 driver를 복사하는 test(tests/scripts/drivers.test.mjs, tests/scripts/failure-messages.test.mjs, tests/scripts/no-silent-skips.test.mjs)가 그것이 바뀔 때 실행된다. RED: make owner-validate가 tests/scripts/driver-fixture.mjs: the path matches no owner로 실패한다. | make owner-validate | [o] |
| T24.10 | 폴더를 kit layout의 S5와 S7에 맞춘다(kit 0.0.38에서 K79가 기록한 red): make layout-check가 141개 finding을 보고하며 첫 줄은 schema: error: layout-root-folders: S5: the root holds only packages/, contracts/, docs/, scripts/, tests/, examples/, benchmarks/ and config/이다. client 폴더 이름이 packages/template-<runtime>이고, 루트 tests/에 package test가 있으며, script 세 개가 PYTHONPATH를 설정하고, infrastructure script가 TypeScript가 아니다(S9). | make layout-check | [o] |
| T24.11 | 모든 test file을 parity unit에 묶는다(kit 0.0.38에서 K79가 기록한 red): make parity-declarations-check가 67개 finding을 보고하며 첫 줄은 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 | kit 0.0.42와 mise로 옮긴다(kit 0.0.38의 red): make test-vscode-integration과 tests/scripts/install-vscode.test.mjs는 @polyspec/kit/install-vscode에 대해 ERR_PACKAGE_PATH_NOT_EXPORTED로 실패하고, make dependency-policy-check는 workspace pattern packages/template-ts에서 중단되며, make dependency-review RECORD=1은 uv가 Python 3.14.8 설치 두 개를 보고해 install-tools에서 실패한다. Go, Python, uv, ruff, vulture, alint, golangci-lint, deadcode, govulncheck, cargo-audit, Composer는 mise.lock으로 잠근 mise.toml의 도구가 되고, config/toolchain.json은 php, pythonBackend, exceptions, mise만 남기며, .python-version을 지우고, Composer platform은 정책의 8.5.0이며, review 기록은 make dependency-review RECORD=1이 쓴다. | make toolchain-check, make test-vscode-integration, make dependency-review RECORD=1, make dependency-policy-check | [o] |
| T24.13 | kit 0.0.43으로 옮긴다: 게시된 파일의 integrity로 @polyspec/kit 0.0.43을 고정하고 make kit-sync로 그 복사본을 쓴다(K138, K139, K142의 lint 설정). php와 php-ext의 tokens와 formatting suite를 data set이 case인 PHPUnit method에 묶는다(K137): PHP package의 ConformanceTest::testTokens와 ConformanceTest::testFormat, PHP 확장의 AnalyzeTest::testEqualsTheAnalysisOfThePhpPackage와 FormatTest::testFormatsTheCase이며, 이를 기다리던 config/parity.json의 exclusion 네 개를 지운다. | make kit-check; make parity-declarations-check가 php나 php-ext의 tokens 또는 formatting binding을 지적하지 않는다; 묶인 네 method가 --filter로 통과한다 | [o] |
| T24.14 | kit 0.0.45로 옮긴다: 게시된 파일의 integrity로 @polyspec/kit 0.0.45를 고정하고 make kit-sync로 그 복사본(K147, K152의 knip.json과 ruff.toml, K150의 bootstrap 복사본)을 쓰며, target rust-toolchain 대신 make install-tools가 Rust toolchain을 설치하게 하고(K151), .ruff_cache를 지우며(K147), layout entry 밖에서 make가 실행하는 파일, packages/ts, packages/codemirror, packages/vscode의 workspace entry, system tool gmake를 config/knip-entries.json에 선언한다(K145, K152, K154). | make kit-check; make toolchain-check; make layout-check | [o] |
| T24.15 | 2026-10-14T17:45:27.781Z review의 최신 stable release보다 오래되었다고 make dependency-policy-check가 보고해 main의 scripts job을 dependency-audit에서 red로 둔 의존성 7개를 올린다: npm으로 @playwright/test 1.64.1, oxc-parser 0.154.0, tsup 9.0.0, vitest 5.0.4를, Composer로 packages/php의 laravel/pint v1.32.1과 packages/php, packages/php-ext의 phpunit/phpunit 13.4.2를 올리고, make dependency-review RECORD=1로 review를 기록한다. compiler digest가 package-lock.json을 포함하므로 make typed-generator와 examples/showcase/build-site.mts가 커밋된 artifact와 examples/site/index.html의 digest를 다시 쓴다. | make dependency-policy-check | [o] |
| T24.16 | Composer 패키지를 하나의 개발 root, crudui의 Composer monorepo로 해석한다. 발행하지 않는 root composer.json은 packages/php의 path repository로 polyspec/template을 요구하고, Composer가 설치하지 않는 php-ext type의 패키지인 packages/php-ext의 class와 test를 map하며, root vendor/를 root composer.lock에 고정한다. packages/php/composer.lock, packages/php-ext/composer.lock, 그리고 packages/php-ext를 위해 COMPOSER_HOME의 config.json에만 있던 path repository를 지운다(red: 그것 없이 packages/php-ext에서 실행한 composer update polyspec/template은 "found polyspec/template[0.0.6] in the lock file but not in remote repositories"로 실패한다). make test-php와 make test-ext는 root vendor/의 PHPUnit을 실행한다(kit run-tests의 COMPOSER_VENDOR_DIR). make lint-php는 root autoloader로 각 패키지에 PHPStan을 실행하고(kit K163), make dependency-review는 packages/php의 symfony/console 같은 발행 manifest의 요구를 root lock의 버전으로 review하며(kit K168), kit은 root composer.json과 composer.lock을 허용한다(kit K167). | root에서 composer install; make test-php test-ext; packages/php와 packages/php-ext에서 composer validate --strict --no-check-version; make dependency-policy-check layout-check lint-php; make dependency-review가 packages/php/composer.json의 symfony/console을 적는다 | |
| T24.17 | 네이티브 엔진의 PHP 쪽을 프로젝트가 설치하는 곳에 싣는다. Composer는 php-ext type의 패키지를 설치하지 않고 PIE는 extension만 설치하므로, packages/php-ext의 NativeRuntime, NativeAnalyzer, bin/template-ext.php는 어떤 프로젝트에도 닿지 않았고 명령은 checkout의 vendor/autoload.php를 읽었다(red: polyspec/template의 릴리스 zip을 설치한 Composer 프로젝트에 vendor/bin/template-ext.php가 없다). 그것들을 packages/php(namespace Polyspec\Template\Native)로 옮기고 명령을 그 bin에 선언하며, 명령은 require $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';로 시작한다. extension이 없으면 class들은 PIE를 적은 ExtensionNotLoaded를 던지고 다른 엔진은 실행되지 않는다. packages/php-ext는 php/와 bin/ 없는 C extension뿐이다. 릴리스 consumer는 두 명령을 vendor/bin의 proxy로 실행하고, 적합성 driver와 CLI test는 proxy를 실행하며, extension의 suite는 root autoloader로 네이티브 class를 test한다. | make test-php test-ext; polyspec/template의 zip을 담은 프로젝트에서 config/release.json의 Composer smoke 명령 | |
| T24.18 | kit 0.0.47로 옮긴다: 게시된 파일의 integrity로 @polyspec/kit 0.0.47을 고정하고 make kit-sync로 그 복사본(K155, K156, K157, K161, K170의 .oxlintrc.json, K164의 .golangci.yml, K167의 config/layout.kit.yml, K157, K158의 bootstrap 복사본)을 쓴다. wheel backend는 config/toolchain.json의 pythonBackend에만 둔다(K166). make lint-ts가 lint하는 모든 TypeScript 파일(package 설정, browser test, docs/.vitepress/tsconfig.json의 VitePress 설정, React와 그 type으로 type check하고 knip entry인 examples/site/scenarios/tsconfig.json의 React sample)을 type check하고, 손으로 쓴 TypeScript에서 새 type 안전 규칙의 각 finding을 코드에서 고친다. | make kit-check; make toolchain-check; make release-coverage; make typecheck; examples/showcase/adapters/generated/typed 밖에 finding이 없는 make lint-ts | |
| T24.19 | Compiler package를 strict TypeScript로 쓴다(S9). 타입이 없던 module *.mjs를 packages/compiler/src/*.mts와 src/backends/*.mts가 대신한다: 저장소는 type stripping으로 이를 실행하고, make build-compiler는 tsup으로 발행 package를 tsc의 declaration과 함께 dist/에 build하므로 exports는 entry point(compiler.mjs, ast-artifact.mjs, type-manifest.mjs, functions.json)를 유지한다. Compiler는 JSON 문서를 ajv로 읽는다(src/documents.mts): AST artifact manifest, generated manifest, type manifest, 함수 계약은 JSONSchemaType이 type과 대조하는 schema로 검증하고, 각 AST는 @polyspec/template의 decodeTemplate으로 읽는다. Backend는 type이 있는 Target을 만들고, IR의 block은 definition과 target을 가지는 경우를 나타내는 union이다. Compiler digest는 저장소에서 src/를, 설치된 경우 build를 포함한다. RED: module에 대한 tsc strict가 오류 879개(암묵적 any parameter 772개)를 보고한다. GREEN: make test-compiler-types가 package를 build하고 tsc가 any, as unknown as, !, @ts-expect-error 없이 깨끗하며, oxlint가 깨끗하고, 커밋된 모든 generated source가 byte 단위로 같으며, manifest와 examples/site/index.html의 compiler digest만 바뀐다 | 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 | kit 0.0.48로 옮긴다: 게시된 파일의 integrity로 @polyspec/kit 0.0.48을 고정하고 make kit-sync로 그 복사본(K169의 action을 SHA로 고정한 release.yml, K171의 bootstrap 복사본과 config/layout.kit.yml)을 쓴다. 각 checkout은 자기 Node.js를 실행한다(K171): .node-version 대신 mise.toml의 node = "26.12.0"과 mise.lock의 archive가 들어가고, 모든 package.json의 engines.node는 >=26.12.0이며, Makefile의 모든 recipe는 node를 $(NODE), go를 $(GO)로 시작하고 맨 이름으로 시작하지 않는다. Python package의 환경은 parse 시점에 $(BOOTSTRAP_NODE)로 읽는다. workflow는 bootstrap을 시작하는 Node.js를 mise.toml에서 가져오고, release.yml처럼 actions/checkout v7.0.1, actions/setup-node v7.1.0, shivammathur/setup-php 2.40.0을 SHA로 고정한다. job ci-passed는 make ci-passed 전에 bootstrap과 package를 설치한다. kit.mk가 없어서 이 job은 어떤 실행에서도 통과하지 못했다. RED: make kit-check가 node나 go를 맨 이름으로 시작하는 recipe 101개를 보고하고, tests/scripts/toolchain-files.test.mts가 .node-version에서 test 5개를 실패한다 | make kit-check; make toolchain-check; make layout-check; make lint-ts; tests/scripts/toolchain-files.test.mts | [o] |
| T24.21 | 저장소 workflow의 모든 job을 timeout-minutes로 제한하여, 진행을 멈춘 job이 CI의 concurrency group을 붙잡지 않고 실패하게 한다. CI 실행 38648904745(commit e06b6b13)의 editor job은 손으로 취소될 때까지 1시간 50분 동안 실행되었고, 어떤 job도 timeout-minutes를 선언하지 않았으며 GitHub의 한도는 360분이므로 main의 이후 실행이 모두 그 뒤에서 기다렸다. 멈춘 곳은 test 전의 make install-vscode였다: @vscode/test-electron 3.1.0의 VS Code 1.138.0 첫 download가 aborted로 실패하고 재시도가 사본을 설치했지만, 중단된 download를 읽던 tar가 끝나지 않아 node가 종료하지 않았다. kit K173이 kit의 installer에서 그 원인을 없앤다. ci.yml, pages.yml, dependency-review.yml의 모든 job(kit이 복사하는 release.yml은 제외)은 최근 실행에서 가장 긴 시간의 약 두 배를 선언한다: scripts, delimiters, php 12, generated, showcase, python 8, rust 6, dependency review 10, docs, typescript, browser, editor, go, ci-passed, pages 5. RED: tests/scripts/job-timeouts.test.mts가 timeout-minutes를 선언하지 않은 job 15개를 보고한다 | tests/scripts/job-timeouts.test.mts; make owner-validate | [o] |
| T24.22 | kit 0.0.50으로 옮긴다: 게시된 파일의 integrity로 @polyspec/kit 0.0.50을 고정하고 make kit-sync로 그 복사본(K176의 ignoreBinaries가 없는 knip.json, K185의 $(NODE)가 있는 kit-bootstrap.mk)을 쓴다. parity의 php binding은 Composer monorepo의 root vendor/에 있는 PHPUnit을 실행한다(K178). kit 0.0.48에서는 각 binding이 R3 not-executed로 보고되었다. script와 test가 실행하는 binary는 이유와 함께 config/knip-entries.json의 binaries에 들어간다: make, php, composer(K176). 추적되는 모든 composer.json을 dependency policy가 검사한다(K181): Composer는 php-ext type의 package를 root의 path repository로 load하지 않으므로("found polyspec/template-php-ext[0.0.6] but these were not loaded"), packages/php-ext/composer.json은 composerPlatforms의 자기 항목과 composer.lock을 갖는다. RED: CI의 make parity-check가 php binding을 실행되지 않음으로 보고하고, make lint-ts가 목록에 없는 binary 10개를, make dependency-policy-check가 composerCoverage로 packages/php-ext/composer.json을 보고한다 | make kit-check; make toolchain-check; make dependency-policy-check; make lint-ts; make layout-check; make parity-check(CI) | [o] |
| T24.23 | oxlint disable comment 없이 AST validator를 생성한다. scripts/generate-ast-validator.mts는 packages/ts/src/ast-validator.mjs의 header에 /* oxlint-disable no-unused-vars, unicorn/no-thenable */를 썼다. 그러나 kit K172 이후 config/knip-entries.json의 ignore에 있는 그 glob(ajv standalone 출력)이 이 file을 knip뿐 아니라 oxlint에서도 제외하므로, 이 comment는 file을 읽지 않는 rule을 끄고 있었다. header는 generator와 이를 쓰는 command만 적는다. RED: packages/ts/src/ast-validator.mjs에 oxlint-disable comment가 있다 | make ast-validator-check; make lint-ts; make artifact-digest-check | [o] |
| T24.24 | VS Code extension의 bundle이 실행 시점에 load하는 module을 esbuild의 metafile에서 읽는다. packages/vscode/tests/extension.test.mjs는 dist/extension.cjs와 dist/server.cjs의 require 호출을 따옴표, 단어 문자, 점 뒤의 match를 건너뛰는 정규식으로 찾았다. 이는 JavaScript를 자기 방식으로 읽는 것이어서 template literal 안이나 comment 뒤의 호출에 속을 수 있다. 이 bundle을 만드는 esbuild는 실행 시점에 남기는 각 import를 metafile에 external: true로 기록한다: scripts/build-package.mts는 bundle과 함께 extension.meta.json과 server.meta.json을 packages/vscode/dist에 쓰고, 이 file은 bundle과 함께 atomic하게 게시되며 .vscodeignore는 이를 package에 넣지 않는다. test는 각 output의 external import를 허용된 module과 비교한다. RED: dist/extension.meta.json이 없으면 test가 실패한다 | make test-vscode-unit; tests/scripts/build-package.test.mts; make lint-ts | [o] |
| T24.25 | 0.0.7을 release한다: config/release.json의 모든 manifest가 0.0.7을 선언하고, 저장소 package 사이의 의존성, PHP extension header, package-lock.json, Cargo lock, Composer lock도 그렇게 하며, 각 Composer lock은 그 폴더에서 composer update --lock으로 다시 잠근다(kit K180). release asset의 consumer project는 0.0.7을 기록하고(make release-consumer-lock), 그 smoke command는 archive에서 통과한다(make release-consumer). 여기에는 extension 없는 vendor/bin/template-ext.php(T24.17)도 포함된다. CHANGELOG.md와 CHANGELOG.ko.md의 ## Unreleased는 새 빈 ## Unreleased 아래의 ## 0.0.7이 된다. review는 새 lock을 기록한다. tag v0.0.7과 packages/go/v0.0.7은 release commit을 가리킨다. T24.28이 대체한다: v0.0.7의 release proof가 실패했으므로 tag v0.0.7에는 게시된 release가 없고, v0.0.8이 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 | 의존성을 고정하고 consumer를 위해 lock한 wheel을 release한다. T25.6-1 이후 Python package는 범위인 jsonschema>=4.26.0을 요구하고, consumer lock packages/python/pylock.wheel.toml에는 package가 없으므로 make release-assets가 wheel을 거부한다(kit K49: Requires-Dist는 name==version이고 wheel의 lock은 모든 전이 의존성과 함께 이를 담는다). pyproject.toml은 pylock.toml이 고정하는 version인 jsonschema==4.26.0을 요구하고, config/release.json의 consumers.wheel.lock은 make python-lock이 package의 requirement와 constraint로 쓰는 hash 포함 universal lock인 packages/python/pylock.toml을 가리킨다. pylock.wheel.toml은 삭제한다. RED: make release-assets TAG=v0.0.7이 "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 | release를 포함한 모든 workflow에서 저장소의 PHP를 시작한다. kit이 복사하는 release workflow는 checkout에 .php-version이 있을 때만 setup-php를 실행하는데, template에는 이 파일이 없었다: CI job은 각 setup-php step에 8.5를 적었으므로 v0.0.7의 release job(실행 38681972237)은 runner의 PHP 8.3.6을 유지했고, Composer는 proof의 consumer project를 거부했다("Your Composer dependencies require a PHP version ">= 8.5.0""). .php-version은 config/toolchain.json의 php의 최소 release인 8.5를 담고, ci.yml과 dependency-review.yml의 모든 setup-php step은 php-version-file: .php-version을 읽는다. 단 php job은 matrix가 config/toolchain.json의 release를 나열한다. RED: tests/scripts/php-version.test.mts가 .php-version이 없다는 것과 8.5를 적은 setup-php step 5개를 보고한다 | tests/scripts/php-version.test.mts; tests/scripts/toolchain-files.test.mts; make toolchain-check; make owner-validate | [o] |
| T24.28 | 0.0.7을 대체하는 0.0.8을 release한다: tag v0.0.7에는 게시된 release가 없다. 그 proof가 runner의 PHP를 실행했기 때문이다(T24.27). config/release.json의 모든 manifest가 0.0.8을 선언하고, 저장소 package 사이의 의존성, PHP extension header, package-lock.json, Cargo lock, Composer lock도 그렇게 하며, 각 Composer lock은 그 폴더에서 다시 잠근다(kit K180). release asset의 consumer project는 0.0.8을 기록하고(make release-consumer-lock), 그 smoke command는 archive에서 통과한다(make release-consumer). CHANGELOG.md와 CHANGELOG.ko.md는 새 빈 ## Unreleased 아래에 ## 0.0.8을 얻는다. review는 새 lock을 기록한다. tag v0.0.8과 packages/go/v0.0.8은 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 | 생성된 Rust나 TypeScript program이 쓰는 것만, 그 consumer가 compile하는 방식에서 내보낸다. template-benchmark는 definition이 없고 loop가 metadata를 읽지 않는 program을 compile하고, Rust program을 binary crate의 private module로 넣으며, TypeScript program을 lint한다. v0.0.8의 compiler로는 clippy -D warnings가 DefinitionDataInputTpl, Definition, ArtifactManifest를 생성되지 않는 것으로, render_values(오류만 반환하는 static root의 stub)를 쓰이지 않는 것으로, generated_bind_definitions의 clippy::never_loop와 clippy::match_single_binding을 보고하고, oxlint no-unused-vars가 definition loop의 raw와 각 loop의 쓰이지 않는 <loop>_size를 보고한다. Rust backend는 program이 definition을 선언할 때만 Definition<T>를, definition이 가리키는 template에만 DefinitionData<T>를 선언하고, ArtifactManifest와 static root의 render_values는 선언하지 않으며, definition이 없으면 generated_bind_definitions는 match arm이 하나인 loop 대신 요청의 첫 항목에서 실패한다. TypeScript backend는 definition이 선언될 때만 raw를 읽고, loop가 size나 마지막 item을 읽거나 empty branch가 있을 때만 <loop>_size를 선언한다. make generated-lint-check는 Rust program을 각각을 렌더하는 binary crate로 lint하고, definition이 없고 loop가 metadata를 읽지 않는 program인 tests/fixtures/generated-lint를 compile한다. RED: make generated-lint-check가 이 9개 finding을 보고한다 | make generated-lint-check; make conformance-generated-rust; make conformance-generated-ts; make artifact-digest-check; make typecheck; make lint-ts | [o] |
| T24.30 | T24.29의 생성된 program으로 0.0.9를 release한다. config/release.json의 모든 manifest가 0.0.9를 선언하고, 저장소 package 사이의 의존성, PHP extension header, package-lock.json, Cargo lock, Composer lock도 그렇게 하며, 각 Composer lock은 그 폴더에서 다시 잠근다(kit K180). release asset의 consumer project는 0.0.9를 기록하고(make release-consumer-lock), 그 smoke command는 archive에서 통과한다(make release-consumer). CHANGELOG.md와 CHANGELOG.ko.md는 새 빈 ## Unreleased 아래에 ## 0.0.9를 얻는다. review는 새 lock을 기록한다. tag v0.0.9와 packages/go/v0.0.9는 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 | showcase의 생성된 Rust program을 consumer가 compile하는 방식으로 lint하고, program이 쓰는 record, field, conversion만 내보낸다. T24.29 이후 make generated-lint-check는 tests/fixtures/generated-lint만 binary crate의 private module로 lint한다. showcase scenario 5개는 library의 public module로 남으며, 여기서 clippy는 모든 public item을 쓰인 것으로 본다. binary로는 clippy -D warnings가 다음을 보고한다: 생성되지 않는 record Slot(compiler-coverage, react-boundary), 읽히지 않는 Definition의 field template와 data, 쓰이지 않는 to_value와 읽히지 않는 Assign의 field page(empty-state). check는 모든 scenario를 그것을 렌더하는 binary의 private module로 포함하고, Rust backend는 #[allow]와 _로 시작하는 이름 없이 program이 쓰는 record, field, conversion만 선언한다. RED: scenario를 binary에 넣은 make generated-lint-check가 이 finding을 보고한다 | make generated-lint-check; make conformance-generated-rust; make artifact-digest-check; make typed-generator-compile-check | [o] |
| T24.32 | 디코딩에 필요 없는 code 없이 browser bundle에서 template AST를 디코드한다. hyper는 0.0.8의 @polyspec/template/render에서 decodeTemplate을 bundle하며, 그 import만 담은 esbuild bundle(browser, ES module, minify)은 127,057 byte, gzip 13,479 byte다: ajv가 schema의 $ref 93개를 각 사용처에 inline하므로 AST schema의 standalone validator가 110,656 byte이고, tsup이 entry가 공유하는 모든 module을 bundler가 top-level code를 지울 수 없는 하나의 chunk에 넣는다(runtime 15.5 KB). ajv는 schema를 inlineRefs: false로 compile하므로 schema의 각 definition이 함수 하나가 되고(85,678 byte), @polyspec/template/decode는 decodeTemplate과 AstDecodeError의 public entry이며, render entry의 bundle은 runtime 없이 그 chunk만 가져간다. validator는 모든 검사와 오류 message를 유지한다. RED: tests/scripts/decode-bundle-size.test.mts가 /render와 /decode의 bundle이 minify 90,000 byte, gzip 7,900 byte 예산(변경 뒤 86,538 byte, 7,490 byte)을 넘는다고 보고한다 | tests/scripts/decode-bundle-size.test.mts; make ast-validator-check; make test-ts; make lint-ts; make test-browser | [o] |
| T24.33 | 요청의 argument error를 모든 target에서 하나의 message 형식으로 보고한다. assign이나 definition data가 선언된 type과 맞지 않는 요청은 argument error(ERR-13)로 실패하는데, 그 문장이 target마다 다르다: TypeScript는 assign.title is not a string, Rust는 title: expected a string을 쓴다. spec은 요청에서 잘못된 값의 경로를 담은 message 형식 하나를 정의하고, 모든 generated backend와 runtime이 이를 쓰며, cross-target vector가 각 case의 정확한 문장을 검사한다 | make generated-arguments-check; make docs-check | [o] |
| T24.34 | T24.31, T24.32, T24.33으로 0.0.10을 release한다. config/release.json의 모든 manifest가 0.0.10을 선언하고, 저장소 package 사이의 의존성, PHP extension header, package-lock.json, Cargo lock, Composer lock도 그렇게 하며, 각 Composer lock은 그 폴더에서 다시 잠근다(kit K180). release asset의 consumer project는 0.0.10을 기록하고(make release-consumer-lock), 그 smoke command는 archive에서 통과한다(make release-consumer). CHANGELOG.md와 CHANGELOG.ko.md는 새 빈 ## Unreleased 아래에 ## 0.0.10을 얻는다. review는 새 lock을 기록한다. tag v0.0.10과 packages/go/v0.0.10은 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 | kit 0.0.52로 옮긴다: 게시된 파일의 integrity로 @polyspec/kit 0.0.52를 고정하고 make kit-sync로 그 복사본(어떤 release 단계보다 먼저 kit-check를 실행하는 K193의 release workflow)을 쓴다. knip.json 복사본은 knip이 필요한 곳에서만 ignore를 유지하므로(K188) make lint-ts는 @polyspec/kit에 대한 hint를 내지 않고, npm은 kit-bootstrap.mk의 allow-remote=root로 실행되며(K189), .mise/locks/pypi-vulture는 mise.lock과 함께 추적되고(K192), checkout은 모든 setup-php step이 읽는 .php-version에 PHP를 선언한다(T24.27, K193) | make kit-check; make toolchain-check; make layout-check; make lint-ts; make dependency-policy-check | [o] |
| T24.36 | object가 아닌 define 옵션을 요청에 담을 수 있는 모든 target에서 인자 오류 define is not an object로 보고한다. 생성된 TypeScript program은 이를 E_DATA_UNSUPPORTED_TYPE의 BindError로, PHP는 bindDefinitions의 type에서 E_INTERNAL로, Python은 AttributeError로 실패했다. Go와 Rust의 typed option은 그런 값을 담을 수 없다. E_DATA_UNSUPPORTED_TYPE은 binding이 binding 없는 값에 내는 data model의 code로 남는다(ERR-5, ERR-14). RED: tests/fixtures/typed-arguments/messages.json에서 define이 5인 요청이 TypeScript, PHP, Python에서 실패한다 | make generated-arguments-check; make docs-check | [o] |
| T24.37 | root 값이 없는 인자를 가진 template 경로 block을 생성된 PHP program에서 렌더한다. {# part.tpl name:title}은 part.tpl의 입력 name에 block scope의 title 값을 주지만, PHP backend는 입력을 root 값이나 정의 data로만 만들었고, scope 값을 대입하기 전에 generated input part.tpl.name is missing을 던졌으므로 TypeScript, Go, Rust, Python이 렌더하는 곳에서 E_RUNTIME_TYPE으로 실패했다. RED: tests/fixtures/typed-arguments-scoped/messages.json의 요청이 PHP에서 실패한다 | make generated-arguments-check; make conformance-generated-php | [o] |
| T24.38 | T24.35, T24.36, T24.37로 0.0.11을 release한다. config/release.json의 모든 manifest가 0.0.11을 선언하고, 저장소 package 사이의 의존성, PHP extension header, package-lock.json, Cargo lock, Composer lock도 그렇게 하며, 각 Composer lock은 그 폴더에서 다시 잠근다(kit K180). release asset의 consumer project는 0.0.11을 기록하고(make release-consumer-lock), 그 smoke command는 archive에서 통과한다(make release-consumer). CHANGELOG.md와 CHANGELOG.ko.md는 새 빈 ## Unreleased 아래에 ## 0.0.11을 얻는다. review는 새 lock을 기록한다. tag v0.0.11과 packages/go/v0.0.11은 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 | kit 0.0.53으로 옮깁니다. 게시된 파일의 integrity로 @polyspec/kit 0.0.53을 고정하고 make kit-sync로 그 복사본을 써서 template이 다른 저장소와 같은 kit을 씁니다. owner check는 바뀐 module의 test를 그 importer로 고르고(K191, template에 필요 없는 선택 field), kit-check는 PHP가 필요한 checkout에 .php-version을 요구하며 template에는 이미 있고(K193), release proof는 각 sibling wheel을 나열합니다(K195). review는 새 lock을 기록합니다 | make kit-check; make toolchain-check; make layout-check; make lint-ts; make dependency-policy-check | |
| T24.40 | 0.0.x의 local link를 가리킵니다. polyspec version이 0.0.x인 동안 개발 checkout은 kit docs/plans/local-links.md가 설명하는 대로 설치된 polyspec package를 그 source에 link하며(kit K198), AGENTS.md는 그 내용 없이 이를 한 문장으로 말합니다. Acceptance: AGENTS.md와 AGENTS.ko.md가 @polyspec/kit 설치 옆에 그 문장을 둡니다. GREEN: make documents-check가 통과합니다 | make docs-check |
Wave 25 — 소비자의 코드인 generated program
| ID | 작업 | 검증 | 완료 |
|---|---|---|---|
| T25.1 | Program이 쓰는 것만 각 target의 정규 형식으로 생성한다. 생성된 Go, Rust, TypeScript, PHP program은 program이 쓰지 않는 helper, type, method, import, 변수를 갖지 않으므로 소비자의 lint가 통과한다(template-benchmark K79-3, T24.6의 showcase lint 지적): Go에는 쓰지 않는 helper, OrderedMap method, strconvQuote가 없고, Rust에는 dead_code, clippy::match_single_binding, clippy::never_loop가 없으며, TypeScript에는 쓰지 않는 generatedToValue나 loop metadata 변수가 없다. Go program은 Go 관례의 header // Code generated by @polyspec/template-compiler. DO NOT EDIT.로 시작하고 gofmt가 바꾸지 않는다. PHP target은 compile option이 지정한 namespace와 directory 아래에 PascalCase class 이름으로 class마다 PSR-4 파일 하나를 쓰고, artifact manifest가 모든 파일을 나열하며, showcase와 generated 적합성 runner는 Composer의 PSR-4 autoload로 class를 읽는다. Emitter가 이 형식을 직접 만들며 compile 시점에 formatter를 쓰지 않는다. RED: 각 target의 generated fixture에서 gofmt -l, golangci-lint unused, 이를 포함한 library crate에 -D warnings를 준 clippy, oxlint no-unused-vars, pint --test --preset psr12, phpcs PSR12가 지적을 보고한다. 커밋된 artifact를 다시 compile한다 | make generated-lint-check | [o] |
| T25.2 | 생성된 PHP를 PSR-12의 120열 안에 두고, 생성된 Rust를 소비하는 방식을 명시한다. PSR-12는 120열의 soft limit을 두며 phpcs가 이를 보고한다(Generic.Files.LineLength). PHP emitter는 모든 인자 목록, 배열, 제어 구조의 조건, 연산자 연결(&&, ` | , ??, ternary)을 group으로 표시하고, 줄이 120열을 넘는 group을 PSR-12가 나누는 대로 한 줄에 항목 하나씩, 한 단계 들여 나눈다(4.5, 4.7, 5, 6절). 피연산자를 변수에 묶지 않으므로 &&, | |
| T25.3 | 생성된 Python을 ruff format의 정규 형식으로, ruff check와 vulture에 깨끗하게 생성한다. Python emitter는 compile 시점에 formatter 없이 그 형식을 직접 쓴다: 문자열이 작은따옴표보다 큰따옴표를 더 많이 가지지 않으면 큰따옴표, top-level 정의 사이 빈 줄 두 개, 88열에 들어가지 않는 group(인자나 parameter 목록, collection, 괄호로 감싼 연산자 연결)은 한 줄에 항목 하나씩 나누고, 목록에는 ruff format이 나눈 채로 두는 trailing comma를 붙인다. Program이 쓰는 helper, schema 상수, import, loop metadata만 남기고, root가 필요로 하는 _generated_bind_assign 정의를 상수 조건 없이 쓴다. RED: showcase의 생성된 Python에서 ruff format --check가 파일 5개, ruff check가 쓰지 않는 import 4개(F401), vulture가 만족할 수 없는 if 조건과 쓰지 않는 loop metadata key_, value_를 보고한다. 커밋된 artifact를 다시 compile한다 | make generated-lint-check | [o] |
| T25.4 | Go backend가 그대로 쓰는 Go를 Go 파일에 둔다. packages/compiler/backends/go/는 OrderedMap과 helper 25개를 각각 package generated의 자기 파일에 gofmt 형식으로 담고, backends/go.mjs는 program을 쓸 때 파일의 package clause와 import 뒤의 선언을 읽는다. make lint-go-format은 이 folder에 gofmt -l을, make test-go-backend-vet은 go vet을 실행하며, Go의 compiler digest는 이 파일들을 포함한다. RED: backends/go.mjs는 이 Go source 26개를 두 칸 들여쓰기의 JavaScript template literal로 tag gofmt 아래에 쓰며, 이 tag는 module이 .editorconfig를 통과하도록 앞의 공백을 tab으로 바꿀 뿐이다. 이 tag가 우회책이다. GREEN: tag gofmt가 없고, 커밋된 모든 artifact의 generated Go가 byte 단위로 같으며(manifest와 examples/site/index.html의 compiler digest만 바뀐다), gofmt -l이 이 folder의 파일을 나열하지 않는다 | make lint-go-format, make test-go-backend-vet, make artifact-digest-check, make typed-generator-check | [o] |
| T25.5 | 생성된 TypeScript가 assertion 없이 type을 나타내도록 생성한다. TypeScript backend는 as unknown as Value, as GeneratedType, as string, type을 바꾸지 않는 assertion과 Map<any, any> 반환을 썼으므로 kit의 type-aware oxlint 규칙이 examples/showcase/adapters/generated/typed/*.ts의 생성된 program에서 196건을 보고했고, cast는 render scope의 record처럼 type이 틀린 값을 가렸다. 이제 type manifest의 type을 가진 값은 그 TypeScript type을 가지며(optional 값은 T | null, any는 Value), program은 record를 담은 값을 그 record의 encoder로 Value로 바꾸고, scope나 runtime에서 다시 읽는 Value는 decoder로 검사하며, render 중에는 값의 위치에서 E_RUNTIME_TYPE으로 실패한다. Block은 input을 field마다 만들고, PHP backend처럼 값이 없는 필수 field에서 E_RUNTIME_TYPE으로 실패한다. @polyspec/template의 RenderOptions.define과 env는 unknown이며, engine과 생성된 program이 이를 검사해 plain object가 아닌 값에서 define is not an object 또는 env is not an object로 실패한다. RED: 생성된 TypeScript의 oxlint finding 196건과 tsc strict 오류 10건. GREEN: make typed-generator-compile-check가 생성된 program을 --strict, --noUncheckedIndexedAccess, --exactOptionalPropertyTypes로 type check하고, oxlint가 그 안에서 finding을 보고하지 않으며, showcase는 같은 출력을 render한다 | 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 | 모든 런타임은 JSON에서 템플릿 AST를 읽는 decode를 공개한다. Go는 ast.DecodeTemplate을 공개하지만 TypeScript 패키지에는 그런 함수가 없어, 컴파일된 AST를 JSON에서 읽는 소비자(hyper의 서버와 클라이언트)는 cast(JSON.parse(...) as Template)하거나 얕은 guard를 직접 쓸 수밖에 없다. Rust는 AST에 Deserialize를 derive하지만 decode를 공개하지 않고, struct tag를 검사하지 않고 읽으며, 모르는 필드를 받아들이고, 빠진 else, empty, id, path, then을 null로 읽는다. TypeScript는 패키지 entry와 render 전용 entry에서 decodeTemplate(json)을 공개하며, 이 함수는 compile<Template>을 통해 ajv로 contracts/schema/ast.schema.json(build가 함께 묶으며 두 번째 사본은 없다)에 대해 검증하고, path가 가장 안쪽의 잘못된 노드의 JSON Pointer인 AstDecodeError를 던진다. Rust는 decode_template(json)을 공개하며, 이 함수는 serde로 모든 노드의 모르는 필드와 빠진 필드를 거부하고 serde_path_to_error로 경로를 말한다. 내부 tag를 가진 노드는 필드를 buffer하므로 경로는 문장 노드까지다. PHP와 Python은 AST-10이 제외한다: 그 AST는 자기 타입이 없는 배열과 dict이다. docs/spec/ast.md(AST-10), docs/spec/runtime.md(RT-1), README가 함수를 적고, parity unit parsed-ast가 TypeScript와 Rust의 decode test를 bind한다. RED: 모든 적합성 case의 expected.ast.json을 decode하고 잘못된 입력을 경로와 함께 실패시키는 packages/ts/tests/decode.test.ts와 packages/rust/tests/decode.rs가 컴파일되지 않는다 | packages/ts vitest tests/decode.test.ts, cargo test --test decode, make parity-declarations-check | [o] |
| T25.6-1 | 모든 런타임에서 한 규칙으로 AST JSON을 decode한다: 런타임은 JSON에서 템플릿 AST를 contracts/schema/ast.schema.json에 대해 검증해 decode하고, 잘못된 AST는 잘못된 노드의 JSON Pointer로 실패한다. T25.6 이후 Rust는 identifier pattern, literal 값의 kind, minLength, minItems를 검사하지 않았고 경로가 문장 노드에서 멈췄다. Go의 ast.DecodeTemplate은 경로를 말하지 않고 모르는 필드를 받아들였다. PHP와 Python은 decode를 공개하지 않았고(AST-10이 제외했다), TypeScript, JavaScript, Rust, PHP, Python의 showcase adapter는 컴파일된 AST를 자기 런타임 대신 자체 ajv, JSON.parse, serde, json_decode, json.loads로 읽었다. 스키마는 노드의 정의를 oneOf 대신 if/then으로 그 type에 따라 고르므로 검증기는 잘못된 값 자체에서 오류를 보고한다. 모든 런타임은 자기 생태계의 JSON Schema 검증기(ajv, santhosh-tekuri/jsonschema/v6, crate jsonschema, opis/json-schema, PyPI의 jsonschema, 각각 최신 안정 릴리스)로 검증하고 오류들의 instance 위치 중 가장 깊은 것을, 그 깊이에서는 code point 순서로 가장 작은 것을 말한다. TypeScript는 contract 파일을 번들하고, Go(embed), Rust(include_str!), Python(package data), PHP(resources/)는 패키지의 테스트가 그 파일과 비교하는 사본을 가진다. PHP는 AstProgram::decodeTemplate을, Python은 decode_template을 공개하며, 이것은 자기 파서의 배열과 dict를 반환한다. Python의 의존성은 packages/python/pylock.toml이 고정하고(make python-lock, make dependency-audit 아래의 make python-lock-check가 검사한다), make python-package가 설치하고 make install이 내려받는다. showcase adapter는 자기 런타임으로 decode한다. parity unit parsed-ast는 모든 런타임의 decode 테스트를, unit ast-decode-errors는 공유 잘못된 AST의 테스트를 묶는다. AST-10은 제외 없이 규칙을 말하고, AST를 받지 않는 PHP 확장은 decode를 공개하지 않는다. RED: 공유 fixture tests/fixtures/ast-decode/invalid.json에서 Rust는 경로로 실패하고, 옛 스키마의 TypeScript는 모르는 노드 타입에 /body/0을 말하며, Go(*ast.DecodeError), PHP, Python의 테스트는 컴파일되거나 import되지 않는다. GREEN: 모든 런타임이 fixture의 각 잘못된 AST에 그 pointer를 말하고 모든 expected.ast.json을 decode한다 | 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 | @polyspec/template의 AST 검증기를 실행 시 ajv 없이 배포한다. T25.6 이후 decodeTemplate은 실행 시 ajv로 contracts/schema/ast.schema.json을 컴파일해 패키지가 ajv에 의존했다: packages/ts/dist의 chunk가 bare specifier ajv를 import해 브라우저 테스트의 페이지가 이를 풀지 못했고(index.html defines no renderCases function), 패킹한 archive의 오프라인 설치가 ajv의 ENOTCACHED로 실패했으며, 'unsafe-eval'이 없는 Content-Security-Policy는 컴파일의 new Function을 거부한다. 또 AST-10을 말하는 evidence group이 없어 make rules-check가 이를 uncovered로 보고했다. scripts/generate-ast-validator.mts는 스키마의 ajv standalone 검증 코드(ajv/dist/standalone)를 decodeTemplate의 옵션으로, ajv의 runtime 모듈을 esbuild로 묶어 packages/ts/src/ast-validator.mjs에 쓴다(make ast-validator). make build-ts는 파일이 낡았으면 실패하는 make ast-validator-check를 실행하고, ajv는 패키지의 개발 의존성이며, decodeTemplate은 JSON Pointer 규칙과 오류를 유지한다. tests/rule-evidence.json의 evidence group ast-decode는 AST-10을 모든 런타임의 decode 테스트와 함께 명시한다. RED: make test-browser, make install-check(CI에서 ajv의 ENOTCACHED), make rules-check(uncovered: AST-10)가 실패한다. GREEN: 세 target이 통과하고 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 | 범위를 벗어난 오프셋을 가진 날짜 문자열을 실패시킨다. +25:00이나 +05:60 같은 오프셋은 날짜 pattern `(Z | [+-]\d{2}:\d{2})에 맞고 오프셋 parser는 이를 거부하는데, TypeScript(parseOffset(...) as number), Go(offset, _ = ParseOffset), PHP((int) self::parseOffset), PHP 확장(offset = 0)은 문자열을 오프셋 0으로 읽고, Python은 None을 빼다가 E_INTERNAL로 실패한다. FUN-38은 해석할 수 없는 문자열이 E_RUNTIME_TYPE으로 실패한다고 정한다. RED: E_RUNTIME_TYPE을 기대하는 case functions/date-offset-hour-error(+25:00)와 functions/date-offset-minute-error(+05:60)가 TypeScript, Go, PHP, Python, PHP 확장에서 실패하고, 같은 것을 확인하는 각 runtime의 date 단위 테스트도 실패한다. GREEN: 모든 runtime이 이런 문자열에서 기본 오프셋 없이 E_RUNTIME_TYPE으로 실패하고, Rust는 and_then(parse_offset).unwrap_or(env_offset)대신 날짜 parser가 읽은 오프셋을 그대로 쓰며, generated program은 runtime 함수를 호출하여 이 case를 통과하고, FUN-37이 오프셋의 범위00–23, 00–59`를 명시한다(en/ko) | 각 runtime의 date 단위 테스트, node tests/runner/conformance.mts --case functions/date-offset-hour-error와 functions/date-offset-minute-error, 이 case로 실행한 generated runner, make parity-declarations-check |
| T25.8 | 생성된 program의 값을 들어오는 곳에서 한 번만 decode한다. T25.5 이후 TypeScript backend는 type이 정해진 local을 render scope에서 읽고 읽을 때마다 decoder로 값 전체를 검사했고, Go backend는 읽을 때마다 JSON 왕복으로 decode했으므로 loop 안의 읽기는 값의 크기만큼 비용이 들고 type이 보장하는 검사를 되풀이했다. type이 정해진 list의 loop는 list를 encode하고 읽을 때마다 각 item을 다시 decode했다. 이제 local은 type이 정해진 값을 담는 생성된 함수의 변수다: template input은 그 field이고, assign과 loop item은 변수를 선언하며, local을 assign할 수 있는 분기나 loop는 그 뒤에 IR이 주는 type의 변수에 local을 담고, 모든 읽기는 검사 없이 변수를 쓴다. render scope는 include와 동적 조회를 위해 각 local의 Value를 유지하므로(RT-21), include 뒤에는 include된 template이 assign할 수 있는 각 local을 scope에서 한 번 decode한다. loop는 type이 정해진 list나 map을 그대로 순회한다. 중간 결과의 오류는 index, +, 함수 이름 대신 label(page.title) is not a string처럼 template 문법으로 그 표현식을 말하고 표현식의 위치에서 난다. RED: scripts/check-generated-typed-locals.mts(make generated-typed-locals-check, make generated-native-check의 일부)가 생성된 TypeScript와 Go가 render scope에서 local을 읽는 것을 찾는다. GREEN: 둘의 생성된 source는 scope에서 local을 읽지 않고 host 호출의 결과만 decode하며, 틀린 결과는 1:12에서 그 message로 E_RUNTIME_TYPE으로 실패하고, showcase와 적합성 case는 같은 출력을 render한다 | make generated-native-check, make typed-generator-compile-check, make conformance-generated-ts, make conformance-generated-go | [o] |
| T25.9 | T25.8이 backend에 남긴 ?? 기본값을 구성으로 표현한다. TypeScript와 Go backend는 if의 각 조건을 tests[index] ?? ''로 내보냈으므로, test가 없으면 error 대신 if ()를 내보냈을 것이다; 이제 각 branch가 자기 test를 가지고, if의 주석이 요구하듯 branch 전에 내보내므로 index를 찾지 않는다. FunctionLocals.merged는 widened가 대입되는 각 local을 적는 if와 loop에서도 IR이 넓히지 않은 local에 이전 type을 주었다(widened.get(name) ?? before.type); IR이 넓히지 않는 if-block은 이제 null을 넘기고, widened에 없는 local은 throw한다. typed-locals check는 lines[at] ?? ''로 줄의 function을 찾았다; 이제 줄을 한 번 훑는다. otherwise ?? []와 empty ?? []는 남는다: else 없는 if와 empty 없는 loop는 statement가 없고, IR은 이를 null로 기록한다. 출력이 바뀌지 않으므로 RED는 없다; GREEN: 모든 생성 program과 digest가 그대로이다 | packages/compiler/**와 scripts/check-generated-typed-locals.mts의 make owner-check | [o] |