@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -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 +13 -0
  10. package/dist/codegen/index.js +12 -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/index.d.ts +17 -0
  24. package/dist/index.js +13 -0
  25. package/dist/validate.d.ts +25 -0
  26. package/dist/validate.js +118 -0
  27. package/dist/version.d.ts +2 -0
  28. package/dist/version.js +2 -0
  29. package/generated/typescript/blueprint.d.ts +83 -0
  30. package/generated/typescript/blueprint.js +3 -0
  31. package/generated/typescript/catalog-schema.d.ts +33 -0
  32. package/generated/typescript/catalog-schema.js +3 -0
  33. package/generated/typescript/diagnostics.d.ts +12 -0
  34. package/generated/typescript/diagnostics.js +3 -0
  35. package/generated/typescript/manifest.d.ts +9 -0
  36. package/generated/typescript/manifest.js +3 -0
  37. package/generated/typescript/vocabulary.d.ts +943 -0
  38. package/generated/typescript/vocabulary.js +138 -0
  39. package/package.json +26 -4
@@ -0,0 +1,118 @@
1
+ import { isRecord, validateInstance } from "./dialect.js";
2
+ const MAX_TREE_DEPTH = 64;
3
+ /**
4
+ * Validate a blueprint envelope against the spec alone: the envelope schema,
5
+ * then every node against the catalog vocabulary (closed ids, closed prop
6
+ * names, prop kinds and enum values, required props, no platform-extension
7
+ * props from data) and the declared profile. With `catalogHash`, an envelope
8
+ * bound to another catalog is refused. This is the shared, target-neutral
9
+ * contract; URL policy, limits, grammar and rendering stay in the runtime
10
+ * validators (`@texuli/catalog/validate`).
11
+ */
12
+ export function validateBlueprint(input, documents, options = {}) {
13
+ const diagnostics = validateInstance(documents.blueprintSchema, input).map((issue) => ({ code: 'schema', path: issue.path, message: `${issue.code}: ${issue.message}` }));
14
+ if (!isRecord(input))
15
+ return { ok: false, diagnostics };
16
+ if (options.catalogHash !== undefined && input.catalog !== options.catalogHash) {
17
+ diagnostics.push({
18
+ code: 'catalog-mismatch',
19
+ path: '$.catalog',
20
+ message: 'The blueprint is bound to another catalog.',
21
+ });
22
+ }
23
+ const portable = input.profile === 'portable';
24
+ if (isRecord(input.tree)) {
25
+ diagnostics.push(...validateTree(input.tree, documents.catalog, { portable, path: '$.tree' }));
26
+ }
27
+ return { ok: diagnostics.length === 0, diagnostics };
28
+ }
29
+ /** Check one catalog tree against the vocabulary. See {@link validateBlueprint}. */
30
+ export function validateTree(tree, catalog, options = {}) {
31
+ const byId = new Map(catalog.components.map((c) => [c.id, c]));
32
+ const out = [];
33
+ const visit = (node, path, depth) => {
34
+ if (!isRecord(node) || typeof node.id !== 'string' || depth > MAX_TREE_DEPTH)
35
+ return;
36
+ const component = byId.get(node.id);
37
+ if (!component) {
38
+ out.push({
39
+ code: 'unknown-id',
40
+ path: `${path}.id`,
41
+ id: node.id,
42
+ message: `"${node.id}" is not in the catalog.`,
43
+ });
44
+ }
45
+ else {
46
+ if (options.portable && component.capability !== 'portable-core') {
47
+ out.push({
48
+ code: 'unsupported-profile',
49
+ path,
50
+ id: node.id,
51
+ message: `"${node.id}" is ${component.capability}, not portable core.`,
52
+ });
53
+ }
54
+ checkProps(node.props, component, `${path}.props`, out);
55
+ }
56
+ if (Array.isArray(node.children)) {
57
+ node.children.forEach((child, index) => {
58
+ visit(child, `${path}.children[${index}]`, depth + 1);
59
+ });
60
+ }
61
+ };
62
+ visit(tree, options.path ?? '$', 0);
63
+ return out;
64
+ }
65
+ function checkProps(props, component, path, out) {
66
+ const given = isRecord(props) ? props : {};
67
+ const id = component.id;
68
+ const declared = new Map(component.props.map((p) => [p.name, p]));
69
+ for (const [name, value] of Object.entries(given)) {
70
+ const here = `${path}.${name}`;
71
+ const prop = declared.get(name);
72
+ if (!prop) {
73
+ out.push({ code: 'unknown-prop', path: here, id, message: `"${id}" has no "${name}" prop.` });
74
+ continue;
75
+ }
76
+ if (prop.extension) {
77
+ out.push({
78
+ code: 'extension-prop',
79
+ path: here,
80
+ id,
81
+ message: `"${name}" is a ${prop.extension} extension (${prop.kind}) and cannot come from data.`,
82
+ });
83
+ continue;
84
+ }
85
+ const kindOk = prop.kind === 'string' || prop.kind === 'enum'
86
+ ? typeof value === 'string'
87
+ : prop.kind === 'number'
88
+ ? typeof value === 'number' && Number.isFinite(value)
89
+ : prop.kind === 'boolean'
90
+ ? typeof value === 'boolean'
91
+ : prop.kind === 'array'
92
+ ? Array.isArray(value)
93
+ : prop.kind === 'object'
94
+ ? isRecord(value)
95
+ : false;
96
+ if (!kindOk) {
97
+ out.push({ code: 'prop-type', path: here, id, message: `"${name}" must be ${prop.kind}.` });
98
+ }
99
+ else if (prop.kind === 'enum' && !(prop.enumValues ?? []).includes(value)) {
100
+ out.push({
101
+ code: 'enum-value',
102
+ path: here,
103
+ id,
104
+ message: `"${name}" must be one of ${(prop.enumValues ?? []).join(', ')}.`,
105
+ });
106
+ }
107
+ }
108
+ for (const prop of component.props) {
109
+ if (prop.required && !prop.extension && !Object.hasOwn(given, prop.name)) {
110
+ out.push({
111
+ code: 'missing-required',
112
+ path: `${path}.${prop.name}`,
113
+ id,
114
+ message: `"${id}" needs "${prop.name}".`,
115
+ });
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,2 @@
1
+ /** The semantic version of the spec documents in this package. */
2
+ export declare const SPEC_VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ /** The semantic version of the spec documents in this package. */
2
+ export const SPEC_VERSION = '0.1.0';
@@ -0,0 +1,83 @@
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 and children only. */
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
+ /**
59
+ * Envelope version 1 around one catalog tree (ADR-003, audit G13). Mirrors what
60
+ * @texuli/catalog/envelope reads today; a declared status, activation or tier is display data,
61
+ * never authority. Planned minor additions: a spec version range, the native profile and several
62
+ * roots.
63
+ */
64
+ export interface BlueprintEnvelope {
65
+ format: 'texuli-blueprint';
66
+ version: 1;
67
+ id: string;
68
+ /** The catalog build hash the tree was written against. */
69
+ catalog: string;
70
+ status: BlueprintStatus;
71
+ createdAt: BlueprintTime;
72
+ updatedAt: BlueprintTime;
73
+ generator: BlueprintGenerator;
74
+ tier?: BlueprintEmitTier;
75
+ profile?: BlueprintProfile;
76
+ asOf?: BlueprintTime;
77
+ sources?: readonly BlueprintSource[];
78
+ trigger?: BlueprintTrigger;
79
+ meetingRef?: BlueprintMeetingRef;
80
+ activatedBy?: BlueprintActivation;
81
+ provenance?: readonly BlueprintProvenanceEntry[];
82
+ tree: BlueprintNode;
83
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by @texuli/spec from schemas/blueprint.schema.json. Do not edit: run the spec generator.
2
+ // Spec 0.1.0, digest 47afaa4f056320fc4111239e03afd97e8a073bd8dbd6d2ef0dec2d171acb2f8e
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.1.0, digest 47afaa4f056320fc4111239e03afd97e8a073bd8dbd6d2ef0dec2d171acb2f8e
3
+ export {};
@@ -0,0 +1,12 @@
1
+ export interface SpecDiagnostic {
2
+ code: SpecDiagnosticCode;
3
+ path: string;
4
+ message: string;
5
+ id?: string;
6
+ }
7
+ export type SpecDiagnosticCode = 'schema' | 'unknown-id' | 'unknown-prop' | 'prop-type' | 'enum-value' | 'missing-required' | 'extension-prop' | 'catalog-mismatch' | 'unsupported-profile';
8
+ /** The stable result shape of validating a blueprint against the spec. */
9
+ export interface SpecValidation {
10
+ ok: boolean;
11
+ diagnostics: readonly SpecDiagnostic[];
12
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by @texuli/spec from schemas/diagnostics.schema.json. Do not edit: run the spec generator.
2
+ // Spec 0.1.0, digest 47afaa4f056320fc4111239e03afd97e8a073bd8dbd6d2ef0dec2d171acb2f8e
3
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The spec version, the dialect and a SHA-256 for every normative file (manifest.json) or generated
3
+ * file (generated-manifest.json).
4
+ */
5
+ export interface SpecManifest {
6
+ specVersion: string;
7
+ dialect: 'https://json-schema.org/draft/2020-12/schema';
8
+ files: Readonly<Record<string, string>>;
9
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by @texuli/spec from schemas/manifest.schema.json. Do not edit: run the spec generator.
2
+ // Spec 0.1.0, digest 47afaa4f056320fc4111239e03afd97e8a073bd8dbd6d2ef0dec2d171acb2f8e
3
+ export {};