@texuli/spec 0.0.0-stage → 0.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -2
  3. package/dist/build.d.ts +47 -0
  4. package/dist/build.js +65 -0
  5. package/dist/canonical.d.ts +17 -0
  6. package/dist/canonical.js +131 -0
  7. package/dist/catalog.d.ts +41 -0
  8. package/dist/catalog.js +45 -0
  9. package/dist/codegen/index.d.ts +14 -0
  10. package/dist/codegen/index.js +13 -0
  11. package/dist/codegen/kotlin.d.ts +18 -0
  12. package/dist/codegen/kotlin.js +328 -0
  13. package/dist/codegen/shared.d.ts +35 -0
  14. package/dist/codegen/shared.js +69 -0
  15. package/dist/codegen/typescript.d.ts +10 -0
  16. package/dist/codegen/typescript.js +167 -0
  17. package/dist/conformance.d.ts +20 -0
  18. package/dist/conformance.js +124 -0
  19. package/dist/dialect.d.ts +56 -0
  20. package/dist/dialect.js +264 -0
  21. package/dist/format.d.ts +9 -0
  22. package/dist/format.js +48 -0
  23. package/dist/generated/typescript/blueprint.d.ts +125 -0
  24. package/dist/generated/typescript/blueprint.js +3 -0
  25. package/dist/generated/typescript/catalog-schema.d.ts +33 -0
  26. package/dist/generated/typescript/catalog-schema.js +3 -0
  27. package/dist/generated/typescript/diagnostics.d.ts +12 -0
  28. package/dist/generated/typescript/diagnostics.js +3 -0
  29. package/dist/generated/typescript/manifest.d.ts +9 -0
  30. package/dist/generated/typescript/manifest.js +3 -0
  31. package/dist/generated/typescript/vocabulary.d.ts +972 -0
  32. package/dist/generated/typescript/vocabulary.js +140 -0
  33. package/dist/index.d.ts +17 -0
  34. package/dist/index.js +13 -0
  35. package/dist/validate.d.ts +27 -0
  36. package/dist/validate.js +143 -0
  37. package/dist/version.d.ts +2 -0
  38. package/dist/version.js +2 -0
  39. package/dist/versioning.d.ts +39 -0
  40. package/dist/versioning.js +87 -0
  41. package/package.json +36 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dailybot, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # @texuli/spec
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The schema-first blueprint contract (spec/Compose design, plan B1): the
4
+ platform-neutral source of truth for blueprint structure, the catalog
5
+ vocabulary, capability tiers and diagnostics. TypeScript and Kotlin are
6
+ deterministic projections generated from it, and a conformance harness keeps
7
+ everything from drifting.
8
+
9
+ **Status: in development, not published.** Layer 0, no dependencies,
10
+ framework-free. Kotlin is generated as committed text by this package's
11
+ TypeScript generator; no Kotlin toolchain runs here, and nothing is compiled
12
+ or published to Maven.
13
+
14
+ The package tarball build compiles `generated/typescript/` into the matching
15
+ `dist/generated/typescript/` location. The source stays beside `src/` so the
16
+ spec generator and integrity manifest keep their established output paths;
17
+ the packer maps runtime and declaration imports to emitted `.js` and `.d.ts`
18
+ files there. Kotlin projections remain generated text and are not part of the
19
+ TypeScript package output.
20
+
21
+ ## Layout
22
+
23
+ | Path | What it holds |
24
+ | --- | --- |
25
+ | `schemas/` | The normative JSON schemas: `blueprint` (envelope v1 and the node tree), `catalog`, `diagnostics`, `manifest` |
26
+ | `catalog/components.json` | The closed vocabulary, projected from the built catalog: id, label, summary, group, placement (`app`/`content`), emit tiers, capability and props |
27
+ | `catalog/capabilities.json` | Reviewed capability claims. Empty: no id is audited portable yet, so every capability is `unknown` (ADR-003) |
28
+ | `fixtures/golden/` | Blueprints that must validate: a report, a dashboard, a proactive deck and a report with a named slot and a `when` condition (fictional data) |
29
+ | `fixtures/invalid/` | Blueprints that must fail with exact diagnostic codes, one or more per code, including slot and condition cases (an undeclared or root `slot`, an unknown condition `op`, an extra condition field) |
30
+ | `manifest.json` | The spec version, the dialect and a SHA-256 per normative file |
31
+ | `versions.json` | Every released spec version pinned to its spec digest (not normative: it pins the normative files) |
32
+ | `generated/typescript/`, `generated/kotlin/` | The projections. Never edit them |
33
+ | `generated-manifest.json` | A SHA-256 per generated file |
34
+
35
+ ## The dialect
36
+
37
+ Schemas use a closed subset of JSON Schema 2020-12 (`DIALECT_KEYWORDS`):
38
+ `type`, `properties`, `required`, `additionalProperties`, `items`, `enum`,
39
+ `const`, string, number and array bounds, `pattern`, and local `$ref` /
40
+ `$defs`. Objects must be closed (`additionalProperties: false`) or explicit
41
+ maps. Anything else fails `checkSchemaDocument`, and generation refuses it.
42
+
43
+ ## Exports
44
+
45
+ - `@texuli/spec`, the runtime half:
46
+ - the generated types (`BlueprintEnvelope`, `SpecCatalog`, `SpecValidation` …);
47
+ - `SPEC_COMPONENT_IDS` and `SpecComponentProps`;
48
+ - `validateInstance` (the dialect validator);
49
+ - `validateBlueprint` (the target-neutral blueprint check: envelope
50
+ schema, closed ids and props, prop kinds and enums, required props, no
51
+ platform-extension props from data, a node `slot` that names a slot prop
52
+ of its parent (`unknown-slot`), the portable profile failing closed,
53
+ and an optional catalog binding);
54
+ - `canonicalJson` and a synchronous `sha256`.
55
+ - `@texuli/spec/codegen`, build time: `buildSpec`, `projectCatalog`,
56
+ `checkConformance`, `formatJson`.
57
+
58
+ URL policy, tree limits, grammar and rendering stay in the runtime validator,
59
+ `@texuli/catalog/validate`.
60
+
61
+ ## Projections
62
+
63
+ - **Web extensions.** Props of kind `slot`, `event` or `unknown`, and the
64
+ `class`, `className`, `style` and chart `renderer` props, are marked
65
+ `extension: "web"`. They are left out of `SpecComponentProps`, and data
66
+ that sets them is refused.
67
+ - **Kotlin** (`com.texuli.spec`, Kotlin 1.9+ for `entries` and
68
+ `data object`): data classes, closed enums and static decoders over a
69
+ generated `JsonValue` model, with no library dependency. Decoders check
70
+ JSON types, required and unknown fields, enums and constants. Patterns
71
+ and limits are the shared validator's job.
72
+ - **Stamp.** Every generated file carries the spec version and a digest of
73
+ the normative files. There are no timestamps, and output is
74
+ byte-identical for the same inputs in any order.
75
+
76
+ ## Regenerate
77
+
78
+ ```bash
79
+ pnpm exec tsx packages/spec/scripts/bootstrap-catalog.ts # refresh catalog/components.json from the built catalog
80
+ pnpm exec tsx packages/spec/scripts/generate.ts # write manifests and projections
81
+ pnpm exec tsx packages/spec/scripts/generate.ts --check # fail on drift
82
+ ```
83
+
84
+ ## Conformance
85
+
86
+ `pnpm vitest run packages/spec` runs the harness. Drift is a failed gate:
87
+ - every schema stays in the dialect;
88
+ - the manifest digests match the normative files;
89
+ - regenerating gives byte-identical files, none missing or extra;
90
+ - the golden fixtures validate and the invalid fixtures fail with exactly
91
+ their codes, covering every diagnostic code;
92
+ - `components.json` equals the projection of the live catalog, and the
93
+ blueprint schema matches the envelope `@texuli/catalog` reads;
94
+ - generation is deterministic and refuses schemas outside the dialect;
95
+ - the Kotlin text declares its package, balances its brackets, defines
96
+ every type it references and decodes closed;
97
+ - the spec version and digest match the newest release in `versions.json`.
98
+
99
+ When the catalog changes, refresh `components.json`, regenerate and review
100
+ the diff. Then classify the change, bump the version, and pin it (see
101
+ Versioning).
102
+
103
+ ## Versioning
104
+
105
+ **The spec digest** is the SHA-256 of `manifest.json`'s canonical JSON: the
106
+ spec version, the dialect and a digest per normative file (schemas,
107
+ `catalog/components.json`, `catalog/capabilities.json` and every fixture).
108
+ Every generated file carries it in its stamp (`// Spec 0.2.0, digest …`).
109
+ The same normative files always give the same digest.
110
+
111
+ **The version follows the digest.** Every normative change changes the
112
+ digest, and a changed digest needs a new semantic version.
113
+ `tests/versioning.test.ts` fails when the digest moves without one. The
114
+ release goes in `versions.json`: its version, its digest, its change class
115
+ and a one-line summary.
116
+
117
+ | Class | What it covers | Before 1.0.0 | From 1.0.0 |
118
+ | --- | --- | --- | --- |
119
+ | patch | Nothing accepted changes: descriptions, diagnostic wording, a new fixture for behavior that already holds | patch | patch |
120
+ | minor | Additive: optional fields, new catalog ids, new optional props, new diagnostic codes for data that was already refused, new named extensions. Existing portable fixtures still pass | minor | minor |
121
+ | major | Removing or renaming ids, props or fields; changing a prop type; making something required; changing what accepted data means | minor | major |
122
+
123
+ `nextSpecVersion(previous, class)` computes the step and
124
+ `checkVersionHistory` enforces it. Before 1.0.0, any change to accepted data
125
+ bumps the minor number. Moving to 1.0.0 is a release decision for the owner,
126
+ not a side effect of a change. 0.1.0, the bootstrap, has no pinned digest:
127
+ its normative files moved without a version until 0.2.0.
128
+
129
+ **What a projection change means for consumers:**
130
+ - **TypeScript (`@texuli/spec`):** a patch release leaves the generated types
131
+ and `SPEC_COMPONENT_IDS` alone, apart from the stamp. Code that compiled
132
+ against a minor release keeps compiling, but new optional fields and ids
133
+ can appear in data it reads. A major-class change (a minor bump before
134
+ 1.0.0) may remove or retype what it relied on.
135
+ - **Kotlin and native (`native/`):** `texuli-spec` compiles
136
+ `generated/kotlin` from this package, so a native build always matches the
137
+ checked-out spec. The generated decoders are closed: a blueprint that uses
138
+ a field from a newer spec fails to decode on an older native build with a
139
+ `schema` diagnostic. It does not pass silently. After a change, regenerate,
140
+ rebuild native, and refresh the conformance traces when goldens moved
141
+ (`pnpm exec tsx native/conformance/web/trace.ts --write`). Native
142
+ conformance compares the native refusals with every invalid fixture's
143
+ codes.
144
+ - **Blueprints:** a blueprint binds to an exact catalog hash today. The
145
+ envelope field for a compatible spec range is **planned, not built**.
146
+ Until it exists, a consumer checks the spec version it ships against the
147
+ stamp of the spec it was built from.
148
+
149
+ Migrations between versions (deterministic blueprint transforms tested per
150
+ version pair) are **planned, not built**: nothing has needed one yet.
151
+
152
+
153
+ ## Named slots and conditions
154
+
155
+ A node may carry `slot` (audit G04: the parent's named slot, a string such as
156
+ `hero`) and `when` (audit G07: the blocks `Condition` AST that shows the node
157
+ when true). Both match what `@texuli/catalog` renders.
158
+
159
+ ## Not yet
160
+
161
+ - Behavior contracts, token roles and the token-to-Compose generator (B1
162
+ steps 2 and 5).
163
+ - Making the spec authoritative for the web packages (B1 step 3 cutover);
164
+ today the catalog manifests are the bootstrap input.
165
+ - Compiling the Kotlin, and the native runtime (B2).
166
+ - A full type for `when`. The dialect has no unions, so `condition` and
167
+ `numberFormula` are closed records whose `op` is an enum and whose operands
168
+ are typed (except `value`, which is a number for `num` and a formula for
169
+ `round` and `clamp`). Which operands each `op` needs, and the formula
170
+ limits, are checked by the runtime parser in `@texuli/blocks/formula`.
@@ -0,0 +1,47 @@
1
+ import type { Stamp } from './codegen/shared.js';
2
+ import { SPEC_VERSION } from './version.js';
3
+ export { SPEC_VERSION };
4
+ /** Where normative files live inside the package. */
5
+ export declare const NORMATIVE_DIRS: readonly ["schemas/", "catalog/", "fixtures/"];
6
+ /** The schema documents, their generated names and output files. */
7
+ export declare const SCHEMA_UNITS: readonly [{
8
+ readonly name: "blueprint";
9
+ readonly prefix: "Blueprint";
10
+ readonly ts: "blueprint.ts";
11
+ readonly kt: "Blueprint.kt";
12
+ }, {
13
+ readonly name: "catalog";
14
+ readonly prefix: "Spec";
15
+ readonly ts: "catalog-schema.ts";
16
+ readonly kt: "CatalogSchema.kt";
17
+ }, {
18
+ readonly name: "diagnostics";
19
+ readonly prefix: "Spec";
20
+ readonly ts: "diagnostics.ts";
21
+ readonly kt: "Diagnostics.kt";
22
+ }, {
23
+ readonly name: "manifest";
24
+ readonly prefix: "Spec";
25
+ readonly ts: "manifest.ts";
26
+ readonly kt: "Manifest.kt";
27
+ }];
28
+ export declare const TS_OUT = "generated/typescript/";
29
+ export declare const KOTLIN_OUT = "generated/kotlin/com/texuli/spec/";
30
+ export interface SpecBuild {
31
+ /** `manifest.json`: the spec version and a digest per normative file. */
32
+ manifest: string;
33
+ /** The stamp every generated file carries. */
34
+ stamp: Stamp;
35
+ /** Generated path → content. */
36
+ generated: Readonly<Record<string, string>>;
37
+ /** `generated-manifest.json`: a digest per generated file. */
38
+ generatedManifest: string;
39
+ }
40
+ /**
41
+ * Build the whole spec from its normative files (path → exact text): the
42
+ * manifest, every TypeScript and Kotlin projection and the generated
43
+ * manifest. Pure and deterministic: the same files give byte-identical
44
+ * output, whatever order they are passed in. Throws when a schema leaves
45
+ * the dialect.
46
+ */
47
+ export declare function buildSpec(files: Readonly<Record<string, string>>, specVersion?: string): SpecBuild;
package/dist/build.js ADDED
@@ -0,0 +1,65 @@
1
+ import { canonicalJson, sha256 } from "./canonical.js";
2
+ import { kotlinJsonRuntime, schemaToKotlin, vocabularyToKotlin } from "./codegen/kotlin.js";
3
+ import { schemaToTypeScript, vocabularyToTypeScript } from "./codegen/typescript.js";
4
+ import { DIALECT_URI } from "./dialect.js";
5
+ import { formatJson } from "./format.js";
6
+ import { SPEC_VERSION } from "./version.js";
7
+ export { SPEC_VERSION };
8
+ /** Where normative files live inside the package. */
9
+ export const NORMATIVE_DIRS = ['schemas/', 'catalog/', 'fixtures/'];
10
+ /** The schema documents, their generated names and output files. */
11
+ export const SCHEMA_UNITS = [
12
+ { name: 'blueprint', prefix: 'Blueprint', ts: 'blueprint.ts', kt: 'Blueprint.kt' },
13
+ { name: 'catalog', prefix: 'Spec', ts: 'catalog-schema.ts', kt: 'CatalogSchema.kt' },
14
+ { name: 'diagnostics', prefix: 'Spec', ts: 'diagnostics.ts', kt: 'Diagnostics.kt' },
15
+ { name: 'manifest', prefix: 'Spec', ts: 'manifest.ts', kt: 'Manifest.kt' },
16
+ ];
17
+ export const TS_OUT = 'generated/typescript/';
18
+ export const KOTLIN_OUT = 'generated/kotlin/com/texuli/spec/';
19
+ /**
20
+ * Build the whole spec from its normative files (path → exact text): the
21
+ * manifest, every TypeScript and Kotlin projection and the generated
22
+ * manifest. Pure and deterministic: the same files give byte-identical
23
+ * output, whatever order they are passed in. Throws when a schema leaves
24
+ * the dialect.
25
+ */
26
+ export function buildSpec(files, specVersion = SPEC_VERSION) {
27
+ const paths = Object.keys(files).sort();
28
+ for (const path of paths) {
29
+ if (!NORMATIVE_DIRS.some((dir) => path.startsWith(dir)) || !path.endsWith('.json')) {
30
+ throw new Error(`${path}: not a normative spec file`);
31
+ }
32
+ }
33
+ const manifestData = {
34
+ specVersion,
35
+ dialect: DIALECT_URI,
36
+ files: Object.fromEntries(paths.map((path) => [path, sha256(files[path])])),
37
+ };
38
+ const stamp = { specVersion, specDigest: sha256(canonicalJson(manifestData)) };
39
+ const read = (path) => {
40
+ const text = files[path];
41
+ if (text === undefined)
42
+ throw new Error(`${path}: missing`);
43
+ return JSON.parse(text);
44
+ };
45
+ const generated = {};
46
+ for (const unit of SCHEMA_UNITS) {
47
+ const source = `schemas/${unit.name}.schema.json`;
48
+ const schemaUnit = { source, schema: read(source), prefix: unit.prefix };
49
+ generated[`${TS_OUT}${unit.ts}`] = schemaToTypeScript(schemaUnit, stamp);
50
+ generated[`${KOTLIN_OUT}${unit.kt}`] = schemaToKotlin(schemaUnit, stamp);
51
+ }
52
+ const catalog = read('catalog/components.json');
53
+ generated[`${TS_OUT}vocabulary.ts`] = vocabularyToTypeScript(catalog, stamp);
54
+ generated[`${KOTLIN_OUT}Vocabulary.kt`] = vocabularyToKotlin(catalog, stamp);
55
+ generated[`${KOTLIN_OUT}Json.kt`] = kotlinJsonRuntime(stamp);
56
+ const sorted = Object.fromEntries(Object.keys(generated)
57
+ .sort()
58
+ .map((path) => [path, generated[path]]));
59
+ const generatedManifest = formatJson({
60
+ specVersion,
61
+ dialect: DIALECT_URI,
62
+ files: Object.fromEntries(Object.entries(sorted).map(([path, text]) => [path, sha256(text)])),
63
+ });
64
+ return { manifest: formatJson(manifestData), stamp, generated: sorted, generatedManifest };
65
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Deterministic serialization and hashing, with no runtime dependency: the
3
+ * same input gives the same bytes and digest in every runtime.
4
+ */
5
+ /**
6
+ * Canonical JSON: object keys sorted by UTF-16 code unit at every depth, no
7
+ * whitespace, `undefined` fields dropped. Throws on values JSON cannot carry
8
+ * exactly (functions, non-finite numbers, cycles).
9
+ */
10
+ export declare function canonicalJson(value: unknown): string;
11
+ /**
12
+ * Pretty canonical JSON for committed files: sorted keys, two-space indent,
13
+ * a trailing newline. Byte-stable for the same data.
14
+ */
15
+ export declare function prettyJson(value: unknown): string;
16
+ /** Lowercase hex SHA-256 of a string's UTF-8 bytes (FIPS 180-4), synchronous. */
17
+ export declare function sha256(text: string): string;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Deterministic serialization and hashing, with no runtime dependency: the
3
+ * same input gives the same bytes and digest in every runtime.
4
+ */
5
+ /**
6
+ * Canonical JSON: object keys sorted by UTF-16 code unit at every depth, no
7
+ * whitespace, `undefined` fields dropped. Throws on values JSON cannot carry
8
+ * exactly (functions, non-finite numbers, cycles).
9
+ */
10
+ export function canonicalJson(value) {
11
+ return encode(value, new Set());
12
+ }
13
+ function encode(value, stack) {
14
+ if (value === null)
15
+ return 'null';
16
+ if (typeof value === 'string' || typeof value === 'boolean')
17
+ return JSON.stringify(value);
18
+ if (typeof value === 'number') {
19
+ if (!Number.isFinite(value))
20
+ throw new TypeError('canonicalJson: non-finite number');
21
+ return JSON.stringify(value);
22
+ }
23
+ if (typeof value !== 'object')
24
+ throw new TypeError(`canonicalJson: ${typeof value} value`);
25
+ if (stack.has(value))
26
+ throw new TypeError('canonicalJson: cycle');
27
+ stack.add(value);
28
+ let out;
29
+ if (Array.isArray(value)) {
30
+ out = `[${value.map((item) => encode(item === undefined ? null : item, stack)).join(',')}]`;
31
+ }
32
+ else {
33
+ const record = value;
34
+ const keys = Object.keys(record)
35
+ .filter((key) => record[key] !== undefined)
36
+ .sort();
37
+ out = `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key], stack)}`).join(',')}}`;
38
+ }
39
+ stack.delete(value);
40
+ return out;
41
+ }
42
+ /**
43
+ * Pretty canonical JSON for committed files: sorted keys, two-space indent,
44
+ * a trailing newline. Byte-stable for the same data.
45
+ */
46
+ export function prettyJson(value) {
47
+ return `${JSON.stringify(JSON.parse(canonicalJson(value)), null, 2)}\n`;
48
+ }
49
+ const K = new Uint32Array([
50
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
51
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
52
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
53
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
54
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
55
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
56
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
57
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
58
+ ]);
59
+ /** UTF-8 bytes of a string (no `TextEncoder` needed). */
60
+ function utf8(text) {
61
+ const bytes = [];
62
+ for (const char of text) {
63
+ const code = char.codePointAt(0) ?? 0;
64
+ if (code < 0x80)
65
+ bytes.push(code);
66
+ else if (code < 0x800)
67
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
68
+ else if (code < 0x10000)
69
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
70
+ else
71
+ bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
72
+ }
73
+ return bytes;
74
+ }
75
+ /** Lowercase hex SHA-256 of a string's UTF-8 bytes (FIPS 180-4), synchronous. */
76
+ export function sha256(text) {
77
+ const bytes = utf8(text);
78
+ const bitLength = bytes.length * 8;
79
+ bytes.push(0x80);
80
+ while (bytes.length % 64 !== 56)
81
+ bytes.push(0);
82
+ const high = Math.floor(bitLength / 0x100000000);
83
+ for (const word of [high, bitLength >>> 0]) {
84
+ bytes.push((word >>> 24) & 0xff, (word >>> 16) & 0xff, (word >>> 8) & 0xff, word & 0xff);
85
+ }
86
+ const hash = new Uint32Array([
87
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
88
+ ]);
89
+ const w = new Uint32Array(64);
90
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
91
+ for (let offset = 0; offset < bytes.length; offset += 64) {
92
+ for (let i = 0; i < 16; i += 1) {
93
+ const at = offset + i * 4;
94
+ w[i] =
95
+ ((bytes[at] ?? 0) << 24) |
96
+ ((bytes[at + 1] ?? 0) << 16) |
97
+ ((bytes[at + 2] ?? 0) << 8) |
98
+ (bytes[at + 3] ?? 0);
99
+ }
100
+ for (let i = 16; i < 64; i += 1) {
101
+ const a = w[i - 15] ?? 0;
102
+ const b = w[i - 2] ?? 0;
103
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
104
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
105
+ w[i] = ((w[i - 16] ?? 0) + s0 + (w[i - 7] ?? 0) + s1) >>> 0;
106
+ }
107
+ let [a, b, c, d, e, f, g, h] = hash;
108
+ for (let i = 0; i < 64; i += 1) {
109
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
110
+ const ch = (e & f) ^ (~e & g);
111
+ const t1 = (h + s1 + ch + (K[i] ?? 0) + (w[i] ?? 0)) >>> 0;
112
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
113
+ const maj = (a & b) ^
114
+ (a & c) ^
115
+ (b & c);
116
+ const t2 = (s0 + maj) >>> 0;
117
+ h = g;
118
+ g = f;
119
+ f = e;
120
+ e = (d + t1) >>> 0;
121
+ d = c;
122
+ c = b;
123
+ b = a;
124
+ a = (t1 + t2) >>> 0;
125
+ }
126
+ const round = [a, b, c, d, e, f, g, h];
127
+ for (let i = 0; i < 8; i += 1)
128
+ hash[i] = ((hash[i] ?? 0) + (round[i] ?? 0)) >>> 0;
129
+ }
130
+ return Array.from(hash, (word) => word.toString(16).padStart(8, '0')).join('');
131
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The spec's catalog vocabulary (`catalog/components.json`) and its
3
+ * projection from a built catalog. The projection is the bootstrap step of
4
+ * spec/Compose design §2.3; the parity check reruns it to catch drift.
5
+ */
6
+ import type { SpecCatalog, SpecComponent, SpecProp } from './generated/typescript/catalog-schema.js';
7
+ export type { SpecCatalog, SpecComponent, SpecProp };
8
+ export declare const CAPABILITIES: readonly ["portable-core", "web-only", "native-only", "unknown"];
9
+ export type Capability = SpecComponent['capability'];
10
+ export type SpecPropKind = SpecProp['kind'];
11
+ /** Prop kinds that cannot cross the shared boundary: they become web extensions. */
12
+ export declare const WEB_ONLY_KINDS: readonly SpecPropKind[];
13
+ /**
14
+ * Props that are web implementation details whatever their kind (spec/Compose
15
+ * design §6): CSS hooks and the chart renderer choice (`svg`, `canvas`, `auto`).
16
+ */
17
+ export declare const WEB_ONLY_PROP_NAMES: readonly string[];
18
+ /** The fields of a built catalog the projection reads (no dependency on the catalog package). */
19
+ export interface BuiltCatalogLike {
20
+ entries: readonly {
21
+ id: string;
22
+ label: string;
23
+ summary: string;
24
+ group: string;
25
+ tier: 'app' | 'content';
26
+ emit: readonly string[];
27
+ props: readonly {
28
+ name: string;
29
+ kind: SpecPropKind;
30
+ required?: boolean;
31
+ enumValues?: readonly string[];
32
+ }[];
33
+ }[];
34
+ }
35
+ /**
36
+ * Project a built catalog into the spec vocabulary. Deterministic: components
37
+ * sorted by id, props by name, emit tiers sorted. Capability is `unknown`
38
+ * unless `capabilities` (the reviewed `catalog/capabilities.json`) records an
39
+ * audited value (ADR-003: no id is portable until audited).
40
+ */
41
+ export declare function projectCatalog(catalog: BuiltCatalogLike, catalogHash: string, capabilities?: Readonly<Record<string, Capability>>): SpecCatalog;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The spec's catalog vocabulary (`catalog/components.json`) and its
3
+ * projection from a built catalog. The projection is the bootstrap step of
4
+ * spec/Compose design §2.3; the parity check reruns it to catch drift.
5
+ */
6
+ export const CAPABILITIES = ['portable-core', 'web-only', 'native-only', 'unknown'];
7
+ /** Prop kinds that cannot cross the shared boundary: they become web extensions. */
8
+ export const WEB_ONLY_KINDS = ['slot', 'event', 'unknown'];
9
+ /**
10
+ * Props that are web implementation details whatever their kind (spec/Compose
11
+ * design §6): CSS hooks and the chart renderer choice (`svg`, `canvas`, `auto`).
12
+ */
13
+ export const WEB_ONLY_PROP_NAMES = ['class', 'className', 'style', 'renderer'];
14
+ /**
15
+ * Project a built catalog into the spec vocabulary. Deterministic: components
16
+ * sorted by id, props by name, emit tiers sorted. Capability is `unknown`
17
+ * unless `capabilities` (the reviewed `catalog/capabilities.json`) records an
18
+ * audited value (ADR-003: no id is portable until audited).
19
+ */
20
+ export function projectCatalog(catalog, catalogHash, capabilities = {}) {
21
+ const byName = (a, b) => (a.name < b.name ? -1 : 1);
22
+ const components = [...catalog.entries]
23
+ .sort((a, b) => (a.id < b.id ? -1 : 1))
24
+ .map((entry) => ({
25
+ id: entry.id,
26
+ label: entry.label,
27
+ summary: entry.summary,
28
+ group: entry.group,
29
+ placement: entry.tier,
30
+ emit: [...entry.emit].sort(),
31
+ capability: Object.hasOwn(capabilities, entry.id)
32
+ ? capabilities[entry.id]
33
+ : 'unknown',
34
+ props: [...entry.props].sort(byName).map((prop) => ({
35
+ name: prop.name,
36
+ kind: prop.kind,
37
+ ...(prop.required === true ? { required: true } : {}),
38
+ ...(prop.enumValues ? { enumValues: [...prop.enumValues] } : {}),
39
+ ...(WEB_ONLY_KINDS.includes(prop.kind) || WEB_ONLY_PROP_NAMES.includes(prop.name)
40
+ ? { extension: 'web' }
41
+ : {}),
42
+ })),
43
+ }));
44
+ return { catalogHash, components };
45
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@texuli/spec/codegen` — build-time tooling: deterministic generation of
3
+ * the manifests and the TypeScript and Kotlin projections, the catalog
4
+ * projection, the committed-file JSON format and the conformance harness.
5
+ * Pure functions: callers read and write the files.
6
+ */
7
+ export { buildSpec, KOTLIN_OUT, NORMATIVE_DIRS, SCHEMA_UNITS, type SpecBuild, TS_OUT, } from '../build.js';
8
+ export { prettyJson } from '../canonical.js';
9
+ export { type BuiltCatalogLike, projectCatalog } from '../catalog.js';
10
+ export { type ConformanceIssue, checkConformance } from '../conformance.js';
11
+ export { formatJson, LINE_WIDTH } from '../format.js';
12
+ export { checkVersionHistory, nextSpecVersion, type SpecChangeClass, type SpecVersionHistory, type SpecVersionPin, type VersionIssue, } from '../versioning.js';
13
+ export { KOTLIN_PACKAGE, kotlinString } from './kotlin.js';
14
+ export type { SchemaUnit, Stamp } from './shared.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@texuli/spec/codegen` — build-time tooling: deterministic generation of
3
+ * the manifests and the TypeScript and Kotlin projections, the catalog
4
+ * projection, the committed-file JSON format and the conformance harness.
5
+ * Pure functions: callers read and write the files.
6
+ */
7
+ export { buildSpec, KOTLIN_OUT, NORMATIVE_DIRS, SCHEMA_UNITS, TS_OUT, } from "../build.js";
8
+ export { prettyJson } from "../canonical.js";
9
+ export { projectCatalog } from "../catalog.js";
10
+ export { checkConformance } from "../conformance.js";
11
+ export { formatJson, LINE_WIDTH } from "../format.js";
12
+ export { checkVersionHistory, nextSpecVersion, } from "../versioning.js";
13
+ export { KOTLIN_PACKAGE, kotlinString } from "./kotlin.js";
@@ -0,0 +1,18 @@
1
+ import type { SpecCatalog } from '../catalog.js';
2
+ import { type SchemaUnit, type Stamp } from './shared.js';
3
+ /**
4
+ * Deterministic Kotlin projections of the spec, written as committed text
5
+ * (no Kotlin toolchain here): data classes, closed enums and static
6
+ * decoders over a small `JsonValue` model, with no library dependency.
7
+ * Decoders check JSON types, required and unknown fields, enums and
8
+ * constants; patterns and limits stay with the shared validator.
9
+ */
10
+ export declare const KOTLIN_PACKAGE = "com.texuli.spec";
11
+ /** A Kotlin string literal (escapes backslash, quote, dollar and controls). */
12
+ export declare function kotlinString(value: string): string;
13
+ /** One schema document as a Kotlin file of data classes, enums and decoders. */
14
+ export declare function schemaToKotlin(unit: SchemaUnit, stamp: Stamp): string;
15
+ /** The JSON model and decode helpers every generated decoder uses. */
16
+ export declare function kotlinJsonRuntime(stamp: Stamp): string;
17
+ /** The catalog vocabulary as a Kotlin registry of `SpecComponent` values. */
18
+ export declare function vocabularyToKotlin(catalog: SpecCatalog, stamp: Stamp): string;