diff --git a/evaluators/rust/src/lib.rs b/evaluators/rust/src/lib.rs index 693e00d..6d204c1 100644 --- a/evaluators/rust/src/lib.rs +++ b/evaluators/rust/src/lib.rs @@ -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)] diff --git a/packages/conformance/provenance-report.md b/packages/conformance/provenance-report.md index 6550107..ee9dd36 100644 --- a/packages/conformance/provenance-report.md +++ b/packages/conformance/provenance-report.md @@ -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 | |---|---|---| diff --git a/packages/conformance/src/chant-agreement.test.ts b/packages/conformance/src/chant-agreement.test.ts index d9b40c5..322db3d 100644 --- a/packages/conformance/src/chant-agreement.test.ts +++ b/packages/conformance/src/chant-agreement.test.ts @@ -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])[] = []; +// 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])[] = [ + ["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)); @@ -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 () => { diff --git a/packages/conformance/src/corpus.ts b/packages/conformance/src/corpus.ts index 02f0cac..bbf0d4d 100644 --- a/packages/conformance/src/corpus.ts +++ b/packages/conformance/src/corpus.ts @@ -1069,7 +1069,8 @@ export interface OriginCounts { * `unknown` fields by the composite that expanded their entity, keyed * ` (project)` when the declaring file, or a project file it * imports, defines a composite of that 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>; @@ -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 |", "|---|---|---|", diff --git a/packages/conformance/src/host.ts b/packages/conformance/src/host.ts index 497c3e8..49b2fa9 100644 --- a/packages/conformance/src/host.ts +++ b/packages/conformance/src/host.ts @@ -81,13 +81,15 @@ const Composite =

