@vectoriox/iox-ui 5.61.0 → 5.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +466 -274
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/generation-core/index.d.ts +181 -99
- package/package.json +1 -1
- package/src/generation-core/ai-run.ts +40 -3
- package/src/generation-core/plan-edits.ts +128 -47
- package/src/generation-core/plan-wire.ts +150 -62
- package/src/generation-core/strict-schema.ts +32 -7
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
import { patternById, patternOptionalParts, patternSlots, patternTakesBinding } from './patterns';
|
|
5
5
|
import {
|
|
6
6
|
contentTypeToWire, dataSourceToWire, expandWireContentType, expandWireDataSource, expandWireField, expandWirePage, expandWireSection, fieldToWire,
|
|
7
|
-
pageToWire, PlanWireShapeError, sectionPatternIds, sectionToWire, wireContentTypeJsonSchema, wireDataSourceJsonSchema,
|
|
8
|
-
wirePageJsonSchema, wireSectionJsonSchema,
|
|
7
|
+
pageToWire, PLAN_PARTS, PlanPart, PlanWireShapeError, sectionPatternIds, sectionToWire, wireContentTypeJsonSchema, wireDataSourceJsonSchema,
|
|
8
|
+
wireFieldJsonSchema, WireOptions, wirePageJsonSchema, wireSectionJsonSchema,
|
|
9
9
|
} from './plan-wire';
|
|
10
10
|
import {
|
|
11
11
|
ContentFieldPlan, ContentTypePlan, DataSourcePlan, PagePlan, PageSectionPlan, PlanDataSourceMode, PlanDataSourceType, SitePlan,
|
|
@@ -68,10 +68,35 @@ export const PLAN_EDIT_SHAPE: Readonly<Record<`${PlanEditOp}`, ShapeEntry>> = {
|
|
|
68
68
|
[PlanEditOp.SwapPattern]: { op: "'swapPattern'", page: 'string — a page key', index: 'integer — the section', pattern: 'string — the pattern it becomes (its slots kept where the new one has them)' },
|
|
69
69
|
[PlanEditOp.SetSlot]: { op: "'setSlot'", page: 'string — a page key', index: 'integer — the section', slot: "string — one of the pattern's slots", text: "string — the words, in the owner's language and voice" },
|
|
70
70
|
[PlanEditOp.AddContentType]: { op: "'addContentType'", contentType: 'contentType — a unique name', 'dataSources?': 'dataSource[] — the list / single sources its pages read' },
|
|
71
|
-
[PlanEditOp.AddField]: { op: "'addField'", contentType: 'string — a content type of the plan', field: 'field', 'sample?': "string | string[] — the samples' value for it" },
|
|
71
|
+
[PlanEditOp.AddField]: { op: "'addField'", contentType: 'string — a content type of the plan', field: 'field', 'sample?': "string | string[] — the samples' value for it; absent: what the type's seed already holds for it, else ''" },
|
|
72
72
|
[PlanEditOp.SetSectionData]: { op: "'setSectionData'", page: 'string — a page key', index: 'integer — the section', binding: 'string — the list source the section repeats' },
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The plan part each op changes (vectoriox/iox-cms#47): a sentence is answered in the plan's two parts, one request each —
|
|
77
|
+
* the structure's edits first, then the pages' written knowing them.
|
|
78
|
+
*/
|
|
79
|
+
export const PLAN_EDIT_OP_PART: Readonly<Record<`${PlanEditOp}`, PlanPart>> = {
|
|
80
|
+
[PlanEditOp.AddPage]: PlanPart.Pages,
|
|
81
|
+
[PlanEditOp.RemovePage]: PlanPart.Pages,
|
|
82
|
+
[PlanEditOp.RenamePage]: PlanPart.Pages,
|
|
83
|
+
[PlanEditOp.MergePages]: PlanPart.Pages,
|
|
84
|
+
[PlanEditOp.MovePage]: PlanPart.Pages,
|
|
85
|
+
[PlanEditOp.AddSection]: PlanPart.Pages,
|
|
86
|
+
[PlanEditOp.RemoveSection]: PlanPart.Pages,
|
|
87
|
+
[PlanEditOp.MoveSection]: PlanPart.Pages,
|
|
88
|
+
[PlanEditOp.SwapPattern]: PlanPart.Pages,
|
|
89
|
+
[PlanEditOp.SetSlot]: PlanPart.Pages,
|
|
90
|
+
[PlanEditOp.SetSectionData]: PlanPart.Pages,
|
|
91
|
+
[PlanEditOp.AddContentType]: PlanPart.Structure,
|
|
92
|
+
[PlanEditOp.AddField]: PlanPart.Structure,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** The ops written in one part, in `PlanEditOp` order. */
|
|
96
|
+
export function planEditOps(part: PlanPart): PlanEditOp[] {
|
|
97
|
+
return Object.values(PlanEditOp).filter(op => PLAN_EDIT_OP_PART[op] === part);
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
export enum PlanChangeKind {
|
|
76
101
|
Page = 'page',
|
|
77
102
|
Section = 'section',
|
|
@@ -207,6 +232,10 @@ function findSection(d: Draft, pageAt: number, index: unknown, c: Collector, pat
|
|
|
207
232
|
|
|
208
233
|
const setSections = (d: Draft, at: number, sections: PageSectionPlan[]) => { d.pages[at] = { ...d.pages[at], sections }; };
|
|
209
234
|
|
|
235
|
+
/** A child added to a page or type the same edits added is part of that addition — the map marks the parent only. */
|
|
236
|
+
const addedHere = (changed: ReadonlyArray<PlanChange>, kind: PlanChangeKind, at: { page?: string; name?: string }) =>
|
|
237
|
+
changed.some(ch => ch.kind === kind && ch.action === PlanChangeAction.Added && (kind === PlanChangeKind.Page ? ch.page === at.page : ch.name === at.name));
|
|
238
|
+
|
|
210
239
|
/** One handler per op — a data table, never an if-chain. */
|
|
211
240
|
const APPLY: Readonly<Record<`${PlanEditOp}`, Apply>> = {
|
|
212
241
|
[PlanEditOp.AddPage]: (d, e, c, path, changed) => {
|
|
@@ -260,7 +289,7 @@ const APPLY: Readonly<Record<`${PlanEditOp}`, Apply>> = {
|
|
|
260
289
|
const index = e.at === undefined ? sections.length : clamp(e.at, sections.length);
|
|
261
290
|
sections.splice(index, 0, e.section as PageSectionPlan);
|
|
262
291
|
setSections(d, at, sections);
|
|
263
|
-
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: d.pages[at].key, index });
|
|
292
|
+
if (!addedHere(changed, PlanChangeKind.Page, { page: d.pages[at].key })) changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: d.pages[at].key, index });
|
|
264
293
|
},
|
|
265
294
|
[PlanEditOp.RemoveSection]: (d, e, c, path, changed) => {
|
|
266
295
|
const at = findPage(d, e.page, c, `${path}.page`);
|
|
@@ -320,9 +349,9 @@ const APPLY: Readonly<Record<`${PlanEditOp}`, Apply>> = {
|
|
|
320
349
|
if (at < 0) return c.add(PlanEditIssueCode.UnknownContentType, `${path}.contentType`, `no content type "${String(e.contentType)}" in the plan`);
|
|
321
350
|
const type = d.contentTypes[at];
|
|
322
351
|
if (type.fields.some(f => f.name === e.field.name)) return c.add(PlanEditIssueCode.Duplicate, `${path}.field.name`, `"${type.name}" already has a field "${e.field.name}"`);
|
|
323
|
-
const seed = type.seed ? { ...type.seed, [e.field.name]: e.sample ?? '' } : undefined;
|
|
352
|
+
const seed = type.seed ? { ...type.seed, [e.field.name]: e.sample ?? type.seed[e.field.name] ?? '' } : undefined;
|
|
324
353
|
d.contentTypes[at] = { ...type, fields: [...type.fields, e.field], ...(seed ? { seed } : {}) };
|
|
325
|
-
changed.push({ kind: PlanChangeKind.Field, action: PlanChangeAction.Added, name: `${type.name}.${e.field.name}` });
|
|
354
|
+
if (!addedHere(changed, PlanChangeKind.ContentType, { name: type.name })) changed.push({ kind: PlanChangeKind.Field, action: PlanChangeAction.Added, name: `${type.name}.${e.field.name}` });
|
|
326
355
|
},
|
|
327
356
|
[PlanEditOp.SetSectionData]: (d, e, c, path, changed) => {
|
|
328
357
|
const at = findPage(d, e.page, c, `${path}.page`);
|
|
@@ -361,7 +390,11 @@ export function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>):
|
|
|
361
390
|
|
|
362
391
|
// ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
|
|
363
392
|
|
|
364
|
-
|
|
393
|
+
/** Each part's answer format name. */
|
|
394
|
+
export const PLAN_EDITS_PART_SCHEMA_NAMES: Readonly<Record<PlanPart, string>> = {
|
|
395
|
+
[PlanPart.Structure]: 'plan_structure_edits',
|
|
396
|
+
[PlanPart.Pages]: 'plan_page_edits',
|
|
397
|
+
};
|
|
365
398
|
|
|
366
399
|
/** A sentence's answer: the edits, or why the ops cannot say it (one line, in the owner's language). */
|
|
367
400
|
export enum PlanEditsOutcome {
|
|
@@ -376,18 +409,28 @@ export interface PlanEditsAnswer {
|
|
|
376
409
|
reason: string;
|
|
377
410
|
}
|
|
378
411
|
|
|
379
|
-
/**
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Each part's answer, explained. The structure is asked first, so it says whether the sentence is a plan change at all
|
|
414
|
+
* (`outcome`); the pages are asked only when it is, and write only their edits.
|
|
415
|
+
*/
|
|
416
|
+
export const PLAN_EDITS_ANSWER_SHAPES: Readonly<Record<PlanPart, ShapeEntry>> = {
|
|
417
|
+
[PlanPart.Structure]: {
|
|
418
|
+
outcome: "'edits' | 'cannot' — cannot: no operation of either part (below) can do what the sentence asks",
|
|
419
|
+
edits: 'edit[] — the content types and fields the sentence adds, in order; empty when it adds none, or cannot',
|
|
420
|
+
reason: "string — when cannot, ONE short line for the owner in their language (what to do instead); else ''",
|
|
421
|
+
},
|
|
422
|
+
[PlanPart.Pages]: {
|
|
423
|
+
edits: 'edit[] — the page and section edits, in order, made after the structure edits (already applied); empty when none',
|
|
424
|
+
},
|
|
384
425
|
};
|
|
385
426
|
|
|
386
427
|
/**
|
|
387
|
-
* ONE edit on the wire (vectoriox/iox-cms#45):
|
|
388
|
-
* branches
|
|
389
|
-
*
|
|
390
|
-
* none. A
|
|
428
|
+
* ONE flat edit per part on the wire (vectoriox/iox-cms#45, #47): an object whose keys are ALL required — the 13 ops as
|
|
429
|
+
* `anyOf` branches came to 395 optional parameters against the Claude API's 24, and one flat edit for all of them to 95
|
|
430
|
+
* properties, a grammar Claude refuses (≤ 60). `op` says which keys are read (`PLAN_EDIT_WIRE_KEYS`, from
|
|
431
|
+
* `PLAN_EDIT_SHAPE`); every other key holds its none. A new page, section, content type or field travels as a list of
|
|
432
|
+
* exactly one, in the plan wire's format — and an added page or content type travels WITHOUT its children: each of its
|
|
433
|
+
* sections is an `addSection` after it, each of its fields an `addField` (a new type's samples ride on its `seed`).
|
|
391
434
|
*/
|
|
392
435
|
interface EditWireKey {
|
|
393
436
|
text: string;
|
|
@@ -400,6 +443,8 @@ interface EditWireKey {
|
|
|
400
443
|
}
|
|
401
444
|
|
|
402
445
|
const NO_INDEX = -1;
|
|
446
|
+
/** A new page or content type on the wire: its children are edits of their own. */
|
|
447
|
+
const CHILDLESS: WireOptions = { children: false };
|
|
403
448
|
|
|
404
449
|
/** A list holding exactly one object: none → absent, one → it (expanded), more → the list (the gate refuses it). */
|
|
405
450
|
const oneOf = (schema: () => JsonSchema, expand: (v: unknown) => unknown, toWire: (v: any) => unknown, what: string): EditWireKey => ({
|
|
@@ -420,17 +465,22 @@ const EDIT_WIRE: Readonly<Record<string, EditWireKey>> = {
|
|
|
420
465
|
after: textKey('addPage: the key of the page it follows in the menu'),
|
|
421
466
|
into: textKey('mergePages: the page that stays'),
|
|
422
467
|
from: textKey('mergePages: the page whose sections move into it, then removed'),
|
|
423
|
-
index: indexKey('the section, 0-based on its page'),
|
|
468
|
+
index: indexKey('the section, 0-based on its page (addSection: the place the new one goes)'),
|
|
424
469
|
to: indexKey('movePage / moveSection: the new 0-based place'),
|
|
425
|
-
at: indexKey('addSection: its 0-based place on the page'),
|
|
426
470
|
pattern: { text: 'string — swapPattern: the section pattern it becomes', none: '', schema: () => jsonOneOf(['', ...sectionPatternIds()]) },
|
|
427
471
|
slot: textKey("setSlot: one of the pattern's slots"),
|
|
428
472
|
text: textKey("setSlot: the words, in the owner's language and voice"),
|
|
429
473
|
binding: textKey('setSectionData: the list source the section repeats'),
|
|
430
474
|
contentType: textKey('addField: a content type of the plan'),
|
|
431
|
-
newPage:
|
|
475
|
+
newPage: {
|
|
476
|
+
...oneOf(() => wirePageJsonSchema(CHILDLESS), v => expandWirePage(v, CHILDLESS), p => pageToWire(p, CHILDLESS), 'page'),
|
|
477
|
+
text: 'page[] — exactly one when the op takes it, WITHOUT sections: each is an addSection after it',
|
|
478
|
+
},
|
|
432
479
|
newSection: oneOf(wireSectionJsonSchema, expandWireSection, sectionToWire, 'section'),
|
|
433
|
-
newContentType:
|
|
480
|
+
newContentType: {
|
|
481
|
+
...oneOf(() => wireContentTypeJsonSchema(CHILDLESS), v => expandWireContentType(v, CHILDLESS), t => contentTypeToWire(t, CHILDLESS), 'contentType'),
|
|
482
|
+
text: "contentType[] — exactly one when the op takes it, WITHOUT fields: each is an addField after it (the samples' values stay in its seed)",
|
|
483
|
+
},
|
|
434
484
|
dataSources: {
|
|
435
485
|
text: 'dataSource[] — addContentType: the list / single sources its pages read', none: [],
|
|
436
486
|
schema: () => jsonListOf(wireDataSourceJsonSchema()),
|
|
@@ -448,7 +498,7 @@ export const PLAN_EDIT_WIRE_KEYS: Readonly<Record<`${PlanEditOp}`, Readonly<Reco
|
|
|
448
498
|
[PlanEditOp.RenamePage]: { page: 'page', name: 'name', route: 'route' },
|
|
449
499
|
[PlanEditOp.MergePages]: { into: 'into', from: 'from' },
|
|
450
500
|
[PlanEditOp.MovePage]: { page: 'page', to: 'to' },
|
|
451
|
-
[PlanEditOp.AddSection]: { page: 'page', section: 'newSection', at: '
|
|
501
|
+
[PlanEditOp.AddSection]: { page: 'page', section: 'newSection', at: 'index' },
|
|
452
502
|
[PlanEditOp.RemoveSection]: { page: 'page', index: 'index' },
|
|
453
503
|
[PlanEditOp.MoveSection]: { page: 'page', from: 'index', to: 'to' },
|
|
454
504
|
[PlanEditOp.SwapPattern]: { page: 'page', index: 'index', pattern: 'pattern' },
|
|
@@ -458,23 +508,32 @@ export const PLAN_EDIT_WIRE_KEYS: Readonly<Record<`${PlanEditOp}`, Readonly<Reco
|
|
|
458
508
|
[PlanEditOp.SetSectionData]: { page: 'page', index: 'index', binding: 'binding' },
|
|
459
509
|
};
|
|
460
510
|
|
|
511
|
+
/** The wire keys a part's edit carries: every key its ops read, in `EDIT_WIRE` order. */
|
|
512
|
+
export function planEditWireKeys(part: PlanPart): string[] {
|
|
513
|
+
const read = new Set(planEditOps(part).flatMap(op => Object.values(PLAN_EDIT_WIRE_KEYS[op])));
|
|
514
|
+
return Object.keys(EDIT_WIRE).filter(k => read.has(k));
|
|
515
|
+
}
|
|
516
|
+
|
|
461
517
|
const isEditWireNone = (wireKey: string, v: unknown) => {
|
|
462
518
|
const none = EDIT_WIRE[wireKey].none;
|
|
463
519
|
return v === undefined || (Array.isArray(none) ? Array.isArray(v) && v.length === 0 : v === none) || (wireKey === 'sample' && Array.isArray(v) && v.length === 0);
|
|
464
520
|
};
|
|
465
521
|
const noneText = (none: unknown) => (Array.isArray(none) ? '[]' : typeof none === 'string' ? "''" : String(none));
|
|
522
|
+
const opsText = (part: PlanPart) => planEditOps(part).map(op => `'${op}'`).join(' | ');
|
|
523
|
+
|
|
524
|
+
/** A part's flat edit, as a shape table: every key required, each with its none. */
|
|
525
|
+
export function planEditWireShape(part: PlanPart): ShapeEntry {
|
|
526
|
+
return {
|
|
527
|
+
op: `${opsText(part)} — which keys below are read`,
|
|
528
|
+
...Object.fromEntries(planEditWireKeys(part).map(k => [k, `${EDIT_WIRE[k].text} — ${noneText(EDIT_WIRE[k].none)} when the op does not read it`])),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
466
531
|
|
|
467
|
-
/**
|
|
468
|
-
export
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
/** Per op, the wire keys it reads, in `PLAN_EDIT_SHAPE`'s words (`?` = may hold its none) — what the model reads. */
|
|
474
|
-
export function planEditWireOps(): Record<string, Record<string, string>> {
|
|
475
|
-
return Object.fromEntries(OPS.map(op => {
|
|
476
|
-
const entry = PLAN_EDIT_SHAPE[op as PlanEditOp];
|
|
477
|
-
const keys = PLAN_EDIT_WIRE_KEYS[op as PlanEditOp];
|
|
532
|
+
/** Per op of a part, the wire keys it reads, in `PLAN_EDIT_SHAPE`'s words (`?` = may hold its none) — what the model reads. */
|
|
533
|
+
export function planEditWireOps(part: PlanPart): Record<string, Record<string, string>> {
|
|
534
|
+
return Object.fromEntries(planEditOps(part).map(op => {
|
|
535
|
+
const entry = PLAN_EDIT_SHAPE[op];
|
|
536
|
+
const keys = PLAN_EDIT_WIRE_KEYS[op];
|
|
478
537
|
return [op, Object.fromEntries(Object.entries(keys).map(([planKey, wireKey]) => {
|
|
479
538
|
const optionalKey = `${planKey}?` in entry;
|
|
480
539
|
return [`${wireKey}${optionalKey ? '?' : ''}`, entry[optionalKey ? `${planKey}?` : planKey]];
|
|
@@ -497,19 +556,21 @@ export function planEditWireGaps(): string[] {
|
|
|
497
556
|
return gaps;
|
|
498
557
|
}
|
|
499
558
|
|
|
500
|
-
|
|
501
|
-
|
|
559
|
+
const ANSWER_SCHEMAS: Readonly<Record<string, (part: PlanPart) => JsonSchema>> = {
|
|
560
|
+
outcome: () => jsonOneOf(Object.values(PlanEditsOutcome)),
|
|
561
|
+
reason: () => jsonText(),
|
|
562
|
+
edits: part => jsonListOf(jsonShapedObject(planEditWireShape(part), {
|
|
563
|
+
op: jsonOneOf(planEditOps(part)),
|
|
564
|
+
...Object.fromEntries(planEditWireKeys(part).map(k => [k, EDIT_WIRE[k].schema()])),
|
|
565
|
+
}, `edit ${part}`)),
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/** The format one part of a sentence's answer is written in — one request each (vectoriox/iox-cms#47). */
|
|
569
|
+
export function planEditsJsonSchema(part: PlanPart): JsonSchema {
|
|
502
570
|
const gaps = planEditWireGaps();
|
|
503
571
|
if (gaps.length) throw new PlanWireShapeError(`the plan edits wire does not carry ${gaps.join(', ')}`);
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
...Object.fromEntries(Object.entries(EDIT_WIRE).map(([k, spec]) => [k, spec.schema()])),
|
|
507
|
-
}, 'edit');
|
|
508
|
-
return jsonShapedObject(PLAN_EDITS_ANSWER_SHAPE, {
|
|
509
|
-
outcome: jsonOneOf(Object.values(PlanEditsOutcome)),
|
|
510
|
-
edits: jsonListOf(edit),
|
|
511
|
-
reason: jsonText(),
|
|
512
|
-
}, 'planEdits');
|
|
572
|
+
const shape = PLAN_EDITS_ANSWER_SHAPES[part];
|
|
573
|
+
return jsonShapedObject(shape, Object.fromEntries(shapeKeys(shape).map(k => [k, ANSWER_SCHEMAS[k](part)])), `planEdits ${part}`);
|
|
513
574
|
}
|
|
514
575
|
|
|
515
576
|
/** One flat edit → the op's edit: the keys it reads, a none left out where the op may go without it. Never repaired. */
|
|
@@ -528,9 +589,10 @@ export function expandWireEdit(wire: unknown): PlanEdit | unknown {
|
|
|
528
589
|
return out;
|
|
529
590
|
}
|
|
530
591
|
|
|
531
|
-
/** An edit →
|
|
532
|
-
|
|
533
|
-
const
|
|
592
|
+
/** An edit's own keys → its flat wire edit, in its part's keys. */
|
|
593
|
+
function flatEdit(edit: PlanEdit): PlainObject {
|
|
594
|
+
const part = PLAN_EDIT_OP_PART[edit.op];
|
|
595
|
+
const out: PlainObject = { op: edit.op, ...Object.fromEntries(planEditWireKeys(part).map(k => [k, Array.isArray(EDIT_WIRE[k].none) ? [] : EDIT_WIRE[k].none])) };
|
|
534
596
|
for (const [planKey, wireKey] of Object.entries(PLAN_EDIT_WIRE_KEYS[edit.op])) {
|
|
535
597
|
const value = (edit as unknown as PlainObject)[planKey];
|
|
536
598
|
if (value === undefined) continue;
|
|
@@ -539,7 +601,26 @@ export function planEditToWire(edit: PlanEdit): PlainObject {
|
|
|
539
601
|
return out;
|
|
540
602
|
}
|
|
541
603
|
|
|
542
|
-
/**
|
|
604
|
+
/** The children an added page or content type travels without, as the edits that add them after it. */
|
|
605
|
+
function childEdits(edit: PlanEdit): PlanEdit[] {
|
|
606
|
+
if (edit.op === PlanEditOp.AddPage) return edit.page.sections.map(section => ({ op: PlanEditOp.AddSection, page: edit.page.key, section }));
|
|
607
|
+
if (edit.op === PlanEditOp.AddContentType) return edit.contentType.fields.map(field => ({ op: PlanEditOp.AddField, contentType: edit.contentType.name, field }));
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** An edit → the flat wire edits a model would write for it: itself, then its children (fakes and specs write them). */
|
|
612
|
+
export function planEditToWire(edit: PlanEdit): PlainObject[] {
|
|
613
|
+
return [flatEdit(edit), ...childEdits(edit).map(flatEdit)];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Edits → each part's flat wire edits, in order (what each part's answer would hold). */
|
|
617
|
+
export function planEditsToWire(edits: ReadonlyArray<PlanEdit>): Record<PlanPart, PlainObject[]> {
|
|
618
|
+
const out = Object.fromEntries(PLAN_PARTS.map(part => [part, [] as PlainObject[]])) as Record<PlanPart, PlainObject[]>;
|
|
619
|
+
for (const edit of edits) out[PLAN_EDIT_OP_PART[edit.op]].push(...planEditToWire(edit));
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** A model's answer (either part's) → edits (each flat edit expanded); never repaired. */
|
|
543
624
|
export function planEditsFromWire(answer: unknown): PlanEditsAnswer | unknown {
|
|
544
625
|
if (!isPlainObject(answer) || !Array.isArray(answer['edits'])) return answer;
|
|
545
626
|
return { ...answer, edits: (answer['edits'] as unknown[]).map(expandWireEdit) };
|
|
@@ -11,16 +11,21 @@ import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
|
|
|
11
11
|
import { isPlainObject, PlainObject, ShapeEntry, shapeKeys } from './shape-check';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* The plan WIRE (AI plan M1-3, vectoriox/iox-cms#45): what a model writes when it plans a site — a compact format
|
|
15
|
-
* the Claude API's structured-output limits (≤ 24 optional parameters and ≤ 16 unions per request
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* stage's gate judge
|
|
14
|
+
* The plan WIRE (AI plan M1-3, vectoriox/iox-cms#45, #47): what a model writes when it plans a site — a compact format
|
|
15
|
+
* inside the Claude API's structured-output limits (≤ 24 optional parameters and ≤ 16 unions per request, and a compiled
|
|
16
|
+
* grammar under the size Claude refuses — measured as ≤ 60 properties; `strict-schema.ts`). The Site Plan itself
|
|
17
|
+
* (`SPEC_SITE_PLAN_SHAPE`) has 213 optional keys once every pattern is a branch, so the model never writes it: it writes
|
|
18
|
+
* this, and ONE pure `expandPlanWire` turns it into the Site Plan that `validateSitePlan` and the stage's gate judge
|
|
19
|
+
* exactly as before.
|
|
20
|
+
*
|
|
21
|
+
* - **Written in two PARTS, one request each** (#47 — the whole plan in one format was 64 properties): the **structure** (the business,
|
|
22
|
+
* the content types, the data sources) and then the **pages** (the pages, the header and footer), written knowing the
|
|
23
|
+
* structure. Every top-level key belongs to exactly one part (`part` below); `expandPlanWire` puts the parts together.
|
|
19
24
|
*
|
|
20
25
|
* - **Every key required.** A plan key that may be absent is written with its stated "none" — `''`, `0`, `false`, `[]`.
|
|
21
26
|
* - **Closed lists as enums** (patterns, page types, needs, slots, parts, forms, entrances, header/footer), from the
|
|
22
27
|
* contract — never per-pattern branches: the gate refuses a slot or a part the chosen pattern does not have, with its path.
|
|
23
|
-
* - **Maps as lists:** slots `[{ slot, text }]`, links `[{ slot, route }]`, seed values `[{ field, value }]`, roles
|
|
28
|
+
* - **Maps as lists:** slots `[{ slot, text }]`, links `[{ slot, route }]`, seed values `[{ field, value }]`, roles `[{ role, field }]`.
|
|
24
29
|
* - **One data source object** for both kinds; `type` says which keys are read (the other kind's hold their none).
|
|
25
30
|
* - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
|
|
26
31
|
* the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
|
|
@@ -31,13 +36,25 @@ import { isPlainObject, PlainObject, ShapeEntry, shapeKeys } from './shape-check
|
|
|
31
36
|
* is placed. The expanders translate, never repair: a value the model got wrong stays wrong, for the gate to refuse.
|
|
32
37
|
*/
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
/** The plan's parts, in the order they are written: the pages bind to the structure's sources and types. */
|
|
40
|
+
export enum PlanPart {
|
|
41
|
+
Structure = 'structure',
|
|
42
|
+
Pages = 'pages',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const PLAN_PARTS: ReadonlyArray<PlanPart> = [PlanPart.Structure, PlanPart.Pages];
|
|
46
|
+
|
|
47
|
+
/** Each part's format name (a request names the format it asks for). */
|
|
48
|
+
export const PLAN_WIRE_PART_SCHEMA_NAMES: Readonly<Record<PlanPart, string>> = {
|
|
49
|
+
[PlanPart.Structure]: 'site_plan_structure',
|
|
50
|
+
[PlanPart.Pages]: 'site_plan_pages',
|
|
51
|
+
};
|
|
35
52
|
|
|
36
53
|
export enum PlanWireEntry {
|
|
37
54
|
Plan = 'plan',
|
|
38
55
|
Business = 'business',
|
|
39
56
|
ContentType = 'contentType',
|
|
40
|
-
|
|
57
|
+
RoleField = 'roleField',
|
|
41
58
|
Field = 'field',
|
|
42
59
|
Seed = 'seed',
|
|
43
60
|
SeedValue = 'seedValue',
|
|
@@ -59,18 +76,24 @@ interface WireKey {
|
|
|
59
76
|
text?: string;
|
|
60
77
|
/** What stands for "absent" (an optional plan key). */
|
|
61
78
|
none?: string;
|
|
79
|
+
/** The part a top-level key is written in. */
|
|
80
|
+
part?: PlanPart;
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
const PLAN_SHAPE = SPEC_SITE_PLAN_SHAPE as unknown as Readonly<Record<string, ShapeEntry>>;
|
|
65
84
|
const key = (ref: string, none?: string): WireKey => ({ refs: [ref], ...(none !== undefined ? { none } : {}) });
|
|
85
|
+
const inPart = (part: PlanPart, spec: WireKey): WireKey => ({ ...spec, part });
|
|
86
|
+
/** The roles a content type's fields play (`roles` of the plan shape), in the plan shape's order. */
|
|
87
|
+
const ROLE_KEYS: ReadonlyArray<string> = shapeKeys(PLAN_SHAPE['roles']);
|
|
88
|
+
const roleWords = ROLE_KEYS.map(role => `${role}: ${describeShapeKey(PLAN_SHAPE['roles'], role).replace(/^string — /, '')}`).join('; ');
|
|
66
89
|
|
|
67
90
|
const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, WireKey>>>> = {
|
|
68
91
|
plan: {
|
|
69
|
-
business: key('plan.business'),
|
|
70
|
-
contentTypes: key('plan.contentTypes'),
|
|
71
|
-
dataSources: { refs: ['plan.dataSources'], text: 'dataSource[] — merged by alias into the org library' },
|
|
72
|
-
pages: key('plan.pages'),
|
|
73
|
-
globals: key('plan.globals'),
|
|
92
|
+
business: inPart(PlanPart.Structure, key('plan.business')),
|
|
93
|
+
contentTypes: inPart(PlanPart.Structure, key('plan.contentTypes')),
|
|
94
|
+
dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text: 'dataSource[] — merged by alias into the org library' }),
|
|
95
|
+
pages: inPart(PlanPart.Pages, key('plan.pages')),
|
|
96
|
+
globals: inPart(PlanPart.Pages, key('plan.globals')),
|
|
74
97
|
},
|
|
75
98
|
business: {
|
|
76
99
|
name: key('business.name'),
|
|
@@ -79,17 +102,13 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
|
|
|
79
102
|
contentType: {
|
|
80
103
|
name: key('contentType.name'),
|
|
81
104
|
label: key('contentType.label'),
|
|
82
|
-
roles:
|
|
105
|
+
roles: { refs: ['contentType.roles'], text: `roleField[] — ${describeShapeKey(PLAN_SHAPE['contentType'], 'roles').replace(/^roles — /, '')}; a role no field plays is left out` },
|
|
83
106
|
fields: key('contentType.fields'),
|
|
84
107
|
seed: { refs: ['contentType.seed'], text: 'seed — the sample items', none: 'count 0 and no values' },
|
|
85
108
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
image: key('roles.image', "''"),
|
|
90
|
-
meta: key('roles.meta', "''"),
|
|
91
|
-
date: key('roles.date', "''"),
|
|
92
|
-
gallery: key('roles.gallery', "''"),
|
|
109
|
+
roleField: {
|
|
110
|
+
role: { refs: ROLE_KEYS.map(role => `roles.${role}`), text: `${ROLE_KEYS.map(r => `'${r}'`).join(' | ')} — ${roleWords}` },
|
|
111
|
+
field: { refs: ['roles.title'], text: 'string — the field of the type that plays it' },
|
|
93
112
|
},
|
|
94
113
|
field: {
|
|
95
114
|
name: key('field.name'),
|
|
@@ -184,6 +203,25 @@ export const PLAN_WIRE_SHAPE: Readonly<Record<`${PlanWireEntry}`, ShapeEntry>> =
|
|
|
184
203
|
Object.entries(WIRE_KEYS).map(([entry, keys]) => [entry, Object.fromEntries(Object.entries(keys).map(([k, spec]) => [k, wireText(spec)]))]),
|
|
185
204
|
) as Record<`${PlanWireEntry}`, ShapeEntry>;
|
|
186
205
|
|
|
206
|
+
/** Each part's top-level keys, from the ONE wire table. */
|
|
207
|
+
export const PLAN_PART_KEYS: Readonly<Record<PlanPart, ReadonlyArray<string>>> = Object.fromEntries(
|
|
208
|
+
PLAN_PARTS.map(part => [part, Object.entries(WIRE_KEYS.plan).filter(([, spec]) => spec.part === part).map(([k]) => k)]),
|
|
209
|
+
) as Record<PlanPart, string[]>;
|
|
210
|
+
|
|
211
|
+
/** The keys of a shape entry, but these. */
|
|
212
|
+
const shapeWithout = (entry: ShapeEntry, keys: ReadonlyArray<string>): ShapeEntry =>
|
|
213
|
+
Object.fromEntries(Object.entries(entry).filter(([k]) => !keys.includes(k.replace(/\?$/, ''))));
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The part a plan path is the answer of (`contentTypes[1].seed.title` → structure, `pages[0].sections[2]` → pages). A
|
|
217
|
+
* path no part writes — the root (the composer), or a key the generation decides — is the LAST part's: it is written
|
|
218
|
+
* knowing everything before it.
|
|
219
|
+
*/
|
|
220
|
+
export function planPartOfPath(path: string): PlanPart {
|
|
221
|
+
const top = path.split(/[.[]/, 1)[0];
|
|
222
|
+
return PLAN_PARTS.find(part => PLAN_PART_KEYS[part].includes(top)) ?? PLAN_PARTS[PLAN_PARTS.length - 1];
|
|
223
|
+
}
|
|
224
|
+
|
|
187
225
|
/** Plan keys neither carried by a wire key nor derived — always empty (a spec and the builder hold it). */
|
|
188
226
|
export function planWireGaps(): string[] {
|
|
189
227
|
const carried = new Set(Object.values(WIRE_KEYS).flatMap(keys => Object.values(keys).flatMap(spec => spec.refs)));
|
|
@@ -220,13 +258,32 @@ export function wireFieldJsonSchema(): JsonSchema {
|
|
|
220
258
|
return shaped(PlanWireEntry.Field, { name: jsonText(), type: jsonText(), controller: jsonText(), required: jsonBoolean(), note: jsonText() });
|
|
221
259
|
}
|
|
222
260
|
|
|
223
|
-
|
|
224
|
-
|
|
261
|
+
/** What an object of the wire travels without when its children are written as their own entries (a plan edit's). */
|
|
262
|
+
export interface WireOptions {
|
|
263
|
+
/** false: a content type without its `fields`, a page without its `sections` (each added by an edit after it). */
|
|
264
|
+
children?: boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The keys an entry leaves out under `options`. */
|
|
268
|
+
const CHILD_KEYS: Readonly<Partial<Record<`${PlanWireEntry}`, string>>> = { contentType: 'fields', page: 'sections' };
|
|
269
|
+
const childless = (entry: `${PlanWireEntry}`, options: WireOptions) => (options.children === false && CHILD_KEYS[entry] ? [CHILD_KEYS[entry]!] : []);
|
|
270
|
+
|
|
271
|
+
/** The shape an entry is written in, under `options`. */
|
|
272
|
+
function wireEntryShape(entry: `${PlanWireEntry}`, options: WireOptions = {}): ShapeEntry {
|
|
273
|
+
return shapeWithout(PLAN_WIRE_SHAPE[entry], childless(entry, options));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const shapedAs = (entry: `${PlanWireEntry}`, options: WireOptions, properties: Record<string, JsonSchema>) => {
|
|
277
|
+
const left = childless(entry, options);
|
|
278
|
+
return jsonShapedObject(wireEntryShape(entry, options), Object.fromEntries(Object.entries(properties).filter(([k]) => !left.includes(k))), `wire ${entry}`);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export function wireContentTypeJsonSchema(options: WireOptions = {}): JsonSchema {
|
|
225
282
|
const value = shaped(PlanWireEntry.SeedValue, { field: jsonText(), value: jsonAnyOf([jsonText(), jsonListOf(jsonText())]) });
|
|
226
|
-
return
|
|
283
|
+
return shapedAs(PlanWireEntry.ContentType, options, {
|
|
227
284
|
name: jsonText(),
|
|
228
285
|
label: jsonText(),
|
|
229
|
-
roles: shaped(PlanWireEntry.
|
|
286
|
+
roles: jsonListOf(shaped(PlanWireEntry.RoleField, { role: jsonOneOf(ROLE_KEYS), field: jsonText() })),
|
|
230
287
|
fields: jsonListOf(wireFieldJsonSchema()),
|
|
231
288
|
seed: shaped(PlanWireEntry.Seed, { count: jsonInteger(), sample: jsonBoolean(), isPublished: jsonBoolean(), values: jsonListOf(value) }),
|
|
232
289
|
});
|
|
@@ -260,8 +317,8 @@ export function wireSectionJsonSchema(): JsonSchema {
|
|
|
260
317
|
});
|
|
261
318
|
}
|
|
262
319
|
|
|
263
|
-
export function wirePageJsonSchema(): JsonSchema {
|
|
264
|
-
return
|
|
320
|
+
export function wirePageJsonSchema(options: WireOptions = {}): JsonSchema {
|
|
321
|
+
return shapedAs(PlanWireEntry.Page, options, {
|
|
265
322
|
key: jsonText(),
|
|
266
323
|
name: jsonText(),
|
|
267
324
|
route: jsonText(),
|
|
@@ -275,20 +332,32 @@ export function wirePageJsonSchema(): JsonSchema {
|
|
|
275
332
|
});
|
|
276
333
|
}
|
|
277
334
|
|
|
278
|
-
|
|
279
|
-
|
|
335
|
+
const PLAN_TOP_SCHEMAS: Readonly<Record<string, () => JsonSchema>> = {
|
|
336
|
+
business: () => shaped(PlanWireEntry.Business, { name: jsonText(), tagline: jsonText() }),
|
|
337
|
+
contentTypes: () => jsonListOf(wireContentTypeJsonSchema()),
|
|
338
|
+
dataSources: () => jsonListOf(wireDataSourceJsonSchema()),
|
|
339
|
+
pages: () => jsonListOf(wirePageJsonSchema()),
|
|
340
|
+
globals: () => shaped(PlanWireEntry.Globals, {
|
|
341
|
+
[GlobalSlot.Header]: jsonOneOf(globalIds(PatternGlobalPosition.Before)),
|
|
342
|
+
[GlobalSlot.Footer]: jsonOneOf(globalIds(PatternGlobalPosition.After)),
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** The plan's top-level shape for one part (its keys only) — what that part's request shows and is built from. */
|
|
347
|
+
export function planWirePartShape(part: PlanPart): ShapeEntry {
|
|
348
|
+
return Object.fromEntries(Object.entries(PLAN_WIRE_SHAPE.plan).filter(([k]) => PLAN_PART_KEYS[part].includes(k)));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The format a model writes one part of a plan in — one request each (vectoriox/iox-cms#47). The same for every studio
|
|
353
|
+
* and run: nothing in it is pinned, so the provider compiles each part's grammar once.
|
|
354
|
+
*/
|
|
355
|
+
export function planWirePartJsonSchema(part: PlanPart): JsonSchema {
|
|
280
356
|
const gaps = planWireGaps();
|
|
281
357
|
if (gaps.length) throw new PlanWireShapeError(`the plan wire neither carries nor derives ${gaps.join(', ')}`);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
dataSources: jsonListOf(wireDataSourceJsonSchema()),
|
|
286
|
-
pages: jsonListOf(wirePageJsonSchema()),
|
|
287
|
-
globals: shaped(PlanWireEntry.Globals, {
|
|
288
|
-
[GlobalSlot.Header]: jsonOneOf(globalIds(PatternGlobalPosition.Before)),
|
|
289
|
-
[GlobalSlot.Footer]: jsonOneOf(globalIds(PatternGlobalPosition.After)),
|
|
290
|
-
}),
|
|
291
|
-
});
|
|
358
|
+
const missing = Object.keys(WIRE_KEYS.plan).filter(k => !PLAN_TOP_SCHEMAS[k] || !WIRE_KEYS.plan[k].part);
|
|
359
|
+
if (missing.length) throw new PlanWireShapeError(`the plan wire's ${missing.join(', ')} has no part or no format`);
|
|
360
|
+
return jsonShapedObject(planWirePartShape(part), Object.fromEntries(PLAN_PART_KEYS[part].map(k => [k, PLAN_TOP_SCHEMAS[k]()])), `wire plan ${part}`);
|
|
292
361
|
}
|
|
293
362
|
|
|
294
363
|
// ── The wire → the plan ────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -329,10 +398,11 @@ export function expandWireField(wire: unknown): ContentFieldPlan | unknown {
|
|
|
329
398
|
return { ...rest, ...optional('required', required, v => v === undefined || v === false), ...optional('note', note) };
|
|
330
399
|
}
|
|
331
400
|
|
|
332
|
-
export function expandWireContentType(wire: unknown): ContentTypePlan | unknown {
|
|
401
|
+
export function expandWireContentType(wire: unknown, options: WireOptions = {}): ContentTypePlan | unknown {
|
|
333
402
|
if (!isPlainObject(wire)) return wire;
|
|
334
|
-
const {
|
|
335
|
-
const
|
|
403
|
+
const written: PlainObject = options.children === false ? { fields: [], ...wire } : wire;
|
|
404
|
+
const { roles, fields, seed, ...rest } = written;
|
|
405
|
+
const out: PlainObject = { ...rest, roles: listToMap(roles, 'role', 'field'), fields: each(fields, expandWireField) };
|
|
336
406
|
if (!isPlainObject(seed)) return seed === undefined ? out : { ...out, seed };
|
|
337
407
|
const { values, ...fixed } = seed;
|
|
338
408
|
if (fixed['count'] === 0 && emptyList(values)) return out;
|
|
@@ -365,9 +435,10 @@ export function expandWireSection(wire: unknown): PageSectionPlan | unknown {
|
|
|
365
435
|
};
|
|
366
436
|
}
|
|
367
437
|
|
|
368
|
-
export function expandWirePage(wire: unknown): PagePlan | unknown {
|
|
438
|
+
export function expandWirePage(wire: unknown, options: WireOptions = {}): PagePlan | unknown {
|
|
369
439
|
if (!isPlainObject(wire)) return wire;
|
|
370
|
-
const
|
|
440
|
+
const written: PlainObject = options.children === false ? { sections: [], ...wire } : wire;
|
|
441
|
+
const { archetype, need, template, routeSource, seo, sections, ...rest } = written;
|
|
371
442
|
const words = isPlainObject(seo) ? { ...optional('title', seo['title']), ...optional('description', seo['description']) } : seo;
|
|
372
443
|
return {
|
|
373
444
|
...rest,
|
|
@@ -387,12 +458,20 @@ function expandWireBusiness(wire: unknown, facts: PlanWireContext['facts']): unk
|
|
|
387
458
|
return { ...rest, ...optional('tagline', tagline), ...(facts && Object.keys(facts).length ? { facts: { ...facts } } : {}) };
|
|
388
459
|
}
|
|
389
460
|
|
|
461
|
+
/** A plan on the wire, by part — what each request answered. A part not written yet is absent. */
|
|
462
|
+
export type PlanWireParts = Partial<Record<PlanPart, unknown>>;
|
|
463
|
+
|
|
390
464
|
/**
|
|
391
|
-
* A model's plan (the wire) + what the generation decides (the look, the run, the language, the owner's
|
|
392
|
-
* Site Plan. Pure. Translates, never repairs: whatever
|
|
465
|
+
* A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
|
|
466
|
+
* facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
|
|
467
|
+
* `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
|
|
468
|
+
* (the validator then names them, at their own paths).
|
|
393
469
|
*/
|
|
394
|
-
export function expandPlanWire(
|
|
395
|
-
|
|
470
|
+
export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext): SitePlan | unknown {
|
|
471
|
+
const written = PLAN_PARTS.filter(part => parts[part] !== undefined).map(part => parts[part]);
|
|
472
|
+
const odd = written.find(value => !isPlainObject(value));
|
|
473
|
+
if (odd !== undefined) return odd;
|
|
474
|
+
const wire: PlainObject = Object.assign({}, ...(written as PlainObject[]));
|
|
396
475
|
const { business, contentTypes, dataSources, pages, ...rest } = wire;
|
|
397
476
|
return {
|
|
398
477
|
...rest,
|
|
@@ -413,11 +492,12 @@ export function fieldToWire(field: ContentFieldPlan): PlainObject {
|
|
|
413
492
|
return { name: field.name, type: field.type, controller: field.controller, required: field.required ?? false, note: field.note ?? NONE };
|
|
414
493
|
}
|
|
415
494
|
|
|
416
|
-
export function contentTypeToWire(type: ContentTypePlan): PlainObject {
|
|
417
|
-
const
|
|
495
|
+
export function contentTypeToWire(type: ContentTypePlan, options: WireOptions = {}): PlainObject {
|
|
496
|
+
const played = type.roles as Readonly<Record<string, string | undefined>>;
|
|
497
|
+
const roles = ROLE_KEYS.filter(role => played[role]).map(role => ({ role, field: played[role] }));
|
|
418
498
|
const { count = 0, sample = false, isPublished = false, ...values } = type.seed ?? {};
|
|
419
499
|
return {
|
|
420
|
-
name: type.name, label: type.label, roles, fields: type.fields.map(fieldToWire),
|
|
500
|
+
name: type.name, label: type.label, roles, ...(options.children === false ? {} : { fields: type.fields.map(fieldToWire) }),
|
|
421
501
|
seed: { count, sample, isPublished, values: mapToList(values, 'field', 'value') },
|
|
422
502
|
};
|
|
423
503
|
}
|
|
@@ -438,21 +518,29 @@ export function sectionToWire(section: PageSectionPlan): PlainObject {
|
|
|
438
518
|
};
|
|
439
519
|
}
|
|
440
520
|
|
|
441
|
-
export function pageToWire(page: PagePlan): PlainObject {
|
|
521
|
+
export function pageToWire(page: PagePlan, options: WireOptions = {}): PlainObject {
|
|
442
522
|
return {
|
|
443
523
|
key: page.key, name: page.name, route: page.route, inNav: page.inNav ?? false, archetype: page.archetype ?? NONE, need: page.need ?? NONE,
|
|
444
524
|
template: page.template ?? NONE, routeSource: page.routeSource ?? NONE,
|
|
445
|
-
seo: { title: page.seo?.title ?? NONE, description: page.seo?.description ?? NONE },
|
|
525
|
+
seo: { title: page.seo?.title ?? NONE, description: page.seo?.description ?? NONE },
|
|
526
|
+
...(options.children === false ? {} : { sections: page.sections.map(sectionToWire) }),
|
|
446
527
|
};
|
|
447
528
|
}
|
|
448
529
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
530
|
+
const PLAN_TO_WIRE: Readonly<Record<string, (plan: SitePlan) => unknown>> = {
|
|
531
|
+
business: plan => ({ name: plan.business.name, tagline: plan.business.tagline ?? NONE }),
|
|
532
|
+
contentTypes: plan => plan.contentTypes.map(type => contentTypeToWire(type)),
|
|
533
|
+
dataSources: plan => plan.dataSources.map(dataSourceToWire),
|
|
534
|
+
pages: plan => plan.pages.map(page => pageToWire(page)),
|
|
535
|
+
globals: plan => ({ ...plan.globals }),
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/** One part of the plan on the wire: what that part's request would have answered. */
|
|
539
|
+
export function planPartToWire(plan: SitePlan, part: PlanPart): PlainObject {
|
|
540
|
+
return Object.fromEntries(PLAN_PART_KEYS[part].map(k => [k, PLAN_TO_WIRE[k](plan)]));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** The plan → the wire, by part: what a model would have written (the change prompt shows it; fakes and evals write it). */
|
|
544
|
+
export function planToWire(plan: SitePlan): Record<PlanPart, PlainObject> {
|
|
545
|
+
return Object.fromEntries(PLAN_PARTS.map(part => [part, planPartToWire(plan, part)])) as Record<PlanPart, PlainObject>;
|
|
458
546
|
}
|