@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.
- package/LICENSE +21 -0
- package/README.md +169 -2
- package/dist/build.d.ts +47 -0
- package/dist/build.js +65 -0
- package/dist/canonical.d.ts +17 -0
- package/dist/canonical.js +131 -0
- package/dist/catalog.d.ts +41 -0
- package/dist/catalog.js +45 -0
- package/dist/codegen/index.d.ts +14 -0
- package/dist/codegen/index.js +13 -0
- package/dist/codegen/kotlin.d.ts +18 -0
- package/dist/codegen/kotlin.js +328 -0
- package/dist/codegen/shared.d.ts +35 -0
- package/dist/codegen/shared.js +69 -0
- package/dist/codegen/typescript.d.ts +10 -0
- package/dist/codegen/typescript.js +167 -0
- package/dist/conformance.d.ts +20 -0
- package/dist/conformance.js +124 -0
- package/dist/dialect.d.ts +56 -0
- package/dist/dialect.js +264 -0
- package/dist/format.d.ts +9 -0
- package/dist/format.js +48 -0
- package/dist/generated/typescript/blueprint.d.ts +125 -0
- package/dist/generated/typescript/blueprint.js +3 -0
- package/dist/generated/typescript/catalog-schema.d.ts +33 -0
- package/dist/generated/typescript/catalog-schema.js +3 -0
- package/dist/generated/typescript/diagnostics.d.ts +12 -0
- package/dist/generated/typescript/diagnostics.js +3 -0
- package/dist/generated/typescript/manifest.d.ts +9 -0
- package/dist/generated/typescript/manifest.js +3 -0
- package/dist/generated/typescript/vocabulary.d.ts +972 -0
- package/dist/generated/typescript/vocabulary.js +140 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +13 -0
- package/dist/validate.d.ts +27 -0
- package/dist/validate.js +143 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/versioning.d.ts +39 -0
- package/dist/versioning.js +87 -0
- 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
|
-
#
|
|
1
|
+
# @texuli/spec
|
|
2
2
|
|
|
3
|
-
|
|
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`.
|
package/dist/build.d.ts
ADDED
|
@@ -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;
|
package/dist/catalog.js
ADDED
|
@@ -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;
|