(factory: (props: P) => Record, 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"); diff --git a/packages/reference/CAVEATS.md b/packages/reference/CAVEATS.md index 100383b..ab162dc 100644 --- a/packages/reference/CAVEATS.md +++ b/packages/reference/CAVEATS.md @@ -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 diff --git a/packages/reference/src/adapter.ts b/packages/reference/src/adapter.ts index e4a8410..fbe669b 100644 --- a/packages/reference/src/adapter.ts +++ b/packages/reference/src/adapter.ts @@ -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) { diff --git a/packages/reference/src/project.ts b/packages/reference/src/project.ts index 0fa0f22..df2aa8c 100644 --- a/packages/reference/src/project.ts +++ b/packages/reference/src/project.ts @@ -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. */ @@ -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; diff --git a/paper/draft.md b/paper/draft.md index 65d1043..6d97485 100644 --- a/paper/draft.md +++ b/paper/draft.md @@ -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. diff --git a/paper/measurements.md b/paper/measurements.md index e63aa72..122d608 100644 --- a/paper/measurements.md +++ b/paper/measurements.md @@ -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). diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md index 2e80fce..b6452e9 100644 --- a/spec/CHANGELOG.md +++ b/spec/CHANGELOG.md @@ -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 diff --git a/spec/README.md b/spec/README.md index 5e1a471..92ac9cc 100644 --- a/spec/README.md +++ b/spec/README.md @@ -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 diff --git a/spec/VERSION b/spec/VERSION index 8bbe6cf..bb576db 100644 --- a/spec/VERSION +++ b/spec/VERSION @@ -1 +1 @@ -2.2 +2.3 diff --git a/spec/figures.test.ts b/spec/figures.test.ts index 7124a5e..d541755 100644 --- a/spec/figures.test.ts +++ b/spec/figures.test.ts @@ -192,7 +192,6 @@ const ALLOWLIST: Record = { "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", diff --git a/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/expect.json b/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/expect.json new file mode 100644 index 0000000..723b1b8 --- /dev/null +++ b/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/expect.json @@ -0,0 +1,46 @@ +{ + "rules": [ + "F-Call", + "F-Host-Composite", + "F-Count" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold", + "shapes.ts": "run" + }, + "rejectRule": { + "shapes.ts": "F-Eval-Function" + }, + "exports": { + "app.ts": { + "s": { + "bucket": { + "entityType": "Bucket", + "lexicon": "shapes", + "props": { + "name": "a" + }, + "attributes": {} + } + }, + "w": { + "inner": { + "entityType": "Bucket", + "lexicon": "shapes", + "props": { + "name": "a" + }, + "attributes": {} + } + } + } + }, + "counters": { + "factoryInterpretations": 2, + "factoryInvocations": 0, + "projectFactoryInvocations": 0 + }, + "note": "A composite registered with the host's Composite form is interpreted (F-Call step 4), and its result is a composite instance, so a read of it inside another such composite's argument qualifies (chant#3610). Store is interpreted once for both s and the read of s.bucket in Wrap's argument (F-Count), which makes two interpretations in all. The bucket in w is the one in s." +} diff --git a/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/project/app.ts b/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/project/app.ts new file mode 100644 index 0000000..3cf7137 --- /dev/null +++ b/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/project/app.ts @@ -0,0 +1,4 @@ +import { Store, Wrap } from "./shapes"; + +export const s = Store({ name: "a" }); +export const w = Wrap({ inner: s.bucket }); diff --git a/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/project/shapes.ts b/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/project/shapes.ts new file mode 100644 index 0000000..5f21a73 --- /dev/null +++ b/spec/fixtures/F-Call/a-host-composite-result-read-in-an-argument-folds/project/shapes.ts @@ -0,0 +1,4 @@ +import { Bucket, Composite } from "@tsad/shapes"; + +export const Store = Composite((props: { name: string }) => ({ bucket: new Bucket({ name: props.name }) }), "Store"); +export const Wrap = Composite((props: { inner: unknown }) => ({ inner: props.inner }), "Wrap"); diff --git a/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/expect.json b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/expect.json new file mode 100644 index 0000000..6670ec4 --- /dev/null +++ b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/expect.json @@ -0,0 +1,35 @@ +{ + "rules": [ + "F-Call", + "F-Count" + ], + "project": true, + "host": "shapes", + "verdicts": { + "after.ts": "run", + "before.ts": "run", + "inside.ts": "fold" + }, + "rejectRule": { + "after.ts": "F-Eval-Reject", + "before.ts": "F-Eval-Reject" + }, + "exports": { + "inside.ts": { + "wrapped": { + "left": { + "inner": { + "left": "a", + "right": "b", + "entityType": "Pair", + "lexicon": "shapes" + } + }, + "right": 0, + "entityType": "Pair", + "lexicon": "shapes" + } + } + }, + "note": "F-Call binds a same-file call's result for the evaluation of the argument that reads it, and for nothing after (chant#3610). inside.ts reads p only inside makePair's argument and folds. after.ts and before.ts add plain, which reads p in an object literal at a declarator, outside any argument. That read is J1's rejection whichever declarator comes first, so both files run: an argument that resolved p earlier does not leave it bound." +} diff --git a/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/after.ts b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/after.ts new file mode 100644 index 0000000..34277ab --- /dev/null +++ b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/after.ts @@ -0,0 +1,6 @@ +import { makePair } from "@tsad/shapes"; + +const p = makePair("a", "b"); + +export const wrapped = makePair({ inner: p }, 0); +export const plain = { inner: p }; diff --git a/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/before.ts b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/before.ts new file mode 100644 index 0000000..762c804 --- /dev/null +++ b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/before.ts @@ -0,0 +1,6 @@ +import { makePair } from "@tsad/shapes"; + +const p = makePair("a", "b"); + +export const plain = { inner: p }; +export const wrapped = makePair({ inner: p }, 0); diff --git a/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/inside.ts b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/inside.ts new file mode 100644 index 0000000..6f1455f --- /dev/null +++ b/spec/fixtures/F-Call/a-read-in-an-argument-binds-for-that-argument-only/project/inside.ts @@ -0,0 +1,5 @@ +import { makePair } from "@tsad/shapes"; + +const p = makePair("a", "b"); + +export const wrapped = makePair({ inner: p }, 0); diff --git a/spec/fixtures/F-Obs-Provenance/a-host-call-at-an-export-is-unknown/expect.json b/spec/fixtures/F-Obs-Provenance/a-host-call-at-an-export-is-unknown/expect.json new file mode 100644 index 0000000..7a37425 --- /dev/null +++ b/spec/fixtures/F-Obs-Provenance/a-host-call-at-an-export-is-unknown/expect.json @@ -0,0 +1,19 @@ +{ + "rules": [ + "F-Obs-Provenance", + "F-Eval-CallEager" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold" + }, + "provenance": { + "app.ts": { + "k": { + "kind": "unknown" + } + } + }, + "note": "Since 2.3 a host call's value written at an export is unknown, never direct (#248). The export is the path itself and no composite is involved. The value is still the host's, so the file's own code did not write it." +} diff --git a/spec/fixtures/F-Obs-Provenance/a-host-call-at-an-export-is-unknown/project/app.ts b/spec/fixtures/F-Obs-Provenance/a-host-call-at-an-export-is-unknown/project/app.ts new file mode 100644 index 0000000..a9301b8 --- /dev/null +++ b/spec/fixtures/F-Obs-Provenance/a-host-call-at-an-export-is-unknown/project/app.ts @@ -0,0 +1,3 @@ +import { count } from "@tsad/shapes"; + +export const k = count([1, 2]); diff --git a/spec/fixtures/F-Obs-Provenance/a-host-call-in-a-field-is-unknown/expect.json b/spec/fixtures/F-Obs-Provenance/a-host-call-in-a-field-is-unknown/expect.json new file mode 100644 index 0000000..500fd91 --- /dev/null +++ b/spec/fixtures/F-Obs-Provenance/a-host-call-in-a-field-is-unknown/expect.json @@ -0,0 +1,22 @@ +{ + "rules": [ + "F-Obs-Provenance", + "F-Eval-CallEager" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold" + }, + "provenance": { + "app.ts": { + "bucket.props.name": { + "kind": "direct" + }, + "bucket.props.replicas": { + "kind": "unknown" + } + } + }, + "note": "Since 2.3 a value a host call produces is unknown wherever it is written (#248). The file's own code writes both fields, outside any composite, but replicas takes its value from count, which the host defines. name beside it is a literal and stays direct." +} diff --git a/spec/fixtures/F-Obs-Provenance/a-host-call-in-a-field-is-unknown/project/app.ts b/spec/fixtures/F-Obs-Provenance/a-host-call-in-a-field-is-unknown/project/app.ts new file mode 100644 index 0000000..4bc60c5 --- /dev/null +++ b/spec/fixtures/F-Obs-Provenance/a-host-call-in-a-field-is-unknown/project/app.ts @@ -0,0 +1,3 @@ +import { Bucket, count } from "@tsad/shapes"; + +export const bucket = new Bucket({ name: "logs", replicas: count(["a", "b"]) }); diff --git a/spec/fixtures/F-Obs-Provenance/a-host-composite-member-is-unknown-never-direct/expect.json b/spec/fixtures/F-Obs-Provenance/a-host-composite-member-is-unknown-never-direct/expect.json index 9e05b51..7cc4d24 100644 --- a/spec/fixtures/F-Obs-Provenance/a-host-composite-member-is-unknown-never-direct/expect.json +++ b/spec/fixtures/F-Obs-Provenance/a-host-composite-member-is-unknown-never-direct/expect.json @@ -24,5 +24,5 @@ } } }, - "note": "F-Obs-Provenance's unknown origin. Archive is a composite the host publishes, so F-Call invokes it rather than interpreting its body (step 6), and nothing in the build says which argument became which field. The fields are written inside a composite, so they are not direct, and the implementation cannot name the parameter or the literal either: the origin is unknown, and an adapter that reports direct here fails." + "note": "F-Obs-Provenance's unknown origin. Archive is a composite the host publishes with no TypeScript source behind it, so F-Call invokes it rather than interpreting its body (step 6), and nothing in the build says which argument became which field. The fields are written inside a composite, so they are not direct, and the implementation cannot name the parameter or the literal either: the origin is unknown, and an adapter that reports direct here fails." } diff --git a/spec/fixtures/F-Obs-Provenance/a-host-value-is-unknown/expect.json b/spec/fixtures/F-Obs-Provenance/a-host-value-is-unknown/expect.json new file mode 100644 index 0000000..6bc6b90 --- /dev/null +++ b/spec/fixtures/F-Obs-Provenance/a-host-value-is-unknown/expect.json @@ -0,0 +1,25 @@ +{ + "rules": [ + "F-Obs-Provenance", + "F-Host-DataExports" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold" + }, + "provenance": { + "app.ts": { + "bucket.props.name": { + "kind": "direct" + }, + "bucket.props.replicas": { + "kind": "unknown" + }, + "large": { + "kind": "unknown" + } + } + }, + "note": "Since 2.3 a host value is unknown wherever it is written, as a host call's value is (#248). SIZES is plain data the host package exports, so SIZES.large folds as a value, in a field and at an export alike, and neither is direct." +} diff --git a/spec/fixtures/F-Obs-Provenance/a-host-value-is-unknown/project/app.ts b/spec/fixtures/F-Obs-Provenance/a-host-value-is-unknown/project/app.ts new file mode 100644 index 0000000..2846b0f --- /dev/null +++ b/spec/fixtures/F-Obs-Provenance/a-host-value-is-unknown/project/app.ts @@ -0,0 +1,4 @@ +import { Bucket, SIZES } from "@tsad/shapes"; + +export const bucket = new Bucket({ name: "logs", replicas: SIZES.large }); +export const large = SIZES.large; diff --git a/spec/hosts.md b/spec/hosts.md index c67365d..a2d63e1 100644 --- a/spec/hosts.md +++ b/spec/hosts.md @@ -40,8 +40,8 @@ what the trust rules need, and the last is the rules contract's. 5. A trust set of the package specifiers this build resolved and loaded (arm 1) plus the host's own module tree (arm 2). 6. A composite registration form, `export const N = Composite(fn, "N")` - with `Composite` imported from the host, that makes a project-defined - factory *interpretable*. + with `Composite` imported from the host, that makes a factory + *interpretable* (F-Host-Composite). 7. Rules (the host's semantic checks over the folded namespace and the artifact) under the contract of `rules.md` (F-Rule-Supply). A project may supply more as a policy. @@ -99,12 +99,14 @@ Explicitly excluded by chant with the reason each fails: ## F-Host-Closed-vs-Open (why packages are closed and project files are open) -A call into a **package** folds only two ways: +A call into a **package** folds only three ways: - Through a closed allowlist, a registered intrinsic or helper, checked by name *and* by the provenance of the binding (F-Div-Provenance). - At a declarator, by invocation (F-Call), reached directly or through a const alias (F-Declarator), where the result is a value whatever it is. +- By interpretation (F-Call step 4) at a declarator when an active package + registers the composite in TypeScript source (F-Host-Composite). Nothing folds through a package from a call written inside an expression. Its *result* may be read in one place. Inside an argument of a call F-Call @@ -117,7 +119,9 @@ with no allowlist. The asymmetry is the trust boundary. Package code is already loaded and executed by the build before discovery begins; admitting a call into it costs no execution the process was not performing so it is admitted -by declaration and verified by registration. The untrusted input is project +by declaration and verified by registration. Reading an active package's +source to interpret a composite adds no trust either: F-Host-Trust arm 1 +already trusts every subpath of the package. The untrusted input is project code. It is admitted only when it can be *evaluated without being executed* (folded or interpreted) and a syntactic body check decides that where an allowlist could not. @@ -141,10 +145,21 @@ This is the CTFE principle ([`prior-art.md`](./prior-art.md)) made a rule. ## F-Host-Composite (the registration that admits interpretation) -A project file's composite is interpretable (rule 2) iff its defining module -has `export const N = Composite(fn, "N")`, where `Composite` is bound, *in -that module*, to an import of the host's own, `fn` is an arrow or function -expression, and the name argument is absent or a string literal. +A composite is interpretable (rule 2) iff its defining module has +`export const N = Composite(fn, "N")` and all three of these hold: + +- `Composite` is bound *in that module* to an import of the host's own; +- `fn` is an arrow or function expression; +- the name argument is absent or a string literal. + +The defining module (rule 1) is either a project file or a TypeScript source +module inside an active package of this build. The package is matched by text first, as F-Host-Trust arm 1 matches a +specifier or a subpath, and only then resolved. A package module that +resolves to compiled JavaScript or a declaration file is not interpreted. The +body of a package's composite reads the package's own imports as trusted +under arm 1 and never as project files. A relative import that leaves the +package is not followed. When interpreting a package's composite declines, +F-Call continues at step 5. A plain helper that returns a composite is not registered and stays on the invoking path. A host that offers interpretation must define an equivalent diff --git a/spec/inventory.md b/spec/inventory.md index 3228c2e..e56f102 100644 --- a/spec/inventory.md +++ b/spec/inventory.md @@ -142,7 +142,7 @@ module is never imported. | # | Decision | Behavior | Covers | |---|---|---|---| -| L7.1 | rule 1; project files only | text check on the specifier; a lexicon-published composite is deliberately never interpreted | F-Call step 4; F-Host-Composite | +| L7.1 | rule 1; project files and active lexicon packages | text check on the specifier; a lexicon package is then resolved and interpreted only when the module is TypeScript source, so compiled JavaScript or a declaration file keeps invoking (chant 0.102.0, chant#3247) | F-Call step 4; F-Host-Composite | | L7.2 | rule 2; `export const N = Composite(, "N")` | and `Composite` must be chant's own **in the defining module** | F-Host-Composite | | L7.3 | rule 3; at most one parameter, bound plainly | no rest, default, nested, or array pattern | S-FactoryBody | | L7.4 | rule 4; body shape | concise expression, or `const`s then a final `return`; empty body rejected; **must end in `return`** (unlike L5.7) | S-FactoryBody | diff --git a/spec/observables.md b/spec/observables.md index 8a9c6c4..c009860 100644 --- a/spec/observables.md +++ b/spec/observables.md @@ -62,8 +62,10 @@ kinds: When more than one writer touches a path, the innermost writer wins. An origin the implementation cannot determine is reported as `unknown` and is -never reported as `direct`. An implementation that does not claim provenance -reports that it does not. Fold and run are not required to agree on origins. +never reported as `direct`. A value that a host call returns or a host's +plain-data export supplies (`SIZES.large`) is `unknown` and never `direct` +where the file's own code writes it at an export or into a field. An +implementation that does not claim provenance reports that it does not. Fold and run are not required to agree on origins. The claim does not enter the equivalence objective. Conformance reports whether it is made. diff --git a/spec/rationale.md b/spec/rationale.md index 27ff0cd..1a2502e 100644 --- a/spec/rationale.md +++ b/spec/rationale.md @@ -294,7 +294,8 @@ makes folding under isolation possible. A composite factory is *interpreted* (L7.1–L7.8) when all five hold: -1. The calling file imports it from a project file, by text, never a package. +1. The calling file imports it from a project file or from an active + package. A package import must resolve to TypeScript source. 2. The defining module has `export const N = Composite(, "N")` with `Composite` bound in *that* module to chant's own. 3. `` takes at most one plainly-bound parameter.