@impetik/xeer-mcp 0.2.6 → 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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impetik/xeer-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Model Context Protocol server for Xeer: the scaffold, check, dev, build, and deploy loop as agent tools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"zod": "^4.0.10",
|
|
50
|
-
"@impetik/xeer": "0.2.
|
|
50
|
+
"@impetik/xeer": "0.2.7"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^24.1.0"
|
package/vendor/spec/review.d.ts
CHANGED
|
@@ -74,6 +74,44 @@ export interface XeerReviewReceiptV0 {
|
|
|
74
74
|
}
|
|
75
75
|
export declare function artifactSourceHash(files: readonly SourceReceipt[]): `sha256:${string}`;
|
|
76
76
|
export declare function artifactReviewMetadata(artifact: ApplicationArtifactV0): ArtifactReviewMetadataV0;
|
|
77
|
+
/**
|
|
78
|
+
* Why review metadata was refused. The three cases look alike from the outside and are nothing alike
|
|
79
|
+
* to repair, which is what #205 cost an afternoon to: two of them are version skew between the build
|
|
80
|
+
* that produced the metadata and the build reading it, and only the third means the bundle is wrong.
|
|
81
|
+
*/
|
|
82
|
+
export type ArtifactReviewRejection = {
|
|
83
|
+
readonly code: 'artifact_review_malformed';
|
|
84
|
+
/**
|
|
85
|
+
* The failing paths within `artifactReview`, deduplicated, in the order they were reported —
|
|
86
|
+
* which is the order the payload declares them, and not sorted, because sorting shows `t1, t10,
|
|
87
|
+
* t2` and hides everything past the eighth lexicographic name.
|
|
88
|
+
*
|
|
89
|
+
* **Empty means the value is not an object of this protocol at all**, which is reported at the
|
|
90
|
+
* root where there is no field to name. It has no second meaning.
|
|
91
|
+
*/
|
|
92
|
+
readonly fields: readonly string[];
|
|
93
|
+
/** Failing paths beyond the ones listed, so a truncated list never reads as a complete one. */
|
|
94
|
+
readonly omitted: number;
|
|
95
|
+
} | {
|
|
96
|
+
/**
|
|
97
|
+
* Fields this build has never heard of. Skew, and the usual shape of it: a newer CLI emitting a
|
|
98
|
+
* schema key an older plane does not know reaches the strict object before any hash is compared,
|
|
99
|
+
* so it can only be told apart here.
|
|
100
|
+
*/
|
|
101
|
+
readonly code: 'artifact_review_unknown_fields';
|
|
102
|
+
readonly fields: readonly string[];
|
|
103
|
+
readonly omitted: number;
|
|
104
|
+
} | {
|
|
105
|
+
readonly code: 'artifact_review_schema_identity_mismatch';
|
|
106
|
+
/** What this build hashes the declared schema to. */
|
|
107
|
+
readonly expected: ApplicationSchemaIdentityV0;
|
|
108
|
+
/** What the metadata claims the same schema hashes to. */
|
|
109
|
+
readonly received: ApplicationSchemaIdentityV0;
|
|
110
|
+
};
|
|
111
|
+
export declare class ArtifactReviewError extends Error {
|
|
112
|
+
readonly rejection: ArtifactReviewRejection;
|
|
113
|
+
constructor(rejection: ArtifactReviewRejection, message: string);
|
|
114
|
+
}
|
|
77
115
|
export declare function parseArtifactReviewMetadata(value: unknown): ArtifactReviewMetadataV0;
|
|
78
116
|
export declare function parseTestEvidence(value: unknown): XeerTestEvidenceV0;
|
|
79
117
|
export declare function parseReviewReceipt(value: unknown): XeerReviewReceiptV0;
|
package/vendor/spec/review.js
CHANGED
|
@@ -135,12 +135,63 @@ export function artifactReviewMetadata(artifact) {
|
|
|
135
135
|
schemaIdentity: artifact.schemaIdentity,
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
+
export class ArtifactReviewError extends Error {
|
|
139
|
+
rejection;
|
|
140
|
+
constructor(rejection, message) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.rejection = rejection;
|
|
143
|
+
this.name = 'ArtifactReviewError';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** How many failing paths are worth showing a person, and how long one segment of one may be. */
|
|
147
|
+
const REPORTED_PATHS = 8;
|
|
148
|
+
const SEGMENT_LENGTH = 40;
|
|
149
|
+
const PLAIN_SEGMENT = /^[A-Za-z0-9_$-]+$/u;
|
|
150
|
+
/**
|
|
151
|
+
* One segment of a path, as it can safely be shown.
|
|
152
|
+
*
|
|
153
|
+
* A segment is not always ours: `tables` and `fields` are records keyed by caller-supplied names, so a
|
|
154
|
+
* table name arrives here verbatim. It is length-bounded because the rendered path reaches a 400 body
|
|
155
|
+
* and an operator's log, and quoted unless it is a plain name, because a table called `a.b` would
|
|
156
|
+
* otherwise read as two segments and send someone looking for a field that does not exist.
|
|
157
|
+
*/
|
|
158
|
+
function segment(value) {
|
|
159
|
+
const text = String(value);
|
|
160
|
+
const shown = text.length > SEGMENT_LENGTH ? `${text.slice(0, SEGMENT_LENGTH)}…` : text;
|
|
161
|
+
return PLAIN_SEGMENT.test(shown) ? shown : JSON.stringify(shown);
|
|
162
|
+
}
|
|
163
|
+
function renderPath(parts) {
|
|
164
|
+
return parts.map(segment).join('.');
|
|
165
|
+
}
|
|
166
|
+
/** The first {@link REPORTED_PATHS} of a deduplicated path list, with the count of the rest. */
|
|
167
|
+
function reported(paths) {
|
|
168
|
+
const unique = [...new Set(paths)];
|
|
169
|
+
return { fields: unique.slice(0, REPORTED_PATHS), omitted: Math.max(0, unique.length - REPORTED_PATHS) };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The rejection a failed parse describes: unknown fields where that is the *only* complaint, and a
|
|
173
|
+
* malformed bundle otherwise. Requiring it to be the only complaint is deliberate — a payload that is
|
|
174
|
+
* both unrecognized and broken is broken, and calling it skew would point the repair at the wrong side.
|
|
175
|
+
*/
|
|
176
|
+
function parseRejection(error) {
|
|
177
|
+
if (error.issues.every((issue) => issue.code === 'unrecognized_keys')) {
|
|
178
|
+
return { code: 'artifact_review_unknown_fields',
|
|
179
|
+
...reported(error.issues.flatMap((issue) => issue.code === 'unrecognized_keys'
|
|
180
|
+
? issue.keys.map((key) => renderPath([...issue.path, key])) : [])) };
|
|
181
|
+
}
|
|
182
|
+
return { code: 'artifact_review_malformed',
|
|
183
|
+
...reported(error.issues.map((issue) => renderPath(issue.path)).filter((path) => path !== '')) };
|
|
184
|
+
}
|
|
138
185
|
export function parseArtifactReviewMetadata(value) {
|
|
139
|
-
const
|
|
186
|
+
const result = artifactReview.safeParse(value);
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
throw new ArtifactReviewError(parseRejection(result.error), 'Artifact review metadata does not match xeer.artifact-review.v0.');
|
|
189
|
+
}
|
|
190
|
+
const parsed = result.data;
|
|
140
191
|
const expected = applicationSchemaIdentity(parsed.schema);
|
|
141
192
|
if (parsed.schemaIdentity.applicationSchemaVersion !== expected.applicationSchemaVersion
|
|
142
193
|
|| parsed.schemaIdentity.schemaHash !== expected.schemaHash) {
|
|
143
|
-
throw new
|
|
194
|
+
throw new ArtifactReviewError({ code: 'artifact_review_schema_identity_mismatch', expected, received: parsed.schemaIdentity }, 'Artifact review schemaIdentity does not match its normalized schema.');
|
|
144
195
|
}
|
|
145
196
|
return parsed;
|
|
146
197
|
}
|
|
@@ -3,4 +3,25 @@ import { type NormalizedDatabaseSchemaV0, type SchemaPlanV0 } from './schema-pla
|
|
|
3
3
|
export * from './schema-plan.js';
|
|
4
4
|
export declare function applicationSchemaHash(schema: NormalizedDatabaseSchemaV0): `sha256:${string}`;
|
|
5
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}`;
|
|
6
27
|
export declare function planSchemaChange(application: string, fromSchema: NormalizedDatabaseSchemaV0, toSchema: NormalizedDatabaseSchemaV0): SchemaPlanV0;
|
|
@@ -54,6 +54,93 @@ export function applicationSchemaHash(schema) {
|
|
|
54
54
|
export function applicationSchemaIdentity(schema) {
|
|
55
55
|
return { applicationSchemaVersion: schema.version, schemaHash: applicationSchemaHash(schema) };
|
|
56
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
|
+
}
|
|
57
144
|
export function planSchemaChange(application, fromSchema, toSchema) {
|
|
58
145
|
return planSchemaChangeWithIdentities(application, fromSchema, applicationSchemaIdentity(fromSchema), toSchema, applicationSchemaIdentity(toSchema));
|
|
59
146
|
}
|