@texuli/spec 0.0.0-stage → 0.1.0-next.0
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 +97 -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 +13 -0
- package/dist/codegen/index.js +12 -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/index.d.ts +17 -0
- package/dist/index.js +13 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.js +118 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/generated/typescript/blueprint.d.ts +83 -0
- package/generated/typescript/blueprint.js +3 -0
- package/generated/typescript/catalog-schema.d.ts +33 -0
- package/generated/typescript/catalog-schema.js +3 -0
- package/generated/typescript/diagnostics.d.ts +12 -0
- package/generated/typescript/diagnostics.js +3 -0
- package/generated/typescript/manifest.d.ts +9 -0
- package/generated/typescript/manifest.js +3 -0
- package/generated/typescript/vocabulary.d.ts +943 -0
- package/generated/typescript/vocabulary.js +138 -0
- package/package.json +26 -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,98 @@
|
|
|
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
|
+
## Layout
|
|
15
|
+
|
|
16
|
+
| Path | What it holds |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `schemas/` | The normative JSON schemas: `blueprint` (envelope v1 and the node tree), `catalog`, `diagnostics`, `manifest` |
|
|
19
|
+
| `catalog/components.json` | The closed vocabulary, projected from the built catalog: id, label, summary, group, placement (`app`/`content`), emit tiers, capability and props |
|
|
20
|
+
| `catalog/capabilities.json` | Reviewed capability claims. Empty: no id is audited portable yet, so every capability is `unknown` (ADR-003) |
|
|
21
|
+
| `fixtures/golden/` | Blueprints that must validate: a report, a dashboard and a proactive deck (fictional data) |
|
|
22
|
+
| `fixtures/invalid/` | Blueprints that must fail with exact diagnostic codes, one or more per code |
|
|
23
|
+
| `manifest.json` | The spec version, the dialect and a SHA-256 per normative file |
|
|
24
|
+
| `generated/typescript/`, `generated/kotlin/` | The projections. Never edit them |
|
|
25
|
+
| `generated-manifest.json` | A SHA-256 per generated file |
|
|
26
|
+
|
|
27
|
+
## The dialect
|
|
28
|
+
|
|
29
|
+
Schemas use a closed subset of JSON Schema 2020-12 (`DIALECT_KEYWORDS`):
|
|
30
|
+
`type`, `properties`, `required`, `additionalProperties`, `items`, `enum`,
|
|
31
|
+
`const`, string, number and array bounds, `pattern`, and local `$ref` /
|
|
32
|
+
`$defs`. Objects must be closed (`additionalProperties: false`) or explicit
|
|
33
|
+
maps. Anything else fails `checkSchemaDocument`, and generation refuses it.
|
|
34
|
+
|
|
35
|
+
## Exports
|
|
36
|
+
|
|
37
|
+
- `@texuli/spec`, the runtime half:
|
|
38
|
+
- the generated types (`BlueprintEnvelope`, `SpecCatalog`, `SpecValidation` …);
|
|
39
|
+
- `SPEC_COMPONENT_IDS` and `SpecComponentProps`;
|
|
40
|
+
- `validateInstance` (the dialect validator);
|
|
41
|
+
- `validateBlueprint` (the target-neutral blueprint check: envelope
|
|
42
|
+
schema, closed ids and props, prop kinds and enums, required props, no
|
|
43
|
+
platform-extension props from data, the portable profile failing closed,
|
|
44
|
+
and an optional catalog binding);
|
|
45
|
+
- `canonicalJson` and a synchronous `sha256`.
|
|
46
|
+
- `@texuli/spec/codegen`, build time: `buildSpec`, `projectCatalog`,
|
|
47
|
+
`checkConformance`, `formatJson`.
|
|
48
|
+
|
|
49
|
+
URL policy, tree limits, grammar and rendering stay in the runtime validator,
|
|
50
|
+
`@texuli/catalog/validate`.
|
|
51
|
+
|
|
52
|
+
## Projections
|
|
53
|
+
|
|
54
|
+
- **Web extensions.** Props of kind `slot`, `event` or `unknown`, and the
|
|
55
|
+
`class`, `className`, `style` and chart `renderer` props, are marked
|
|
56
|
+
`extension: "web"`. They are left out of `SpecComponentProps`, and data
|
|
57
|
+
that sets them is refused.
|
|
58
|
+
- **Kotlin** (`com.texuli.spec`, Kotlin 1.9+ for `entries` and
|
|
59
|
+
`data object`): data classes, closed enums and static decoders over a
|
|
60
|
+
generated `JsonValue` model, with no library dependency. Decoders check
|
|
61
|
+
JSON types, required and unknown fields, enums and constants. Patterns
|
|
62
|
+
and limits are the shared validator's job.
|
|
63
|
+
- **Stamp.** Every generated file carries the spec version and a digest of
|
|
64
|
+
the normative files. There are no timestamps, and output is
|
|
65
|
+
byte-identical for the same inputs in any order.
|
|
66
|
+
|
|
67
|
+
## Regenerate
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm exec tsx packages/spec/scripts/bootstrap-catalog.ts # refresh catalog/components.json from the built catalog
|
|
71
|
+
pnpm exec tsx packages/spec/scripts/generate.ts # write manifests and projections
|
|
72
|
+
pnpm exec tsx packages/spec/scripts/generate.ts --check # fail on drift
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Conformance
|
|
76
|
+
|
|
77
|
+
`pnpm vitest run packages/spec` runs the harness. Drift is a failed gate:
|
|
78
|
+
- every schema stays in the dialect;
|
|
79
|
+
- the manifest digests match the normative files;
|
|
80
|
+
- regenerating gives byte-identical files, none missing or extra;
|
|
81
|
+
- the golden fixtures validate and the invalid fixtures fail with exactly
|
|
82
|
+
their codes, covering every diagnostic code;
|
|
83
|
+
- `components.json` equals the projection of the live catalog, and the
|
|
84
|
+
blueprint schema matches the envelope `@texuli/catalog` reads;
|
|
85
|
+
- generation is deterministic and refuses schemas outside the dialect;
|
|
86
|
+
- the Kotlin text declares its package, balances its brackets, defines
|
|
87
|
+
every type it references and decodes closed.
|
|
88
|
+
|
|
89
|
+
When the catalog changes, refresh `components.json`, regenerate and review
|
|
90
|
+
the diff.
|
|
91
|
+
|
|
92
|
+
## Not yet
|
|
93
|
+
|
|
94
|
+
- Behavior contracts, token roles and the token-to-Compose generator (B1
|
|
95
|
+
steps 2 and 5).
|
|
96
|
+
- Making the spec authoritative for the web packages (B1 step 3 cutover);
|
|
97
|
+
today the catalog manifests are the bootstrap input.
|
|
98
|
+
- Compiling the Kotlin, and the native runtime (B2).
|
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,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, 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 { KOTLIN_PACKAGE, kotlinString } from './kotlin.js';
|
|
13
|
+
export type { SchemaUnit, Stamp } from './shared.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
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 { 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;
|