@impetik/xeer-mcp 0.2.5 → 0.2.7

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 (54) hide show
  1. package/README.md +4 -1
  2. package/dist/dev-session.d.ts +1 -1
  3. package/dist/network-policy.js +1 -1
  4. package/dist/server.d.ts +1 -1
  5. package/dist/server.js +4 -4
  6. package/dist/test-run.d.ts +1 -1
  7. package/dist/xeer-cli.d.ts +1 -1
  8. package/package.json +8 -5
  9. package/vendor/spec/actions.d.ts +1250 -0
  10. package/vendor/spec/actions.js +805 -0
  11. package/vendor/spec/admin-sql.d.ts +59 -0
  12. package/vendor/spec/admin-sql.js +147 -0
  13. package/vendor/spec/admin.d.ts +110 -0
  14. package/vendor/spec/admin.js +58 -0
  15. package/vendor/spec/canonical.d.ts +3 -0
  16. package/vendor/spec/canonical.js +36 -0
  17. package/vendor/spec/diagnostics.d.ts +49 -0
  18. package/vendor/spec/diagnostics.js +500 -0
  19. package/vendor/spec/docs.d.ts +21 -0
  20. package/vendor/spec/docs.js +57 -0
  21. package/vendor/spec/events.d.ts +8 -0
  22. package/vendor/spec/events.js +21 -0
  23. package/vendor/spec/identity-keys.d.ts +36 -0
  24. package/vendor/spec/identity-keys.js +72 -0
  25. package/vendor/spec/index.d.ts +20 -0
  26. package/vendor/spec/index.js +20 -0
  27. package/vendor/spec/local-identity.d.ts +69 -0
  28. package/vendor/spec/local-identity.js +132 -0
  29. package/vendor/spec/network-policy.d.ts +16 -0
  30. package/vendor/spec/network-policy.js +50 -0
  31. package/vendor/spec/public-assets.d.ts +153 -0
  32. package/vendor/spec/public-assets.js +166 -0
  33. package/vendor/spec/review.d.ts +120 -0
  34. package/vendor/spec/review.js +226 -0
  35. package/vendor/spec/route.d.ts +43 -0
  36. package/vendor/spec/route.js +87 -0
  37. package/vendor/spec/schema-lifecycle.d.ts +27 -0
  38. package/vendor/spec/schema-lifecycle.js +146 -0
  39. package/vendor/spec/schema-plan.d.ts +98 -0
  40. package/vendor/spec/schema-plan.js +194 -0
  41. package/vendor/spec/schema.d.ts +166 -0
  42. package/vendor/spec/schema.js +409 -0
  43. package/vendor/spec/sql-expression.d.ts +91 -0
  44. package/vendor/spec/sql-expression.js +650 -0
  45. package/vendor/spec/state-export.d.ts +143 -0
  46. package/vendor/spec/state-export.js +341 -0
  47. package/vendor/spec/storage.d.ts +61 -0
  48. package/vendor/spec/storage.js +120 -0
  49. package/vendor/spec/table-ddl.d.ts +162 -0
  50. package/vendor/spec/table-ddl.js +508 -0
  51. package/vendor/spec/types.d.ts +275 -0
  52. package/vendor/spec/types.js +11 -0
  53. package/vendor/spec/value.d.ts +22 -0
  54. package/vendor/spec/value.js +72 -0
