Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion evaluators/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::collections::HashMap;
use serde_json::{json, Value};

/// The specification version this evaluator declares (spec/VERSION).
const SPEC_VERSION: &str = "2.2";
const SPEC_VERSION: &str = "2.3";
const PROFILE: &str = "data-host";

#[derive(serde::Deserialize)]
Expand Down
2 changes: 1 addition & 1 deletion packages/conformance/provenance-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Unknown fields by reason:
|---|---|---|
| `composite-not-interpreted` | 5249 | 962 |

Unknown fields by the composite that expanded them, largest first. `project` marks a composite defined in the declaring file or a project file it imports, by the name it gives `Composite`; `package` is every other, a lexicon's composite, which chant invokes rather than interprets.
Unknown fields by the composite that expanded them, largest first. `project` marks a composite defined in the declaring file or a project file it imports, by the name it gives `Composite`; `package` is every other, a lexicon's composite, which chant interprets when its body is in the subset and invokes otherwise.

| Composite | Fold | Run |
|---|---|---|
Expand Down
26 changes: 22 additions & 4 deletions packages/conformance/src/chant-agreement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,20 @@ const all = loadFixtures(join(dirname(fileURLToPath(import.meta.url)), "..", "..
// a chant issue as its reason, the way each of these had.
// chant#3329 held F-Call/a-call-result-read-inside-an-argument out until
// chant-v0.109.0: the pin predated the fold of a same-file call's result read
// inside an argument (spec 2.2, F-Call step 6). Nothing is held out now.
const HELD: readonly (readonly [string, Set<string>])[] = [];
// inside an argument (spec 2.2, F-Call step 6).
//
// chant#3610 holds two F-Call fixtures out until the pin carries chant#3618.
// chant-v0.109.0 keeps a call result an argument resolved bound for the rest
// of the file, and it does not count a host composite's result as an instance.
//
// tsad#248 holds three provenance fixtures out. chant-v0.109.0 reports a host
// value written outside any composite as direct, or reports no origin for it,
// where spec 2.3 says unknown. Those differ from the fixture only, never from
// the reference's verdict, so the guard below reads chant's own report too.
const HELD: readonly (readonly [string, Set<string>])[] = [
["chant#3610", new Set(["F-Call/a-read-in-an-argument-binds-for-that-argument-only", "F-Call/a-host-composite-result-read-in-an-argument-folds"])],
["tsad#248", new Set(["F-Obs-Provenance/a-host-call-in-a-field-is-unknown", "F-Obs-Provenance/a-host-call-at-an-export-is-unknown", "F-Obs-Provenance/a-host-value-is-unknown"])],
];
const heldIds = new Set(HELD.flatMap(([, names]) => [...names]));
const fixtures = all.filter((f) => !heldIds.has(f.id));

Expand Down Expand Up @@ -92,8 +104,14 @@ describe("chant cross-check (#11)", () => {
const skipped = reports.filter((r) => r.skipped).map((r) => r.fixture);
expect(skipped, `${reason}: chant answered none of these, so agreement cannot be read from them:\n${skipped.join("\n")}`).toEqual([]);

const dis = await compareAdapters(referenceAdapter, chantAdapter, held);
expect(dis.length, `${reason} looks settled — drop its hold-out`).toBeGreaterThan(0);
// Each fixture on its own, so one that still disagrees cannot excuse
// another that has settled. A fixture still disagrees when chant differs
// from the reference or fails the fixture's own assertions.
for (const f of held) {
const dis = await compareAdapters(referenceAdapter, chantAdapter, [f]);
const report = reports.find((r) => r.fixture === f.id)!;
expect(dis.length > 0 || !report.pass, `${reason}: ${f.id} looks settled — drop its hold-out`).toBe(true);
}
}
});
test("chant's shape classifier is available (chant-v0.64.0+, chant#2362) and agrees on every fixture", async () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/conformance/src/corpus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,8 @@ export interface OriginCounts {
* `unknown` fields by the composite that expanded their entity, keyed
* `<name> (project)` when the declaring file, or a project file it
* imports, defines a composite of that name; `<name> (package)` otherwise,
* a lexicon's composite, which chant invokes rather than interprets; and
* a lexicon's composite, which chant interprets when its body is in the
* subset and invokes otherwise; and
* `(none)` when the record names no composite.
*/
readonly unknownByComposite: Readonly<Record<string, number>>;
Expand Down Expand Up @@ -1277,7 +1278,7 @@ export function renderProvenanceSection(reports: readonly EntryReport[], externa
"|---|---|---|",
...(reasons.length ? reasons.map((k) => `| \`${k}\` | ${s.fold.counts.unknown[k] ?? 0} | ${s.run.counts.unknown[k] ?? 0} |`) : ["| none | 0 | 0 |"]),
"",
"Unknown fields by the composite that expanded them, largest first. `project` marks a composite defined in the declaring file or a project file it imports, by the name it gives `Composite`; `package` is every other, a lexicon's composite, which chant invokes rather than interprets.",
"Unknown fields by the composite that expanded them, largest first. `project` marks a composite defined in the declaring file or a project file it imports, by the name it gives `Composite`; `package` is every other, a lexicon's composite, which chant interprets when its body is in the subset and invokes otherwise.",
"",
"| Composite | Fold | Run |",
"|---|---|---|",
Expand Down
8 changes: 5 additions & 3 deletions packages/conformance/src/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ const Composite = <P>(factory: (props: P) => Record<string, unknown>, name = "an
return Object.assign(definition, { compositeName: name });
};

/** A host-published composite (L7.1: never interpreted, invoked at a declarator or read through a const, F-Eval-CompositeConst). */
/** A host-published composite with no TypeScript source behind it, so L7.1 does not admit it: invoked at a declarator or read through a const, F-Eval-CompositeConst. */
const Stack = Composite((props: { left: unknown; right: unknown }) => ({ pair: new Pair(props.left, props.right) }), "Stack");

/**
* A host-published composite whose members are entities with props. Invoked,
* never interpreted, so an implementation that reports provenance cannot say
* which argument produced a member's field (F-Obs-Provenance's `unknown`).
* never interpreted, since this host supplies it in memory and no TypeScript
* source module stands behind it (F-Host-Composite). An implementation that
* reports provenance therefore cannot say which argument produced a member's
* field (F-Obs-Provenance's `unknown`).
*/
const Archive = Composite((props: { name: string }) => ({ bucket: new Bucket({ name: props.name, versioned: true }) }), "Archive");

Expand Down
7 changes: 7 additions & 0 deletions packages/reference/CAVEATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ step 6 for a *project* module: this package never imports project code, so
in open mode a registered composite that step 4 cannot interpret runs for
want of the invocation chant would perform.

Spec 2.3 lets step 4 interpret a composite an active package registers in
TypeScript source. That arm never applies here: the conformance host supplies
its exports in memory and no source module stands behind them, so a host's
composite is always invoked. An interpreted composite's result counts as a
composite instance for F-Call's reads in an argument, although its members
object has a plain prototype (chant#3610).

## Isolation is observable at one step

`Host.isolation` is `open` or `isolated`, and a project fixture asks for one
Expand Down
2 changes: 1 addition & 1 deletion packages/reference/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function adapterFor(profile: Profile): ConformanceAdapter {
name: profile === "full" ? "reference" : `reference/${profile}`,
// Bumped by hand when the rule set this package implements moves; the
// conformance suite fails when it and spec/VERSION disagree (#18).
specVersion: "2.2",
specVersion: "2.3",
// F-Obs-Provenance: reported in both profiles, since the reference interprets every project function it folds through.
provenance: true,
shape(source, exportName) {
Expand Down
8 changes: 6 additions & 2 deletions packages/reference/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@ function foldFile(path: string, session: Session): Verdict {
* `[byTrace]`, where the name is bound by `const n = c(…)` with a callee
* F-Call admits. The call is resolved by F-Call once per file (F-Count,
* `callMemo`) and the name reads its result while this argument folds. A
* result that is not a live object (F-Val-Live), or a call that refuses,
* result that is neither a live object (F-Val-Live) nor the instance an
* interpreted composite (step 4) returns, or a call that refuses,
* leaves the name to F-Eval-Ident, which rejects as before. A reference
* inside a function, a type, or in a property-name position is not a read.
*/
Expand All @@ -540,7 +541,10 @@ function foldFile(path: string, session: Session): Verdict {
const call = consts.get(name) as ts.CallExpression;
let value: unknown;
try { value = fCall(call); } catch (e) { if (e instanceof FoldRejection) continue; throw e; }
if (!isLiveObject(value) || typeof value === "function") continue;
// An interpreted composite's members object is a composite instance
// although its prototype is plain (chant#3610).
const instance = isCompositeFactory(externals.get((call.expression as ts.Identifier).text)) && value !== null && typeof value === "object";
if (typeof value === "function" || !(instance || isLiveObject(value))) continue;
bound.set(name, value);
}
if (bound.size === 0) return scope;
Expand Down
2 changes: 1 addition & 1 deletion paper/draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,4 @@ The subset is not the contribution; six languages have carved one more completel

## Appendix B. Artifacts

The specification is at version `2.2`, tagged `spec-2.2`, in the typescript-as-data repository under the INTENTIUS organization. The reference implementation and the conformance suite sit beside it with the Rust evaluator, and the two packages are published to npm at `2.2.0`. Every number in section 6 was taken on 7 October 2026 with chant pinned at `0.109.0` and the corpus at that release's revision, from the corpus report committed there; the external checkouts are pinned by revision in the conformance package's manifest. The documentation site renders every figure from those artifacts at build time, and one of its pages folds a file in the reader's browser with the WebAssembly evaluator.
The specification is at version `2.3`, tagged `spec-2.3`, in the typescript-as-data repository under the INTENTIUS organization. The reference implementation and the conformance suite sit beside it with the Rust evaluator, and the two packages are published to npm at `2.2.0`. Every number in section 6 was taken on 7 October 2026 with chant pinned at `0.109.0` and the corpus at that release's revision, from the corpus report committed there; the external checkouts are pinned by revision in the conformance package's manifest. The documentation site renders every figure from those artifacts at build time, and one of its pages folds a file in the reader's browser with the WebAssembly evaluator.
4 changes: 2 additions & 2 deletions paper/measurements.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ The chant side's `false` is also an unsampled invariant: the run fails unless ev
|---|---|---|---|
| chant `0.72.5` | 127, of which 65 are whole-build | 135 of 139 | all, on the 120 the pin answers; three wait on the adapter (the counters fixture and the executing pair, both halves in the release), and the four round-trip fixtures skip, chant having no generator |

The 3 rules without a fixture are listed in `spec/fixtures/UNCOVERED.md` with a reason each. All are properties no adapter can observe from verdicts: the host's own module tree, and what a host may vary or must not execute. Provenance has fixtures now. The capability is optional, so they are judged for an adapter that claims it and reported skipped for one that does not. chant claims it since `0.109.0` and passes all five. One disagreement existed between the reference and chant, on an envelope inside a template span. The recommendation went into the spec, chant-v0.68.0 implemented it, and a fixture now pins it (chant#2349).
The 3 rules without a fixture are listed in `spec/fixtures/UNCOVERED.md` with a reason each. All are properties no adapter can observe from verdicts: the host's own module tree, and what a host may vary or must not execute. Provenance has fixtures now. The capability is optional, so they are judged for an adapter that claims it and reported skipped for one that does not. chant claims it since `0.109.0` and passes five of the eight. The other three are held out until chant reports a host value as `unknown` (tsad#248). One disagreement existed between the reference and chant, on an envelope inside a template span. The recommendation went into the spec, chant-v0.68.0 implemented it, and a fixture now pins it (chant#2349).

**Two profiles.** Spec `1.1` names `full` and `data-host` (F-Profile, F-Profile-DataHost); the second is the specification for an evaluator with no JavaScript runtime, and it is what a platform in another language implements. Fixtures carry profile tags, and 77 of the 134 belong to `data-host`, and the reference judged in that profile passes every one of them. That is a JavaScript implementation passing a profile defined by the absence of JavaScript, so it establishes that the profile is consistent. Whether it is implementable without an engine is what `evaluators/rust` establishes (#86): `tsad-eval`, written from the text in Rust on oxc, passes every fixture tagged for the profile, the four round-trip fixtures among them since it generates as well as folds (F-Val-Source), and agrees with the reference on each; compiled to WebAssembly, with no imports, the same crate passes the same fixtures and agrees with its own binary answer for answer (`rust-wasm-agreement.test.ts`), so the embedded form is the evaluator measured here and not a second one. Over chant's corpus it sits as a third column, both evaluators judged in `data-host` on the same host description: at `chant-v0.109.0` the two agree on all 467 files, and 149 fold on both sides to the same namespace, envelopes included. Writing it found the reference reviving and tainting in a profile that does neither, both fixed in the reference; the corpus report records the column's numbers.
**Two profiles.** Spec `1.1` names `full` and `data-host` (F-Profile, F-Profile-DataHost); the second is the specification for an evaluator with no JavaScript runtime, and it is what a platform in another language implements. Fixtures carry profile tags, and 77 of the 139 belong to `data-host`, and the reference judged in that profile passes every one of them. That is a JavaScript implementation passing a profile defined by the absence of JavaScript, so it establishes that the profile is consistent. Whether it is implementable without an engine is what `evaluators/rust` establishes (#86): `tsad-eval`, written from the text in Rust on oxc, passes every fixture tagged for the profile, the four round-trip fixtures among them since it generates as well as folds (F-Val-Source), and agrees with the reference on each; compiled to WebAssembly, with no imports, the same crate passes the same fixtures and agrees with its own binary answer for answer (`rust-wasm-agreement.test.ts`), so the embedded form is the evaluator measured here and not a second one. Over chant's corpus it sits as a third column, both evaluators judged in `data-host` on the same host description: at `chant-v0.109.0` the two agree on all 467 files, and 149 fold on both sides to the same namespace, envelopes included. Writing it found the reference reviving and tainting in a profile that does neither, both fixed in the reference; the corpus report records the column's numbers.

**Coercion, stated as fixtures.** Thirteen expression fixtures pin the parts of ECMAScript an evaluator in another language has to reproduce bit for bit. Number formatting in template literals is the largest of them (shortest round-trip digits; `1e+21`; `-0` as `0`; a literal beyond 2^53 rounded before it is printed). The others are `+`'s string-or-number dispatch and relational comparison on strings; the logical operators returning an operand; unary coercion and IEEE division. The one deliberate departure, array spread refusing a string (R10.6), is pinned as well. The same family pins that an `undefined`-valued property is present in the folded namespace and travels through a spread, which is what a selective-by-omission consumer reads (#82). Both implementations agree on all thirteen. Their sufficiency is not established by that: the reference is JavaScript and passes them for free, so whether they are enough is known only once an evaluator with no JavaScript engine runs them (#86).

Expand Down
27 changes: 27 additions & 0 deletions spec/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ The versioned history of the rule set. The policy is in [`README.md`](./README.m

Each entry lists the rules that changed and how in the four kinds the policy defines. A change confined to non-normative text is not versioned and is not listed here; the rationale sections and the inventory are the usual cases.

## 2.3, 2026-10-07

**Widened. `F-Call` step 4 rule 1 with `F-Host-Composite`.** A composite that
an active package registers in TypeScript source is now interpretable the way
a project file's composite is. The package is matched by text as F-Host-Trust
arm 1 matches it and only then resolved. A module that resolves to compiled
JavaScript or a declaration file is still invoked. Inside the body the
package's own imports are trusted under arm 1 and never folded as project
files. When interpreting a package's composite declines, F-Call continues at
step 5. F-Host-Trust is unchanged because arm 1 already trusts every subpath
of an active package. Reading that source adds no trust. `F-Host-Interface`
item 6 and `F-Host-Closed-vs-Open` now agree with the rule as inventory row
L7.1 does. chant has interpreted these composites since 0.102.0 (chant#3247) and
the text had not caught up (chant#3607).

**Added. `F-Obs-Provenance` host values.** A value that a host call returns
or a host's plain-data export supplies is `unknown` and never `direct`. That
holds at an export and in a field alike. The rule defined `direct` as the
file's own code and left open whether a host call outside any composite is
that code (#248).

**Why this is minor.** No verdict changes. A package composite that step 6
used to invoke folds whether it is interpreted or invoked. One whose
interpretation declines is still invoked at step 6. Only an implementation
that claims provenance is bound by the new obligation, and that is the added
case in `README.md`'s policy.

## 2.2, 2026-10-07

**Added. `F-Obs-Provenance`.** An implementation that claims path provenance
Expand Down
2 changes: 1 addition & 1 deletion spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ A host supplies seven things listed under `F-Host-Interface` in
- an intrinsic registry
- an allowlist of authoring helpers
- a trust set of packages
- the registration form that makes a project-defined factory interpretable
- the registration form that makes a composite factory interpretable
- rules over the folded values

chant's lexicons are one instantiation of it. The generality this buys covers
Expand Down
2 changes: 1 addition & 1 deletion spec/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.2
2.3
1 change: 0 additions & 1 deletion spec/figures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ const ALLOWLIST: Record<string, string> = {
"draft.md:79": "same coverage history",
"draft.md:10": "same coverage history",
"draft.md:107": "the corpus size during the coverage-history analysis being described",
"measurements.md:77": "fixtures tagged data-host, computed by the conformance package's profilesOf rather than from the tree",
"measurements.md:79": "entries the fourth run moved, from the coverage history",
"measurements.md:10": "same coverage history",
"measurements.md:21": "same coverage history",
Expand Down
Loading
Loading