@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
@@ -0,0 +1,124 @@
1
+ import { buildSpec } from "./build.js";
2
+ import { CAPABILITIES } from "./catalog.js";
3
+ import { checkSchemaDocument, isRecord, validateInstance } from "./dialect.js";
4
+ import { validateBlueprint } from "./validate.js";
5
+ /**
6
+ * Run the spec's conformance harness (spec/Compose design §7.1 and §7.3)
7
+ * over the normative files and the committed outputs (`manifest.json`,
8
+ * `generated-manifest.json` and every generated file):
9
+ * - every schema stays in the dialect;
10
+ * - `catalog/components.json` and `catalog/capabilities.json` are valid;
11
+ * - the manifest digests match the normative files;
12
+ * - regenerating gives byte-identical TypeScript and Kotlin, with no file
13
+ * missing or extra;
14
+ * - every golden blueprint validates, and every invalid fixture fails with
15
+ * exactly its expected diagnostic codes.
16
+ * Runtime-neutral: the caller reads the files.
17
+ */
18
+ export function checkConformance(normative, committed) {
19
+ const issues = [];
20
+ const json = (path) => JSON.parse(normative[path] ?? 'null');
21
+ const schemas = {};
22
+ for (const path of Object.keys(normative).filter((p) => p.startsWith('schemas/'))) {
23
+ const schema = json(path);
24
+ schemas[path] = schema;
25
+ for (const issue of checkSchemaDocument(schema)) {
26
+ issues.push({
27
+ check: 'dialect',
28
+ path,
29
+ message: `${issue.path} ${issue.code}: ${issue.message}`,
30
+ });
31
+ }
32
+ }
33
+ const catalogSchema = schemas['schemas/catalog.schema.json'];
34
+ const blueprintSchema = schemas['schemas/blueprint.schema.json'];
35
+ const catalog = json('catalog/components.json');
36
+ if (catalogSchema) {
37
+ for (const issue of validateInstance(catalogSchema, catalog)) {
38
+ issues.push({
39
+ check: 'catalog',
40
+ path: 'catalog/components.json',
41
+ message: `${issue.path} ${issue.code}`,
42
+ });
43
+ }
44
+ }
45
+ const ids = new Set((catalog?.components ?? []).map((c) => c.id));
46
+ const capabilities = json('catalog/capabilities.json');
47
+ if (!isRecord(capabilities)) {
48
+ issues.push({
49
+ check: 'capabilities',
50
+ path: 'catalog/capabilities.json',
51
+ message: 'must be an object',
52
+ });
53
+ }
54
+ else {
55
+ for (const [id, value] of Object.entries(capabilities)) {
56
+ if (!ids.has(id) || !CAPABILITIES.includes(value)) {
57
+ issues.push({
58
+ check: 'capabilities',
59
+ path: 'catalog/capabilities.json',
60
+ message: `${id}: an audited capability needs a catalog id and a known tier`,
61
+ });
62
+ }
63
+ }
64
+ }
65
+ let build;
66
+ try {
67
+ build = buildSpec(normative);
68
+ }
69
+ catch (cause) {
70
+ issues.push({ check: 'generated', path: '', message: cause.message });
71
+ }
72
+ if (build) {
73
+ if (committed['manifest.json'] !== build.manifest) {
74
+ issues.push({
75
+ check: 'manifest',
76
+ path: 'manifest.json',
77
+ message: 'digests do not match the normative files',
78
+ });
79
+ }
80
+ if (committed['generated-manifest.json'] !== build.generatedManifest) {
81
+ issues.push({
82
+ check: 'generated',
83
+ path: 'generated-manifest.json',
84
+ message: 'does not match a fresh generation',
85
+ });
86
+ }
87
+ const expected = new Set(Object.keys(build.generated));
88
+ for (const [path, text] of Object.entries(build.generated)) {
89
+ if (committed[path] !== text) {
90
+ issues.push({
91
+ check: 'generated',
92
+ path,
93
+ message: committed[path] === undefined ? 'missing' : 'differs from a fresh generation',
94
+ });
95
+ }
96
+ }
97
+ for (const path of Object.keys(committed)) {
98
+ if (path.startsWith('generated/') && !expected.has(path)) {
99
+ issues.push({ check: 'generated', path, message: 'no generator produces this file' });
100
+ }
101
+ }
102
+ }
103
+ if (blueprintSchema) {
104
+ const documents = { blueprintSchema, catalog };
105
+ for (const path of Object.keys(normative).filter((p) => p.startsWith('fixtures/golden/'))) {
106
+ const result = validateBlueprint(json(path), documents);
107
+ for (const d of result.diagnostics) {
108
+ issues.push({ check: 'golden', path, message: `${d.path} ${d.code}: ${d.message}` });
109
+ }
110
+ }
111
+ for (const path of Object.keys(normative).filter((p) => p.startsWith('fixtures/invalid/'))) {
112
+ const fixture = json(path);
113
+ const got = validateBlueprint(fixture.blueprint, documents, fixture.options).diagnostics.map((d) => d.code);
114
+ if (got.join(',') !== fixture.expect.join(',')) {
115
+ issues.push({
116
+ check: 'invalid',
117
+ path,
118
+ message: `expected [${fixture.expect.join(', ')}], got [${got.join(', ')}]`,
119
+ });
120
+ }
121
+ }
122
+ }
123
+ return issues;
124
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The pinned, closed schema dialect: a small subset of JSON Schema 2020-12
3
+ * that both the TypeScript and Kotlin generators can project exactly. A
4
+ * schema that uses anything else fails `checkSchemaDocument`, and code
5
+ * generation refuses it (spec/Compose design §2.3: unsupported constructs
6
+ * fail generation). Schemas are read as data; nothing is evaluated.
7
+ */
8
+ export declare const DIALECT_URI = "https://json-schema.org/draft/2020-12/schema";
9
+ /** Every keyword the dialect understands. */
10
+ export declare const DIALECT_KEYWORDS: readonly ["$schema", "$id", "$comment", "$defs", "$ref", "title", "description", "type", "properties", "required", "additionalProperties", "items", "enum", "const", "minLength", "maxLength", "pattern", "minimum", "maximum", "minItems", "maxItems"];
11
+ export declare const DIALECT_TYPES: readonly ["object", "array", "string", "number", "integer", "boolean"];
12
+ export type DialectType = (typeof DIALECT_TYPES)[number];
13
+ /** A schema node in the dialect. `{}` (no keywords) accepts any JSON value. */
14
+ export interface Schema {
15
+ $schema?: string;
16
+ $id?: string;
17
+ $comment?: string;
18
+ $defs?: Readonly<Record<string, Schema>>;
19
+ $ref?: string;
20
+ title?: string;
21
+ description?: string;
22
+ type?: DialectType;
23
+ properties?: Readonly<Record<string, Schema>>;
24
+ required?: readonly string[];
25
+ /** `false` for a closed object, or a schema for map values. */
26
+ additionalProperties?: false | Schema;
27
+ items?: Schema;
28
+ enum?: readonly (string | number | boolean)[];
29
+ const?: string | number | boolean;
30
+ minLength?: number;
31
+ maxLength?: number;
32
+ pattern?: string;
33
+ minimum?: number;
34
+ maximum?: number;
35
+ minItems?: number;
36
+ maxItems?: number;
37
+ }
38
+ export interface SchemaIssue {
39
+ code: string;
40
+ path: string;
41
+ message: string;
42
+ }
43
+ /** The `$defs` name a `$ref` points at. */
44
+ export declare function refName(ref: string): string | undefined;
45
+ /**
46
+ * Check that a schema document stays inside the dialect: known keywords only,
47
+ * local `$ref`s that resolve, a single `type`, and every object either closed
48
+ * (`additionalProperties: false`) or an explicit map. Fails closed.
49
+ */
50
+ export declare function checkSchemaDocument(document: unknown): SchemaIssue[];
51
+ /**
52
+ * Validate a JSON value against a dialect schema. `root` resolves `$ref`s
53
+ * (defaults to `schema`). Returns every issue, with JSON paths from `$`.
54
+ */
55
+ export declare function validateInstance(schema: Schema, value: unknown, root?: Schema): SchemaIssue[];
56
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * The pinned, closed schema dialect: a small subset of JSON Schema 2020-12
3
+ * that both the TypeScript and Kotlin generators can project exactly. A
4
+ * schema that uses anything else fails `checkSchemaDocument`, and code
5
+ * generation refuses it (spec/Compose design §2.3: unsupported constructs
6
+ * fail generation). Schemas are read as data; nothing is evaluated.
7
+ */
8
+ export const DIALECT_URI = 'https://json-schema.org/draft/2020-12/schema';
9
+ /** Every keyword the dialect understands. */
10
+ export const DIALECT_KEYWORDS = [
11
+ '$schema',
12
+ '$id',
13
+ '$comment',
14
+ '$defs',
15
+ '$ref',
16
+ 'title',
17
+ 'description',
18
+ 'type',
19
+ 'properties',
20
+ 'required',
21
+ 'additionalProperties',
22
+ 'items',
23
+ 'enum',
24
+ 'const',
25
+ 'minLength',
26
+ 'maxLength',
27
+ 'pattern',
28
+ 'minimum',
29
+ 'maximum',
30
+ 'minItems',
31
+ 'maxItems',
32
+ ];
33
+ export const DIALECT_TYPES = ['object', 'array', 'string', 'number', 'integer', 'boolean'];
34
+ const REF = /^#\/\$defs\/([A-Za-z][A-Za-z0-9]*)$/;
35
+ const MAX_DEPTH = 64;
36
+ /** The `$defs` name a `$ref` points at. */
37
+ export function refName(ref) {
38
+ return REF.exec(ref)?.[1];
39
+ }
40
+ /**
41
+ * Check that a schema document stays inside the dialect: known keywords only,
42
+ * local `$ref`s that resolve, a single `type`, and every object either closed
43
+ * (`additionalProperties: false`) or an explicit map. Fails closed.
44
+ */
45
+ export function checkSchemaDocument(document) {
46
+ const issues = [];
47
+ if (!isRecord(document)) {
48
+ return [{ code: 'not-a-schema', path: '$', message: 'A schema must be an object.' }];
49
+ }
50
+ if (document.$schema !== DIALECT_URI) {
51
+ issues.push({
52
+ code: 'dialect',
53
+ path: '$.$schema',
54
+ message: `"$schema" must be ${DIALECT_URI}.`,
55
+ });
56
+ }
57
+ const defs = isRecord(document.$defs) ? document.$defs : {};
58
+ const visit = (node, path, depth) => {
59
+ if (depth > MAX_DEPTH) {
60
+ issues.push({ code: 'too-deep', path, message: 'The schema nests too deeply.' });
61
+ return;
62
+ }
63
+ if (!isRecord(node)) {
64
+ issues.push({ code: 'not-a-schema', path, message: 'A schema node must be an object.' });
65
+ return;
66
+ }
67
+ for (const key of Object.keys(node)) {
68
+ if (!DIALECT_KEYWORDS.includes(key)) {
69
+ issues.push({
70
+ code: 'unsupported-keyword',
71
+ path: `${path}.${key}`,
72
+ message: `"${key}" is not in the dialect.`,
73
+ });
74
+ }
75
+ }
76
+ if (node.$ref !== undefined) {
77
+ const name = typeof node.$ref === 'string' ? refName(node.$ref) : undefined;
78
+ if (name === undefined || !Object.hasOwn(defs, name)) {
79
+ issues.push({
80
+ code: 'bad-ref',
81
+ path: `${path}.$ref`,
82
+ message: 'A $ref must name a local $defs entry.',
83
+ });
84
+ }
85
+ }
86
+ if (node.type !== undefined && !DIALECT_TYPES.includes(node.type)) {
87
+ issues.push({
88
+ code: 'bad-type',
89
+ path: `${path}.type`,
90
+ message: '"type" must be one dialect type.',
91
+ });
92
+ }
93
+ if (node.type === 'object') {
94
+ if (node.additionalProperties === undefined || node.additionalProperties === true) {
95
+ issues.push({
96
+ code: 'open-object',
97
+ path,
98
+ message: 'Objects must set "additionalProperties": false or a map schema.',
99
+ });
100
+ }
101
+ if (Array.isArray(node.required) && isRecord(node.properties)) {
102
+ for (const name of node.required) {
103
+ if (typeof name !== 'string' || !Object.hasOwn(node.properties, name)) {
104
+ issues.push({
105
+ code: 'bad-required',
106
+ path: `${path}.required`,
107
+ message: `Required "${String(name)}" is not a declared property.`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+ if (node.enum !== undefined) {
114
+ const values = node.enum;
115
+ const valid = Array.isArray(values) &&
116
+ values.length > 0 &&
117
+ values.every((v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean');
118
+ if (!valid) {
119
+ issues.push({
120
+ code: 'bad-enum',
121
+ path: `${path}.enum`,
122
+ message: '"enum" must list scalars.',
123
+ });
124
+ }
125
+ }
126
+ if (isRecord(node.properties)) {
127
+ for (const [name, child] of Object.entries(node.properties)) {
128
+ visit(child, `${path}.properties.${name}`, depth + 1);
129
+ }
130
+ }
131
+ if (isRecord(node.additionalProperties)) {
132
+ visit(node.additionalProperties, `${path}.additionalProperties`, depth + 1);
133
+ }
134
+ if (node.items !== undefined)
135
+ visit(node.items, `${path}.items`, depth + 1);
136
+ if (isRecord(node.$defs)) {
137
+ for (const [name, child] of Object.entries(node.$defs)) {
138
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name)) {
139
+ issues.push({
140
+ code: 'bad-def-name',
141
+ path: `${path}.$defs.${name}`,
142
+ message: 'Def names are alphanumeric.',
143
+ });
144
+ }
145
+ visit(child, `${path}.$defs.${name}`, depth + 1);
146
+ }
147
+ }
148
+ };
149
+ visit(document, '$', 0);
150
+ return issues;
151
+ }
152
+ /**
153
+ * Validate a JSON value against a dialect schema. `root` resolves `$ref`s
154
+ * (defaults to `schema`). Returns every issue, with JSON paths from `$`.
155
+ */
156
+ export function validateInstance(schema, value, root = schema) {
157
+ const issues = [];
158
+ const fail = (code, path, message) => {
159
+ issues.push({ code, path, message });
160
+ };
161
+ const visit = (node, current, path, depth) => {
162
+ if (depth > MAX_DEPTH) {
163
+ fail('too-deep', path, 'The value nests too deeply.');
164
+ return;
165
+ }
166
+ if (node.$ref !== undefined) {
167
+ const name = refName(node.$ref);
168
+ const target = name === undefined ? undefined : root.$defs?.[name];
169
+ if (target === undefined)
170
+ fail('bad-ref', path, `Unresolved ${node.$ref}.`);
171
+ else
172
+ visit(target, current, path, depth + 1);
173
+ return;
174
+ }
175
+ if (node.const !== undefined && current !== node.const) {
176
+ fail('const', path, `Must be ${JSON.stringify(node.const)}.`);
177
+ return;
178
+ }
179
+ if (node.enum !== undefined && !node.enum.includes(current)) {
180
+ fail('enum', path, `Must be one of ${node.enum.map((v) => JSON.stringify(v)).join(', ')}.`);
181
+ return;
182
+ }
183
+ switch (node.type) {
184
+ case undefined:
185
+ return;
186
+ case 'string':
187
+ if (typeof current !== 'string') {
188
+ fail('type', path, 'Must be a string.');
189
+ return;
190
+ }
191
+ if (node.minLength !== undefined && [...current].length < node.minLength)
192
+ fail('min-length', path, `At least ${node.minLength} characters.`);
193
+ if (node.maxLength !== undefined && [...current].length > node.maxLength)
194
+ fail('max-length', path, `At most ${node.maxLength} characters.`);
195
+ if (node.pattern !== undefined && !new RegExp(node.pattern, 'u').test(current))
196
+ fail('pattern', path, `Must match ${node.pattern}.`);
197
+ return;
198
+ case 'number':
199
+ case 'integer':
200
+ if (typeof current !== 'number' || !Number.isFinite(current)) {
201
+ fail('type', path, 'Must be a finite number.');
202
+ return;
203
+ }
204
+ if (node.type === 'integer' && !Number.isInteger(current)) {
205
+ fail('type', path, 'Must be an integer.');
206
+ return;
207
+ }
208
+ if (node.minimum !== undefined && current < node.minimum)
209
+ fail('minimum', path, `At least ${node.minimum}.`);
210
+ if (node.maximum !== undefined && current > node.maximum)
211
+ fail('maximum', path, `At most ${node.maximum}.`);
212
+ return;
213
+ case 'boolean':
214
+ if (typeof current !== 'boolean')
215
+ fail('type', path, 'Must be a boolean.');
216
+ return;
217
+ case 'array':
218
+ if (!Array.isArray(current)) {
219
+ fail('type', path, 'Must be an array.');
220
+ return;
221
+ }
222
+ if (node.minItems !== undefined && current.length < node.minItems)
223
+ fail('min-items', path, `At least ${node.minItems} items.`);
224
+ if (node.maxItems !== undefined && current.length > node.maxItems) {
225
+ fail('max-items', path, `At most ${node.maxItems} items.`);
226
+ return;
227
+ }
228
+ if (node.items) {
229
+ const items = node.items;
230
+ current.forEach((item, index) => {
231
+ visit(items, item, `${path}[${index}]`, depth + 1);
232
+ });
233
+ }
234
+ return;
235
+ case 'object': {
236
+ if (!isRecord(current)) {
237
+ fail('type', path, 'Must be an object.');
238
+ return;
239
+ }
240
+ const properties = node.properties ?? {};
241
+ for (const name of node.required ?? []) {
242
+ if (!Object.hasOwn(current, name))
243
+ fail('required', `${path}.${name}`, `"${name}" is required.`);
244
+ }
245
+ for (const [key, child] of Object.entries(current)) {
246
+ const here = `${path}.${key}`;
247
+ const declared = Object.hasOwn(properties, key) ? properties[key] : undefined;
248
+ if (declared)
249
+ visit(declared, child, here, depth + 1);
250
+ else if (node.additionalProperties)
251
+ visit(node.additionalProperties, child, here, depth + 1);
252
+ else
253
+ fail('additional-property', here, `"${key}" is not allowed.`);
254
+ }
255
+ return;
256
+ }
257
+ }
258
+ };
259
+ visit(schema, value, '$', 0);
260
+ return issues;
261
+ }
262
+ export function isRecord(value) {
263
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
264
+ }
@@ -0,0 +1,9 @@
1
+ /** The repository formatter's line width (biome.json). */
2
+ export declare const LINE_WIDTH = 100;
3
+ /**
4
+ * Committed JSON in the repository formatter's style, so generated files
5
+ * stay byte-stable under `pnpm lint`: keys sorted, two-space indent, objects
6
+ * always expanded, and an array of scalars kept on one line when it fits the
7
+ * line width. Ends with a newline.
8
+ */
9
+ export declare function formatJson(value: unknown): string;
package/dist/format.js ADDED
@@ -0,0 +1,48 @@
1
+ import { canonicalJson } from "./canonical.js";
2
+ /** The repository formatter's line width (biome.json). */
3
+ export const LINE_WIDTH = 100;
4
+ /**
5
+ * Committed JSON in the repository formatter's style, so generated files
6
+ * stay byte-stable under `pnpm lint`: keys sorted, two-space indent, objects
7
+ * always expanded, and an array of scalars kept on one line when it fits the
8
+ * line width. Ends with a newline.
9
+ */
10
+ export function formatJson(value) {
11
+ // Round-trip through canonical JSON: sorted keys, undefined dropped.
12
+ return `${print(JSON.parse(canonicalJson(value)), 0, '', 0)}\n`;
13
+ }
14
+ function isScalar(value) {
15
+ return value === null || typeof value !== 'object';
16
+ }
17
+ /**
18
+ * `prefix` is what precedes the value on its line (indent and key) and
19
+ * `suffix` the length of what follows it (a comma, or nothing for the last
20
+ * entry), for the width check.
21
+ */
22
+ function print(value, indent, prefix, suffix) {
23
+ if (isScalar(value))
24
+ return JSON.stringify(value);
25
+ const pad = ' '.repeat(indent + 1);
26
+ const close = ' '.repeat(indent);
27
+ if (Array.isArray(value)) {
28
+ if (value.length === 0)
29
+ return '[]';
30
+ if (value.every(isScalar)) {
31
+ const inline = `[${value.map((item) => JSON.stringify(item)).join(', ')}]`;
32
+ if (prefix.length + inline.length + suffix <= LINE_WIDTH)
33
+ return inline;
34
+ }
35
+ const last = value.length - 1;
36
+ const items = value.map((item, index) => `${pad}${print(item, indent + 1, pad, index < last ? 1 : 0)}`);
37
+ return `[\n${items.join(',\n')}\n${close}]`;
38
+ }
39
+ const entries = Object.entries(value);
40
+ if (entries.length === 0)
41
+ return '{}';
42
+ const last = entries.length - 1;
43
+ const lines = entries.map(([key, child], index) => {
44
+ const head = `${pad}${JSON.stringify(key)}: `;
45
+ return `${head}${print(child, indent + 1, head, index < last ? 1 : 0)}`;
46
+ });
47
+ return `{\n${lines.join(',\n')}\n${close}}`;
48
+ }
@@ -0,0 +1,125 @@
1
+ export type BlueprintStatus = 'draft' | 'validated' | 'active' | 'superseded';
2
+ export type BlueprintEmitTier = 'developer' | 'untrusted';
3
+ /** ADR-003 capability profile; portable fails closed until ids are audited portable. */
4
+ export type BlueprintProfile = 'web' | 'portable';
5
+ /** ISO 8601 with a zone. */
6
+ export type BlueprintTime = string;
7
+ export interface BlueprintGenerator {
8
+ kind: 'agent' | 'person' | 'tool';
9
+ name: string;
10
+ model?: string;
11
+ }
12
+ export interface BlueprintSource {
13
+ label: string;
14
+ /** Must also pass the untrusted URL policy. */
15
+ href?: string;
16
+ }
17
+ export interface BlueprintTrigger {
18
+ kind: 'request' | 'schedule' | 'calendar' | 'event';
19
+ label: string;
20
+ }
21
+ export interface BlueprintMeetingRef {
22
+ id: string;
23
+ title?: string;
24
+ startsAt?: BlueprintTime;
25
+ }
26
+ export interface BlueprintActivation {
27
+ by: string;
28
+ at: BlueprintTime;
29
+ digest: string;
30
+ }
31
+ export interface BlueprintProvenanceEntry {
32
+ at: BlueprintTime;
33
+ actor: string;
34
+ kind: 'apply' | 'undo' | 'redo';
35
+ ops: readonly BlueprintEditOp[];
36
+ }
37
+ /** One edit op. The fields each op kind takes are checked by the edit parser. */
38
+ export interface BlueprintEditOp {
39
+ op: 'insert' | 'remove' | 'move' | 'setProp' | 'unsetProp' | 'replace' | 'replaceAll';
40
+ path?: BlueprintNodePath;
41
+ parent?: BlueprintNodePath;
42
+ index?: number;
43
+ name?: string;
44
+ /** Any plain JSON value. */
45
+ value?: unknown;
46
+ node?: BlueprintNode;
47
+ tree?: BlueprintNode;
48
+ }
49
+ export type BlueprintNodePath = readonly number[];
50
+ /** One catalog node: id, props, children, and an optional named slot and condition. */
51
+ export interface BlueprintNode {
52
+ /** A catalog component id. */
53
+ id: string;
54
+ /** Props checked against the component's catalog entry. */
55
+ props?: Readonly<Record<string, unknown>>;
56
+ children?: readonly BlueprintNode[];
57
+ /**
58
+ * The parent's named slot this node fills (audit G04): a slot prop of the parent's catalog entry.
59
+ * Omitted or "children" is the default slot.
60
+ */
61
+ slot?: string;
62
+ when?: BlueprintCondition;
63
+ }
64
+ /**
65
+ * Shows the node when true (audit G07): the closed Condition AST of the blocks formula grammar.
66
+ * False or unknown hides the node. Structural shape only; which operands each op needs, and the
67
+ * size limits, are checked by the runtime formula parser.
68
+ */
69
+ export interface BlueprintCondition {
70
+ op: 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'neq' | 'and' | 'or' | 'not';
71
+ left?: BlueprintNumberFormula;
72
+ right?: BlueprintNumberFormula;
73
+ /** The conditions of and / or. */
74
+ args?: readonly BlueprintCondition[];
75
+ value?: BlueprintCondition;
76
+ }
77
+ /**
78
+ * A number expression of the blocks formula grammar. Structural shape only; the runtime formula
79
+ * parser checks which operands each op needs.
80
+ */
81
+ export interface BlueprintNumberFormula {
82
+ op: 'num' | 'var' | 'add' | 'sub' | 'mul' | 'div' | 'pow' | 'min' | 'max' | 'round' | 'clamp' | 'if';
83
+ /** A number for num; a number formula for round and clamp. */
84
+ value?: unknown;
85
+ /** The input a var reads. */
86
+ name?: string;
87
+ left?: BlueprintNumberFormula;
88
+ right?: BlueprintNumberFormula;
89
+ base?: BlueprintNumberFormula;
90
+ exponent?: BlueprintNumberFormula;
91
+ /** The operands of min / max. */
92
+ args?: readonly BlueprintNumberFormula[];
93
+ digits?: number;
94
+ min?: BlueprintNumberFormula;
95
+ max?: BlueprintNumberFormula;
96
+ test?: BlueprintCondition;
97
+ ifTrue?: BlueprintNumberFormula;
98
+ ifFalse?: BlueprintNumberFormula;
99
+ }
100
+ /**
101
+ * Envelope version 1 around one catalog tree (ADR-003, audit G13). Mirrors what
102
+ * @texuli/catalog/envelope reads today; a declared status, activation or tier is display data,
103
+ * never authority. Planned minor additions: a spec version range, the native profile and several
104
+ * roots.
105
+ */
106
+ export interface BlueprintEnvelope {
107
+ format: 'texuli-blueprint';
108
+ version: 1;
109
+ id: string;
110
+ /** The catalog build hash the tree was written against. */
111
+ catalog: string;
112
+ status: BlueprintStatus;
113
+ createdAt: BlueprintTime;
114
+ updatedAt: BlueprintTime;
115
+ generator: BlueprintGenerator;
116
+ tier?: BlueprintEmitTier;
117
+ profile?: BlueprintProfile;
118
+ asOf?: BlueprintTime;
119
+ sources?: readonly BlueprintSource[];
120
+ trigger?: BlueprintTrigger;
121
+ meetingRef?: BlueprintMeetingRef;
122
+ activatedBy?: BlueprintActivation;
123
+ provenance?: readonly BlueprintProvenanceEntry[];
124
+ tree: BlueprintNode;
125
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by @texuli/spec from schemas/blueprint.schema.json. Do not edit: run the spec generator.
2
+ // Spec 0.2.0, digest d9eccaba1826e7d87648a4a33787d873245592dda843607a10b2c68a77f1806f
3
+ export {};
@@ -0,0 +1,33 @@
1
+ export interface SpecComponent {
2
+ id: string;
3
+ label: string;
4
+ summary: string;
5
+ group: string;
6
+ /** Product placement: the manifest tier. */
7
+ placement: 'app' | 'content';
8
+ /** Emission trust: the tiers that may emit the id. */
9
+ emit: readonly ('developer' | 'untrusted')[];
10
+ /** ADR-003 portability. unknown until audited. */
11
+ capability: 'portable-core' | 'web-only' | 'native-only' | 'unknown';
12
+ props: readonly SpecProp[];
13
+ }
14
+ export interface SpecProp {
15
+ name: string;
16
+ kind: 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object' | 'slot' | 'event' | 'unknown';
17
+ required?: boolean;
18
+ enumValues?: readonly string[];
19
+ /**
20
+ * A named platform extension: the prop does not cross the shared boundary (slot, event and
21
+ * unknown kinds).
22
+ */
23
+ extension?: 'web';
24
+ }
25
+ /**
26
+ * The platform-neutral catalog vocabulary, bootstrapped from the built catalog. Placement, emission
27
+ * trust and portability are separate fields.
28
+ */
29
+ export interface SpecCatalog {
30
+ /** The built catalog this vocabulary was projected from. */
31
+ catalogHash: string;
32
+ components: readonly SpecComponent[];
33
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by @texuli/spec from schemas/catalog.schema.json. Do not edit: run the spec generator.
2
+ // Spec 0.2.0, digest d9eccaba1826e7d87648a4a33787d873245592dda843607a10b2c68a77f1806f
3
+ export {};