@vectoriox/iox-ui 5.60.0 → 5.62.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/fesm2022/vectoriox-iox-ui-generation-core.mjs +846 -296
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/generation-core/index.d.ts +221 -55
- package/package.json +1 -1
- package/src/generation-core/ai-run.ts +40 -3
- package/src/generation-core/plan-edits.ts +251 -52
- package/src/generation-core/plan-wire.ts +544 -0
- package/src/generation-core/public-api.ts +2 -1
- package/src/generation-core/strict-schema.ts +121 -0
- package/src/generation-core/site-plan-schema.ts +0 -252
|
@@ -10818,23 +10818,23 @@ const RESERVED_PREFIX = '$';
|
|
|
10818
10818
|
const ID_FIELD = '_id';
|
|
10819
10819
|
/** The documented plan shape (contract `site.planShape`): its key lists are READ (`shape-check`), so a key
|
|
10820
10820
|
* the plan documents is a key the gate validates — and an undocumented one is refused. */
|
|
10821
|
-
const SHAPE
|
|
10822
|
-
const PLAN_KEYS = shapeKeys(SHAPE
|
|
10823
|
-
const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE
|
|
10824
|
-
const AI_SOURCE_KEYS = shapeKeys(SHAPE
|
|
10825
|
-
const BUSINESS_KEYS = shapeKeys(SHAPE
|
|
10826
|
-
const DESIGN_REQUIRED = requiredShapeKeys(SHAPE
|
|
10827
|
-
const DESIGN_KEYS = shapeKeys(SHAPE
|
|
10828
|
-
const CONTENT_TYPE_KEYS = shapeKeys(SHAPE
|
|
10829
|
-
const ROLE_KEYS = shapeKeys(SHAPE
|
|
10830
|
-
const PAGE_SEO_KEYS = shapeKeys(SHAPE
|
|
10821
|
+
const SHAPE = SPEC_SITE_PLAN_SHAPE;
|
|
10822
|
+
const PLAN_KEYS = shapeKeys(SHAPE['plan']);
|
|
10823
|
+
const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE['sourceTemplate']);
|
|
10824
|
+
const AI_SOURCE_KEYS = shapeKeys(SHAPE['sourceAi']);
|
|
10825
|
+
const BUSINESS_KEYS = shapeKeys(SHAPE['business']);
|
|
10826
|
+
const DESIGN_REQUIRED = requiredShapeKeys(SHAPE['design']);
|
|
10827
|
+
const DESIGN_KEYS = shapeKeys(SHAPE['design']);
|
|
10828
|
+
const CONTENT_TYPE_KEYS = shapeKeys(SHAPE['contentType']);
|
|
10829
|
+
const ROLE_KEYS$1 = shapeKeys(SHAPE['roles']);
|
|
10830
|
+
const PAGE_SEO_KEYS = shapeKeys(SHAPE['pageSeo']);
|
|
10831
10831
|
const ARRAY_TYPE = 'array';
|
|
10832
|
-
const FIELD_KEYS = shapeKeys(SHAPE
|
|
10833
|
-
const SEED_FIXED_KEYS = shapeKeys(SHAPE
|
|
10834
|
-
const CMS_SOURCE_KEYS = shapeKeys(SHAPE
|
|
10835
|
-
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE
|
|
10836
|
-
const PAGE_KEYS = shapeKeys(SHAPE
|
|
10837
|
-
const SECTION_KEYS = shapeKeys(SHAPE
|
|
10832
|
+
const FIELD_KEYS = shapeKeys(SHAPE['field']);
|
|
10833
|
+
const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
10834
|
+
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
10835
|
+
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
|
|
10836
|
+
const PAGE_KEYS = shapeKeys(SHAPE['page']);
|
|
10837
|
+
const SECTION_KEYS = shapeKeys(SHAPE['section']);
|
|
10838
10838
|
const GLOBAL_POSITION = {
|
|
10839
10839
|
[GlobalSlot.Header]: PatternGlobalPosition.Before,
|
|
10840
10840
|
[GlobalSlot.Footer]: PatternGlobalPosition.After,
|
|
@@ -10984,8 +10984,8 @@ function validateRoles(c, roles, path, typeName, fields, fieldTypes) {
|
|
|
10984
10984
|
}
|
|
10985
10985
|
const out = {};
|
|
10986
10986
|
for (const [role, field] of Object.entries(roles)) {
|
|
10987
|
-
if (!ROLE_KEYS.includes(role)) {
|
|
10988
|
-
c.add(SitePlanIssueCode.UnknownRole, `${path}.${role}`, `"${role}" is not a role (${ROLE_KEYS.join(', ')})`);
|
|
10987
|
+
if (!ROLE_KEYS$1.includes(role)) {
|
|
10988
|
+
c.add(SitePlanIssueCode.UnknownRole, `${path}.${role}`, `"${role}" is not a role (${ROLE_KEYS$1.join(', ')})`);
|
|
10989
10989
|
continue;
|
|
10990
10990
|
}
|
|
10991
10991
|
if (typeof field !== 'string' || !fields.has(field)) {
|
|
@@ -11348,13 +11348,13 @@ const ENTRANCE_TRIGGER = 'viewportEnter';
|
|
|
11348
11348
|
const ENTRANCE_EASING = 'ease-out';
|
|
11349
11349
|
const LEVELS = SPEC_AXES.animations;
|
|
11350
11350
|
const FAMILIES = SPEC_AXES.animationFamilies;
|
|
11351
|
-
const NOTHING
|
|
11351
|
+
const NOTHING = { content: null, parts: {} };
|
|
11352
11352
|
/** What a section of `family` moves at `level` — nothing for an unknown level or family. */
|
|
11353
11353
|
function sectionEntrances(level, family) {
|
|
11354
11354
|
const lv = level ? LEVELS[level] : undefined;
|
|
11355
11355
|
const allow = FAMILIES[family] ?? [];
|
|
11356
11356
|
if (!lv || !allow.length)
|
|
11357
|
-
return NOTHING
|
|
11357
|
+
return NOTHING;
|
|
11358
11358
|
if (lv.parts.content && allow.includes(EntrancePart.Content))
|
|
11359
11359
|
return { content: lv.parts.content, parts: {} };
|
|
11360
11360
|
const parts = {};
|
|
@@ -11484,7 +11484,7 @@ const TEXT$1 = 'Text';
|
|
|
11484
11484
|
const HEADING = 'Heading';
|
|
11485
11485
|
const NAV_STYLE = 'nav';
|
|
11486
11486
|
const DISPLAY = 'display';
|
|
11487
|
-
const NONE = 'none';
|
|
11487
|
+
const NONE$1 = 'none';
|
|
11488
11488
|
const CLICK = 'click';
|
|
11489
11489
|
const HOME_ROUTE = '/';
|
|
11490
11490
|
const COMPOSED$1 = { releaseLayoutDefaults: true };
|
|
@@ -11557,7 +11557,7 @@ function navShapeOf(globals) {
|
|
|
11557
11557
|
return NavShape.Bar;
|
|
11558
11558
|
return drawer ? NavShape.Drawer : null;
|
|
11559
11559
|
}
|
|
11560
|
-
const hiddenAt = (n) => Object.entries(n.responsive ?? {}).find(([, layer]) => layer?.styleProps?.[DISPLAY] === NONE)?.[0] ?? null;
|
|
11560
|
+
const hiddenAt = (n) => Object.entries(n.responsive ?? {}).find(([, layer]) => layer?.styleProps?.[DISPLAY] === NONE$1)?.[0] ?? null;
|
|
11561
11561
|
/** The breakpoint the phone shape takes over at, read off the header's links row (default Mobile). */
|
|
11562
11562
|
function navShapeBreakpoint(globals) {
|
|
11563
11563
|
const links = find(globals, NavRole.Links);
|
|
@@ -11649,12 +11649,12 @@ function clickTo(kit, suffix, type, target) {
|
|
|
11649
11649
|
}
|
|
11650
11650
|
/** Show at the phone width, hidden above it (base `display: none`, the breakpoint's own `display`). */
|
|
11651
11651
|
function phoneOnly(node, at, display) {
|
|
11652
|
-
node.styleProps[DISPLAY] = NONE;
|
|
11652
|
+
node.styleProps[DISPLAY] = NONE$1;
|
|
11653
11653
|
node.responsive = { ...(node.responsive ?? {}), [at]: { ...(node.responsive?.[at] ?? {}), styleProps: { ...(node.responsive?.[at]?.styleProps ?? {}), [DISPLAY]: display } } };
|
|
11654
11654
|
}
|
|
11655
11655
|
/** Hidden at the phone width: the header's links row. */
|
|
11656
11656
|
function hideAt(node, at) {
|
|
11657
|
-
node.responsive = { ...(node.responsive ?? {}), [at]: { ...(node.responsive?.[at] ?? {}), styleProps: { ...(node.responsive?.[at]?.styleProps ?? {}), [DISPLAY]: NONE } } };
|
|
11657
|
+
node.responsive = { ...(node.responsive ?? {}), [at]: { ...(node.responsive?.[at] ?? {}), styleProps: { ...(node.responsive?.[at]?.styleProps ?? {}), [DISPLAY]: NONE$1 } } };
|
|
11658
11658
|
}
|
|
11659
11659
|
/** Move every `display` override the shape owns to the new breakpoint. */
|
|
11660
11660
|
function moveDisplayOverride(node, to) {
|
|
@@ -13380,6 +13380,471 @@ function buildStepFacts(plan, actions = {}) {
|
|
|
13380
13380
|
};
|
|
13381
13381
|
}
|
|
13382
13382
|
|
|
13383
|
+
/**
|
|
13384
|
+
* The plan WIRE (AI plan M1-3, vectoriox/iox-cms#45, #47): what a model writes when it plans a site — a compact format
|
|
13385
|
+
* inside the Claude API's structured-output limits (≤ 24 optional parameters and ≤ 16 unions per request, and a compiled
|
|
13386
|
+
* grammar under the size Claude refuses — measured as ≤ 60 properties; `strict-schema.ts`). The Site Plan itself
|
|
13387
|
+
* (`SPEC_SITE_PLAN_SHAPE`) has 213 optional keys once every pattern is a branch, so the model never writes it: it writes
|
|
13388
|
+
* this, and ONE pure `expandPlanWire` turns it into the Site Plan that `validateSitePlan` and the stage's gate judge
|
|
13389
|
+
* exactly as before.
|
|
13390
|
+
*
|
|
13391
|
+
* - **Written in two PARTS, one request each** (#47 — the whole plan in one format was 64 properties): the **structure** (the business,
|
|
13392
|
+
* the content types, the data sources) and then the **pages** (the pages, the header and footer), written knowing the
|
|
13393
|
+
* structure. Every top-level key belongs to exactly one part (`part` below); `expandPlanWire` puts the parts together.
|
|
13394
|
+
*
|
|
13395
|
+
* - **Every key required.** A plan key that may be absent is written with its stated "none" — `''`, `0`, `false`, `[]`.
|
|
13396
|
+
* - **Closed lists as enums** (patterns, page types, needs, slots, parts, forms, entrances, header/footer), from the
|
|
13397
|
+
* contract — never per-pattern branches: the gate refuses a slot or a part the chosen pattern does not have, with its path.
|
|
13398
|
+
* - **Maps as lists:** slots `[{ slot, text }]`, links `[{ slot, route }]`, seed values `[{ field, value }]`, roles `[{ role, field }]`.
|
|
13399
|
+
* - **One data source object** for both kinds; `type` says which keys are read (the other kind's hold their none).
|
|
13400
|
+
* - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
|
|
13401
|
+
* the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
|
|
13402
|
+
* So the format is the same for every studio and every run — one compiled grammar, cached by the provider.
|
|
13403
|
+
*
|
|
13404
|
+
* ONE source: every wire key names the plan keys it carries (`refs`) and is described in the plan table's own words, and
|
|
13405
|
+
* every plan key is either carried or derived (`planWireGaps()`); a key the plan shape gains breaks the builder until it
|
|
13406
|
+
* is placed. The expanders translate, never repair: a value the model got wrong stays wrong, for the gate to refuse.
|
|
13407
|
+
*/
|
|
13408
|
+
/** The plan's parts, in the order they are written: the pages bind to the structure's sources and types. */
|
|
13409
|
+
var PlanPart;
|
|
13410
|
+
(function (PlanPart) {
|
|
13411
|
+
PlanPart["Structure"] = "structure";
|
|
13412
|
+
PlanPart["Pages"] = "pages";
|
|
13413
|
+
})(PlanPart || (PlanPart = {}));
|
|
13414
|
+
const PLAN_PARTS = [PlanPart.Structure, PlanPart.Pages];
|
|
13415
|
+
/** Each part's format name (a request names the format it asks for). */
|
|
13416
|
+
const PLAN_WIRE_PART_SCHEMA_NAMES = {
|
|
13417
|
+
[PlanPart.Structure]: 'site_plan_structure',
|
|
13418
|
+
[PlanPart.Pages]: 'site_plan_pages',
|
|
13419
|
+
};
|
|
13420
|
+
var PlanWireEntry;
|
|
13421
|
+
(function (PlanWireEntry) {
|
|
13422
|
+
PlanWireEntry["Plan"] = "plan";
|
|
13423
|
+
PlanWireEntry["Business"] = "business";
|
|
13424
|
+
PlanWireEntry["ContentType"] = "contentType";
|
|
13425
|
+
PlanWireEntry["RoleField"] = "roleField";
|
|
13426
|
+
PlanWireEntry["Field"] = "field";
|
|
13427
|
+
PlanWireEntry["Seed"] = "seed";
|
|
13428
|
+
PlanWireEntry["SeedValue"] = "seedValue";
|
|
13429
|
+
PlanWireEntry["DataSource"] = "dataSource";
|
|
13430
|
+
PlanWireEntry["Page"] = "page";
|
|
13431
|
+
PlanWireEntry["PageSeo"] = "pageSeo";
|
|
13432
|
+
PlanWireEntry["Section"] = "section";
|
|
13433
|
+
PlanWireEntry["SlotText"] = "slotText";
|
|
13434
|
+
PlanWireEntry["SlotLink"] = "slotLink";
|
|
13435
|
+
PlanWireEntry["Globals"] = "globals";
|
|
13436
|
+
})(PlanWireEntry || (PlanWireEntry = {}));
|
|
13437
|
+
const PLAN_SHAPE = SPEC_SITE_PLAN_SHAPE;
|
|
13438
|
+
const key = (ref, none) => ({ refs: [ref], ...(none !== undefined ? { none } : {}) });
|
|
13439
|
+
const inPart = (part, spec) => ({ ...spec, part });
|
|
13440
|
+
/** The roles a content type's fields play (`roles` of the plan shape), in the plan shape's order. */
|
|
13441
|
+
const ROLE_KEYS = shapeKeys(PLAN_SHAPE['roles']);
|
|
13442
|
+
const roleWords = ROLE_KEYS.map(role => `${role}: ${describeShapeKey(PLAN_SHAPE['roles'], role).replace(/^string — /, '')}`).join('; ');
|
|
13443
|
+
const WIRE_KEYS = {
|
|
13444
|
+
plan: {
|
|
13445
|
+
business: inPart(PlanPart.Structure, key('plan.business')),
|
|
13446
|
+
contentTypes: inPart(PlanPart.Structure, key('plan.contentTypes')),
|
|
13447
|
+
dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text: 'dataSource[] — merged by alias into the org library' }),
|
|
13448
|
+
pages: inPart(PlanPart.Pages, key('plan.pages')),
|
|
13449
|
+
globals: inPart(PlanPart.Pages, key('plan.globals')),
|
|
13450
|
+
},
|
|
13451
|
+
business: {
|
|
13452
|
+
name: key('business.name'),
|
|
13453
|
+
tagline: key('business.tagline', "''"),
|
|
13454
|
+
},
|
|
13455
|
+
contentType: {
|
|
13456
|
+
name: key('contentType.name'),
|
|
13457
|
+
label: key('contentType.label'),
|
|
13458
|
+
roles: { refs: ['contentType.roles'], text: `roleField[] — ${describeShapeKey(PLAN_SHAPE['contentType'], 'roles').replace(/^roles — /, '')}; a role no field plays is left out` },
|
|
13459
|
+
fields: key('contentType.fields'),
|
|
13460
|
+
seed: { refs: ['contentType.seed'], text: 'seed — the sample items', none: 'count 0 and no values' },
|
|
13461
|
+
},
|
|
13462
|
+
roleField: {
|
|
13463
|
+
role: { refs: ROLE_KEYS.map(role => `roles.${role}`), text: `${ROLE_KEYS.map(r => `'${r}'`).join(' | ')} — ${roleWords}` },
|
|
13464
|
+
field: { refs: ['roles.title'], text: 'string — the field of the type that plays it' },
|
|
13465
|
+
},
|
|
13466
|
+
field: {
|
|
13467
|
+
name: key('field.name'),
|
|
13468
|
+
type: key('field.type'),
|
|
13469
|
+
controller: key('field.controller'),
|
|
13470
|
+
required: { refs: ['field.required'], text: 'boolean — the field must be filled', none: 'false' },
|
|
13471
|
+
note: key('field.note', "''"),
|
|
13472
|
+
},
|
|
13473
|
+
seed: {
|
|
13474
|
+
count: key('seed.count'),
|
|
13475
|
+
sample: key('seed.sample'),
|
|
13476
|
+
isPublished: key('seed.isPublished'),
|
|
13477
|
+
values: { refs: ['seed.[field]'], text: "seedValue[] — one per field: the samples' value for it" },
|
|
13478
|
+
},
|
|
13479
|
+
seedValue: {
|
|
13480
|
+
field: { refs: ['seed.[field]'], text: 'string — a field of the type' },
|
|
13481
|
+
value: { refs: ['seed.[field]'], text: `string | string[] — ${describeShapeKey(PLAN_SHAPE['seed'], '[field]')} (a list for an array field)` },
|
|
13482
|
+
},
|
|
13483
|
+
dataSource: {
|
|
13484
|
+
alias: { refs: ['cmsDataSource.alias', 'relativeDataSource.alias'] },
|
|
13485
|
+
type: { refs: ['cmsDataSource.type', 'relativeDataSource.type'], text: "'cms' | 'relative' — which of the keys below are read" },
|
|
13486
|
+
mode: key('cmsDataSource.mode', "'' (relative)"),
|
|
13487
|
+
contentType: key('cmsDataSource.contentType', "'' (relative)"),
|
|
13488
|
+
limit: key('cmsDataSource.limit', '0 (all, or relative)'),
|
|
13489
|
+
filterField: { refs: ['cmsDataSource.filterByRouteParam'], text: "string — single only: the field that holds the route param's value (e.g. slug)", none: "''" },
|
|
13490
|
+
routeParam: { refs: ['cmsDataSource.filterByRouteParam'], text: 'string — single only: the route param that picks the item (the :param of its page route)', none: "''" },
|
|
13491
|
+
of: key('relativeDataSource.of', "'' (cms)"),
|
|
13492
|
+
op: key('relativeDataSource.op', "'' (cms)"),
|
|
13493
|
+
within: key('relativeDataSource.within', "'' (cms)"),
|
|
13494
|
+
},
|
|
13495
|
+
page: {
|
|
13496
|
+
key: key('page.key'),
|
|
13497
|
+
name: key('page.name'),
|
|
13498
|
+
route: { refs: ['page.route'], text: `${describeShapeKey(PLAN_SHAPE['page'], 'route')}; the page at "/" is the home page` },
|
|
13499
|
+
inNav: key('page.inNav', 'false'),
|
|
13500
|
+
archetype: key('page.archetype', "''"),
|
|
13501
|
+
need: key('page.need', "''"),
|
|
13502
|
+
template: key('page.template', "''"),
|
|
13503
|
+
routeSource: key('page.routeSource', "''"),
|
|
13504
|
+
seo: key('page.seo'),
|
|
13505
|
+
sections: key('page.sections'),
|
|
13506
|
+
},
|
|
13507
|
+
pageSeo: {
|
|
13508
|
+
title: key('pageSeo.title', "''"),
|
|
13509
|
+
description: key('pageSeo.description', "''"),
|
|
13510
|
+
},
|
|
13511
|
+
section: {
|
|
13512
|
+
pattern: key('section.pattern'),
|
|
13513
|
+
slots: { refs: ['section.slots'], text: "slotText[] — the pattern's declared slots it fills, each within its budget (site.patterns … max × site.copyLengths)", none: '[]' },
|
|
13514
|
+
links: { refs: ['section.links'], text: 'slotLink[] — re-points a linked slot (a button, a link) to another page route of the plan', none: '[]' },
|
|
13515
|
+
binding: key('section.binding', "''"),
|
|
13516
|
+
without: key('section.without', '[]'),
|
|
13517
|
+
form: key('section.form', "''"),
|
|
13518
|
+
enter: key('section.enter', "'' (the look's)"),
|
|
13519
|
+
},
|
|
13520
|
+
slotText: {
|
|
13521
|
+
slot: { refs: ['section.slots'], text: "string — one of the pattern's slots" },
|
|
13522
|
+
text: { refs: ['section.slots'], text: "string — the words, in the owner's language and voice" },
|
|
13523
|
+
},
|
|
13524
|
+
slotLink: {
|
|
13525
|
+
slot: { refs: ['section.links'], text: "string — one of the pattern's linked slots" },
|
|
13526
|
+
route: { refs: ['section.links'], text: 'string — a page route of the plan (no :param)' },
|
|
13527
|
+
},
|
|
13528
|
+
globals: {
|
|
13529
|
+
header: key('globals.header'),
|
|
13530
|
+
footer: key('globals.footer'),
|
|
13531
|
+
},
|
|
13532
|
+
};
|
|
13533
|
+
/** The plan keys the model does not write, and where the expander takes each from. */
|
|
13534
|
+
const PLAN_WIRE_DERIVED = {
|
|
13535
|
+
'plan.version': `always '${SITE_PLAN_VERSION}'`,
|
|
13536
|
+
'plan.source': 'this run (the context)',
|
|
13537
|
+
'plan.locale': "the reading's language (the context)",
|
|
13538
|
+
'plan.design': "the owner's pick (the context)",
|
|
13539
|
+
'business.facts': "the owner's own facts, verbatim (the context)",
|
|
13540
|
+
'page.isHome': 'the page at "/"',
|
|
13541
|
+
'page.required': "a template page's (the wizard's Pages step), never the AI's",
|
|
13542
|
+
sourceTemplate: 'the AI never writes a template plan',
|
|
13543
|
+
sourceAi: 'this run (the context)',
|
|
13544
|
+
design: "the owner's pick (the context)",
|
|
13545
|
+
};
|
|
13546
|
+
const wireText = (spec) => {
|
|
13547
|
+
const [entry, planKey] = spec.refs[0].split('.');
|
|
13548
|
+
const base = spec.text ?? describeShapeKey(PLAN_SHAPE[entry], planKey);
|
|
13549
|
+
return spec.none === undefined ? base : `${base} — ${spec.none} = none`;
|
|
13550
|
+
};
|
|
13551
|
+
/** The wire's shape table, in the plan table's words — what the model reads, and what the format is built from. */
|
|
13552
|
+
const PLAN_WIRE_SHAPE = Object.fromEntries(Object.entries(WIRE_KEYS).map(([entry, keys]) => [entry, Object.fromEntries(Object.entries(keys).map(([k, spec]) => [k, wireText(spec)]))]));
|
|
13553
|
+
/** Each part's top-level keys, from the ONE wire table. */
|
|
13554
|
+
const PLAN_PART_KEYS = Object.fromEntries(PLAN_PARTS.map(part => [part, Object.entries(WIRE_KEYS.plan).filter(([, spec]) => spec.part === part).map(([k]) => k)]));
|
|
13555
|
+
/** The keys of a shape entry, but these. */
|
|
13556
|
+
const shapeWithout = (entry, keys) => Object.fromEntries(Object.entries(entry).filter(([k]) => !keys.includes(k.replace(/\?$/, ''))));
|
|
13557
|
+
/**
|
|
13558
|
+
* The part a plan path is the answer of (`contentTypes[1].seed.title` → structure, `pages[0].sections[2]` → pages). A
|
|
13559
|
+
* path no part writes — the root (the composer), or a key the generation decides — is the LAST part's: it is written
|
|
13560
|
+
* knowing everything before it.
|
|
13561
|
+
*/
|
|
13562
|
+
function planPartOfPath(path) {
|
|
13563
|
+
const top = path.split(/[.[]/, 1)[0];
|
|
13564
|
+
return PLAN_PARTS.find(part => PLAN_PART_KEYS[part].includes(top)) ?? PLAN_PARTS[PLAN_PARTS.length - 1];
|
|
13565
|
+
}
|
|
13566
|
+
/** Plan keys neither carried by a wire key nor derived — always empty (a spec and the builder hold it). */
|
|
13567
|
+
function planWireGaps() {
|
|
13568
|
+
const carried = new Set(Object.values(WIRE_KEYS).flatMap(keys => Object.values(keys).flatMap(spec => spec.refs)));
|
|
13569
|
+
const gaps = [];
|
|
13570
|
+
for (const [entry, shape] of Object.entries(PLAN_SHAPE)) {
|
|
13571
|
+
if (PLAN_WIRE_DERIVED[entry])
|
|
13572
|
+
continue;
|
|
13573
|
+
for (const raw of Object.keys(shape)) {
|
|
13574
|
+
const ref = `${entry}.${raw.replace(/\?$/, '')}`;
|
|
13575
|
+
if (!carried.has(ref) && !PLAN_WIRE_DERIVED[ref])
|
|
13576
|
+
gaps.push(ref);
|
|
13577
|
+
}
|
|
13578
|
+
}
|
|
13579
|
+
return gaps;
|
|
13580
|
+
}
|
|
13581
|
+
class PlanWireShapeError extends Error {
|
|
13582
|
+
}
|
|
13583
|
+
// ── The format ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
13584
|
+
const unique = (values) => [...new Set(values)];
|
|
13585
|
+
const NONE = '';
|
|
13586
|
+
const orNone = (values) => jsonOneOf([NONE, ...values]);
|
|
13587
|
+
const closedOrText = (values) => (values.length ? jsonOneOf(values) : jsonText());
|
|
13588
|
+
const shaped = (entry, properties) => jsonShapedObject(PLAN_WIRE_SHAPE[entry], properties, `wire ${entry}`);
|
|
13589
|
+
/** The patterns a page section may use (every non-global one), in library order. */
|
|
13590
|
+
function sectionPatternIds() {
|
|
13591
|
+
return Object.entries(SITE_PATTERNS).filter(([, p]) => !p.global).map(([id]) => id);
|
|
13592
|
+
}
|
|
13593
|
+
const sectionPatterns = () => sectionPatternIds().map(id => SITE_PATTERNS[id]);
|
|
13594
|
+
const globalIds = (position) => Object.entries(SITE_PATTERNS).filter(([, p]) => p.global === position).map(([id]) => id);
|
|
13595
|
+
function wireFieldJsonSchema() {
|
|
13596
|
+
return shaped(PlanWireEntry.Field, { name: jsonText(), type: jsonText(), controller: jsonText(), required: jsonBoolean(), note: jsonText() });
|
|
13597
|
+
}
|
|
13598
|
+
/** The keys an entry leaves out under `options`. */
|
|
13599
|
+
const CHILD_KEYS = { contentType: 'fields', page: 'sections' };
|
|
13600
|
+
const childless = (entry, options) => (options.children === false && CHILD_KEYS[entry] ? [CHILD_KEYS[entry]] : []);
|
|
13601
|
+
/** The shape an entry is written in, under `options`. */
|
|
13602
|
+
function wireEntryShape(entry, options = {}) {
|
|
13603
|
+
return shapeWithout(PLAN_WIRE_SHAPE[entry], childless(entry, options));
|
|
13604
|
+
}
|
|
13605
|
+
const shapedAs = (entry, options, properties) => {
|
|
13606
|
+
const left = childless(entry, options);
|
|
13607
|
+
return jsonShapedObject(wireEntryShape(entry, options), Object.fromEntries(Object.entries(properties).filter(([k]) => !left.includes(k))), `wire ${entry}`);
|
|
13608
|
+
};
|
|
13609
|
+
function wireContentTypeJsonSchema(options = {}) {
|
|
13610
|
+
const value = shaped(PlanWireEntry.SeedValue, { field: jsonText(), value: jsonAnyOf([jsonText(), jsonListOf(jsonText())]) });
|
|
13611
|
+
return shapedAs(PlanWireEntry.ContentType, options, {
|
|
13612
|
+
name: jsonText(),
|
|
13613
|
+
label: jsonText(),
|
|
13614
|
+
roles: jsonListOf(shaped(PlanWireEntry.RoleField, { role: jsonOneOf(ROLE_KEYS), field: jsonText() })),
|
|
13615
|
+
fields: jsonListOf(wireFieldJsonSchema()),
|
|
13616
|
+
seed: shaped(PlanWireEntry.Seed, { count: jsonInteger(), sample: jsonBoolean(), isPublished: jsonBoolean(), values: jsonListOf(value) }),
|
|
13617
|
+
});
|
|
13618
|
+
}
|
|
13619
|
+
function wireDataSourceJsonSchema() {
|
|
13620
|
+
return shaped(PlanWireEntry.DataSource, {
|
|
13621
|
+
alias: jsonText(),
|
|
13622
|
+
type: jsonOneOf(Object.values(PlanDataSourceType)),
|
|
13623
|
+
mode: orNone(Object.values(PlanDataSourceMode)),
|
|
13624
|
+
contentType: jsonText(),
|
|
13625
|
+
limit: jsonInteger(),
|
|
13626
|
+
filterField: jsonText(),
|
|
13627
|
+
routeParam: jsonText(),
|
|
13628
|
+
of: jsonText(),
|
|
13629
|
+
op: orNone(Object.values(RelativeOp)),
|
|
13630
|
+
within: jsonText(),
|
|
13631
|
+
});
|
|
13632
|
+
}
|
|
13633
|
+
function wireSectionJsonSchema() {
|
|
13634
|
+
const patterns = sectionPatterns();
|
|
13635
|
+
return shaped(PlanWireEntry.Section, {
|
|
13636
|
+
pattern: jsonOneOf(sectionPatternIds()),
|
|
13637
|
+
slots: jsonListOf(shaped(PlanWireEntry.SlotText, { slot: closedOrText(unique(patterns.flatMap(patternSlots))), text: jsonText() })),
|
|
13638
|
+
links: jsonListOf(shaped(PlanWireEntry.SlotLink, { slot: closedOrText(unique(patterns.flatMap(patternLinkSlots))), route: jsonText() })),
|
|
13639
|
+
binding: jsonText(),
|
|
13640
|
+
without: jsonListOf(closedOrText(unique(patterns.flatMap(patternOptionalParts)))),
|
|
13641
|
+
form: orNone(FORM_PRESETS.map(p => p.id)),
|
|
13642
|
+
enter: orNone(ANIMATE_IDS),
|
|
13643
|
+
});
|
|
13644
|
+
}
|
|
13645
|
+
function wirePageJsonSchema(options = {}) {
|
|
13646
|
+
return shapedAs(PlanWireEntry.Page, options, {
|
|
13647
|
+
key: jsonText(),
|
|
13648
|
+
name: jsonText(),
|
|
13649
|
+
route: jsonText(),
|
|
13650
|
+
inNav: jsonBoolean(),
|
|
13651
|
+
archetype: orNone(Object.keys(PAGE_ARCHETYPES)),
|
|
13652
|
+
need: orNone(SITE_NEED_IDS),
|
|
13653
|
+
template: jsonText(),
|
|
13654
|
+
routeSource: jsonText(),
|
|
13655
|
+
seo: shaped(PlanWireEntry.PageSeo, { title: jsonText(), description: jsonText() }),
|
|
13656
|
+
sections: jsonListOf(wireSectionJsonSchema()),
|
|
13657
|
+
});
|
|
13658
|
+
}
|
|
13659
|
+
const PLAN_TOP_SCHEMAS = {
|
|
13660
|
+
business: () => shaped(PlanWireEntry.Business, { name: jsonText(), tagline: jsonText() }),
|
|
13661
|
+
contentTypes: () => jsonListOf(wireContentTypeJsonSchema()),
|
|
13662
|
+
dataSources: () => jsonListOf(wireDataSourceJsonSchema()),
|
|
13663
|
+
pages: () => jsonListOf(wirePageJsonSchema()),
|
|
13664
|
+
globals: () => shaped(PlanWireEntry.Globals, {
|
|
13665
|
+
[GlobalSlot.Header]: jsonOneOf(globalIds(PatternGlobalPosition.Before)),
|
|
13666
|
+
[GlobalSlot.Footer]: jsonOneOf(globalIds(PatternGlobalPosition.After)),
|
|
13667
|
+
}),
|
|
13668
|
+
};
|
|
13669
|
+
/** The plan's top-level shape for one part (its keys only) — what that part's request shows and is built from. */
|
|
13670
|
+
function planWirePartShape(part) {
|
|
13671
|
+
return Object.fromEntries(Object.entries(PLAN_WIRE_SHAPE.plan).filter(([k]) => PLAN_PART_KEYS[part].includes(k)));
|
|
13672
|
+
}
|
|
13673
|
+
/**
|
|
13674
|
+
* The format a model writes one part of a plan in — one request each (vectoriox/iox-cms#47). The same for every studio
|
|
13675
|
+
* and run: nothing in it is pinned, so the provider compiles each part's grammar once.
|
|
13676
|
+
*/
|
|
13677
|
+
function planWirePartJsonSchema(part) {
|
|
13678
|
+
const gaps = planWireGaps();
|
|
13679
|
+
if (gaps.length)
|
|
13680
|
+
throw new PlanWireShapeError(`the plan wire neither carries nor derives ${gaps.join(', ')}`);
|
|
13681
|
+
const missing = Object.keys(WIRE_KEYS.plan).filter(k => !PLAN_TOP_SCHEMAS[k] || !WIRE_KEYS.plan[k].part);
|
|
13682
|
+
if (missing.length)
|
|
13683
|
+
throw new PlanWireShapeError(`the plan wire's ${missing.join(', ')} has no part or no format`);
|
|
13684
|
+
return jsonShapedObject(planWirePartShape(part), Object.fromEntries(PLAN_PART_KEYS[part].map(k => [k, PLAN_TOP_SCHEMAS[k]()])), `wire plan ${part}`);
|
|
13685
|
+
}
|
|
13686
|
+
/** A value standing for "absent": `''` (or nothing at all). */
|
|
13687
|
+
const isNone = (v) => v === undefined || v === NONE;
|
|
13688
|
+
const optional = (name, v, none = isNone) => (none(v) ? {} : { [name]: v });
|
|
13689
|
+
const emptyList = (v) => v === undefined || (Array.isArray(v) && v.length === 0);
|
|
13690
|
+
/** `[{ <keyName>, <valueName> }]` → `{ key: value }`; anything else is left for the gate to refuse. */
|
|
13691
|
+
function listToMap(list, keyName, valueName) {
|
|
13692
|
+
if (!Array.isArray(list))
|
|
13693
|
+
return list;
|
|
13694
|
+
const out = {};
|
|
13695
|
+
for (const entry of list) {
|
|
13696
|
+
if (!isPlainObject(entry) || typeof entry[keyName] !== 'string')
|
|
13697
|
+
return list;
|
|
13698
|
+
out[entry[keyName]] = entry[valueName];
|
|
13699
|
+
}
|
|
13700
|
+
return out;
|
|
13701
|
+
}
|
|
13702
|
+
const mapToList = (map, keyName, valueName) => Object.entries(map ?? {}).map(([k, v]) => ({ [keyName]: k, [valueName]: v }));
|
|
13703
|
+
const each = (list, expand) => (Array.isArray(list) ? list.map(expand) : list);
|
|
13704
|
+
function expandWireField(wire) {
|
|
13705
|
+
if (!isPlainObject(wire))
|
|
13706
|
+
return wire;
|
|
13707
|
+
const { required, note, ...rest } = wire;
|
|
13708
|
+
return { ...rest, ...optional('required', required, v => v === undefined || v === false), ...optional('note', note) };
|
|
13709
|
+
}
|
|
13710
|
+
function expandWireContentType(wire, options = {}) {
|
|
13711
|
+
if (!isPlainObject(wire))
|
|
13712
|
+
return wire;
|
|
13713
|
+
const { roles, fields, seed, ...rest } = options.children === false ? { fields: [], ...wire } : wire;
|
|
13714
|
+
const out = { ...rest, roles: listToMap(roles, 'role', 'field'), fields: each(fields, expandWireField) };
|
|
13715
|
+
if (!isPlainObject(seed))
|
|
13716
|
+
return seed === undefined ? out : { ...out, seed };
|
|
13717
|
+
const { values, ...fixed } = seed;
|
|
13718
|
+
if (fixed['count'] === 0 && emptyList(values))
|
|
13719
|
+
return out;
|
|
13720
|
+
const map = listToMap(values ?? [], 'field', 'value');
|
|
13721
|
+
return { ...out, seed: isPlainObject(map) ? { ...fixed, ...map } : { ...fixed, values } };
|
|
13722
|
+
}
|
|
13723
|
+
function expandWireDataSource(wire) {
|
|
13724
|
+
if (!isPlainObject(wire))
|
|
13725
|
+
return wire;
|
|
13726
|
+
const { alias, type, mode, contentType, limit, filterField, routeParam, of, op, within, ...rest } = wire;
|
|
13727
|
+
if (type === PlanDataSourceType.Cms) {
|
|
13728
|
+
const filter = isNone(filterField) && isNone(routeParam) ? {} : { filterByRouteParam: { field: filterField, routeParam } };
|
|
13729
|
+
return { ...rest, alias, type, mode, contentType, ...optional('limit', limit, v => v === undefined || v === 0), ...filter };
|
|
13730
|
+
}
|
|
13731
|
+
if (type === PlanDataSourceType.Relative)
|
|
13732
|
+
return { ...rest, alias, type, of, op, within };
|
|
13733
|
+
return wire;
|
|
13734
|
+
}
|
|
13735
|
+
function expandWireSection(wire) {
|
|
13736
|
+
if (!isPlainObject(wire))
|
|
13737
|
+
return wire;
|
|
13738
|
+
const { slots, links, binding, without, form, enter, ...rest } = wire;
|
|
13739
|
+
return {
|
|
13740
|
+
...rest,
|
|
13741
|
+
...optional('slots', listToMap(slots, 'slot', 'text'), v => emptyList(slots) || v === undefined),
|
|
13742
|
+
...optional('links', listToMap(links, 'slot', 'route'), v => emptyList(links) || v === undefined),
|
|
13743
|
+
...optional('binding', binding),
|
|
13744
|
+
...optional('without', without, emptyList),
|
|
13745
|
+
...optional('form', form),
|
|
13746
|
+
...optional('enter', enter),
|
|
13747
|
+
};
|
|
13748
|
+
}
|
|
13749
|
+
function expandWirePage(wire, options = {}) {
|
|
13750
|
+
if (!isPlainObject(wire))
|
|
13751
|
+
return wire;
|
|
13752
|
+
const { archetype, need, template, routeSource, seo, sections, ...rest } = options.children === false ? { sections: [], ...wire } : wire;
|
|
13753
|
+
const words = isPlainObject(seo) ? { ...optional('title', seo['title']), ...optional('description', seo['description']) } : seo;
|
|
13754
|
+
return {
|
|
13755
|
+
...rest,
|
|
13756
|
+
...(wire['route'] === '/' ? { isHome: true } : {}),
|
|
13757
|
+
...optional('archetype', archetype),
|
|
13758
|
+
...optional('need', need),
|
|
13759
|
+
...optional('template', template),
|
|
13760
|
+
...optional('routeSource', routeSource),
|
|
13761
|
+
...optional('seo', words, v => v === undefined || (isPlainObject(v) && Object.keys(v).length === 0)),
|
|
13762
|
+
sections: each(sections, expandWireSection),
|
|
13763
|
+
};
|
|
13764
|
+
}
|
|
13765
|
+
function expandWireBusiness(wire, facts) {
|
|
13766
|
+
if (!isPlainObject(wire))
|
|
13767
|
+
return wire;
|
|
13768
|
+
const { tagline, ...rest } = wire;
|
|
13769
|
+
return { ...rest, ...optional('tagline', tagline), ...(facts && Object.keys(facts).length ? { facts: { ...facts } } : {}) };
|
|
13770
|
+
}
|
|
13771
|
+
/**
|
|
13772
|
+
* A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
|
|
13773
|
+
* facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
|
|
13774
|
+
* `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
|
|
13775
|
+
* (the validator then names them, at their own paths).
|
|
13776
|
+
*/
|
|
13777
|
+
function expandPlanWire(parts, context) {
|
|
13778
|
+
const written = PLAN_PARTS.filter(part => parts[part] !== undefined).map(part => parts[part]);
|
|
13779
|
+
const odd = written.find(value => !isPlainObject(value));
|
|
13780
|
+
if (odd !== undefined)
|
|
13781
|
+
return odd;
|
|
13782
|
+
const wire = Object.assign({}, ...written);
|
|
13783
|
+
const { business, contentTypes, dataSources, pages, ...rest } = wire;
|
|
13784
|
+
return {
|
|
13785
|
+
...rest,
|
|
13786
|
+
version: SITE_PLAN_VERSION,
|
|
13787
|
+
source: { ...context.source },
|
|
13788
|
+
business: expandWireBusiness(business, context.facts),
|
|
13789
|
+
...(context.locale ? { locale: context.locale } : {}),
|
|
13790
|
+
design: { ...context.design },
|
|
13791
|
+
contentTypes: each(contentTypes, expandWireContentType),
|
|
13792
|
+
dataSources: each(dataSources, expandWireDataSource),
|
|
13793
|
+
pages: each(pages, expandWirePage),
|
|
13794
|
+
};
|
|
13795
|
+
}
|
|
13796
|
+
// ── The plan → the wire ────────────────────────────────────────────────────────────────────────────────────────────
|
|
13797
|
+
function fieldToWire(field) {
|
|
13798
|
+
return { name: field.name, type: field.type, controller: field.controller, required: field.required ?? false, note: field.note ?? NONE };
|
|
13799
|
+
}
|
|
13800
|
+
function contentTypeToWire(type, options = {}) {
|
|
13801
|
+
const played = type.roles;
|
|
13802
|
+
const roles = ROLE_KEYS.filter(role => played[role]).map(role => ({ role, field: played[role] }));
|
|
13803
|
+
const { count = 0, sample = false, isPublished = false, ...values } = type.seed ?? {};
|
|
13804
|
+
return {
|
|
13805
|
+
name: type.name, label: type.label, roles, ...(options.children === false ? {} : { fields: type.fields.map(fieldToWire) }),
|
|
13806
|
+
seed: { count, sample, isPublished, values: mapToList(values, 'field', 'value') },
|
|
13807
|
+
};
|
|
13808
|
+
}
|
|
13809
|
+
function dataSourceToWire(source) {
|
|
13810
|
+
const blank = { alias: source.alias, type: source.type, mode: NONE, contentType: NONE, limit: 0, filterField: NONE, routeParam: NONE, of: NONE, op: NONE, within: NONE };
|
|
13811
|
+
if (source.type === PlanDataSourceType.Relative)
|
|
13812
|
+
return { ...blank, of: source.of, op: source.op, within: source.within };
|
|
13813
|
+
return {
|
|
13814
|
+
...blank, mode: source.mode, contentType: source.contentType, limit: source.limit ?? 0,
|
|
13815
|
+
filterField: source.filterByRouteParam?.field ?? NONE, routeParam: source.filterByRouteParam?.routeParam ?? NONE,
|
|
13816
|
+
};
|
|
13817
|
+
}
|
|
13818
|
+
function sectionToWire(section) {
|
|
13819
|
+
return {
|
|
13820
|
+
pattern: section.pattern, slots: mapToList(section.slots, 'slot', 'text'), links: mapToList(section.links, 'slot', 'route'),
|
|
13821
|
+
binding: section.binding ?? NONE, without: [...(section.without ?? [])], form: section.form ?? NONE, enter: section.enter ?? NONE,
|
|
13822
|
+
};
|
|
13823
|
+
}
|
|
13824
|
+
function pageToWire(page, options = {}) {
|
|
13825
|
+
return {
|
|
13826
|
+
key: page.key, name: page.name, route: page.route, inNav: page.inNav ?? false, archetype: page.archetype ?? NONE, need: page.need ?? NONE,
|
|
13827
|
+
template: page.template ?? NONE, routeSource: page.routeSource ?? NONE,
|
|
13828
|
+
seo: { title: page.seo?.title ?? NONE, description: page.seo?.description ?? NONE },
|
|
13829
|
+
...(options.children === false ? {} : { sections: page.sections.map(sectionToWire) }),
|
|
13830
|
+
};
|
|
13831
|
+
}
|
|
13832
|
+
const PLAN_TO_WIRE = {
|
|
13833
|
+
business: plan => ({ name: plan.business.name, tagline: plan.business.tagline ?? NONE }),
|
|
13834
|
+
contentTypes: plan => plan.contentTypes.map(type => contentTypeToWire(type)),
|
|
13835
|
+
dataSources: plan => plan.dataSources.map(dataSourceToWire),
|
|
13836
|
+
pages: plan => plan.pages.map(page => pageToWire(page)),
|
|
13837
|
+
globals: plan => ({ ...plan.globals }),
|
|
13838
|
+
};
|
|
13839
|
+
/** One part of the plan on the wire: what that part's request would have answered. */
|
|
13840
|
+
function planPartToWire(plan, part) {
|
|
13841
|
+
return Object.fromEntries(PLAN_PART_KEYS[part].map(k => [k, PLAN_TO_WIRE[k](plan)]));
|
|
13842
|
+
}
|
|
13843
|
+
/** The plan → the wire, by part: what a model would have written (the change prompt shows it; fakes and evals write it). */
|
|
13844
|
+
function planToWire(plan) {
|
|
13845
|
+
return Object.fromEntries(PLAN_PARTS.map(part => [part, planPartToWire(plan, part)]));
|
|
13846
|
+
}
|
|
13847
|
+
|
|
13383
13848
|
/**
|
|
13384
13849
|
* The AI pipeline's WIRE vocabulary (AI plan §3c, §3n, M0): what iox-cms's `ai` API answers and what every client
|
|
13385
13850
|
* of it reads — the steps a model works on, who pays (IOX credits or the studio's own key), a model of the vetted
|
|
@@ -13432,11 +13897,38 @@ const AI_STEP_NEEDS = {
|
|
|
13432
13897
|
[AiStep.MakePictures]: [AiCapability.ImageOutput],
|
|
13433
13898
|
[AiStep.NameStructures]: [AiCapability.StructuredOutput],
|
|
13434
13899
|
};
|
|
13435
|
-
/**
|
|
13900
|
+
/**
|
|
13901
|
+
* The steps a model answers in the plan's parts, one request per part (vectoriox/iox-cms#47 — the whole plan's format
|
|
13902
|
+
* compiles to a grammar Claude refuses), and the tokens a typical request of each part reads and writes. Each part reads
|
|
13903
|
+
* the same instructions; the pages read the structure too and write most of the words.
|
|
13904
|
+
*/
|
|
13905
|
+
const AI_STEP_PART_TOKENS = {
|
|
13906
|
+
[AiStep.PlanSite]: {
|
|
13907
|
+
[PlanPart.Structure]: { input: 10_000, output: 1_500, cacheWrite: 0, cacheRead: 0 },
|
|
13908
|
+
[PlanPart.Pages]: { input: 12_000, output: 5_500, cacheWrite: 0, cacheRead: 0 },
|
|
13909
|
+
},
|
|
13910
|
+
[AiStep.ChangePlan]: {
|
|
13911
|
+
[PlanPart.Structure]: { input: 6_000, output: 300, cacheWrite: 0, cacheRead: 0 },
|
|
13912
|
+
[PlanPart.Pages]: { input: 6_500, output: 400, cacheWrite: 0, cacheRead: 0 },
|
|
13913
|
+
},
|
|
13914
|
+
};
|
|
13915
|
+
/** The parts a step is asked in, in order — none for a step asked in one request. */
|
|
13916
|
+
function aiStepParts(step) {
|
|
13917
|
+
return AI_STEP_PART_TOKENS[step] ? PLAN_PARTS : [];
|
|
13918
|
+
}
|
|
13919
|
+
const addTokens = (a, b) => ({
|
|
13920
|
+
input: a.input + b.input, output: a.output + b.output, cacheWrite: a.cacheWrite + b.cacheWrite, cacheRead: a.cacheRead + b.cacheRead,
|
|
13921
|
+
...(a.imageOutput !== undefined || b.imageOutput !== undefined ? { imageOutput: (a.imageOutput ?? 0) + (b.imageOutput ?? 0) } : {}),
|
|
13922
|
+
});
|
|
13923
|
+
const partsTotal = (step) => PLAN_PARTS.map(part => AI_STEP_PART_TOKENS[step][part]).reduce(addTokens);
|
|
13924
|
+
/**
|
|
13925
|
+
* Tokens a typical use of a step reads and writes (plan §3d "Where credits go") — what a shown estimate is made of. A
|
|
13926
|
+
* step asked in parts is every part's request together, so the estimate (and a generation's start gate) counts them all.
|
|
13927
|
+
*/
|
|
13436
13928
|
const AI_STEP_TYPICAL_TOKENS = {
|
|
13437
13929
|
[AiStep.ReadMessage]: { input: 8_800, output: 1_500, cacheWrite: 0, cacheRead: 0 },
|
|
13438
|
-
[AiStep.PlanSite]:
|
|
13439
|
-
[AiStep.ChangePlan]:
|
|
13930
|
+
[AiStep.PlanSite]: partsTotal(AiStep.PlanSite),
|
|
13931
|
+
[AiStep.ChangePlan]: partsTotal(AiStep.ChangePlan),
|
|
13440
13932
|
// a 1 K picture is 1,120 image tokens (Google's figure for Nano Banana 2) — I-D4
|
|
13441
13933
|
[AiStep.MakePictures]: { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, imageOutput: 1_120 },
|
|
13442
13934
|
[AiStep.NameStructures]: { input: 80_000, output: 10_000, cacheWrite: 0, cacheRead: 0 },
|
|
@@ -13459,6 +13951,10 @@ function costUsd(tokens, price) {
|
|
|
13459
13951
|
function creditsOfUsd(usd) {
|
|
13460
13952
|
return Math.round(usd * CREDITS_PER_USD * CREDIT_PRECISION) / CREDIT_PRECISION;
|
|
13461
13953
|
}
|
|
13954
|
+
/** What a typical request of `step` reads and writes — one part's when the step is asked in parts, else the whole step's. */
|
|
13955
|
+
function typicalStepTokens(step, part) {
|
|
13956
|
+
return (part && AI_STEP_PART_TOKENS[step]?.[part]) || AI_STEP_TYPICAL_TOKENS[step];
|
|
13957
|
+
}
|
|
13462
13958
|
/** The credits a typical use of `step` costs on a model of `price` — never typed, always computed (§3d). */
|
|
13463
13959
|
function estimateStepCredits(step, price, tokens = AI_STEP_TYPICAL_TOKENS[step]) {
|
|
13464
13960
|
return creditsOfUsd(costUsd(tokens, price));
|
|
@@ -13544,227 +14040,109 @@ var AiCallStatus;
|
|
|
13544
14040
|
})(AiCallStatus || (AiCallStatus = {}));
|
|
13545
14041
|
|
|
13546
14042
|
/**
|
|
13547
|
-
*
|
|
13548
|
-
*
|
|
13549
|
-
*
|
|
14043
|
+
* What a structured-output request costs the provider's grammar compiler, counted the way the Claude API counts it
|
|
14044
|
+
* (platform.claude.com/docs/en/build-with-claude/structured-outputs, "Schema complexity limits", read 2026-10-27):
|
|
14045
|
+
* "Total optional parameters across all strict tool schemas and JSON output schemas. Each parameter not listed in
|
|
14046
|
+
* `required` counts toward this limit" (24); "Total parameters that use `anyOf` or type arrays (for example,
|
|
14047
|
+
* `"type": ["string", "null"]`) across all strict schemas" (16); "Maximum number of tools with `strict: true`" (20).
|
|
14048
|
+
* The limits are per REQUEST — every strict schema in it together. The ONE counter: the specs hold every schema iox-cms
|
|
14049
|
+
* sends to it, and the fake Claude API refuses with it exactly as Claude does — the plan stage once sent 213 optional
|
|
14050
|
+
* parameters and every suite stayed green because the fake accepted anything (vectoriox/iox-cms#45).
|
|
13550
14051
|
*
|
|
13551
|
-
*
|
|
13552
|
-
*
|
|
13553
|
-
*
|
|
13554
|
-
*
|
|
13555
|
-
* source and the version are PINNED to the owner's pick and this run (one-value enums), so the provider cannot let
|
|
13556
|
-
* a model change the look. The gate still checks both (`plan.design` deep-equals the pick).
|
|
14052
|
+
* Beyond those, Claude refuses a grammar it compiles too large — a limit the docs name ("additional internal limits on
|
|
14053
|
+
* the compiled grammar size") without a number. MEASURED against the API on 2026-10-27 (vectoriox/iox-cms#47): the
|
|
14054
|
+
* total number of properties decides it, not enums or descriptions — 64 and 61 properties refused, 53 and fewer
|
|
14055
|
+
* accepted (53 already took ~9 s to compile). So 60 is the line we refuse above, and 40 is what we send.
|
|
13557
14056
|
*/
|
|
13558
|
-
const
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
const
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
13586
|
-
|
|
13587
|
-
|
|
13588
|
-
|
|
13589
|
-
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
if (source.kind === SitePlanSourceKind.Ai)
|
|
13594
|
-
return jsonShapedObject(SHAPE['sourceAi'], { kind: one$1(source.kind), runId: one$1(source.runId) }, 'sourceAi');
|
|
13595
|
-
return jsonShapedObject(SHAPE['sourceTemplate'], {
|
|
13596
|
-
kind: one$1(source.kind), templateId: one$1(source.templateId), templateVersion: { type: 'integer', enum: [source.templateVersion] },
|
|
13597
|
-
}, 'sourceTemplate');
|
|
13598
|
-
}
|
|
13599
|
-
function businessSchema() {
|
|
13600
|
-
const facts = jsonClosedObject(Object.fromEntries(FACT_KEYS.map(key => [key, jsonText()])), []);
|
|
13601
|
-
return jsonShapedObject(SHAPE['business'], { name: jsonText(), tagline: jsonText(), facts }, 'business');
|
|
13602
|
-
}
|
|
13603
|
-
/** A content type's field. */
|
|
13604
|
-
function fieldJsonSchema() {
|
|
13605
|
-
return jsonShapedObject(SHAPE['field'], { name: jsonText(), type: jsonText(), controller: jsonText(), required: jsonBoolean(), note: jsonText() }, 'field');
|
|
13606
|
-
}
|
|
13607
|
-
/** A content type, its seed's field values as a list (`values: [{ field, value }]`). */
|
|
13608
|
-
function contentTypeJsonSchema() {
|
|
13609
|
-
const value = jsonAnyOf([jsonText(), jsonListOf(jsonText())]);
|
|
13610
|
-
const values = jsonListOf(jsonClosedObject({ [SitePlanWireKey.SeedField]: jsonText(), [SitePlanWireKey.SeedValue]: value }, [SitePlanWireKey.SeedField, SitePlanWireKey.SeedValue]));
|
|
13611
|
-
const seed = jsonShapedObject(SHAPE['seed'], { count: jsonInteger(), sample: jsonBoolean(), isPublished: jsonBoolean() }, 'seed', { key: SitePlanWireKey.SeedValues, schema: values });
|
|
13612
|
-
const roleKeys = Object.keys(SHAPE['roles']).map(k => k.replace(/\?$/, ''));
|
|
13613
|
-
const roles = jsonShapedObject(SHAPE['roles'], Object.fromEntries(roleKeys.map(k => [k, jsonText()])), 'roles');
|
|
13614
|
-
return jsonShapedObject(SHAPE['contentType'], {
|
|
13615
|
-
name: jsonText(), label: jsonText(), roles, fields: jsonListOf(fieldJsonSchema()), seed,
|
|
13616
|
-
}, 'contentType');
|
|
13617
|
-
}
|
|
13618
|
-
/** A data source: a cms list / single item, or a relative one. */
|
|
13619
|
-
function dataSourceJsonSchema() {
|
|
13620
|
-
const byRoute = jsonClosedObject({ field: jsonText(), routeParam: jsonText() }, ['field', 'routeParam']);
|
|
13621
|
-
const cms = jsonShapedObject(SHAPE['cmsDataSource'], {
|
|
13622
|
-
alias: jsonText(), type: one$1(PlanDataSourceType.Cms), mode: jsonOneOf(Object.values(PlanDataSourceMode)),
|
|
13623
|
-
contentType: jsonText(), limit: jsonInteger(), filterByRouteParam: byRoute,
|
|
13624
|
-
}, 'cmsDataSource');
|
|
13625
|
-
const relative = jsonShapedObject(SHAPE['relativeDataSource'], {
|
|
13626
|
-
alias: jsonText(), type: one$1(PlanDataSourceType.Relative), of: jsonText(), op: jsonOneOf(Object.values(RelativeOp)), within: jsonText(),
|
|
13627
|
-
}, 'relativeDataSource');
|
|
13628
|
-
return jsonAnyOf([cms, relative]);
|
|
13629
|
-
}
|
|
13630
|
-
/** The patterns a page section may use (every non-global one), in library order. */
|
|
13631
|
-
function sectionPatternIds() {
|
|
13632
|
-
return Object.entries(SITE_PATTERNS).filter(([, p]) => !p.global).map(([id]) => id);
|
|
13633
|
-
}
|
|
13634
|
-
/** A key a pattern has nothing for: only `{}` (or leaving it out) — the decoder drops it. */
|
|
13635
|
-
const NOTHING = () => jsonClosedObject({}, []);
|
|
13636
|
-
/** One section branch: this pattern's id, its declared slots (a list), the slots it may re-point (`links`, closed per
|
|
13637
|
-
* pattern), its optional parts, a binding when it takes one, a form preset when it holds a form, its own entrance. */
|
|
13638
|
-
function sectionBranch(id, pattern) {
|
|
13639
|
-
const slots = patternSlots(pattern);
|
|
13640
|
-
const parts = patternOptionalParts(pattern);
|
|
13641
|
-
const linkSlots = patternLinkSlots(pattern);
|
|
13642
|
-
const slotList = slots.length
|
|
13643
|
-
? jsonListOf(jsonClosedObject({ [SitePlanWireKey.SlotName]: jsonOneOf(slots), [SitePlanWireKey.SlotText]: jsonText() }, [SitePlanWireKey.SlotName, SitePlanWireKey.SlotText]))
|
|
13644
|
-
: NOTHING();
|
|
13645
|
-
const branch = jsonShapedObject(SHAPE['section'], {
|
|
13646
|
-
pattern: one$1(id),
|
|
13647
|
-
slots: slotList,
|
|
13648
|
-
links: linkSlots.length ? jsonClosedObject(Object.fromEntries(linkSlots.map(slot => [slot, jsonText()])), []) : NOTHING(),
|
|
13649
|
-
binding: patternTakesBinding(pattern) ? jsonText() : NOTHING(),
|
|
13650
|
-
without: parts.length ? jsonListOf(jsonOneOf(parts)) : NOTHING(),
|
|
13651
|
-
form: patternHasForm(pattern) ? jsonOneOf(FORM_PRESETS.map(p => p.id)) : NOTHING(),
|
|
13652
|
-
enter: jsonOneOf(ANIMATE_IDS),
|
|
13653
|
-
}, 'section');
|
|
13654
|
-
return { ...branch, description: `${id}: ${pattern.intent}` };
|
|
13655
|
-
}
|
|
13656
|
-
/** A page section: one branch per pattern. */
|
|
13657
|
-
function sectionJsonSchema() {
|
|
13658
|
-
return jsonAnyOf(sectionPatternIds().map(id => sectionBranch(id, SITE_PATTERNS[id])));
|
|
13659
|
-
}
|
|
13660
|
-
/** A page. */
|
|
13661
|
-
function pageJsonSchema() {
|
|
13662
|
-
const seo = jsonShapedObject(SHAPE['pageSeo'], { title: jsonText(), description: jsonText() }, 'pageSeo');
|
|
13663
|
-
return jsonShapedObject(SHAPE['page'], {
|
|
13664
|
-
key: jsonText(), name: jsonText(), route: jsonText(), isHome: jsonBoolean(), required: jsonBoolean(), inNav: jsonBoolean(),
|
|
13665
|
-
archetype: jsonOneOf(Object.keys(PAGE_ARCHETYPES)), need: jsonOneOf(SITE_NEED_IDS), template: jsonText(), routeSource: jsonText(), seo, sections: jsonListOf(sectionJsonSchema()),
|
|
13666
|
-
}, 'page');
|
|
13667
|
-
}
|
|
13668
|
-
const globalIds = (position) => Object.entries(SITE_PATTERNS).filter(([, p]) => p.global === position).map(([id]) => id);
|
|
13669
|
-
/** The Site Plan format, pinned to the owner's pick and this run. */
|
|
13670
|
-
function sitePlanJsonSchema(pin) {
|
|
13671
|
-
const globals = jsonShapedObject(SHAPE['globals'], {
|
|
13672
|
-
[GlobalSlot.Header]: jsonOneOf(globalIds(PatternGlobalPosition.Before)),
|
|
13673
|
-
[GlobalSlot.Footer]: jsonOneOf(globalIds(PatternGlobalPosition.After)),
|
|
13674
|
-
}, 'globals');
|
|
13675
|
-
return jsonShapedObject(SHAPE['plan'], {
|
|
13676
|
-
version: one$1(SITE_PLAN_VERSION),
|
|
13677
|
-
source: sourceSchema(pin.source),
|
|
13678
|
-
business: businessSchema(),
|
|
13679
|
-
locale: jsonOneOf(Object.keys(SITE_LOCALES)),
|
|
13680
|
-
design: pinnedDesign(pin.design),
|
|
13681
|
-
contentTypes: jsonListOf(contentTypeJsonSchema()),
|
|
13682
|
-
dataSources: jsonListOf(dataSourceJsonSchema()),
|
|
13683
|
-
pages: jsonListOf(pageJsonSchema()),
|
|
13684
|
-
globals,
|
|
13685
|
-
}, 'plan');
|
|
13686
|
-
}
|
|
13687
|
-
// ── The wire → the plan ─────────────────────────────────────────────────────────────────────────────
|
|
13688
|
-
const isEmptyObject = (v) => isPlainObject(v) && Object.keys(v).length === 0;
|
|
13689
|
-
/** `[{ slot, text }]` → `{ slot: text }`; anything that is not that list is left for the gate to refuse. */
|
|
13690
|
-
function slotsFromWire(slots) {
|
|
13691
|
-
if (!Array.isArray(slots))
|
|
13692
|
-
return slots;
|
|
13693
|
-
const out = {};
|
|
13694
|
-
for (const entry of slots) {
|
|
13695
|
-
if (!isPlainObject(entry) || typeof entry[SitePlanWireKey.SlotName] !== 'string')
|
|
13696
|
-
return slots;
|
|
13697
|
-
out[entry[SitePlanWireKey.SlotName]] = entry[SitePlanWireKey.SlotText];
|
|
14057
|
+
const STRICT_SCHEMA_LIMITS = {
|
|
14058
|
+
strictTools: 20,
|
|
14059
|
+
optional: 24,
|
|
14060
|
+
unions: 16,
|
|
14061
|
+
/** MEASURED, not documented (2026-10-27): the smallest refused request held 61 properties, the largest accepted 53. */
|
|
14062
|
+
properties: 60,
|
|
14063
|
+
};
|
|
14064
|
+
/**
|
|
14065
|
+
* What one request iox-cms sends may use: half of each documented limit, so a step can gain a field (or a strict tool)
|
|
14066
|
+
* without a 400 — and 40 properties, a third under the measured grammar line, where compiling stays a few seconds.
|
|
14067
|
+
*/
|
|
14068
|
+
const STRICT_SCHEMA_BUDGET = { optional: 12, unions: 8, properties: 40 };
|
|
14069
|
+
/** The keywords the docs list as not supported ("Numerical constraints", "String constraints", "Array constraints beyond `minItems` of 0 or 1"). */
|
|
14070
|
+
const UNSUPPORTED_KEYWORDS = [
|
|
14071
|
+
'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength', 'maxItems', 'uniqueItems', 'contains',
|
|
14072
|
+
'minContains', 'maxContains', 'prefixItems', 'patternProperties', 'propertyNames', 'dependentSchemas', 'if', 'then', 'else', 'not',
|
|
14073
|
+
];
|
|
14074
|
+
const NESTED_LISTS = ['anyOf', 'allOf', 'oneOf'];
|
|
14075
|
+
const NESTED_MAPS = ['properties', '$defs', 'definitions'];
|
|
14076
|
+
const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
14077
|
+
function count(node, path, out) {
|
|
14078
|
+
if (!isObject(node))
|
|
14079
|
+
return;
|
|
14080
|
+
const at = (key) => (path ? `${path}.${key}` : key);
|
|
14081
|
+
if (Array.isArray(node['anyOf']) || Array.isArray(node['type']))
|
|
14082
|
+
out.unions++;
|
|
14083
|
+
const properties = isObject(node['properties']) ? node['properties'] : null;
|
|
14084
|
+
if (node['type'] === 'object' || properties) {
|
|
14085
|
+
const required = new Set(Array.isArray(node['required']) ? node['required'] : []);
|
|
14086
|
+
out.optional += Object.keys(properties ?? {}).filter(key => !required.has(key)).length;
|
|
14087
|
+
out.properties += Object.keys(properties ?? {}).length;
|
|
14088
|
+
if (properties)
|
|
14089
|
+
out.objects++;
|
|
14090
|
+
if (node['additionalProperties'] !== false)
|
|
14091
|
+
out.unsupported.push(`${at('additionalProperties')}: must be false`);
|
|
13698
14092
|
}
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
}
|
|
13713
|
-
/** A content type off the wire: its seed's `values` list back to field keys. */
|
|
13714
|
-
function contentTypeFromWire(type) {
|
|
13715
|
-
if (!isPlainObject(type) || !isPlainObject(type['seed']))
|
|
13716
|
-
return type;
|
|
13717
|
-
const { [SitePlanWireKey.SeedValues]: values, ...seed } = type['seed'];
|
|
13718
|
-
if (Array.isArray(values)) {
|
|
13719
|
-
for (const entry of values) {
|
|
13720
|
-
if (isPlainObject(entry) && typeof entry[SitePlanWireKey.SeedField] === 'string')
|
|
13721
|
-
seed[entry[SitePlanWireKey.SeedField]] = entry[SitePlanWireKey.SeedValue];
|
|
13722
|
-
}
|
|
14093
|
+
for (const key of UNSUPPORTED_KEYWORDS)
|
|
14094
|
+
if (key in node)
|
|
14095
|
+
out.unsupported.push(`${at(key)}: not supported`);
|
|
14096
|
+
if ('minItems' in node && node['minItems'] !== 0 && node['minItems'] !== 1)
|
|
14097
|
+
out.unsupported.push(`${at('minItems')}: only 0 or 1`);
|
|
14098
|
+
if (Array.isArray(node['enum']) && node['enum'].some(v => v !== null && typeof v === 'object'))
|
|
14099
|
+
out.unsupported.push(`${at('enum')}: complex types`);
|
|
14100
|
+
if (typeof node['$ref'] === 'string' && !node['$ref'].startsWith('#'))
|
|
14101
|
+
out.unsupported.push(`${at('$ref')}: external`);
|
|
14102
|
+
for (const key of NESTED_MAPS) {
|
|
14103
|
+
if (isObject(node[key]))
|
|
14104
|
+
for (const [name, child] of Object.entries(node[key]))
|
|
14105
|
+
count(child, at(`${key}.${name}`), out);
|
|
13723
14106
|
}
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
}
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
}
|
|
13733
|
-
/** The
|
|
13734
|
-
function
|
|
13735
|
-
|
|
13736
|
-
|
|
13737
|
-
return
|
|
14107
|
+
for (const key of NESTED_LISTS) {
|
|
14108
|
+
if (Array.isArray(node[key]))
|
|
14109
|
+
node[key].forEach((child, i) => count(child, at(`${key}[${i}]`), out));
|
|
14110
|
+
}
|
|
14111
|
+
if (isObject(node['items']))
|
|
14112
|
+
count(node['items'], at('items'), out);
|
|
14113
|
+
if (isObject(node['additionalProperties']))
|
|
14114
|
+
count(node['additionalProperties'], at('additionalProperties'), out);
|
|
14115
|
+
}
|
|
14116
|
+
/** The cost of ONE request's strict schemas together: its JSON output format and its strict tools' input schemas. */
|
|
14117
|
+
function strictSchemaComplexity(schemas, strictTools = 0) {
|
|
14118
|
+
const out = { optional: 0, unions: 0, strictTools, properties: 0, objects: 0, unsupported: [] };
|
|
14119
|
+
schemas.forEach((schema, i) => count(schema, schemas.length > 1 ? `[${i}]` : '', out));
|
|
14120
|
+
return out;
|
|
13738
14121
|
}
|
|
14122
|
+
/** Claude's words for a grammar it compiles too large (2026-10-27, the plan stage — vectoriox/iox-cms#47), verbatim. */
|
|
14123
|
+
const GRAMMAR_TOO_LARGE_MESSAGE = 'The compiled grammar is too large, which would cause performance issues. Simplify your tool schemas or reduce the number of strict tools.';
|
|
13739
14124
|
/**
|
|
13740
|
-
*
|
|
13741
|
-
*
|
|
14125
|
+
* The 400 the Claude API answers a request over its limits with, or null. The optional-parameter and the grammar-size
|
|
14126
|
+
* messages are the ones Claude answered on 2026-10-27, verbatim; the union, strict-tool and unsupported messages follow
|
|
14127
|
+
* their shape (not yet captured). The documented limits are checked first: a request over one of them never reaches the
|
|
14128
|
+
* grammar compiler.
|
|
13742
14129
|
*/
|
|
13743
|
-
function
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
if (
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13751
|
-
|
|
13752
|
-
|
|
13753
|
-
|
|
13754
|
-
}
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
return
|
|
13759
|
-
...plan,
|
|
13760
|
-
contentTypes: plan.contentTypes.map(t => {
|
|
13761
|
-
if (!t.seed)
|
|
13762
|
-
return t;
|
|
13763
|
-
const { count, sample, isPublished, ...values } = t.seed;
|
|
13764
|
-
return { ...t, seed: { count, sample, isPublished, [SitePlanWireKey.SeedValues]: Object.entries(values).map(([field, value]) => ({ [SitePlanWireKey.SeedField]: field, [SitePlanWireKey.SeedValue]: value })) } };
|
|
13765
|
-
}),
|
|
13766
|
-
pages: plan.pages.map(p => ({ ...p, sections: p.sections.map(s => (s.slots ? { ...s, slots: slotsToWire(s.slots) } : s)) })),
|
|
13767
|
-
};
|
|
14130
|
+
function strictSchemaRefusal(complexity) {
|
|
14131
|
+
const refuse = (message) => ({ status: 400, type: 'invalid_request_error', message });
|
|
14132
|
+
if (complexity.unsupported.length)
|
|
14133
|
+
return refuse(`Invalid schema: ${complexity.unsupported.join('; ')}`);
|
|
14134
|
+
if (complexity.strictTools > STRICT_SCHEMA_LIMITS.strictTools) {
|
|
14135
|
+
return refuse(`Too many strict tools (${complexity.strictTools}). Reduce the number of tools with strict: true (limit: ${STRICT_SCHEMA_LIMITS.strictTools}).`);
|
|
14136
|
+
}
|
|
14137
|
+
if (complexity.optional > STRICT_SCHEMA_LIMITS.optional) {
|
|
14138
|
+
return refuse(`Schemas contains too many optional parameters (${complexity.optional}), which would make grammar compilation inefficient. Reduce the number of optional parameters in your tool schemas (limit: ${STRICT_SCHEMA_LIMITS.optional}).`);
|
|
14139
|
+
}
|
|
14140
|
+
if (complexity.unions > STRICT_SCHEMA_LIMITS.unions) {
|
|
14141
|
+
return refuse(`Schemas contains too many parameters with union types (${complexity.unions}), which would make grammar compilation inefficient. Reduce the number of parameters with union types in your tool schemas (limit: ${STRICT_SCHEMA_LIMITS.unions}).`);
|
|
14142
|
+
}
|
|
14143
|
+
if (complexity.properties > STRICT_SCHEMA_LIMITS.properties)
|
|
14144
|
+
return refuse(GRAMMAR_TOO_LARGE_MESSAGE);
|
|
14145
|
+
return null;
|
|
13768
14146
|
}
|
|
13769
14147
|
|
|
13770
14148
|
/**
|
|
@@ -13807,9 +14185,32 @@ const PLAN_EDIT_SHAPE = {
|
|
|
13807
14185
|
[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)' },
|
|
13808
14186
|
[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" },
|
|
13809
14187
|
[PlanEditOp.AddContentType]: { op: "'addContentType'", contentType: 'contentType — a unique name', 'dataSources?': 'dataSource[] — the list / single sources its pages read' },
|
|
13810
|
-
[PlanEditOp.AddField]: { op: "'addField'", contentType: 'string — a content type of the plan', field: 'field', 'sample?': "string | string[] — the samples' value for it" },
|
|
14188
|
+
[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 ''" },
|
|
13811
14189
|
[PlanEditOp.SetSectionData]: { op: "'setSectionData'", page: 'string — a page key', index: 'integer — the section', binding: 'string — the list source the section repeats' },
|
|
13812
14190
|
};
|
|
14191
|
+
/**
|
|
14192
|
+
* The plan part each op changes (vectoriox/iox-cms#47): a sentence is answered in the plan's two parts, one request each —
|
|
14193
|
+
* the structure's edits first, then the pages' written knowing them.
|
|
14194
|
+
*/
|
|
14195
|
+
const PLAN_EDIT_OP_PART = {
|
|
14196
|
+
[PlanEditOp.AddPage]: PlanPart.Pages,
|
|
14197
|
+
[PlanEditOp.RemovePage]: PlanPart.Pages,
|
|
14198
|
+
[PlanEditOp.RenamePage]: PlanPart.Pages,
|
|
14199
|
+
[PlanEditOp.MergePages]: PlanPart.Pages,
|
|
14200
|
+
[PlanEditOp.MovePage]: PlanPart.Pages,
|
|
14201
|
+
[PlanEditOp.AddSection]: PlanPart.Pages,
|
|
14202
|
+
[PlanEditOp.RemoveSection]: PlanPart.Pages,
|
|
14203
|
+
[PlanEditOp.MoveSection]: PlanPart.Pages,
|
|
14204
|
+
[PlanEditOp.SwapPattern]: PlanPart.Pages,
|
|
14205
|
+
[PlanEditOp.SetSlot]: PlanPart.Pages,
|
|
14206
|
+
[PlanEditOp.SetSectionData]: PlanPart.Pages,
|
|
14207
|
+
[PlanEditOp.AddContentType]: PlanPart.Structure,
|
|
14208
|
+
[PlanEditOp.AddField]: PlanPart.Structure,
|
|
14209
|
+
};
|
|
14210
|
+
/** The ops written in one part, in `PlanEditOp` order. */
|
|
14211
|
+
function planEditOps(part) {
|
|
14212
|
+
return Object.values(PlanEditOp).filter(op => PLAN_EDIT_OP_PART[op] === part);
|
|
14213
|
+
}
|
|
13813
14214
|
var PlanChangeKind;
|
|
13814
14215
|
(function (PlanChangeKind) {
|
|
13815
14216
|
PlanChangeKind["Page"] = "page";
|
|
@@ -13930,10 +14331,12 @@ function findSection(d, pageAt, index, c, path) {
|
|
|
13930
14331
|
return ok;
|
|
13931
14332
|
}
|
|
13932
14333
|
const setSections = (d, at, sections) => { d.pages[at] = { ...d.pages[at], sections }; };
|
|
14334
|
+
/** A child added to a page or type the same edits added is part of that addition — the map marks the parent only. */
|
|
14335
|
+
const addedHere = (changed, kind, at) => changed.some(ch => ch.kind === kind && ch.action === PlanChangeAction.Added && (kind === PlanChangeKind.Page ? ch.page === at.page : ch.name === at.name));
|
|
13933
14336
|
/** One handler per op — a data table, never an if-chain. */
|
|
13934
14337
|
const APPLY = {
|
|
13935
14338
|
[PlanEditOp.AddPage]: (d, e, c, path, changed) => {
|
|
13936
|
-
const page =
|
|
14339
|
+
const page = e.page;
|
|
13937
14340
|
if (pageIndex(d, page.key) >= 0)
|
|
13938
14341
|
return c.add(PlanEditIssueCode.Duplicate, `${path}.page.key`, `page "${page.key}" is already in the plan`);
|
|
13939
14342
|
const after = e.after === undefined ? d.pages.length - 1 : findPage(d, e.after, c, `${path}.after`);
|
|
@@ -13992,9 +14395,10 @@ const APPLY = {
|
|
|
13992
14395
|
return;
|
|
13993
14396
|
const sections = [...d.pages[at].sections];
|
|
13994
14397
|
const index = e.at === undefined ? sections.length : clamp(e.at, sections.length);
|
|
13995
|
-
sections.splice(index, 0,
|
|
14398
|
+
sections.splice(index, 0, e.section);
|
|
13996
14399
|
setSections(d, at, sections);
|
|
13997
|
-
changed
|
|
14400
|
+
if (!addedHere(changed, PlanChangeKind.Page, { page: d.pages[at].key }))
|
|
14401
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: d.pages[at].key, index });
|
|
13998
14402
|
},
|
|
13999
14403
|
[PlanEditOp.RemoveSection]: (d, e, c, path, changed) => {
|
|
14000
14404
|
const at = findPage(d, e.page, c, `${path}.page`);
|
|
@@ -14045,7 +14449,7 @@ const APPLY = {
|
|
|
14045
14449
|
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
|
|
14046
14450
|
},
|
|
14047
14451
|
[PlanEditOp.AddContentType]: (d, e, c, path, changed) => {
|
|
14048
|
-
const type =
|
|
14452
|
+
const type = e.contentType;
|
|
14049
14453
|
if (d.contentTypes.some(t => t.name === type.name))
|
|
14050
14454
|
return c.add(PlanEditIssueCode.Duplicate, `${path}.contentType.name`, `content type "${type.name}" is already in the plan`);
|
|
14051
14455
|
d.contentTypes.push(type);
|
|
@@ -14066,9 +14470,10 @@ const APPLY = {
|
|
|
14066
14470
|
const type = d.contentTypes[at];
|
|
14067
14471
|
if (type.fields.some(f => f.name === e.field.name))
|
|
14068
14472
|
return c.add(PlanEditIssueCode.Duplicate, `${path}.field.name`, `"${type.name}" already has a field "${e.field.name}"`);
|
|
14069
|
-
const seed = type.seed ? { ...type.seed, [e.field.name]: e.sample ?? '' } : undefined;
|
|
14473
|
+
const seed = type.seed ? { ...type.seed, [e.field.name]: e.sample ?? type.seed[e.field.name] ?? '' } : undefined;
|
|
14070
14474
|
d.contentTypes[at] = { ...type, fields: [...type.fields, e.field], ...(seed ? { seed } : {}) };
|
|
14071
|
-
changed
|
|
14475
|
+
if (!addedHere(changed, PlanChangeKind.ContentType, { name: type.name }))
|
|
14476
|
+
changed.push({ kind: PlanChangeKind.Field, action: PlanChangeAction.Added, name: `${type.name}.${e.field.name}` });
|
|
14072
14477
|
},
|
|
14073
14478
|
[PlanEditOp.SetSectionData]: (d, e, c, path, changed) => {
|
|
14074
14479
|
const at = findPage(d, e.page, c, `${path}.page`);
|
|
@@ -14107,66 +14512,211 @@ function applyPlanEdits(plan, edits) {
|
|
|
14107
14512
|
return { plan: { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources }, changed, issues: [] };
|
|
14108
14513
|
}
|
|
14109
14514
|
// ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
|
|
14110
|
-
|
|
14515
|
+
/** Each part's answer format name. */
|
|
14516
|
+
const PLAN_EDITS_PART_SCHEMA_NAMES = {
|
|
14517
|
+
[PlanPart.Structure]: 'plan_structure_edits',
|
|
14518
|
+
[PlanPart.Pages]: 'plan_page_edits',
|
|
14519
|
+
};
|
|
14111
14520
|
/** A sentence's answer: the edits, or why the ops cannot say it (one line, in the owner's language). */
|
|
14112
14521
|
var PlanEditsOutcome;
|
|
14113
14522
|
(function (PlanEditsOutcome) {
|
|
14114
14523
|
PlanEditsOutcome["Edits"] = "edits";
|
|
14115
14524
|
PlanEditsOutcome["Cannot"] = "cannot";
|
|
14116
14525
|
})(PlanEditsOutcome || (PlanEditsOutcome = {}));
|
|
14117
|
-
/**
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14526
|
+
/**
|
|
14527
|
+
* Each part's answer, explained. The structure is asked first, so it says whether the sentence is a plan change at all
|
|
14528
|
+
* (`outcome`); the pages are asked only when it is, and write only their edits.
|
|
14529
|
+
*/
|
|
14530
|
+
const PLAN_EDITS_ANSWER_SHAPES = {
|
|
14531
|
+
[PlanPart.Structure]: {
|
|
14532
|
+
outcome: "'edits' | 'cannot' — cannot: no operation of either part (below) can do what the sentence asks",
|
|
14533
|
+
edits: 'edit[] — the content types and fields the sentence adds, in order; empty when it adds none, or cannot',
|
|
14534
|
+
reason: "string — when cannot, ONE short line for the owner in their language (what to do instead); else ''",
|
|
14535
|
+
},
|
|
14536
|
+
[PlanPart.Pages]: {
|
|
14537
|
+
edits: 'edit[] — the page and section edits, in order, made after the structure edits (already applied); empty when none',
|
|
14538
|
+
},
|
|
14122
14539
|
};
|
|
14123
|
-
const
|
|
14124
|
-
/**
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14540
|
+
const NO_INDEX = -1;
|
|
14541
|
+
/** A new page or content type on the wire: its children are edits of their own. */
|
|
14542
|
+
const CHILDLESS = { children: false };
|
|
14543
|
+
/** A list holding exactly one object: none → absent, one → it (expanded), more → the list (the gate refuses it). */
|
|
14544
|
+
const oneOf = (schema, expand, toWire, what) => ({
|
|
14545
|
+
text: `${what}[] — exactly one when the op takes it`,
|
|
14546
|
+
none: [],
|
|
14547
|
+
schema: () => jsonListOf(schema()),
|
|
14548
|
+
expand: v => (Array.isArray(v) ? (v.length === 0 ? undefined : v.length === 1 ? expand(v[0]) : v) : v),
|
|
14549
|
+
toWire: v => [toWire(v)],
|
|
14550
|
+
});
|
|
14551
|
+
const textKey = (text) => ({ text: `string — ${text}`, none: '', schema: jsonText });
|
|
14552
|
+
const indexKey = (text) => ({ text: `integer — ${text}`, none: NO_INDEX, schema: jsonInteger });
|
|
14553
|
+
const EDIT_WIRE = {
|
|
14554
|
+
page: textKey('a page key'),
|
|
14555
|
+
name: textKey('renamePage: its new name (the menu label)'),
|
|
14556
|
+
route: textKey('renamePage: its new route'),
|
|
14557
|
+
after: textKey('addPage: the key of the page it follows in the menu'),
|
|
14558
|
+
into: textKey('mergePages: the page that stays'),
|
|
14559
|
+
from: textKey('mergePages: the page whose sections move into it, then removed'),
|
|
14560
|
+
index: indexKey('the section, 0-based on its page (addSection: the place the new one goes)'),
|
|
14561
|
+
to: indexKey('movePage / moveSection: the new 0-based place'),
|
|
14562
|
+
pattern: { text: 'string — swapPattern: the section pattern it becomes', none: '', schema: () => jsonOneOf(['', ...sectionPatternIds()]) },
|
|
14563
|
+
slot: textKey("setSlot: one of the pattern's slots"),
|
|
14564
|
+
text: textKey("setSlot: the words, in the owner's language and voice"),
|
|
14565
|
+
binding: textKey('setSectionData: the list source the section repeats'),
|
|
14566
|
+
contentType: textKey('addField: a content type of the plan'),
|
|
14567
|
+
newPage: {
|
|
14568
|
+
...oneOf(() => wirePageJsonSchema(CHILDLESS), v => expandWirePage(v, CHILDLESS), p => pageToWire(p, CHILDLESS), 'page'),
|
|
14569
|
+
text: 'page[] — exactly one when the op takes it, WITHOUT sections: each is an addSection after it',
|
|
14570
|
+
},
|
|
14571
|
+
newSection: oneOf(wireSectionJsonSchema, expandWireSection, sectionToWire, 'section'),
|
|
14572
|
+
newContentType: {
|
|
14573
|
+
...oneOf(() => wireContentTypeJsonSchema(CHILDLESS), v => expandWireContentType(v, CHILDLESS), t => contentTypeToWire(t, CHILDLESS), 'contentType'),
|
|
14574
|
+
text: "contentType[] — exactly one when the op takes it, WITHOUT fields: each is an addField after it (the samples' values stay in its seed)",
|
|
14575
|
+
},
|
|
14576
|
+
dataSources: {
|
|
14577
|
+
text: 'dataSource[] — addContentType: the list / single sources its pages read', none: [],
|
|
14578
|
+
schema: () => jsonListOf(wireDataSourceJsonSchema()),
|
|
14579
|
+
expand: v => (Array.isArray(v) ? v.map(expandWireDataSource) : v),
|
|
14580
|
+
toWire: (v) => v.map(dataSourceToWire),
|
|
14581
|
+
},
|
|
14582
|
+
newField: oneOf(wireFieldJsonSchema, expandWireField, fieldToWire, 'field'),
|
|
14583
|
+
sample: { text: "string | string[] — addField: the samples' value for it", none: '', schema: () => jsonAnyOf([jsonText(), jsonListOf(jsonText())]) },
|
|
14584
|
+
};
|
|
14585
|
+
/** Each op's keys (`PLAN_EDIT_SHAPE`) → the wire key that carries it. */
|
|
14586
|
+
const PLAN_EDIT_WIRE_KEYS = {
|
|
14587
|
+
[PlanEditOp.AddPage]: { page: 'newPage', after: 'after' },
|
|
14588
|
+
[PlanEditOp.RemovePage]: { page: 'page' },
|
|
14589
|
+
[PlanEditOp.RenamePage]: { page: 'page', name: 'name', route: 'route' },
|
|
14590
|
+
[PlanEditOp.MergePages]: { into: 'into', from: 'from' },
|
|
14591
|
+
[PlanEditOp.MovePage]: { page: 'page', to: 'to' },
|
|
14592
|
+
[PlanEditOp.AddSection]: { page: 'page', section: 'newSection', at: 'index' },
|
|
14593
|
+
[PlanEditOp.RemoveSection]: { page: 'page', index: 'index' },
|
|
14594
|
+
[PlanEditOp.MoveSection]: { page: 'page', from: 'index', to: 'to' },
|
|
14595
|
+
[PlanEditOp.SwapPattern]: { page: 'page', index: 'index', pattern: 'pattern' },
|
|
14596
|
+
[PlanEditOp.SetSlot]: { page: 'page', index: 'index', slot: 'slot', text: 'text' },
|
|
14597
|
+
[PlanEditOp.AddContentType]: { contentType: 'newContentType', dataSources: 'dataSources' },
|
|
14598
|
+
[PlanEditOp.AddField]: { contentType: 'contentType', field: 'newField', sample: 'sample' },
|
|
14599
|
+
[PlanEditOp.SetSectionData]: { page: 'page', index: 'index', binding: 'binding' },
|
|
14600
|
+
};
|
|
14601
|
+
/** The wire keys a part's edit carries: every key its ops read, in `EDIT_WIRE` order. */
|
|
14602
|
+
function planEditWireKeys(part) {
|
|
14603
|
+
const read = new Set(planEditOps(part).flatMap(op => Object.values(PLAN_EDIT_WIRE_KEYS[op])));
|
|
14604
|
+
return Object.keys(EDIT_WIRE).filter(k => read.has(k));
|
|
14605
|
+
}
|
|
14606
|
+
const isEditWireNone = (wireKey, v) => {
|
|
14607
|
+
const none = EDIT_WIRE[wireKey].none;
|
|
14608
|
+
return v === undefined || (Array.isArray(none) ? Array.isArray(v) && v.length === 0 : v === none) || (wireKey === 'sample' && Array.isArray(v) && v.length === 0);
|
|
14609
|
+
};
|
|
14610
|
+
const noneText = (none) => (Array.isArray(none) ? '[]' : typeof none === 'string' ? "''" : String(none));
|
|
14611
|
+
const opsText = (part) => planEditOps(part).map(op => `'${op}'`).join(' | ');
|
|
14612
|
+
/** A part's flat edit, as a shape table: every key required, each with its none. */
|
|
14613
|
+
function planEditWireShape(part) {
|
|
14614
|
+
return {
|
|
14615
|
+
op: `${opsText(part)} — which keys below are read`,
|
|
14616
|
+
...Object.fromEntries(planEditWireKeys(part).map(k => [k, `${EDIT_WIRE[k].text} — ${noneText(EDIT_WIRE[k].none)} when the op does not read it`])),
|
|
14141
14617
|
};
|
|
14142
|
-
return jsonShapedObject(entry, Object.fromEntries(shapeKeys(entry).map(key => [key, valueOf(key)])), op);
|
|
14143
14618
|
}
|
|
14144
|
-
/**
|
|
14145
|
-
function
|
|
14146
|
-
return
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14619
|
+
/** Per op of a part, the wire keys it reads, in `PLAN_EDIT_SHAPE`'s words (`?` = may hold its none) — what the model reads. */
|
|
14620
|
+
function planEditWireOps(part) {
|
|
14621
|
+
return Object.fromEntries(planEditOps(part).map(op => {
|
|
14622
|
+
const entry = PLAN_EDIT_SHAPE[op];
|
|
14623
|
+
const keys = PLAN_EDIT_WIRE_KEYS[op];
|
|
14624
|
+
return [op, Object.fromEntries(Object.entries(keys).map(([planKey, wireKey]) => {
|
|
14625
|
+
const optionalKey = `${planKey}?` in entry;
|
|
14626
|
+
return [`${wireKey}${optionalKey ? '?' : ''}`, entry[optionalKey ? `${planKey}?` : planKey]];
|
|
14627
|
+
}))];
|
|
14628
|
+
}));
|
|
14629
|
+
}
|
|
14630
|
+
/** Op keys no wire key carries, or wire keys that do not exist — always empty (a spec and the builder hold it). */
|
|
14631
|
+
function planEditWireGaps() {
|
|
14632
|
+
const gaps = [];
|
|
14633
|
+
for (const op of OPS) {
|
|
14634
|
+
const keys = PLAN_EDIT_WIRE_KEYS[op] ?? {};
|
|
14635
|
+
for (const planKey of shapeKeys(PLAN_EDIT_SHAPE[op])) {
|
|
14636
|
+
if (planKey === 'op')
|
|
14637
|
+
continue;
|
|
14638
|
+
if (!keys[planKey])
|
|
14639
|
+
gaps.push(`${op}.${planKey}`);
|
|
14640
|
+
else if (!EDIT_WIRE[keys[planKey]])
|
|
14641
|
+
gaps.push(`${op}.${planKey} → ${keys[planKey]}`);
|
|
14642
|
+
}
|
|
14643
|
+
for (const planKey of Object.keys(keys))
|
|
14644
|
+
if (!shapeKeys(PLAN_EDIT_SHAPE[op]).includes(planKey))
|
|
14645
|
+
gaps.push(`${op}.${planKey} (not an op key)`);
|
|
14646
|
+
}
|
|
14647
|
+
return gaps;
|
|
14648
|
+
}
|
|
14649
|
+
const ANSWER_SCHEMAS = {
|
|
14650
|
+
outcome: () => jsonOneOf(Object.values(PlanEditsOutcome)),
|
|
14651
|
+
reason: () => jsonText(),
|
|
14652
|
+
edits: part => jsonListOf(jsonShapedObject(planEditWireShape(part), {
|
|
14653
|
+
op: jsonOneOf(planEditOps(part)),
|
|
14654
|
+
...Object.fromEntries(planEditWireKeys(part).map(k => [k, EDIT_WIRE[k].schema()])),
|
|
14655
|
+
}, `edit ${part}`)),
|
|
14656
|
+
};
|
|
14657
|
+
/** The format one part of a sentence's answer is written in — one request each (vectoriox/iox-cms#47). */
|
|
14658
|
+
function planEditsJsonSchema(part) {
|
|
14659
|
+
const gaps = planEditWireGaps();
|
|
14660
|
+
if (gaps.length)
|
|
14661
|
+
throw new PlanWireShapeError(`the plan edits wire does not carry ${gaps.join(', ')}`);
|
|
14662
|
+
const shape = PLAN_EDITS_ANSWER_SHAPES[part];
|
|
14663
|
+
return jsonShapedObject(shape, Object.fromEntries(shapeKeys(shape).map(k => [k, ANSWER_SCHEMAS[k](part)])), `planEdits ${part}`);
|
|
14664
|
+
}
|
|
14665
|
+
/** One flat edit → the op's edit: the keys it reads, a none left out where the op may go without it. Never repaired. */
|
|
14666
|
+
function expandWireEdit(wire) {
|
|
14667
|
+
if (!isPlainObject(wire))
|
|
14668
|
+
return wire;
|
|
14669
|
+
const keys = PLAN_EDIT_WIRE_KEYS[wire['op']];
|
|
14670
|
+
if (!keys)
|
|
14671
|
+
return wire;
|
|
14672
|
+
const entry = PLAN_EDIT_SHAPE[wire['op']];
|
|
14673
|
+
const out = Object.fromEntries(Object.entries(wire).filter(([k]) => k === 'op' || !(k in EDIT_WIRE)));
|
|
14674
|
+
for (const [planKey, wireKey] of Object.entries(keys)) {
|
|
14675
|
+
const raw = wire[wireKey];
|
|
14676
|
+
if (`${planKey}?` in entry && isEditWireNone(wireKey, raw))
|
|
14677
|
+
continue;
|
|
14678
|
+
const value = EDIT_WIRE[wireKey].expand ? EDIT_WIRE[wireKey].expand(raw) : raw;
|
|
14679
|
+
if (value !== undefined)
|
|
14680
|
+
out[planKey] = value;
|
|
14681
|
+
}
|
|
14682
|
+
return out;
|
|
14683
|
+
}
|
|
14684
|
+
/** An edit's own keys → its flat wire edit, in its part's keys. */
|
|
14685
|
+
function flatEdit(edit) {
|
|
14686
|
+
const part = PLAN_EDIT_OP_PART[edit.op];
|
|
14687
|
+
const out = { op: edit.op, ...Object.fromEntries(planEditWireKeys(part).map(k => [k, Array.isArray(EDIT_WIRE[k].none) ? [] : EDIT_WIRE[k].none])) };
|
|
14688
|
+
for (const [planKey, wireKey] of Object.entries(PLAN_EDIT_WIRE_KEYS[edit.op])) {
|
|
14689
|
+
const value = edit[planKey];
|
|
14690
|
+
if (value === undefined)
|
|
14691
|
+
continue;
|
|
14692
|
+
out[wireKey] = EDIT_WIRE[wireKey].toWire ? EDIT_WIRE[wireKey].toWire(value) : value;
|
|
14693
|
+
}
|
|
14694
|
+
return out;
|
|
14151
14695
|
}
|
|
14152
|
-
/**
|
|
14696
|
+
/** The children an added page or content type travels without, as the edits that add them after it. */
|
|
14697
|
+
function childEdits(edit) {
|
|
14698
|
+
if (edit.op === PlanEditOp.AddPage)
|
|
14699
|
+
return edit.page.sections.map(section => ({ op: PlanEditOp.AddSection, page: edit.page.key, section }));
|
|
14700
|
+
if (edit.op === PlanEditOp.AddContentType)
|
|
14701
|
+
return edit.contentType.fields.map(field => ({ op: PlanEditOp.AddField, contentType: edit.contentType.name, field }));
|
|
14702
|
+
return [];
|
|
14703
|
+
}
|
|
14704
|
+
/** An edit → the flat wire edits a model would write for it: itself, then its children (fakes and specs write them). */
|
|
14705
|
+
function planEditToWire(edit) {
|
|
14706
|
+
return [flatEdit(edit), ...childEdits(edit).map(flatEdit)];
|
|
14707
|
+
}
|
|
14708
|
+
/** Edits → each part's flat wire edits, in order (what each part's answer would hold). */
|
|
14709
|
+
function planEditsToWire(edits) {
|
|
14710
|
+
const out = Object.fromEntries(PLAN_PARTS.map(part => [part, []]));
|
|
14711
|
+
for (const edit of edits)
|
|
14712
|
+
out[PLAN_EDIT_OP_PART[edit.op]].push(...planEditToWire(edit));
|
|
14713
|
+
return out;
|
|
14714
|
+
}
|
|
14715
|
+
/** A model's answer (either part's) → edits (each flat edit expanded); never repaired. */
|
|
14153
14716
|
function planEditsFromWire(answer) {
|
|
14154
14717
|
if (!isPlainObject(answer) || !Array.isArray(answer['edits']))
|
|
14155
14718
|
return answer;
|
|
14156
|
-
|
|
14157
|
-
if (!isPlainObject(raw))
|
|
14158
|
-
return raw;
|
|
14159
|
-
const e = { ...raw };
|
|
14160
|
-
if (isPlainObject(e['page']) && Array.isArray(e['page']['sections'])) {
|
|
14161
|
-
e['page'] = { ...e['page'], sections: e['page']['sections'].map(sectionFromWire) };
|
|
14162
|
-
}
|
|
14163
|
-
if (isPlainObject(e['section']))
|
|
14164
|
-
e['section'] = sectionFromWire(e['section']);
|
|
14165
|
-
if (isPlainObject(e['contentType']))
|
|
14166
|
-
e['contentType'] = contentTypeFromWire(e['contentType']);
|
|
14167
|
-
return e;
|
|
14168
|
-
});
|
|
14169
|
-
return { ...answer, edits };
|
|
14719
|
+
return { ...answer, edits: answer['edits'].map(expandWireEdit) };
|
|
14170
14720
|
}
|
|
14171
14721
|
|
|
14172
14722
|
/**
|
|
@@ -14745,5 +15295,5 @@ function addStockCredits(footer, credits) {
|
|
|
14745
15295
|
* Generated bundle index. Do not edit.
|
|
14746
15296
|
*/
|
|
14747
15297
|
|
|
14748
|
-
export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiFeature, AiGenerationStage, AiKeyMode, AiLimit, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CONTENT_ROLE_MEANINGS, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GlobalSlot, HEBREW_PREFIXES, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PAGE_ARCHETYPES,
|
|
15298
|
+
export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_PART_TOKENS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiFeature, AiGenerationStage, AiKeyMode, AiLimit, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CONTENT_ROLE_MEANINGS, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GRAMMAR_TOO_LARGE_MESSAGE, GlobalSlot, HEBREW_PREFIXES, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PAGE_ARCHETYPES, PLAN_EDITS_ANSWER_SHAPES, PLAN_EDITS_PART_SCHEMA_NAMES, PLAN_EDIT_OP_PART, PLAN_EDIT_SHAPE, PLAN_EDIT_WIRE_KEYS, PLAN_PARTS, PLAN_PART_KEYS, PLAN_WIRE_DERIVED, PLAN_WIRE_PART_SCHEMA_NAMES, PLAN_WIRE_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanPart, PlanWireEntry, PlanWireShapeError, PlanWordIssueCode, PreflightRefusalCode, PublishRefusalCode, REQUIRED_CONTENT_ROLES, RelativeOp, SAMPLE_IMAGE_REF, SITE_CHECKS, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_LIBRARY, SITE_LOCALES, SITE_NEED_IDS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_ARCHETYPES, SPEC_SITE_COPY_LENGTHS, SPEC_SITE_DENSITY_COPY, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_LOCALES, SPEC_SITE_NEED_IDS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_REQUIRED_ROLES, SPEC_SITE_ROLES, SPEC_SITE_TEMPLATES, STOCK_FAMILIES, STOCK_PROVIDER_TERMS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockProviderId, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockPhotos, archetypeIssues, assertStyleSlots, assertValidSitePlan, auditSite, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypePreviewFields, contentTypeToWire, copyLengthOf, costUsd, createSerializedNode, creditsOfUsd, dataSourceToWire, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, expandPlanWire, expandWireContentType, expandWireDataSource, expandWireEdit, expandWireField, expandWirePage, expandWireSection, failedBuildStep, fieldToWire, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, isRtlLocale, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, pageToWire, parsePatternPath, parseSampleImageRef, pathAt, patternById, patternHasForm, patternLinkSlots, patternManifest, patternNeedsBinding, patternNodes, patternOptionalParts, patternRoleNeeds, patternScopes, patternSlotBudgets, patternSlots, patternTakesBinding, phoneMenuPages, planEditOps, planEditToWire, planEditWireGaps, planEditWireKeys, planEditWireOps, planEditWireShape, planEditsFromWire, planEditsJsonSchema, planEditsToWire, planEntrances, planFromTemplate, planNavShape, planPartOfPath, planPartToWire, planStockPhotos, planToWire, planWireGaps, planWirePartJsonSchema, planWirePartShape, planWordIssues, publishRefusal, publishVerdict, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, roleField, routeMatches, routeParams, sampleImageRef, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockSlots, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, wordPattern, writtenTexts };
|
|
14749
15299
|
//# sourceMappingURL=vectoriox-iox-ui-generation-core.mjs.map
|