diff --git a/docs/content/spec/conformance/coverage.md b/docs/content/spec/conformance/coverage.md index 21edab7..7d94b78 100644 --- a/docs/content/spec/conformance/coverage.md +++ b/docs/content/spec/conformance/coverage.md @@ -59,8 +59,8 @@ The file's own header states the count, {{< figure "rulesWithFixture" >}} of `paper/measurements.md` sorts the rest by what each waits on. Five need a form the reference implementation lacks, the composite factory and isolation -mode. The six of `rules.md` need the harness half of the rules contract -(#101), and the remainder are waiting for the corpus to grow (#24). +mode, and five are properties no adapter can observe, such as execution +counters and provenance. A few entries are worth reading as a group, because they say something about where the limits of a conformance suite are rather than where the gaps in the diff --git a/docs/content/what-it-enables/semantic-linting.md b/docs/content/what-it-enables/semantic-linting.md index f44b447..9f6bfbf 100644 --- a/docs/content/what-it-enables/semantic-linting.md +++ b/docs/content/what-it-enables/semantic-linting.md @@ -19,4 +19,4 @@ chant's lexicons carry semantic lint rules for each target, running over folded ## Where the rule lives -Evaluability, "is this file data", is already specified as the `S-*` classifier and `F-Div-*`. The contract for semantic rules over values, `F-Rule-*`, is [issue #79](https://github.com/INTENTIUS/typescript-as-data/issues/79). It fixes the input a rule sees and its purity, along with its located findings and the two phases. It is stated here as a forward reference rather than a claim. +Evaluability, "is this file data", is already specified as the `S-*` classifier and `F-Div-*`. The contract for semantic rules over values, `F-Rule-*`, is [`rules.md`](/typescript-as-data/spec/normative/rules/) since spec `1.4`. It fixes the input a rule sees and its purity, along with its findings and the two phases, and each of its six rules has a fixture whose expectation is a set of findings as data. diff --git a/packages/conformance/src/adapter.ts b/packages/conformance/src/adapter.ts index 3c38dd4..469f74e 100644 --- a/packages/conformance/src/adapter.ts +++ b/packages/conformance/src/adapter.ts @@ -24,6 +24,27 @@ export interface ProjectResult { taintedBy?: Record; } +/** F-Rule-Finding's closed severity set. */ +export type Severity = "error" | "warning" | "info"; +/** F-Rule-Phase: named by the input the rule reads, the folded namespace or the artifact. */ +export type RulePhase = "pre" | "post"; +/** + * A finding, as data (F-Rule-Finding). `rule`, `severity` and `subject` are + * compared; `message` is not (F-Obs-Messages); `at` only when the fixture + * gives one and the implementation reports one (F-Obs-Provenance is + * optional). `file` names the file whose namespace holds the subject for a + * pre-synthesis finding; a post-synthesis finding concerns the artifact and + * carries none. + */ +export interface Finding { + rule: string; + severity: Severity; + subject: string; + message: string; + file?: string; + at?: { line: number; column: number }; +} + export interface ConformanceAdapter { readonly name: string; /** @@ -49,4 +70,15 @@ export interface ConformanceAdapter { files: Map, host?: ConformanceHost, ): ProjectResult | "unavailable" | Promise; + /** + * The findings of the host's rules of one phase over the build (#101). + * A rule is host code, so the harness carries only its identifier and its + * findings; an implementation that cannot run a rule the host names + * reports "unavailable" and the fixture is skipped visibly. + */ + rules?( + files: Map, + host: ConformanceHost, + phase: RulePhase, + ): Finding[] | "unavailable" | Promise; } diff --git a/packages/conformance/src/fixture.ts b/packages/conformance/src/fixture.ts index bc4d992..0816063 100644 --- a/packages/conformance/src/fixture.ts +++ b/packages/conformance/src/fixture.ts @@ -24,11 +24,17 @@ * "exports": { "config.ts": { "port": 8080 } }, // optional, for files that finally fold * "rejectRule": { "app.ts": "F-Eval-CallLocal" }, // optional, the rule a run verdict must name; checked when the adapter reports one * "host": "shapes", // optional, a named host from host.ts; required if the sources import one + * "findings": { "bucket.ts": [ { "rule": "SHAPES001", "subject": "bad", "severity": "error" } ], // optional (#101): the host's rules' findings, keyed by the + * "artifact": [ { "rule": "SHAPES002", "subject": "missing: Bucket", "severity": "warning" } ] }, // file whose namespace holds the subject (pre) or "artifact" (post); "at" optional * "profiles": ["full"], // optional; see profilesOf for the default * "note": "why this fixture exists" } * `tentative` and `taintedBy` are what separate "folds because nothing * reached it" from "would have folded, and an edge killed it" — without them * a project fixture cannot tell F-Seed from F-Taint. + * + * `findings` is what an F-Rule-* fixture asserts (#101): the findings of the + * named host's rules, as data. Matched on rule, subject and severity; the + * message is non-normative, and a location only when both sides give one. */ import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; @@ -41,6 +47,7 @@ export interface ExpressionFixture { shape: "accept" | "reject"; fold: "fold" | "run"; value?: unknown; rejectAt?: { line: number; column: number }; note?: string; } +export interface ExpectedFinding { rule: string; subject: string; severity: "error" | "warning" | "info"; at?: { line: number; column: number } } export interface ProjectFixture { kind: "project"; profiles: Profile[]; @@ -55,6 +62,8 @@ export interface ProjectFixture { rejectRule?: Record; /** A named host from host.ts. Required for any fixture whose sources import one. */ host?: string; + /** The host's rules' findings, keyed by file (pre-synthesis) or "artifact" (post-synthesis). */ + findings?: Record; note?: string; } export type Fixture = ExpressionFixture | ProjectFixture; @@ -102,7 +111,7 @@ export function loadFixtures(root: string): Fixture[] { const id = `${rule}/${name}`; if (e.project) { const fx: ProjectFixture = { kind: "project", id, dir: d, rules: e.rules, files: readProject(join(d, "project")), profiles: [], - verdicts: e.verdicts, tentative: e.tentative, taintedBy: e.taintedBy, exports: e.exports, rejectRule: e.rejectRule, host: e.host, note: e.note }; + verdicts: e.verdicts, tentative: e.tentative, taintedBy: e.taintedBy, exports: e.exports, rejectRule: e.rejectRule, host: e.host, findings: e.findings, note: e.note }; fx.profiles = profilesOf(fx, e.profiles); out.push(fx); } else { diff --git a/packages/conformance/src/host.ts b/packages/conformance/src/host.ts index 59a9b54..93a4a09 100644 --- a/packages/conformance/src/host.ts +++ b/packages/conformance/src/host.ts @@ -17,6 +17,18 @@ export interface HostIntrinsic { readonly outputKey?: string; } +/** + * A rule the host supplies (F-Rule-Supply), by identifier. The rule's code is + * the implementation's; what the harness carries is the id, the phase, the + * rule's own severity (L11.6) and what it checks, in words. + */ +export interface HostRule { + readonly id: string; + readonly phase: "pre" | "post"; + readonly severity: "error" | "warning" | "info"; + readonly description: string; +} + export interface ConformanceHost { readonly name: string; /** F-Host-Trust arm 1: the specifiers this host owns. */ @@ -25,6 +37,8 @@ export interface ConformanceHost { readonly helpers: readonly { name: string; module: string; note: string }[]; /** Specifier, then export name, to the real value. Revival calls these (F-Val-Fate). */ readonly values: ReadonlyMap>; + /** F-Host-Interface item 7: the rules this host supplies, if any. */ + readonly rules?: readonly HostRule[]; } const DECLARABLE = Symbol.for("tsad.conformance.declarable"); @@ -79,6 +93,14 @@ const SHAPES: ConformanceHost = { { name: "count", isTag: false, foldsEagerly: true }, ], helpers: [{ name: "upper", module: "@tsad/shapes", note: "pure string transform, no environment read" }], + // Two rules, one per phase (F-Rule-Phase). The pre-synthesis one reads + // the folded namespace and names the export it concerns; the + // post-synthesis one reads the artifact and, finding nothing to attach + // to, reports the missing-resource form (F-Rule-Finding, L11.5). + rules: [ + { id: "SHAPES001", phase: "pre", severity: "error", description: "every Bucket declares a BucketName; the subject is the export that does not" }, + { id: "SHAPES002", phase: "post", severity: "warning", description: "the artifact declares at least one Bucket; otherwise the subject is `missing: Bucket`" }, + ], values: new Map([ [ "@tsad/shapes", diff --git a/packages/conformance/src/runner.test.ts b/packages/conformance/src/runner.test.ts index 8802da9..72adfc1 100644 --- a/packages/conformance/src/runner.test.ts +++ b/packages/conformance/src/runner.test.ts @@ -14,10 +14,14 @@ describe("conformance runner (#7) over the reference implementation (#10)", () = const failed = reports.filter((r) => !r.pass).map((r) => `${r.fixture}: ${r.failures.join("; ")}`); expect(failed, failed.join("\n")).toEqual([]); }); - test("the data-host profile: the reference with an empty host passes every fixture tagged for it (#78)", async () => { + test("the data-host profile: the reference passes every fixture tagged for it, a named host reduced to its description (#78)", async () => { const tagged = fixtures.filter((f) => f.profiles.includes("data-host")); expect(tagged.length).toBeGreaterThan(40); - expect(tagged.every((f) => f.kind === "expression" || !f.host)).toBe(true); + // A data-host fixture may name a host for its intrinsic registry and trust + // set (F-Profile-DataHost, F-Host-Interface items 3 and 5); the adapter is + // what drops the classes, helpers and values, and these fixtures are what + // would fail if it did not. + expect(tagged.some((f) => f.kind === "project" && f.host)).toBe(true); const failed = (await runFixtures(referenceDataHostAdapter, tagged)).filter((r) => !r.pass).map((r) => `${r.fixture}: ${r.failures.join("; ")}`); expect(failed, failed.join("\n")).toEqual([]); }); diff --git a/packages/conformance/src/runner.ts b/packages/conformance/src/runner.ts index d82f9df..ccd1d09 100644 --- a/packages/conformance/src/runner.ts +++ b/packages/conformance/src/runner.ts @@ -1,4 +1,4 @@ -import type { ConformanceAdapter } from "./adapter.js"; +import type { ConformanceAdapter, Finding, RulePhase } from "./adapter.js"; import type { ExpressionFixture, Fixture, ProjectFixture } from "./fixture.js"; import { expressionFixtures, projectFixtures } from "./fixture.js"; import { requireHost } from "./host.js"; @@ -83,9 +83,55 @@ export async function runProjectFixture(adapter: ConformanceAdapter, f: ProjectF } } } + if (f.findings) { + const got = await collectFindings(adapter, f); + if (got === "unavailable") return { ...base, pass: failures.length === 0, skipped: "rules unavailable", failures }; + failures.push(...compareFindings(f.findings, got)); + } return { ...base, pass: failures.length === 0, failures }; } +/** The key a finding is filed under: the file whose namespace holds the subject, or the artifact. */ +const findingKey = (x: Finding, phase: RulePhase): string => (phase === "post" ? "artifact" : x.file ?? "artifact"); +const findingSig = (x: { rule: string; subject: string; severity: string }): string => `${x.rule} ${x.severity} ${x.subject}`; + +/** + * Every finding of both phases (#101). Each phase is run twice and the two + * runs must agree: F-Rule-Pure says a rule is a function of its input, and + * a rule that reads a clock or the environment fails here before its + * findings are compared with anything. + */ +export async function collectFindings(adapter: ConformanceAdapter, f: ProjectFixture): Promise | "unavailable"> { + if (!adapter.rules || !f.host) return "unavailable"; + const host = requireHost(f.host); + const out = new Map(); + for (const phase of ["pre", "post"] as const) { + const first = await adapter.rules(f.files, host, phase); + if (first === "unavailable") return "unavailable"; + const second = await adapter.rules(f.files, host, phase); + if (second === "unavailable") return "unavailable"; + const a = first.map((x) => `${findingKey(x, phase)}: ${findingSig(x)}`).sort(), b = second.map((x) => `${findingKey(x, phase)}: ${findingSig(x)}`).sort(); + if (a.join("\n") !== b.join("\n")) throw new Error(`${f.id}: ${phase}-synthesis findings differ between two runs of the same input (F-Rule-Pure)\n${a.join("\n")}\n---\n${b.join("\n")}`); + for (const x of first) { const k = findingKey(x, phase); out.set(k, [...(out.get(k) ?? []), x]); } + } + return out; +} + +/** Findings matched on rule, subject and severity; `at` only when both sides carry one. */ +export function compareFindings(want: Record, got: Map): string[] { + const failures: string[] = []; + for (const key of new Set([...Object.keys(want), ...got.keys()])) { + const w = want[key] ?? [], g = got.get(key) ?? []; + for (const x of w) { + const hit = g.find((y) => findingSig(y) === findingSig(x)); + if (!hit) { failures.push(`${key}: expected finding ${findingSig(x)}, not reported`); continue; } + if (x.at && hit.at && (hit.at.line !== x.at.line || hit.at.column !== x.at.column)) failures.push(`${key}: ${findingSig(x)} at ${hit.at.line}:${hit.at.column}, expected ${x.at.line}:${x.at.column}`); + } + for (const y of g) if (!w.some((x) => findingSig(x) === findingSig(y))) failures.push(`${key}: finding ${findingSig(y)} reported but not expected (${y.message})`); + } + return failures; +} + /** #11 — two implementations must agree on every fixture, independently of what the fixture expects. */ export async function compareAdapters(a: ConformanceAdapter, b: ConformanceAdapter, fixtures: Fixture[]): Promise { const dis: string[] = []; @@ -107,6 +153,15 @@ export async function compareAdapters(a: ConformanceAdapter, b: ConformanceAdapt dis.push(`${f.id} ${path}: tainted by — ${a.name} ${ra.taintedBy[path] ?? "nothing"}, ${b.name} ${rb.taintedBy[path] ?? "nothing"}`); } } + // F-Rule-Equivalence, across implementations: the same host rules over + // the same source report the same findings, whichever path each took. + if (f.findings) { + const [fa, fb] = await Promise.all([collectFindings(a, f), collectFindings(b, f)]); + if (fa !== "unavailable" && fb !== "unavailable") { + const sig = (m: Map) => [...m.entries()].flatMap(([k, xs]) => xs.map((x) => `${k}: ${findingSig(x)}`)).sort().join("\n"); + if (sig(fa) !== sig(fb)) dis.push(`${f.id}: findings — ${a.name}\n${sig(fa)}\n${b.name}\n${sig(fb)}`); + } + } } for (const f of expressionFixtures(fixtures)) { const ra = a.foldExport(f.input, f.exportName), rb = b.foldExport(f.input, f.exportName); diff --git a/packages/reference/CAVEATS.md b/packages/reference/CAVEATS.md index e348269..a2a5d89 100644 --- a/packages/reference/CAVEATS.md +++ b/packages/reference/CAVEATS.md @@ -71,9 +71,9 @@ chant as F-Import's text says; it now does both (#96). A project-local function is excluded at the import, since it is a callable rather than a value and F-CallLeak decides its edge at the call. -## No rules, and no provenance +## Two rules, and no provenance -`rules.md` (spec `1.4`) specifies the contract a semantic rule runs under. This package runs none: it has no rule hook, and no value provenance, so a finding it produced could name no source line. Both wait on the harness half (#101). +`rules.md` (spec `1.4`) specifies the contract a semantic rule runs under. This package carries the two rules the `shapes` host names, `SHAPES001` over the folded namespace and `SHAPES002` over its JSON serialization (`rules.ts`, #101), and nothing else: a host that names a rule this file does not carry gets `"unavailable"`. A file that runs has no folded namespace here, so no rule sees it; chant answers that case by executing the file. There is no value provenance, so a finding names an export and never a source line, which `F-Rule-Finding` allows. ## No filesystem, no module resolution algorithm diff --git a/packages/reference/src/adapter.ts b/packages/reference/src/adapter.ts index 91a63be..8c56ebf 100644 --- a/packages/reference/src/adapter.ts +++ b/packages/reference/src/adapter.ts @@ -2,16 +2,25 @@ import type { ConformanceAdapter, ConformanceHost, ProjectResult } from "@intent import { EMPTY_HOST, type Host, type Profile } from "./host.js"; import { shapeOfExport, foldExport } from "./module.js"; import { foldProject } from "./project.js"; +import { canRun, runRules } from "./rules.js"; -/** A named conformance host, in this implementation's own terms. */ +/** + * A named conformance host, in this implementation's own terms. In + * `data-host` the host is a description and not code (F-Profile-DataHost, + * F-Host-Interface): the intrinsic registry and the trust set are kept, and + * the helpers, the classes and the live values, items 1, 2, 4 and 6, are + * absent. + */ function hostOf(h: ConformanceHost | undefined, profile: Profile): Host { if (!h) return { ...EMPTY_HOST, profile }; + const data = profile === "data-host"; return { profile, intrinsics: h.intrinsics.map((i) => ({ name: i.name, isTag: i.isTag, foldsAsCall: i.foldsAsCall, foldsEagerly: i.foldsEagerly, outputKey: i.outputKey })), - helpers: h.helpers, + helpers: data ? [] : h.helpers, ownedSpecifierPrefixes: h.ownedSpecifierPrefixes, - values: h.values, + values: data ? new Map() : h.values, + rules: h.rules, }; } @@ -40,6 +49,11 @@ function adapterFor(profile: Profile): ConformanceAdapter { for (const [path, from] of r.taintSource) out.taintedBy![path] = from; return out; }, + rules(files, host, phase) { + const h = hostOf(host, profile); + if (!canRun(h)) return "unavailable"; + return runRules(foldProject(files, h), h, phase); + }, }; } diff --git a/packages/reference/src/host.ts b/packages/reference/src/host.ts index d33488b..7cdfc84 100644 --- a/packages/reference/src/host.ts +++ b/packages/reference/src/host.ts @@ -34,5 +34,7 @@ export interface Host { readonly helpers: readonly { name: string; module: string; note: string }[]; readonly ownedSpecifierPrefixes: readonly string[]; readonly values: HostValues; + /** F-Host-Interface item 7: the rules the host supplies, by id. The code for each is this implementation's (rules.ts). */ + readonly rules?: readonly { id: string; phase: "pre" | "post"; severity: "error" | "warning" | "info" }[]; } export const EMPTY_HOST: Host = { profile: "full", intrinsics: [], helpers: [], ownedSpecifierPrefixes: [], values: new Map() }; diff --git a/packages/reference/src/index.ts b/packages/reference/src/index.ts index 4d0ae61..1d18df4 100644 --- a/packages/reference/src/index.ts +++ b/packages/reference/src/index.ts @@ -6,3 +6,4 @@ export * from "./foldable-helpers.js"; export * from "./module.js"; export * from "./project.js"; export { referenceAdapter, referenceDataHostAdapter } from "./adapter.js"; +export { runRules, RULES, namespaceOf, artifactOf, type Finding } from "./rules.js"; diff --git a/packages/reference/src/project.ts b/packages/reference/src/project.ts index e16f9d1..03cd781 100644 --- a/packages/reference/src/project.ts +++ b/packages/reference/src/project.ts @@ -311,8 +311,13 @@ function foldFile(path: string, session: Session): Verdict { const scope: Scope = { consts, externals, depth: 0, captures }; const evalHost = { intrinsics: session.host.intrinsics }; const exports = new Map(); - /** F-Val-Fate: what the declarator produced, revived through this file's own imports. */ + /** + * F-Val-Fate: what the declarator produced, revived through this file's own + * imports. In `data-host` revival is serialization (F-Profile-DataHost): no + * constructor and no function is invoked, and the envelope is the output. + */ const live = (v: unknown, node: ts.Node, what: string) => { + if (session.host.profile === "data-host") return v; const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart()); return revive(v, externals, { line: line + 1, column: character + 1, what }); }; diff --git a/packages/reference/src/rules.ts b/packages/reference/src/rules.ts new file mode 100644 index 0000000..d4b8416 --- /dev/null +++ b/packages/reference/src/rules.ts @@ -0,0 +1,92 @@ +/** + * The rules contract (rules.md, #101) in this implementation: the host names a + * rule by id and this file carries its code, which is what F-Rule-Supply says + * a rule is. A rule sees one of two inputs and nothing else (F-Rule-Input): + * the folded namespace, every file that finally folded with its exports as + * final values, or the artifact, which here is the JSON serialization of + * that namespace, parsed once. Which input it reads is its phase + * (F-Rule-Phase). A rule is a function of that input (F-Rule-Pure): nothing + * in this file reads a clock, the environment or the network. + * + * A file that runs has no folded namespace, so a rule does not see it; that + * is the case chant answers by executing the file, which this package does + * not do (CAVEATS.md). + */ +import { isFoldableFunction } from "./fold.js"; +import type { Host } from "./host.js"; +import type { ProjectResult } from "./project.js"; + +export type Severity = "error" | "warning" | "info"; +export interface Finding { rule: string; severity: Severity; subject: string; message: string; file?: string } + +/** The folded namespace: file, then export, to its final value. */ +export type Namespace = ReadonlyMap>; +/** The artifact, parsed: the same shape as the namespace, after JSON. */ +export type Artifact = Record>; + +interface Rule { + readonly phase: "pre" | "post"; + readonly pre?: (ns: Namespace, severity: Severity) => Finding[]; + readonly post?: (doc: Artifact, severity: Severity) => Finding[]; +} + +const isEntity = (v: unknown, type: string): v is { props?: Record } => + typeof v === "object" && v !== null && (v as { entityType?: unknown }).entityType === type; + +/** The rules this implementation carries, by the id a host names them by. */ +export const RULES: ReadonlyMap = new Map([ + ["SHAPES001", { + phase: "pre", + pre(ns, severity) { + const out: Finding[] = []; + for (const [file, exports] of ns) for (const [name, value] of exports) { + if (isEntity(value, "Bucket") && !(value.props && "BucketName" in value.props)) { + out.push({ rule: "SHAPES001", severity, subject: name, file, message: `Bucket ${name} declares no BucketName` }); + } + } + return out; + }, + }], + ["SHAPES002", { + phase: "post", + post(doc, severity) { + const any = Object.values(doc).some((exports) => Object.values(exports).some((v) => isEntity(v, "Bucket"))); + return any ? [] : [{ rule: "SHAPES002", severity, subject: "missing: Bucket", message: "the artifact declares no Bucket" }]; + }, + }], +]); + +/** Every rule the host names is one this implementation carries; otherwise the host's rules cannot be run here. */ +export function canRun(host: Host): boolean { + return (host.rules ?? []).every((r) => RULES.get(r.id)?.phase === r.phase); +} + +/** A callable is never a value (F-Val-Callable) and never reaches a serializer (F-Val-Serializable), so an exported function is not in the namespace a rule sees. */ +export function namespaceOf(result: ProjectResult): Namespace { + const ns = new Map>(); + for (const [file, v] of result.verdicts) { + if (v.kind !== "fold") continue; + ns.set(file, new Map([...v.exports].filter(([, x]) => !isFoldableFunction(x) && typeof x !== "function"))); + } + return ns; +} + +/** The serializer's output, then parsed once (L11.2, L11.3): a rule of the post phase reads this and nothing else. */ +export function artifactOf(ns: Namespace): { text: string; doc: Artifact } { + const text = JSON.stringify(Object.fromEntries([...ns].map(([f, e]) => [f, Object.fromEntries(e)]))); + return { text, doc: JSON.parse(text) as Artifact }; +} + +export function runRules(result: ProjectResult, host: Host, phase: "pre" | "post"): Finding[] { + const ns = namespaceOf(result); + const out: Finding[] = []; + for (const r of host.rules ?? []) { + if (r.phase !== phase) continue; + const rule = RULES.get(r.id); + if (!rule || rule.phase !== r.phase) throw new Error(`rule ${r.id} (${r.phase}) is not one this implementation carries`); + // The configured severity is the host's declaration (L11.6); the rule's own is what it would say unconfigured. + if (phase === "pre") out.push(...rule.pre!(ns, r.severity)); + else out.push(...rule.post!(artifactOf(ns).doc, r.severity)); + } + return out; +} diff --git a/paper/measurements.md b/paper/measurements.md index 6fd8467..7a8abd0 100644 --- a/paper/measurements.md +++ b/paper/measurements.md @@ -52,9 +52,9 @@ The chant side's `false` is also an unsampled invariant: the run fails unless ev | Pin | Fixtures | Rules with a fixture | Shape and fold agreement | |---|---|---|---| -| chant `0.71.0` | 90, of which 36 are whole-build | 107 of 138 | all, on the 73 the pin can answer | +| chant `0.71.0` | 90, of which 36 are whole-build | 128 of 138 | all, on the 73 the pin can answer | -The 31 rules without a fixture are listed in `spec/fixtures/UNCOVERED.md` with a reason each; five wait on a form the reference does not implement (the composite factory and isolation mode), the six of `rules.md` wait on the harness half of the rules contract (#101) and the rest on the corpus growing (#24). One disagreement existed between the reference and chant, on an envelope inside a template span. The spec recorded the recommendation, chant-v0.68.0 implemented it, and a fixture now pins it (chant#2349). +The 10 rules without a fixture are listed in `spec/fixtures/UNCOVERED.md` with a reason each: five wait on a form the reference does not implement (the composite factory and isolation mode), and five are properties no adapter can observe, execution counters, provenance, the host's own module tree and what a host may vary. One disagreement existed between the reference and chant, on an envelope inside a template span. The spec recorded the recommendation, 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 59 of the 90 belong to `data-host`, and the reference with an empty host 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, not that it is implementable without an engine; the latter is #86's to establish. @@ -135,7 +135,7 @@ The corpus is chant's own examples, and chant's documentation says the number is ## Limits - Twelve mixed entries and one adversarial build are a small sample, all from one project. -- Fixture coverage is 107 of 138 rules. The 31 without one are listed with a reason, and the list may only shrink. +- Fixture coverage is 128 of 138 rules. The 10 without one are listed with a reason, and the list may only shrink. - J3's whole-build fixtures reach both implementations where no host is involved, and one where a host is. Comparing verdicts alone would not be enough, since a seed and a taint casualty are both `run`; the tentative verdict and the taint edge are compared too. - Revival is implemented for five of the six envelopes; `{__compositeStep}` needs a composite factory form the reference does not have (`packages/reference/CAVEATS.md`). - The independent rewrite found two specification gaps. Two is a small sample, and it is the sample a single author working alone can produce. diff --git a/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/expect.json b/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/expect.json new file mode 100644 index 0000000..a33eadf --- /dev/null +++ b/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/expect.json @@ -0,0 +1,21 @@ +{ + "rules": [ + "F-Rule-Equivalence" + ], + "project": true, + "host": "shapes", + "verdicts": { + "buckets.ts": "fold", + "helpers.ts": "fold" + }, + "findings": { + "buckets.ts": [ + { + "rule": "SHAPES001", + "subject": "b", + "severity": "error" + } + ] + }, + "note": "Bucket props built three ways, a same-file arrow, a sibling-file function and a literal, and one finding either way. compareAdapters holds two implementations to the same findings from this source whichever path each took, folding or running; that is the property, and it is measured the day a second implementation exposes the rules hook." +} diff --git a/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/project/buckets.ts b/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/project/buckets.ts new file mode 100644 index 0000000..047b86c --- /dev/null +++ b/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/project/buckets.ts @@ -0,0 +1,8 @@ +import { Bucket } from "@tsad/shapes"; +import { named } from "./helpers"; + +const props = (name?: string) => (name ? { BucketName: name } : {}); + +export const a = new Bucket(props("a")); +export const b = new Bucket(props()); +export const c = new Bucket(named("c")); diff --git a/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/project/helpers.ts b/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/project/helpers.ts new file mode 100644 index 0000000..a48ac81 --- /dev/null +++ b/spec/fixtures/F-Rule-Equivalence/same-source-same-findings/project/helpers.ts @@ -0,0 +1,3 @@ +export function named(name: string) { + return { BucketName: name }; +} diff --git a/spec/fixtures/F-Rule-Input/a-rule-sees-the-folded-namespace/expect.json b/spec/fixtures/F-Rule-Input/a-rule-sees-the-folded-namespace/expect.json new file mode 100644 index 0000000..9f521d9 --- /dev/null +++ b/spec/fixtures/F-Rule-Input/a-rule-sees-the-folded-namespace/expect.json @@ -0,0 +1,21 @@ +{ + "rules": [ + "F-Rule-Input", + "F-Rule-Finding" + ], + "project": true, + "host": "shapes", + "verdicts": { + "buckets.ts": "fold" + }, + "findings": { + "buckets.ts": [ + { + "rule": "SHAPES001", + "subject": "bad", + "severity": "error" + } + ] + }, + "note": "The pre-synthesis rule reads final values: two Buckets, one without a BucketName. The finding names the export, the rule's id and the rule's own severity; its message is not compared, and no location is asked for because F-Obs-Provenance is optional." +} diff --git a/spec/fixtures/F-Rule-Input/a-rule-sees-the-folded-namespace/project/buckets.ts b/spec/fixtures/F-Rule-Input/a-rule-sees-the-folded-namespace/project/buckets.ts new file mode 100644 index 0000000..f906936 --- /dev/null +++ b/spec/fixtures/F-Rule-Input/a-rule-sees-the-folded-namespace/project/buckets.ts @@ -0,0 +1,4 @@ +import { Bucket } from "@tsad/shapes"; + +export const good = new Bucket({ BucketName: "logs" }); +export const bad = new Bucket({}); diff --git a/spec/fixtures/F-Rule-Phase/a-post-rule-reads-the-artifact/expect.json b/spec/fixtures/F-Rule-Phase/a-post-rule-reads-the-artifact/expect.json new file mode 100644 index 0000000..1e199c3 --- /dev/null +++ b/spec/fixtures/F-Rule-Phase/a-post-rule-reads-the-artifact/expect.json @@ -0,0 +1,21 @@ +{ + "rules": [ + "F-Rule-Phase", + "F-Rule-Finding" + ], + "project": true, + "host": "shapes", + "verdicts": { + "pairs.ts": "fold" + }, + "findings": { + "artifact": [ + { + "rule": "SHAPES002", + "subject": "missing: Bucket", + "severity": "warning" + } + ] + }, + "note": "No Bucket anywhere, so the post-synthesis rule, which reads the serialized artifact and not the namespace, reports the missing-resource form: the subject states what is absent because there is nothing to attach it to (L11.5). The pre-synthesis rule has nothing to say, and a finding it did not make must not appear." +} diff --git a/spec/fixtures/F-Rule-Phase/a-post-rule-reads-the-artifact/project/pairs.ts b/spec/fixtures/F-Rule-Phase/a-post-rule-reads-the-artifact/project/pairs.ts new file mode 100644 index 0000000..177498f --- /dev/null +++ b/spec/fixtures/F-Rule-Phase/a-post-rule-reads-the-artifact/project/pairs.ts @@ -0,0 +1,3 @@ +import { Pair } from "@tsad/shapes"; + +export const p = new Pair(1, 2); diff --git a/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/expect.json b/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/expect.json new file mode 100644 index 0000000..8528fa1 --- /dev/null +++ b/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/expect.json @@ -0,0 +1,21 @@ +{ + "rules": [ + "F-Rule-Pure" + ], + "project": true, + "host": "shapes", + "verdicts": { + "buckets.ts": "fold", + "clock.ts": "run" + }, + "findings": { + "buckets.ts": [ + { + "rule": "SHAPES001", + "subject": "bad", + "severity": "error" + } + ] + }, + "note": "The runner asks for each phase twice and requires the same findings both times; a rule that read a clock or the environment would fail there before any comparison. clock.ts runs, so it has no folded namespace and no rule sees it; the findings are the same whether or not it exists." +} diff --git a/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/project/buckets.ts b/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/project/buckets.ts new file mode 100644 index 0000000..f906936 --- /dev/null +++ b/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/project/buckets.ts @@ -0,0 +1,4 @@ +import { Bucket } from "@tsad/shapes"; + +export const good = new Bucket({ BucketName: "logs" }); +export const bad = new Bucket({}); diff --git a/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/project/clock.ts b/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/project/clock.ts new file mode 100644 index 0000000..b3af00a --- /dev/null +++ b/spec/fixtures/F-Rule-Pure/findings-are-a-function-of-the-input/project/clock.ts @@ -0,0 +1 @@ +export const now = Date.now(); diff --git a/spec/fixtures/F-Rule-Supply/the-host-names-the-rule/expect.json b/spec/fixtures/F-Rule-Supply/the-host-names-the-rule/expect.json new file mode 100644 index 0000000..fe4eb5e --- /dev/null +++ b/spec/fixtures/F-Rule-Supply/the-host-names-the-rule/expect.json @@ -0,0 +1,21 @@ +{ + "rules": [ + "F-Rule-Supply", + "F-Rule-Finding" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold" + }, + "findings": { + "app.ts": [ + { + "rule": "SHAPES001", + "subject": "store", + "severity": "error" + } + ] + }, + "note": "The shapes host supplies two rules by id and phase and no code; the code is the implementation's, and an implementation that carries no rule of that id reports the hook unavailable and the fixture is skipped visibly. Here a Bucket exists, so the post-synthesis rule is silent and only the host's pre-synthesis rule speaks, under the severity the host declared for it (L11.6)." +} diff --git a/spec/fixtures/F-Rule-Supply/the-host-names-the-rule/project/app.ts b/spec/fixtures/F-Rule-Supply/the-host-names-the-rule/project/app.ts new file mode 100644 index 0000000..6f04fa7 --- /dev/null +++ b/spec/fixtures/F-Rule-Supply/the-host-names-the-rule/project/app.ts @@ -0,0 +1,4 @@ +import { Bucket, Pair } from "@tsad/shapes"; + +export const pair = new Pair("x", "y"); +export const store = new Bucket({}); diff --git a/spec/fixtures/F-Val-Domain/every-case-is-a-value/expect.json b/spec/fixtures/F-Val-Domain/every-case-is-a-value/expect.json new file mode 100644 index 0000000..ea4e3c5 --- /dev/null +++ b/spec/fixtures/F-Val-Domain/every-case-is-a-value/expect.json @@ -0,0 +1,49 @@ +{ + "rules": [ + "F-Val-Domain", + "F-Val-Serializable" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "scalars": { + "s": "text", + "n": 1.5, + "b": false, + "z": null, + "u": "$undefined" + }, + "list": [ + 1, + "two", + [ + 3 + ], + { + "four": 4 + } + ], + "bucket": { + "entityType": "Bucket", + "lexicon": "shapes", + "props": { + "name": "logs" + }, + "attributes": {} + }, + "reference": { + "arn": { + "__attrRef": { + "entity": "bucket", + "attribute": "arn" + } + } + } + } + }, + "note": "The closed union's cases that can reach the namespace in the full profile: every scalar including undefined, arrays and objects of them, a resource revived to a live instance, and the one envelope that survives, an attribute reference. Nothing else is a value." +} diff --git a/spec/fixtures/F-Val-Domain/every-case-is-a-value/project/app.ts b/spec/fixtures/F-Val-Domain/every-case-is-a-value/project/app.ts new file mode 100644 index 0000000..7b59bef --- /dev/null +++ b/spec/fixtures/F-Val-Domain/every-case-is-a-value/project/app.ts @@ -0,0 +1,6 @@ +import { Bucket } from "@tsad/shapes"; + +export const scalars = { s: "text", n: 1.5, b: false, z: null, u: undefined }; +export const list = [1, "two", [3], { four: 4 }]; +export const bucket = new Bucket({ name: "logs" }); +export const reference = { arn: bucket.arn }; diff --git a/spec/fixtures/F-Val-Envelope/envelopes-are-the-output-in-data-host/expect.json b/spec/fixtures/F-Val-Envelope/envelopes-are-the-output-in-data-host/expect.json new file mode 100644 index 0000000..f2d50fa --- /dev/null +++ b/spec/fixtures/F-Val-Envelope/envelopes-are-the-output-in-data-host/expect.json @@ -0,0 +1,41 @@ +{ + "rules": [ + "F-Val-Envelope", + "F-Profile-DataHost" + ], + "project": true, + "host": "shapes", + "profiles": [ + "data-host" + ], + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "tagged": { + "__intrinsic": "join", + "strings": [ + "bucket-", + "" + ], + "values": [ + 1 + ] + }, + "called": { + "__intrinsic": "ref", + "args": [ + "Logs" + ] + }, + "resource": { + "__resource": "Bucket", + "props": { + "name": "logs" + } + } + } + }, + "note": "Three envelopes, recognised by key, as the data-host profile emits them: no fate runs, the envelope is the output. Each is a finished value that denotes something not yet constructed, never a thunk. A helper call is not here because the profile has no helpers: there is nothing to invoke." +} diff --git a/spec/fixtures/F-Val-Envelope/envelopes-are-the-output-in-data-host/project/app.ts b/spec/fixtures/F-Val-Envelope/envelopes-are-the-output-in-data-host/project/app.ts new file mode 100644 index 0000000..cd39044 --- /dev/null +++ b/spec/fixtures/F-Val-Envelope/envelopes-are-the-output-in-data-host/project/app.ts @@ -0,0 +1,5 @@ +import { Bucket, join, ref } from "@tsad/shapes"; + +export const tagged = join`bucket-${1}`; +export const called = ref("Logs"); +export const resource = new Bucket({ name: "logs" }); diff --git a/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/expect.json b/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/expect.json new file mode 100644 index 0000000..aeecfbf --- /dev/null +++ b/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/expect.json @@ -0,0 +1,32 @@ +{ + "rules": [ + "F-Val-Symbol-Scope", + "F-Eval-Interior" + ], + "project": true, + "host": "shapes", + "profiles": [ + "data-host" + ], + "verdicts": { + "interior.ts": "fold", + "top.ts": "run" + }, + "exports": { + "interior.ts": { + "inside": { + "__intrinsic": "join", + "strings": [ + "arn:", + ":logs" + ], + "values": [ + { + "__symbol": "Region.Name" + } + ] + } + } + }, + "note": "The same unresolved chain twice. Inside a tag's interpolation it folds to a __symbol envelope, which the intrinsic will resolve; at the top of an expression it is a located rejection and the file runs. A __symbol never appears at the top of a folded tree." +} diff --git a/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/project/interior.ts b/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/project/interior.ts new file mode 100644 index 0000000..77b8cf4 --- /dev/null +++ b/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/project/interior.ts @@ -0,0 +1,3 @@ +import { join } from "@tsad/shapes"; + +export const inside = join`arn:${Region.Name}:logs`; diff --git a/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/project/top.ts b/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/project/top.ts new file mode 100644 index 0000000..a0cc024 --- /dev/null +++ b/spec/fixtures/F-Val-Symbol-Scope/a-symbol-only-inside-an-intrinsic/project/top.ts @@ -0,0 +1 @@ +export const top = Region.Name; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/expect.json b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/expect.json new file mode 100644 index 0000000..c2e86c5 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/expect.json @@ -0,0 +1,34 @@ +{ + "rules": [ + "S-Disqualify", + "F-Scan" + ], + "project": true, + "verdicts": { + "ok.ts": "fold", + "let.ts": "run", + "var.ts": "run", + "class.ts": "run", + "star.ts": "run", + "uninitialized.ts": "run", + "array.ts": "run", + "rest.ts": "run", + "default-function.ts": "run" + }, + "rejectRule": { + "let.ts": "F-Scan", + "var.ts": "F-Scan", + "class.ts": "F-Scan", + "star.ts": "F-Scan", + "uninitialized.ts": "F-Scan", + "array.ts": "F-Scan", + "rest.ts": "F-Scan", + "default-function.ts": "F-Scan" + }, + "exports": { + "ok.ts": { + "fine": 1 + } + }, + "note": "One file per disqualifying statement form, and one that folds. A single match rejects the whole file at F-Scan before any expression is judged; export default with an expression is the arm S-ExportDefault admits in data-host, so it is not here." +} diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/array.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/array.ts new file mode 100644 index 0000000..64d45c9 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/array.ts @@ -0,0 +1 @@ +export const [x] = [1]; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/class.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/class.ts new file mode 100644 index 0000000..a247b44 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/class.ts @@ -0,0 +1 @@ +export class X {} diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/default-function.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/default-function.ts new file mode 100644 index 0000000..5d33520 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/default-function.ts @@ -0,0 +1,3 @@ +export default function x() { + return 1; +} diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/let.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/let.ts new file mode 100644 index 0000000..6dab971 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/let.ts @@ -0,0 +1 @@ +export let x = 1; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/ok.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/ok.ts new file mode 100644 index 0000000..4aa956b --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/ok.ts @@ -0,0 +1 @@ +export const fine = 1; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/rest.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/rest.ts new file mode 100644 index 0000000..e9a8cc2 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/rest.ts @@ -0,0 +1 @@ +export const { ...rest } = { a: 1 }; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/star.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/star.ts new file mode 100644 index 0000000..5931ab2 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/star.ts @@ -0,0 +1 @@ +export * from "./let"; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/uninitialized.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/uninitialized.ts new file mode 100644 index 0000000..226fa6a --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/uninitialized.ts @@ -0,0 +1 @@ +export const x: number; diff --git a/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/var.ts b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/var.ts new file mode 100644 index 0000000..35ba806 --- /dev/null +++ b/spec/fixtures/S-Disqualify/each-arm-disqualifies-the-file/project/var.ts @@ -0,0 +1 @@ +export var x = 1; diff --git a/spec/fixtures/S-ExportDestructure/plain-elements/expect.json b/spec/fixtures/S-ExportDestructure/plain-elements/expect.json new file mode 100644 index 0000000..a233592 --- /dev/null +++ b/spec/fixtures/S-ExportDestructure/plain-elements/expect.json @@ -0,0 +1,16 @@ +{ + "rules": [ + "S-ExportDestructure" + ], + "project": true, + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "port": 8080, + "hostname": "localhost" + } + }, + "note": "A destructured export with plain elements only, one of them renamed. Each element becomes an export bound to the property of the folded initializer; the property nobody named is not exported." +} diff --git a/spec/fixtures/S-ExportDestructure/plain-elements/project/app.ts b/spec/fixtures/S-ExportDestructure/plain-elements/project/app.ts new file mode 100644 index 0000000..25cb160 --- /dev/null +++ b/spec/fixtures/S-ExportDestructure/plain-elements/project/app.ts @@ -0,0 +1,3 @@ +const config = { port: 8080, host: "localhost", debug: false }; + +export const { port, host: hostname } = config; diff --git a/spec/fixtures/S-ExportNamed/local-names-with-an-alias/expect.json b/spec/fixtures/S-ExportNamed/local-names-with-an-alias/expect.json new file mode 100644 index 0000000..047b6d2 --- /dev/null +++ b/spec/fixtures/S-ExportNamed/local-names-with-an-alias/expect.json @@ -0,0 +1,19 @@ +{ + "rules": [ + "S-ExportNamed", + "S-TopConst" + ], + "project": true, + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "a": 1, + "c": { + "two": 2 + } + } + }, + "note": "An export list over local names, one aliased. Each element exports the binding S-TopConst made under the name the list gives it." +} diff --git a/spec/fixtures/S-ExportNamed/local-names-with-an-alias/project/app.ts b/spec/fixtures/S-ExportNamed/local-names-with-an-alias/project/app.ts new file mode 100644 index 0000000..7eb236c --- /dev/null +++ b/spec/fixtures/S-ExportNamed/local-names-with-an-alias/project/app.ts @@ -0,0 +1,4 @@ +const a = 1; +const b = { two: 2 }; + +export { a, b as c }; diff --git a/spec/fixtures/S-ExportResource/new-with-props/expect.json b/spec/fixtures/S-ExportResource/new-with-props/expect.json new file mode 100644 index 0000000..c135519 --- /dev/null +++ b/spec/fixtures/S-ExportResource/new-with-props/expect.json @@ -0,0 +1,26 @@ +{ + "rules": [ + "S-ExportResource", + "F-Val-Fate" + ], + "project": true, + "host": "shapes", + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "logs": { + "entityType": "Bucket", + "lexicon": "shapes", + "props": { + "BucketName": "logs" + }, + "attributes": { + "region": "eu" + } + } + } + }, + "note": "The resource declarator: export const, an identifier, a new expression with arguments. J1 yields a __resource envelope and revival constructs the class the file imported from props and attributes." +} diff --git a/spec/fixtures/S-ExportResource/new-with-props/project/app.ts b/spec/fixtures/S-ExportResource/new-with-props/project/app.ts new file mode 100644 index 0000000..96390bc --- /dev/null +++ b/spec/fixtures/S-ExportResource/new-with-props/project/app.ts @@ -0,0 +1,3 @@ +import { Bucket } from "@tsad/shapes"; + +export const logs = new Bucket({ BucketName: "logs" }, { region: "eu" }); diff --git a/spec/fixtures/S-ExportSingle/an-expression-initializer/expect.json b/spec/fixtures/S-ExportSingle/an-expression-initializer/expect.json new file mode 100644 index 0000000..0c56db0 --- /dev/null +++ b/spec/fixtures/S-ExportSingle/an-expression-initializer/expect.json @@ -0,0 +1,21 @@ +{ + "rules": [ + "S-ExportSingle", + "S-Object" + ], + "export": "x", + "shape": "accept", + "fold": "fold", + "value": { + "a": 1, + "b": [ + true, + null, + "s" + ], + "c": { + "d": 2 + } + }, + "note": "The plain declarator: export const, one identifier, one expression. The initializer is judged by the expression grammar and folds to its value." +} diff --git a/spec/fixtures/S-ExportSingle/an-expression-initializer/input.ts b/spec/fixtures/S-ExportSingle/an-expression-initializer/input.ts new file mode 100644 index 0000000..2457bbd --- /dev/null +++ b/spec/fixtures/S-ExportSingle/an-expression-initializer/input.ts @@ -0,0 +1 @@ +export const x = { a: 1, b: [true, null, "s"], c: { d: 2 } }; diff --git a/spec/fixtures/S-ExportTypeOnly/type-exports-are-invisible/expect.json b/spec/fixtures/S-ExportTypeOnly/type-exports-are-invisible/expect.json new file mode 100644 index 0000000..e399ca9 --- /dev/null +++ b/spec/fixtures/S-ExportTypeOnly/type-exports-are-invisible/expect.json @@ -0,0 +1,19 @@ +{ + "rules": [ + "S-ExportTypeOnly", + "S-ExportNamed" + ], + "project": true, + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "square": { + "sides": 4 + }, + "x": 4 + } + }, + "note": "export type { }, an exported type alias and a type-only element of an export list are erased; the gate neither admits nor rejects them. What remains, a value export list and a const, folds." +} diff --git a/spec/fixtures/S-ExportTypeOnly/type-exports-are-invisible/project/app.ts b/spec/fixtures/S-ExportTypeOnly/type-exports-are-invisible/project/app.ts new file mode 100644 index 0000000..4b291f2 --- /dev/null +++ b/spec/fixtures/S-ExportTypeOnly/type-exports-are-invisible/project/app.ts @@ -0,0 +1,12 @@ +type Shape = { sides: number }; +interface Named { + name: string; +} + +export type { Shape }; +export type Alias = Named; + +const square: Shape = { sides: 4 }; + +export { square, type Named }; +export const x = square.sides; diff --git a/spec/fixtures/S-Module/non-exported-statements-are-invisible/expect.json b/spec/fixtures/S-Module/non-exported-statements-are-invisible/expect.json new file mode 100644 index 0000000..e63da11 --- /dev/null +++ b/spec/fixtures/S-Module/non-exported-statements-are-invisible/expect.json @@ -0,0 +1,17 @@ +{ + "rules": [ + "S-Module" + ], + "project": true, + "verdicts": { + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "x": { + "answer": 42 + } + } + }, + "note": "The gate examines only statements carrying export. A non-exported let, an assignment, an if, a class and a bare call are neither admitted nor disqualifiers; they are invisible and their effects are never performed, so the file folds on its one export." +} diff --git a/spec/fixtures/S-Module/non-exported-statements-are-invisible/project/app.ts b/spec/fixtures/S-Module/non-exported-statements-are-invisible/project/app.ts new file mode 100644 index 0000000..d2e7eac --- /dev/null +++ b/spec/fixtures/S-Module/non-exported-statements-are-invisible/project/app.ts @@ -0,0 +1,13 @@ +let counter = 0; +counter += 1; +if (counter > 0) { + counter = 2; +} +class Hidden { + run() { + return counter; + } +} +console.log(new Hidden().run()); + +export const x = { answer: 42 }; diff --git a/spec/fixtures/S-Prop/a-computed-key-rejects/expect.json b/spec/fixtures/S-Prop/a-computed-key-rejects/expect.json new file mode 100644 index 0000000..b1d16f0 --- /dev/null +++ b/spec/fixtures/S-Prop/a-computed-key-rejects/expect.json @@ -0,0 +1,10 @@ +{ + "rules": [ + "S-Prop", + "S-Reject" + ], + "export": "x", + "shape": "reject", + "fold": "run", + "note": "A property key must be a literal key: an identifier, a string or a number. A computed key is a shape rejection even when it would evaluate to a constant." +} diff --git a/spec/fixtures/S-Prop/a-computed-key-rejects/input.ts b/spec/fixtures/S-Prop/a-computed-key-rejects/input.ts new file mode 100644 index 0000000..cdc33d9 --- /dev/null +++ b/spec/fixtures/S-Prop/a-computed-key-rejects/input.ts @@ -0,0 +1,3 @@ +const k = "a"; + +export const x = { [k]: 1 }; diff --git a/spec/fixtures/S-Prop/literal-keys/expect.json b/spec/fixtures/S-Prop/literal-keys/expect.json new file mode 100644 index 0000000..7d7bdbe --- /dev/null +++ b/spec/fixtures/S-Prop/literal-keys/expect.json @@ -0,0 +1,15 @@ +{ + "rules": [ + "S-Prop", + "S-Object" + ], + "export": "x", + "shape": "accept", + "fold": "fold", + "value": { + "plain": 1, + "with-dash": 2, + "3": "three" + }, + "note": "The three literal key forms, each with an expression value; the numeric key folds to the string key JavaScript gives it." +} diff --git a/spec/fixtures/S-Prop/literal-keys/input.ts b/spec/fixtures/S-Prop/literal-keys/input.ts new file mode 100644 index 0000000..628a3b1 --- /dev/null +++ b/spec/fixtures/S-Prop/literal-keys/input.ts @@ -0,0 +1 @@ +export const x = { plain: 1, "with-dash": 2, 3: "three" }; diff --git a/spec/fixtures/S-ReExport/named-from-a-sibling/expect.json b/spec/fixtures/S-ReExport/named-from-a-sibling/expect.json new file mode 100644 index 0000000..caa875a --- /dev/null +++ b/spec/fixtures/S-ReExport/named-from-a-sibling/expect.json @@ -0,0 +1,18 @@ +{ + "rules": [ + "S-ReExport", + "F-Import" + ], + "project": true, + "verdicts": { + "config.ts": "fold", + "app.ts": "fold" + }, + "exports": { + "app.ts": { + "listenOn": 8080, + "host": "localhost" + } + }, + "note": "A re-export list with a specifier: each element exports the sibling's folded value under the name given, and nothing in this file is evaluated. The values are scalars, so no capture edge is recorded." +} diff --git a/spec/fixtures/S-ReExport/named-from-a-sibling/project/app.ts b/spec/fixtures/S-ReExport/named-from-a-sibling/project/app.ts new file mode 100644 index 0000000..7ea92f8 --- /dev/null +++ b/spec/fixtures/S-ReExport/named-from-a-sibling/project/app.ts @@ -0,0 +1 @@ +export { port as listenOn, host } from "./config"; diff --git a/spec/fixtures/S-ReExport/named-from-a-sibling/project/config.ts b/spec/fixtures/S-ReExport/named-from-a-sibling/project/config.ts new file mode 100644 index 0000000..c6bf457 --- /dev/null +++ b/spec/fixtures/S-ReExport/named-from-a-sibling/project/config.ts @@ -0,0 +1,2 @@ +export const port = 8080; +export const host = "localhost"; diff --git a/spec/fixtures/S-Shorthand/a-shorthand-property-resolves-as-an-identifier/expect.json b/spec/fixtures/S-Shorthand/a-shorthand-property-resolves-as-an-identifier/expect.json new file mode 100644 index 0000000..4b732dd --- /dev/null +++ b/spec/fixtures/S-Shorthand/a-shorthand-property-resolves-as-an-identifier/expect.json @@ -0,0 +1,14 @@ +{ + "rules": [ + "S-Shorthand", + "S-Ident" + ], + "export": "x", + "shape": "accept", + "fold": "fold", + "value": { + "name": "svc", + "port": 80 + }, + "note": "A shorthand property is always shape-valid; its name resolves the way an identifier does, here to two top-level consts." +} diff --git a/spec/fixtures/S-Shorthand/a-shorthand-property-resolves-as-an-identifier/input.ts b/spec/fixtures/S-Shorthand/a-shorthand-property-resolves-as-an-identifier/input.ts new file mode 100644 index 0000000..d731ab2 --- /dev/null +++ b/spec/fixtures/S-Shorthand/a-shorthand-property-resolves-as-an-identifier/input.ts @@ -0,0 +1,4 @@ +const name = "svc"; +const port = 80; + +export const x = { name, port }; diff --git a/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/expect.json b/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/expect.json new file mode 100644 index 0000000..f11188f --- /dev/null +++ b/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/expect.json @@ -0,0 +1,16 @@ +{ + "rules": [ + "S-TopConst" + ], + "project": true, + "verdicts": { + "binding.ts": "fold", + "invisible.ts": "run" + }, + "exports": { + "binding.ts": { + "x": 42 + } + }, + "note": "A top-level const with an identifier and an initializer binds its name whether or not it is exported, so binding.ts folds through base. A destructured top-level const is not a binding: q is invisible, and invisible.ts runs because y reads a name nothing bound." +} diff --git a/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/project/binding.ts b/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/project/binding.ts new file mode 100644 index 0000000..a3f3ba6 --- /dev/null +++ b/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/project/binding.ts @@ -0,0 +1,3 @@ +const base = 21; + +export const x = base * 2; diff --git a/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/project/invisible.ts b/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/project/invisible.ts new file mode 100644 index 0000000..5862430 --- /dev/null +++ b/spec/fixtures/S-TopConst/an-unexported-const-is-a-binding/project/invisible.ts @@ -0,0 +1,3 @@ +const { q } = { q: 1 }; + +export const y = q; diff --git a/spec/fixtures/UNCOVERED.md b/spec/fixtures/UNCOVERED.md index 8eea5e3..18e3429 100644 --- a/spec/fixtures/UNCOVERED.md +++ b/spec/fixtures/UNCOVERED.md @@ -2,16 +2,9 @@ Deliberate, and this list may only shrink (#8). Each entry names the rule and why it is still uncovered. Remove an entry when its fixture lands; the gate fails if an entry is stale. -107 of 138 rules have fixtures. Most of the rules below are uncovered because the corpus is still growing (#24); where a rule needs something the reference implementation does not have, the entry says so. +128 of 138 rules have fixtures. Every rule below needs something the reference implementation does not have, or something no adapter can observe; the entry says which. -- `F-Rule-Input` — the harness has no rule hook and no findings-as-data fixture kind yet (#101) -- `F-Rule-Phase` — the harness has no rule hook and no findings-as-data fixture kind yet (#101) -- `F-Rule-Pure` — the harness has no rule hook and no findings-as-data fixture kind yet (#101) -- `F-Rule-Finding` — the harness has no rule hook and no findings-as-data fixture kind yet (#101) -- `F-Rule-Equivalence` — the harness has no rule hook and no findings-as-data fixture kind yet (#101) -- `F-Rule-Supply` — the harness has no rule hook and no findings-as-data fixture kind yet (#101) - `F-Div-Step` — needs `{__compositeStep}`, which has no fate in the reference implementation (no composite factory form) -- `F-Eval-Interior` — an unresolved chain inside an intrinsic call folds to a symbol only with a registry in scope, and an expression fixture names no host - `F-Host-Composite` — needs the composite registration form `Composite(fn, "N")`, which the reference implementation does not have - `F-Host-Generality` — says what a host may vary and what it may not; no verdict exercises it - `F-IsolatedRefusal` — needs isolation mode, which the reference implementation does not have @@ -19,19 +12,5 @@ Deliberate, and this list may only shrink (#8). Each entry names the rule and wh - `F-NotProject` — a file inside the host's own module tree; the reference's project is a map with no outside - `F-Obs-Counters` — the conformance adapter exposes no counters - `F-Obs-Provenance` — an optional capability, reported rather than asserted -- `F-Val-Domain` — no fixture yet; corpus is #24's -- `F-Val-Envelope` — no fixture yet; corpus is #24's -- `F-Val-Symbol-Scope` — no fixture yet; corpus is #24's -- `S-Disqualify` — no fixture yet; corpus is #24's -- `S-ExportDestructure` — no fixture yet; corpus is #24's -- `S-ExportNamed` — no fixture yet; corpus is #24's -- `S-ExportResource` — no fixture yet; corpus is #24's -- `S-ExportSingle` — no fixture yet; corpus is #24's -- `S-ExportTypeOnly` — no fixture yet; corpus is #24's - `S-FactoryBody` — needs the composite factory form, which the reference implementation does not have - `S-FactoryParams` — needs the composite factory form, which the reference implementation does not have -- `S-Module` — no fixture yet; corpus is #24's -- `S-Prop` — no fixture yet; corpus is #24's -- `S-ReExport` — no fixture yet; corpus is #24's -- `S-Shorthand` — no fixture yet; corpus is #24's -- `S-TopConst` — no fixture yet; corpus is #24's diff --git a/spec/rules.md b/spec/rules.md index a67e0db..d9d6b1d 100644 --- a/spec/rules.md +++ b/spec/rules.md @@ -96,7 +96,10 @@ refuses to load one into its own process while the sandbox is armed and runs it in the child instead (chant#1131), which is the isolation mode's boundary drawn once more around rules. -**Fixtures.** No `F-Rule-*` rule has a fixture yet: the harness has no hook -that runs a rule and no fixture kind whose expectation is a finding. That -work is #101; until it lands the family is listed in `fixtures/UNCOVERED.md` -with that reason. +The fixtures for this family carry the finding as data and not the rule, +since a rule is host code (#101): the `shapes` host names two rules by id and +phase, a fixture's `findings` says what they report, and an implementation +that carries no rule of that id says so and is skipped visibly. The runner +asks for each phase twice and holds the two runs to the same answer, which +is F-Rule-Pure tested before anything else, and `compareAdapters` holds two +implementations to the same findings, which is F-Rule-Equivalence.