From b7783a52d404ef6b1cb5ca2629b5c078690125cb Mon Sep 17 00:00:00 2001 From: "agon (KERN)" <292465531+KERN-Agon@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:30:33 +0200 Subject: [PATCH] feat(kir): migrate diagnostic parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚔️ Forged by [Agon](https://github.com/KERNlang/agon) Co-Authored-By: agon (KERN) <292465531+KERN-Agon@users.noreply.github.com> --- .../spec.md | 220 ++++++++++++++++++ docs/kern-5-release-train.md | 38 +++ examples/capstone-assertion-engine/diag.kern | 28 ++- examples/capstone-checker-subset/main.kern | 20 +- package.json | 2 +- packages/core/src/codegen/type-system.ts | 18 +- packages/core/src/core-runtime/index.ts | 2 + packages/core/src/index.ts | 7 + packages/core/src/parameter-declarations.ts | 35 +++ packages/core/src/semantic-validator.ts | 4 + packages/core/tests/core-runtime.test.ts | 19 ++ .../tests/param-declaration-semantics.test.ts | 62 +++++ packages/core/tests/param-value.test.ts | 26 ++- packages/python/src/codegen-helpers.ts | 8 +- .../tests/param-declarations-python.test.ts | 36 +++ .../capstone-checker-subset/flatten-kern.mjs | 34 ++- .../flatten-kern.test.mjs | 50 ++++ scripts/check-kern-canonicalizer-coverage.mjs | 15 +- .../kern-canonicalizer/coverage-policy.json | 2 +- .../kern-canonicalizer/coverage-status.mjs | 3 + .../kern-canonicalizer/coverage-summary.json | 44 ++-- scripts/kern-canonicalizer/coverage.test.mjs | 42 +++- 22 files changed, 652 insertions(+), 63 deletions(-) create mode 100644 .Codex/specs/kern-5-r2-m4-3a-diag-structured-parameters/spec.md create mode 100644 packages/core/src/parameter-declarations.ts create mode 100644 packages/core/tests/param-declaration-semantics.test.ts create mode 100644 packages/python/tests/param-declarations-python.test.ts create mode 100644 scripts/capstone-checker-subset/flatten-kern.test.mjs create mode 100644 scripts/kern-canonicalizer/coverage-status.mjs diff --git a/.Codex/specs/kern-5-r2-m4-3a-diag-structured-parameters/spec.md b/.Codex/specs/kern-5-r2-m4-3a-diag-structured-parameters/spec.md new file mode 100644 index 000000000..f1c0a9d00 --- /dev/null +++ b/.Codex/specs/kern-5-r2-m4-3a-diag-structured-parameters/spec.md @@ -0,0 +1,220 @@ +# KERN 5 R2 M4.3a — Mixed-Parameter Guard and `diag.kern` Migration + +**Status:** SEALED +**Date:** 2026-07-18 +**Confidence:** 0.97 + +## Executive Summary + +[VERIFIED] M4.2 measures 98 handwritten self-host functions and finds 97 +blocked first by the legacy `fn.params` property; the checked-in receipt records +that result and a null tranche winner +(`scripts/kern-canonicalizer/coverage-summary.json`, +`docs/kern-5-release-train.md:851`). + +[VERIFIED] The next independently releasable prerequisite is the two-checkpoint +verdict from Agon tribunal +`tribunal-1784394202630-ov5bqb`: reject mixed legacy and structured parameter +declarations everywhere they can be consumed directly, then migrate only the +seven parameterized functions in the 35-line +`examples/capstone-assertion-engine/diag.kern`. Remeasure once and stop whether +the winner remains null or becomes a concrete family. + +[REJECTED] This slice does not parse legacy signatures inside the measurement +host, pre-split unrelated KERN modules, migrate a second module, implement a +newly selected tranche, or promote a public ABI/semantic owner. + +## Current State / Root Cause + +[VERIFIED] Every non-empty `params=` signature in the seven-member M4.2 corpus +uses only `boolean`, `boolean[]`, `number`, `number[]`, `string`, or `string[]`. +The Node 22 parser inventory command over `coverage-policy.json` returned 98 +functions, 97 non-empty legacy signatures, zero structured parameter children, +and no type outside that six-type set on 2026-07-18. + +[VERIFIED] `diag.kern` contains eight ordered functions. Seven have exactly two +legacy parameters each; `passResult` has none. The file is 35 lines +(`examples/capstone-assertion-engine/diag.kern:1-35`, `wc -l` on 2026-07-18). +Replacing the seven header properties with fourteen ordered child rows yields +49 lines, well below the handwritten-source ceiling. + +[VERIFIED] The language already has the desired structured representation. +TypeScript codegen reads direct `param` children before the compatibility +property (`packages/core/src/codegen/type-system.ts:851-875`), Python does the +same (`packages/python/src/codegen-helpers.ts:336-360`), and the source runner +already rejects a non-empty property combined with child parameters +(`packages/core/src/runner-runtime-scope.ts:27-32`). No new type grammar or +runtime binding model is required. + +[VERIFIED] The root defect is inconsistent mixed-mode handling. The direct +TypeScript API currently has an executable test proving that children silently +win and the legacy declaration is ignored +(`packages/core/tests/param-value.test.ts:241-255`). Python's direct parameter +builder likewise branches to children without rejecting the property +(`packages/python/src/codegen-helpers.ts:336-354`). Meanwhile nested KERN-body +functions and the source runner already reject mixed mode +(`packages/core/src/codegen/body-ts.ts:2368-2370`, +`packages/core/src/runner-runtime-scope.ts:27-32`). + +## What Already Works + +- [VERIFIED] Pure legacy signatures remain a compatibility input in both + TypeScript and Python emitters + (`packages/core/src/codegen/type-system.ts:870-872`, + `packages/python/src/codegen-helpers.ts:349-359`). +- [VERIFIED] Pure structured parameters already preserve authored order and + types through both emitters + (`packages/core/src/codegen/type-system.ts:866-869`, + `packages/python/src/codegen-helpers.ts:340-348`). +- [VERIFIED] CLI compilation and `check --with-semantics` already consume + `validateSemantics`; semantic violations are hard errors + (`packages/cli/src/shared.ts:579-588`, + `packages/cli/src/commands/check.ts:226-237`). +- [VERIFIED] Assertion-engine behavior is protected by a read-only capstone + gate that executes the checked-in KERN engine, rejects stderr/nonzero status, + and byte-compares every fixture verdict against the TypeScript reference + (`scripts/check-capstone-assertion-engine.mjs:36-63,76-95,98-144`). +- [VERIFIED] `diag.kern` source bytes are already a hash-bound corpus member + (`scripts/kern-canonicalizer/coverage-policy.json:20-24`). + +## Contract (Verified) + +> Verified against the current source tree and Node 22 build on 2026-07-18. + +| Behavior | Contract | Evidence | Tag | +|---|---|---|---| +| Legacy-only function | Continue accepting non-empty `fn.params` for compatibility outside the migrated module | `packages/core/src/codegen/type-system.ts:870-872`; `packages/python/src/codegen-helpers.ts:349-359` | VERIFIED | +| Structured-only function | Direct `param` children are ordered and authoritative | `packages/core/src/codegen/type-system.ts:866-869`; `packages/python/src/codegen-helpers.ts:340-348` | VERIFIED | +| Mixed function | Reject; never choose one representation silently | Runner precedent at `packages/core/src/runner-runtime-scope.ts:27-32`; body-function precedent at `packages/core/src/codegen/body-ts.ts:2368-2370` | VERIFIED | +| Semantic validation | Emit one stable `mixed-parameter-declarations` rule with source location | `SemanticViolation` shape at `packages/core/src/semantic-validator.ts:59-64`; traversal entry at `:459-465` | VERIFIED | +| Direct TypeScript emitter | Throw before emitting a partial signature | Current branch point at `packages/core/src/codegen/type-system.ts:863-872` | VERIFIED | +| Direct Python emitter | Throw before name mapping or signature emission | Current branch points at `packages/python/src/generators/core.ts:86-100` and `packages/python/src/codegen-helpers.ts:336-354` | VERIFIED | +| Migrated `diag.kern` order | Preserve each ordered `(name,type)` pair and leave bodies/calls/exports/returns unchanged | `examples/capstone-assertion-engine/diag.kern:1-35` | VERIFIED | +| Coverage receipt | Source digest drift rejects before measurement; regenerated receipt must remain byte-canonical and deterministic | `scripts/kern-canonicalizer/coverage-implementation.mjs:411-428`; `scripts/kern-canonicalizer/coverage-summary-writer.mjs:13-42` | VERIFIED | + +## Implementation Decision + +[VERIFIED] Follow the tribunal's bounded migration, not a normalization seam or +preemptive split (`tribunal-1784394202630-ov5bqb`, 4/4 seats, two rounds): + +1. Add RED tests for mixed declarations at semantic, TypeScript-emitter, + Python-emitter, and coverage/corpus boundaries. +2. Add the semantic rule and fail-closed direct-emitter guards while preserving + both pure modes. +3. Rewrite only the seven parameterized `diag.kern` signatures into fourteen + ordered `param name=... type=...` child rows; do not touch bodies or + `passResult`. +4. Prove the old corpus digest rejects, update only the `diag.kern` manifest + digest, regenerate the canonical coverage summary, and pin the exact new + blocker/ranking result. +5. Run focused gates, the complete KERN 5 wall, and a terminal six-engine Agon + review. Stop after publishing this slice. + +The winner is deliberately not preselected. A deterministic non-null winner is +a successful measurement result and a hard stop, not permission to implement +that family in this slice. + +## Measured Result + +[VERIFIED] The bounded migration changed the leading blocker from 97 to exactly +90 `fn.params` functions while preserving the 98-function, four-tool corpus. +The exact deterministic winner is `binary-expression`: three complete +functions in one tool, 941 corpus occurrences, and witnesses +`diag.kern#4:reasonTypeMismatch`, `diag.kern#5:reasonValueMismatch`, and +`diag.kern#7:reasonKeyMismatch`. The remaining ranking rows all complete zero +functions. This slice records that result and does not implement the family. + +[VERIFIED] The source expansion was the predicted fourteen lines: `diag.kern` +is exactly 49 lines, contains fourteen direct `param` nodes across seven +functions, and contains no `fn.params` property. The unchanged parameterless +`passResult` remains the single base-complete function. + +## Blast Radius + +| File | Action | Reason | +|---|---|---| +| `packages/core/src/semantic-validator.ts` | Modify | Common mixed-declaration semantic rule | +| `packages/core/src/codegen/type-system.ts` | Modify | Direct TypeScript emitter must fail closed | +| `packages/python/src/codegen-helpers.ts` and/or `generators/core.ts` | Modify | Direct Python consumers must fail closed before precedence | +| focused core/Python tests | Modify/add | RED oracles for mixed and pure modes | +| `examples/capstone-assertion-engine/diag.kern` | Modify | Seven exact structured-signature migrations | +| `scripts/kern-canonicalizer/coverage-policy.json` | Modify | Bind the new `diag.kern` source bytes | +| `scripts/kern-canonicalizer/coverage-summary.json` | Regenerate | Record the honest post-migration measurement | +| `scripts/kern-canonicalizer/coverage*.test.mjs` | Modify | Pin provenance, blocker delta, determinism, and stop condition | +| checker-subset flattener, test, and generated main | Modify | Preserve the existing checker facts for direct structured parameters | +| release train and this spec | Modify | Durable release evidence | + +## Acceptance Criteria + +- [x] RED first: a top-level `fn` with non-empty `params=` plus at least one + direct `param` child is currently accepted by `validateSemantics`, the + TypeScript direct emitter, and the Python direct emitter; new tests prove + all three failures before production changes. +- [x] `validateSemantics` returns exactly one + `mixed-parameter-declarations` violation for that function, with its + source line/column, and no such rule for either pure mode. +- [x] Direct TypeScript and Python parameter emitters throw on mixed mode before + emitting or mapping any parameter; their legacy-only and structured-only + tests remain green. +- [x] `diag.kern` has exactly eight functions, seven parameterized functions + with the tribunal-pinned ordered pairs, `passResult` with zero parameters, + zero `fn.params` properties, and 49 source lines. +- [x] `pnpm test:capstone-assertion-engine` remains 13/13 byte-identical with + empty stderr, unchanged fixture ids/order, and no generated artifact diff. +- [x] Before the manifest update, canonicalizer coverage fails specifically on + the changed `diag.kern` digest; only that corpus digest is then changed. +- [x] The regenerated receipt still has exactly 98 functions and four tools; + the `fn.params` blocker count changes from 97 to exactly 90; the seven + migrated witnesses do not carry `fn.params`; every other corpus witness + retains its prior legacy-parameter classification. +- [x] The full ranking and winner are asserted exactly from the deterministic + post-migration receipt. If the winner is non-null, no production + canonicalizer or ownership file changes in this slice. +- [x] `diag.kern` remains below the 500-line handwritten-source ceiling. The + observed 14-lines-for-seven-functions expansion is recorded as evidence + for deciding whether a later module must split first. +- [x] Focused typecheck/tests pass. The exact receipt-bound Node 22 + `pnpm fitness:kern-5` wall passed after review-driven guard + centralization with `KERN 5 current fitness wall passed.` +- [x] Terminal full-roster Agon review passes with no unresolved material + finding. All six usable engines completed + (`review-1784406342479-xc3w91-kern-5-r2-m4-3a-terminal-r4`) with zero + verified findings; five needs-checks were rejected against the bound + winner schema, shared pre-fallback guard, fn-only checker fact profile, + semantic location owner, and direct-runtime boundary contract. + +## Out of Scope + +- Migrating the remaining 90 legacy functions or any second corpus module. +- Splitting `checker.kern`, `canonicalizer.kern`, or `validator.kern`. +- A host-side parser/normalizer for legacy parameter text. +- Removing the legacy parameter compatibility path globally. +- Implementing a measured tranche winner. +- Public KIR/runtime ABI or semantic-ownership promotion. + +## Open Questions + +None blocks implementation. The exact post-migration winner is an output of the +existing deterministic oracle, not an input decision. + +## Deploy Order + +[VERIFIED] This commit is stacked after M4.2 because it modifies the measurement +contract introduced there. In the monorepo there is no supported skewed deploy: +the semantic guard, source migration, corpus digest, receipt, tests, and docs +ship atomically. If M4.2 lands first, rebase this slice onto `origin/main` before +pushing; if it remains unmerged, keep the same stacked feature branch. + +## Corrections Log + +| Original Claim | Reality | Impact | +|---|---|---| +| A normalization seam might avoid source churn | It would move signature semantics into the host evidence path and weaken the M4.2 trust boundary | Rejected | +| The large modules should split before any migration | `diag.kern` is 35 lines and reaches only 49 lines after its complete migration | Split deferred until measured expansion requires it | +| `checker.kern` is the smallest useful first module | `diag.kern` is far smaller and has only fourteen simple parameters across seven functions | Exact scope narrowed to `diag.kern` | +| The guard should invent a `KERN-E-*` code | This repository's common semantic contract exposes stable rule names through `SemanticViolation` | Use `mixed-parameter-declarations` plus emitter errors | +| The checker-subset generated fixture would remain byte-stable | Its adapter encoded parameter facts only from legacy `fn.params` and walked direct `param` children as ordinary statements | Add a fail-closed structured-parameter adapter and regenerate the bound fixture | +| The shared mixed-declaration guard only needed to recognize `fn` | `emitParamList`, Python codegen, semantic member analysis, and the core runtime consume parameters for other callable nodes; the `fn` gate left TS/Python behavior divergent and runtime binding ambiguous | Make the predicate representation-based for every callable consumer, add non-`fn` and runtime RED regressions, and use the shared guard in core runtime | +| The exact winner assertion could run before both check and write modes | A future measured winner change would make the receipt regeneration command fail before it could write the new canonical result | Keep the exact winner assertion in check mode; allow `--write` to regenerate, while the receipt test independently pins the checked-in winner | +| Successful write mode could always print `winner.id` | A valid null-winner measurement would write its receipt, then throw while formatting status and exit nonzero | Add a null-safe formatter with a RED null-winner test and bind it into the coverage implementation receipt | +| Package-boundary consumers could safely duplicate the mixed-declaration predicate and message | The third terminal review independently identified silent drift risk in both the Python emitter and checker adapter | Export the core guard, consume it from Python and the built-core checker seam, add a RED export-contract test, and regenerate the compiled-core-bound receipt | diff --git a/docs/kern-5-release-train.md b/docs/kern-5-release-train.md index ba62c86a6..ea7dc5e70 100644 --- a/docs/kern-5-release-train.md +++ b/docs/kern-5-release-train.md @@ -981,6 +981,44 @@ trusted-publishing/provenance configuration is inspected. engines with zero verified findings (`review-1784393191142-xojaa8-kern-5-r2-m4-2-coverage-terminal`). M4.2 is sealed with a null winner and no canonicalizer or ownership promotion. + - [x] M4.3a structured-parameter prerequisite: direct semantic, TypeScript, + and Python consumers now reject mixed legacy `params=` plus structured + `param` declarations instead of silently selecting one representation. + Seven parameterized functions in the assertion-engine `diag.kern` module + use fourteen ordered structured children; `passResult`, bodies, calls, + exports, and returns remain unchanged. The hash-bound corpus gate first + rejected the expected source drift, then remeasured the same 98 functions + across four tools with 90 remaining `fn.params` blockers. The exact new + winner is `binary-expression`: three complete assertion-engine functions, + 941 occurrences, and witnesses `reasonTypeMismatch`, + `reasonValueMismatch`, and `reasonKeyMismatch`. The result is pinned but + deliberately not implemented in this slice. Focused gates and the initial + complete Node 22 `pnpm fitness:kern-5` wall pass, including + assertion-engine 13/13, checker subset 48/48 plus 36 rejected + accept-but-abstain attacks, and self-host validator 39/39. The first + terminal review (`review-1784398592317-alchda-kern-5-r2-m4-3a-terminal`) + exposed and drove RED fixes for the incorrectly `fn`-only guard, missing + core-runtime rejection, and write-mode winner assertion. The corrected + exact tree passes the complete Node 22 `pnpm fitness:kern-5` wall with the + regenerated compiled-core-bound receipt. The second terminal review + (`review-1784402070934-k183s3-kern-5-r2-m4-3a-terminal-r2`) found no + verified issues across five completed engines; Kimi timed out, while Codex + and Claude exposed a valid null-winner status-printer edge. That edge is + now RED-covered and fixed. The next exact receipt-bound Node 22 + `pnpm fitness:kern-5` wall passed. The third terminal review completed all + six usable engines + (`review-1784404337202-jjc35k-kern-5-r2-m4-3a-terminal-r3`) with zero + verified findings and independently exposed duplicated package-boundary + guard/message drift. Python and the checker adapter now consume the shared + core guard, a RED export-contract test proves the dependency, and the + compiled-core-bound receipt is regenerated. The corrected exact-tree Node + 22 `pnpm fitness:kern-5` wall passes. The final terminal review completed + all six usable engines with zero verified findings + (`review-1784406342479-xc3w91-kern-5-r2-m4-3a-terminal-r4`). Its five + needs-checks were rejected against the bound winner row-or-null schema, + the shared guard that runs before target-local fallback parsing, the + fn-only checker fact contract, semantic validation's location ownership, + and the runtime's required direct-invocation defense. 1. Correct the support matrix and make `fitness:kern-5` the planned aggregate, without pretending missing commands already exist. diff --git a/examples/capstone-assertion-engine/diag.kern b/examples/capstone-assertion-engine/diag.kern index 9c402ff8a..1e9f9c510 100644 --- a/examples/capstone-assertion-engine/diag.kern +++ b/examples/capstone-assertion-engine/diag.kern @@ -1,10 +1,14 @@ -fn name=pathAppendKey params="path:string,key:string" returns=string export=true +fn name=pathAppendKey returns=string export=true + param name=path type=string + param name=key type=string handler lang="kern" if cond="path == \"\"" return value="key" return value="path + \".\" + key" -fn name=pathAppendIndex params="path:string,idx:number" returns=string export=true +fn name=pathAppendIndex returns=string export=true + param name=path type=string + param name=idx type=number handler lang="kern" return value="path + \"[\" + String(idx) + \"]\"" @@ -12,24 +16,34 @@ fn name=passResult returns=string export=true handler lang="kern" return value="\"PASS|-|-\"" -fn name=failResult params="path:string,reason:string" returns=string export=true +fn name=failResult returns=string export=true + param name=path type=string + param name=reason type=string handler lang="kern" if cond="path == \"\"" return value="\"FAIL|$|\" + reason" return value="\"FAIL|\" + path + \"|\" + reason" -fn name=reasonTypeMismatch params="typeA:string,typeB:string" returns=string export=true +fn name=reasonTypeMismatch returns=string export=true + param name=typeA type=string + param name=typeB type=string handler lang="kern" return value="\"type mismatch: \" + typeA + \" vs \" + typeB" -fn name=reasonValueMismatch params="valueA:string,valueB:string" returns=string export=true +fn name=reasonValueMismatch returns=string export=true + param name=valueA type=string + param name=valueB type=string handler lang="kern" return value="\"value mismatch: \" + valueA + \" vs \" + valueB" -fn name=reasonLengthMismatch params="countA:number,countB:number" returns=string export=true +fn name=reasonLengthMismatch returns=string export=true + param name=countA type=number + param name=countB type=number handler lang="kern" return value="\"length mismatch: \" + String(countA) + \" vs \" + String(countB)" -fn name=reasonKeyMismatch params="keyA:string,keyB:string" returns=string export=true +fn name=reasonKeyMismatch returns=string export=true + param name=keyA type=string + param name=keyB type=string handler lang="kern" return value="\"key mismatch: \" + keyA + \" vs \" + keyB" diff --git a/examples/capstone-checker-subset/main.kern b/examples/capstone-checker-subset/main.kern index dd7b8251b..2aecbb644 100644 --- a/examples/capstone-checker-subset/main.kern +++ b/examples/capstone-checker-subset/main.kern @@ -73,25 +73,25 @@ fn name=main returns=void do value="stmtParent0.push(18)" let name=stmtLine0 value="[]" do value="stmtLine0.push(1)" - do value="stmtLine0.push(3)" - do value="stmtLine0.push(4)" do value="stmtLine0.push(5)" + do value="stmtLine0.push(6)" do value="stmtLine0.push(7)" do value="stmtLine0.push(9)" - do value="stmtLine0.push(11)" do value="stmtLine0.push(13)" do value="stmtLine0.push(15)" do value="stmtLine0.push(17)" - do value="stmtLine0.push(18)" do value="stmtLine0.push(19)" - do value="stmtLine0.push(21)" do value="stmtLine0.push(23)" + do value="stmtLine0.push(24)" do value="stmtLine0.push(25)" do value="stmtLine0.push(27)" - do value="stmtLine0.push(29)" do value="stmtLine0.push(31)" do value="stmtLine0.push(33)" - do value="stmtLine0.push(35)" + do value="stmtLine0.push(37)" + do value="stmtLine0.push(39)" + do value="stmtLine0.push(43)" + do value="stmtLine0.push(45)" + do value="stmtLine0.push(49)" let name=stmtCol0 value="[]" do value="stmtCol0.push(1)" do value="stmtCol0.push(5)" @@ -573,9 +573,9 @@ fn name=main returns=void do value="callStmtKind0.push(\"return\")" do value="callStmtKind0.push(\"return\")" let name=callLine0 value="[]" - do value="callLine0.push(9)" - do value="callLine0.push(31)" - do value="callLine0.push(31)" + do value="callLine0.push(13)" + do value="callLine0.push(43)" + do value="callLine0.push(43)" let name=callCol0 value="[]" do value="callCol0.push(5)" do value="callCol0.push(5)" diff --git a/package.json b/package.json index 8bcdef647..0e9ae14f6 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test:runner-smoke": "pnpm --filter @kernlang/core build && pnpm --filter @kernlang/cli build && node ./scripts/check-kern-run-smoke.mjs && node ./scripts/check-kern-5-preview-app.mjs && node --test ./scripts/runner-browser-budget-policy.test.mjs ./scripts/selfhost-smoke-policy.test.mjs && node ./scripts/check-runner-browser-budget.mjs && pnpm test:capstone-flatten && pnpm test:capstone-assertion-engine && pnpm test:capstone-checker-subset && pnpm test:selfhost-validator", "test:capstone-flatten": "node --test scripts/capstone/flatten.test.mjs", "test:capstone-assertion-engine": "pnpm --filter @kernlang/core build && pnpm --filter @kernlang/cli build && node ./scripts/check-capstone-assertion-engine.mjs", - "test:capstone-checker-subset": "pnpm --filter @kernlang/core build && pnpm --filter @kernlang/cli build && node ./scripts/check-capstone-checker-subset.mjs", + "test:capstone-checker-subset": "pnpm --filter @kernlang/core build && pnpm --filter @kernlang/cli build && node --test ./scripts/capstone-checker-subset/flatten-kern.test.mjs && node ./scripts/check-capstone-checker-subset.mjs", "test:selfhost-validator": "pnpm --filter @kernlang/core build && pnpm --filter @kernlang/cli build && node ./scripts/check-selfhost-validator.mjs", "test:app-behavior": "node ./scripts/app-behavior-conformance.mjs", "test:drift-showcase": "pnpm --filter @kernlang/core --filter @kernlang/express --filter @kernlang/python build && node ./scripts/check-drift-showcase.mjs", diff --git a/packages/core/src/codegen/type-system.ts b/packages/core/src/codegen/type-system.ts index bdbdfa498..94fd2c9a3 100644 --- a/packages/core/src/codegen/type-system.ts +++ b/packages/core/src/codegen/type-system.ts @@ -8,6 +8,7 @@ import type { ExprEmitContext } from '../codegen-expression.js'; import { emitExpression } from '../codegen-expression.js'; import { hasDirectSuperCtorCall } from '../constructor-super.js'; import { propsOf } from '../node-props.js'; +import { assertNoMixedParameterDeclarations } from '../parameter-declarations.js'; import { parseExpression } from '../parser-expression.js'; import { type IRNode, isExprObject } from '../types.js'; import { typescriptClosureClassifier, validateClosureBlockHostNamespacesTS } from '../typescript-closure-classifier.js'; @@ -851,24 +852,21 @@ export function parseParamListFromChildren(paramNodes: IRNode[], options?: Param /** * Slice 3c — unified TS parameter-list emitter for any callable IR node. * - * Reads the node's `param` children first (canonical, ValueIR-routed). If - * none, falls back to the legacy `params="..."` string. If neither, returns - * the fallback (default empty). - * - * Children win when present. Mixed mode is intentionally unsupported — a - * signature is either fully-structured-children or fully-legacy-string. - * Producers (importer, migrate-class-body) emit children all-or-nothing - * per signature; consumers don't need to reconcile partial states. + * Reads the node's `param` children (canonical, ValueIR-routed) or falls back + * to the legacy `params="..."` string. Mixed mode fails closed so a direct IR + * consumer cannot silently choose one authored signature over the other. */ export function emitParamList(node: IRNode, options?: ParamListOptions): string { const didValidate = beginIRHostNamespacesValidatedTS(node, { userBindings: options?.userBindings }); try { const paramChildren = kids(node, 'param'); + assertNoMixedParameterDeclarations(node); + const rawParams = p(node).params; + const hasLegacyParams = typeof rawParams === 'string' && rawParams.trim() !== ''; if (paramChildren.length > 0) { return parseParamListFromChildren(paramChildren, options); } - const params = (p(node).params as string | undefined) ?? ''; - if (params) return parseParamList(params, options); + if (hasLegacyParams) return parseParamList(rawParams, options); return options?.fallback ?? ''; } finally { endIRHostNamespacesValidatedTS(node, didValidate); diff --git a/packages/core/src/core-runtime/index.ts b/packages/core/src/core-runtime/index.ts index 36eb06a33..9c07f4d48 100644 --- a/packages/core/src/core-runtime/index.ts +++ b/packages/core/src/core-runtime/index.ts @@ -15,6 +15,7 @@ import { makeDecimalValue, } from '../ir/semantics/portable-scalar.js'; import { numberToInt32, numberToUint32 } from '../ir/semantics/to-numeric.js'; +import { assertNoMixedParameterDeclarations } from '../parameter-declarations.js'; import { parseExpression } from '../parser-expression.js'; import { splitPortableExpressionList } from '../portable-expression-list.js'; import type { IRNode } from '../types.js'; @@ -2104,6 +2105,7 @@ function runtimeChildren(node: IRNode): IRNode[] { } function runtimeParams(node: IRNode): RuntimeParam[] { + assertNoMixedParameterDeclarations(node); const childParams = node.children ?.filter((child) => child.type === 'param') diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9a030a9b6..7df73308d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -593,6 +593,13 @@ export type { } from './node-props.js'; // Typed node props export { propsOf, propsUntyped } from './node-props.js'; +export type { MixedParameterDeclarationViolation } from './parameter-declarations.js'; +export { + assertNoMixedParameterDeclarations, + hasMixedParameterDeclarations, + MIXED_PARAMETER_DECLARATION_MESSAGE, + mixedParameterDeclarationViolation, +} from './parameter-declarations.js'; export type { Token, TokenKind } from './parser.js'; // Core export { diff --git a/packages/core/src/parameter-declarations.ts b/packages/core/src/parameter-declarations.ts new file mode 100644 index 000000000..a9f7134fe --- /dev/null +++ b/packages/core/src/parameter-declarations.ts @@ -0,0 +1,35 @@ +import type { IRNode } from './types.js'; + +export const MIXED_PARAMETER_DECLARATION_MESSAGE = + 'Callable cannot combine legacy `params=` with structured `param` children.'; + +export interface MixedParameterDeclarationViolation { + readonly rule: 'mixed-parameter-declarations'; + readonly nodeType: string; + readonly message: typeof MIXED_PARAMETER_DECLARATION_MESSAGE; + readonly line?: number; + readonly col?: number; +} + +export function hasMixedParameterDeclarations(node: IRNode): boolean { + return ( + typeof node.props?.params === 'string' && + node.props.params.trim() !== '' && + node.children?.some((child) => child.type === 'param') === true + ); +} + +export function mixedParameterDeclarationViolation(node: IRNode): MixedParameterDeclarationViolation | undefined { + if (!hasMixedParameterDeclarations(node)) return undefined; + return { + rule: 'mixed-parameter-declarations', + nodeType: node.type, + message: MIXED_PARAMETER_DECLARATION_MESSAGE, + line: node.loc?.line, + col: node.loc?.col, + }; +} + +export function assertNoMixedParameterDeclarations(node: IRNode): void { + if (hasMixedParameterDeclarations(node)) throw new Error(MIXED_PARAMETER_DECLARATION_MESSAGE); +} diff --git a/packages/core/src/semantic-validator.ts b/packages/core/src/semantic-validator.ts index 7be3de16c..09cb6af99 100644 --- a/packages/core/src/semantic-validator.ts +++ b/packages/core/src/semantic-validator.ts @@ -20,6 +20,7 @@ import { hasDirectSuperCtorCall } from './constructor-super.js'; import { type CoreShapeDiagnostic, type CoreShapeInterfaceFact, collectCoreShapeFacts } from './core-shape-facts.js'; import { collectExternalImportSymbols, type ExternalImportSymbolTable } from './external-symbols.js'; import { importRegistryOf } from './import-metadata.js'; +import { mixedParameterDeclarationViolation } from './parameter-declarations.js'; import { parseExpression } from './parser-expression.js'; import { RAG_ASSERTION_KIND_SET, RAG_ASSERTION_KINDS } from './rag-assertions.js'; import { @@ -500,6 +501,9 @@ function validateNode( ancestry: string[], ancestorNodes: IRNode[], ): void { + const parameterViolation = mixedParameterDeclarationViolation(node); + if (parameterViolation) violations.push(parameterViolation); + // ── Machine transition cross-ref ─────────────────────────────────── if (node.type === 'machine' && node.children) { const stateNames = new Set(); diff --git a/packages/core/tests/core-runtime.test.ts b/packages/core/tests/core-runtime.test.ts index b1468bf18..07033ea46 100644 --- a/packages/core/tests/core-runtime.test.ts +++ b/packages/core/tests/core-runtime.test.ts @@ -1853,6 +1853,25 @@ describe('KERN core runtime functions', () => { expect(toHostValue(result.value)).toBe('hi world'); }); + test('mixed legacy and structured parameters fail before runtime binding', () => { + const fnNode: IRNode = { + type: 'fn', + props: { name: 'ambiguous', params: 'legacy:number', returns: 'number' }, + children: [ + { type: 'param', props: { name: 'structured', type: 'number' } }, + { + type: 'handler', + props: { lang: 'kern' }, + children: [{ type: 'return', props: { value: 'structured' } }], + }, + ], + }; + + expect(() => callCoreFunction(fnNode, [kNumber(7)])).toThrow( + 'Callable cannot combine legacy `params=` with structured `param` children.', + ); + }); + test('structured default prop quoting is supported', () => { const fnNode: IRNode = { type: 'fn', diff --git a/packages/core/tests/param-declaration-semantics.test.ts b/packages/core/tests/param-declaration-semantics.test.ts new file mode 100644 index 000000000..b3cdb0658 --- /dev/null +++ b/packages/core/tests/param-declaration-semantics.test.ts @@ -0,0 +1,62 @@ +import { parseDocumentWithDiagnostics } from '../src/parser.js'; +import { validateSemantics } from '../src/semantic-validator.js'; + +function mixedParameterViolations(source: string) { + return validateSemantics(parseDocumentWithDiagnostics(source).root).filter( + (violation) => violation.rule === 'mixed-parameter-declarations', + ); +} + +describe('semantic-validator — callable parameter declarations', () => { + test('rejects mixed legacy and structured declarations at the function location', () => { + const violations = mixedParameterViolations( + [ + 'fn name=load params="id:string" returns=string', + ' param name=name type=string', + ' handler code="return name;"', + ].join('\n'), + ); + + expect(violations).toEqual([ + { + rule: 'mixed-parameter-declarations', + nodeType: 'fn', + message: 'Callable cannot combine legacy `params=` with structured `param` children.', + line: 1, + col: 1, + }, + ]); + }); + + test('rejects mixed declarations on non-function callables', () => { + const violations = mixedParameterViolations( + [ + 'class name=Store', + ' method name=load params="id:string" returns=string', + ' param name=name type=string', + ' handler code="return name;"', + ].join('\n'), + ); + + expect(violations).toEqual([ + { + rule: 'mixed-parameter-declarations', + nodeType: 'method', + message: 'Callable cannot combine legacy `params=` with structured `param` children.', + line: 2, + col: 3, + }, + ]); + }); + + test('accepts legacy-only and structured-only declarations', () => { + expect( + mixedParameterViolations('fn name=legacy params="id:string" returns=string\n handler code="return id;"'), + ).toEqual([]); + expect( + mixedParameterViolations( + 'fn name=structured returns=string\n param name=id type=string\n handler code="return id;"', + ), + ).toEqual([]); + }); +}); diff --git a/packages/core/tests/param-value.test.ts b/packages/core/tests/param-value.test.ts index f83f961f3..763b31a38 100644 --- a/packages/core/tests/param-value.test.ts +++ b/packages/core/tests/param-value.test.ts @@ -3,13 +3,14 @@ * Slice 1j: const.value, 3a: let.value, 3b: field.value. Slice 3c extends the * same pattern to fn/method/constructor parameter defaults via structured * `param` child nodes. The legacy `params="..."` string with embedded defaults - * stays supported for back-compat. When both forms are present on the same - * node, structured children win (canonical). The `default=` rawExpr + * stays supported for back-compat. Using both forms on one node fails closed. + * The `default=` rawExpr * passthrough stays on the `param` node for back-compat with MCP usage and * for callers that need to opt out of canonicalisation. */ import { capabilitySupport } from '../src/capability-matrix.js'; import { parseParamList } from '../src/codegen/helpers.js'; +import { emitParamList } from '../src/codegen/type-system.js'; import { generateCoreNode } from '../src/codegen-core.js'; import { decompile } from '../src/decompiler.js'; import { importTypeScript } from '../src/importer.js'; @@ -238,8 +239,7 @@ describe('param.value — slice 3c (native ValueIR form)', () => { ); }); - it('structured param children win over legacy params= string', () => { - // When both are authored, children take precedence (canonical form). + it('rejects mixed structured param children and legacy params= string', () => { const node: IRNode = { type: 'fn', props: { name: 'load', params: 'id:string,retries:number=3', returns: 'string' }, @@ -248,11 +248,19 @@ describe('param.value — slice 3c (native ValueIR form)', () => { mk('handler', { code: 'return name;' }), ], }; - const code = generateCoreNode(node).join('\n'); - // Only the structured-form params are emitted; legacy is ignored. - expect(code).toContain('function load(name: string = world): string {'); - expect(code).not.toContain('id: string'); - expect(code).not.toContain('retries: number = 3'); + expect(() => generateCoreNode(node)).toThrow( + 'Callable cannot combine legacy `params=` with structured `param` children.', + ); + }); + + it('rejects mixed declarations for every callable consumed by emitParamList', () => { + const method = mk('method', { name: 'load', params: 'id:string', returns: 'string' }, [ + mk('param', { name: 'name', type: 'string' }), + ]); + + expect(() => emitParamList(method)).toThrow( + 'Callable cannot combine legacy `params=` with structured `param` children.', + ); }); }); diff --git a/packages/python/src/codegen-helpers.ts b/packages/python/src/codegen-helpers.ts index b528d1f69..b0e92085b 100644 --- a/packages/python/src/codegen-helpers.ts +++ b/packages/python/src/codegen-helpers.ts @@ -3,7 +3,7 @@ * used across all Python code generators. */ -import { type IRNode, isExprObject } from '@kernlang/core'; +import { assertNoMixedParameterDeclarations, type IRNode, isExprObject } from '@kernlang/core'; import { mapTsTypeToPython, mapTsTypeToPythonAnnotation, toSnakeCase } from './type-map.js'; // ── Micro-helpers ────────────────────────────────────────────────────── @@ -337,7 +337,10 @@ export function buildPythonParamList( node: IRNode, options?: { selfPrefix?: boolean; lazyAnnotations?: boolean }, ): string { + assertNoMixedParameterDeclarations(node); const paramChildren = kids(node, 'param'); + const rawParams = p(node).params; + const hasLegacyParams = typeof rawParams === 'string' && rawParams.trim() !== ''; const lazyAnnotations = options?.lazyAnnotations === true; let signature: string; @@ -348,8 +351,7 @@ export function buildPythonParamList( .join(', '); } else { // Legacy `params="..."` string fallback. - const rawParams = (p(node).params as string) || ''; - if (!rawParams) signature = ''; + if (!hasLegacyParams) signature = ''; else signature = parseLegacyParamParts(rawParams) .map((part) => { diff --git a/packages/python/tests/param-declarations-python.test.ts b/packages/python/tests/param-declarations-python.test.ts new file mode 100644 index 000000000..54829b112 --- /dev/null +++ b/packages/python/tests/param-declarations-python.test.ts @@ -0,0 +1,36 @@ +import { type IRNode, MIXED_PARAMETER_DECLARATION_MESSAGE } from '@kernlang/core'; + +import { buildPythonParamList } from '../src/codegen-helpers.js'; + +function fn(props: Record, children: IRNode[] = []): IRNode { + return { type: 'fn', props, children }; +} + +describe('Python parameter declarations', () => { + test('rejects mixed legacy and structured declarations', () => { + const node = fn({ name: 'load', params: 'id:string' }, [fnParam({ name: 'name', type: 'string' })]); + + expect(() => buildPythonParamList(node)).toThrow(MIXED_PARAMETER_DECLARATION_MESSAGE); + }); + + test('rejects mixed declarations on non-function callables', () => { + const method: IRNode = { + type: 'method', + props: { name: 'load', params: 'id:string' }, + children: [fnParam({ name: 'name', type: 'string' })], + }; + + expect(() => buildPythonParamList(method)).toThrow(MIXED_PARAMETER_DECLARATION_MESSAGE); + }); + + test('preserves legacy-only and structured-only declarations', () => { + expect(buildPythonParamList(fn({ name: 'legacy', params: 'userId:string' }))).toBe('user_id: str'); + expect(buildPythonParamList(fn({ name: 'structured' }, [fnParam({ name: 'userId', type: 'string' })]))).toBe( + 'user_id: str', + ); + }); +}); + +function fnParam(props: Record): IRNode { + return { type: 'param', props, children: [] }; +} diff --git a/scripts/capstone-checker-subset/flatten-kern.mjs b/scripts/capstone-checker-subset/flatten-kern.mjs index aef37d9a0..57bd02b1a 100644 --- a/scripts/capstone-checker-subset/flatten-kern.mjs +++ b/scripts/capstone-checker-subset/flatten-kern.mjs @@ -1,5 +1,9 @@ import { parseDocument } from '../../packages/core/dist/parser.js'; import { parseExpression } from '../../packages/core/dist/parser-expression.js'; +import { + hasMixedParameterDeclarations, + MIXED_PARAMETER_DECLARATION_MESSAGE, +} from '../../packages/core/dist/parameter-declarations.js'; export const DATA_ARRAYS = Object.freeze([ ['stmtKind', 'string'], @@ -87,7 +91,16 @@ function visitIr(node, out, currentFn, currentStmt) { if (node.type === 'fn') { nextFn = stringProp(node, 'name'); - const params = parseParamEntries(stringProp(node, 'params')); + const rawParams = stringProp(node, 'params'); + const paramChildren = Array.isArray(node.children) + ? node.children.filter((child) => child?.type === 'param') + : []; + if (hasMixedParameterDeclarations(node)) { + throw new TypeError(MIXED_PARAMETER_DECLARATION_MESSAGE); + } + const params = paramChildren.length > 0 + ? paramChildren.map((param) => structuredParamEntry(param)) + : parseParamEntries(rawParams); params.forEach((param, ordinal) => { out.paramFn.push(nextFn); out.paramName.push(param.name); @@ -103,8 +116,25 @@ function visitIr(node, out, currentFn, currentStmt) { } if (Array.isArray(node.children)) { - for (const child of node.children) visitIr(child, out, nextFn, stmtIndex); + for (const child of node.children) { + if (node.type === 'fn' && child?.type === 'param') continue; + visitIr(child, out, nextFn, stmtIndex); + } + } +} + +function structuredParamEntry(node) { + const props = node.props ?? {}; + const unsupportedProps = Object.keys(props).filter((key) => key !== 'name' && key !== 'type'); + if (unsupportedProps.length > 0 || (Array.isArray(node.children) && node.children.length > 0)) { + throw new TypeError('checker parameter facts support only `name` and `type`'); + } + const name = stringProp(node, 'name').trim(); + const rawType = props.type; + if (name === '' || (rawType !== undefined && typeof rawType !== 'string')) { + throw new TypeError('checker structured parameter requires a string `name` and optional string `type`'); } + return { name, type: typeof rawType === 'string' ? rawType.trim() : '' }; } function pushStmt(out, node, currentFn, parentStmt) { diff --git a/scripts/capstone-checker-subset/flatten-kern.test.mjs b/scripts/capstone-checker-subset/flatten-kern.test.mjs new file mode 100644 index 000000000..7f808fc70 --- /dev/null +++ b/scripts/capstone-checker-subset/flatten-kern.test.mjs @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { MIXED_PARAMETER_DECLARATION_MESSAGE } from '../../packages/core/dist/parameter-declarations.js'; +import { flattenKernSource } from './flatten-kern.mjs'; + +test('structured direct parameters produce the same parameter facts as legacy text', () => { + const legacy = flattenKernSource( + 'legacy.kern', + 'fn name=join params="left:string,right:number" returns=string\n handler lang=kern\n return value="left"\n', + ); + const structured = flattenKernSource( + 'structured.kern', + [ + 'fn name=join returns=string', + ' param name=left type=string', + ' param name=right type=number', + ' handler lang=kern', + ' return value="left"', + '', + ].join('\n'), + ); + + for (const key of ['paramFn', 'paramName', 'paramType', 'paramOrdinal']) { + assert.deepEqual(structured[key], legacy[key]); + } + assert.deepEqual(structured.stmtKind, ['fn', 'return']); +}); + +test('mixed legacy and structured declarations fail closed in the checker adapter', () => { + assert.throws( + () => + flattenKernSource( + 'mixed.kern', + 'fn name=join params="left:string" returns=string\n param name=right type=string\n', + ), + { message: MIXED_PARAMETER_DECLARATION_MESSAGE }, + ); +}); + +test('structured parameter features outside the checker fact profile fail closed', () => { + assert.throws( + () => + flattenKernSource( + 'default.kern', + 'fn name=join returns=string\n param name=left type=string value="fallback"\n', + ), + /checker parameter facts support only `name` and `type`/, + ); +}); diff --git a/scripts/check-kern-canonicalizer-coverage.mjs b/scripts/check-kern-canonicalizer-coverage.mjs index 60bc9c401..95a9e3c2a 100644 --- a/scripts/check-kern-canonicalizer-coverage.mjs +++ b/scripts/check-kern-canonicalizer-coverage.mjs @@ -2,18 +2,29 @@ import assert from 'node:assert/strict'; import { summarizeCanonicalizerCoverage } from './kern-canonicalizer/coverage.mjs'; import { assertCoverageSummary, writeCoverageSummary } from './kern-canonicalizer/coverage-summary-writer.mjs'; +import { formatCoverageWinnerStatus } from './kern-canonicalizer/coverage-status.mjs'; const summaryUrl = new URL('./kern-canonicalizer/coverage-summary.json', import.meta.url); const actual = summarizeCanonicalizerCoverage(); -assert.equal(actual.selection.winner, null, 'evidence-only fallback must remain explicit until a tranche qualifies'); if (process.argv.includes('--write')) { writeCoverageSummary(summaryUrl, actual); } else { + assert.deepEqual(actual.selection.winner, { + completeFunctions: 3, + completeTools: 1, + id: 'binary-expression', + occurrences: 941, + witnesses: [ + 'examples/capstone-assertion-engine/diag.kern#4:reasonTypeMismatch', + 'examples/capstone-assertion-engine/diag.kern#5:reasonValueMismatch', + 'examples/capstone-assertion-engine/diag.kern#7:reasonKeyMismatch', + ], + }, 'measured binary-expression tranche must remain exact'); assertCoverageSummary(summaryUrl, actual); } const leadingBlocker = actual.blockers[0]; process.stdout.write( `KERN canonicalizer coverage: ${actual.baseCompleteFunctions}/${actual.functionCount} base-complete; ` + `${leadingBlocker ? `${leadingBlocker.count} blocked by ${leadingBlocker.id}` : 'no profile blockers'}; ` + - 'no tranche selected.\n', + `${formatCoverageWinnerStatus(actual.selection.winner)}.\n`, ); diff --git a/scripts/kern-canonicalizer/coverage-policy.json b/scripts/kern-canonicalizer/coverage-policy.json index 5681bfc4d..acc4eafa5 100644 --- a/scripts/kern-canonicalizer/coverage-policy.json +++ b/scripts/kern-canonicalizer/coverage-policy.json @@ -18,7 +18,7 @@ "tool": "assertion-engine" }, { - "digest": "18c3d9def86d464d008311d4fd35a3e6d23e4d9499243c90d1fcdb473a51af61", + "digest": "9fb0c91cdc2e92d849a8bb83e245b7425f865cb5fb3cc45cbed142353966ac4e", "path": "examples/capstone-assertion-engine/diag.kern", "sourceKind": "handwritten", "tool": "assertion-engine" diff --git a/scripts/kern-canonicalizer/coverage-status.mjs b/scripts/kern-canonicalizer/coverage-status.mjs new file mode 100644 index 000000000..fa58c7f5d --- /dev/null +++ b/scripts/kern-canonicalizer/coverage-status.mjs @@ -0,0 +1,3 @@ +export function formatCoverageWinnerStatus(winner) { + return winner === null ? 'no tranche selected' : `${winner.id} tranche selected`; +} diff --git a/scripts/kern-canonicalizer/coverage-summary.json b/scripts/kern-canonicalizer/coverage-summary.json index d95c6034b..f2e476b69 100644 --- a/scripts/kern-canonicalizer/coverage-summary.json +++ b/scripts/kern-canonicalizer/coverage-summary.json @@ -2,7 +2,7 @@ "baseCompleteFunctions": 1, "blockers": [ { - "count": 97, + "count": 90, "id": "fn.params" }, { @@ -61,32 +61,36 @@ "catalogDigest": "fa3a0cddc280ff2d8dd9f09cf575953b5adbaaf6f8c716c05e06faf2d43cd6ea", "canonicalizerDigest": "2f0fec12660ba194c92371ce10121461e917258555a792b0b22ead8bc5c4f081", "canonicalizerPolicyDigest": "87463f6a56c75aeffc853c52923312a99b6ff864e9e37afe8d984c5704f917c2", - "compiledCoreDigest": "7b00119bb78af4ed955f7f0f3d636393b9ab6f0685bf11df661d5ab9da132725", + "compiledCoreDigest": "5c26e8d394f79dac034e88e035251e5e65dc5e9d259269a6d15c1087c4eb239f", "corpusMembers": 7, - "corpusDigest": "414746c7d4d1fd5e1027b02dff2b8f20402b36348a445b77a5a8f32fd2d7c089", - "coverageImplementationDigest": "fd6f0a53ec23088f82d8a6c06ad40c5ff31f4b1db00f6a6f6251efd77b361550", - "coveragePolicyDigest": "7788328e7862f5a697f5d750633cc1b005069f09b53ad38a090b30d0c9a537fa", + "corpusDigest": "2d5359347a1f89f33130fcea981025a14abc40c0a7f3ec3f2cd96ab201b2c199", + "coverageImplementationDigest": "c0d3042757cc5f8516201010daad1ab7c62cb4f1973ac4b2ea3e3661353ade93", + "coveragePolicyDigest": "f414bdb8729d5a692cf01efa23c5166253723b6c1425baceb57cd03fa7500748", "familyRegistryDigest": "a7ea4bdc1af766f893b7491a59c727b0459ecb637a71f9f54d6087ee5baeeb87", "expressionCatalogDigest": "d6d2a37f3b2ecc9989e3ff52c2d13357a85bd181e6b066e6ee523a127f05340d", "format": "kern.kir-canonicalizer.coverage-summary.1", - "functionFactsDigest": "088ff03fb9157e42f8c28e40f7df7ed4720a30e5008ce135816ae0e8e09244dd", + "functionFactsDigest": "b43e472e6189b03ead210c9fb30abcf9b6616f2d4b2dcb539b70610a9d0cc583", "functionCount": 98, - "policyDigest": "56e9d72beac6349eba3d161bd5b6d05cec3ce60f9b606f3dc58932981832d858", + "policyDigest": "b330b95d416928841ce05b507d2fa0106cfbaaa6225dc1228c0e77f65c7bfaf3", "profileDigest": "89b8d503b64d83a6475283ba3b58763b675a23a0f053b7875964c2867b2d3f85", "selection": { "ranking": [ { - "completeFunctions": 0, - "completeTools": 0, - "id": "conditional", - "occurrences": 1097, - "witnesses": [] + "completeFunctions": 3, + "completeTools": 1, + "id": "binary-expression", + "occurrences": 941, + "witnesses": [ + "examples/capstone-assertion-engine/diag.kern#4:reasonTypeMismatch", + "examples/capstone-assertion-engine/diag.kern#5:reasonValueMismatch", + "examples/capstone-assertion-engine/diag.kern#7:reasonKeyMismatch" + ] }, { "completeFunctions": 0, "completeTools": 0, - "id": "binary-expression", - "occurrences": 941, + "id": "conditional", + "occurrences": 1097, "witnesses": [] }, { @@ -153,7 +157,17 @@ "witnesses": [] } ], - "winner": null + "winner": { + "completeFunctions": 3, + "completeTools": 1, + "id": "binary-expression", + "occurrences": 941, + "witnesses": [ + "examples/capstone-assertion-engine/diag.kern#4:reasonTypeMismatch", + "examples/capstone-assertion-engine/diag.kern#5:reasonValueMismatch", + "examples/capstone-assertion-engine/diag.kern#7:reasonKeyMismatch" + ] + } }, "toolCount": 4 } diff --git a/scripts/kern-canonicalizer/coverage.test.mjs b/scripts/kern-canonicalizer/coverage.test.mjs index bbf2fe4b8..c898cd7fd 100644 --- a/scripts/kern-canonicalizer/coverage.test.mjs +++ b/scripts/kern-canonicalizer/coverage.test.mjs @@ -22,6 +22,7 @@ import { canonicalProfileRowsForFunction, profileBlockersForFunction, } from './coverage-profile.mjs'; +import { formatCoverageWinnerStatus } from './coverage-status.mjs'; import { VALID_FIXTURES } from './fixtures.mjs'; function canonicalExpression(kind, fields) { @@ -41,6 +42,11 @@ function canonicalExpression(kind, fields) { }; } +test('coverage status reports a null winner without throwing after receipt writes', () => { + assert.equal(formatCoverageWinnerStatus(null), 'no tranche selected'); + assert.equal(formatCoverageWinnerStatus({ id: 'binary-expression' }), 'binary-expression tranche selected'); +}); + test('the handwritten corpus produces one deterministic catalog-bound selection receipt', () => { const policy = loadCoveragePolicy(); const first = measureCanonicalizerCoverage(policy); @@ -58,11 +64,41 @@ test('the handwritten corpus produces one deterministic catalog-bound selection assert.deepEqual(new Set(first.corpus.map(({ sourceKind }) => sourceKind)), new Set(['handwritten'])); assert.ok(first.functions.length > 0); assert.ok(first.functions.some(({ firstUnsupported }) => firstUnsupported !== null)); - assert.equal(first.selection.winner, null); - assert.equal(first.selection.ranking.every(({ completeFunctions }) => completeFunctions === 0), true); + const diagFunctions = first.functions.filter(({ id }) => id.startsWith('examples/capstone-assertion-engine/diag.kern#')); + assert.deepEqual(diagFunctions.map(({ id }) => id), [ + 'examples/capstone-assertion-engine/diag.kern#0:pathAppendKey', + 'examples/capstone-assertion-engine/diag.kern#1:pathAppendIndex', + 'examples/capstone-assertion-engine/diag.kern#2:passResult', + 'examples/capstone-assertion-engine/diag.kern#3:failResult', + 'examples/capstone-assertion-engine/diag.kern#4:reasonTypeMismatch', + 'examples/capstone-assertion-engine/diag.kern#5:reasonValueMismatch', + 'examples/capstone-assertion-engine/diag.kern#6:reasonLengthMismatch', + 'examples/capstone-assertion-engine/diag.kern#7:reasonKeyMismatch', + ]); + assert.equal(diagFunctions.every(({ excludedProperties }) => !excludedProperties.includes('fn.params')), true); + assert.equal( + diagFunctions.flatMap(({ nodeOccurrences }) => nodeOccurrences).filter((kind) => kind === 'param').length, + 14, + ); + assert.equal(first.functions.filter(({ excludedProperties }) => excludedProperties.includes('fn.params')).length, 90); + assert.deepEqual(first.selection.winner, { + completeFunctions: 3, + completeTools: 1, + id: 'binary-expression', + occurrences: 941, + witnesses: [ + 'examples/capstone-assertion-engine/diag.kern#4:reasonTypeMismatch', + 'examples/capstone-assertion-engine/diag.kern#5:reasonValueMismatch', + 'examples/capstone-assertion-engine/diag.kern#7:reasonKeyMismatch', + ], + }); + assert.deepEqual( + first.selection.ranking.map(({ completeFunctions }) => completeFunctions), + [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ); assert.deepEqual(first.selection.ranking.map(({ id }) => id), [ - 'conditional', 'binary-expression', + 'conditional', 'binding', 'index-expression', 'call-expression',