@myna-sh/cli 0.9.0 → 0.10.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/dist/dsl.d.ts +117 -1
- package/dist/dsl.js +169 -1
- package/dist/dsl.js.map +1 -1
- package/dist/main.js +93 -17
- package/dist/main.js.map +1 -1
- package/package.json +3 -3
package/dist/dsl.d.ts
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content policy, declared in schema-as-code next to the collection's guidance.
|
|
3
|
+
*
|
|
4
|
+
* Myna used to ship a fixed set of editorial checks — an SEO length rule, a
|
|
5
|
+
* mandatory alt-text rule — and run them against every project whether or not
|
|
6
|
+
* anyone asked. Two things were wrong with that. The rules were opinions nobody
|
|
7
|
+
* consented to, and they were guesses: the SEO rule looked for fields named
|
|
8
|
+
* `seoTitle` and `metaTitle`, so a collection whose title field is called
|
|
9
|
+
* `title` got a `passed` verdict on a rule that had inspected nothing. An
|
|
10
|
+
* unasked-for check that silently protects nobody is worse than no check, and
|
|
11
|
+
* the gap it leaves gets filled by a script in someone's build — which is where
|
|
12
|
+
* a content rule fires *after* publishing rather than before.
|
|
13
|
+
*
|
|
14
|
+
* So a collection declares its own rules, in the same file as its fields, and
|
|
15
|
+
* `guidance` says what good looks like while `policy` says what is refused.
|
|
16
|
+
* The distinction that matters:
|
|
17
|
+
*
|
|
18
|
+
* - `schema`, `references`, `conflicts`, and `slugs` stay built in. They are
|
|
19
|
+
* preconditions for a coherent publish, not taste — the way git refuses a
|
|
20
|
+
* non-fast-forward push.
|
|
21
|
+
* - Everything editorial is declared here or does not run.
|
|
22
|
+
*
|
|
23
|
+
* Rules are plain data, so they version and diff with the schema, travel to
|
|
24
|
+
* `myna_get_collection_schema`, and reach an agent *before* it writes.
|
|
25
|
+
*/
|
|
26
|
+
type PolicyRule = {
|
|
27
|
+
rule: "length";
|
|
28
|
+
/** Dotted path to the field, e.g. `title` or `seo.description`. */
|
|
29
|
+
field: string;
|
|
30
|
+
min?: number;
|
|
31
|
+
max?: number;
|
|
32
|
+
unit: "characters" | "words";
|
|
33
|
+
reason?: string;
|
|
34
|
+
} | {
|
|
35
|
+
rule: "required";
|
|
36
|
+
fields: string[];
|
|
37
|
+
reason?: string;
|
|
38
|
+
} | {
|
|
39
|
+
rule: "bannedTerms";
|
|
40
|
+
terms: string[];
|
|
41
|
+
fields?: string[];
|
|
42
|
+
reason?: string;
|
|
43
|
+
} | {
|
|
44
|
+
rule: "altText";
|
|
45
|
+
reason?: string;
|
|
46
|
+
};
|
|
47
|
+
interface LengthOptions {
|
|
48
|
+
min?: number;
|
|
49
|
+
max?: number;
|
|
50
|
+
/** Count words rather than characters. Characters by default. */
|
|
51
|
+
words?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Why the limit exists, shown with the violation.
|
|
54
|
+
*
|
|
55
|
+
* Worth writing: "title is 78 characters (limit 53)" tells someone what to do
|
|
56
|
+
* but not what they broke, and a limit whose reason nobody remembers is a
|
|
57
|
+
* limit somebody eventually raises.
|
|
58
|
+
*/
|
|
59
|
+
reason?: string;
|
|
60
|
+
}
|
|
61
|
+
declare const policy: {
|
|
62
|
+
/** Bound a field's length, in characters or words. */
|
|
63
|
+
length(field: string, options: LengthOptions): PolicyRule;
|
|
64
|
+
/**
|
|
65
|
+
* Fields that must carry a value before this collection publishes.
|
|
66
|
+
*
|
|
67
|
+
* Distinct from `required` on the field itself, which refuses the draft. A
|
|
68
|
+
* draft is allowed to be incomplete — that is what drafting is — and this is
|
|
69
|
+
* the gate at the end of it.
|
|
70
|
+
*/
|
|
71
|
+
required(fields: string[], options?: {
|
|
72
|
+
reason?: string;
|
|
73
|
+
}): PolicyRule;
|
|
74
|
+
/** Refuse terms anywhere in the entry's text, or in named fields only. */
|
|
75
|
+
bannedTerms(terms: string[], options?: {
|
|
76
|
+
fields?: string[];
|
|
77
|
+
reason?: string;
|
|
78
|
+
}): PolicyRule;
|
|
79
|
+
/**
|
|
80
|
+
* Every image the entry references must carry alt text.
|
|
81
|
+
*
|
|
82
|
+
* Shipped as a rule you declare rather than one Myna assumes. It was a
|
|
83
|
+
* built-in check until it became clear that "every project wants this" is a
|
|
84
|
+
* claim about other people's products.
|
|
85
|
+
*/
|
|
86
|
+
altText(options?: {
|
|
87
|
+
reason?: string;
|
|
88
|
+
}): PolicyRule;
|
|
89
|
+
};
|
|
90
|
+
interface PolicyViolation {
|
|
91
|
+
/** Dotted field path, or empty when the rule is about the entry as a whole. */
|
|
92
|
+
path: string;
|
|
93
|
+
message: string;
|
|
94
|
+
rule: PolicyRule["rule"];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Evaluate the rules that depend only on the entry's own data.
|
|
98
|
+
*
|
|
99
|
+
* `altText` is deliberately not evaluated here: it needs to resolve asset
|
|
100
|
+
* records, which is the API's job. Callers that can do that lookup add its
|
|
101
|
+
* violations to this result.
|
|
102
|
+
*/
|
|
103
|
+
declare function evaluatePolicy(rules: PolicyRule[], data: Record<string, unknown>): PolicyViolation[];
|
|
104
|
+
|
|
1
105
|
/**
|
|
2
106
|
* Canonical schema representation (SPEC 6.2 / 7).
|
|
3
107
|
*
|
|
@@ -5,6 +109,7 @@
|
|
|
5
109
|
* order is significant and preserved as an array; object *keys* are sorted only
|
|
6
110
|
* during canonical stringification for a stable hash.
|
|
7
111
|
*/
|
|
112
|
+
|
|
8
113
|
type FieldType = "text" | "number" | "boolean" | "date" | "datetime" | "markdown" | "richText" | "json" | "asset" | "reference" | "list" | "object" | "blocks" | "slug";
|
|
9
114
|
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
10
115
|
[key: string]: JsonValue;
|
|
@@ -177,6 +282,12 @@ interface CollectionSchema {
|
|
|
177
282
|
* the rest of it.
|
|
178
283
|
*/
|
|
179
284
|
guidance?: string;
|
|
285
|
+
/**
|
|
286
|
+
* Rules this collection refuses to publish against, declared here rather than
|
|
287
|
+
* assumed by Myna. `guidance` says what good looks like; this says what is
|
|
288
|
+
* rejected. Evaluated by the `policy` check on every change set.
|
|
289
|
+
*/
|
|
290
|
+
policy?: PolicyRule[];
|
|
180
291
|
fields: FieldDef[];
|
|
181
292
|
}
|
|
182
293
|
/** Maximum object/list nesting depth (SPEC 6.2). */
|
|
@@ -324,6 +435,11 @@ interface CollectionInput {
|
|
|
324
435
|
* dashboard editor, the MCP schema tool, an agent planning a draft.
|
|
325
436
|
*/
|
|
326
437
|
guidance?: string;
|
|
438
|
+
/**
|
|
439
|
+
* Rules this collection refuses to publish against. `guidance` is advice;
|
|
440
|
+
* this is enforced, by the `policy` check, before anything ships.
|
|
441
|
+
*/
|
|
442
|
+
policy?: PolicyRule[];
|
|
327
443
|
fields: Record<string, FieldInput>;
|
|
328
444
|
}
|
|
329
445
|
/**
|
|
@@ -332,4 +448,4 @@ interface CollectionInput {
|
|
|
332
448
|
*/
|
|
333
449
|
declare function collection(input: CollectionInput): CollectionSchema;
|
|
334
450
|
|
|
335
|
-
export { type AssetField, type AssetOptions, type BlockDef, type BlocksField, type BlocksOptions, type BooleanField, type BooleanOptions, type CollectionInput, type CollectionKind, type CollectionSchema, type DateField, type DateOptions, type EntryLocaleOptions, type FieldDef, type FieldInput, type FieldType, type FieldUi, type JsonField, type JsonOptions, type JsonValue, type ListField, type ListItem, type ListOptions, MAX_NESTING_DEPTH, type MarkdownField, type MarkdownOptions, type NumberField, type NumberOptions, type ObjectField, type ObjectOptions, type PrimitiveItem, type ReferenceField, type ReferenceOptions, type RichTextField, type RichTextOptions, type SlugConfig, type SlugField, type SlugOptions, type TextField, type TextOptions, type Visibility, assignKeys, block, collection, field, item };
|
|
451
|
+
export { type AssetField, type AssetOptions, type BlockDef, type BlocksField, type BlocksOptions, type BooleanField, type BooleanOptions, type CollectionInput, type CollectionKind, type CollectionSchema, type DateField, type DateOptions, type EntryLocaleOptions, type FieldDef, type FieldInput, type FieldType, type FieldUi, type JsonField, type JsonOptions, type JsonValue, type LengthOptions, type ListField, type ListItem, type ListOptions, MAX_NESTING_DEPTH, type MarkdownField, type MarkdownOptions, type NumberField, type NumberOptions, type ObjectField, type ObjectOptions, type PolicyRule, type PolicyViolation, type PrimitiveItem, type ReferenceField, type ReferenceOptions, type RichTextField, type RichTextOptions, type SlugConfig, type SlugField, type SlugOptions, type TextField, type TextOptions, type Visibility, assignKeys, block, collection, evaluatePolicy, field, item, policy };
|
package/dist/dsl.js
CHANGED
|
@@ -188,6 +188,7 @@ function collection(input) {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
const guidance = input.guidance ? { guidance: input.guidance } : {};
|
|
191
|
+
const policy2 = input.policy && input.policy.length > 0 ? { policy: input.policy } : {};
|
|
191
192
|
return {
|
|
192
193
|
name: input.name,
|
|
193
194
|
label: input.label ?? input.name,
|
|
@@ -197,15 +198,182 @@ function collection(input) {
|
|
|
197
198
|
slug,
|
|
198
199
|
path: input.path ?? null,
|
|
199
200
|
...guidance,
|
|
201
|
+
...policy2,
|
|
200
202
|
fields
|
|
201
203
|
};
|
|
202
204
|
}
|
|
205
|
+
|
|
206
|
+
// ../schema/src/diff.ts
|
|
207
|
+
function isPlainObject(v) {
|
|
208
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
209
|
+
}
|
|
210
|
+
function plainText(value) {
|
|
211
|
+
if (typeof value === "string") return value;
|
|
212
|
+
if (Array.isArray(value)) return nodeText(value);
|
|
213
|
+
if (isPlainObject(value) && value.type === "doc" && Array.isArray(value.content)) {
|
|
214
|
+
return nodeText(value.content);
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function nodeText(nodes) {
|
|
219
|
+
const parts = [];
|
|
220
|
+
let hasBlocks = false;
|
|
221
|
+
for (const node of nodes) {
|
|
222
|
+
if (!isPlainObject(node) || typeof node.type !== "string") return null;
|
|
223
|
+
if (typeof node.text === "string") {
|
|
224
|
+
parts.push(node.text);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
hasBlocks = true;
|
|
228
|
+
if (Array.isArray(node.content)) {
|
|
229
|
+
const inner = nodeText(node.content);
|
|
230
|
+
if (inner === null) return null;
|
|
231
|
+
parts.push(inner);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return parts.join(hasBlocks ? "\n\n" : "");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ../schema/src/policy.ts
|
|
238
|
+
var policy = {
|
|
239
|
+
/** Bound a field's length, in characters or words. */
|
|
240
|
+
length(field2, options) {
|
|
241
|
+
const rule = {
|
|
242
|
+
rule: "length",
|
|
243
|
+
field: field2,
|
|
244
|
+
unit: options.words ? "words" : "characters"
|
|
245
|
+
};
|
|
246
|
+
if (options.min !== void 0) rule.min = options.min;
|
|
247
|
+
if (options.max !== void 0) rule.max = options.max;
|
|
248
|
+
if (options.reason) rule.reason = options.reason;
|
|
249
|
+
return rule;
|
|
250
|
+
},
|
|
251
|
+
/**
|
|
252
|
+
* Fields that must carry a value before this collection publishes.
|
|
253
|
+
*
|
|
254
|
+
* Distinct from `required` on the field itself, which refuses the draft. A
|
|
255
|
+
* draft is allowed to be incomplete — that is what drafting is — and this is
|
|
256
|
+
* the gate at the end of it.
|
|
257
|
+
*/
|
|
258
|
+
required(fields, options = {}) {
|
|
259
|
+
const rule = { rule: "required", fields };
|
|
260
|
+
if (options.reason) rule.reason = options.reason;
|
|
261
|
+
return rule;
|
|
262
|
+
},
|
|
263
|
+
/** Refuse terms anywhere in the entry's text, or in named fields only. */
|
|
264
|
+
bannedTerms(terms, options = {}) {
|
|
265
|
+
const rule = { rule: "bannedTerms", terms };
|
|
266
|
+
if (options.fields) rule.fields = options.fields;
|
|
267
|
+
if (options.reason) rule.reason = options.reason;
|
|
268
|
+
return rule;
|
|
269
|
+
},
|
|
270
|
+
/**
|
|
271
|
+
* Every image the entry references must carry alt text.
|
|
272
|
+
*
|
|
273
|
+
* Shipped as a rule you declare rather than one Myna assumes. It was a
|
|
274
|
+
* built-in check until it became clear that "every project wants this" is a
|
|
275
|
+
* claim about other people's products.
|
|
276
|
+
*/
|
|
277
|
+
altText(options = {}) {
|
|
278
|
+
const rule = { rule: "altText" };
|
|
279
|
+
if (options.reason) rule.reason = options.reason;
|
|
280
|
+
return rule;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function valueAt(data, path) {
|
|
284
|
+
let cursor = data;
|
|
285
|
+
for (const key of path.split(".")) {
|
|
286
|
+
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) return void 0;
|
|
287
|
+
cursor = cursor[key];
|
|
288
|
+
}
|
|
289
|
+
return cursor;
|
|
290
|
+
}
|
|
291
|
+
function isEmpty(value) {
|
|
292
|
+
if (value === void 0 || value === null) return true;
|
|
293
|
+
if (typeof value === "string") return value.trim() === "";
|
|
294
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
function allText(value, out = []) {
|
|
298
|
+
if (typeof value === "string") out.push(value);
|
|
299
|
+
else if (Array.isArray(value)) for (const v of value) allText(v, out);
|
|
300
|
+
else if (typeof value === "object" && value !== null) {
|
|
301
|
+
for (const v of Object.values(value)) allText(v, out);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
function countWords(text) {
|
|
306
|
+
const trimmed = text.trim();
|
|
307
|
+
return trimmed === "" ? 0 : trimmed.split(/\s+/).length;
|
|
308
|
+
}
|
|
309
|
+
function evaluatePolicy(rules, data) {
|
|
310
|
+
const violations = [];
|
|
311
|
+
const because = (rule) => rule.reason ? ` ${rule.reason}` : "";
|
|
312
|
+
for (const rule of rules) {
|
|
313
|
+
switch (rule.rule) {
|
|
314
|
+
case "length": {
|
|
315
|
+
const text = plainText(valueAt(data, rule.field));
|
|
316
|
+
if (text === null) break;
|
|
317
|
+
const size = rule.unit === "words" ? countWords(text) : text.length;
|
|
318
|
+
const unit = rule.unit === "words" ? "words" : "characters";
|
|
319
|
+
if (rule.max !== void 0 && size > rule.max) {
|
|
320
|
+
violations.push({
|
|
321
|
+
path: rule.field,
|
|
322
|
+
rule: "length",
|
|
323
|
+
message: `${rule.field} is ${size} ${unit} (limit ${rule.max}).${because(rule)}`
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (rule.min !== void 0 && size < rule.min) {
|
|
327
|
+
violations.push({
|
|
328
|
+
path: rule.field,
|
|
329
|
+
rule: "length",
|
|
330
|
+
message: `${rule.field} is ${size} ${unit} (minimum ${rule.min}).${because(rule)}`
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case "required": {
|
|
336
|
+
for (const field2 of rule.fields) {
|
|
337
|
+
if (isEmpty(valueAt(data, field2))) {
|
|
338
|
+
violations.push({
|
|
339
|
+
path: field2,
|
|
340
|
+
rule: "required",
|
|
341
|
+
message: `${field2} must be set before publishing.${because(rule)}`
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "bannedTerms": {
|
|
348
|
+
const haystacks = rule.fields ? rule.fields.map((f) => ({ path: f, text: plainText(valueAt(data, f)) ?? "" })) : [{ path: "", text: allText(data).join("\n") }];
|
|
349
|
+
for (const { path, text } of haystacks) {
|
|
350
|
+
const lower = text.toLowerCase();
|
|
351
|
+
for (const term of rule.terms) {
|
|
352
|
+
if (lower.includes(term.toLowerCase())) {
|
|
353
|
+
violations.push({
|
|
354
|
+
path,
|
|
355
|
+
rule: "bannedTerms",
|
|
356
|
+
message: `Contains "${term}".${because(rule)}`
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case "altText":
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return violations;
|
|
368
|
+
}
|
|
203
369
|
export {
|
|
204
370
|
MAX_NESTING_DEPTH,
|
|
205
371
|
assignKeys,
|
|
206
372
|
block,
|
|
207
373
|
collection,
|
|
374
|
+
evaluatePolicy,
|
|
208
375
|
field,
|
|
209
|
-
item
|
|
376
|
+
item,
|
|
377
|
+
policy
|
|
210
378
|
};
|
|
211
379
|
//# sourceMappingURL=dsl.js.map
|
package/dist/dsl.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../schema/src/types.ts","../../schema/src/fields.ts","../../schema/src/collection.ts"],"sourcesContent":["/**\n * Canonical schema representation (SPEC 6.2 / 7).\n *\n * These are the normalized, serializable shapes stored as `schema_json`. Field\n * order is significant and preserved as an array; object *keys* are sorted only\n * during canonical stringification for a stable hash.\n */\n\nexport type FieldType =\n | \"text\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"datetime\"\n | \"markdown\"\n | \"richText\"\n | \"json\"\n | \"asset\"\n | \"reference\"\n | \"list\"\n | \"object\"\n | \"blocks\"\n | \"slug\";\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\n/** UI metadata carried on every field (non-semantic, does not affect validation). */\nexport interface FieldUi {\n widget?: string;\n placeholder?: string;\n helpText?: string;\n group?: string;\n hidden?: boolean;\n}\n\ninterface FieldBase {\n key: string;\n type: FieldType;\n label: string;\n description?: string;\n required: boolean;\n /** Localized fields store `{ [locale]: value }` keyed by project locale.\n * Only top-level, non-slug fields may be localized. */\n localized?: boolean;\n ui?: FieldUi;\n}\n\n/** Locale context for entry validation and locale resolution. */\nexport interface EntryLocaleOptions {\n locales: string[];\n defaultLocale: string;\n}\n\nexport interface TextField extends FieldBase {\n type: \"text\";\n multiline: boolean;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n enum?: string[];\n default?: string;\n}\n\nexport interface NumberField extends FieldBase {\n type: \"number\";\n integer: boolean;\n min?: number;\n max?: number;\n default?: number;\n}\n\nexport interface BooleanField extends FieldBase {\n type: \"boolean\";\n default?: boolean;\n}\n\nexport interface DateField extends FieldBase {\n type: \"date\" | \"datetime\";\n min?: string;\n max?: string;\n default?: string;\n}\n\nexport interface MarkdownField extends FieldBase {\n type: \"markdown\";\n minLength?: number;\n maxLength?: number;\n default?: string;\n}\n\nexport interface JsonField extends FieldBase {\n type: \"json\";\n default?: JsonValue;\n}\n\nexport interface AssetField extends FieldBase {\n type: \"asset\";\n /** Allowed MIME families, e.g. `image/*`, `application/pdf`. */\n allowed: string[];\n multiple: boolean;\n}\n\nexport interface ReferenceField extends FieldBase {\n type: \"reference\";\n /** Target collection key. */\n target: string;\n multiple: boolean;\n}\n\nexport interface SlugField extends FieldBase {\n type: \"slug\";\n /** Source field key to derive the slug from. */\n from?: string;\n}\n\nexport type PrimitiveItem =\n | { kind: \"text\"; minLength?: number; maxLength?: number; pattern?: string; enum?: string[] }\n | { kind: \"number\"; integer?: boolean; min?: number; max?: number }\n | { kind: \"boolean\" }\n | { kind: \"date\" | \"datetime\"; min?: string; max?: string }\n | { kind: \"markdown\"; minLength?: number; maxLength?: number }\n | { kind: \"json\" };\n\nexport type ListItem =\n | PrimitiveItem\n | { kind: \"reference\"; target: string }\n | { kind: \"asset\"; allowed: string[] }\n | { kind: \"object\"; fields: FieldDef[] };\n\nexport interface ListField extends FieldBase {\n type: \"list\";\n item: ListItem;\n minItems?: number;\n maxItems?: number;\n}\n\nexport interface ObjectField extends FieldBase {\n type: \"object\";\n fields: FieldDef[];\n}\n\n/** Semantic rich text stored as a portable JSON document tree — never HTML.\n * The root is `{ \"type\": \"doc\", \"content\": [...] }`; nodes may embed entry\n * references (`ent_` ids) and assets (`ast_` ids). */\nexport interface RichTextField extends FieldBase {\n type: \"richText\";\n}\n\n/** One schema-defined component usable inside a `blocks` field. */\nexport interface BlockDef {\n key: string;\n label: string;\n fields: FieldDef[];\n}\n\n/** Heterogeneous, schema-defined component list. Stored as portable JSON:\n * `[{ \"type\": \"<blockKey>\", \"fields\": { ... } }, ...]`. */\nexport interface BlocksField extends FieldBase {\n type: \"blocks\";\n blocks: BlockDef[];\n minItems?: number;\n maxItems?: number;\n}\n\nexport type FieldDef =\n | TextField\n | NumberField\n | BooleanField\n | DateField\n | MarkdownField\n | RichTextField\n | JsonField\n | AssetField\n | ReferenceField\n | SlugField\n | ListField\n | ObjectField\n | BlocksField;\n\nexport type CollectionKind = \"collection\" | \"singleton\";\nexport type Visibility = \"public\" | \"private\";\n\nexport interface SlugConfig {\n from: string;\n required: boolean;\n}\n\n/** Canonical collection schema (one `collection_versions.schema_json`). */\nexport interface CollectionSchema {\n name: string;\n label: string;\n kind: CollectionKind;\n visibility: Visibility;\n titleField: string | null;\n slug: SlugConfig | null;\n path: string | null;\n /**\n * How this collection is meant to be written: voice, length, conventions,\n * what belongs here and what does not.\n *\n * A field `description` says what a field is for. This says what *good*\n * looks like, which is the thing an agent has no way to infer from types and\n * the thing a new human contributor is told verbally and then forgets. It\n * travels with the schema, so it is versioned, reviewed, and deployed like\n * the rest of it.\n */\n guidance?: string;\n fields: FieldDef[];\n}\n\n/** Maximum object/list nesting depth (SPEC 6.2). */\nexport const MAX_NESTING_DEPTH = 4;\n","import type {\n AssetField,\n BlockDef,\n BlocksField,\n BooleanField,\n DateField,\n FieldDef,\n FieldUi,\n JsonField,\n JsonValue,\n ListField,\n ListItem,\n MarkdownField,\n NumberField,\n ObjectField,\n ReferenceField,\n RichTextField,\n SlugField,\n TextField,\n} from \"./types.js\";\n\n/** A field definition before its `key` is assigned by `collection()`. */\nexport type FieldInput = DistributiveOmit<FieldDef, \"key\">;\n\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;\n\ninterface CommonOptions {\n label?: string;\n description?: string;\n required?: boolean;\n localized?: boolean;\n ui?: FieldUi;\n}\n\nfunction common(o: CommonOptions): {\n label?: string;\n description?: string;\n required: boolean;\n localized?: boolean;\n ui?: FieldUi;\n} {\n const base: { label?: string; description?: string; required: boolean; localized?: boolean; ui?: FieldUi } = {\n required: o.required ?? false,\n };\n if (o.label !== undefined) base.label = o.label;\n if (o.description !== undefined) base.description = o.description;\n if (o.localized !== undefined) base.localized = o.localized;\n if (o.ui !== undefined) base.ui = o.ui;\n return base;\n}\n\nexport interface TextOptions extends CommonOptions {\n multiline?: boolean;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n enum?: string[];\n default?: string;\n}\nexport interface NumberOptions extends CommonOptions {\n integer?: boolean;\n min?: number;\n max?: number;\n default?: number;\n}\nexport interface BooleanOptions extends CommonOptions {\n default?: boolean;\n}\nexport interface DateOptions extends CommonOptions {\n min?: string;\n max?: string;\n default?: string;\n}\nexport interface MarkdownOptions extends CommonOptions {\n minLength?: number;\n maxLength?: number;\n default?: string;\n}\nexport interface JsonOptions extends CommonOptions {\n default?: JsonValue;\n}\nexport interface AssetOptions extends CommonOptions {\n allowed?: string[];\n multiple?: boolean;\n}\nexport interface ReferenceOptions extends CommonOptions {\n to: string;\n multiple?: boolean;\n}\nexport interface SlugOptions extends CommonOptions {\n from?: string;\n}\nexport interface ListOptions extends CommonOptions {\n of: ListItem;\n minItems?: number;\n maxItems?: number;\n}\nexport interface ObjectOptions extends CommonOptions {\n fields: Record<string, FieldInput>;\n}\nexport type RichTextOptions = CommonOptions;\nexport interface BlocksOptions extends CommonOptions {\n /** The block components entries may compose, built with `block()`. */\n allowed: BlockDef[];\n minItems?: number;\n maxItems?: number;\n}\n\n/** Define a reusable block component for `field.blocks({ allowed: [...] })`. */\nexport function block(key: string, o: { label?: string; fields: Record<string, FieldInput> }): BlockDef {\n return { key, label: o.label ?? key, fields: assignKeys(o.fields) };\n}\n\nfunction defined<T extends Record<string, unknown>>(obj: T): T {\n for (const key of Object.keys(obj)) {\n if (obj[key] === undefined) delete obj[key];\n }\n return obj;\n}\n\nexport const field = {\n text(o: TextOptions = {}): Omit<TextField, \"key\"> {\n return defined({\n type: \"text\",\n ...common(o),\n multiline: o.multiline ?? false,\n minLength: o.minLength,\n maxLength: o.maxLength,\n pattern: o.pattern,\n enum: o.enum,\n default: o.default,\n }) as Omit<TextField, \"key\">;\n },\n\n number(o: NumberOptions = {}): Omit<NumberField, \"key\"> {\n return defined({\n type: \"number\",\n ...common(o),\n integer: o.integer ?? false,\n min: o.min,\n max: o.max,\n default: o.default,\n }) as Omit<NumberField, \"key\">;\n },\n\n boolean(o: BooleanOptions = {}): Omit<BooleanField, \"key\"> {\n return defined({\n type: \"boolean\",\n ...common(o),\n default: o.default,\n }) as Omit<BooleanField, \"key\">;\n },\n\n date(o: DateOptions = {}): Omit<DateField, \"key\"> {\n return defined({\n type: \"date\",\n ...common(o),\n min: o.min,\n max: o.max,\n default: o.default,\n }) as Omit<DateField, \"key\">;\n },\n\n datetime(o: DateOptions = {}): Omit<DateField, \"key\"> {\n return defined({\n type: \"datetime\",\n ...common(o),\n min: o.min,\n max: o.max,\n default: o.default,\n }) as Omit<DateField, \"key\">;\n },\n\n markdown(o: MarkdownOptions = {}): Omit<MarkdownField, \"key\"> {\n return defined({\n type: \"markdown\",\n ...common(o),\n minLength: o.minLength,\n maxLength: o.maxLength,\n default: o.default,\n }) as Omit<MarkdownField, \"key\">;\n },\n\n json(o: JsonOptions = {}): Omit<JsonField, \"key\"> {\n return defined({\n type: \"json\",\n ...common(o),\n default: o.default,\n }) as Omit<JsonField, \"key\">;\n },\n\n asset(o: AssetOptions = {}): Omit<AssetField, \"key\"> {\n return defined({\n type: \"asset\",\n ...common(o),\n allowed: o.allowed ?? [\"*/*\"],\n multiple: o.multiple ?? false,\n }) as Omit<AssetField, \"key\">;\n },\n\n reference(o: ReferenceOptions): Omit<ReferenceField, \"key\"> {\n return defined({\n type: \"reference\",\n ...common(o),\n target: o.to,\n multiple: o.multiple ?? false,\n }) as Omit<ReferenceField, \"key\">;\n },\n\n slug(o: SlugOptions = {}): Omit<SlugField, \"key\"> {\n return defined({\n type: \"slug\",\n ...common(o),\n from: o.from,\n }) as Omit<SlugField, \"key\">;\n },\n\n list(o: ListOptions): Omit<ListField, \"key\"> {\n return defined({\n type: \"list\",\n ...common(o),\n item: o.of,\n minItems: o.minItems,\n maxItems: o.maxItems,\n }) as Omit<ListField, \"key\">;\n },\n\n object(o: ObjectOptions): Omit<ObjectField, \"key\"> {\n return defined({\n type: \"object\",\n ...common(o),\n fields: assignKeys(o.fields),\n }) as Omit<ObjectField, \"key\">;\n },\n\n richText(o: RichTextOptions = {}): Omit<RichTextField, \"key\"> {\n return defined({\n type: \"richText\",\n ...common(o),\n }) as Omit<RichTextField, \"key\">;\n },\n\n blocks(o: BlocksOptions): Omit<BlocksField, \"key\"> {\n return defined({\n type: \"blocks\",\n ...common(o),\n blocks: o.allowed,\n minItems: o.minItems,\n maxItems: o.maxItems,\n }) as Omit<BlocksField, \"key\">;\n },\n} as const;\n\n/** Item builders for `field.list({ of: item.<type>() })`. */\nexport const item = {\n text(o: { minLength?: number; maxLength?: number; pattern?: string; enum?: string[] } = {}): ListItem {\n return defined({ kind: \"text\", ...o }) as ListItem;\n },\n number(o: { integer?: boolean; min?: number; max?: number } = {}): ListItem {\n return defined({ kind: \"number\", ...o }) as ListItem;\n },\n boolean(): ListItem {\n return { kind: \"boolean\" };\n },\n date(o: { min?: string; max?: string } = {}): ListItem {\n return defined({ kind: \"date\", ...o }) as ListItem;\n },\n datetime(o: { min?: string; max?: string } = {}): ListItem {\n return defined({ kind: \"datetime\", ...o }) as ListItem;\n },\n markdown(o: { minLength?: number; maxLength?: number } = {}): ListItem {\n return defined({ kind: \"markdown\", ...o }) as ListItem;\n },\n json(): ListItem {\n return { kind: \"json\" };\n },\n reference(o: { to: string }): ListItem {\n return { kind: \"reference\", target: o.to };\n },\n asset(o: { allowed?: string[] } = {}): ListItem {\n return { kind: \"asset\", allowed: o.allowed ?? [\"*/*\"] };\n },\n object(o: { fields: Record<string, FieldInput> }): ListItem {\n return { kind: \"object\", fields: assignKeys(o.fields) };\n },\n} as const;\n\n/** Turn a `{ key: FieldInput }` map into an ordered array of `FieldDef`. */\nexport function assignKeys(fields: Record<string, FieldInput>): FieldDef[] {\n return Object.entries(fields).map(([key, def]) => ({ key, ...def }) as FieldDef);\n}\n","import { assignKeys, type FieldInput } from \"./fields.js\";\nimport type { CollectionKind, CollectionSchema, SlugConfig, Visibility } from \"./types.js\";\n\nexport interface CollectionInput {\n /** Immutable collection key, e.g. `posts`. */\n name: string;\n label?: string;\n kind?: CollectionKind;\n /** Visibility defaults to `private` and must be explicitly declared `public`. */\n visibility?: Visibility;\n titleField?: string;\n /** Path template such as `/blog/{slug}`. */\n path?: string;\n /**\n * House style for this collection, read by anyone writing into it — the\n * dashboard editor, the MCP schema tool, an agent planning a draft.\n */\n guidance?: string;\n fields: Record<string, FieldInput>;\n}\n\n/**\n * Author a collection schema. Returns the canonical `CollectionSchema` with an\n * ordered fields array. Defaults: `kind=collection`, `visibility=private`.\n */\nexport function collection(input: CollectionInput): CollectionSchema {\n const fields = assignKeys(input.fields);\n const slugField = fields.find((f) => f.type === \"slug\");\n\n let slug: SlugConfig | null = null;\n if (slugField && slugField.type === \"slug\") {\n slug = {\n from: slugField.from ?? input.titleField ?? slugField.key,\n required: slugField.required,\n };\n }\n\n // `guidance` is omitted rather than set to null when absent: it is part of the\n // canonical form the schema hash is computed over, so emitting a placeholder\n // would re-version every collection in every project that upgrades.\n const guidance = input.guidance ? { guidance: input.guidance } : {};\n\n return {\n name: input.name,\n label: input.label ?? input.name,\n kind: input.kind ?? \"collection\",\n visibility: input.visibility ?? \"private\",\n titleField: input.titleField ?? null,\n slug,\n path: input.path ?? null,\n ...guidance,\n fields,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../schema/src/types.ts","../../schema/src/fields.ts","../../schema/src/collection.ts","../../schema/src/diff.ts","../../schema/src/policy.ts"],"sourcesContent":["/**\n * Canonical schema representation (SPEC 6.2 / 7).\n *\n * These are the normalized, serializable shapes stored as `schema_json`. Field\n * order is significant and preserved as an array; object *keys* are sorted only\n * during canonical stringification for a stable hash.\n */\n\nimport type { PolicyRule } from \"./policy.js\";\n\nexport type FieldType =\n | \"text\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"datetime\"\n | \"markdown\"\n | \"richText\"\n | \"json\"\n | \"asset\"\n | \"reference\"\n | \"list\"\n | \"object\"\n | \"blocks\"\n | \"slug\";\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\n/** UI metadata carried on every field (non-semantic, does not affect validation). */\nexport interface FieldUi {\n widget?: string;\n placeholder?: string;\n helpText?: string;\n group?: string;\n hidden?: boolean;\n}\n\ninterface FieldBase {\n key: string;\n type: FieldType;\n label: string;\n description?: string;\n required: boolean;\n /** Localized fields store `{ [locale]: value }` keyed by project locale.\n * Only top-level, non-slug fields may be localized. */\n localized?: boolean;\n ui?: FieldUi;\n}\n\n/** Locale context for entry validation and locale resolution. */\nexport interface EntryLocaleOptions {\n locales: string[];\n defaultLocale: string;\n}\n\nexport interface TextField extends FieldBase {\n type: \"text\";\n multiline: boolean;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n enum?: string[];\n default?: string;\n}\n\nexport interface NumberField extends FieldBase {\n type: \"number\";\n integer: boolean;\n min?: number;\n max?: number;\n default?: number;\n}\n\nexport interface BooleanField extends FieldBase {\n type: \"boolean\";\n default?: boolean;\n}\n\nexport interface DateField extends FieldBase {\n type: \"date\" | \"datetime\";\n min?: string;\n max?: string;\n default?: string;\n}\n\nexport interface MarkdownField extends FieldBase {\n type: \"markdown\";\n minLength?: number;\n maxLength?: number;\n default?: string;\n}\n\nexport interface JsonField extends FieldBase {\n type: \"json\";\n default?: JsonValue;\n}\n\nexport interface AssetField extends FieldBase {\n type: \"asset\";\n /** Allowed MIME families, e.g. `image/*`, `application/pdf`. */\n allowed: string[];\n multiple: boolean;\n}\n\nexport interface ReferenceField extends FieldBase {\n type: \"reference\";\n /** Target collection key. */\n target: string;\n multiple: boolean;\n}\n\nexport interface SlugField extends FieldBase {\n type: \"slug\";\n /** Source field key to derive the slug from. */\n from?: string;\n}\n\nexport type PrimitiveItem =\n | { kind: \"text\"; minLength?: number; maxLength?: number; pattern?: string; enum?: string[] }\n | { kind: \"number\"; integer?: boolean; min?: number; max?: number }\n | { kind: \"boolean\" }\n | { kind: \"date\" | \"datetime\"; min?: string; max?: string }\n | { kind: \"markdown\"; minLength?: number; maxLength?: number }\n | { kind: \"json\" };\n\nexport type ListItem =\n | PrimitiveItem\n | { kind: \"reference\"; target: string }\n | { kind: \"asset\"; allowed: string[] }\n | { kind: \"object\"; fields: FieldDef[] };\n\nexport interface ListField extends FieldBase {\n type: \"list\";\n item: ListItem;\n minItems?: number;\n maxItems?: number;\n}\n\nexport interface ObjectField extends FieldBase {\n type: \"object\";\n fields: FieldDef[];\n}\n\n/** Semantic rich text stored as a portable JSON document tree — never HTML.\n * The root is `{ \"type\": \"doc\", \"content\": [...] }`; nodes may embed entry\n * references (`ent_` ids) and assets (`ast_` ids). */\nexport interface RichTextField extends FieldBase {\n type: \"richText\";\n}\n\n/** One schema-defined component usable inside a `blocks` field. */\nexport interface BlockDef {\n key: string;\n label: string;\n fields: FieldDef[];\n}\n\n/** Heterogeneous, schema-defined component list. Stored as portable JSON:\n * `[{ \"type\": \"<blockKey>\", \"fields\": { ... } }, ...]`. */\nexport interface BlocksField extends FieldBase {\n type: \"blocks\";\n blocks: BlockDef[];\n minItems?: number;\n maxItems?: number;\n}\n\nexport type FieldDef =\n | TextField\n | NumberField\n | BooleanField\n | DateField\n | MarkdownField\n | RichTextField\n | JsonField\n | AssetField\n | ReferenceField\n | SlugField\n | ListField\n | ObjectField\n | BlocksField;\n\nexport type CollectionKind = \"collection\" | \"singleton\";\nexport type Visibility = \"public\" | \"private\";\n\nexport interface SlugConfig {\n from: string;\n required: boolean;\n}\n\n/** Canonical collection schema (one `collection_versions.schema_json`). */\nexport interface CollectionSchema {\n name: string;\n label: string;\n kind: CollectionKind;\n visibility: Visibility;\n titleField: string | null;\n slug: SlugConfig | null;\n path: string | null;\n /**\n * How this collection is meant to be written: voice, length, conventions,\n * what belongs here and what does not.\n *\n * A field `description` says what a field is for. This says what *good*\n * looks like, which is the thing an agent has no way to infer from types and\n * the thing a new human contributor is told verbally and then forgets. It\n * travels with the schema, so it is versioned, reviewed, and deployed like\n * the rest of it.\n */\n guidance?: string;\n /**\n * Rules this collection refuses to publish against, declared here rather than\n * assumed by Myna. `guidance` says what good looks like; this says what is\n * rejected. Evaluated by the `policy` check on every change set.\n */\n policy?: PolicyRule[];\n fields: FieldDef[];\n}\n\n/** Maximum object/list nesting depth (SPEC 6.2). */\nexport const MAX_NESTING_DEPTH = 4;\n","import type {\n AssetField,\n BlockDef,\n BlocksField,\n BooleanField,\n DateField,\n FieldDef,\n FieldUi,\n JsonField,\n JsonValue,\n ListField,\n ListItem,\n MarkdownField,\n NumberField,\n ObjectField,\n ReferenceField,\n RichTextField,\n SlugField,\n TextField,\n} from \"./types.js\";\n\n/** A field definition before its `key` is assigned by `collection()`. */\nexport type FieldInput = DistributiveOmit<FieldDef, \"key\">;\n\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;\n\ninterface CommonOptions {\n label?: string;\n description?: string;\n required?: boolean;\n localized?: boolean;\n ui?: FieldUi;\n}\n\nfunction common(o: CommonOptions): {\n label?: string;\n description?: string;\n required: boolean;\n localized?: boolean;\n ui?: FieldUi;\n} {\n const base: { label?: string; description?: string; required: boolean; localized?: boolean; ui?: FieldUi } = {\n required: o.required ?? false,\n };\n if (o.label !== undefined) base.label = o.label;\n if (o.description !== undefined) base.description = o.description;\n if (o.localized !== undefined) base.localized = o.localized;\n if (o.ui !== undefined) base.ui = o.ui;\n return base;\n}\n\nexport interface TextOptions extends CommonOptions {\n multiline?: boolean;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n enum?: string[];\n default?: string;\n}\nexport interface NumberOptions extends CommonOptions {\n integer?: boolean;\n min?: number;\n max?: number;\n default?: number;\n}\nexport interface BooleanOptions extends CommonOptions {\n default?: boolean;\n}\nexport interface DateOptions extends CommonOptions {\n min?: string;\n max?: string;\n default?: string;\n}\nexport interface MarkdownOptions extends CommonOptions {\n minLength?: number;\n maxLength?: number;\n default?: string;\n}\nexport interface JsonOptions extends CommonOptions {\n default?: JsonValue;\n}\nexport interface AssetOptions extends CommonOptions {\n allowed?: string[];\n multiple?: boolean;\n}\nexport interface ReferenceOptions extends CommonOptions {\n to: string;\n multiple?: boolean;\n}\nexport interface SlugOptions extends CommonOptions {\n from?: string;\n}\nexport interface ListOptions extends CommonOptions {\n of: ListItem;\n minItems?: number;\n maxItems?: number;\n}\nexport interface ObjectOptions extends CommonOptions {\n fields: Record<string, FieldInput>;\n}\nexport type RichTextOptions = CommonOptions;\nexport interface BlocksOptions extends CommonOptions {\n /** The block components entries may compose, built with `block()`. */\n allowed: BlockDef[];\n minItems?: number;\n maxItems?: number;\n}\n\n/** Define a reusable block component for `field.blocks({ allowed: [...] })`. */\nexport function block(key: string, o: { label?: string; fields: Record<string, FieldInput> }): BlockDef {\n return { key, label: o.label ?? key, fields: assignKeys(o.fields) };\n}\n\nfunction defined<T extends Record<string, unknown>>(obj: T): T {\n for (const key of Object.keys(obj)) {\n if (obj[key] === undefined) delete obj[key];\n }\n return obj;\n}\n\nexport const field = {\n text(o: TextOptions = {}): Omit<TextField, \"key\"> {\n return defined({\n type: \"text\",\n ...common(o),\n multiline: o.multiline ?? false,\n minLength: o.minLength,\n maxLength: o.maxLength,\n pattern: o.pattern,\n enum: o.enum,\n default: o.default,\n }) as Omit<TextField, \"key\">;\n },\n\n number(o: NumberOptions = {}): Omit<NumberField, \"key\"> {\n return defined({\n type: \"number\",\n ...common(o),\n integer: o.integer ?? false,\n min: o.min,\n max: o.max,\n default: o.default,\n }) as Omit<NumberField, \"key\">;\n },\n\n boolean(o: BooleanOptions = {}): Omit<BooleanField, \"key\"> {\n return defined({\n type: \"boolean\",\n ...common(o),\n default: o.default,\n }) as Omit<BooleanField, \"key\">;\n },\n\n date(o: DateOptions = {}): Omit<DateField, \"key\"> {\n return defined({\n type: \"date\",\n ...common(o),\n min: o.min,\n max: o.max,\n default: o.default,\n }) as Omit<DateField, \"key\">;\n },\n\n datetime(o: DateOptions = {}): Omit<DateField, \"key\"> {\n return defined({\n type: \"datetime\",\n ...common(o),\n min: o.min,\n max: o.max,\n default: o.default,\n }) as Omit<DateField, \"key\">;\n },\n\n markdown(o: MarkdownOptions = {}): Omit<MarkdownField, \"key\"> {\n return defined({\n type: \"markdown\",\n ...common(o),\n minLength: o.minLength,\n maxLength: o.maxLength,\n default: o.default,\n }) as Omit<MarkdownField, \"key\">;\n },\n\n json(o: JsonOptions = {}): Omit<JsonField, \"key\"> {\n return defined({\n type: \"json\",\n ...common(o),\n default: o.default,\n }) as Omit<JsonField, \"key\">;\n },\n\n asset(o: AssetOptions = {}): Omit<AssetField, \"key\"> {\n return defined({\n type: \"asset\",\n ...common(o),\n allowed: o.allowed ?? [\"*/*\"],\n multiple: o.multiple ?? false,\n }) as Omit<AssetField, \"key\">;\n },\n\n reference(o: ReferenceOptions): Omit<ReferenceField, \"key\"> {\n return defined({\n type: \"reference\",\n ...common(o),\n target: o.to,\n multiple: o.multiple ?? false,\n }) as Omit<ReferenceField, \"key\">;\n },\n\n slug(o: SlugOptions = {}): Omit<SlugField, \"key\"> {\n return defined({\n type: \"slug\",\n ...common(o),\n from: o.from,\n }) as Omit<SlugField, \"key\">;\n },\n\n list(o: ListOptions): Omit<ListField, \"key\"> {\n return defined({\n type: \"list\",\n ...common(o),\n item: o.of,\n minItems: o.minItems,\n maxItems: o.maxItems,\n }) as Omit<ListField, \"key\">;\n },\n\n object(o: ObjectOptions): Omit<ObjectField, \"key\"> {\n return defined({\n type: \"object\",\n ...common(o),\n fields: assignKeys(o.fields),\n }) as Omit<ObjectField, \"key\">;\n },\n\n richText(o: RichTextOptions = {}): Omit<RichTextField, \"key\"> {\n return defined({\n type: \"richText\",\n ...common(o),\n }) as Omit<RichTextField, \"key\">;\n },\n\n blocks(o: BlocksOptions): Omit<BlocksField, \"key\"> {\n return defined({\n type: \"blocks\",\n ...common(o),\n blocks: o.allowed,\n minItems: o.minItems,\n maxItems: o.maxItems,\n }) as Omit<BlocksField, \"key\">;\n },\n} as const;\n\n/** Item builders for `field.list({ of: item.<type>() })`. */\nexport const item = {\n text(o: { minLength?: number; maxLength?: number; pattern?: string; enum?: string[] } = {}): ListItem {\n return defined({ kind: \"text\", ...o }) as ListItem;\n },\n number(o: { integer?: boolean; min?: number; max?: number } = {}): ListItem {\n return defined({ kind: \"number\", ...o }) as ListItem;\n },\n boolean(): ListItem {\n return { kind: \"boolean\" };\n },\n date(o: { min?: string; max?: string } = {}): ListItem {\n return defined({ kind: \"date\", ...o }) as ListItem;\n },\n datetime(o: { min?: string; max?: string } = {}): ListItem {\n return defined({ kind: \"datetime\", ...o }) as ListItem;\n },\n markdown(o: { minLength?: number; maxLength?: number } = {}): ListItem {\n return defined({ kind: \"markdown\", ...o }) as ListItem;\n },\n json(): ListItem {\n return { kind: \"json\" };\n },\n reference(o: { to: string }): ListItem {\n return { kind: \"reference\", target: o.to };\n },\n asset(o: { allowed?: string[] } = {}): ListItem {\n return { kind: \"asset\", allowed: o.allowed ?? [\"*/*\"] };\n },\n object(o: { fields: Record<string, FieldInput> }): ListItem {\n return { kind: \"object\", fields: assignKeys(o.fields) };\n },\n} as const;\n\n/** Turn a `{ key: FieldInput }` map into an ordered array of `FieldDef`. */\nexport function assignKeys(fields: Record<string, FieldInput>): FieldDef[] {\n return Object.entries(fields).map(([key, def]) => ({ key, ...def }) as FieldDef);\n}\n","import { assignKeys, type FieldInput } from \"./fields.js\";\nimport type { CollectionKind, CollectionSchema, SlugConfig, Visibility } from \"./types.js\";\nimport type { PolicyRule } from \"./policy.js\";\n\nexport interface CollectionInput {\n /** Immutable collection key, e.g. `posts`. */\n name: string;\n label?: string;\n kind?: CollectionKind;\n /** Visibility defaults to `private` and must be explicitly declared `public`. */\n visibility?: Visibility;\n titleField?: string;\n /** Path template such as `/blog/{slug}`. */\n path?: string;\n /**\n * House style for this collection, read by anyone writing into it — the\n * dashboard editor, the MCP schema tool, an agent planning a draft.\n */\n guidance?: string;\n /**\n * Rules this collection refuses to publish against. `guidance` is advice;\n * this is enforced, by the `policy` check, before anything ships.\n */\n policy?: PolicyRule[];\n fields: Record<string, FieldInput>;\n}\n\n/**\n * Author a collection schema. Returns the canonical `CollectionSchema` with an\n * ordered fields array. Defaults: `kind=collection`, `visibility=private`.\n */\nexport function collection(input: CollectionInput): CollectionSchema {\n const fields = assignKeys(input.fields);\n const slugField = fields.find((f) => f.type === \"slug\");\n\n let slug: SlugConfig | null = null;\n if (slugField && slugField.type === \"slug\") {\n slug = {\n from: slugField.from ?? input.titleField ?? slugField.key,\n required: slugField.required,\n };\n }\n\n // `guidance` is omitted rather than set to null when absent: it is part of the\n // canonical form the schema hash is computed over, so emitting a placeholder\n // would re-version every collection in every project that upgrades.\n const guidance = input.guidance ? { guidance: input.guidance } : {};\n // Same reasoning as `guidance`: absent means absent, not an empty array, so a\n // collection that declares no policy hashes exactly as it did before policy\n // existed and does not re-version on upgrade.\n const policy = input.policy && input.policy.length > 0 ? { policy: input.policy } : {};\n\n return {\n name: input.name,\n label: input.label ?? input.name,\n kind: input.kind ?? \"collection\",\n visibility: input.visibility ?? \"private\",\n titleField: input.titleField ?? null,\n slug,\n path: input.path ?? null,\n ...guidance,\n ...policy,\n fields,\n };\n}\n","import { canonicalStringify } from \"./canonical.js\";\nimport type { CollectionSchema, FieldDef } from \"./types.js\";\n\n/** How risky a schema operation is for existing content (SPEC section 7). */\nexport type Classification = \"safe\" | \"conditionally_destructive\" | \"destructive\";\n\nexport type SchemaOpKind =\n | \"add_collection\"\n | \"remove_collection\"\n | \"change_visibility\"\n | \"change_kind\"\n | \"modify_collection\"\n | \"add_field\"\n | \"remove_field\"\n | \"modify_field\";\n\nexport interface SchemaOp {\n kind: SchemaOpKind;\n collection: string;\n /** Dotted field path for field-level ops. */\n field?: string;\n classification: Classification;\n detail: string;\n before?: unknown;\n after?: unknown;\n}\n\nexport interface SchemaDiff {\n ops: SchemaOp[];\n /** Highest-severity classification across all ops. */\n classification: Classification;\n hasDestructive: boolean;\n}\n\nconst SEVERITY: Record<Classification, number> = {\n safe: 0,\n conditionally_destructive: 1,\n destructive: 2,\n};\n\n/**\n * Produce a stable, ordered diff between the currently deployed schemas and the\n * proposed next schemas. Collections are compared by key; the resulting op list\n * is deterministic (collections sorted by key, fields by key within each).\n */\nexport function diffSchemas(current: CollectionSchema[], next: CollectionSchema[]): SchemaDiff {\n const ops: SchemaOp[] = [];\n const currentByKey = new Map(current.map((c) => [c.name, c]));\n const nextByKey = new Map(next.map((c) => [c.name, c]));\n const allKeys = [...new Set([...currentByKey.keys(), ...nextByKey.keys()])].sort();\n\n for (const key of allKeys) {\n const before = currentByKey.get(key);\n const after = nextByKey.get(key);\n\n if (!before && after) {\n ops.push({ kind: \"add_collection\", collection: key, classification: \"safe\", detail: `Add collection \"${key}\".`, after });\n continue;\n }\n if (before && !after) {\n ops.push({ kind: \"remove_collection\", collection: key, classification: \"destructive\", detail: `Remove collection \"${key}\".`, before });\n continue;\n }\n if (before && after) {\n diffCollection(before, after, ops);\n }\n }\n\n const classification = ops.reduce<Classification>((max, op) => (SEVERITY[op.classification] > SEVERITY[max] ? op.classification : max), \"safe\");\n return { ops, classification, hasDestructive: ops.some((o) => o.classification === \"destructive\") };\n}\n\nfunction diffCollection(before: CollectionSchema, after: CollectionSchema, ops: SchemaOp[]): void {\n const key = after.name;\n\n if (before.visibility !== after.visibility) {\n ops.push({\n kind: \"change_visibility\",\n collection: key,\n classification: after.visibility === \"private\" ? \"conditionally_destructive\" : \"safe\",\n detail: `Visibility ${before.visibility} → ${after.visibility}.`,\n before: before.visibility,\n after: after.visibility,\n });\n }\n\n if (before.kind !== after.kind) {\n ops.push({ kind: \"change_kind\", collection: key, classification: \"destructive\", detail: `Kind ${before.kind} → ${after.kind}.`, before: before.kind, after: after.kind });\n }\n\n if (\n before.titleField !== after.titleField ||\n before.path !== after.path ||\n (before.guidance ?? null) !== (after.guidance ?? null) ||\n canonicalStringify(before.policy ?? null) !== canonicalStringify(after.policy ?? null) ||\n canonicalStringify(before.slug) !== canonicalStringify(after.slug)\n ) {\n ops.push({ kind: \"modify_collection\", collection: key, classification: \"safe\", detail: `Update collection metadata for \"${key}\".`, before: { titleField: before.titleField, path: before.path, slug: before.slug, guidance: before.guidance ?? null, policy: before.policy ?? null }, after: { titleField: after.titleField, path: after.path, slug: after.slug, guidance: after.guidance ?? null, policy: after.policy ?? null } });\n }\n\n diffFieldList(before.fields, after.fields, key, \"\", ops);\n}\n\nfunction diffFieldList(before: FieldDef[], after: FieldDef[], collection: string, prefix: string, ops: SchemaOp[]): void {\n const beforeByKey = new Map(before.map((f) => [f.key, f]));\n const afterByKey = new Map(after.map((f) => [f.key, f]));\n const keys = [...new Set([...beforeByKey.keys(), ...afterByKey.keys()])].sort();\n\n for (const k of keys) {\n const b = beforeByKey.get(k);\n const a = afterByKey.get(k);\n const path = prefix ? `${prefix}.${k}` : k;\n\n if (!b && a) {\n ops.push({ kind: \"add_field\", collection, field: path, classification: a.required && !hasDefault(a) ? \"conditionally_destructive\" : \"safe\", detail: a.required && !hasDefault(a) ? `Add required field \"${path}\" (fails if entries exist without a default).` : `Add field \"${path}\".`, after: a });\n continue;\n }\n if (b && !a) {\n ops.push({ kind: \"remove_field\", collection, field: path, classification: \"destructive\", detail: `Remove field \"${path}\".`, before: b });\n continue;\n }\n if (b && a) diffField(b, a, collection, path, ops);\n }\n}\n\nfunction diffField(b: FieldDef, a: FieldDef, collection: string, path: string, ops: SchemaOp[]): void {\n if (b.type !== a.type) {\n ops.push({ kind: \"modify_field\", collection, field: path, classification: \"destructive\", detail: `Change type of \"${path}\": ${b.type} → ${a.type}.`, before: b, after: a });\n return;\n }\n\n // Recurse into nested structures.\n if (a.type === \"object\" && b.type === \"object\") {\n diffFieldList(b.fields, a.fields, collection, path, ops);\n }\n if (a.type === \"list\" && b.type === \"list\" && a.item.kind === \"object\" && b.item.kind === \"object\") {\n diffFieldList(b.item.fields, a.item.fields, collection, `${path}[]`, ops);\n }\n\n const classification = classifyFieldModification(b, a);\n if (classification === null) return;\n ops.push({ kind: \"modify_field\", collection, field: path, classification, detail: describeModification(b, a, path), before: b, after: a });\n}\n\n/** Returns a classification, or `null` when the fields are effectively equal. */\nfunction classifyFieldModification(b: FieldDef, a: FieldDef): Classification | null {\n if (canonicalStringify(stripCosmetic(b)) === canonicalStringify(stripCosmetic(a))) {\n // Only cosmetic (label/description/ui/default) differences, if any at all.\n return canonicalStringify(b) === canonicalStringify(a) ? null : \"safe\";\n }\n\n // Becoming required is conditionally destructive.\n if (!b.required && a.required) return \"conditionally_destructive\";\n\n // Tightening scalar bounds is conditionally destructive.\n if (isTightened(b, a)) return \"conditionally_destructive\";\n\n // Any other constraint change (loosening, target/item changes) — treat as safe\n // unless it materially changes shape, which is handled by type/kind checks.\n if (isReferenceRetargeted(b, a) || isListItemChanged(b, a)) return \"destructive\";\n\n return \"safe\";\n}\n\nfunction stripCosmetic(f: FieldDef): Record<string, unknown> {\n const { label: _label, description: _description, ui: _ui, default: _default, ...rest } = f as Record<string, unknown> & FieldDef;\n return rest;\n}\n\nfunction isTightened(b: FieldDef, a: FieldDef): boolean {\n const numLower = (x?: number, y?: number): boolean => x === undefined && y !== undefined ? true : x !== undefined && y !== undefined && y < x;\n const numRaise = (x?: number, y?: number): boolean => x === undefined && y !== undefined ? true : x !== undefined && y !== undefined && y > x;\n\n if ((b.type === \"text\" || b.type === \"markdown\") && (a.type === \"text\" || a.type === \"markdown\")) {\n if (numLower(b.maxLength, a.maxLength) || numRaise(b.minLength, a.minLength)) return true;\n if (b.type === \"text\" && a.type === \"text\" && a.pattern && a.pattern !== b.pattern) return true;\n if (b.type === \"text\" && a.type === \"text\" && a.enum && (!b.enum || a.enum.length < b.enum.length)) return true;\n }\n if (b.type === \"number\" && a.type === \"number\") {\n if (numRaise(b.min, a.min) || numLower(b.max, a.max)) return true;\n if (!b.integer && a.integer) return true;\n }\n if (b.type === \"list\" && a.type === \"list\") {\n if (numRaise(b.minItems, a.minItems) || numLower(b.maxItems, a.maxItems)) return true;\n }\n if (b.type === \"asset\" && a.type === \"asset\") {\n if (b.multiple && !a.multiple) return true;\n }\n if (b.type === \"reference\" && a.type === \"reference\") {\n if (b.multiple && !a.multiple) return true;\n }\n return false;\n}\n\nfunction isReferenceRetargeted(b: FieldDef, a: FieldDef): boolean {\n return b.type === \"reference\" && a.type === \"reference\" && b.target !== a.target;\n}\n\nfunction isListItemChanged(b: FieldDef, a: FieldDef): boolean {\n return b.type === \"list\" && a.type === \"list\" && b.item.kind !== a.item.kind;\n}\n\nfunction describeModification(b: FieldDef, a: FieldDef, path: string): string {\n if (!b.required && a.required) return `Field \"${path}\" becomes required.`;\n if (isReferenceRetargeted(b, a)) return `Field \"${path}\" reference target changes.`;\n if (isListItemChanged(b, a)) return `Field \"${path}\" list item type changes.`;\n return `Modify field \"${path}\".`;\n}\n\nfunction hasDefault(f: FieldDef): boolean {\n return \"default\" in f && (f as { default?: unknown }).default !== undefined;\n}\n\n// --- Entry data diff --------------------------------------------------------\n\n/** One run of unchanged, inserted, or deleted text within a prose field. */\nexport interface TextSegment {\n op: \"equal\" | \"insert\" | \"delete\";\n value: string;\n}\n\n/** One field-level difference between two versions of an entry's data. */\nexport interface FieldChange {\n /** Dotted path, e.g. `title` or `seo.description` or `blocks.0.heading`. */\n path: string;\n before: unknown;\n after: unknown;\n kind: \"added\" | \"removed\" | \"changed\";\n /**\n * Word-level segments for a prose leaf, when `words` was requested and both\n * sides carry readable text. A summary of the change, not a second source of\n * truth: `before` and `after` remain the authoritative values, and for rich\n * text the segments cover the prose only, not marks or node attributes.\n */\n segments?: TextSegment[];\n}\n\nexport interface DiffEntryDataOptions {\n /**\n * Attach word-level `segments` to long prose leaves.\n *\n * Off by default, and deliberately incapable of affecting anything else: the\n * paths, kinds, and values are identical either way, so a review view that\n * asks for segments and a rebase that does not still describe the same change.\n */\n words?: boolean;\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\n/**\n * A flat, ordered list of the field-level changes between two entry payloads.\n *\n * Descends through nested objects so a change deep inside a `seo` group or a\n * localized field reads as one changed leaf rather than a whole rewritten\n * object. Arrays are compared whole: reordering a list is a change to the list,\n * not to each of its items.\n */\nexport function diffEntryData(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n options: DiffEntryDataOptions = {},\n): FieldChange[] {\n const changes: FieldChange[] = [];\n const walk = (a: unknown, b: unknown, path: string): void => {\n if (canonicalStringify(a) === canonicalStringify(b)) return;\n if (isPlainObject(a) && isPlainObject(b)) {\n for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {\n walk(a[k], b[k], path ? `${path}.${k}` : k);\n }\n return;\n }\n const change: FieldChange = {\n path,\n before: a,\n after: b,\n kind: a === undefined ? \"added\" : b === undefined ? \"removed\" : \"changed\",\n };\n if (options.words) {\n const segments = proseSegments(a, b);\n if (segments) change.segments = segments;\n }\n changes.push(change);\n };\n walk(before, after, \"\");\n return changes;\n}\n\n// --- Word-level text diffs --------------------------------------------------\n\n/**\n * Below this, the whole value is readable at a glance and segmenting it is\n * noise. A body of a few thousand words is the case this exists for: reported\n * whole, it says only \"changed\", which is exactly as much as a reviewer — or an\n * agent summarizing its own work — already knew.\n */\nconst MIN_PROSE_CHARS = 160;\n\n/**\n * Ceiling on the LCS table. A word diff of two texts that share nothing is a\n * list of every word deleted followed by every word inserted, which is what the\n * whole-value view already said — so past this size the honest answer is to say\n * \"replaced\" rather than to spend a second on saying it at length.\n */\nconst MAX_LCS_CELLS = 250_000;\n\n/** Words and the whitespace between them, so segments rejoin into the original. */\nfunction tokenize(text: string): string[] {\n return text.match(/\\s+|\\S+/g) ?? [];\n}\n\n/** Segments for one leaf, or null when it has no comparable prose. */\nfunction proseSegments(before: unknown, after: unknown): TextSegment[] | null {\n const a = plainText(before);\n const b = plainText(after);\n if (a === null || b === null || a === b) return null;\n if (Math.max(a.length, b.length) < MIN_PROSE_CHARS) return null;\n return diffWords(a, b);\n}\n\n/**\n * The plain text a value carries: the string itself, or the text nodes of a\n * rich-text document. Null for anything that is not prose, which is how a\n * number, a reference id, or a block list opts out.\n *\n * Exported because a length policy has to measure the same thing a reader sees:\n * a rich-text body is 800 words of prose, not 4kB of JSON, and two answers to\n * \"how long is this field\" would be one answer too many.\n */\nexport function plainText(value: unknown): string | null {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return nodeText(value);\n if (isPlainObject(value) && value.type === \"doc\" && Array.isArray(value.content)) {\n return nodeText(value.content);\n }\n return null;\n}\n\n/** Concatenate a rich-text node list, or null if it is not one. */\nfunction nodeText(nodes: unknown[]): string | null {\n const parts: string[] = [];\n let hasBlocks = false;\n for (const node of nodes) {\n if (!isPlainObject(node) || typeof node.type !== \"string\") return null;\n if (typeof node.text === \"string\") {\n parts.push(node.text);\n continue;\n }\n // Anything that is not an inline text run separates paragraphs; a node with\n // no content at all (an image, a rule) still occupies a position.\n hasBlocks = true;\n if (Array.isArray(node.content)) {\n const inner = nodeText(node.content);\n if (inner === null) return null;\n parts.push(inner);\n }\n }\n return parts.join(hasBlocks ? \"\\n\\n\" : \"\");\n}\n\n/**\n * Word-level diff of two texts.\n *\n * Common prefix and suffix are trimmed first, which is what makes the usual\n * case — a paragraph edited inside a long body — cost almost nothing. What\n * remains is compared with a longest-common-subsequence walk over tokens.\n */\nexport function diffWords(before: string, after: string): TextSegment[] {\n const a = tokenize(before);\n const b = tokenize(after);\n\n let head = 0;\n while (head < a.length && head < b.length && a[head] === b[head]) head += 1;\n let tail = 0;\n while (\n tail < a.length - head &&\n tail < b.length - head &&\n a[a.length - 1 - tail] === b[b.length - 1 - tail]\n ) {\n tail += 1;\n }\n\n const midA = a.slice(head, a.length - tail);\n const midB = b.slice(head, b.length - tail);\n const middle: TextSegment[] =\n (midA.length + 1) * (midB.length + 1) > MAX_LCS_CELLS\n ? [\n { op: \"delete\", value: midA.join(\"\") },\n { op: \"insert\", value: midB.join(\"\") },\n ]\n : lcsSegments(midA, midB);\n\n return coalesce([\n { op: \"equal\", value: a.slice(0, head).join(\"\") },\n ...middle,\n { op: \"equal\", value: a.slice(a.length - tail).join(\"\") },\n ]);\n}\n\nfunction lcsSegments(a: string[], b: string[]): TextSegment[] {\n const n = a.length;\n const m = b.length;\n const width = m + 1;\n // table[i][j] is the LCS length of a[i..] and b[j..], filled backwards so the\n // walk below can move forward and emit segments in reading order.\n const table = new Uint32Array((n + 1) * width);\n for (let i = n - 1; i >= 0; i -= 1) {\n for (let j = m - 1; j >= 0; j -= 1) {\n table[i * width + j] =\n a[i] === b[j]\n ? table[(i + 1) * width + j + 1]! + 1\n : Math.max(table[(i + 1) * width + j]!, table[i * width + j + 1]!);\n }\n }\n\n const out: TextSegment[] = [];\n let i = 0;\n let j = 0;\n while (i < n && j < m) {\n if (a[i] === b[j]) {\n out.push({ op: \"equal\", value: a[i]! });\n i += 1;\n j += 1;\n } else if (table[(i + 1) * width + j]! >= table[i * width + j + 1]!) {\n out.push({ op: \"delete\", value: a[i]! });\n i += 1;\n } else {\n out.push({ op: \"insert\", value: b[j]! });\n j += 1;\n }\n }\n while (i < n) out.push({ op: \"delete\", value: a[i++]! });\n while (j < m) out.push({ op: \"insert\", value: b[j++]! });\n return out;\n}\n\n/** Join neighbouring segments of the same operation and drop empty ones. */\nfunction coalesce(segments: TextSegment[]): TextSegment[] {\n const out: TextSegment[] = [];\n for (const segment of segments) {\n if (segment.value === \"\") continue;\n const last = out[out.length - 1];\n if (last && last.op === segment.op) last.value += segment.value;\n else out.push({ ...segment });\n }\n return out;\n}\n","import { plainText } from \"./diff.js\";\n\n/**\n * Content policy, declared in schema-as-code next to the collection's guidance.\n *\n * Myna used to ship a fixed set of editorial checks — an SEO length rule, a\n * mandatory alt-text rule — and run them against every project whether or not\n * anyone asked. Two things were wrong with that. The rules were opinions nobody\n * consented to, and they were guesses: the SEO rule looked for fields named\n * `seoTitle` and `metaTitle`, so a collection whose title field is called\n * `title` got a `passed` verdict on a rule that had inspected nothing. An\n * unasked-for check that silently protects nobody is worse than no check, and\n * the gap it leaves gets filled by a script in someone's build — which is where\n * a content rule fires *after* publishing rather than before.\n *\n * So a collection declares its own rules, in the same file as its fields, and\n * `guidance` says what good looks like while `policy` says what is refused.\n * The distinction that matters:\n *\n * - `schema`, `references`, `conflicts`, and `slugs` stay built in. They are\n * preconditions for a coherent publish, not taste — the way git refuses a\n * non-fast-forward push.\n * - Everything editorial is declared here or does not run.\n *\n * Rules are plain data, so they version and diff with the schema, travel to\n * `myna_get_collection_schema`, and reach an agent *before* it writes.\n */\n\nexport type PolicyRule =\n | {\n rule: \"length\";\n /** Dotted path to the field, e.g. `title` or `seo.description`. */\n field: string;\n min?: number;\n max?: number;\n unit: \"characters\" | \"words\";\n reason?: string;\n }\n | { rule: \"required\"; fields: string[]; reason?: string }\n | { rule: \"bannedTerms\"; terms: string[]; fields?: string[]; reason?: string }\n | { rule: \"altText\"; reason?: string };\n\nexport interface LengthOptions {\n min?: number;\n max?: number;\n /** Count words rather than characters. Characters by default. */\n words?: boolean;\n /**\n * Why the limit exists, shown with the violation.\n *\n * Worth writing: \"title is 78 characters (limit 53)\" tells someone what to do\n * but not what they broke, and a limit whose reason nobody remembers is a\n * limit somebody eventually raises.\n */\n reason?: string;\n}\n\nexport const policy = {\n /** Bound a field's length, in characters or words. */\n length(field: string, options: LengthOptions): PolicyRule {\n const rule: PolicyRule = {\n rule: \"length\",\n field,\n unit: options.words ? \"words\" : \"characters\",\n };\n if (options.min !== undefined) rule.min = options.min;\n if (options.max !== undefined) rule.max = options.max;\n if (options.reason) rule.reason = options.reason;\n return rule;\n },\n\n /**\n * Fields that must carry a value before this collection publishes.\n *\n * Distinct from `required` on the field itself, which refuses the draft. A\n * draft is allowed to be incomplete — that is what drafting is — and this is\n * the gate at the end of it.\n */\n required(fields: string[], options: { reason?: string } = {}): PolicyRule {\n const rule: PolicyRule = { rule: \"required\", fields };\n if (options.reason) rule.reason = options.reason;\n return rule;\n },\n\n /** Refuse terms anywhere in the entry's text, or in named fields only. */\n bannedTerms(terms: string[], options: { fields?: string[]; reason?: string } = {}): PolicyRule {\n const rule: PolicyRule = { rule: \"bannedTerms\", terms };\n if (options.fields) rule.fields = options.fields;\n if (options.reason) rule.reason = options.reason;\n return rule;\n },\n\n /**\n * Every image the entry references must carry alt text.\n *\n * Shipped as a rule you declare rather than one Myna assumes. It was a\n * built-in check until it became clear that \"every project wants this\" is a\n * claim about other people's products.\n */\n altText(options: { reason?: string } = {}): PolicyRule {\n const rule: PolicyRule = { rule: \"altText\" };\n if (options.reason) rule.reason = options.reason;\n return rule;\n },\n};\n\nexport interface PolicyViolation {\n /** Dotted field path, or empty when the rule is about the entry as a whole. */\n path: string;\n message: string;\n rule: PolicyRule[\"rule\"];\n}\n\n/** Read a dotted path out of an entry payload. */\nfunction valueAt(data: Record<string, unknown>, path: string): unknown {\n let cursor: unknown = data;\n for (const key of path.split(\".\")) {\n if (typeof cursor !== \"object\" || cursor === null || Array.isArray(cursor)) return undefined;\n cursor = (cursor as Record<string, unknown>)[key];\n }\n return cursor;\n}\n\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/** Every string in the payload, for rules that scan the whole entry. */\nfunction allText(value: unknown, out: string[] = []): string[] {\n if (typeof value === \"string\") out.push(value);\n else if (Array.isArray(value)) for (const v of value) allText(v, out);\n else if (typeof value === \"object\" && value !== null) {\n for (const v of Object.values(value)) allText(v, out);\n }\n return out;\n}\n\nfunction countWords(text: string): number {\n const trimmed = text.trim();\n return trimmed === \"\" ? 0 : trimmed.split(/\\s+/).length;\n}\n\n/**\n * Evaluate the rules that depend only on the entry's own data.\n *\n * `altText` is deliberately not evaluated here: it needs to resolve asset\n * records, which is the API's job. Callers that can do that lookup add its\n * violations to this result.\n */\nexport function evaluatePolicy(\n rules: PolicyRule[],\n data: Record<string, unknown>,\n): PolicyViolation[] {\n const violations: PolicyViolation[] = [];\n const because = (rule: { reason?: string }): string => (rule.reason ? ` ${rule.reason}` : \"\");\n\n for (const rule of rules) {\n switch (rule.rule) {\n case \"length\": {\n // Prose fields are compared by the text they carry, so a rich-text\n // document is measured as what a reader sees rather than as its JSON.\n const text = plainText(valueAt(data, rule.field));\n if (text === null) break;\n const size = rule.unit === \"words\" ? countWords(text) : text.length;\n const unit = rule.unit === \"words\" ? \"words\" : \"characters\";\n if (rule.max !== undefined && size > rule.max) {\n violations.push({\n path: rule.field,\n rule: \"length\",\n message: `${rule.field} is ${size} ${unit} (limit ${rule.max}).${because(rule)}`,\n });\n }\n if (rule.min !== undefined && size < rule.min) {\n violations.push({\n path: rule.field,\n rule: \"length\",\n message: `${rule.field} is ${size} ${unit} (minimum ${rule.min}).${because(rule)}`,\n });\n }\n break;\n }\n\n case \"required\": {\n for (const field of rule.fields) {\n if (isEmpty(valueAt(data, field))) {\n violations.push({\n path: field,\n rule: \"required\",\n message: `${field} must be set before publishing.${because(rule)}`,\n });\n }\n }\n break;\n }\n\n case \"bannedTerms\": {\n const haystacks = rule.fields\n ? rule.fields.map((f) => ({ path: f, text: plainText(valueAt(data, f)) ?? \"\" }))\n : [{ path: \"\", text: allText(data).join(\"\\n\") }];\n for (const { path, text } of haystacks) {\n const lower = text.toLowerCase();\n for (const term of rule.terms) {\n if (lower.includes(term.toLowerCase())) {\n violations.push({\n path,\n rule: \"bannedTerms\",\n message: `Contains \"${term}\".${because(rule)}`,\n });\n }\n }\n }\n break;\n }\n\n case \"altText\":\n break;\n }\n }\n\n return violations;\n}\n"],"mappings":"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","names":["policy","field"]}
|
package/dist/main.js
CHANGED
|
@@ -3691,22 +3691,8 @@ function registerBilling(program) {
|
|
|
3691
3691
|
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3692
3692
|
import { join as join6 } from "path";
|
|
3693
3693
|
|
|
3694
|
-
// src/
|
|
3695
|
-
var VERSION = true ? "0.9.0" : "0.0.0-dev";
|
|
3696
|
-
|
|
3697
|
-
// src/commands/doctor.ts
|
|
3694
|
+
// src/registry.ts
|
|
3698
3695
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
3699
|
-
var SCAN_IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".react-router", ".turbo", "coverage"]);
|
|
3700
|
-
var GENERATED_MARKERS = [
|
|
3701
|
-
"// Generated by `myna types generate`. Do not edit by hand.",
|
|
3702
|
-
"export interface MynaCollections {"
|
|
3703
|
-
];
|
|
3704
|
-
function looksGenerated(contents) {
|
|
3705
|
-
return GENERATED_MARKERS.every((marker) => contents.includes(marker));
|
|
3706
|
-
}
|
|
3707
|
-
function check(id, title, status, detail, remedy) {
|
|
3708
|
-
return remedy ? { id, title, status, detail, remedy } : { id, title, status, detail };
|
|
3709
|
-
}
|
|
3710
3696
|
async function latestPublished(pkg) {
|
|
3711
3697
|
try {
|
|
3712
3698
|
const res = await fetch(`${NPM_REGISTRY}/${pkg}/latest`, {
|
|
@@ -3720,8 +3706,45 @@ async function latestPublished(pkg) {
|
|
|
3720
3706
|
return void 0;
|
|
3721
3707
|
}
|
|
3722
3708
|
}
|
|
3709
|
+
function detectPackageManager(binPath) {
|
|
3710
|
+
const path = binPath.replaceAll("\\", "/");
|
|
3711
|
+
if (path.includes("/.bun/")) return "bun";
|
|
3712
|
+
if (path.includes("/pnpm/") || path.includes("/.pnpm/")) return "pnpm";
|
|
3713
|
+
if (path.includes("/yarn/") || path.includes("/.yarn/")) return "yarn";
|
|
3714
|
+
return "npm";
|
|
3715
|
+
}
|
|
3716
|
+
function installCommand(manager, version) {
|
|
3717
|
+
const spec = `@myna-sh/cli@${version}`;
|
|
3718
|
+
switch (manager) {
|
|
3719
|
+
case "pnpm":
|
|
3720
|
+
return ["pnpm", "add", "-g", spec];
|
|
3721
|
+
case "yarn":
|
|
3722
|
+
return ["yarn", "global", "add", spec];
|
|
3723
|
+
case "bun":
|
|
3724
|
+
return ["bun", "add", "-g", spec];
|
|
3725
|
+
case "npm":
|
|
3726
|
+
return ["npm", "install", "-g", spec];
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// src/version.ts
|
|
3731
|
+
var VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
3732
|
+
var IS_RELEASE_BUILD = true;
|
|
3733
|
+
|
|
3734
|
+
// src/commands/doctor.ts
|
|
3735
|
+
var SCAN_IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".react-router", ".turbo", "coverage"]);
|
|
3736
|
+
var GENERATED_MARKERS = [
|
|
3737
|
+
"// Generated by `myna types generate`. Do not edit by hand.",
|
|
3738
|
+
"export interface MynaCollections {"
|
|
3739
|
+
];
|
|
3740
|
+
function looksGenerated(contents) {
|
|
3741
|
+
return GENERATED_MARKERS.every((marker) => contents.includes(marker));
|
|
3742
|
+
}
|
|
3743
|
+
function check(id, title, status, detail, remedy) {
|
|
3744
|
+
return remedy ? { id, title, status, detail, remedy } : { id, title, status, detail };
|
|
3745
|
+
}
|
|
3723
3746
|
async function checkCliVersion() {
|
|
3724
|
-
if (!
|
|
3747
|
+
if (!IS_RELEASE_BUILD) {
|
|
3725
3748
|
return check(
|
|
3726
3749
|
"cli.version",
|
|
3727
3750
|
"CLI version",
|
|
@@ -3758,7 +3781,7 @@ function checkApi(meta, apiUrl) {
|
|
|
3758
3781
|
const checks = [
|
|
3759
3782
|
check("api.reachable", "API reachable", "pass", `${apiUrl} speaks API ${meta.apiVersion}.`)
|
|
3760
3783
|
];
|
|
3761
|
-
if (!
|
|
3784
|
+
if (!IS_RELEASE_BUILD) {
|
|
3762
3785
|
checks.push(
|
|
3763
3786
|
check("api.compatibility", "Client compatibility", "skip", "Unreleased CLI build; nothing to compare.")
|
|
3764
3787
|
);
|
|
@@ -4030,6 +4053,58 @@ ${failed} failure(s), ${warned} warning(s).`
|
|
|
4030
4053
|
);
|
|
4031
4054
|
}
|
|
4032
4055
|
|
|
4056
|
+
// src/commands/update.ts
|
|
4057
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4058
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4059
|
+
function registerUpdate(program) {
|
|
4060
|
+
program.command("update").description("Update the CLI to the latest published release").option("--check", "report whether an update exists without installing it", false).action(
|
|
4061
|
+
handle(async (_ctx, _args, opts) => {
|
|
4062
|
+
const current = VERSION;
|
|
4063
|
+
const checkOnly = Boolean(opts.check);
|
|
4064
|
+
if (!IS_RELEASE_BUILD) {
|
|
4065
|
+
emit(
|
|
4066
|
+
{ current, latest: null, upToDate: false, updated: false, reason: "unreleased" },
|
|
4067
|
+
() => diag(`Running an unreleased build (${current}); nothing to update. Install a release with: npm install -g @myna-sh/cli`)
|
|
4068
|
+
);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const latest = await latestPublished("@myna-sh/cli");
|
|
4072
|
+
if (!latest) {
|
|
4073
|
+
throw new CliError("Could not reach the npm registry to check for updates.");
|
|
4074
|
+
}
|
|
4075
|
+
if (compareSemVer(current, latest) >= 0) {
|
|
4076
|
+
emit(
|
|
4077
|
+
{ current, latest, upToDate: true, updated: false },
|
|
4078
|
+
() => diag(`${current} is the latest release.`)
|
|
4079
|
+
);
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
const manager = detectPackageManager(fileURLToPath2(import.meta.url));
|
|
4083
|
+
const command = installCommand(manager, latest);
|
|
4084
|
+
if (checkOnly) {
|
|
4085
|
+
emit(
|
|
4086
|
+
{ current, latest, upToDate: false, updated: false, command: command.join(" ") },
|
|
4087
|
+
() => diag(`${current} is behind ${latest}. Update with: ${command.join(" ")}`)
|
|
4088
|
+
);
|
|
4089
|
+
process.exitCode = 1;
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
diag(`Updating ${current} \u2192 ${latest} with: ${command.join(" ")}`);
|
|
4093
|
+
try {
|
|
4094
|
+
execFileSync3(command[0], command.slice(1), { stdio: "inherit" });
|
|
4095
|
+
} catch (error) {
|
|
4096
|
+
throw new CliError(
|
|
4097
|
+
`Update failed: ${error instanceof Error ? error.message : String(error)}. Run it yourself with: ${command.join(" ")}`
|
|
4098
|
+
);
|
|
4099
|
+
}
|
|
4100
|
+
emit(
|
|
4101
|
+
{ current, latest, upToDate: false, updated: true, command: command.join(" ") },
|
|
4102
|
+
() => diag(`Updated to ${latest}. Run \`myna doctor\` to confirm the API agrees.`)
|
|
4103
|
+
);
|
|
4104
|
+
})
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4033
4108
|
// src/commands/sync.ts
|
|
4034
4109
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
4035
4110
|
import { join as join7 } from "path";
|
|
@@ -4211,6 +4286,7 @@ function buildProgram() {
|
|
|
4211
4286
|
program.name("myna").description("Myna \u2014 content infrastructure for developers and agents").version(VERSION, "-v, --version").option("--json", "emit a single machine-readable JSON value on stdout").option("--project <ref>", "project id or slug").option("--organization <ref>", "organization id or slug").option("--token <token>", "API token (overrides stored credentials)").option("--api-url <url>", "API base URL").option("--no-interactive", "disable prompts and browser opening").showHelpAfterError();
|
|
4212
4287
|
registerAuth(program);
|
|
4213
4288
|
registerDoctor(program);
|
|
4289
|
+
registerUpdate(program);
|
|
4214
4290
|
registerWorkspace(program);
|
|
4215
4291
|
registerSchema(program);
|
|
4216
4292
|
registerEntries(program);
|