@@ -0,0 +1,27 @@
1
+ import type { ApplicationSchemaIdentityV0 } from './types.js';
2
+ import { type NormalizedDatabaseSchemaV0, type SchemaPlanV0 } from './schema-plan.js';
3
+ export * from './schema-plan.js';
4
+ export declare function applicationSchemaHash(schema: NormalizedDatabaseSchemaV0): `sha256:${string}`;
5
+ export declare function applicationSchemaIdentity(schema: NormalizedDatabaseSchemaV0): ApplicationSchemaIdentityV0;
6
+ /** A fresh copy of the reference schema, so nothing that reads it can move the revision for everyone. */
7
+ export declare function schemaIdentityReference(): NormalizedDatabaseSchemaV0;
8
+ /**
9
+ * Which schema-identity projection *this build* computes, as the hash it gives the reference schema
10
+ * above.
11
+ *
12
+ * A schema hash is only an identity because two builds agree on it, and when they stop agreeing every
13
+ * symptom is somewhere else: the control plane refuses a `schemaIdentity` the CLI computed correctly
14
+ * for its own build, and the deployment reads as a malformed bundle (#205). Nothing about a version
15
+ * number settles it either way — a release can move the projection or leave it untouched — so the
16
+ * answer has to be a value derived from the projection itself. Two builds reporting the same revision
17
+ * hash the same schema to the same identity; two reporting different ones do not, and the older is the
18
+ * one to upgrade.
19
+ *
20
+ * Derived rather than declared, so it cannot be forgotten: adding, removing or renaming a key in
21
+ * `normalizedSchemaPayload` moves this hash in the same commit that moves it, because every key is
22
+ * emitted unconditionally and the reference declares one of each. What it cannot see is a change to how
23
+ * a value is *derived* for a shape the reference does not declare — index normalization has its own
24
+ * spellings, and only the ones written here are covered.
25
+ */
26
+ export declare function schemaIdentityRevision(): `sha256:${string}`;
27
+ export declare function planSchemaChange(application: string, fromSchema: NormalizedDatabaseSchemaV0, toSchema: NormalizedDatabaseSchemaV0): SchemaPlanV0;
@@ -0,0 +1,146 @@
1
+ import { canonicalHash } from './canonical.js';
2
+ import { normalizedIndex } from './table-ddl.js';
3
+ import { planSchemaChangeWithIdentities, } from './schema-plan.js';
4
+ export * from './schema-plan.js';
5
+ /**
6
+ * The projection of a schema that its hash is taken over.
7
+ *
8
+ * Every key here is one that changes what the database will accept, store, or return: the hash is
9
+ * the identity of the *contract*, and two schemas that hold data to different rules must not share
10
+ * one. That is why nothing is omitted as a detail — a collation decides which values collide under
11
+ * a UNIQUE, a default decides what an omitted column receives, an index's direction and predicate
12
+ * decide which physical index has to exist. A key that only changed how a schema *reads* — a label,
13
+ * a description — would belong outside this projection, and there is no such key today.
14
+ *
15
+ * The projection is explicit rather than a spread of the definition so that adding a key to the
16
+ * manifest is a decision made here, in one place, instead of a hash that quietly moves.
17
+ */
18
+ function normalizedSchemaPayload(schema) {
19
+ return {
20
+ tables: Object.fromEntries(Object.keys(schema.tables).sort().map((tableName) => {
21
+ const table = schema.tables[tableName];
22
+ return [tableName, {
23
+ fields: Object.fromEntries(Object.keys(table.fields).sort().map((fieldName) => {
24
+ const field = table.fields[fieldName];
25
+ return [fieldName, {
26
+ type: field.type,
27
+ optional: field.optional ?? false,
28
+ maxLength: field.maxLength ?? null,
29
+ unique: field.unique ?? false,
30
+ enum: field.enum === undefined ? null : [...field.enum],
31
+ default: field.default ?? null,
32
+ collate: field.collate ?? null,
33
+ table: field.table ?? null,
34
+ onDelete: field.onDelete ?? null,
35
+ onUpdate: field.onUpdate ?? null,
36
+ generated: field.generated === undefined
37
+ ? null
38
+ : { expression: field.generated.expression, stored: field.generated.stored ?? false },
39
+ }];
40
+ })),
41
+ // Normalized, so the shorthand and the full form of one index hash the same.
42
+ indexes: Object.fromEntries(Object.keys(table.indexes ?? {}).sort()
43
+ .map((indexName) => [indexName, normalizedIndex(table.indexes[indexName])])),
44
+ unique: (table.unique ?? []).map((tuple) => [...tuple]),
45
+ checks: Object.fromEntries(Object.keys(table.checks ?? {}).sort()
46
+ .map((checkName) => [checkName, table.checks[checkName]])),
47
+ }];
48
+ })),
49
+ };
50
+ }
51
+ export function applicationSchemaHash(schema) {
52
+ return canonicalHash(normalizedSchemaPayload(schema));
53
+ }
54
+ export function applicationSchemaIdentity(schema) {
55
+ return { applicationSchemaVersion: schema.version, schemaHash: applicationSchemaHash(schema) };
56
+ }
57
+ /**
58
+ * A fixed schema declaring one of everything the projection above reads, so that hashing it says which
59
+ * projection a build carries.
60
+ *
61
+ * Every attribute sits on a field named after it, which is what lets the test delete them one at a time
62
+ * and prove the revision notices. The declaration is a hash probe and not a manifest: `onDelete` and
63
+ * `onUpdate` need a `ref` field to sit on, so there are three of those, and nothing here is validated
64
+ * against the manifest rules because nothing here is ever deployed.
65
+ *
66
+ * Two properties of the *shape* are load-bearing, and both exist to close a way the revision could
67
+ * otherwise agree while the builds do not.
68
+ *
69
+ * **Two of everything the projection can iterate.** One table, one check, one index, one unique tuple
70
+ * would each let a projection that read only the first of them hash this schema exactly as a correct
71
+ * one does.
72
+ *
73
+ * **Every order-preserving array declared in an order sorting would change.** Object keys are sorted by
74
+ * canonical JSON and cannot carry order, but four arrays can and all four are semantic: an enum's
75
+ * values, the outer and inner arrays of a composite UNIQUE, and an index's columns — `UNIQUE (a, b)`
76
+ * and `UNIQUE (b, a)` build different indexes. A reference declaring a single unique tuple, or a tuple
77
+ * that happened to be in sorted order, would survive a future commit that started sorting them
78
+ * unchanged, while the hash of every real schema with two moved. Two builds would then compute
79
+ * different schema identities and report the *same* revision, and a `schema_identity_mismatch` would
80
+ * quote it as proof that they agree.
81
+ */
82
+ const SCHEMA_IDENTITY_REFERENCE = {
83
+ version: 1,
84
+ tables: {
85
+ reference: {
86
+ fields: {
87
+ bytes: { type: 'bytes' },
88
+ collate: { type: 'string', collate: 'nocase' },
89
+ datetime: { type: 'datetime' },
90
+ default: { type: 'string', default: 'a' },
91
+ enum: { type: 'string', enum: ['b', 'a'] },
92
+ generated: { type: 'string', generated: { expression: 'lower(plain)', stored: true } },
93
+ json: { type: 'json' },
94
+ maxLength: { type: 'string', maxLength: 64 },
95
+ number: { type: 'number' },
96
+ onDelete: { type: 'ref', table: 'reference', onDelete: 'cascade' },
97
+ onUpdate: { type: 'ref', table: 'reference', onUpdate: 'restrict' },
98
+ optional: { type: 'string', optional: true },
99
+ plain: { type: 'string' },
100
+ table: { type: 'ref', table: 'reference' },
101
+ unique: { type: 'string', unique: true },
102
+ },
103
+ indexes: {
104
+ by_plain: ['plain'],
105
+ by_pair: ['plain', 'number'],
106
+ by_unique: { columns: [{ column: 'unique', desc: true }], unique: true, where: 'plain IS NOT NULL' },
107
+ },
108
+ unique: [['plain', 'number'], ['optional', 'maxLength']],
109
+ checks: { positive: 'number > 0', bounded: 'maxLength IS NOT NULL' },
110
+ },
111
+ beta: {
112
+ fields: { second: { type: 'string' }, first: { type: 'number' } },
113
+ indexes: { by_second: ['second'], by_first: ['first'] },
114
+ unique: [['second', 'first']],
115
+ checks: { nonzero: 'first <> 0', named: 'second <> \'\'' },
116
+ },
117
+ },
118
+ };
119
+ /** A fresh copy of the reference schema, so nothing that reads it can move the revision for everyone. */
120
+ export function schemaIdentityReference() {
121
+ return structuredClone(SCHEMA_IDENTITY_REFERENCE);
122
+ }
123
+ /**
124
+ * Which schema-identity projection *this build* computes, as the hash it gives the reference schema
125
+ * above.
126
+ *
127
+ * A schema hash is only an identity because two builds agree on it, and when they stop agreeing every
128
+ * symptom is somewhere else: the control plane refuses a `schemaIdentity` the CLI computed correctly
129
+ * for its own build, and the deployment reads as a malformed bundle (#205). Nothing about a version
130
+ * number settles it either way — a release can move the projection or leave it untouched — so the
131
+ * answer has to be a value derived from the projection itself. Two builds reporting the same revision
132
+ * hash the same schema to the same identity; two reporting different ones do not, and the older is the
133
+ * one to upgrade.
134
+ *
135
+ * Derived rather than declared, so it cannot be forgotten: adding, removing or renaming a key in
136
+ * `normalizedSchemaPayload` moves this hash in the same commit that moves it, because every key is
137
+ * emitted unconditionally and the reference declares one of each. What it cannot see is a change to how
138
+ * a value is *derived* for a shape the reference does not declare — index normalization has its own
139
+ * spellings, and only the ones written here are covered.
140
+ */
141
+ export function schemaIdentityRevision() {
142
+ return applicationSchemaHash(SCHEMA_IDENTITY_REFERENCE);
143
+ }
144
+ export function planSchemaChange(application, fromSchema, toSchema) {
145
+ return planSchemaChangeWithIdentities(application, fromSchema, applicationSchemaIdentity(fromSchema), toSchema, applicationSchemaIdentity(toSchema));
146
+ }
@@ -0,0 +1,98 @@
1
+ import type { ApplicationSchemaIdentityV0, NormalizedApplicationManifestV0, ScalarType } from './types.js';
2
+ export declare const SCHEMA_PROTOCOL: "xeer.schema.v0";
3
+ export type NormalizedDatabaseSchemaV0 = NormalizedApplicationManifestV0['database'];
4
+ /**
5
+ * Field keys with no dedicated step. Each one changes what the column's DDL says, so a change to any
6
+ * of them has to reach the plan — otherwise a schema whose hash moved would produce no steps and be
7
+ * classified as an unchanged schema that somehow needs a version bump.
8
+ */
9
+ export declare const FIELD_ATTRIBUTES: readonly ["collate", "default", "enum", "generated", "onDelete", "onUpdate", "table", "unique"];
10
+ export type FieldAttribute = (typeof FIELD_ATTRIBUTES)[number];
11
+ export type SchemaPlanStepV0 = {
12
+ kind: 'table.add';
13
+ table: string;
14
+ } | {
15
+ kind: 'table.remove';
16
+ table: string;
17
+ } | {
18
+ kind: 'table.constraint';
19
+ table: string;
20
+ constraint: 'unique' | 'checks';
21
+ from: unknown;
22
+ to: unknown;
23
+ } | {
24
+ kind: 'field.add';
25
+ table: string;
26
+ field: string;
27
+ optional: boolean;
28
+ } | {
29
+ kind: 'field.remove';
30
+ table: string;
31
+ field: string;
32
+ } | {
33
+ kind: 'field.type';
34
+ table: string;
35
+ field: string;
36
+ from: ScalarType;
37
+ to: ScalarType;
38
+ } | {
39
+ kind: 'field.optional';
40
+ table: string;
41
+ field: string;
42
+ from: boolean;
43
+ to: boolean;
44
+ } | {
45
+ kind: 'field.maxLength';
46
+ table: string;
47
+ field: string;
48
+ from: number | null;
49
+ to: number | null;
50
+ } | {
51
+ kind: 'field.attribute';
52
+ table: string;
53
+ field: string;
54
+ attribute: FieldAttribute;
55
+ from: unknown;
56
+ to: unknown;
57
+ } | {
58
+ kind: 'index.add';
59
+ table: string;
60
+ index: string;
61
+ fields: string[];
62
+ } | {
63
+ kind: 'index.remove';
64
+ table: string;
65
+ index: string;
66
+ fields: string[];
67
+ } | {
68
+ kind: 'index.change';
69
+ table: string;
70
+ index: string;
71
+ from: string[];
72
+ to: string[];
73
+ } | {
74
+ kind: 'index.attribute';
75
+ table: string;
76
+ index: string;
77
+ attribute: 'unique' | 'where';
78
+ from: unknown;
79
+ to: unknown;
80
+ } | {
81
+ kind: 'unknown.change';
82
+ path: string;
83
+ };
84
+ export interface SchemaPlanV0 {
85
+ protocol: typeof SCHEMA_PROTOCOL;
86
+ application: string;
87
+ from: ApplicationSchemaIdentityV0;
88
+ to: ApplicationSchemaIdentityV0;
89
+ classification: 'unchanged' | 'compatible' | 'incompatible';
90
+ steps: SchemaPlanStepV0[];
91
+ requiresReset: boolean;
92
+ }
93
+ export type SchemaPlanningErrorCode = 'schema_version_regressed' | 'schema_version_unchanged' | 'schema_version_skipped';
94
+ export declare class SchemaPlanningError extends Error {
95
+ readonly code: SchemaPlanningErrorCode;
96
+ constructor(code: SchemaPlanningErrorCode, message: string);
97
+ }
98
+ export declare function planSchemaChangeWithIdentities(application: string, fromSchema: NormalizedDatabaseSchemaV0, from: ApplicationSchemaIdentityV0, toSchema: NormalizedDatabaseSchemaV0, to: ApplicationSchemaIdentityV0): SchemaPlanV0;
@@ -0,0 +1,194 @@
1
+ import { indexTermLabel, normalizedIndex } from './table-ddl.js';
2
+ export const SCHEMA_PROTOCOL = 'xeer.schema.v0';
3
+ /**
4
+ * Field keys with no dedicated step. Each one changes what the column's DDL says, so a change to any
5
+ * of them has to reach the plan — otherwise a schema whose hash moved would produce no steps and be
6
+ * classified as an unchanged schema that somehow needs a version bump.
7
+ */
8
+ export const FIELD_ATTRIBUTES = Object.freeze([
9
+ 'collate', 'default', 'enum', 'generated', 'onDelete', 'onUpdate', 'table', 'unique',
10
+ ]);
11
+ export class SchemaPlanningError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'SchemaPlanningError';
17
+ }
18
+ }
19
+ function sameArray(left, right) {
20
+ return left.length === right.length && left.every((value, index) => value === right[index]);
21
+ }
22
+ function maxLengthStep(table, field, from, to) {
23
+ if (from === to)
24
+ return null;
25
+ return { kind: 'field.maxLength', table, field, from: from ?? null, to: to ?? null };
26
+ }
27
+ function tableSteps(tableName, from, to) {
28
+ const steps = [];
29
+ const fromFields = new Set(Object.keys(from.fields));
30
+ const toFields = new Set(Object.keys(to.fields));
31
+ for (const field of [...fromFields].filter((name) => !toFields.has(name)).sort()) {
32
+ steps.push({ kind: 'field.remove', table: tableName, field });
33
+ }
34
+ for (const field of [...toFields].filter((name) => !fromFields.has(name)).sort()) {
35
+ steps.push({ kind: 'field.add', table: tableName, field, optional: to.fields[field].optional ?? false });
36
+ }
37
+ for (const field of [...fromFields].filter((name) => toFields.has(name)).sort()) {
38
+ const before = from.fields[field];
39
+ const after = to.fields[field];
40
+ if (before.type !== after.type) {
41
+ steps.push({ kind: 'field.type', table: tableName, field, from: before.type, to: after.type });
42
+ }
43
+ const beforeOptional = before.optional ?? false;
44
+ const afterOptional = after.optional ?? false;
45
+ if (beforeOptional !== afterOptional) {
46
+ steps.push({ kind: 'field.optional', table: tableName, field, from: beforeOptional, to: afterOptional });
47
+ }
48
+ const length = maxLengthStep(tableName, field, before.maxLength, after.maxLength);
49
+ if (length)
50
+ steps.push(length);
51
+ for (const attribute of FIELD_ATTRIBUTES) {
52
+ if (sameUnknownValue(before[attribute], after[attribute]))
53
+ continue;
54
+ steps.push({ kind: 'field.attribute', table: tableName, field, attribute,
55
+ from: before[attribute] ?? null, to: after[attribute] ?? null });
56
+ }
57
+ }
58
+ for (const constraint of ['unique', 'checks']) {
59
+ if (sameUnknownValue(from[constraint], to[constraint]))
60
+ continue;
61
+ steps.push({ kind: 'table.constraint', table: tableName, constraint,
62
+ from: from[constraint] ?? null, to: to[constraint] ?? null });
63
+ }
64
+ // Indexes are compared through their normalized form, so `["slug"]` and `{ columns: ["slug"] }`
65
+ // are one index written two ways rather than a change from one to the other.
66
+ const terms = (index) => normalizedIndex(index).columns.map(indexTermLabel);
67
+ const fromIndexes = from.indexes ?? {};
68
+ const toIndexes = to.indexes ?? {};
69
+ const fromNames = new Set(Object.keys(fromIndexes));
70
+ const toNames = new Set(Object.keys(toIndexes));
71
+ for (const index of [...fromNames].filter((name) => !toNames.has(name)).sort()) {
72
+ steps.push({ kind: 'index.remove', table: tableName, index, fields: terms(fromIndexes[index]) });
73
+ }
74
+ for (const index of [...toNames].filter((name) => !fromNames.has(name)).sort()) {
75
+ steps.push({ kind: 'index.add', table: tableName, index, fields: terms(toIndexes[index]) });
76
+ }
77
+ for (const index of [...fromNames].filter((name) => toNames.has(name)).sort()) {
78
+ const before = normalizedIndex(fromIndexes[index]);
79
+ const after = normalizedIndex(toIndexes[index]);
80
+ if (!sameArray(terms(fromIndexes[index]), terms(toIndexes[index]))) {
81
+ steps.push({ kind: 'index.change', table: tableName, index,
82
+ from: terms(fromIndexes[index]), to: terms(toIndexes[index]) });
83
+ }
84
+ for (const attribute of ['unique', 'where']) {
85
+ if (before[attribute] === after[attribute])
86
+ continue;
87
+ steps.push({ kind: 'index.attribute', table: tableName, index, attribute,
88
+ from: before[attribute], to: after[attribute] });
89
+ }
90
+ }
91
+ return steps;
92
+ }
93
+ /**
94
+ * Whether a step leaves the rows already stored valid. Nothing in the extended vocabulary does,
95
+ * with one exception: a DEFAULT decides what a later insert that omits the column receives and says
96
+ * nothing about the rows already there, so changing one cannot invalidate them.
97
+ */
98
+ function isCompatible(step) {
99
+ if (step.kind === 'table.add' || step.kind === 'index.add')
100
+ return true;
101
+ if (step.kind === 'field.add')
102
+ return step.optional;
103
+ if (step.kind === 'field.optional')
104
+ return step.from === false && step.to === true;
105
+ if (step.kind === 'field.maxLength') {
106
+ return step.from !== null && (step.to === null || step.to > step.from);
107
+ }
108
+ if (step.kind === 'field.attribute')
109
+ return step.attribute === 'default';
110
+ return false;
111
+ }
112
+ function sameUnknownValue(left, right) {
113
+ if (Object.is(left, right))
114
+ return true;
115
+ if (typeof left !== typeof right || left === null || right === null)
116
+ return false;
117
+ if (Array.isArray(left) || Array.isArray(right)) {
118
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length
119
+ && left.every((value, index) => sameUnknownValue(value, right[index]));
120
+ }
121
+ if (typeof left !== 'object')
122
+ return false;
123
+ const before = left;
124
+ const after = right;
125
+ const beforeKeys = Object.keys(before).sort();
126
+ const afterKeys = Object.keys(after).sort();
127
+ return sameArray(beforeKeys, afterKeys)
128
+ && beforeKeys.every((key) => sameUnknownValue(before[key], after[key]));
129
+ }
130
+ function unknownKeySteps(path, from, to, allowed) {
131
+ const before = typeof from === 'object' && from !== null && !Array.isArray(from)
132
+ ? from : {};
133
+ const after = typeof to === 'object' && to !== null && !Array.isArray(to)
134
+ ? to : {};
135
+ const known = new Set(allowed);
136
+ return [...new Set([...Object.keys(before), ...Object.keys(after)])]
137
+ .filter((key) => !known.has(key))
138
+ .sort()
139
+ .filter((key) => {
140
+ const beforeHas = Object.hasOwn(before, key);
141
+ const afterHas = Object.hasOwn(after, key);
142
+ return beforeHas !== afterHas || !sameUnknownValue(before[key], after[key]);
143
+ })
144
+ .map((key) => ({ kind: 'unknown.change', path: `${path}.${key}` }));
145
+ }
146
+ function unknownSchemaSteps(fromSchema, toSchema) {
147
+ const steps = unknownKeySteps('database', fromSchema, toSchema, ['version', 'tables']);
148
+ const tableNames = [...new Set([...Object.keys(fromSchema.tables), ...Object.keys(toSchema.tables)])].sort();
149
+ for (const tableName of tableNames) {
150
+ const beforeTable = fromSchema.tables[tableName];
151
+ const afterTable = toSchema.tables[tableName];
152
+ steps.push(...unknownKeySteps(`database.tables.${tableName}`, beforeTable, afterTable, ['fields', 'indexes', 'unique', 'checks']));
153
+ const beforeFields = beforeTable?.fields ?? {};
154
+ const afterFields = afterTable?.fields ?? {};
155
+ const fieldNames = [...new Set([...Object.keys(beforeFields), ...Object.keys(afterFields)])].sort();
156
+ for (const fieldName of fieldNames) {
157
+ steps.push(...unknownKeySteps(`database.tables.${tableName}.fields.${fieldName}`, beforeFields[fieldName], afterFields[fieldName], ['type', 'optional', 'maxLength', ...FIELD_ATTRIBUTES]));
158
+ }
159
+ }
160
+ return steps;
161
+ }
162
+ export function planSchemaChangeWithIdentities(application, fromSchema, from, toSchema, to) {
163
+ const unknownSteps = unknownSchemaSteps(fromSchema, toSchema);
164
+ if (to.applicationSchemaVersion < from.applicationSchemaVersion) {
165
+ throw new SchemaPlanningError('schema_version_regressed', `Application schema version cannot regress from ${from.applicationSchemaVersion} to ${to.applicationSchemaVersion}.`);
166
+ }
167
+ if (to.applicationSchemaVersion === from.applicationSchemaVersion
168
+ && (to.schemaHash !== from.schemaHash || unknownSteps.length > 0)) {
169
+ throw new SchemaPlanningError('schema_version_unchanged', `Application schema changed without increasing database.version from ${from.applicationSchemaVersion}.`);
170
+ }
171
+ if (to.applicationSchemaVersion > from.applicationSchemaVersion + 1) {
172
+ throw new SchemaPlanningError('schema_version_skipped', `Application schema version must advance one step from ${from.applicationSchemaVersion}; received ${to.applicationSchemaVersion}.`);
173
+ }
174
+ if (to.schemaHash === from.schemaHash && unknownSteps.length === 0
175
+ && to.applicationSchemaVersion === from.applicationSchemaVersion) {
176
+ return { protocol: SCHEMA_PROTOCOL, application, from, to,
177
+ classification: 'unchanged', steps: [], requiresReset: false };
178
+ }
179
+ const steps = [...unknownSteps];
180
+ const fromTables = new Set(Object.keys(fromSchema.tables));
181
+ const toTables = new Set(Object.keys(toSchema.tables));
182
+ for (const table of [...fromTables].filter((name) => !toTables.has(name)).sort()) {
183
+ steps.push({ kind: 'table.remove', table });
184
+ }
185
+ for (const table of [...toTables].filter((name) => !fromTables.has(name)).sort()) {
186
+ steps.push({ kind: 'table.add', table });
187
+ }
188
+ for (const table of [...fromTables].filter((name) => toTables.has(name)).sort()) {
189
+ steps.push(...tableSteps(table, fromSchema.tables[table], toSchema.tables[table]));
190
+ }
191
+ const classification = steps.every(isCompatible) ? 'compatible' : 'incompatible';
192
+ return { protocol: SCHEMA_PROTOCOL, application, from, to, classification, steps,
193
+ requiresReset: classification === 'incompatible' };
194
+ }
@@ -0,0 +1,166 @@
1
+ import { z } from 'zod';
2
+ import { type ApplicationManifestV0, type NormalizedApplicationManifestV0 } from './types.js';
3
+ /** The normalized database fragment embedded in artifacts and review metadata. */
4
+ export declare const normalizedDatabaseSchema: z.ZodObject<{
5
+ version: z.ZodNumber;
6
+ tables: z.ZodRecord<z.ZodString, z.ZodObject<{
7
+ fields: z.ZodRecord<z.ZodString, z.ZodObject<{
8
+ type: z.ZodEnum<{
9
+ string: "string";
10
+ number: "number";
11
+ boolean: "boolean";
12
+ datetime: "datetime";
13
+ bytes: "bytes";
14
+ json: "json";
15
+ ref: "ref";
16
+ }>;
17
+ optional: z.ZodOptional<z.ZodBoolean>;
18
+ maxLength: z.ZodOptional<z.ZodNumber>;
19
+ unique: z.ZodOptional<z.ZodBoolean>;
20
+ enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
22
+ collate: z.ZodOptional<z.ZodEnum<{
23
+ binary: "binary";
24
+ nocase: "nocase";
25
+ rtrim: "rtrim";
26
+ }>>;
27
+ table: z.ZodOptional<z.ZodString>;
28
+ onDelete: z.ZodOptional<z.ZodEnum<{
29
+ noAction: "noAction";
30
+ restrict: "restrict";
31
+ cascade: "cascade";
32
+ setNull: "setNull";
33
+ setDefault: "setDefault";
34
+ }>>;
35
+ onUpdate: z.ZodOptional<z.ZodEnum<{
36
+ noAction: "noAction";
37
+ restrict: "restrict";
38
+ cascade: "cascade";
39
+ setNull: "setNull";
40
+ setDefault: "setDefault";
41
+ }>>;
42
+ generated: z.ZodOptional<z.ZodObject<{
43
+ expression: z.ZodString;
44
+ stored: z.ZodOptional<z.ZodBoolean>;
45
+ }, z.core.$strict>>;
46
+ }, z.core.$strict>>;
47
+ indexes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
48
+ columns: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
49
+ column: z.ZodString;
50
+ collate: z.ZodOptional<z.ZodEnum<{
51
+ binary: "binary";
52
+ nocase: "nocase";
53
+ rtrim: "rtrim";
54
+ }>>;
55
+ desc: z.ZodOptional<z.ZodBoolean>;
56
+ }, z.core.$strict>, z.ZodObject<{
57
+ expression: z.ZodString;
58
+ desc: z.ZodOptional<z.ZodBoolean>;
59
+ }, z.core.$strict>]>>;
60
+ unique: z.ZodOptional<z.ZodBoolean>;
61
+ where: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strict>]>>>;
63
+ unique: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodString>>>;
64
+ checks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
65
+ }, z.core.$strict>>;
66
+ }, z.core.$strict>;
67
+ export declare const applicationManifestSchema: z.ZodObject<{
68
+ $schema: z.ZodOptional<z.ZodString>;
69
+ format: z.ZodLiteral<"xeer.application-source.v0">;
70
+ name: z.ZodString;
71
+ entrypoints: z.ZodObject<{
72
+ client: z.ZodString;
73
+ server: z.ZodString;
74
+ }, z.core.$strict>;
75
+ app: z.ZodOptional<z.ZodObject<{
76
+ spa: z.ZodOptional<z.ZodBoolean>;
77
+ title: z.ZodOptional<z.ZodString>;
78
+ language: z.ZodOptional<z.ZodString>;
79
+ description: z.ZodOptional<z.ZodString>;
80
+ favicon: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strict>>;
82
+ database: z.ZodOptional<z.ZodObject<{
83
+ version: z.ZodOptional<z.ZodNumber>;
84
+ tables: z.ZodRecord<z.ZodString, z.ZodObject<{
85
+ fields: z.ZodRecord<z.ZodString, z.ZodObject<{
86
+ type: z.ZodEnum<{
87
+ string: "string";
88
+ number: "number";
89
+ boolean: "boolean";
90
+ datetime: "datetime";
91
+ bytes: "bytes";
92
+ json: "json";
93
+ ref: "ref";
94
+ }>;
95
+ optional: z.ZodOptional<z.ZodBoolean>;
96
+ maxLength: z.ZodOptional<z.ZodNumber>;
97
+ unique: z.ZodOptional<z.ZodBoolean>;
98
+ enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
99
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
100
+ collate: z.ZodOptional<z.ZodEnum<{
101
+ binary: "binary";
102
+ nocase: "nocase";
103
+ rtrim: "rtrim";
104
+ }>>;
105
+ table: z.ZodOptional<z.ZodString>;
106
+ onDelete: z.ZodOptional<z.ZodEnum<{
107
+ noAction: "noAction";
108
+ restrict: "restrict";
109
+ cascade: "cascade";
110
+ setNull: "setNull";
111
+ setDefault: "setDefault";
112
+ }>>;
113
+ onUpdate: z.ZodOptional<z.ZodEnum<{
114
+ noAction: "noAction";
115
+ restrict: "restrict";
116
+ cascade: "cascade";
117
+ setNull: "setNull";
118
+ setDefault: "setDefault";
119
+ }>>;
120
+ generated: z.ZodOptional<z.ZodObject<{
121
+ expression: z.ZodString;
122
+ stored: z.ZodOptional<z.ZodBoolean>;
123
+ }, z.core.$strict>>;
124
+ }, z.core.$strict>>;
125
+ indexes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
126
+ columns: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
127
+ column: z.ZodString;
128
+ collate: z.ZodOptional<z.ZodEnum<{
129
+ binary: "binary";
130
+ nocase: "nocase";
131
+ rtrim: "rtrim";
132
+ }>>;
133
+ desc: z.ZodOptional<z.ZodBoolean>;
134
+ }, z.core.$strict>, z.ZodObject<{
135
+ expression: z.ZodString;
136
+ desc: z.ZodOptional<z.ZodBoolean>;
137
+ }, z.core.$strict>]>>;
138
+ unique: z.ZodOptional<z.ZodBoolean>;
139
+ where: z.ZodOptional<z.ZodString>;
140
+ }, z.core.$strict>]>>>;
141
+ unique: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodString>>>;
142
+ checks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
143
+ }, z.core.$strict>>;
144
+ }, z.core.$strict>>;
145
+ storage: z.ZodOptional<z.ZodObject<{
146
+ maxObjectBytes: z.ZodOptional<z.ZodNumber>;
147
+ readBytes: z.ZodOptional<z.ZodNumber>;
148
+ writeBytes: z.ZodOptional<z.ZodNumber>;
149
+ }, z.core.$strict>>;
150
+ capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
151
+ database: "database";
152
+ storage: "storage";
153
+ }>>>;
154
+ budgets: z.ZodOptional<z.ZodObject<{
155
+ queryRows: z.ZodOptional<z.ZodNumber>;
156
+ mutationWrites: z.ZodOptional<z.ZodNumber>;
157
+ requestBytes: z.ZodOptional<z.ZodNumber>;
158
+ responseBytes: z.ZodOptional<z.ZodNumber>;
159
+ liveConnections: z.ZodOptional<z.ZodNumber>;
160
+ }, z.core.$strict>>;
161
+ }, z.core.$strict>;
162
+ /** Stable editor-facing schema location emitted into every scaffolded `xeer.app.json`. */
163
+ export declare const APPLICATION_MANIFEST_SCHEMA_URL: "https://docs.xeer.run/application-v0.schema.json";
164
+ export declare const applicationManifestJsonSchema: Readonly<Record<string, unknown>>;
165
+ export declare function parseApplicationManifest(value: unknown): ApplicationManifestV0;
166
+ export declare function normalizeApplicationManifest(manifest: ApplicationManifestV0): NormalizedApplicationManifestV0;