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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/scripts/sync-spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const BOLD_ID = /^\*\*((?:[SF]-[A-Za-z0-9-]+))[.\s]/;
// listed ahead of its arrival (#18): spec/CHANGELOG.md does not exist yet, and
// naming it here is inert until it does, because the loop below iterates the
// directory rather than this list.
const ORDER = ["README", "grammar", "judgments", "values", "divergence", "hosts", "inventory", "CHANGELOG", "prior-art"];
const ORDER = ["README", "grammar", "judgments", "values", "divergence", "hosts", "rules", "inventory", "CHANGELOG", "prior-art"];

for (const file of readdirSync(specDir).filter((f) => f.endsWith(".md"))) {
const name = basename(file, ".md");
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/conformance/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@intentius/tsad-conformance",
"version": "1.3.0",
"version": "1.4.0",
"description": "The conformance suite of the typescript-as-data specification: fixture loader, runner, and the named hosts. An implementation supplies an adapter and gets a report.",
"license": "Apache-2.0",
"repository": {
Expand Down Expand Up @@ -28,7 +28,7 @@
"spec/"
],
"scripts": {
"build": "rm -rf dist fixtures spec && tsc -p tsconfig.build.json && cp -R ../../spec/fixtures fixtures && mkdir spec && cp ../../spec/VERSION ../../spec/grammar.md ../../spec/judgments.md ../../spec/values.md ../../spec/divergence.md ../../spec/hosts.md spec/",
"build": "rm -rf dist fixtures spec && tsc -p tsconfig.build.json && cp -R ../../spec/fixtures fixtures && mkdir spec && cp ../../spec/VERSION ../../spec/grammar.md ../../spec/judgments.md ../../spec/values.md ../../spec/divergence.md ../../spec/hosts.md ../../spec/rules.md spec/",
"prepack": "npm run build"
},
"dependencies": {}
Expand Down
2 changes: 1 addition & 1 deletion packages/conformance/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";

/** The five files that define rules, in reading order. The same list `spec/fixtures.test.ts` gates on. */
export const RULE_FILES = ["grammar.md", "judgments.md", "values.md", "divergence.md", "hosts.md"] as const;
export const RULE_FILES = ["grammar.md", "judgments.md", "values.md", "divergence.md", "hosts.md", "rules.md"] as const;

export interface Rule {
readonly id: string;
Expand Down
4 changes: 4 additions & 0 deletions packages/reference/CAVEATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ 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

`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).

## No filesystem, no module resolution algorithm

`foldProject` takes a map of path to source. Specifier resolution joins the
Expand Down
4 changes: 2 additions & 2 deletions packages/reference/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@intentius/tsad-reference",
"version": "1.3.0",
"version": "1.4.0",
"description": "The reference implementation of the typescript-as-data specification, written from the specification text and nothing else.",
"license": "Apache-2.0",
"repository": {
Expand Down Expand Up @@ -31,7 +31,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@intentius/tsad-conformance": "^1.3.0",
"@intentius/tsad-conformance": "^1.4.0",
"typescript": "^5.9.0"
}
}
2 changes: 1 addition & 1 deletion packages/reference/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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: "1.3",
specVersion: "1.4",
shape(source, exportName) {
const v = shapeOfExport(source, exportName, { ...EMPTY_HOST, profile });
if (v === "no-such-export") return { accepted: false, line: 1, column: 1, message: `no export named ${exportName}` };
Expand Down
2 changes: 1 addition & 1 deletion paper/enables.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A file in the subset becomes its artifact by being read. Nothing runs, so the ar

## Semantic rules over values

A syntax linter sees tokens and can say a key is misspelt. A configuration language with constraints in the type can say a port is out of range. Neither can say that two fields of one resource contradict each other, or that a resource in one file makes a resource in another file incoherent, because neither has the values of every file in the build before anything is emitted. The fold does. A rule that runs over folded values is a pure function of its input, deterministic and free of I/O, which is what lets it run in an editor, and it sees the same data in the editor that it sees in CI, because the data is a function of the source. Two phases follow from this: rules over the declared values before serialization, and rules over the emitted artifact after. chant's lexicons carry the first kind and its post-synth checks the second; a governance tool built on the same evaluator carries both without naming them, in its config loader and in its plan guardrails. The contract those rules run under is not yet in the specification and is stated here as a forward reference (typescript-as-data#79).
A syntax linter sees tokens and can say a key is misspelt. A configuration language with constraints in the type can say a port is out of range. Neither can say that two fields of one resource contradict each other, and neither can say that a resource in one file makes a resource in another file incoherent. They lack the values of every file in the build before anything is emitted. The fold does. A rule that runs over folded values is a pure function of its input, deterministic and free of I/O, which is what lets it run in an editor, and it sees the same data in the editor that it sees in CI, because the data is a function of the source. Two phases follow from this: rules over the declared values before serialization, and rules over the emitted artifact after. chant's lexicons carry the first kind and its post-synth checks the second; a governance tool built on the same evaluator carries both without naming them, in its config loader and in its plan guardrails. The contract those rules run under is `rules.md` (spec `1.4`). It names the input a check sees and when it runs, what a check may do and what a finding carries, and who supplies it. Fixtures for it wait on a harness hook (typescript-as-data#101).

## Round-trip generation

Expand Down
4 changes: 2 additions & 2 deletions scripts/docs-sentences-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"spec/judgments.md": {
"discourse/punchy-fragments": 45,
"claude/colon-reveal": 21,
"repetition/near-duplicate": 19,
"anaphora/repeated-opening": 11,
"repetition/near-duplicate": 17,
"anaphora/repeated-opening": 10,
"tricolon/comma-series": 12,
"tricolon/density": 1
},
Expand Down
12 changes: 11 additions & 1 deletion scripts/lint-docs-sentences.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ function docFiles(dir) {
return out.sort();
}

/** The frontmatter block replaced by spaces, so every offset after it is unchanged. */
function blankFrontmatter(text) {
const m = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(text);
return m ? m[0].replace(/[^\n]/g, " ") + text.slice(m[0].length) : text;
}

/** 1-based line of a prose-text offset — extractProse blanks non-prose but
* preserves every offset, so spans map straight onto the source file. */
function lineOf(text, offset) {
Expand All @@ -73,7 +79,11 @@ const detail = [];
for (const file of files) {
const rel = relative(join(here, ".."), file);
const text = readFileSync(file, "utf8");
const prose = extractProse(text);
// YAML frontmatter is metadata, not prose. `extractProse` blanks fences,
// tables and inline code but leaves the `---` block, whose delimiters read
// as em dashes and whose `key: value` lines read as colon nameplates; every
// page was paying for its own frontmatter. Blank it, offsets preserved.
const prose = extractProse(blankFrontmatter(text));
const doc = buildDocAnalysis(prose);
const { findings, errors } = runRules(RULES, doc);
for (const e of errors) detail.push(`${rel}: rule ${e.ruleId} errored: ${e.message}`);
Expand Down
6 changes: 5 additions & 1 deletion spec/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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.

## 1.4, 2026-09-12

Tag `spec-1.4`. One addition, `rules.md`, which is the `F-Rule-*` family (#79) and the contract a semantic rule runs under. It is extracted from chant's post-synthesis engine and policy layer the way the other files were, with inventory rows L11.1 to L11.8, and it adds item 7 to `F-Host-Interface`. A finding's subject is an artifact-side name and a source location is optional, because that is what chant does and what `F-Obs-Provenance` already allows. No fixture exercises the family yet; the harness half is #101.

## 1.3, 2026-09-12

Tag `spec-1.3`. One widening and one clarification, the two open questions the corpus and the fixtures left.
Expand All @@ -16,7 +20,7 @@ The second change is a clarification of `F-Eval-Ident` step 3 and `F-Import`, wi

Tag `spec-1.2`. Two widenings, both found by the first consumer of the published packages (forgejo-warden#33) in the first policy file anybody wrote.

The added rule `S-LocalFunction` (#95) binds a top-level `function` declaration, exported or not, and a `const` bound to an arrow or function expression, as a project-local function in the file's own scope; `F-Bind` says so and `F-Eval-CallLocal` folds the call. chant folded both forms already and the text said so for neither, so this is the specification catching up with an implementation. The reference now does the same. The classifier's half is `S-CallLocal`: a call whose callee this file binds, by `S-LocalFunction` or by an import from a project specifier, is shape-valid; chant's classifier rejects every such call today, the direction `F-Direction` forbids, and chant#2435 tracks it.
The added rule `S-LocalFunction` (#95) binds a top-level `function` declaration, exported or not, and a `const` bound to an arrow or function expression, as a project-local function in the file's own scope; `F-Bind` says so and `F-Eval-CallLocal` folds the call. chant folded both forms already and the text said so for neither, so this is the specification catching up with an implementation. The reference now does the same. The classifier's half is `S-CallLocal`, under which a call whose callee this file binds, by `S-LocalFunction` or by an import from a project specifier, is shape-valid; chant's classifier rejects every such call today, the direction `F-Direction` forbids, and chant#2435 tracks it.

The added rule `S-ExportDefault` (#94) admits `export default ⟨Expr⟩` as the declarator named `default` in the `data-host` profile, with `F-Declarator` and `F-Import` extended for it. In `full` it stays a disqualifier and the profile table records it as permitted, not required, until chant admits it.

Expand Down
4 changes: 3 additions & 1 deletion spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ The normative files, in reading order: [`grammar.md`](./grammar.md) (`S-*`),
[`judgments.md`](./judgments.md) (`F-*`: J1 evaluation, J2 verdict, J3 taint,
J4 properties and observables), [`values.md`](./values.md) (`F-Val-*`),
[`divergence.md`](./divergence.md) (`F-Direction`, `F-Div-*`, `F-Exc-*`),
[`hosts.md`](./hosts.md) (`F-Host-*`). Each ends with a non-normative
[`hosts.md`](./hosts.md) (`F-Host-*`), [`rules.md`](./rules.md)
(`F-Rule-*`). Each ends with a non-normative
Rationale section. [`inventory.md`](./inventory.md) is the coverage ledger.
Every decision point in chant core cites the rule that governs it, and #44
gates on it. [`prior-art.md`](./prior-art.md) is the #31 finding.
Expand Down Expand Up @@ -194,6 +195,7 @@ part of the meaning:
| `F-Val-` | values.md |
| `F-Div-`, `F-Exc-`, `F-Direction` | divergence.md |
| `F-Host-` | hosts.md |
| `F-Rule-` | rules.md |

Names are `Prefix-CamelWords`, no digits in the name part, specific enough to
read alone (`F-Eval-Member`, not `F-Eval-3`). A rule with numbered steps is
Expand Down
2 changes: 1 addition & 1 deletion spec/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3
1.4
2 changes: 1 addition & 1 deletion spec/coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const specDir = dirname(fileURLToPath(import.meta.url));
const RULE_FILES = ["grammar.md", "judgments.md", "values.md", "divergence.md", "hosts.md"];
const RULE_FILES = ["grammar.md", "judgments.md", "values.md", "divergence.md", "hosts.md", "rules.md"];
const ID = /\b([SF]-[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)\b/g;

function definedIds(): Set<string> {
Expand Down
6 changes: 6 additions & 0 deletions spec/fixtures/UNCOVERED.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Deliberate, and this list may only shrink (#8). Each entry names the rule and wh

107 of 132 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.

- `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
Expand Down
3 changes: 3 additions & 0 deletions spec/hosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ what the trust rules need.
6. **A composite registration form**, `export const N = Composite(fn, "N")`
with `Composite` imported from the host, that makes a project-defined
factory *interpretable* (R7.2).
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.

In the `data-host` profile (F-Profile-DataHost, judgments.md) a host is a
description rather than code: item 3, item 5, and a serialization mapping
Expand Down
8 changes: 8 additions & 0 deletions spec/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ module is never imported.
| L10.3 | per-file decision line | `[fold:fold]` / `[fold:run] <reason>`, summarized without `--verbose` | F-Obs-Report |
| L10.4 | `FoldError` | located, carries an EVL rule id, constructed with `stackTraceLimit = 0` | F-Reason |
| L10.5 | one wording per rejection kind | shared message builders so two sites cannot drift | F-Obs-Messages |
| L11.1 | `PostSynthContext.entities` (added #79) | every declared entity by name, the folded namespace after J3 | F-Rule-Input (rules.md) |
| L11.2 | `PostSynthContext.outputs` (added #79) | the serialized output per lexicon, text | F-Rule-Input (rules.md) |
| L11.3 | `PostSynthContext.docs` (added #79) | the outputs parsed once per build, cached (chant#975) | F-Rule-Input (rules.md) |
| L11.4 | `PostSynthContext.env` (added #79) | the environment or stack name, so a policy may branch on it | F-Rule-Input (rules.md) |
| L11.5 | `PostSynthDiagnostic` (added #79) | `checkId`, `severity`, `message`, an artifact-side `entity` or a missing-resource marker (chant#2113), never a source line | F-Rule-Finding (rules.md) |
| L11.6 | severity configuration (added #79) | `lint.config` overrides a check's own severity | F-Rule-Finding (rules.md) |
| L11.7 | project policies under `--sandbox` (added #79) | `loadPolicyChecks` refuses in-process while the sandbox is armed; checks run in the child (chant#1131) | F-Rule-Pure; F-Rule-Supply (rules.md) |
| L11.8 | rule registry (added #79) | lexicon checks and project policies keyed by id; a duplicate id is a registry error | F-Rule-Supply (rules.md) |

---

Expand Down
1 change: 1 addition & 0 deletions spec/judgments.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ another language embeds, or a JavaScript tool that folds and never runs. It is
| `S-ExportDefault` | applies: a default export is the declarator named `default`. In `full` it is permitted, not required, and chant does not yet admit it |
| `F-Eval-New`, `F-Prebuild`, `F-Count` | permitted, not required. An implementation that supports `new` folds it to a `{__resource}` envelope and binds the same envelope at every reference; one that does not rejects `new` under `F-Eval-Reject`. A fixture that uses `new` is tagged `full` unless it says otherwise |
| `F-Host-Interface` | the host is a description, not code: the intrinsic registry (item 3), the trust set (item 5), and the serialization mapping for envelopes. Items 1, 2, 4 and 6 are absent |
| `F-Rule-*` | apply; a rule is code in the evaluator's own language (F-Rule-Supply). A source location in a finding stays optional, since provenance is |
| `F-Obs-Counters` | trivially satisfied: every counter is zero |
| `F-NoOwnExecution`, `F-Obs-Report`, `F-Obs-Messages`, `F-Reason` | apply in full |

Expand Down
Loading