Skip to content

Commit 9cc66a0

Browse files
authored
spec 1.4: rules.md, the contract a semantic check runs under (F-Rule-*) (#79) (#102)
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 item 7 of F-Host-Interface. A check sees the folded namespace and the artifact, runs after every verdict is final, is a pure function of its input, reports a finding whose subject is an artifact-side name (a source location is optional, as provenance is), and comes from the host or the project. F-Rule-Equivalence is the objective restated for checks. No fixture yet: the harness half is #101, and the family sits in UNCOVERED.md with that reason. Version 1.4, packages 1.4.0. Also: the prose linter blanks YAML frontmatter before reading a file. The sentences package leaves the --- block in, so every docs page paid for its own frontmatter (the delimiters as em dashes, key: value as nameplates); the docs-prose pass (#100) hit that wall with zero body findings left. Baseline ratcheted down to match.
1 parent 2639276 commit 9cc66a0

19 files changed

Lines changed: 158 additions & 18 deletions

docs/scripts/sync-spec.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const BOLD_ID = /^\*\*((?:[SF]-[A-Za-z0-9-]+))[.\s]/;
3030
// listed ahead of its arrival (#18): spec/CHANGELOG.md does not exist yet, and
3131
// naming it here is inert until it does, because the loop below iterates the
3232
// directory rather than this list.
33-
const ORDER = ["README", "grammar", "judgments", "values", "divergence", "hosts", "inventory", "CHANGELOG", "prior-art"];
33+
const ORDER = ["README", "grammar", "judgments", "values", "divergence", "hosts", "rules", "inventory", "CHANGELOG", "prior-art"];
3434

3535
for (const file of readdirSync(specDir).filter((f) => f.endsWith(".md"))) {
3636
const name = basename(file, ".md");

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/conformance/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@intentius/tsad-conformance",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"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.",
55
"license": "Apache-2.0",
66
"repository": {
@@ -28,7 +28,7 @@
2828
"spec/"
2929
],
3030
"scripts": {
31-
"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/",
31+
"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/",
3232
"prepack": "npm run build"
3333
},
3434
"dependencies": {}

packages/conformance/src/rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { readFileSync } from "node:fs";
2929
import { join } from "node:path";
3030

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

3434
export interface Rule {
3535
readonly id: string;

packages/reference/CAVEATS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ chant as F-Import's text says; it now does both (#96). A project-local
7171
function is excluded at the import, since it is a callable rather than a
7272
value and F-CallLeak decides its edge at the call.
7373

74+
## No rules, and no provenance
75+
76+
`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).
77+
7478
## No filesystem, no module resolution algorithm
7579

7680
`foldProject` takes a map of path to source. Specifier resolution joins the

packages/reference/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@intentius/tsad-reference",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "The reference implementation of the typescript-as-data specification, written from the specification text and nothing else.",
55
"license": "Apache-2.0",
66
"repository": {
@@ -31,7 +31,7 @@
3131
"prepack": "npm run build"
3232
},
3333
"dependencies": {
34-
"@intentius/tsad-conformance": "^1.3.0",
34+
"@intentius/tsad-conformance": "^1.4.0",
3535
"typescript": "^5.9.0"
3636
}
3737
}

packages/reference/src/adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function adapterFor(profile: Profile): ConformanceAdapter {
2121
name: profile === "full" ? "reference" : `reference/${profile}`,
2222
// Bumped by hand when the rule set this package implements moves; the
2323
// conformance suite fails when it and spec/VERSION disagree (#18).
24-
specVersion: "1.3",
24+
specVersion: "1.4",
2525
shape(source, exportName) {
2626
const v = shapeOfExport(source, exportName, { ...EMPTY_HOST, profile });
2727
if (v === "no-such-export") return { accepted: false, line: 1, column: 1, message: `no export named ${exportName}` };

paper/enables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A file in the subset becomes its artifact by being read. Nothing runs, so the ar
88

99
## Semantic rules over values
1010

11-
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).
11+
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).
1212

1313
## Round-trip generation
1414

scripts/docs-sentences-baseline.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
"spec/judgments.md": {
3535
"discourse/punchy-fragments": 45,
3636
"claude/colon-reveal": 21,
37-
"repetition/near-duplicate": 19,
38-
"anaphora/repeated-opening": 11,
37+
"repetition/near-duplicate": 17,
38+
"anaphora/repeated-opening": 10,
3939
"tricolon/comma-series": 12,
4040
"tricolon/density": 1
4141
},

scripts/lint-docs-sentences.mjs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ function docFiles(dir) {
5858
return out.sort();
5959
}
6060

61+
/** The frontmatter block replaced by spaces, so every offset after it is unchanged. */
62+
function blankFrontmatter(text) {
63+
const m = /^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(text);
64+
return m ? m[0].replace(/[^\n]/g, " ") + text.slice(m[0].length) : text;
65+
}
66+
6167
/** 1-based line of a prose-text offset — extractProse blanks non-prose but
6268
* preserves every offset, so spans map straight onto the source file. */
6369
function lineOf(text, offset) {
@@ -73,7 +79,11 @@ const detail = [];
7379
for (const file of files) {
7480
const rel = relative(join(here, ".."), file);
7581
const text = readFileSync(file, "utf8");
76-
const prose = extractProse(text);
82+
// YAML frontmatter is metadata, not prose. `extractProse` blanks fences,
83+
// tables and inline code but leaves the `---` block, whose delimiters read
84+
// as em dashes and whose `key: value` lines read as colon nameplates; every
85+
// page was paying for its own frontmatter. Blank it, offsets preserved.
86+
const prose = extractProse(blankFrontmatter(text));
7787
const doc = buildDocAnalysis(prose);
7888
const { findings, errors } = runRules(RULES, doc);
7989
for (const e of errors) detail.push(`${rel}: rule ${e.ruleId} errored: ${e.message}`);

0 commit comments

Comments
 (0)